강의)로또-캐싱/정펙매/조합/불변일급컬렉션
로또번호로 보는 정적팩토리메서드, 캐싱, 상속, 불변일급컬렉션
📜 제목으로 보기
- 지금까지 클래스
- 정펙매로 Class를 객체(생성의) 능동적 관리자로 승격
- 정펙매를 통한 캐싱 -> [new처럼 매번 새로 생성]안해도 되는 객체 생성
- 01 프로덕션 원시값을 포장 -> [public 주생성자]만 빨간줄 자동생성
- 02 도메인 Test코드에서 생성자를 테스트하는데, [new 키워드의 public 주생성자]는 템플릿일 뿐이며, 항상 new사용과 동시에 100% 객체를 새로 생성(하여 할당) 할 수 밖에 없다.
- 03 정펙매로 객체 생성을 능동적으로 관리(100% 생성이 아니라 재사용 = 캐싱)
- 01 캐싱될 객체는 미리 생성자가 받는 raw파라미터가 제한된 범위로 예외처리 되어있다
- 02 new키워드가 아니라 정적팩메(Class.of(, ), Class.from())를 사용하고, 정펙매 내부에서 캐싱한다.
- 03 캐싱은 <외부에서 요청하며 메서드 파라미터로 들어오는 rawType, 현재 내부인 캐슁 객체Class>로 hashMap 제네릭 타입을 결정 + 사이즈를 알면 initialcapacity로 미리 준다. + 정펙매 내부에서 만드는 척만 해서 변수추출
- 04 캐싱객체는 private + static final의 싱글톤으로서 상수처럼 미리 빈 map을 초기화해놓는다.
- 05 캐싱의 핵심은 요청이 왔을 때, if없으면 -> 빈 맵에 생성해서 넣고 || 맵에서 그외 꺼내주기인데, hashMap.computeIfAbsent(확인할 key, 없으면key 해당 value자리에 넣어줄 값 생성 Function)을 제공해주므로 없으면 new로 새로 생성 로직만 넣어주면 된다.
- 06 정펙매 사용순간부터는 주생성자 private으로 잠궈주기
- 07 최종 필요한 것 정리
- Q. 생성자 테스트를 위해 기본생성자를 private말고 default로 열어두어도 되나?
- 04 캐싱 객체는 isSameAs로 비교하면 된다.
- 05 삭제도 안되는데 cache?
- 06 없을 때 생성 이 아니라 미리 만들고 -> 요청시 조회get만? -> static 블록 생기는게 짜증
- 참고) 블랙잭 카드 캐싱
- 정펙매를 통한 캐싱 -> [new처럼 매번 새로 생성]안해도 되는 객체 생성
- 생성자가 많으면, 응집도가 높고 견고한 클래스가 된다.
- 다른Type 추가 정펙매
- 다른Type 추가 생성자
- 여러 Type의 생성자 제공이라도 인스턴스 변수(상태값) 초기화는 주생성자 1개에서만 -> 나머지는 위에서 주생성자를 convert후 -> 기존 생성자를 this()로 호출만 해주는 부생성자들로 -> 중복을 피하자
- 01 다른Type의 생성자 추가는 부생성자로서 convert후 this()로 주생성자를 호출해야한다. -> 직접 주생성자의 역할인 인스턴스변수 초기화를 해주면 안된다
- 02 다른Type용 생성자 -> 부생성자 -> 주생성자 위에 정의 + convert후 this로 주생성자호출-> convert후 원래Type의 주생성자 코드(검증후 인변 초기화)를 반복하지마라.
- 정리) 생성자 코드 중복 in 부생성자 -> 인수 늘어날 때, 코드 변경될 때 -> 모든 부생성자까지 다 초기화 해줘야한다 -> 그러니 convert후 this() 주생성자를 호출해주자.
- 여러 Type의 생성자 제공이라도 인스턴스 변수(상태값) 초기화는 주생성자 1개에서만 -> 나머지는 위에서 주생성자를 convert후 -> 기존 생성자를 this()로 호출만 해주는 부생성자들로 -> 중복을 피하자
- 상속
- 상속 잘 사용 하기 (부작용이 크니까)
- 상속 부작용
- 상속 문제점 예제 만들어보기
- 기본적으로 제공하는 api들 내부작동방식을 모르므로 상속후 기능 추가를 위한 재정의시 조심해서 사용해야하므로 상속을 비추한다. -> 상속 대신 조합-> 포장도.. 조합?!
- 참고) super -> 자식이 생성자 재정의할라고 갖다쓰는 것은 주생성자 쓰는 것처럼 가능하다 -> but 메서드 호출까지 super를 호출한다면, 부모의 구조를 다알아야해서 결합도가 너무 커져 안좋다.
- 조합
- 추가 얘기거리 -> class 생성자 final or abstract를 붙이자
- 불변 일급컬렉션의 증감메서드
- VO처럼, 불변 일급컬렉션이라면 VO.증감() -> 새객체 응답처럼 컬렉션.add/remove호출()시 가변 가능한 new 새컬랙션<>()을 복사한 새 컬렉션를 응답해줘야한다.
- 01 컬렉션의 add/remove()메소드를 호출받는 일급컬렉션 객체로 그대로 응답되어야한다.
- 02 가변 컬렉션을 new 키워드로 복사해준다
- 03 복사한 컬렉션에 add/remove등 컬렉션 증감해준다
- 04 증감된 복사컬렉션으로 새 일급컬렉션을 만들어서 반환한다. -> 생성자 필요
- 05 대박) 재료(인자)에 의해 일급컬렉션에 [재료(컬렉션) 파라미터 생성자만 최초]로 생긴다면 그것을 주생성자로 -> 파라미터 없이 내부 빈컬랙션으로 초기화 with this()의 부생성자도 같이 만들어주자. -> 멋도 모르고 과거에 테코 등에서 쓴 기본생성자 = 주생성자this()를 이용한 빈 컬렉션으로 초기화 넣어주는 부생성자
- 05 원소 추가시마다 새 가변 컬렉션이 복사 후 add한 뒤 새 일급컬렉션 객체가 반환되므로 호출부에서는 원소추가/제거 메서드 호출후 다시 할당해야된다. -> 확인은 .size()를 추가 정의해줘서 확인하면 된다.
- (변화 전 기존 객체의)불변 일급컬렉션의 장점 정리
- VO처럼, 불변 일급컬렉션이라면 VO.증감() -> 새객체 응답처럼 컬렉션.add/remove호출()시 가변 가능한 new 새컬랙션<>()을 복사한 새 컬렉션를 응답해줘야한다.
지금까지 클래스
- 지금까지의 Class는 객체를 찍어내는 Factory일 뿐이었다.
- 조금 더 나아가서 객체 생성 Factory(찍어내는 템플릿)일 뿐만 아니라
객체의 능동적인 관리자의 역할도 할 수 있다.
제한적인 자바의 new 키워드
- java에서는
new키워드가 제한을 가지고 있다.- 무조건 응답해야하며
- 무조건 객체가 생성되어야한다.
정펙매로 Class를 객체(생성의) 능동적 관리자로 승격
정펙매를 통한 캐싱 -> [new처럼 매번 새로 생성]안해도 되는 객체 생성
01 프로덕션 원시값을 포장 -> [public 주생성자]만 빨간줄 자동생성
-
기본적으로 클래스내 원시값int를
인스턴스 변수=상태값으로 포장하여 선언하면 빨간줄로 생성자 만들라고 한다.

