// Wrapping 전 — 일반 컬렉션Map<String,String> map =newHashMap<>();map.put("1","A");map.put("2","B");map.put("3","C");// Wrapping 후 — 일급 컬렉션 (다른 멤버 변수 없음)publicclassGameRanking{privateMap<String,String> ranks;publicGameRanking(Map<String,String> ranks){this.ranks = ranks;}}
// 검증 로직이 서비스에 흩어짐 — 로또 번호가 필요한 모든 곳에 중복 작성 필요publicclassLottoService{privatestaticfinalint LOTTO_NUMBERS_SIZE =6;publicvoidcreateLottoNumber(){List<Long> lottoNumbers =createNonDuplicateNumbers();validateSize(lottoNumbers);validateDuplicate(lottoNumbers);// 이후 로직 쭉쭉 실행}privatevoidvalidateSize(List<Long> lottoNumbers){if(lottoNumbers.size()!= LOTTO_NUMBERS_SIZE){thrownewIllegalArgumentException("로또 번호는 6개만 가능합니다.");}}privatevoidvalidateDuplicate(List<Long> lottoNumbers){Set<Long> nonDuplicateNumbers =newHashSet<>(lottoNumbers);if(nonDuplicateNumbers.size()!= LOTTO_NUMBERS_SIZE){thrownewIllegalArgumentException("로또 번호들은 중복될 수 없습니다.");}}}
// 일급 컬렉션 — 6개 / 중복되지 않은 숫자만 가능한 자료구조publicclassLottoTicket{privatestaticfinalint LOTTO_NUMBERS_SIZE =6;privatefinalList<Long> lottoNumbers;publicLottoTicket(List<Long> lottoNumbers){validateSize(lottoNumbers);// 생성 시점에 검증 강제validateDuplicate(lottoNumbers);this.lottoNumbers = lottoNumbers;}privatevoidvalidateSize(List<Long> lottoNumbers){if(lottoNumbers.size()!= LOTTO_NUMBERS_SIZE){thrownewIllegalArgumentException("로또 번호는 6개만 가능합니다.");}}privatevoidvalidateDuplicate(List<Long> lottoNumbers){Set<Long> nonDuplicateNumbers =newHashSet<>(lottoNumbers);if(nonDuplicateNumbers.size()!= LOTTO_NUMBERS_SIZE){thrownewIllegalArgumentException("로또 번호들은 중복될 수 없습니다.");}}}// 서비스는 단순히 LottoTicket을 생성하기만 하면 됨 — 필요한 로직은 모두 LottoTicket으로publicclassLottoService{publicvoidcreateLottoNumber(){LottoTicket lottoTicket =newLottoTicket(createNonDuplicateNumbers());// 이후 로직 쭉쭉 실행}}
@Testvoidfinal도_값변경이_가능하다(){// givenfinalMap<String,Boolean> collection =newHashMap<>();// when — final 선언에도 불구하고 값 추가 가능 collection.put("1",true); collection.put("2",true); collection.put("3",true); collection.put("4",true);// then — 테스트 통과 (값이 추가됨)assertThat(collection.size()).isEqualTo(4);}@Testvoidfinal은_재할당이_불가능하다(){// givenfinalMap<String,Boolean> collection =newHashMap<>();// when — 컴파일 에러: Cannot assign a value to final variable 'collection'// collection = new HashMap<>(); // 이 줄은 컴파일 에러 발생assertThat(collection.size()).isEqualTo(0);}
publicclassLottoTicket{privatefinalList<Long> lottoNumbers;publicLottoTicket(List<Long> lottoNumbers){this.lottoNumbers =newArrayList<>(lottoNumbers);// 방어적 복사}// 값을 변경하는 메서드가 없음 — 진정한 불변publicList<Long>getLottoNumbers(){returnCollections.unmodifiableList(lottoNumbers);// 읽기 전용으로 노출}}
// 기존 방식 — 상태(pays)와 로직(계산)이 서로 다른 클래스에 흩어짐publicclassPayService{publicLonggetNaverPaySum(List<Pay> pays){return pays.stream().filter(pay ->PayType.NAVER_PAY.equals(pay.getPayType())).mapToLong(Pay::getAmount).sum();// 다른 서비스에서도 같은 계산 로직이 중복될 수 있음}}
// 일급 컬렉션 적용 — 상태(pays)와 행위(계산)를 한 곳에서 관리publicclassPayGroups{privateList<Pay> pays;publicPayGroups(List<Pay> pays){this.pays = pays;}publicLonggetNaverPaySum(){returngetFilteredPays(pay ->PayType.isNaverPay(pay.getPayType()));}publicLonggetKakaoPaySum(){returngetFilteredPays(pay ->PayType.isKakaoPay(pay.getPayType()));}privateLonggetFilteredPays(Predicate<Pay> predicate){return pays.stream().filter(predicate).mapToLong(Pay::getAmount).sum();}}
// 기존 방식 — 변수명으로만 구분, 코드베이스 검색 불가List<Pay> naverPays = pays.stream().filter(pay ->PayType.NAVER_PAY.equals(pay.getPayType())).collect(Collectors.toList());List<Pay> kakaoPays = pays.stream().filter(pay ->PayType.KAKAO_PAY.equals(pay.getPayType())).collect(Collectors.toList());// 일급 컬렉션 — 타입으로 명확히 구분, 코드베이스 전체 검색 가능NaverPays naverPays =newNaverPays(pays);KakaoPays kakaoPays =newKakaoPays(pays);