일급컬렉션(First Class Collection)

공병주(Chris)·2022년 2월 26일

한달 전까지만 해도, 일급 컬렉션에 대한 나의 이해도는 엄청 낮았다. 사실 낮다는 말보다는 나보다 객체지향에 대한 이해도가 높은 사람의 “일급컬렉션을 쓰는게 어때?” 라는 말 때문에 그냥 썼다.. 과거의 나 자신의 엉덩이를 때리며 일급컬렉션에 대한 정리를 해보려 한다.

일급 컬렉션의 기본 개념

  • Collection을 Wrapping 한 것
  • 일급 컬렉션에는 필드가 Collection 하나만 존재해야 한다 (규칙)

일급 컬렉션을 사용하는 이유 (장점)

1. 중복 되는 로직을 제거할 수 있다.


public class FootballTeam {
    private final List<Player> players;

    public FootballTeam(final List<Player> players) {
        this.players = players;
    }

    public List<String> findUserNamesHigherHeightThan(final Height height) {
        return players.stream()
                .filter(player -> player.hasHigherHeight(height))
                .map(Player::getName)
                .collect(Collectors.toUnmodifiableList());
    }

    public List<String> findUserNameHigherAgeThan(final Age age) {
        return players.stream()
                .filter(player -> player.hasHigherAge(age))
                .map(Player::getName)
                .collect(Collectors.toUnmodifiableList());
    }

    // 추가적으로 필요한 메소드
}

여기 List 를 필드로 가지는 FootballTeam이라는 객체가 있다.
FootballTeam 객체는 키 값(혹은 나이 값)을 받아
자신의 팀에 받아온 키보다 더 큰 키(더 많은 나이)를 가진 선수들의 이름을 반환한다.

public class BaseballTeam {
    private final List<Player> players;

		//추가적인 필드
		//...

    public BaseballTeam(final List<Player> players) {
        this.players = players;
    }

    public List<Player> findUserNamesHigherHeightThan(final Height height) {
        return players.stream()
                .filter(player -> player.hasHigherHeight(height))
                .collect(Collectors.toUnmodifiableList());
    }

    public List<Player> findUserNameHigherAgeThan(final Age age) {
        return players.stream()
                .filter(player -> player.hasHigherAge(age))
                .collect(Collectors.toUnmodifiableList());
    }
		//추가적인 메소드
		//...
}

그런데, 여기 BaseballTeam이 또 있다. BaseballTeam은 FootballTeam과 같은
findUserNamesHigherHeightThan 메소드와 findUserNameHigherAgeThan 메소드를 제공한다.

이렇게 되면 두 군데에서 똑같은 코드가 존재한다. 중복이 발생하는 것이다.

또한, 배드민턴팀, 농구팀, 하키팀 등등이 생기면 해당 코드들도 똑같이 적어줘야하고 그에 따른 중복이 추가적으로 발생한다.

이런 경우에 아래와 같이 List를 가지는 일급 컬렉션 Players를 통해 중복을 제거할 수 있다.

public class Players {
    private final List<Player> players;

		//추가적인 필드
		//...

    public Players(final List<Player> players) {
        this.players = players;
    }

    public List<Player> findUserNamesHigherHeightThan(final Height height) {
        return players.stream()
                .filter(player -> player.hasHigherHeight(height))
                .collect(Collectors.toUnmodifiableList());
    }

    public List<Player> findUserNameHigherAgeThan(final Age age) {
        return players.stream()
                .filter(player -> player.hasHigherAge(age))
                .collect(Collectors.toUnmodifiableList());
    }
		//추가적인 메소드
		//...
}
public class BaseballTeam {
    private final Players players;

		//추가적인 필드
		//...

    public BaseballTeam(final List<Player> players) {
        this.players = new Players(players);
    }

    public List<Player> findUserNamesHigherHeightThan(final Height height) {
        return players.findUserNamesHigherHeightThan(height);
    }

    public List<Player> findUserNameHigherAgeThan(final Age age) {
        return players.findUserNameHigherAgeThan(age);
    }
		//추가적인 메소드
		//...
}
public class FootballTeam {
    private final Players players;

		//추가적인 필드
		//...

    public FootballTeam(final List<Player> players) {
        this.players = new Players(players);
    }

