📜 제목으로 보기

지금까지 클래스

  • 지금까지의 Class는 객체를 찍어내는 Factory일 뿐이었다.
  • 조금 더 나아가서 객체 생성 Factory(찍어내는 템플릿)일 뿐만 아니라 객체의 능동적인 관리자의 역할도 할 수 있다.

제한적인 자바의 new 키워드

  • java에서는 new키워드가 제한을 가지고 있다.
    1. 무조건 응답해야하며
    2. 무조건 객체가 생성되어야한다.

정펙매로 Class를 객체(생성의) 능동적 관리자로 승격

정펙매를 통한 캐싱 -> [new처럼 매번 새로 생성]안해도 되는 객체 생성

01 프로덕션 원시값을 포장 -> [public 주생성자]만 빨간줄 자동생성

  1. 기본적으로 클래스내 원시값int인스턴스 변수 = 상태값으로 포장하여 선언하면 빨간줄로 생성자 만들라고 한다.

    image-20220316005417318

    image-20220316005429355

     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% 객체를 새로 생성(하여 할당) 할 수 밖에 없다.

  1. LottoNumberTest를 만들고 생성자 생성 연습시작

    • go to test(직접지정 ctrl+shift+t)로 생성하거나 이동하기!

      image-20220316012658877

  2. 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())를 사용하고, 정펙매 내부에서 캐싱한다.
  1. 객체 생성을 100% 새로 생성하는 new Class대신 Class.of( , )Class.from()으로 생성해보자.

    • 빨간줄로 public static 정적 메서드 생성

      image-20220316205105905 image-20220316205156052

    • 파라미터명은 value로 수정해놓고 시작

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

    • 외부에서 들어오는 rawType동일한 Type으로 key타입을 맞춰줘야 꺼내쓸 수 있다.

    image-20220316211943255

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

    image-20220316212249600

    image-20220316212345286

     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로 새로 생성 로직만 넣어주면 된다.

image-20220316212856485

  • 람다용 Function자리의 변수추천은 map의 keyType에 따라 바껴서 추천된다.
    • Map<Integer, 객체>일 경우 -> integer -> {} 추천 image-20220316213019063
    • Map<Long, 객체>일 경우 -> aLong -> {}추천 image-20220316213101197
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으로 잠궈주기

image-20220316213559982 image-20220316213606905

  • public으로 남게 되면, 의도했던 능동적 객체관리와 다르게 코드가 돌아갈 수 있다.

    • 정펙매 사용으로 유도하고 기본생성자를 private로 막고 사용

    • 테스트를 위해 잠시 여기서는 허용

07 최종 필요한 것 정리
  1. private static final Map<외부생성자로 들어올 rawType, 현재객체Class> cache = new HashMap<> (갯수);

  2. 정펙매 내부에서 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으로 선언

image-20220316222612465

private static final Map<Integer, LottoNumber> cache = new WeakHashMap<>(45);

06 없을 때 생성 이 아니라 미리 만들고 -> 요청시 조회get만? -> static 블록 생기는게 짜증

image-20220316222632465

image-20220316222655010

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개를 원소로 가지는 클래스를 두고 한다?

image-20220317182716264

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

    image-20220317182858270

    image-20220317182754613

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

생성자가 많으면, 응집도가 높고 견고한 클래스가 된다.

다른Type 추가 정펙매

image-20220316223609297

  • 여러가지 방식으로 제공해주면 클라이언트 입장에서 사용하기 좋은 코드가 된다.

대박) 포장이 아닌 다른Type 입력 제공으로 인한 추가 정펙매 제공은 부생성자로서 위에 위치 + convert후 기존 정펙매(=기준 정펙매)를 호출한다.

