본문 바로가기

JAVA

일급 컬렉션 (First Class Collection) 을 사용하자

1. 일급 컬렉션이란?


일급 컬렉션 (First Class Collection) 은 소트웍스 앤솔로지의 객체지향 생활체조 규칙 8. 일급 콜렉션 사용에서 언급되었다.

규칙 8 : 일급 콜렉션 사용

이 규칙의 적용은 간단하다. 콜렉션을 포함한 클래스는 반드시 다른 멤버 변수가 없어야 한다. 
각 콜렉션은 그자체로 포장돼어 있으므로 이제 콜렉션과 관련된 동작은 근거지가 마련된 셈이다.

필터가 이 새 클래스의 일부가 됨을 알 수 있다. 필터는 또한 스스로 함수 객체가 될 수 없다.
새 클래스는 두 그룹을 같이 묶는다든가 그룹의 각 원소에 규칙을 적용하는 등의 동작을 처리할 수 있다.

이는 인스턴스 변수에 대한 규칙의 확실한 확장이지만 그 자체를 위해서도 중요하다. 
콜렉션은 실로 매우 유용한 원시 타입이다. 많은 동작이 있지만 후임 프로그래머나 유지보수 담당자에 의미적 의도나 단초는 거의 없다.

 

요약하자면 Collection을 Wrapping 하면서, Wrapping 한 Collection 외 다른 멤버 변수가 없는 상태를 일급 컬렉션이라고 한다. 

이를 통해 우리는 다음과 같은 이점을 가지게 된다.

  1. 비즈니스에 종속적인 자료구조
  2. 상태와 행위를 한 곳에서 관리
  3. Collection의 불변성 보장
  4. 이름이 있는 컬렉션

 

2. 비즈니스에 종속적인 자료구조


예를들어 해당 서비스를 사용하는 사용자에게 쿠폰 서비스를 한다고 가정하겠습니다.

쿠폰 서비스는 다음과 같은 조건이 있다.

  • 사용자는 10개의 쿠폰만 가질수 있다.
  • 사용자는 중복되는 쿠폰을 가질수 없다.

위 두조건을 만족시키는 CouponService 서비스 메소드를 작성해보면

public class Coupon {
    private String name;
    private String discountRate;
    
    public String getName() {
        return name;
    }

    public String getDiscountRate() {
        return discountRate;
    }
}

public class CouponService {
    private static final int COUPON_MAX_SIZE = 10;

    public void createCouponList(){
        List<Coupon> coupons = createNonDuplicateCoupons();
        validateCouponsSize(coupons);
        validateCouponsDuplicate(coupons);
    }

    private void validateCouponsSize(List<Coupon> coupons) {
        if (coupons.size() > COUPON_MAX_SIZE){
            throw new IllegalArgumentException("쿠폰은 10개까지만 가질수 있습니다.");
        }
    }

    private void validateCouponsDuplicate(List<Coupon> coupons) {
        Set<Coupon> nonDuplicateCoupon = new HashSet<>();
        if (nonDuplicateCoupon.size() != coupons.size()){
            throw new IllegalArgumentException("중복되는 쿠폰을 가질수 없습니다.");
        }
    }

    private List<Coupon> createNonDuplicateCoupons() {
        return new ArrayList<Coupon>();
    }
}

 

위와 같이 서비스 메소드에서 비즈니스 로직을 수행하였다.

하지만 이렇게 코드를 작성하는 경우 문제점이 발생한다.

  • List<Coupon> 객체로된 데이터는 모두 검증 로직을 사용해야 하는가?
  • 신규 입사자가 어떻게 검증로직이 필요한지 알 수 있는가?

내가 필요한 조건을 만족하는 자료구조를 만들수 없을까?

 

이러한 의문점에서 나온 개념이 바로 1급 컬렉션이다. 

즉, 우리의 제약조건인 10개의 쿠폰만 담아둘 수 있으며, 중복된 쿠폰을 담지 못하는 자료구조를 직접 만들어서 사용하는 것이다.

한번 일급 컬렉션 클래스를 만들어보자.

public class CouponBasket {
    private static final int COUPON_MAX_SIZE = 10;

    private final List<Coupon> coupons;

    public CouponBasket(List<Coupon> coupons) {
        validateCouponsSize(coupons);
        validateCouponsDuplicate(coupons);
        this.coupons = coupons;
    }

    private void validateCouponsSize(List<Coupon> coupons) {
        if (coupons.size() > COUPON_MAX_SIZE){
            throw new IllegalArgumentException("쿠폰은 10개까지만 가질수 있습니다.");
        }
    }

    private void validateCouponsDuplicate(List<Coupon> coupons) {
        Set<Coupon> nonDuplicateCoupon = new HashSet<>();
        if (nonDuplicateCoupon.size() != coupons.size()){
            throw new IllegalArgumentException("중복되는 쿠폰을 가질수 없습니다.");
        }
    }
}