public class LottoNumber { //1. 내부 포장 상태값 -> 빨간줄 private final int value; //2. 빨간줄 -> add constructor parameter // -> private으로 생성하면 좋겠지만, public으로 기본 생성함 public LottoNumber(final int value) { this.value = value; } }
02 도메인 Test코드에서 생성자를 테스트하는데, [new 키워드의 public 주생성자]는 템플릿일 뿐이며, 항상 new사용과 동시에 100% 객체를 새로 생성(하여 할당) 할 수 밖에 없다.
-
LottoNumber
Test를 만들고 생성자 생성 연습시작-
go to test(직접지정ctrl+shift+t)로 생성하거나 이동하기!
-
-
new키워드를 사용해한다면객체를 생성하는 public 주생성자로서사용과 동시에 항상 객체를 100% 새로 생성해야만한다.class LottoNumberTest { @Test void create() { //new 키워드(public주생)는 템플릿으로서, 항상 사용과 동시에 객체를 100% 새로 생성해야만 한다. final LottoNumber lottoNumber = new LottoNumber(1); final LottoNumber lottoNumber2 = new LottoNumber(2); } }
03 정펙매로 객체 생성을 능동적으로 관리(100% 생성이 아니라 재사용 = 캐싱)
01 캐싱될 객체는 미리 생성자가 받는 raw파라미터가 제한된 범위로 예외처리 되어있다
public class LottoNumber {
private final int value;
public LottoNumber(final int value) {
// 생성자로 들어오는 재료인 raw파라미터에 대해
// 범위 검증(예외처리)를 해주면, 객체가 -> 제한된 raw값 범위만 가지게 된다.
// 제한된 raw값의 범위라면 미리 생성해두는 캐싱이 가능하다.
if (value < 1 || value > 45) {
throw new IllegalArgumentException();
}
this.value = value;
}
}
02 new키워드가 아니라 정적팩메(Class.of(, ), Class.from())를 사용하고, 정펙매 내부에서 캐싱한다.
-
객체 생성을
100% 새로 생성하는 new Class대신Class.of( , )나Class.from()으로 생성해보자.-
빨간줄로
public static정적 메서드 생성

-
파라미터명은 value로 수정해놓고 시작
public static LottoNumber of(final int value) { throw new UnsupportedOperationException("LottoNumber#of not write."); }
-
03 캐싱은 <외부에서 요청하며 메서드 파라미터로 들어오는 rawType, 현재 내부인 캐슁 객체Class>로 hashMap 제네릭 타입을 결정 + 사이즈를 알면 initialcapacity로 미리 준다. + 정펙매 내부에서 만드는 척만 해서 변수추출
-
변수 블럭에서 적상하면
변수 추출안되니,편의상 정펙매 내부에서 캐쉬 만드는 척하자
- 외부에서 들어오는 rawType과 동일한 Type으로 key타입을 맞춰줘야 꺼내쓸 수 있다.

04 캐싱객체는 private + static final의 싱글톤으로서 상수처럼 미리 빈 map을 초기화해놓는다.
-
변수추출로 변수Type이 자동생성되었다면,
private static final로 싱글톤 형태의 빈 map을 클래스 변수로 선언하게 한다.

public class LottoNumber { private static final int MIN = 1; private static final int MAX = 45; private static final Map<Integer, LottoNumber> cache = new HashMap<>(45); private final int value;
05 캐싱의 핵심은 요청이 왔을 때, if없으면 -> 빈 맵에 생성해서 넣고 || 맵에서 그외 꺼내주기인데, hashMap.computeIfAbsent(확인할 key, 없으면key 해당 value자리에 넣어줄 값 생성 Function)을 제공해주므로 없으면 new로 새로 생성 로직만 넣어주면 된다.

- 람다용
Function자리의 변수추천은 map의 keyType에 따라 바껴서 추천된다.- Map<
Integer, 객체>일 경우 ->integer -> {}추천
- Map<
Long, 객체>일 경우 ->aLong -> {}추천
- Map<
public static LottoNumber of(final int value) {
// 1. 정펙매로 부터 rawType변수로 요청이 온다.
// -> cache에서 없으면 map의 value에 넣어줄 값을 생성하는 람다식 --> 있으면 꺼내주기
// 2. 없을 때, new 생성자로 객체새로 생성하는데, [람다에 쓰이는 변수명은 파라미터와 다르게] 수정만 해주면 된다.
// -> i를 눌러주면 알아서 integer -> 로 람다 작성하라고 뜬다.
return cache.computeIfAbsent(value, integer -> new LottoNumber(integer));
}
06 정펙매 사용순간부터는 주생성자 private으로 잠궈주기

-
public으로 남게 되면, 의도했던 능동적 객체관리와 다르게 코드가 돌아갈 수 있다.
-
정펙매 사용으로 유도하고 기본생성자를 private로 막고 사용
-
테스트를 위해 잠시 여기서는 허용
-
07 최종 필요한 것 정리
-
private static final Map<
외부생성자로 들어올 rawType,현재객체Class> cache = new HashMap<> (갯수); -
정펙매 내부에서 cache.compuateIfAbsent(외부 key로 꺼낼 raw값,없으면 key에 해당하는 value로 넣어줄 객체생성해줄 new 생성자 & Function으로 객체 생성)public class LottoNumber { private static final int MIN = 1; private static final int MAX = 45; private static final Map<Integer, LottoNumber> cache = new HashMap<>(45);public static LottoNumber of(final int value) { return cache.computeIfAbsent(value, integer -> new LottoNumber(integer)); }
Q. 생성자 테스트를 위해 기본생성자를 private말고 default로 열어두어도 되나?
-
테스트를 위해 생성자를 열어둬야하는데,
클래스 분리를 고려한다.- 과하다 싶으면 -> default를 열어주는 case도 있다.
04 캐싱 객체는 isSameAs로 비교하면 된다.
-
메모리 주소까지 같은것만 True로 나오기 때문에 캐싱된 것인지 검사를isSameAs로 하면 된다.@Test void create() { final LottoNumber lottoNumber = new LottoNumber(1); final LottoNumber lottoNumber2 = new LottoNumber(1); // isSameAs: 메모리 주소가 같은지, 오버라이딩된 VO도 같다고 안나옴. 더 엄격한 완전히 같은 객체일 때, True assertThat(lottoNumber).isSameAs(lottoNumber2); // false }
05 삭제도 안되는데 cache?
01 변수명을 cache -> pool로 바꾸기
02 캐쉬 때문에 메모리가 가득찼을 때 GC대상으로 삭제시켜주는 hashMap인 -> WeakHashMap으로 선언

private static final Map<Integer, LottoNumber> cache = new WeakHashMap<>(45);
06 없을 때 생성 이 아니라 미리 만들고 -> 요청시 조회get만? -> static 블록 생기는게 짜증


public class LottoNumber {
private static final int MIN = 1;
private static final int MAX = 45;
private static final Map<Integer, LottoNumber> cache = new WeakHashMap<>(45);
// 1. 미리 생성을 위한 static 블럭
static {
for (int i = MIN; i <= MAX; i++) {
cache.put(i, new LottoNumber(i));
}
}
private final int value;
public LottoNumber(final int value) {
if (value < MIN || value > MAX) {
throw new IllegalArgumentException();
}
this.value = value;
}
public static LottoNumber of(final int value) {
//return cache.computeIfAbsent(value, integer -> new LottoNumber(integer));
// 2. 없으면 생성 과정이 없어지고 오로지 get으로 조회만
return cache.get(value);
}
}
- static으로 인해 코드량도 늘어나서 처음 분석하는데 힘이 들어감.
여러 쓰레드에서 접근한다면? -> ConcurrentHashMap + 미리 생성해두는게 좋다
참고) 블랙잭 카드 캐싱
- key 2개를 원소로 가지는 클래스를 두고 한다?

-
Map의 key를
String1개로 바꾸고 -> toString()으로 2개를 더해서 key1개로 사용해도 된다

-
enum이라면, .name() + ,name()로 스트링으로 더해서 key1개로 만들면 된다.

생성자가 많으면, 응집도가 높고 견고한 클래스가 된다.
다른Type 추가 정펙매

- 여러가지 방식으로 제공해주면
클라이언트 입장에서 사용하기 좋은 코드가 된다.
대박) 포장이 아닌 다른Type 입력 제공으로 인한 추가 정펙매 제공은 부생성자로서 위에 위치 + convert후 기존 정펙매(=기준 정펙매)를 호출한다.
01 포장으로 인한 메서드 추가의 상황이라면 -> [새기준 메서드 생성+내용복붙후 처리 완성]후 [기존 rawInput메서드는 포장후 새기준 호출]하도록 변경
02 다른Type입력을 위한 생성자/정펙매를 제공한다면 -> 기존 생성자/정펙매가 기준 메서드라 생각하고 내부convert후 호출
-
새 기준
포장하는 파라미터 변화가 아니라편의를 위해 다른 type의 입력을 제공한다고 치고 빨간줄 생성하자

-
포장이 아닐 경우, 다른Type메서드는 기준메서드가 아니다 ->
부 생성자로서 기존생성자보다 위에 + 포장해서 기존생성자=기준메서드로 호출해준다.
public class LottoNumber { private static final int MIN = 1; private static final int MAX = 45; private static final Map<Integer, LottoNumber> cache = new WeakHashMap<>(45); private final int value; public LottoNumber(final int value) { if (value < MIN || value > MAX) { throw new IllegalArgumentException(); } this.value = value; } //1. 다른Type입력 생성자 -> 부생성자 -> 위치는 [기존생성자=기준생성자]보다 위에 위치한다. public static LottoNumber of(final String value) { //2. convert해서 기존생성자=기준생성자를 호출해준다. return of(Integer.parseInt(value)); } public static LottoNumber of(final int value) { return cache.computeIfAbsent(value, integer -> new LottoNumber(integer)); } }
정리) 너무 많은 Type Or 방식의 생성자는 제공하면 문제가 된다. but 일반적으로 생성자는 많은 것이 좋다.
- 메서드와 다르게 많이 제공할 수록 좋단다.
다른Type 추가 생성자
여러 Type의 생성자 제공이라도 인스턴스 변수(상태값) 초기화는 주생성자 1개에서만 -> 나머지는 위에서 주생성자를 convert후 -> 기존 생성자를 this()로 호출만 해주는 부생성자들로 -> 중복을 피하자

- 자바만
부생성자라는 관례적이 이름이 있다.- 무조건 주생성자에서만 인변을 초기화 하기 위해 관례적으로 지은 이름
- 다른언어는 무조건 주 생성자를 이용해서만 호출하도록 지원하는 것이 많다.
01 다른Type의 생성자 추가는 부생성자로서 convert후 this()로 주생성자를 호출해야한다. -> 직접 주생성자의 역할인 인스턴스변수 초기화를 해주면 안된다
- 직접 주생성자의 역할인
인스턴스변수 초기화를 해주면 안된다.


02 다른Type용 생성자 -> 부생성자 -> 주생성자 위에 정의 + convert후 this로 주생성자호출-> convert후 원래Type의 주생성자 코드(검증후 인변 초기화)를 반복하지마라.
-
위치를 주생성자보다 위로 올려준다.


-
대부분
다른 Type 으로서 String을 받아주므로 rawInput에 해당하는rawValue로 네이밍 해보자.//부 : rawValue public LottoNumber(final String rawValue) { //주 : value public LottoNumber(final int value) { -
convert만 하고, 원래Type이 했던
검증+ 인변 초기화해주면 똑같이 해주면코드 중복이다.-
코드중복 = 나쁜 예

-
생성자 내부코드는 주생성자를 호출해서
인스턴스 변수 초기화의 중복 = 코드 중복을 막자.public LottoNumber(final String rawValue) { this(Integer.parseInt(rawValue)); }
-
정리) 생성자 코드 중복 in 부생성자 -> 인수 늘어날 때, 코드 변경될 때 -> 모든 부생성자까지 다 초기화 해줘야한다 -> 그러니 convert후 this() 주생성자를 호출해주자.
- 주 생성자는 객체 초기화 프로세스의 유일한 장소 -> 추가 생성자에서 초기화 하지말자
상속

상속 잘 사용 하기 (부작용이 크니까)
- 상속은 나쁜 것이다?
- 고민을 해봐야한다. 내가 쓰레기처럼 쓰는 것은 아닌지..
- 스프링에서도 상속을 잘 쓴다.
상속 부작용
- 부모 변화(탑레벨 클래스 기능 변경) -> 자식 모두 수정(하위 클래스 모두 변경)
- 안좋은 결합도가 높아짐.
- 나쁜 것도 물려준다. 선택이 없다.
부->자로 상속되서 사용되는 변수/메서드는 부모 내부에서 정의되어 사용되는 메서드라도 자식에 오버라이딩된 메서드부터 찾는다. -> 오버라이딩 전 메서드를 호출하는 방법이 없다?
- 동일한 형태, 구조를 가질 예정이므로
is-a에 해당하여상속을 했다.- cf)
has-a구조라면조합을 사용한다.
- cf)
- 합리적으로 보이지만, 자식이
물려받고 오버라이딩 안한 length()호출시부모 content()?자식오버라이딩한 content()어느것이 호출될까- 자식에서 호출했으면 오버라이딩 된 것이 호출된다.
- 사용하는 클라이언트는 구현안한 메서드라면, 내부 호출도 부모 것만 원할 수 도 있다.
- 따로 구현안하고
물려받은 메서드를 호출한다면,내부에서도 부모 것만 사용하도록 원할 수 도 있다.
- 따로 구현안하고
상속 문제점 예제 만들어보기
객체List 일급컬렉션( 포장 ) 요구사항이 없었다면? 컬렉션<객체Type>를 상속해서 컬렉션메서드의 기능을 다사용하도록 했다.
-
객체List는 일급컬렉션으로 포장했었다.
public class LottoNumbers { private final List<LottoNumber> value; } -
예전에는
extends 컬렉션<원소Type>을 통해 클래스를 생성했다.

-
사용과 생성은
c+s+t로 지정한go to test로 가서 생성하고 테스트하자.

문제01) 부모인 컬렉션 기능 다 사용되서 좋아보이지만, 위험한+필요없는 기능들도 강제로 제공된다.
-
.찍어보면 사용하지 필요없는 기능들도 다 물려받는다.
-
위험한 기능도 다 받은 상태라
클라이언트에도 노출되서 위험하다
문제02) 부모 물려받은 메서드에 오버라이딩해서 super.메서드()로 쓰고 있지만, 그 메서드의 내부 작동방식을 모른다 -> add기능추가 후 addAll 기능추가 시 의도와 다르게 작동
02-1 가장 원초적인 super.메서드() ex> .add() 의 경우, 기능을 추가해도 문제가 없다
-
오버라이딩부터 한다.
-
선택할 메서드가 너무 많으면
add 를 검색하면 된다