01 포장으로 인한 메서드 추가의 상황이라면 -> [새기준 메서드 생성+내용복붙후 처리 완성]후 [기존 rawInput메서드는 포장후 새기준 호출]하도록 변경
02 다른Type입력을 위한 생성자/정펙매를 제공한다면 -> 기존 생성자/정펙매가 기준 메서드라 생각하고 내부convert후 호출
  1. 새 기준 포장하는 파라미터 변화가 아니라 편의를 위해 다른 type의 입력을 제공한다고 치고 빨간줄 생성하자

    image-20220316224807700 image-20220316224820639

  2. 포장이 아닐 경우, 다른Type메서드는 기준메서드가 아니다 -> 부 생성자로서 기존생성자보다 위에 + 포장해서 기존생성자=기준메서드로 호출해준다.

    image-20220316225149477

     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 일반적으로 생성자는 많은 것이 좋다.

  • 메서드와 다르게 많이 제공할 수록 좋단다. image-20220317000732875

다른Type 추가 생성자

여러 Type의 생성자 제공이라도 인스턴스 변수(상태값) 초기화는 주생성자 1개에서만 -> 나머지는 위에서 주생성자를 convert후 -> 기존 생성자를 this()로 호출만 해주는 부생성자들로 -> 중복을 피하자

image-20220316231502148

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

image-20220316235016077

image-20220316235116910

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

    image-20220316235225200

  2. 대부분 다른 Type 으로서 String을 받아주므로 rawInput에 해당하는 rawValue로 네이밍 해보자.

     //부 : rawValue
     public LottoNumber(final String rawValue) {
        
     //주 : value
     public LottoNumber(final int value) {
        
    
  3. convert만 하고, 원래Type이 했던 검증+ 인변 초기화해주면 똑같이 해주면 코드 중복이다.

    • 코드중복 = 나쁜 예 image-20220317000105636

    • 생성자 내부코드는 주생성자를 호출해서 인스턴스 변수 초기화의 중복 = 코드 중복을 막자.

        public LottoNumber(final String rawValue) {
            this(Integer.parseInt(rawValue));
        }
      

      image-20220317000701858

정리) 생성자 코드 중복 in 부생성자 -> 인수 늘어날 때, 코드 변경될 때 -> 모든 부생성자까지 다 초기화 해줘야한다 -> 그러니 convert후 this() 주생성자를 호출해주자.
  • 주 생성자는 객체 초기화 프로세스의 유일한 장소 -> 추가 생성자에서 초기화 하지말자 image-20220317000759443

상속

image-20220317092045468

상속 잘 사용 하기 (부작용이 크니까)

  • 상속은 나쁜 것이다?
    • 고민을 해봐야한다. 내가 쓰레기처럼 쓰는 것은 아닌지..
    • 스프링에서도 상속을 잘 쓴다.

상속 부작용

  • 부모 변화(탑레벨 클래스 기능 변경) -> 자식 모두 수정(하위 클래스 모두 변경)
    • 안좋은 결합도가 높아짐.
    • 나쁜 것도 물려준다. 선택이 없다.
부->자로 상속되서 사용되는 변수/메서드부모 내부에서 정의되어 사용되는 메서드라도 자식에 오버라이딩된 메서드부터 찾는다. -> 오버라이딩 전 메서드를 호출하는 방법이 없다?
  1. 동일한 형태, 구조를 가질 예정이므로 is-a에 해당하여 상속 했다.
    • cf) has-a구조라면 조합 사용한다. image-20220317094056660
  2. 합리적으로 보이지만, 자식이 물려받고 오버라이딩 안한 length()호출시 부모 content()? 자식오버라이딩한 content() 어느것이 호출될까
    • 자식에서 호출했으면 오버라이딩 된 것이 호출된다.
  • 사용하는 클라이언트는 구현안한 메서드라면, 내부 호출도 부모 것만 원할 수 도 있다.
    • 따로 구현안하고 물려받은 메서드를 호출한다면, 내부에서도 부모 것만 사용하도록 원할 수 도 있다.

상속 문제점 예제 만들어보기

