규칙 8 : 일급 콜렉션 사용
이 규칙의 적용은 간단하다.
콜렉션을 포함한 클래스는 반드시 다른 멤버 변수가 없어야 한다.
각 콜렉션은 그 자체로 포장돼 있으므로 이제 콜렉션과 관련된 동작은 근거지가 마련된 셈이다.
필터가 이 새 클래스의 일부가 됨을 알 수 있다.
필터는 또한 스스로 함수 객체가 될 수 있다,
또한 새 클래스는 두 그룹을 같이 묶는다든가 그룹의 각 원소에 규칙을 적용하는 드으이 동작을 처리할 수 있다.
이는 인스턴스 변수에 대한 규칙의 확실한 확장이지만 그 자체를 위해서도 중요하다.
콜렉션은 실로 매우 유용한 원시타입이다.
많은 동작이 있지만 후임 프로그래머나 유지보수 담당자에 의미적 의도나 단초는 거의 없다.
아래의 코드를
Map<String, String> map = new HashMap<>();
map.put("1","A");
map.put("2","B");
map.put("3","C");
아래와 같이 Wrapping 하는 것을 얘기한다고 한다.
public class GameRanking P
private Map<String, String> ranks;
public GameRanking(Map<String, String> ranks) {
this.ranks = ranks;
}
}
일급 컬렉션?
컬렉션을 Wrapping하면서, 그 외 다른 멤버 변수가 없는 상태를 일급 컬렉션이라 한단다.. 근데.. 이걸 왜 할까?
다음과 같은 조건의 로또 복권 게임을 만든다고 가정
public class LottoService {
private static final int LOTTO_NUMBERS_SIZE = 6;
public void createLottoNumber() {
List<Long> lottoNumbers = createNonDuplicateNumbers();
validateSize(lottoNumbers);
validateDuplicate(lottoNumbers);
...
}
private void validateSize(List<Long> lottoNumbers) {
// 6개의 번호 유효성 검사
}
}
private void validateDuplicate(List<Long> lottoNumbers) {
// 중복 검증
서비스 메서드에서 비즈니스 로직을 처리할 경우 다음과 같은 큰 문제가 있다.
로또 번호가 필요한 모든 장소에서 검증 로직이 들어가야만 한다.
- List으로 된 데이터는 모두 검증이 필요할까?
- 모든 코드와 도메인을 알고 있지 않다면 어떻게 이 검증로직이 필요한지 알 수 있을까?
해당 조건으로만 생성할 수 있는 자료구조를 만들면 문제가 해결된다.
그렇다면 유효성 검사의 내용을 자료구조 생성과 통합할 순 없을까?
이러한 것이 가능한 클래스를 '일급 컬렉션'이라고 부른다고 한다.
public class LottoTicket {
private static final int LOTTO_NUMBERS_SIZE = 6;
private final List<Long> lottoNumbers;
public LottoTicket(List<Long> lottoNumbers) {
validateSize(lottoNumbers); // 6개
validateDuplicate(lottoNumbers); // 중복되지 않은 숫자
this.lottoNumbers = lottoNumbers;
}
private void validateSize(List<Long> lottoNumbers) {
// 6개의 번호 유효성 검사
}
}
private void validateDuplicate(List<Long> lottoNumbers) {
// 중복 검증
이제 로또 번호가 필요한 모든 로직은 이 일급 컬렉션만 있으면 된다.
public class LotterService {
public void createLottoNumber() { // 필요한 로직은 모두 LottoTicket으로
LottoTicket lottoTicket = new LottoTicket(createNonDuplicateNumbers());
// 로직 ..
비즈니스에 종속적인 자료구조가 만들어져, 이후 발생할 문제가 최소화되었다.
일급 컬렉션은 컬렉션의 불변을 보장한다.
java의 final은 정확히는 불변을 만들어주는 것이 아니라, 재할당만 금지한다.
불변 객체가 왜 중요할까?
요즘같이 소프트웨어 규모가 커지고 있는 상황에서 불변 객체는 아주 중요하다고 한다.
각 객체들이 절대 값이 바뀌지 않는다는게 보장되면 그만큼 코드를 이해하고 수정하는데 사이드 이펙트최소화 되기 때문
java에서는 final로 그 문제를 해결할 수 없기 때문에 일급 컬렉션과 래퍼 클래스 등의 방법으로 해결해야만 한다.
컬렉션의 값을 변경할 수 있는 메서드가 없는 컬렉션을 만들면? -> 불변 컬렉션 생성
public class Orders {
private final List<Order> orders;
public Orders(List<Order> orders) {
this.orders = orders;
}
public long getAmountSum() {
return orders.stream()
.mapToLong(Order::getAmount)
.sum();
}
}
이 클래스는 생성자와 getAmountSum() 외에 다른 메서드가 없다.
즉, 새로 만들거나 값을 가져오는 것 뿐
List라는 컬렉션에 접근할 수 있는 방법도 없다. -> 값 변경 / 추가가 제한된다.
일급 컬렉션 사용을 통해 불변 컬렉션 만드는 것이 가능하다.
값과 로직이 함께 존재한다.
여러 Pay들이 모여있고, 이 중 NaverPay 금액의 합이 필요하다고 가정
@DisplayName("로직이 밖에 있는 상태")
@Test
public void 로직이_밖에_있는_상태() {
List<Pay> pays = Arrays.asList(
new Pay(NAVER_PAY, 1000), // 값은 여기에
new Pay(NAVER_PAY, 1500),
new Pay(KAKAO_PAY, 1200),
new Pay(TOSS, 3000L)_;
Long naverPaySum = pays.stream() // 계산은 여기에서?
.filter(pay -> pay.getPayType().equals(NAVER_PAY))
.mapToLong(Pay::getAmount)
.sum();
assertThat(naverPaySum).isEqualTo(2500);
문제점
결국 pays라는 컬렉션과 계산 로직은 서로 관계가 있는데, 이를 코드로 표현이 안 된다.
Pay타입의 상태에 따라 지정된 메서드에서만 계산되길 원하는데, 현재 상태로는 강제할 수 있는 수단이 없다.
지금은 Pay타입의 List라면 사용될 수 있기 때문에 히스토리를 모른다면 실수할 여지가 많다.
결국 네이버페이 총 금액을 뽑으려면 이렇게 해야한다는 계산식을 컬렉션과 함께 두어야 한다.
만약 네이버페이 외 카카오페이의 총금액도 필요하다면 더더욱 코드가 흩어질 확률이 높다.
public class PayGroups {
private List<Pay> pays;
public PayGroups(List<Pay> pays) {
this.pays = pays;
}
public Long getNaverPaySum() {
return pays.stream()
.filter(pay -> PayType.isNaverPay(pay.getPayType()))
.mapToLong(Pay::getAmount)
.sum();
}
}
다른 결제 수단들의 합이 필요하다면 람다식으로 리팩토링 가능하다고 한다.
public class PayGroups {
private List<Pay> pays;
public PayGroups(List<Pay> pays) {
this.pays = pays;
}
public Long getNaverPaySum() {
return getFilteredPays(pay -> PayType.isNaverPay(pay.getPayType()));
}
public Long getKaKaoPaySum() {
return getFilteredPays(pay -> PayType.isKaKaoPay(pay.getPayType()));
}
public Long getFilteredPays(Predicate<Pay> predicate) {
return pays.stream()
.filter(predicate)
.mapToLong(Pay::getAmount)
.sum();
}
}
PayGroups라는 일급 컬렉션 생성을 통해 상태와 로직이 한 곳에서 관리 가능하다.
@DisplayName("로직이 밖에 있는 상태")
@Test
public void 로직이_밖에_있는_상태() {
List<Pay> pays = Arrays.asList(
new Pay(NAVER_PAY, 1000),
new Pay(NAVER_PAY, 1500),
new Pay(KAKAO_PAY, 1200),
new Pay(TOSS, 3000L)_;
PayGroups payGroups = new PayGroups(pays);
Long naverPaySum = payGroups.getNaverPaySum();
assertThat(naverPaySum).isEqualTo(2500);
마지막으로 컬렉션에 이름을 붙일 수 있다고 한다.
같은 Pay들의 모임이지만 네이버페이의 List와 카카오페이의 List는 다르다.
이 둘을 구분하려면 어떻게 해야 할까?
흔한 방법 : 변수명으로 구분
@Test
public void 컬렉션을_변수명으로() {
List<Pay> naverPays = createNaverPays();
List<Pay> kakaoPays = createKakaoPays();
}
검색이 어렵다.
명확한 표현 불가능
- 변수명에 불과하기 때문에 의미를 부여하기 어렵다.
- 의사소통시 보편적 언어로 사용하기가 어려움을 의미한다.
- 중요한 값임에도 이를 표현할 명확한 단어가 없다.
네이버 페이 그룹과 카카오 페이 그룹 각각 일급 컬렉션으로 만든다.
-> 컬렉션 기반 용어사용과 검색
@Test
public void 컬렉션을_변수명으로() {
List<Pay> naverPays = new NaverPays(createNaverPays());
List<Pay> kakaoPays = new KakaoPays(createKakaoPays());
}
사용될 표현을 이제 이 컬렉션에 맞추면 된다고 한다.
검색 역시 컬렉션 클래스를 검색하면 모든 사용 코드를 찾아낼 수 있다.
객체지향 코드로 가기 위해 꼭 익혀둬야 할 방법 중 하나라고 한다..
열심히 배우자!