티스토리 뷰

일급 컬렉션

 컬렉션은 wrapping 하면서 컬렉션 외의 다른 필드를 가지고 있지 않은 클래스를 일급 컬렉션 이라고 한다. 이번 여덟번째 지침에서는 도메인 클래스를 컬렉션으로 감싸 처리하는 경우, 이를 일급 컬렉션으로 구현하라는 가이드를 제시한다.

숨은 의미

 컬렉션은 '무리', '모음' 이라는 의미를 갖는다. 클래스 인스턴스들을 컬렉션 자료구조로 감싸게 되면 구조로부터 도메인 로직을 얻을 수 있다. 인스턴스들의 집합에서 특정 인스턴스를 찾아내거나, 정렬을 할 수도 있고, 특정한 패턴으로 자료구조의 내용을 변형하는 것도 가능하다.

 

 일급컬렉션은 인스턴스의 집합을 '복수형 클래스'로 정의함으로써 단수형 클래스가 가질 수 없는 비즈니스 로직을 구현할 수 있도록 도와주는 도메인 설계라고 볼 수 있다.

예시

 고객이 자동차할부 대출의 상담을 받는다고 가정할 때, 회사의 정책이 다음과 같이

고객에게는 오직 한 대의 차량에 대해서만 대출해 줄 수 있다.
H사의 차량에 대해서만 대출이 가능하다.

라고 운영중이라고 가정해보자. 보통의 경우 서비스레이어에 로직을 녹여 다음과 같이 구현할 것이다.

public class AutoLoanAccountService {

    private static final int AUTO_LOAN_CAR_COUNT = 1;
    private static final String AUTO_LOAN_AVAILABLE_COMPANY = "H";

    public void registerConsulation(AutoLoanConsulationRegisterDto registerDto) {
        validateCount(registerDto.getCarList());
        validateCompany(registerDto.getCarList());
        // ...
    }

    private void validateCount(List<Car> carList) {
        if (carList.size() != AUTO_LOAN_CAR_COUNT) {
            throw new IllegalArgumentException("오직 " + AUTO_LOAN_CAR_COUNT + " 대의 자동차만 등록이 가능합니다.");
        }
    }

    private void validateCompany(List<Car> carList) {
        for (Car car : carList) {
            // 제조사 검증로직 ...
        }
    }
}

 위 경우 외에 자동차의 정보를 변경하거나, 다른 기타 로직에서도 이와 같은 로직을 구현할 것이다. 문제는 회사의 정책이 다음과 같이 변동될 수 있다는 점이다.

고객에게는 두 대의 차량구입자금을 대출해 줄 수 있다.
H사와 K사의 차량에 대해서 대출이 가능하다.
서로 다른 종류의 차량만 가능하다.

 이와 같이 비즈니스 요건의 변동이 일어날 때, 요건의 변동이 '차량' 이라는 객체의 내부에서 관리할 수 있는 책임이 아니라 '차량들' 이라는 클래스의 집합에서 해결할 수 있는 책임이라면 앞서 제시한 코드에서는 동일한 부분의 로직을 모두 다 관리해 주어야 한다.

 

 이 때, 다음과 같이 일급컬렉션을 정의하면 '차량들' 이라는 집합에 관한 도메인 로직을 단일 클래스에서 관리할 수 있다.

public class AutoLoanCars {

    private static final int AUTO_LOAN_CARS_MAX_COUNT = 2;
    private static final int AUTO_LOAN_CARS_MIN_COUNT = 1;

    private List<Car> cars = new ArrayList<>();

    public AutoLoanCars(List<Car> cars) {
        validateSize(cars);
        validateDupCarType(cars);
        validateCompany(cars);
        this.cars = cars;
    }

    private void validateSize(List<Car> cars) {
        if (cars.size() < AUTO_LOAN_CARS_MIN_COUNT) {
            throw new IllegalArgumentException("최소 " + AUTO_LOAN_CARS_MIN_COUNT + " 대의 차량은 입력해야 합니다.");
        }
        if (cars.size() > AUTO_LOAN_CARS_MAX_COUNT) {
            throw new IllegalArgumentException("최대 " + AUTO_LOAN_CARS_MAX_COUNT + " 대의 차량을 입력할 수 있습니다.");
        }
    }

    private void validateDupCarType(List<Car> cars) {
        // ... 차량종류 중복체크
    }
    