-
-
부모에서 물려받은 것(super생략가능)을 쓰기 전에, 누적하는 기능 ->
매 인스턴스마다 상태값을 누적할 인스턴스 변수도 필요하다.누적전에 0으로 초기화해야할텐데,int는 기본적으로 0으로 초기화 해준다.


-
내부 상태값을 얻기 위해서 getter가 필요하다.
-
이것도
generate 띄운 상태에서 검색으로 빠르게 생성할 수 있다.

-
public class LottoNumbers extends HashSet<LottoNumber> {
private int addCount;
@Override
public boolean add(final LottoNumber lottoNumber) {
addCount ++;
return super.add(lottoNumber);
}
public int getAddCount() {
return addCount;
}
}
class LottoNumbersTest {
@Test
void add() {
final LottoNumbers lottoNumbers = new LottoNumbers();
lottoNumbers.add(LottoNumber.of(1));
lottoNumbers.add(LottoNumber.of(2));
lottoNumbers.add(LottoNumber.of(3));
lottoNumbers.add(LottoNumber.of(4));
lottoNumbers.add(LottoNumber.of(5));
lottoNumbers.add(LottoNumber.of(6));
assertThat(lottoNumbers.getAddCount()).isEqualTo(6);
}
}
02-2 내부에서 add()를 사용하고 있는지도 모르고, addAll()를 오버라이딩 + 기능 추가하면 -> 기능이 2배로 추가 된다.
-
addAll()을 호출하는 테스트를 만들어보자.
-
새자료구조 ex> Set, List를 만들 땐.of를 쓴다.**
-
addAll()은 가변인자에
원소가 아니라 같은 자료구조의 묶음은 add한다.- ex> 빈 셋 -> 빈셋.addAll( 셋묶음 )
-
위에서 add로 여러번 선언한 것들 중 인자들만 복사해서, 활용할 수 있다.



