Kim-Baek 개발자 이야기

일급 컬렉션이란? 본문

개발/java basic

일급 컬렉션이란?

김백개발자 2024. 11. 13. 13:09

일급 컬렉션(First-Class Collection)은 소프트웨어 설계에서 컬렉션을 별도의 클래스로 감싸서 관리하는 패턴을 말합니다. 이 개념은 컬렉션(List, Set, 등)을 단순히 도메인 객체의 속성으로 사용하는 대신, 컬렉션 자체를 하나의 일급 객체로 취급하여 도메인 로직을 캡슐화하고 책임을 분리하는 데 목적이 있습니다.

일급 컬렉션의 정의
일급 컬렉션은 다음과 같은 특징을 가집니다:

1. 단일 컬렉션 포장: 하나의 도메인 컬렉션만을 포함합니다.
2. 불변성 유지: 컬렉션의 불변성을 보장하여 외부에서 직접 수정할 수 없게 합니다.
3. 비즈니스 로직 포함 가능: 컬렉션 자체에 관련된 도메인 로직을 포함할 수 있습니다.
4. 도메인 용어 사용: 컬렉션을 도메인 용어에 맞춰 네이밍하여 코드의 가독성을 높입니다.


왜 일급 컬렉션을 사용하는가?
일반적으로 도메인 객체가 단순히 컬렉션을 포함할 때 발생할 수 있는 문제점을 해결하기 위해 사용됩니다:

1. 책임 분리: 도메인 객체가 컬렉션과 관련된 로직을 직접 다루지 않고, 컬렉션 전용 클래스로 책임을 분리합니다.
2. 캡슐화 강화: 컬렉션의 내부 구조를 숨기고, 불변성을 유지함으로써 예기치 않은 변경을 방지합니다.
3. 가독성 및 유지보수성 향상: 도메인 용어에 맞는 클래스를 사용하여 코드의 의도를 명확히 하고, 유지보수를 용이하게 합니다.
4. 도메인 규칙 강제: 컬렉션에 추가되거나 제거될 때 특정 규칙을 강제할 수 있습니다.


일급 컬렉션의 구현 예시
예시 시나리오
회원(Member)과 그 회원이 소유한 주문(Order)들을 관리하는 상황을 가정해보겠습니다. 각 회원은 여러 개의 주문을 가질 수 있습니다.

단순한 구현 (일급 컬렉션 미사용)

public class Member {
    private Long id;
    private String name;
    private List<Order> orders = new ArrayList<>();

    // 생성자, getter, setter 생략

    public void addOrder(Order order) {
        this.orders.add(order);
    }

    public List<Order> getOrders() {
        return Collections.unmodifiableList(orders);
    }
}

이 경우 Member 클래스가 orders 컬렉션에 대한 모든 책임을 지게 됩니다.

일급 컬렉션 사용

// 주문(Order) 클래스
public class Order {
    private Long id;
    private String productName;
    private int quantity;

    // 생성자, getter, setter 생략
}

// 주문 컬렉션 일급 컬렉션 클래스
public class Orders {
    private final List<Order> orders;

    public Orders(List<Order> orders) {
        // 컬렉션 복사 및 불변성 유지
        this.orders = Collections.unmodifiableList(new ArrayList<>(orders));
    }

    public List<Order> getOrders() {
        return orders;
    }

    public void addOrder(Order order) {
        // 새로운 리스트를 생성하여 불변성을 유지하는 방식
        List<Order> updatedOrders = new ArrayList<>(this.orders);
        updatedOrders.add(order);
        // 새로운 Orders 인스턴스를 반환하거나, Builder 패턴 등을 사용할 수 있음
    }

    // 도메인 로직 추가 가능
    public int getTotalQuantity() {
        return orders.stream().mapToInt(Order::getQuantity).sum();
    }

    // equals, hashCode 등의 메서드 오버라이드 가능
}

// 회원(Member) 클래스
public class Member {
    private Long id;
    private String name;
    private Orders orders;

    public Member(Long id, String name, Orders orders) {
        this.id = id;
        this.name = name;
        this.orders = orders;
    }

    // 생성자, getter, setter 생략

    public void addOrder(Order order) {
        this.orders.addOrder(order);
    }

    public Orders getOrders() {
        return orders;
    }
}

이와 같이 Orders 클래스가 컬렉션을 감싸면서 관련 로직을 담당하게 됩니다.

일급 컬렉션의 장점
응집도 향상: 컬렉션과 관련된 모든 로직을 한 곳에 모아 응집도를 높입니다.
도메인 규칙 구현 용이: 컬렉션에 대한 특정 비즈니스 규칙을 쉽게 구현하고 강제할 수 있습니다.
불변성 보장: 외부에서 컬렉션을 수정하지 못하게 막아 데이터 무결성을 유지할 수 있습니다.
테스트 용이성: 컬렉션 관련 로직을 별도로 테스트할 수 있어 테스트가 용이해집니다.
가독성 향상: 도메인 용어를 사용하여 코드의 의도를 명확히 합니다.

일급 컬렉션의 단점 및 고려사항
복잡도 증가: 간단한 컬렉션을 감싸기 위해 별도의 클래스를 작성해야 하므로 코드가 다소 복잡해질 수 있습니다.
과도한 응집: 모든 컬렉션을 일급 컬렉션으로 만들려 하면 오히려 코드가 복잡해지고 가독성이 떨어질 수 있습니다.
추가적인 유지보수 비용: 새로운 클래스가 추가되므로 유지보수 비용이 증가할 수 있습니다.
따라서, 모든 컬렉션에 대해 일급 컬렉션을 적용하는 것보다는 도메인 로직이 복잡하거나 컬렉션에 대한 특별한 규칙이 필요한 경우에 한하여 적용하는 것이 좋습니다.

일급 컬렉션 적용 시 고려사항
필요성 판단: 컬렉션에 대한 특별한 비즈니스 규칙이나 로직이 존재하는지 평가합니다.
적절한 네이밍: 컬렉션을 감싸는 클래스의 이름은 도메인 용어에 맞춰 명확하게 지정합니다.
불변성 유지: 컬렉션의 불변성을 유지하여 데이터의 일관성을 보장합니다.
적절한 메서드 제공: 필요한 컬렉션 조작 메서드만을 제공하고, 불필요한 메서드는 최소화합니다.


추가 예시: Java의 Value Object로 활용
일급 컬렉션은 Value Object의 일종으로 볼 수 있습니다. Value Object는 변경 불가능하며, 동일성보다는 동등성(값의 동일성)에 집중하는 객체입니다. 따라서, 일급 컬렉션을 Value Object로 설계할 때는 다음을 고려해야 합니다:

불변성: 필드를 final로 선언하고, 외부에서 변경할 수 없도록 합니다.
동등성 구현: equals와 hashCode 메서드를 오버라이드하여 값 기반의 동등성을 구현합니다.
직렬화 지원: 필요에 따라 Serializable 인터페이스를 구현하여 직렬화를 지원할 수 있습니다.

 

public class Orders {
    private final List<Order> orders;

    public Orders(List<Order> orders) {
        this.orders = Collections.unmodifiableList(new ArrayList<>(orders));
    }

    public List<Order> getOrders() {
        return orders;
    }

    // equals and hashCode based on 'orders' list
    @Override
    public boolean equals(Object o) {
        if (this == o) return true;
        if (o == null || getClass() != o.getClass()) return false;
        
        Orders orders1 = (Orders) o;
        return orders.equals(orders1.orders);
    }

    @Override
    public int hashCode() {
        return orders.hashCode();
    }
}
반응형
Comments