객체List 일급컬렉션( 포장 ) 요구사항이 없었다면? 컬렉션<객체Type>상속해서 컬렉션메서드의 기능을 다사용하도록 했다.
  1. 객체List는 일급컬렉션으로 포장했었다. image-20220317100922716

     public class LottoNumbers {
         private final List<LottoNumber> value;
     }
    
  2. 예전에는 extends 컬렉션<원소Type>을 통해 클래스를 생성했다.

    image-20220317101808200 image-20220317101816818

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

    image-20220317102102538 image-20220317103142253

문제01) 부모인 컬렉션 기능 다 사용되서 좋아보이지만, 위험한+필요없는 기능들도 강제로 제공된다.
  • . 찍어보면 사용하지 필요없는 기능들도 다 물려받는다. image-20220317103242530

  • 위험한 기능도 다 받은 상태라 클라이언트에도 노출되서 위험하다 image-20220317103422085

문제02) 부모 물려받은 메서드에 오버라이딩해서 super.메서드()로 쓰고 있지만, 그 메서드의 내부 작동방식을 모른다 -> add기능추가 후 addAll 기능추가 시 의도와 다르게 작동
02-1 가장 원초적인 super.메서드() ex> .add() 의 경우, 기능을 추가해도 문제가 없다
  1. 오버라이딩부터 한다.

    • 선택할 메서드가 너무 많으면 add 를 검색하면 된다 image-20220317104343260

      image-20220317104514293 image-20220317104639434

  2. 부모에서 물려받은 것(super생략가능)을 쓰기 전에, 누적하는 기능 -> 매 인스턴스마다 상태값을 누적할 인스턴스 변수도 필요하다.

    • 누적전에 0으로 초기화해야할텐데, int는 기본적으로 0으로 초기화 해준다.

    image-20220317113915500

    image-20220317113951377 image-20220317114002672

  3. 내부 상태값을 얻기 위해서 getter가 필요하다.

    • 이것도 generate 띄운 상태에서 검색으로 빠르게 생성할 수 있다.

      image-20220317114102074 image-20220317114109074 image-20220317114118966

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배로 추가 된다.
  1. addAll()을 호출하는 테스트를 만들어보자.

    • 자료구조 ex> Set, List를 만들 땐 .of를 쓴다.** image-20220317133920474

    • addAll()가변인자에 원소가 아니라 같은 자료구조의 묶음은 add한다.

      • ex> 빈 셋 -> 빈셋.addAll( 셋묶음 )
    • 위에서 add로 여러번 선언한 것들 중 인자들만 복사해서, 활용할 수 있다.image-20220317133647023image-20220317133940653image-20220317133952286

        @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);
        }
      
  2. addAlll() 갯수 세는 기능을 추가해보자.

    • Generate띄우고, over라이딩도 검색할 수 있다

      • 오버라이딩할 메서드고 검색해서 오버라이딩하자.

      image-20220317134653685 image-20220317134707601 image-20220317134717881

      image-20220317134816479

  3. add시마다 더해주는 기능을 추가 해보자.

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

문제02 디버깅) sout로 찍어보기 -> 정상이면 타고 올라가보기
  1. 디버깅으로서 일단, size가 잘못더해지는지 찍어볼 것이다.

     @Override
     public boolean addAll(final Collection<? extends LottoNumber> c) {
         addCount += c.size();
        
         System.out.println("c.size() = " + c.size());
        
         return super.addAll(c);
     }
    

    image-20220317135120355

  2. 더해주는게 정상인데…? 메서드를 직접 까봐야한다.

    • 컨트롤 클릭으로 타고 올라가기 image-20220317135412077

      image-20220317135436046

  3. 분석하기

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

image-20220317151216257