@Test void addAll() { final LottoNumbers lottoNumbers = new LottoNumbers(); lottoNumbers.addAll(Set.of( LottoNumber.of(1), LottoNumber.of(2), LottoNumber.of(3), LottoNumber.of(4), LottoNumber.of(5), LottoNumber.of(6) )); assertThat(lottoNumbers.getAddCount()).isEqualTo(6); }
-
-
addAlll() 갯수 세는 기능을 추가해보자.
-
Generate띄우고,over라이딩도 검색할 수 있다- 오버라이딩할 메서드고 검색해서 오버라이딩하자.


-
-
add시마다 더해주는 기능을 추가 해보자.
@Override public boolean addAll(final Collection<? extends LottoNumber> c) { //넘어오는 단일객체를 원소로 가지는 Collection의 사이즈를 더해주자 addCount += c.size(); return super.addAll(c); } -
6개가 더해져야하는데, 12가 나온다.

문제02 디버깅) sout로 찍어보기 -> 정상이면 타고 올라가보기
-
디버깅으로서 일단, size가 잘못더해지는지 찍어볼 것이다.
@Override public boolean addAll(final Collection<? extends LottoNumber> c) { addCount += c.size(); System.out.println("c.size() = " + c.size()); return super.addAll(c); }
-
더해주는게 정상인데…? 메서드를 직접 까봐야한다.
-
컨트롤 클릭으로 타고 올라가기


