woowacourse-study / 2022-modern-java-in-action

우아한테크코스 4기 모던 자바 인 액션 스터디
10 stars 4 forks source link

Collectors.toMap 실습 #47

Open bugoverdose opened 2 years ago

bugoverdose commented 2 years ago

주제

스트림의 collect() 연산을 통해 Map을 만드는 연습을 해보자

선정 배경

관련 챕터

bugoverdose commented 2 years ago

기본적으로 아래와 같은 PieceEntity 데이터의 리스트를 Map<Position, Piece> 형식으로 변환해야 한다고 가정하자. 개별 PieceEntity 객체에는 PositionPiece 객체를 생성하는 데 필요한 모든 정보가 담겨있다.

private final List<PieceEntity> PIECE_ENTITIES = List.of(
        new PieceEntity("a1", "PAWN", "WHITE"),
        new PieceEntity("c3", "KNIGHT", "BLACK"),
        new PieceEntity("e3", "QUEEN", "BLACK"));

private final Map<Position, Piece> expected = new HashMap<>(){{
    put(Position.of("a1"), Piece.of(Color.WHITE, PieceType.PAWN));
    put(Position.of("c3"), Piece.of(Color.BLACK, PieceType.KNIGHT));
    put(Position.of("e3"), Piece.of(Color.BLACK, PieceType.QUEEN));
}};

일단 스트림이 없는 경우 아래와 같이 (1) 비어있는 맵을 초기화하고, (2) for문을 통해 리스트를 돌며 초기화된 맵에 한쌍식 데이터를 추가할 수 있다. 가독성이 낮지도 않고 굉장히 표준적인 방법이라고 볼 수 있다.

@Test
void withoutStream() {
    final Map<Position, Piece> actual = new HashMap<>();
    for (PieceEntity pieceEntity : PIECE_ENTITIES) {
        Position position = pieceEntity.getPosition();
        Piece piece = pieceEntity.toModel();
        actual.put(position, piece);
    }

    assertThat(actual).isEqualTo(expected);
}

다음은 리스트를 스트림으로 만든 후 Collector 없이 collect() 메서드를 사용하는 방법이다. 굉장히 가독성이 낮지만 기본 구조는 거의 위와 유사하다. (1) 스트림 요소를 순회하며 데이터를 담을 맵을 초기화하기 위한 함수를 전달하고, (2) 개별 데이터를 맵에 추가하기 위한 함수를 collect의 매개변수로 전달하였다. 3번째 매개변수의 경우 병렬적으로 처리될 때 복수의 맵을 하나로 합치기 위한 기능인데, 순차 스트림으로만 사용하는 경우 아래와 같이 구실만 맞추기 위해 임의의 맵 데이터를 반환하도록 하여도 무관하다.

솔직히 아래와 같이 할 바에야 위의 방식대로 하는 것이 더 좋다고 생각한다.

@Test
void collectWithoutCollector() {
    final Map<Position, Piece> actual = PIECE_ENTITIES.stream()
            .collect(HashMap::new, // (1) supplier
                    (map, pieceEntity) -> map.put(pieceEntity.getPosition(), pieceEntity.toModel()), // (2) accumulator
                    (map1, map2) -> new HashMap<>() // (3) combiner
            );
    assertThat(actual).isEqualTo(expected);
}

다음은 리스트를 스트림으로 만든 후 Collector.toMap()을 활용하는 방식이다. toMap()은 스트림의 개별 요소를 통해 맵의 key 값과 value 값을 만드는 함수 두 개를 매개변수로 받는다. 사실 위의 for문에 비해 세로로는 짧아졌지만 가로로는 길어졌기 때문에 가독성이 개선되었다고 보기는 다소 어렵다고 생각한다. 다만, 맵에 데이터를 추가하기 전에 수행되어야 하는 작업이 별도로 존재하는 경우, 스트림 파이프라인에 중간연산을 추가하는 방식으로 로직을 추가할 수 있을 것이다.

@Test
void collectToMap() {
    final Map<Position, Piece> actual = PIECE_ENTITIES.stream()
            .collect(Collectors.toMap(PieceEntity::getPosition, PieceEntity::toModel));

    assertThat(actual).isEqualTo(expected);
}
bugoverdose commented 2 years ago

다만, toMap은 기본적으로 맵에 추가하고 싶은 모든 데이터 요소들이 하나의 컬렉션으로 모아져 있어야 하며, 맵을 구성하는 데 필요한 키-값 쌍이 개별 데이터에 담겨져 있어야 사용하기 용이하다.

아래는 억지로 toMap을 사용하기 위해 (1) 맵에 추가하기 위한 키-값 쌍이 담긴 PositionPiecePair라는 클래스를 만들고, (2) 맵을 구성하는 모든 데이터를 굳이 하나의 리스트로 만들어서 스트림을 생성한 예제이다. 아래와 같은 코드를 작성함으로써 얻을 수 있는 이점이 그다지 보이지 않는다.

public Map<Position, Piece> initFullChessBoard() {
    return initAllPieces().stream()
            .collect(toMap(PositionPiecePair::position, PositionPiecePair::piece));
}

private List<PositionPiecePair> initAllPieces() {
    final List<PositionPiecePair> pieces = new ArrayList<>();
    pieces.addAll(initNonPawns(Color.BLACK, BLACK_NON_PAWN_INIT_RANK));
    pieces.addAll(initPawns(Color.BLACK, BLACK_PAWN_INIT_RANK));
    pieces.addAll(initPawns(Color.WHITE, WHITE_PAWN_INIT_RANK));
    pieces.addAll(initNonPawns(Color.WHITE, WHITE_NON_PAWN_INIT_RANK));
    return pieces;
}