티스토리 뷰

java

일급 컬렉션

주다애 2024. 11. 23. 22:40

☝ 일급 컬렉션

일급 컬렉션이란 하나의 컬렉션을 감싸는 클래스를 만들고 해당 클래스에서 컬렉션과 관련된 비즈니스 로직을 관리하는 패턴이다. Wrapping된 컬렉션 이외의 다른 멤버 변수는 없어야한다.
public class Orders {
    private final List<Order> orders;

    public Orders(List<Order> orders) {
       this.orders = orders;
    }
}

여기서 Order List 컬렉션을 감싼 Orders 클래스가 일급 컬렉션이다.

 

왜 사용하는가?

1️⃣ 비즈니스에 종속적인 자료구조

일급 컬렉션에 비즈니스 로직을 작성하면 중복 제거, 안정성 향상, 유지보수성 증가 등의 이점을 얻을 수 있다.

 

Orders라는 일급 컬렉션이 Order 클래스를 관리한다고 해보자. 그리고 다음과 같은 시나리오가 있다.

1. 주문 데이터 관리 : Order는 개별 주문을 나타내고, Orders는 다수의 Order를 담는 컬렉션이ㅏㄷ.\

2. 비즈니스 규칙

  1. 주문 목록에는 반드시 한 개 이상의 주문이 있어야 한다.
  2. 주문 목록은 변경 불가하다.
  3. 전체 주문 금액을 계산하는 로직이 필요하다.
public class Order {
	private final long amount; // 주문 금액

	public Order(long amount) {
		if (amount <= 0) {
			throw new IllegalArgumentException("주문 금액은 0보다 커야 합니다.");
		}
		this.amount = amount;
	}

	public long getAmount() {
		return amount;
	}
}

기본적인 Order 클래스이다.

 

💡 먼저 Orders라는 일급 컬렉션을 사용하지 않고 비즈니스 로직을 구현해보자. 구현은 OrderService 클래스에서 한다.

public class OrderService {
    public void processOrders(List<Order> orders) {
        if (orders == null || orders.isEmpty()) {
            throw new IllegalArgumentException("주문은 최소 한 개 이상이어야 합니다.");
        }
        // 주문 처리 로직
        long totalAmount = calculateTotalAmount(orders);
        System.out.println("총 주문 금액: " + totalAmount);
    }

    private long calculateTotalAmount(List<Order> orders) {
        return orders.stream().mapToLong(Order::getAmount).sum();
    }
}

어떤 문제가 있을까?

  1. 중복 로직의 위험 : orders가 null인지, 비어있는지 확인하는 검증 로직은 주문 객체가 필요한 모든 곳에서 필요하다. 즉, 예외 처리 코드가 중복된다.
  2. 데이터 무결성 보장 어려움 : List<Order>가 매개변수로 사용되기 때문에 외부에서 데이터를 변경할 수 있다.
  3. 비즈니스 로직 분산 : calculateTotalAmount()같은 총 주문 금액 계산 비즈니스 로직은 Orders 클래스에 더 적합하다.

💡 이를 해결하기 위해 Orders라는 일급 컬렉션을 사용해보자

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

    public Orders(List<Order> orders) {
        if (orders == null || orders.isEmpty()) {
            throw new IllegalArgumentException("주문은 최소 한 개 이상이어야 합니다.");
        }
        this.orders = Collections.unmodifiableList(orders); // 불변 컬렉션
    }

    public long calculateTotalAmount() {
        return orders.stream().mapToLong(Order::getAmount).sum();
    }

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

이렇게 Orders 클래스에서 비즈니스 로직을 처리해줄 수 있다. 그렇게 되면 검증 로직이 Orders 생성자에 존재하므로 Orders 객체는 항상 유효한 상태를 가진다. 물론 검증 로직의 중복도 제거된다.

또한 내부에서 불변 컬렉션으로 만들어 주어서 객체의 불변성이 보장된다.

마지막으로 총 주문 금액 계산 같은 비즈니스 로직이 Orders 클래스 안에 존재하므로 코드의 응집도가 높아지고 OrderService 클래스의 책임도 줄어든다.

