강의)로또-캐싱/정펙매/조합/불변일급컬렉션
로또번호로 보는 정적팩토리메서드, 캐싱, 상속, 불변일급컬렉션
📜 제목으로 보기
- 지금까지 클래스
- 정펙매로 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; } }
new 키워드
의 public 주생성자]는 템플릿일 뿐
이며, 항상 new사용과 동시에 100% 객체를 새로 생성(하여 할당)
할 수 밖에 없다.
02 도메인 Test코드에서 생성자를 테스트하는데, [-
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;
}
}
Class.of(, )
, Class.from()
)를 사용하고, 정펙매 내부에서 캐싱한다.
02 new키워드가 아니라 정적팩메(-
객체 생성을
100% 새로 생성하는 new Class
대신Class.of( , )
나Class.from()
으로 생성해보자.-
빨간줄로
public static
정적 메서드 생성 -
파라미터명은 value로 수정해놓고 시작
public static LottoNumber of(final int value) { throw new UnsupportedOperationException("LottoNumber#of not write."); }
-
<외부에서 요청하며 메서드 파라미터로 들어오는 rawType, 현재 내부인 캐슁 객체Class>
로 hashMap 제네릭 타입을 결정 + 사이즈를 알면 initialcapacity로 미리 준다. + 정펙매 내부에서 만드는 척
만 해서 변수추출
03 캐싱은 -
변수 블럭에서 적상하면
변수 추출
안되니,편의상 정펙매 내부에서 캐쉬 만드는 척
하자- 외부에서 들어오는 rawType과 동일한 Type으로 key타입을 맞춰줘야 꺼내쓸 수 있다.
private + static final
의 싱글톤으로서 상수처럼 미리 빈 map을 초기화
해놓는다.
04 캐싱객체는 -
변수추출로 변수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;
요청이 왔을 때, if없으면 -> 빈 맵에 생성해서 넣고 || 맵에서 그외 꺼내주기
인데, hashMap.computeIfAbsent(확인할 key, 없으면key 해당 value자리에 넣어줄 값 생성 Function)
을 제공해주므로 없으면 new로 새로 생성
로직만 넣어주면 된다.
05 캐싱의 핵심은 - 람다용
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를
String
1개로 바꾸고 -> toString()으로 2개를 더해서 key1개로 사용해도 된다 -
enum이라면, .name() + ,name()로 스트링으로 더해서 key1개로 만들면 된다.
생성자가 많으면, 응집도가 높고 견고한 클래스가 된다.
다른Type 추가 정펙매
- 여러가지 방식으로 제공해주면
클라이언트 입장에서 사용하기 좋은 코드가 된다.
다른Type 입력 제공으로 인한 추가 정펙매 제공
은 부생성자로서 위에 위치 + convert후 기존 정펙매(=기준 정펙매)
를 호출한다.
대박) 포장이 아닌
포장으로 인한 메서드 추가
의 상황이라면 -> [새기준 메서드 생성+내용복붙후 처리 완성]후 [기존 rawInput메서드는 포장후 새기준 호출]하도록 변경
01
다른Type입력
을 위한 생성자/정펙매를 제공한다면 -> 기존 생성자/정펙매가 기준 메서드
라 생각하고 내부convert후 호출
02 -
새 기준
포장
하는 파라미터 변화가 아니라편의를 위해 다른 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 추가 생성자
인스턴스 변수(상태값) 초기화는 주생성자 1개
에서만 -> 나머지는 위에서 주생성자를 convert후 -> 기존 생성자를 this()로 호출
만 해주는 부생성자들로 -> 중복을 피하자
여러 Type의 생성자 제공이라도 - 자바만
부생성자
라는 관례적이 이름이 있다.- 무조건 주생성자에서만 인변을 초기화 하기 위해 관례적으로 지은 이름
- 다른언어는 무조건 주 생성자를 이용해서만 호출하도록 지원하는 것이 많다.
부생성자
로서 convert후 this()로 주생성자를 호출해야한다. -> 직접 주생성자의 역할인 인스턴스변수 초기화
를 해주면 안된다
01 다른Type의 생성자 추가는 - 직접 주생성자의 역할인
인스턴스변수 초기화
를 해주면 안된다.
위에 정의 + convert후 this로 주생성자호출
-> convert후 원래Type의 주생성자 코드(검증후 인변 초기화)를 반복하지마라.
02 다른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)); }
-
인수 늘어날 때, 코드 변경될 때 -> 모든 부생성자까지 다 초기화 해줘야한다
-> 그러니 convert후 this() 주생성자를 호출해주자.
정리) 생성자 코드 중복 in 부생성자 -> - 주 생성자는 객체 초기화 프로세스의 유일한 장소 -> 추가 생성자에서 초기화 하지말자
상속
상속 잘 사용 하기 (부작용이 크니까)
- 상속은 나쁜 것이다?
- 고민을 해봐야한다. 내가 쓰레기처럼 쓰는 것은 아닌지..
- 스프링에서도 상속을 잘 쓴다.
상속 부작용
- 부모 변화(탑레벨 클래스 기능 변경) -> 자식 모두 수정(하위 클래스 모두 변경)
- 안좋은 결합도가 높아짐.
- 나쁜 것도 물려준다. 선택이 없다.
상속되서 사용되는 변수/메서드
는 부모 내부에서 정의되어 사용되는 메서드
라도 자식에 오버라이딩된 메서드부터 찾는
다. -> 오버라이딩 전 메서드를 호출하는 방법이 없다?
부->자로 - 동일한 형태, 구조를 가질 예정이므로
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) 부모인 컬렉션 기능 다 사용되서 좋아보이지만, -
.
찍어보면 사용하지 필요없는 기능들도 다 물려받는다. -
위험한 기능도 다 받은 상태라
클라이언트에도 노출되서 위험
하다
오버라이딩해서 super.메서드()로 쓰고
있지만, 그 메서드의 내부 작동방식을 모른다
-> add기능추가 후 addAll 기능추가
시 의도와 다르게 작동
문제02) 부모 물려받은 메서드에 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기능까지 작동하게 된다.
상속후 기능 추가를 위한 재정의
시 조심해서 사용해야하므로 상속을 비추한다. -> 상속 대신 조합-> 포장도.. 조합?!
기본적으로 제공하는 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개면
일부자식만 새로운 기능추가의 확장 포인트
가 생겨 필요하다? -> final을 풀지말고 포장으로 확장
해서 사용하면 된다
is-a의 abstract 상속 클래스에 -> -
class를
final class
로 막아뒀다. -> 상속 및 재정의가 불가능하다.public final class LottoNumbers { private final Set<LottoNumber> value = new HashSet<>();
-
기능을 추가해서 확장
해야하는 경우가 생긴다면 ->기존 상속
은 건들지말아야함. -
자식들 모두 공통메서드 or 각각 모두 개별구현 가능
이 아니라면 ->포장하는클래스
를 파고 내부에서 확장하자 -
좀더 세부적이고 확장된 이름으로
포장 클래스를 파서 확장
하는조합
개념을 쓴다.-
확장하고 싶은 클래스
의 인스턴스를인스턴스 변수
로 가지게 한다.public class SomeLottoNumbers { private LottoNumber lottoNumber; }
-
불변 일급컬렉션의 증감메서드
VO.증감() -> 새객체 응답
처럼 컬렉션.add/remove호출()
시 가변 가능한 new 새컬랙션<>()을 복사한 새 컬렉션
를 응답해줘야한다.
VO처럼, 불변 일급컬렉션이라면 -
기존 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);
}
}
가변 컬렉션을 new 키워드로 복사
해준다
02
복사한 컬렉션에 add/remove등 컬렉션 증감
해준다
03
증감된 복사컬렉션
으로 새 일급컬렉션을 만들어서 반환한다. -> 생성자 필요
04
일급컬렉션에 [재료(컬렉션) 파라미터 생성자만 최초]로 생긴다
면 그것을 주생성자
로 -> 파라미터 없이 내부 빈컬랙션으로 초기화 with this()의 부생성자
도 같이 만들어주자. -> 멋도 모르고 과거에 테코 등에서 쓴 기본생성자 = 주생성자this()를 이용한 빈 컬렉션으로 초기화 넣어주는 부생성자
05 대박) 재료(인자)에 의해 -
필요에 의해 컬렉션을 재료로 받는 생성자가 생겼다.
-
파라미터 받아 초기화 해주는 생성자가 생겨나는 순간,
인변의 선언과 동시에 초기화
는 무용지물이 된다. -
기존 외부 인자(재료)없이
빈컬렉션으로 초기화하는 일급컬렉션
의 사용이 에러가 난다.-
재료 사용 생성자가
주생성자
-
처음에 멋도 모르고 쓴 것이
부생성자
-
-
generate -> construct ->
Select None
으로 빠르게파라미터 없는 빈 기본생성자
를 생성해준다. -
재료받는 주생성자
this
를 활용해서,초기화용 빈 컬렉션을 주생성자의 재료
에 넣어준다. -
인변을 직접 초기화 해줬던 부분을 삭제한다.
원소 추가
시마다 새 가변 컬렉션이 복사 후 add
한 뒤 새 일급컬렉션 객체
가 반환되므로 호출부
에서는 원소추가/제거 메서드 호출후 다시 할당
해야된다. -> 확인은 .size()
를 추가 정의해줘서 확인하면 된다.
05 - 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로 사용하는 객체는 불변객체
여야한다.
-
도중에 실패하는 경우, 일부만 달라질 수 있다. -> 기존 객체는 불변을 유지하게 해줘서 그런 문제(
실패 원자성
)를 막자.
-