[우테코 7기 프리코스 2주 차] 일급 컬렉션

2024. 10. 29. 13:03카테고리 없음

 

 

1주 차, 많은 코드를 리뷰하며 일급 컬렉션(First Class Collection)이라는 새로운 개념을 배우게 되었다.

 

이는 소트웍스 앤솔로지의 객체지향 생활체조 규칙 8번에서 언급되는 내용으로,

컬렉션을 객체로 캡슐화해 책임을 분리하고 불변성을 유지하는 설계 방식을 의미한다.

 

새롭게 알게 된 개념을 적용해가며 느낀 점과 배운 내용을 기록해보고자 한다.

 


 

 

개념
일급 컬렉션

 

단일 Collection만을 멤버변수로 갖고, 이를 Wrapping하여 사용하는 클래스

 

  • 비즈니스 종속적인 자료구조:
    • 비즈니스 로직에 맞춘 컬렉션 구조를 설계할 수 있습니다.
  • 상태와 행위의 일원화:
    • 데이터를 담고 처리하는 행위(method)를 한 클래스 내에서 통합 관리할 수 있습니다.
  • 테스트 용이성:
    • 컬렉션에 대한 로직이 한 곳에 집중되므로, 테스트가 간단하고 체계적입니다.
  • 이름이 있는 컬렉션:
    • 컬렉션에 의미 있는 이름을 부여하여 가독성과 유지보수성을 향상시킬 수 있습니다.
  • 불변성 보장:
    • 컬렉션을 외부에서 직접 수정할 수 없도록 하여 데이터의 안정성을 확보합니다.

 


 

일급 컬렉션 적용 전

 

RacingGame 클래스
public class RacingGame {

    private final InputHandler inputHandler;
	...
    private final List<Car> cars;  // 일급 컬렉션 대신 List<Car>로 관리

    public RacingGame(InputHandler inputHandler,
                      ...		) {
        this.inputHandler = inputHandler;
        ...;
    }

	...

    private void initCars(String carNames) {
        cars = new ArrayList<>();
        for (String carName : StringSplitter.splitByComma(carNames)) {
            Car car = new Car(carName);
            CarObserverHelper.addObserverToCar(car, CarMovePrinter.class, outputHandler);
            cars.add(car);
        }
    }

    private void runRaceRound() {
        List<Integer> randomValues = randomValueGenerator.generateMultiple(0, 9, cars.size());
        if (randomValues.size() != cars.size()) {
            throw new IllegalStateException("랜덤 값의 개수와 자동차 개수가 일치하지 않습니다.");
        }

        for (int i = 0; i < cars.size(); i++) {
            cars.get(i).move(randomValues.get(i));
        }
    }

    private List<Car> getWinners() {
        int maxPosition = getMaxPosition();
        List<Car> winners = new ArrayList<>();
        for (Car car : cars) {
            if (car.getPosition() == maxPosition) {
                winners.add(car);
            }
        }
        return winners;
    }

	...
}

 

컬렉션에 대한 관리 로직 분산

 

List<Car>를 다루는 로직이 여러 메서드에 흩어져 있어 관리가 복잡해집니다. 예를 들어, initCars, runRaceRound, getWinners 메서드가 모두 cars 리스트를 직접적으로 접근하고 조작하고 있습니다. 이처럼 리스트를 직접 다루는 코드가 여러 메서드에 분산되면 코드가 중복되고, 변경해야 할 때 여러 메서드를 함께 수정해야 합니다.

 

컬렉션 불변성 부족

 

final 키워드를 사용하더라도 컬렉션의 참조 자체는 변경되지 않지만, 리스트의 내용은 여전히 수정될 수 있습니다.

 

따라서 cars 리스트는 add와 같은 메서드를 통해 언제든지 수정될 수 있으며, List<Car>가 직접적으로 노출되어 있어 컬렉션의 불변성을 유지하기 어렵고, 관리되지 않은 상태 변화가 발생할 수 있습니다. 예를 들어, cars.add(new Car(...))와 같은 코드가 있다면, 의도치 않은 Car 객체가 리스트에 추가될 수 있습니다. 이는 컬렉션의 일관성을 보장하기 어렵게 하며, 데이터 무결성에도 영향을 줄 수 있습니다.

 

RacingGame 클래스의 책임 증가

 

RacingGame 클래스가 게임 진행과 동시에 List<Car>의 상태 관리까지 담당합니다. 예를 들어, runRaceRoundgetWinners에서는 List<Car>에 접근하여 자동차들의 이동과 우승자 계산을 직접 수행합니다. 이로 인해 RacingGame 클래스가 실제 게임 로직 외에도 자동차 리스트와 관련된 세부적인 로직까지 떠안게 됩니다. 이러한 구조는 RacingGame 클래스가 지나치게 많은 역할을 수행하게 하며, 코드의 응집성을 떨어뜨립니다.

 