public void processOrders(Orders orders) {
    long totalAmount = orders.calculateTotalAmount();
    System.out.println("총 주문 금액: " + totalAmount);
}

이렇게 OrderService의 processOrders() 메소드가 한층 깔끔해졌다.

 

2️⃣ 컬렉션의 불변성 보장

일급 컬렉션은 wrapping된 컬렉션의 불변성을 보장한다.

 

단순히 final 키워드를 선언하는 것과는 다르다. final 키워드는 재할당을 금지하지만 한 번 선언하고 값을 변경하는 것은 가능하다. 즉, 불변성을 완전히 보장하지는 않는다.

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

	public Orders(List<Order> orders) {
		if (orders == null || orders.isEmpty()) {
			throw new IllegalArgumentException("주문은 최소 한 개 이상이어야 합니다.");
		}
		this.orders = Collections.unmodifiableList(orders); // 불변 컬렉션
	}

	public long calculateTotalAmount() {
		return orders.stream().mapToLong(Order::getAmount).sum();
	}

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

위의 코드에서 생성자와 getOrders() 메소드는 Collections.unmodifiableList() 를 사용해 불변 리스트로 변환하고 그 값을 반환함으로써 불변성을 보장하고 있다. 즉, Order객체는 생성 후 값을 변경할 수 없다.

 

코드로 예시를 보자

💡 먼저, Orders 일급 컬렉션을 사용하지 않은 코드이다.

public class FirstCollectionMain {
	public static void main(String[] args) {
		Order order1 = new Order(10);
		Order order2 = new Order(20);

		List<Order> orders = Arrays.asList(order1, order2);
        
		OrderService orderService = new OrderService();
		orderService.processOrders(orders);

        	try {
			orders.set(0, new Order(20)); // 맨 처음 order 객체 수정 시도
		} catch (Exception e) {
			System.out.println("데이터 수정 시도 실패: " + e.getMessage());
		}

		// 다시 서비스 메소드 호출
		orderService.processOrders(orders);
	}
}
  1. 생성한 order 객체들을 orders라는 List에 담고 orderService의 processOrders() 메소드의 인자로 담아 호출했다.
    그럼 맨 처음 processOrders(orders)는 총 주문 금액 : 30 이라고 출력된다.
  2. 그런데 그 이후 orders라는 List에 담긴 맨 처음 order 객체의 주문량을 20으로 수정했다. 즉, 외부에서 객체 값을 수정한 것이다. 그 이후 processOrders(orders)를 호출하면 총 주문 금액 : 40 이라고 출력된다. 객체 값이 변경된 것이다.

 

 

💡 이번엔 Orders라는 일급 컬렉션을 사용해보자

public class FirstCollectionMain {
	public static void main(String[] args) {
		Order order1 = new Order(10);
		Order order2 = new Order(20);

		Orders orders = new Orders(Arrays.asList(order1, order2));
        
		OrderService orderService = new OrderService();
		orderService.processOrders(orders);

		// 외부에서 orders 내부 데이터 수정 시도
		try {
			orders.getOrders().set(0, new Order(20)); // UnsupportedOperationException 발생
		} catch (UnsupportedOperationException e) {
			System.out.println("데이터 수정 시도 실패: " + e.getMessage());
		}

		// 다시 서비스 메소드 호출
		orderService.processOrders(orders);
	}
}

마찬가지로 맨 처음 order 객체를 수정하려고 했지만 이번에는 UnsupportedOperationException 예외가 발생한다.

결과를 보면

이렇게 맨 처음의 order 객체의 값이 수정되지 않은 것을 알 수 있다.

 

3️⃣ 객체의 상태와 동작을 한 곳에서 관리

일급 컬렉션은 객체의 상태와 동작을 한 곳에서 관리할 수 있게 해준다.

 

만약 Order 클래스의 멤버 변수에 주문 타입이 추가된다고 해보자

public class Order {
	private final long amount; // 주문 금액
	private final String type;

	public Order(long amount, String type) {
		if (amount <= 0) {
			throw new IllegalArgumentException("주문 금액은 0보다 커야 합니다.");
		}
		this.amount = amount;
		this.type = type;
	}

	public long getAmount() {
		return amount;
	}

	public String getType() {
		return type;
	}
}

 