만든 일급 컬랙션 객체를 사용해서 비즈니스 로직에 사용하면

public class CouponService {
    private static final int COUPON_MAX_SIZE = 10;

    public void createCouponList(){
        CouponBasket couponBasket = new CouponBasket(createNonDuplicateCoupons());
    }

    private List<Coupon> createNonDuplicateCoupons() {
        return new ArrayList<Coupon>();
    }
}

다음과 같이 비즈니스에 종속적인 자료구조가 만들어 지면서 발생할 문제가 최소화 되었다.

 

3. 상태와 행위를 한 곳에서 관리


그렇다면 List<Coupon> coupons 의 원소중에서 내가 원하는 쿠폰을 조회하기 위해서 find 메서드를 만든다고 가정해 보면 

만약 비즈니스 로직에서 find 행위를 수행하는 메소드를 작성하면, List<Coupon> 을 사용하는 모든 영역에서 중복되는 find 메소드를 작성해야 할 것이다.

이런것을 막아주는 역할 또한 일급 컬렉션이 수행할 수 있다. 

즉, 멤버변수의 상태와 필요한 로직을 수행하는 행위일급 컬렉션 클래스에서 한번에 관리하는 것이다.

위에서 작성한 일급 컬렉션 객체에 find 메소드를 추가해보자

public class CouponBasket {
    private static final int COUPON_MAX_SIZE = 10;

    private final List<Coupon> coupons;

    public CouponBasket(List<Coupon> coupons) {
        validateCouponsSize(coupons);
        validateCouponsDuplicate(coupons);
        this.coupons = coupons;
    }

    private void validateCouponsSize(List<Coupon> coupons) {
        if (coupons.size() > COUPON_MAX_SIZE){
            throw new IllegalArgumentException("쿠폰은 10개까지만 가질수 있습니다.");
        }
    }

    private void validateCouponsDuplicate(List<Coupon> coupons) {
        Set<Coupon> nonDuplicateCoupon = new HashSet<>();
        if (nonDuplicateCoupon.size() != coupons.size()){
            throw new IllegalArgumentException("중복되는 쿠폰을 가질수 없습니다.");
        }
    }

    public Coupon find(String name){
        return coupons.stream()
                .filter(coupon -> coupon.getName().equals(name))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("일치하는 쿠폰이 없습니다."));
    }
}

 

4. Collection 의 불변성 보장


일급 컬렉션 클래스는 상태와 행위를 한곳에서 처리하기 때문에 가변객체인 Collection 객체를 불변으로 만들 수 있다.

객체를 불변으로 만드는 것에대해서는 이전에 올렸던 포스팅을 참고하길 바란다.

불변객체(Immutable Object)를 사용해야하는 이유

 

불변객체(Immutable Object)를 사용해야하는 이유

1. 불변객체(Immutable Object)란? 불변객체는 말그대로 변하지않는 객체로 객체가 생성된후 내부 상태가 변하지 않는 객체를 의미한다. 불변객체는 Setter 메소드를 제공하지 않으며, 내부상태를 제공

dev-cool.tistory.com

 

일급 컬렉션 클래스를 불변 객체로 만들어 보자.

public class CouponBasket {
    private static final int COUPON_MAX_SIZE = 10;

    //변수를 final 로 선언
    private final List<Coupon> coupons;

    public CouponBasket(List<Coupon> coupons) {
        validateCouponsSize(coupons);
        validateCouponsDuplicate(coupons);
        //방어적 복사
        this.coupons = new ArrayList<>(coupons);
    }

    public List<Coupon> getCoupons() {
        //UnmodifiableList 로 객체 복사
        return Collections.unmodifiableList(coupons);
    }

    private void validateCouponsSize(List<Coupon> coupons) {
        if (coupons.size() > COUPON_MAX_SIZE){
            throw new IllegalArgumentException("쿠폰은 10개까지만 가질수 있습니다.");
        }
    }

    private void validateCouponsDuplicate(List<Coupon> coupons) {
        Set<Coupon> nonDuplicateCoupon = new HashSet<>();
        if (nonDuplicateCoupon.size() != coupons.size()){
            throw new IllegalArgumentException("중복되는 쿠폰을 가질수 없습니다.");
        }
    }

    public Coupon find(String name){
        return coupons.stream()
                .filter(coupon -> coupon.getName().equals(name))
                .findFirst()
                .orElseThrow(() -> new IllegalArgumentException("일치하는 쿠폰이 없습니다."));
    }
}

 

참조 : https://jojoldu.tistory.com/412

불변성 부분은 저의 의견을 조금더 넣어서 작성하였습니다.

 

일급 컬렉션 (First Class Collection)의 소개와 써야할 이유

최근 클린코드 & TDD 강의의 리뷰어로 참가하면서 많은 분들이 공통적으로 어려워 하는 개념 한가지를 발견하게 되었습니다. 바로 일급 컬렉션인데요. 왜 객체지향적으로, 리팩토링하기 쉬운 코

jojoldu.tistory.com