기본적으로 제공하는 api들 내부작동방식을 모르므로 상속후 기능 추가를 위한 재정의시 조심해서 사용해야하므로 상속을 비추한다. -> 상속 대신 조합-> 포장도.. 조합?!

  1. 상속을 제거하고 기존 클래스(HashSet<객체>)의 인스턴스를 -> 인스턴스 변수로 가지도록 바꾸자. image-20220317145418860

    • 필드 선언을 위해 변수추출부터 하고 싶다면 메서드 내부로 잠시 이동해서 하자

      image-20220317145831275 image-20220317145856698

    • 필드의 자료형은 shift+tab으로 앞으로 넘어와서 다형성을 적용시켜서 완성하자

      image-20220317145958545

    • private가 아닌 메서드에서 선언했다면 접근제한자도 확인을 해주자.

      image-20220317150150782

    • 다시 필드 선언부로 가지고 오자. image-20220317150216523

  2. 상속에서 기능추가한 오버라이딩삭제 + 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;
        }
    }
    
  3. 참고-> .add()의 결과로는 boolean을 반환한다 -> 호출시마다 if를 달아서 실패시 예외내야하는 건 아닌가? image-20220317150655254

    • 실패하는 케이스가 거의 없다 -> 응답할 필요 없다. -> 잘못 설계된 것으로 추정된다.
      • capacity가 정해져있어도 내부적으로 resizing해서 실패를 안함.

참고) super -> 자식이 생성자 재정의할라고 갖다쓰는 것은 주생성자 쓰는 것처럼 가능하다 -> but 메서드 호출까지 super를 호출한다면, 부모의 구조를 다알아야해서 결합도가 너무 커져 안좋다.

조합

image-20220317151228381

상속은 is-a관계가 기본 조건(모두 내려받아야하고+내용변경만 재정의용) -> 재사용성

  • 앞서 예제인 Document도 완전 동일한 is-a관계지만, 상황에 따라 상속시 원하지 않는 결과를 가져올 수 있다.

  • 더군다나 모두 받을 필요 없거나 + 더 추가될 기능이 있다면 상속은 포기해야한다.

    image-20220317152333563

안받아도 되거나 추가될 기능이 존재한다면, 상속은 ㅂㅂ2 extends는 확장개념이 아니라 refine개념(is-a로 일치하면서 일부만 바꿔쓰기용)

image-20220317152627766

  • 과거에, OOP 처음 나왔을 때는 재사용성 -> 상속받아서 필요한 부분만 변경해서 쓰자.의 개념이었다. image-20220317152819064
  • 지금은 부작용이 많은 설계법 -> 현대는 유연성이 더 중요

추가 얘기거리 -> class 생성자 final or abstract를 붙이자

image-20220317153240259

image-20220317153252296

image-20220317153302901

  • 테스트를 위해 interface를 쓸 수 있다.
    • 추상메서드 1개 -> 바깥에서 람다로 구현 가능 -> 테스트에서 람다로 추상메서드까지 구현
    • 추상클래스도 가능?? -> 가능해진다. 하지만 람다=익클처럼 사용이 오히려 부작용으로 작용한다.
  • 상속을 염두에 안두었다면 final을 붙여서 class를 설계하라.
    • final을 붙여서 상속안되게 해라.
  • 최소 1곳 이상을 개별 구현해야하는 추상화된 메서드를 포함하고 is-a관계로 개별구현 빼고는 기능이 모두 똑같아서 상속해야하는 class라면 abstract 붙여서 설계해라.
    • 추메 1개면 인터페이스로 가고 -> 익클,람다,테스트가 가능한 것이 된다.
    • 추메 + @(상태or공통메서드)로 메서드 2개 이상이면 abstract를 붙혀 is-a관계의 상속만 가져갈 예정이라고 생각하자
    • 일부자식이 추메를 기능 구현 안할라면 -> 추상클래스로 만들지 말자