    public List<Player> findUserNamesHigherHeightThan(final Height height) {
        return players.findUserNamesHigherHeightThan(height);
    }

    public List<Player> findUserNameHigherAgeThan(final Age age) {
        return players.findUserNameHigherAgeThan(age);
    }
		//추가적인 메소드
		//...
}

위의 방식대로라면 BasketBallTeam, BadmintonTeam .. 등등이 생겨도 List을 사용하는 로직을 중복없이 사용할 수 있다.

2. 책임 분리

일급 컬렉션 사용하지 않은 예

public class FootballTeam {
    private final List<Player> players;

		'''

    public FootballTeam(final List<Player> players) {
        this.players = players;
    }

    public List<String> findUserNamesHigherHeightThan(final Height height) {
        return players.stream()
                .filter(player -> player.hasHigherHeight(height))
                .map(Player::getName)
                .collect(Collectors.toUnmodifiableList());
    }

    public List<String> findUserNameHigherAgeThan(final Age age) {
        return players.stream()
                .filter(player -> player.hasHigherAge(age))
                .map(Player::getName)
                .collect(Collectors.toUnmodifiableList());
    }
		
	  //추가적인 메소드
		//...
}

일급 컬렉션을 사용한 예

public class FootballTeam {
    private final Players players;

		//추가적인 필드
		//...

    public FootballTeam(final List<Player> players) {
        this.players = new Players(players);
    }

    public List<Player> findUserNamesHigherHeightThan(final Height height) {
        return players.findUserNamesHigherHeightThan(height);
    }

    public List<Player> findUserNameHigherAgeThan(final Age age) {
        return players.findUserNameHigherAgeThan(age);
    }
		//추가적인 메소드
		//...
}

FootBallTeam은 findUserNamesHigherHeightThan, findUserNameHigherAgeThan을 제외하고
외부에 추가적으로 제공 해야하는 일들이 있을 것이다.

위의 두 예시를 보면 일급 컬렉션을 적용하지 않은 구조는 FootballTeam에 책임이 몰려있고,
일급 컬렉션을 적용한 구조FootballTeam과 Players와 책임의 나눠져있다.

3. 불변

public class Players {
    private final List<Player> players;

    public Players(final List<Player> players) {
        this.players = new ArrayList<>(players);
    }
}

위의 방식처럼 List를 받아와 생성자에서 다른 주소 값을 가지는 새로운 객체를 만들면
아래와 같은 코드에서도 Players의 List players를 변경으로부터 막을 수 있다.

void someWhere() {
        final List<Player> woowaPlayers = new ArrayList<>();
        woowaPlayers.add(new Player("Chris", 25, 183));
        final Players woowaFootBallPlayers = new Players(woowaPlayers);
        woowaPlayers.add(new Player("Roma", 26, 184));
        woowaPlayers.add(new Player("Alex", 27, 185));
}

해당 메소드에서의 woowaPlayers와 Players 객체의 필드는 다른 객체이기 때문에 ( Players의 생성자에서 새로운 객체를 생성하기 때문에), 위와 같은 경우에서도 Players의 컬렉션필드를 변경으로부터 보호할 수 있다.

public class Players {
    private final List<Player> players;
		
		//...

    public List<Player> getPlayers() {
				return Collections.unmodifiableList(players);
        //return new ArrayList<>(players)
    }
}

또한, getter 메소드에 대해서도 위와 같은 개념으로 같은 값을 가진 새로운 객체를 반환해 원본 객체를 보호할 수 있다.

‘일급컬렉션을 통해 불변의 장점을 가질 수 있다.’ 보다는, ‘일급컬렉션을 불변으로 만들어라’가 맞는 것 같다.

List players를 불변하게 하려면 일급컬렉션 없이도 충분히 할 수 있다는 생각이 든다.

핵심은 상태와 행위를 한번에 관리하고 중복 로직을 제거하는 것이라고 생각한다.

참고자료

https://jojoldu.tistory.com/412

https://tecoble.techcourse.co.kr/post/2020-05-08-First-Class-Collection/

1개의 댓글

comment-user-thumbnail
2022년 2월 26일

잘 읽고갑니다! 중복 로직하고 불변 관련 내용이 이해가 잘 되네요.

답글 달기