-
-
분석하기
- addAll()은 내부에서 매번 add()를 호출한다.
- add()는 추상메서드로서 우리가
오버라이딩=재정의해준 add()가 호출되어, 개별 count기능까지 작동하게 된다.
- addAll()은 내부에서 매번 add()를 호출한다.

기본적으로 제공하는 api들 내부작동방식을 모르므로 상속후 기능 추가를 위한 재정의시 조심해서 사용해야하므로 상속을 비추한다. -> 상속 대신 조합-> 포장도.. 조합?!
-
상속을 제거하고 기존 클래스(HashSet
<객체>)의 인스턴스를 -> 인스턴스 변수로 가지도록 바꾸자.
-
필드 선언을 위해 변수추출부터 하고 싶다면
메서드 내부로 잠시 이동해서 하자

-
필드의 자료형은
shift+tab으로 앞으로 넘어와서 다형성을 적용시켜서 완성하자
-
private가 아닌 메서드에서 선언했다면 접근제한자도 확인을 해주자.
-
다시 필드 선언부로 가지고 오자.

-
-
상속에서 기능추가한
오버라이딩삭제 +super.대신 -> value.로 변경해주자.public class LottoNumbers { private final Set<LottoNumber> value = new HashSet<>(); private int addCount; public boolean add(final LottoNumber lottoNumber) { addCount++; return value.add(lottoNumber); } public boolean addAll(final Collection<? extends LottoNumber> c) { addCount += c.size(); return value.addAll(c); } public int getAddCount() { return addCount; } } -
참고->
.add()의 결과로는 boolean을 반환한다-> 호출시마다 if를 달아서 실패시 예외내야하는 건 아닌가?
- 실패하는 케이스가 거의 없다 -> 응답할 필요 없다. -> 잘못 설계된 것으로 추정된다.
- capacity가 정해져있어도 내부적으로 resizing해서 실패를 안함.
- 실패하는 케이스가 거의 없다 -> 응답할 필요 없다. -> 잘못 설계된 것으로 추정된다.
참고) super -> 자식이 생성자 재정의할라고 갖다쓰는 것은 주생성자 쓰는 것처럼 가능하다 -> but 메서드 호출까지 super를 호출한다면, 부모의 구조를 다알아야해서 결합도가 너무 커져 안좋다.
조합
-
- 조합 블로그
- 블랙잭에서 조합 적용 블로그
-
[이펙티브 자바] 추상 클래스보다는 인터페이스를 우선하라
- 우테코에서 언급
- 인-추(코드중복제거?)-상속
- 매트 추상화 글
- 연록조합글
- 대놓고 추상-골격구현클래스으로 PR 리뷰 받았다! 젤 참고
- 동기인듯? 코드중복 제거 및 상태를 가지므로 추클이 더 적합
- 인터페이스 > 추클의 옛날 글
- cf) 합성: 조합의 수만큼 클래스로 나누어서 외부에서 주입 어려움 -> 블로그
public class Man { public void move() { System.out.println("걷는다"); } public void eat() { System.out.println("먹는다"); } } class SuperMan { private final Man man = new Man(); public void move() { man.move(); } public void eat() { man.eat(); } public boolean canTouchKryptonite(){ return false; } public void fly() { System.out.println("날아간다."); } } // 다형성을 위해서는 인터페이스나, 추상 클래스를 이용한 구현을 고려해보자. // 상속을 기능의 재활용보다는, 정제를 위해 사용하자! // 이런 경우에는 상속을 고려해보자. // 코드 재사용을 주목적으로 하기보다는 확장성, 유연성을 고려해야할때 // IS-A 관계가 명확할때 // 부모 메소드에 이미 구현된 내용이 절대 바뀌지 않는다고 확신이 들때