    private void validateCompany(List<Car> carList) {
        for (Car car : carList) {
            // 제조사 검증로직 ...
        }
    }
}
public class AutoLoanAccountService {

    public void registerConsulation(AutoLoanConsulationRegisterDto registerDto) {
        AutoLoanCars autoLoanCars = new AutoLoanCars(registerDto.getCarList());
        // ...
    }
}

 위 코드는 AutoLoanCars 이라는 일급컬렉션 클래스를 새로 정의하고, Car 클래스의 집합이 생성자의 인자로 넘겨질 때 초기화 단계에서 검증처리를 하도록 구현하고 있다. 비즈니스 로직이 도메인 클래스 내부에 구현되면서 자연스럽게 Service 클래스도 가벼워졌음을 확인할 수 있다. 이제 자동차할부대출 대상물건에 대한 요구사항이 변경되어도 보다 쉽게 대응할 수가 있을 것이다.

다양한 종류의 컬렉션을 Wrapping 할 수 있다

 일급컬렉션이라고 하면 보통 List를 감싼다고 떠올리는 경우가 많은데 Map, Stack 과 같이 어떤 인스턴스들의 '집합' 이라는 개념을 갖는 모든 클래스를 다 일급컬렉션으로 구현할 수 있다. 중요한 것은 집합에서 얻을 수 있는 비즈니스적 의미가 무엇이냐에 초점을 두는 것이다. 찾아낸 의미에 따라 가장 활용도가 높은 자료구조를 선택하면 된다.

컬렉션에 업무적 의미를 갖는 이름을 부여할 수 있다.

 위 코드에서 Car 인스턴스의 집합을 AutoLoanCars 라는 집합으로 표현했다. 이는 복수의 자동차를 '자동차대출과 관련된 차량들' 이라는 의미로 묶었다는 것을 의미한다. Car 인스턴스의 집합은 다른 목적으로도 묶을 수 있다. 가령 A 영업사원의 판매차량 리스트 와 같은 형태이다.

List<Car> autoLoanCars;
List<Car> salesmanCars;
AutoLoanCars autoLoanCars;
SalesmanCars salesmanCars;

 위 코드보다 아래 코드가 더욱 객체지향적이다. 클래스 타입을 명시해 줌으로써, 비즈니스적 의미를 표현할 수 있고, 클래스를 사용하는 코드에 시그니처로 제약을 줄 수 있다. 코드가 더욱 명확한 비즈니스적 표현을 할 수 있는 것이다.

Iterable 인터페이스를 구현해보자

 일급컬렉션은 컬렉션을 한 번 포장하고 있기 때문에, 인스턴스 요소에 접근하고자 할 때 다음과 같이 어색한 코드를 작성할 수 있다.

    public void sampleService(AutoLoanCars autoLoanCars) {
        for (Car car : autoLoanCars.getCars()) {
            // Car 인스턴스 처리로직
        }
    }

 AutoLoanCars 는 이미 인스턴스의 집합을 표현하고 있다. 그런데 위 코드에서는 집합에서 다시 집합을 꺼내와 내부 요소에 접근한다. 이런 경우에 AutoLoanCars 클래스에 다음과 같이 Iterable 인터페이스를 구현해 줄 수 있다.

public class AutoLoanCars implements Iterable<Car> {

    private List<Car> cars = new ArrayList<>();

    @Override
    public Iterator<Car> iterator() {
        return cars.iterator();
    }
}
    public void sampleService(AutoLoanCars autoLoanCars) {
        for (Car car : autoLoanCars) {
            // Car 인스턴스 처리로직
        }
    }

 Iterable 인터페이스를 구현함으로써 AutoLoanCars 는 '순회가능한' 특성을 표현하게 된다. 코드의 흐름도 보다 자연스럽고, 앞서 작성한 코드와 비교했을 때 내부 필드에 직접 접근할 수 있는 getter 를 제거할 수 있다는 점에서 안전한 설계라고 생각할 수 있겠다.

마치며

 일급컬렉션은 '복수형 클래스' 를 표현하기 위한 좋은 방법을 제시한다. 단일 인스턴스가 집합을 이루면서 가질 수 있는  비즈니스적 의미에 대해 고민해보면 더욱 다채로운 설계를 할 수 있을 것이다.

댓글
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday