✅일급 컬렉션이란?
Collection을 Wrapping하면서, 그 외 다른 멤버 변수가 없는 상태
public class Orders {
private final List<Order> orders; // Set, Map
public Orders(List<Order> orders, int maxOrderCount){
if(orders.size() > maxOrderCount){
throw new RuntimeException();
}
this.orders = orders;
// this.orders = Collections.unmodifiableList(orders);
}
}
일급 컬렉션의 장점
- 비즈니스에 종속적인 자료구조
- 특정 비즈니스 로직이나 규칙을 반영하도록 설계된 자료구조
- Collection의 불변성 보장
- 상태와 행위를 한 곳에서 관리
- 이름이 있는 컬렉션
1. 비즈니스에 종속적인 자료구조
로또 번호 생성기
public class LottoTicket {
privte static final int LOTTO_NUMBER_SIZE = 6;
private final List<Integer> lottoNumbers;
public LottoTicket(){
validateSize(lottoNumbers);
validateDuplicate(lottoNumbers);
this.lottoNumbers = lottoNumbers;
}
}
- 리스트(자료구조)의 크기는 6이다.
- 중복된 값이 없어야 한다.
Service 레이어에 검증 로직이 있는 경우 문제점
- 로또 번호 생성은 비즈니스 로직이기 때문에 service 레이어에 검증 로직이 들어갈 경우 응집도가 낮아지고 중복코드가 생길 가능성이 높아짐.
- List<Long> 타입에 대한 검증이기 때문에 어느 경우(로또 번호 생성)에 검증 로직이 필요한지에 대한 도메인 지식이 필요함.
TMS 주문 등록 - 바코드 검증
public class Barcodes {
private final static int DEFAULT_MAX_BOX_COUNT = 3;
private final List<String> barcodes;
public Barcodes(List<String> barcodes){
this(barcodes, DEFAULT_MAX_BOX_COUNT);
}
public Barcodes(List<String> barcodes, int maxBoxCount){
validateBarcodeCount(barcodes, maxBoxCount);
validateBarcodeNumberDuplicated(barcodes);
this.barcodes = Collections.unmodifiableList(barcodes);
}
}
- 고객 서비스에 등록된 최대 박스 수를 넘게 주문할 수 없다
- 중복된 바코드가 존재하면 안된다.
2. 불변
final 키워드는 변수 재할당을 불가능하도록 하지만, final로 선언된 Collection에 데이터 추가 가능.
final List<String> list = new ArrayList();
list.add("1"); // 데이터 추가 가능
list = new ArrayList(); // 재할당 불가, 컴파일 에러
일급 컬렉션 안에 데이터 추가 로직을 만들지 않으면 일급 컬렉션이 Wrapping하고 있는 컬렉션에 데이터 추가 불가.
public class Orders {
private final List<Order> orders;
public Orders(List<Order> orders, int maxOrderCount){
if(orders.size() > maxOrderCount){
throw new RuntimeException();
}
this.orders = Collections.unmodifiableList(orders);
}
}
3. 상태와 행위를 한곳에서 관리
예시 코드
List에 데이터를 담고 Service 또는 Util 클래스에서 필요한 로직 실행
// 값 (데이터)
List<Pay> pays = Arrays.asList(
new Pay(NAVER_PAY, 1000),
new Pay(NAVER_PAY, 1500),
new Pay(KAKAO_PAY, 1000),
new Pay(TOSS, 3000)
);
// 계산
Long naverPaySum = pays.stream()
.filter(pay -> pay.getPayType().equals(NAVER_PAY))
.mapToLong(Pay::getAmount)
.sum();
→ pays 라는 컬렉션과 계산 로직은 서로 관계가 있는데, 이를 코드로 표현이 안됨.
나는 아래와 같이 해석했다.
위의 코드를 읽어보면 “네이버페이 총액을 구하기”라는 것을 알 수 있다.
여기서 “컬렉션과 계산 로직이 서로 관계가 있는데, 코드로 표현이 안된다.”라는 말은 pays와 총액을 구하는 로직이 서로 관계가 있는데 클래스 내부에서 관리되는 것이 아니라 외부(service 또는 util)에서 관리되고 있다는 것을 의미한다.
즉, 코드 구조상으로 드러나지 않는다는 것을 의미한다.
데이터와 로직이 흩어져있을 경우
- 중복 코드
- 비즈니스 로직 누락 가능성 (계산로직)
- 타입만 일치하면 되기 때문에
GRASP 패턴 Information Expert(정보 전문가)
→ 데이터를 알고 있는 객체에게 그 데이터를 기반으로 한 책임을 준다.
일급 컬렉션을 구성해서 데이터와 로직이 서로 연관이 있다는 것을 코드 구조로 표현해줄 수 있음.
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()));
}
private Long getFilteredPays(Predicate<Pay> predicate) {
return pays.stream()
.filter(predicate)
.mapToLong(Pay::getAmount)
.sum();
}
}
TMS - 주문
public class Orders {
private final List<OrderTb> orders;
public Orders(List<OrderTb> orders) {
this.orders = Collections.unmodifiableList(orders); // 불변성 강화
}
public List<OrderTb> 새벽배송() {
return orders.stream()
.filter(order -> !order.getOrderNumber().startsWith("B"))
.filter(order -> order.getDeliveryType() == 0)
.collect(Collectors.toList());
}
public int 새벽배송_주문수() {
return 새벽배송().size();
}
public List<OrderTb> 당일배송() {
return orders.stream()
.filter(order -> !order.getOrderNumber().startsWith("B"))
.filter(order -> order.getDeliveryType() == 1)
.collect(Collectors.toList());
}
public int 당일배송_주문수() {
return 당일배송().size();
}
public List<OrderTb> 반품() {
return orders.stream()
.filter(order -> order.getOrderNumber().startsWith("B"))
.collect(Collectors.toList());
}
public int 반품_개수() {
return 반품().size();
}
public int 주문_취소_수(){
return orders.stream()
.filter(order -> order.getProgressLevel() == 2)
.collect(Collectors.toList())
.size();
}
public int 미출고_주문_수(){
return orders.stream()
.filter(order -> order.getProgressLevel() == 4)
.collect(Collectors.toList())
.size();
}
public int 배송완료_주문_수(){
return orders.stream()
.filter(order -> order.getProgressLevel() == 6)
.collect(Collectors.toList())
.size();
}
}
4. 이름이 있는 컬렉션
List<Pay> naverPays = createNaverPays();
List<Pay> kakaoPays = createKakaoPays();
- 검색의 어려움
- 변수명으로만 naverPays, kakaoPays를 검색해야 함
- → 오타, 팀별 컨벤션 등
- 명확한 표현 불가능
- 표현해줄 수 있는 방법은 변수명 뿐이기 때문에 명확한 의미 부여 어려움
일급 컬렉션을 타입으로 검색이 쉽고, 명확한 표현이 가능
NaverPays naverPays = new NaverPays(createNaverPays());
KakaoPays kakaoPays = new KakaoPays(createKakaoPays());
REFERENCE
https://jojoldu.tistory.com/412
일급 컬렉션 (First Class Collection)의 소개와 써야할 이유
최근 클린코드 & TDD 강의의 리뷰어로 참가하면서 많은 분들이 공통적으로 어려워 하는 개념 한가지를 발견하게 되었습니다. 바로 일급 컬렉션인데요. 왜 객체지향적으로, 리팩토링하기 쉬운 코
jojoldu.tistory.com