상속은 is-a관계가 기본 조건(모두 내려받아야하고+내용변경만 재정의용) -> 재사용성
-
앞서 예제인
Document도 완전 동일한is-a관계지만, 상황에 따라 상속시 원하지 않는 결과를 가져올 수 있다. -
더군다나
모두 받을 필요 없거나+더 추가될 기능이 있다면상속은 포기해야한다.
안받아도 되거나 추가될 기능이 존재한다면, 상속은 ㅂㅂ2 extends는 확장개념이 아니라 refine개념(is-a로 일치하면서 일부만 바꿔쓰기용)

- 과거에, OOP 처음 나왔을 때는
재사용성-> 상속받아서 필요한 부분만 변경해서 쓰자.의 개념이었다.
- 지금은 부작용이 많은 설계법 -> 현대는
유연성이 더 중요
추가 얘기거리 -> class 생성자 final or abstract를 붙이자



- 테스트를 위해 interface를 쓸 수 있다.
- 추상메서드 1개 -> 바깥에서 람다로 구현 가능 -> 테스트에서 람다로 추상메서드까지 구현
- 추상클래스도 가능?? -> 가능해진다. 하지만
람다=익클처럼 사용이 오히려 부작용으로 작용한다.
-
상속을 염두에 안두었다면
final을 붙여서 class를 설계하라.- final을 붙여서 상속안되게 해라.
-
최소 1곳 이상을 개별 구현해야하는 추상화된 메서드를 포함하고is-a관계로 개별구현 빼고는 기능이 모두 똑같아서상속해야하는 class라면abstract붙여서 설계해라.- 추메 1개면
인터페이스로 가고 ->익클,람다,테스트가 가능한 것이 된다. - 추메 + @(상태or공통메서드)로 메서드 2개 이상이면
abstract를 붙혀is-a관계의상속만 가져갈 예정이라고 생각하자 일부자식이 추메를 기능 구현 안할라면 -> 추상클래스로 만들지 말자
- 추메 1개면
is-a의 abstract 상속 클래스에 -> 일부자식만 새로운 기능추가의 확장 포인트가 생겨 필요하다? -> final을 풀지말고 포장으로 확장해서 사용하면 된다
-
class를
final class로 막아뒀다. -> 상속 및 재정의가 불가능하다.public final class LottoNumbers { private final Set<LottoNumber> value = new HashSet<>(); -
기능을 추가해서 확장해야하는 경우가 생긴다면 ->기존 상속은 건들지말아야함. -
자식들 모두 공통메서드 or 각각 모두 개별구현 가능이 아니라면 ->포장하는클래스를 파고 내부에서 확장하자
-
좀더 세부적이고 확장된 이름으로
포장 클래스를 파서 확장하는조합개념을 쓴다.
-
확장하고 싶은 클래스의 인스턴스를인스턴스 변수로 가지게 한다.public class SomeLottoNumbers { private LottoNumber lottoNumber; }
-
불변 일급컬렉션의 증감메서드
VO처럼, 불변 일급컬렉션이라면 VO.증감() -> 새객체 응답처럼 컬렉션.add/remove호출()시 가변 가능한 new 새컬랙션<>()을 복사한 새 컬렉션를 응답해줘야한다.
-
기존 add: 기존 내부
포장 컬렉션에 add후 증감은 ->내부증감처리후 노응답public final class LottoNumbers { private final Set<LottoNumber> value = new HashSet<>(); public boolean add(final LottoNumber lottoNumber) { return value.add(lottoNumber); } }
01 컬렉션의 add/remove()메소드를 호출받는 일급컬렉션 객체로 그대로 응답되어야한다.
- 원래 add/remove boolean 응답 하나 사용안함

public final class LottoNumbers {
private final Set<LottoNumber> value = new HashSet<>();
//1. add/remove등 컬렉션의 증감호출시 -> 응답이 일급컬렉션 자신이어야한다.(원래 add/remove boolean응답하나 사용안함)
public LottoNumbers add(final LottoNumber lottoNumber) {
return value.add(lottoNumber);
}
}
02 가변 컬렉션을 new 키워드로 복사해준다


03 복사한 컬렉션에 add/remove등 컬렉션 증감해준다

04 증감된 복사컬렉션으로 새 일급컬렉션을 만들어서 반환한다. -> 생성자 필요

05 대박) 재료(인자)에 의해 일급컬렉션에 [재료(컬렉션) 파라미터 생성자만 최초]로 생긴다면 그것을 주생성자로 -> 파라미터 없이 내부 빈컬랙션으로 초기화 with this()의 부생성자도 같이 만들어주자. -> 멋도 모르고 과거에 테코 등에서 쓴 기본생성자 = 주생성자this()를 이용한 빈 컬렉션으로 초기화 넣어주는 부생성자
-
필요에 의해 컬렉션을 재료로 받는 생성자가 생겼다.