반복적이고 유사한 코드 패턴

 

예를 들어, runRaceRoundgetWinners에서 cars 리스트를 순회하며 개별 Car 객체에 접근하는 방식이 반복됩니다. 각각의 메서드에서 리스트 순회와 상태 비교 및 업데이트 로직이 중복적으로 나타나며, 개별적인 수정이 필요할 때 비슷한 코드를 여러 위치에서 수정해야 할 가능성이 생깁니다.

 

컬렉션 상태와 조작 로직의 결합

 

cars.get(i).move(randomValues.get(i))처럼, List<Car>의 개별 요소에 대한 조작이 RacingGame 클래스 내부에 존재합니다. 이 구조는 List<Car>RacingGame이 직접적으로 다루게 하여 특정 로직이 변경될 때마다 게임 클래스에서 컬렉션 조작 방식을 함께 수정해야 합니다. 컬렉션을 다루는 로직이 한 곳에 집중되지 않고 흩어져 있기 때문에, 코드가 복잡해지고 유지보수가 어려워질 수 있습니다.

 

 

결론적으로, 이러한 특징들은 클래스가 본래의 목적과는 다른 책임을 떠안게 만들고, 코드의 중복과 함께 유지보수의 복잡성을 증가시킵니다. Cars와 같은 일급 컬렉션을 도입하면 RacingGame 클래스에서 컬렉션 관련 로직을 분리하여, 책임을 분산하고 코드를 더 간결하게 만들 수 있습니다.

 

 


 

 

일급 컬렉션 적용 후

 

RacingGame 클래스
public class RacingGame {

    private final InputHandler inputHandler;
	...

    public RacingGame(InputHandler inputHandler,
                      ...		) {
        this.inputHandler = inputHandler;
        ...;
    }

	...

    private Cars initCars(String carNames) {
        Cars cars = new Cars(StringSplitter.splitByComma(carNames));
        cars.addObserverToAll(CarMovePrinter.class, outputHandler);
        return cars;
    }

    private void runRaceRound(Cars cars) {
        List<Integer> randomValues = randomValueGenerator.generateMultiple(0, 9, cars.size());
        cars.moveAll(randomValues);
    }
    
    ...
}

 

 

Cars 클래스 (일급 컬렉션)
public class Cars {
    private final List<Car> cars;

    public Cars(List<String> cars) {
        this.cars = cars.stream()
                .map(Car::new)
                .toList();
    }

    public void moveAll(List<Integer> randomValues) {
        for (int i = 0; i < cars.size(); i++) {
            cars.get(i).move(randomValues.get(i));
        }
    }

	...

    public List<Car> getWinners() {
        int maxPosition = getMaxPosition();
        return cars.stream()
                .filter(car -> car.getPosition() == maxPosition)
                .toList();
    }

    private int getMaxPosition() {
        return cars.stream()
                .mapToInt(Car::getPosition)
                .max()
                .orElseThrow();
    }

	...

}

 

컬렉션 관리 로직의 응집

 

Cars 클래스가 List<Car>를 캡슐화하여 컬렉션과 관련된 책임을 모두 담당함으로써 컬렉션 관리 로직이 Cars 내부에 응집됩니다. 예를 들어, moveAllgetWinners 메서드가 Cars 클래스 내부에 위치하여 RacingGame에서 컬렉션을 직접 다루지 않아도 되도록 했습니다. 이렇게 응집된 구조는 컬렉션 관리와 관련된 로직을 한 곳에서 관리할 수 있어 중복을 줄이고 일관성을 높여줍니다.

 

컬렉션 불변성 유지

 

일급 컬렉션 클래스는 컬렉션을 외부에 직접 노출하지 않기 때문에, 컬렉션의 변경을 제어할 수 있습니다. 컬렉션에 추가나 삭제를 위한 메서드를 직접 제공하지 않는 한, 외부에서 addremove 같은 메서드를 통해 컬렉션을 수정할 수 없습니다. 이렇게 함으로써 컬렉션의 상태가 일급 컬렉션 내부에서만 관리되어 불변성을 유지할 수 있습니다.

 

RacingGame 클래스의 역할 집중과 책임 분산

 

이전 구조에서는 RacingGame 클래스가 게임 진행 로직뿐만 아니라 List<Car>의 상태 관리까지 맡고 있었습니다. 그러나 Cars 일급 컬렉션 도입 후에는 RacingGame 클래스가 게임 로직에만 집중할 수 있습니다. runRaceRound 메서드처럼 원래 컬렉션 상태를 조작하던 메서드가 Cars의 메서드를 호출하는 방식으로 변하면서 RacingGame 클래스의 책임이 분산되고, 코드의 응집도가 높아졌습니다.

 

코드 중복 감소와 유지보수 용이성 향상

 