is-a의 abstract 상속 클래스에 -> 일부자식만 새로운 기능추가의 확장 포인트가 생겨 필요하다? -> final을 풀지말고 포장으로 확장해서 사용하면 된다

  1. class를 final class로 막아뒀다. -> 상속 및 재정의가 불가능하다.

     public final class LottoNumbers {
        
         private final Set<LottoNumber> value = new HashSet<>();
    
  2. 기능을 추가해서 확장해야하는 경우가 생긴다면 -> 기존 상속은 건들지말아야함.

  3. 자식들 모두 공통메서드 or 각각 모두 개별구현 가능이 아니라면 -> 포장하는클래스를 파고 내부에서 확장하자 image-20220317162013658

  4. 좀더 세부적이고 확장된 이름으로 포장 클래스를 파서 확장하는 조합개념을 쓴다.

    image-20220317162107602

    • 확장하고 싶은 클래스의 인스턴스를 인스턴스 변수로 가지게 한다.

        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 응답 하나 사용안함

image-20220317172412174

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 키워드로 복사해준다

image-20220317172650824

image-20220317172702559

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

image-20220317172808861

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

image-20220317172914005

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

    image-20220317174018771

  2. 파라미터 받아 초기화 해주는 생성자가 생겨나는 순간, 인변의 선언과 동시에 초기화는 무용지물이 된다.

    image-20220317174621440

  3. 기존 외부 인자(재료)없이 빈컬렉션으로 초기화하는 일급컬렉션의 사용이 에러가 난다.

    • 재료 사용 생성자가 주생성자

    • 처음에 멋도 모르고 쓴 것이 부생성자

      image-20220317174801210

  4. generate -> construct -> Select None으로 빠르게 파라미터 없는 빈 기본생성자를 생성해준다.

    image-20220317174958638

    image-20220317175011045

    image-20220317175019562

    image-20220317175024526

    image-20220317175031796

  5. 재료받는 주생성자 this를 활용해서, 초기화용 빈 컬렉션을 주생성자의 재료에 넣어준다. image-20220317175242032

  6. 인변을 직접 초기화 해줬던 부분을 삭제한다. image-20220317175303865

05 원소 추가시마다 새 가변 컬렉션이 복사 후 add한 뒤 새 일급컬렉션 객체가 반환되므로 호출부에서는 원소추가/제거 메서드 호출후 다시 할당해야된다. -> 확인은 .size()를 추가 정의해줘서 확인하면 된다.
  • VO의 경우
    • count +=1; -> Count count = 는 호출부 그대로 두고 -> +1의 증감부분에 새객체 반환
  • 일급컬렉션의 경우
    • .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);
    }
}
  1. 이제부터 일급컬렉션의 add는 새 일급 객체를 응답하므로 -> 호출부에서는 할당으로 받아줘야한다.

    • 기존: 기존 내부값이 변했음 image-20220317180125937

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

      image-20220317180303970 image-20220317180310058

      image-20220317180343091

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

    image-20220317175709812

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);
}

(변화 전 기존 객체의)불변 일급컬렉션의 장점 정리

  1. 기존 객체의 변화가 아니라 불변을 유지하면서 새로운 객체로 응답
    • 한번 생성한 객체는 불변하며, 변화를 요구시 새로운 객체가 반환된다.
    • 기존 객체는 불변유지로 안전하다.
  2. 그로 인해 final 사용할 때 처럼 동일하게 안정화된다.

    1. 반대로 가변 객체여러 클래스에서 동시 접근(시간처 접근)시 굉장히 불안하다

      • 다른데서 중간에 접근해서 내부값을 변화시키면, 또다른 곳에선 변화된 객체가 반영된다.
      • 포장(포인터의 포인터)은 업데이트 상태를 반영해주지만, 동시 접근시 불안해지니 기존 객체는 불변으로 접근 한곳에서는 복사본을 이용한 새 객체를 제공해준다. image-20220317181950752
    2. 기존객체만 불면이고 호출시 복사된 새 객체를 제공해주니 DTO처럼 view에 막 던져도 안정적이다

      • 건들이고 싶어도 기존 객체는 불변이라 못건들이게 되니 -> view에서도 복사된 가변 새컬렉션새 객체로 넘겨주면 된다.
    3. 불변객체를 map의 key로 사용하게 될 경우 식별자 변경의 문제가 발생하지 않는다.

      • key로 사용하는 객체는 불변객체여야한다.

      image-20220317181700114

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