-
파라미터 받아 초기화 해주는 생성자가 생겨나는 순간,
인변의 선언과 동시에 초기화는 무용지물이 된다.
-
기존 외부 인자(재료)없이
빈컬렉션으로 초기화하는 일급컬렉션의 사용이 에러가 난다.-
재료 사용 생성자가
주생성자 -
처음에 멋도 모르고 쓴 것이
부생성자
-
-
generate -> construct ->
Select None으로 빠르게파라미터 없는 빈 기본생성자를 생성해준다.




-
재료받는 주생성자
this를 활용해서,초기화용 빈 컬렉션을 주생성자의 재료에 넣어준다.
-
인변을 직접 초기화 해줬던 부분을 삭제한다.

05 원소 추가시마다 새 가변 컬렉션이 복사 후 add한 뒤 새 일급컬렉션 객체가 반환되므로 호출부에서는 원소추가/제거 메서드 호출후 다시 할당해야된다. -> 확인은 .size()를 추가 정의해줘서 확인하면 된다.
- VO의 경우
- count +=1; ->
Count count =는 호출부 그대로 두고 ->+1의 증감부분에 새객체 반환
- count +=1; ->
- 일급컬렉션의 경우
- .add(원소) ->
일급형 일급 =는 호출부로 두고 ->.add(원소)의 원소추가제거 부분에 복사된 가변 새컬렉션에 add해서 반환
- .add(원소) ->
public final class LottoNumbers {
private Set<LottoNumber> value;
public LottoNumbers() {
this(new HashSet<>());
}
public LottoNumbers(final Set<LottoNumber> value) {
this.value = value;
}
public LottoNumbers add(final LottoNumber lottoNumber) {
final Set<LottoNumber> lottoNumbers = new HashSet<>(value);
lottoNumbers.add(lottoNumber);
return new LottoNumbers(lottoNumbers);
}
}
-
이제부터 일급컬렉션의 add는
새 일급 객체를 응답하므로 ->호출부에서는 할당으로 받아줘야한다.-
기존: 기존 내부값이 변했음