이전 코드에서는 runRaceRoundgetWinners 메서드에서 컬렉션 순회와 같은 유사한 코드 패턴이 반복되었습니다. Cars 클래스가 도입되면서 이런 반복적인 순회와 상태 관리 로직이 Cars 내부로 들어갔고, RacingGame에서는 Cars의 메서드를 호출하기만 하면 됩니다. 이렇게 코드 중복이 감소하면서, 수정이나 개선이 필요할 때 한 곳만 수정하면 되는 장점을 얻었습니다.

 

컬렉션 상태와 조작 로직의 분리

 

기존 구조에서는 RacingGame 클래스가 List<Car>의 상태와 조작을 직접 수행하여, 특정 로직을 변경할 때마다 RacingGame의 컬렉션 처리 로직을 함께 수정해야 했습니다. 그러나 Cars 클래스가 List<Car>의 상태와 조작을 책임지게 됨으로써, RacingGame 클래스가 Cars에 있는 메서드만 호출하는 간결한 구조로 변했습니다. 이는 로직의 변경과 유지보수가 용이하게 해주고, 코드의 복잡성을 줄입니다.

 

 

결론적으로, Cars 일급 컬렉션을 도입하여 RacingGame 클래스와 컬렉션 관리의 책임을 명확히 분리했습니다. 이를 통해 RacingGame 클래스는 본래의 역할인 게임 진행 로직에 집중할 수 있으며, 컬렉션 관리 로직이 응집되어 코드의 간결성과 유지보수성이 향상되었습니다.

 


 

 

스프링, 도메인 주도 설계 관점에서 일급 컬렉션

 

 

스프링 프레임워크에서도 일급 컬렉션이라는 개념을 적극적으로 사용할 수 있습니다. 스프링 자체에서 일급 컬렉션을 특별히 제공하는 것은 아니지만, 도메인 주도 설계(Domain-Driven Design, DDD) 에서 일급 컬렉션을 권장하고 있으며, 이를 스프링 애플리케이션에서도 자주 응용합니다.

 

일급 컬렉션은 컬렉션을 캡슐화하고 불변성을 유지하면서 컬렉션에 대한 책임을 부여하기 때문에, 스프링 애플리케이션 내에서 엔티티나 도메인 객체가 관리해야 하는 목록을 다룰 때 좋은 설계 방법입니다. 예를 들어, 특정 엔티티가 여러 개의 자식 엔티티를 포함해야 할 경우, 자식 엔티티들을 ListSet으로 관리하기보다는 일급 컬렉션으로 감싸 컬렉션 관련 검증이나 비즈니스 로직을 한 곳에서 관리할 수 있습니다.

 

( 참고: https://jojoldu.tistory.com/412 )

 

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

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

jojoldu.tistory.com

 

 

스프링에서는 일급 컬렉션을 단순히 값 객체로 사용하거나 new로 생성하는 대신, configuration 클래스에서 빈으로 관리할 수도 있습니다. 이를 통해 일급 컬렉션을 다른 컴포넌트에 의존성 주입(DI)하여, 도메인 객체의 생명주기와 컬렉션 상태를 일관성 있게 관리할 수 있습니다. 예를 들어, Order와 여러 OrderItem을 다루는 OrderItemList 같은 일급 컬렉션은 configuration 클래스에서 생성하고 관리하면, 스프링 IoC 컨테이너가 이를 자동으로 주입하고 생명주기를 관리해 줍니다.

 

( 참고: https://www.inflearn.com/community/questions/1251450/di-%EC%A0%81%EC%9A%A9%ED%95%B4%EB%B3%B4%EA%B8%B0?srsltid=AfmBOoqTrsc6FUsZAilz9WuVlGAMpgDqrEKBWzWgpAGnuiA3i8zraDTO )

맥락은 조금 다르지만, 예시를 확인하실 수 있습니다.

 

DI 적용해보기 - 인프런 | 커뮤니티 질문&답변

누구나 함께하는 인프런 커뮤니티. 모르면 묻고, 해답을 찾아보세요.

www.inflearn.com

 


 

 

마무리

 

 

자바와 스프링에서의 일급 컬렉션을 다양한 관점에서 살펴보았다. 자바에서는 컬렉션을 Wrapping하여 책임을 분리하고 관리의 편의성을 높이며, 테스트가 용이하도록 하는 장점을 얻을 수 있었다. 특히, 프리코스 2주 차(racingcar)에서 Cars 일급 컬렉션을 사용해 보며 직접 체감할 수 있었다.

 

스프링에서는 일급 컬렉션을 비즈니스 로직에 맞춘 컬렉션 구조로 설계하고, 필요한 경우 IoC 컨테이너에서 빈으로 관리할 수 있다는 점이 유용해보였다.

 

무엇보다 내 코드들이 더 예뻐졌다.