💡 먼저, Orders 일급 컬렉션을 사용하지 않은 코드이다.

type이 "QUICK"인 order 객체의 합을 구하는 코드를 작성하고자 한다.

public static void main(String[] args) {
    Order order1 = new Order(10, "QUICK");
    Order order2 = new Order(20, "SLOW");

    List<Order> orders = Arrays.asList(order1, order2);

    long quickOrderAmountSum = orders.stream()
        .filter(order -> order.getType().equals("QUICK"))
        .mapToLong(Order::getAmount)
        .sum();

    System.out.println(quickOrderAmountSum);
}

List에 데이터를 담고, 합을 구하는 로직을 수행했다. 어떤 문제가 있을까?

  1. 로직의 분산 : 현재 "QUICK" 타입의 order 합계 계산이라는 비즈니스 로직이 main 클래스에 존재한다. 
  2. 상태와 동작의 분리 : List<Order>는 단순히 상태를 담고 있을 뿐, 데이터에 대한 동작은 외부에서 처리해야 한다.
  3. 새로운 요구사항 대응 어려움 : 만약 "SLOW" 타입의 order 합계를 계산하는 로직이라는 요구사항이 추가되거나 다른 타입이 추가되면 관련 로직을 다시 만들거나 일일이 수정해야 한다.

💡 이번엔 Orders라는 일급 컬렉션을 사용해보자

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

	public Orders(List<Order> orders) {
		if (orders == null || orders.isEmpty()) {
			throw new IllegalArgumentException("주문은 최소 한 개 이상이어야 합니다.");
		}
		this.orders = Collections.unmodifiableList(orders); // 불변 컬렉션
	}

	public long calculateTotalAmount() {
		return orders.stream().mapToLong(Order::getAmount).sum();
	}

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

	public long calculateAmountSumByType(String type) {
		return orders.stream()
			.filter(order -> order.getType().equals(type))
			.mapToLong(Order::getAmount)
			.sum();
	}
}

이렇게 calculateAmountSumByTpe(String Type)이라는 메소드를 만들었다.

public static void main(String[] args) {
    Order order1 = new Order(10, "QUICK");
    Order order2 = new Order(20, "SLOW");

    Orders orders = new Orders(Arrays.asList(order1, order2));

    // QUICK 주문 합계 계산
    long quickOrderAmountSum = orders.calculateAmountSumByType("QUICK");
    System.out.println("QUICK 주문 합계: " + quickOrderAmountSum);

    // SLOW 주문 합계 계산
    long slowOrderAmountSum = orders.calculateAmountSumByType("SLOW");
    System.out.println("SLOW 주문 합계: " + slowOrderAmountSum);
}

먼저, calculateAmountSumByTpe() 이라는 메소드 하나로 모든 타입의 order 합계를 구할 수 있게 되었다. 즉, 타입이 추가되어도 로직을 중복해서 작성할 필요가 없는 것이다.

그리고 상태와 동작을 한 곳에서 관리할 수 있게 되었다. Orders안에서 order 객체들을 List로 관리하고 관련 동작(행위)도 처리하고 있다. 외부에서 데이터 처리 작업을 이해하지 않아도 Orders 클래스의 메소드를 호출하기만 하면 작업이 수행되는 것이다.

 

일급 컬렉션을 사용하여 로직과 데이터를 한 곳에서 관리하게 된 것이다. 즉, 코드의 응집도가 높아진다.

 

일급 컬렉션을 객체지향적인 코드를 작성하기 위한 좋은 선택이라고 할 수 있다.

 

참고 자료

https://jojoldu.tistory.com/412

 

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

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

jojoldu.tistory.com

 

'java' 카테고리의 다른 글

동일성과 동등성  (0) 2024.12.03
얕은 복사와 깊은 복사  (0) 2024.11.27
자바의 Checked Exception VS Unchecked Exception  (1) 2024.11.21
인터페이스 vs 추상클래스  (0) 2024.05.16
상속과 메모리 구조  (0) 2023.12.21
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
«   2025/04   »
1 2 3 4 5
6 7 8 9 10 11 12
13 14 15 16 17 18 19
20 21 22 23 24 25 26
27 28 29 30
글 보관함