-
불변: 변화된 새 컬렉션이 응답된다. -> 기존값 변화가 아니라 새로 생기므로 1 변수에서 받아줘야한다.


-
-
getter후 .size()때리지말고,
.size()를 정의해서 사용하다가 필요없으면 지우자

void add() {
LottoNumbers lottoNumbers = new LottoNumbers();
//
lottoNumbers = lottoNumbers.add(LottoNumber.of(1));
lottoNumbers = lottoNumbers.add(LottoNumber.of(2));
lottoNumbers = lottoNumbers.add(LottoNumber.of(3));
lottoNumbers = lottoNumbers.add(LottoNumber.of(4));
lottoNumbers = lottoNumbers.add(LottoNumber.of(5));
lottoNumbers = lottoNumbers.add(LottoNumber.of(6));
assertThat(lottoNumbers.size()).isEqualTo(6);
}
(변화 전 기존 객체의)불변 일급컬렉션의 장점 정리
-
기존 객체의 변화가 아니라 불변을 유지하면서 새로운 객체로 응답
- 한번 생성한 객체는 불변하며, 변화를 요구시 새로운 객체가 반환된다.
- 기존 객체는 불변유지로 안전하다.
-
그로 인해 final 사용할 때 처럼 동일하게 안정화된다.
-
반대로
가변 객체는여러 클래스에서 동시 접근(시간처 접근)시 굉장히 불안하다- 다른데서 중간에 접근해서 내부값을 변화시키면, 또다른 곳에선 변화된 객체가 반영된다.
-
포장(포인터의 포인터)은 업데이트 상태를 반영해주지만, 동시 접근시 불안해지니
기존 객체는 불변으로접근 한곳에서는 복사본을 이용한 새 객체를 제공해준다.
-
기존객체만 불면이고
호출시 복사된 새 객체를 제공해주니DTO처럼 view에 막 던져도 안정적이다- 건들이고 싶어도 기존 객체는 불변이라 못건들이게 되니 -> view에서도
복사된 가변 새컬렉션을새 객체로 넘겨주면 된다.
- 건들이고 싶어도 기존 객체는 불변이라 못건들이게 되니 -> view에서도
-
불변객체를 map의 key로 사용하게 될 경우식별자 변경의 문제가 발생하지 않는다.key로 사용하는 객체는 불변객체여야한다.

-
도중에 실패하는 경우, 일부만 달라질 수 있다. -> 기존 객체는 불변을 유지하게 해줘서 그런 문제(
실패 원자성)를 막자.
-