우아한테크코스 레벨1 블랙잭 미션 정리

디우·2022년 4월 17일
0

1단계 - 블랙잭

블랙잭 미션 1단계 저장소 링크

PR 링크

리뷰어: 로운

고민한 내용

Dealer와 Player의 구분

이 둘의 기능적인 차이점은 '추가 카드를 뽑는 방법'과 '처음에 카드를 몇 장 보여주는지'에 있다. 하지만 이외의 기능들에서는 공통적이기 때문에 이 둘을 User라고 하는 추상클래스로 추상화하여 구현하였다.
그런데 View에서 딜러와 일반 Player들을 구분해주다보니 Users라고 하는 클래스를 만들고 여기에 PlayersDealer를 두게 되었고, 각각을 호출하게 되어 결국 추상화의 이점을 활용하지 못하고 있는 것 아닌가 하는 생각이 들었다.

	public void setInitCardsPerPlayer(Deck deck) {
        players.drawInitCards(deck);
        dealer.drawInitCards(deck);
    }

View와 DTO

DTO는 단순하게 이야기해서 Controller와 View 사이에서 데이터를 운반하는 역할을 한다고 생각한다.
하지만 뷰에서 모든 처리를 해주려고 하다보니 OutputView에서 구현한 calculateDealerYield와 같이 너무 뷰의 책임이 커진 것이 아닌가 하는 고민이 들었다.
그래도 View의 출력 조건이 변경되었을 때 View에서만 영향이 있도록 하는 것이 적절해보여 View쪽에서 데이터를 가공해주도록 하였다.
하지만 여기에 더불어서 아직 UserDtogetInitCardsInfo()와 같은 부분이나, getCardsInfo() 부분에서 뷰를 위한 데이터 가공을 해주고 있는게 아닌가? 하는 생각도 들었다.
(페어와 논의하였을 때 Card를 그냥 넘겨주는 것은 DTO를 만드는 의미가 없어지며 CardDto를 만드는 것은 오버엔지니어링이 아니냐 하는 고민을 하였고, 문자열로 만들어서 반환하는 것으로 결정하여 DTO에서 가공해주도록 하였다.)

고민에 대한 답변

  1. User라는 추상클래스를 사용하며 추상클래스의 이점을 활용하고 있다고 생각하는데요. Users라는 일급객체를 만들며 안에서 User가 아닌 각 클래스를 명시해 주는 것에서 아쉬움이 남았던거 같아요.
    추상클래스를 사용하는 이유를 찾아보면 좋을거 같고, Users 내에서도 분리없이 하려면 우선 딜러인지 아닌지 물어볼 수 있어야할 거 같아요. 조금 더 방법을 생각해보시고 생각이 안나면 제가 생각한 방법을 말씀드릴게요ㅎㅎ

  2. view의 책임은 무엇일까요?? 실제 앱이나 웹에서도 어떻게 보여줄지 서버에서 고민을 할까요? view를 위한 데이터를 가공하는 이유가 무엇인가요?? 이 데이터를 가공한 dto가 view에서만 쓰일까요?? dto는 값을 전달하는 것이기 때문에 어떠한 형태로 넘겨줄지는 개발자들이 정하면 된다고 생각하는데요. 다만 이 값들이 어떠한 정보인지 명확하게 알 수 있게 표현해야한다고 생각해요. 이러한 의미에서 cardDto가 필요하다고 생각이 들면 만드는 것도 좋다고 생각합니다~ dto를 사용하는 이유와 사용방법에 대해서 알아보면 좋을거 같아요. 알아보고 알려주세요ㅎㅎ

답변에 대한 나의 생각

  1. 추상클래스인 UserisDealer(), isPlayer() 두가지 메소드를 둔다. 이를 통해 딜러인지 아닌지를 물어볼 수 있도록 하였다. 이렇게 하고나니 의도한 대로 List로 관리할 수 있어 Users레벨까지 User 추상화의 이점을 활용할 수 있게 된다고 생각하였다.

  2. 실제 앱이나 웹 서비스에서 '서버'는 데이터를 전달하는데에만 집중하는게 맞다고 생각된다. 즉, "어떻게" 보여줄지에 대한 것은 프런트쪽에서 처리를 해주는 것이 적절하다고 생각한다. 그리고 스스로 정의하고 있는 DTO는 말 그대로 "데이터 전송 객체"이며 view와 domain의 의존성을 끊어내기위한 도구라고 생각한다. 또한 User 라는 객체에 패스워드 등 전달되기 민감한 데이터 들이 있을 수 있기 때문에 도메인을 바로 뷰로 전달하지 않고, DTO로 필요한 데이터만을 감싸서 전송할 수 있도록 하기 위해 DTO를 사용한다.

리뷰어님의 답변을 받고 또 질문에 답하기 위해서 고민하는 시간을 가지고 나니, 적어도 view에 대한 변경이 DTO에 까지 영향이 가면 안되는거 아닌가? 하는 생각을 하였고, String.join(", ", ....); 과 같이 문자열을 조인하는 부분에서 분리자가 ", " 가 아니라 "와" 와 같이 view단에 요구사항 변경이 들어오면 이것이 DTO에 까지 영향이 가는 것은 적절하지 않다고 생각하게 되었고, 이를 코드에 반영하였다.

피드백 내용

  • 다시 한 번 더 정적 팩토리 메소드 네이밍 컨벤션
    from: 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메소드
    of: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메소드
    예를 들어 List와 Dealer 인스턴스를 받아 객체를 생성하는 Users의 정적 팩토리 메소드의 경우 from 보다는 of 가 더 적절한 네이밍이다.

Q. 전략 패턴을 적용해보면 어떨까요? 참고 자료

A. 처음에 위 질문을 받고 든 생각은 왜? 였다.

  • 이를 적용함으로써 자동차 미션 때 처럼 "테스트가 용이"해지는가?
    -> 덱은 52장이어야한다는 제약 이외에 덱의 순서에 대한 제약이나 각각의 카드에 대한 제약이 없다.
  • 로또 때처럼 자동 생성, 수동 생성과 같이 여러 전략을 가지는가?
    -> 셔플된 덱 하나만 필요함. 또 다른 전략은 불필요해 보인다.
    위와 같은 생각이 들었다. 그래서 리뷰어님께서 왜 전략패턴을 적용해보라고 권유하는지 고민이 들었다.

내가 찾은 답은 아래와 같다.

DeckGenerateStrategy 인터페이스와 이를 구현하는 ShuffledDeckGenerateStrategy를 두어 덱 생성에 대한 책임을 해당 전략으로 분리할 수 있다.
만약에 순서대로 정리된 Deck을 사용해야 한다고 하면 NonShuffled 덱 전략을 사용하기 용이해진다.
다음으로는 Deck에서는 덱 생성에 대해서는 관심이 없고, 덱에서 카드를 한 장씩 반환해준다. 라고 하는 책임에만 전념할 수 있으므로 객체가 갖는 책임의 크기는 작게라고 하는 내용에도 부합하게 된다.

이와 더불어 리뷰어가 왜 '전략 패턴' 사용을 권유하였는지 그 이유에 대해서 궁금해졌다.
나는 이전까지 요구사항과 같이 눈에 보이는 곳에서 '수동, 자동'과 같이 전략을 사용해야겠다. 를 느낄 때 사용하였었다. 그렇다면 리뷰어는 어떤 부분에서 '전략 패턴' 사용을 고민하게 되었던 것일까? 리뷰어님의 답변은 다음과 같다.

저도 어떨 때 적용하면 좋겠다라고 생각이 딱 들지는 않는데요;; 😅
기본적으로 생각이 드는 것은 random 한 것에 대한 로직이 들어간다면 테스트를 할때 필요하기 때문에 생각을 하게 되는 것 같고요.
(블랙잭의 경우에는 deck에서 card를 한장 줬을때 실제로 내가 원했던 위치에 있는 카드가 user에게 갔는가를 테스트)
이외에는 비슷한 기능을 하는 것들이 더 생기는 상황이 발생하면 그때 적용을 하게 되는거 같아요.
이거는 저희 팀의 예를 들 수 밖에 없을거 같은데요.
저희가 배달대행사에 주문을 전달을 하는데 이 전달하는 방식이 배달대행사마다 다른데요. 실제로 주문을 전달한다는 같게되죠.
이럴때 처음에는 하나의 배달대행사만 적용하면서 전략패턴을 생각하지 않다가 다른 배달대행사들을 추가로 구현하게 되면서 적용을 고려하게 됐던거 같습니다~ 비슷한 도메인을 해봤거나 경험이 많이 쌓이게 되면 처음부터 고려를 할 수도 있겠죠??
아마 pg사나 은행쪽 로직(결제, 뱅킹)도 비슷하지 않을까 생각이 드네요~

  • enhanced-for문 보다는 stream을 사용하자.

        List<User> players = new ArrayList<>();
    
            for (User user : users) {
                checkPlayer(players, user);
            }
    
            return Collections.unmodifiableList(players);
        return users.stream()
                .filter(user -> !user.isDealer(user))
                .collect(Collectors.toUnmodifiableList());
  • 메소드의 순서를 정하기 어려울 때 혹은 메소드간 순서에 대한 가독성이 없을 때는, 다음과 같은 규칙을 따라서 작성하도록 노력해보자.

    public class ClassName {
        // private static final
        // 상수
        // 클래스 변수
        // 인스턴스 변수
        // 생성자
        // 팩토리 메서드
        // 일반 메서드
        // getter, setter
        // equals, hashCode, toString
    }
  • instanceof의 사용을 지양하자. 참고 자료

        public boolean isPlayer() {
            if (this instanceof Player) {
                ...
            }
        }

    예를 들어 위와 같은 코드의 경우, User 추상 클래스에 public abstract boolean isDealer();와 같은 추상메소드를 두고, 각각의 Dealer와 Player 구현체에서 그에 맞게끔 재정의 해줌으로써 instanceof의 사용을 피할 수 있다.
    그렇다면 왜 instanceof를 사용하면 지양해야 할까?
    가장 먼저 캡슐화에 대해서 고민해볼 수 있다.
    캡슐화란 객체가 가진 상태나 행위를 감추고 숨기는 것을 의미하는데, instanceof를 사용하는 것은 각 객체가 무엇인지에 대한 정보를 불필요한 외부에 노출하게 되는 것이다. 즉, 캡슐화가 깨지게 되는 것이다.
    다음으로는 OCP 원칙을 지키지 못하기 때문이다. 만약 User를 상속하는 Concrete Class가 더 늘어난다고 생각해보자. 다형성을 이용하면 새로운 클래스를 구현하기만 하면 되는데, instanceof를 사용하게 되는 경우 기존 코드에 대한 변경을 유발하게 된다.
    마지막은 SRP을 위배하게 된다. 각 타입에게 책임을 부여하면 되는 일을 하나의 메소드에서 모든 책임을 지려고 하기 때문에 instanceof로 특정 타입임을 알아내고 특정 코드의 실행을 요청하게 되는 것이다.
    하지만 instanceof 사용을 하지 않고는 구현이 어렵다고 느껴질 때가 있다. 이럴 때는 다형성을 고려해보자.
    instanceof 사용을 하지 않고 '다형성'을 통해서 원하는 로직을 충분히 표현해 낼 수 있기 때문이다.

  • 메소드 참조를 사용할 수 있으면 사용하자.

        return cards.stream()
                    .anyMatch(card -> card.isAce());

    위의 코드 보다는 아래 코드가 보다 간결하다. 더군다나 card라고 하는 변수명에 대한 고려를 하는 비용도 제거할 수 있다.

        return cards.stream()
                    .anyMatch(Card::isAce);
  • 테스트코드에서 여러개의 코드(Card, player, deck 등)가 중복되는 경우 테스트에서 공통으로 사용할 수 있는 객체를 만들어서 필요한 객체를 반환하는 메소드를 제공하여 여러 테스트에서 사용하면 중복을 줄일 수 있다.

        private Cards createCards(List<Card> initCards) {
            Cards cards = new Cards();
    
            for (Card initCard : initCards) {
                cards.add(initCard);
            }
    
            return cards;
        }

    이에 대한 코멘트: 제가 활용하는 개인적인 방법은 TestUtils와 같은 객체를 만들어 이 메서드를 그쪽으로 옮겨서 필요한 테스트 클래스들에서 사용하도록 하고 있어요~ 참고만 하시면 될 거 같아요~

  • Consumer 사용
    뷰에서 입력받은 'Y' or 'N'에 따라서 추가적으로 카드를 draw할 수 있어야 한다는 요구사항이 존재한다. 즉, 도메인이 뷰쪽의 결과를 도메인에 전달해야하므로 도메인에서 뷰로의 의존이 존재하게 된다는 것인데, 이를 Consumer라는 함수형 인터페이스를 활용하여 끊어낼 수 있다.
    Consumer에 대해서 간단히 설명하면, void accept(T t) 추상 메소드를 가지는 함수형 인터페이스로 "객체 T를 받아서 이를 소비한다." 라는 의미를 가진다.
    이를 활용하여 블랙잭 게임의 진행 역할을 담당하는 BlackJack 이라는 도메인을 사용하는 Application 코드에서 다음과 같이 구현하게 되면 BlackJack 도메인에서 View에 대한 의존을 끊어낼 수 있게 된다.

    private static void drawAdditionalCard(BlackJack blackJack) {
        Consumer<User> consumerPlayer = user -> drawCardPerPlayer(blackJack, user);
    
        Consumer<User> consumerDealer = user -> drawDealerCard(blackJack, user);
    
        blackJack.drawAdditionalCard(consumerPlayer, consumerDealer);
    }

2단계 - 블랙잭(베팅)

블랙잭 미션 2단계 저장소 링크

PR 링크

리뷰어: 로운

추가된 요구사항

  • 플레이어는 게임을 시작할 때 배팅 금액을 정해야 한다.
  • 카드의 숫자 계산은 카드 숫자를 기본으로 하며, 예외로 Ace는 1 또는 11로 계산할 수 있으며, King, Queen, Jack은 각각 10으로 계산한다.
  • 게임을 시작하면 플레이어는 두 장의 카드를 지급 받으며, 두 장의 카드 숫자를 합쳐 21을 초과하지 않으면서 21에 가깝게 만들면 이긴다. 21을 넘지 않을 경우 원한다면 얼마든지 카드를 계속 뽑을 수 있다. 단, 카드를 추가로 뽑아 21을 초과할 경우 배팅 금액을 모두 잃게 된다.
  • 처음 두 장의 카드 합이 21일 경우 블랙잭이 되면 베팅 금액의 1.5 배를 딜러에게 받는다. 딜러와 플레이어가 모두 동시에 블랙잭인 경우 플레이어는 베팅한 금액을 돌려받는다.
  • 딜러는 처음에 받은 2장의 합계가 16이하이면 반드시 1장의 카드를 추가로 받아야 하고, 17점 이상이면 추가로 받을 수 없다. 딜러가 21을 초과하면 그 시점까지 남아 있던 플레이어들은 가지고 있는 패에 상관 없이 승리해 베팅 금액을 받는다.

고민한 내용

  • 이번에 2단계 미션을 진행하면서 결과(승,패)출력 부분이 불필요하게 되었고, 최종 수익을 출력하도록 Result를 수정하였다.
  • 또 지난 1단계에서 피드백 받은 것 처럼 BlackJack이라는 도메인을 두고, Controller를 제거해보았다. 그런데 이 부분에서 view와의 의존때문에 main() 메소드를 가진 Application과 로직이 반반 나뉘는 느낌이 들어 제대로 피드백을 반영한 것인지 고민이다..
  • 이외에도 기존에 Getter 사용이 잦았던 것 같아 최대한 메시지를 보내서 기능 수행을 요청하도록 수정하려고 노력해보았다.

피드백 내용

  • 지난 1단계에서 consumer를 사용하였다. 그 이유는 inputView와 도메인 로직이 얽혀있기 때문에 이를 풀어내기 위한 방법 중 하나로서 사용한 것이었다. 하지만 consumer를 남용하는 것은 코드의 해석을 어렵게 할 수 있고, 객체의 책임과 역할에 맞지 않을 수 있다. 따라서 도메인과 로직이 얽혀있지 않은 outputView 쪽은 다음과 같이 불필요한 consumer 사용 대신 BlackJack으로 부터 결과를 가져오도록 하여 책임과 역할을 명확히 할 수 있다.

    private static void printFinalResult(BlackJack blackJack) {
            Consumer<User> consumer = user -> outputView.printWithScore(UserDto.from(user), user.getScore());
    private static void printFinalResult(BlackJack blackJack) {
          Map<UserDto, Integer> result = blackJack.getResultCardInfo();
          outputView.printWithScore(result);
    
          Map<String, Integer> revenue = blackJack.calculateRevenueAllUser();
          outputView.printRevenue(revenue);
      }
  • Fixtures 적용하기

    테스트 픽스처(test fixture)란 테스트를 반복적으로 수행할 수 있게 도와주고 매번 동일한 결과를 얻을 수 있게 도와주는 '기반이 되는 상태나 환경'을 의미한다. 여러 테스트에서 공용으로 사용할 수 있는 테스트 픽스처는 테스트의 인스턴스 변수 혹은 별도의 클래스에 모아 본다.

    다음과 같이 테스트 코드에서 중복적으로 사용되는 Card 객체에 대해서 static하게 미리 생성하여 상수로써 관리하고 필요한 곳에서 사용할 수 있도록 하였다.

    public class Fixtures {
        public static final Card SPADE_ACE = new Card(SPADE, ACE);
        public static final Card SPADE_EIGHT = new Card(SPADE, EIGHT);
        public static final Card CLOVER_FIVE = new Card(CLOVER, FIVE);
        public static final Card HEART_KING = new Card(HEART, KING);
        ...
    }
  • Stream 사용과 Function.identity()

        Map<Name, BettingMoney> playerInfo = new HashMap<>();
    
            for (Name playerName : playerNames) {
                BettingMoney bettingMoney = createBettingMoney(playerName);
                playerInfo.put(playerName, bettingMoney);
            }
    
            return playerInfo;

    위의 코드를 다음과 같이 간략화 할 수 있으며 훨씬 가독성 있는 코드가 된다.

    	return playerNames.stream()
                    .collect(Collectors.toMap(Function.identity(), Application::createBettingMoney));

    이 때 사용한 Function.identity() 는 "인자로 넘어온 타입을 받아서 그대로 반환" 이라는 의미의 메소드로 위의 코드를 해석해보면 playerNames에 대해서 스트림을 생성하고, Map으로써 컬렉션을 만들어 반환하는데 이 때 키는 인자로 넘어온 타입 그대로인 "playerNames" 의 타입인 Name이 되고, value는 createBettingMoney() 메소드의 결과가 된다.

  • 로직에 순서 가 존재하는 경우, 이를 메소드 분리등을 통하며 명확히 해주자.
    예를 들어 다음의 코드는 BlackJack에서 추가 카드를 드로우 하는 메소드인데, "플레이어 이후에 딜러" 라고 하는 순서에 대한 조건이 포함되어 있다. 하지만 다음의 코드를 보고는 코드를 처음보는 사람 입장에서 위와 같은 조건을 인지하기 어렵다.

    public void drawAdditionalCard(Consumer<User> consumerPlayer, Consumer<User> consumerDealer) {
        users.drawAdditionalCard(consumerPlayer, consumerDealer);
    }

    따라서 다음과 같이 순서를 명확히 해주는 것이 보다 좋은 코드이다.

    public void drawAdditionalCard(Consumer<User> consumerPlayer, Consumer<User> consumerDealer) {
        users.drawPlayerAdditionalCard(consumerPlayer);
        users.drawDealerAdditionalCard(consumerDealer);
    }

Q. players와 dealer를 구분해서 호출하는 경우가 users 전체를 도는 로직보다 많다면 내부에서 dealer와 players를 분리해서 관리해도 좋을 거 같아요.
return을 할 때 User가 아닌 각각의 클래스로 return 하는 이유가 있을까요??

// 이전 코드
public List<User> getPlayers() {

// 변경된 코드
public List<Player> getPlayers() {

A. 저는 딜러는 BettingMoney를 가질 필요가 없다고 판단하였습니다. (실제로 현재 요구사항에서는 플레이어들의 수익의 반대(ex. 플레이어들 총 수익: 15000 이면 딜러는 -15000)입니다.) 때문에 Player들에만 BettingMoney에 대한 필드를 추가해주었습니다.
그러다보니 Result의 calculateRevenue() 메소드에서는 player의 getRevenue() 메소드 호출이 필요하게 되었고, 그에 따라서 Users 에서 List<Player> 를 명시적으로 반환해줄 필요가 있게 되었습니다. 그렇지 않으면 calculateRevenue() 에서 다운캐스팅을 해주어야했습니다.
저는 calculateRevenue() 에서 다운캐스팅을 하는 것보다 메소드 명에도 명시되어 있다시피 "getPlayers()" 메소드에서 Player로 다운캐스팅하는 것이 보다 나은 것 같아 현재와 같이 구현하였습니다...
(결국 다운캐스팅 해준다는 점에서는 동일)

이전에는 List 하나로 관리하고 싶어 이와 같이 구현해보았는데, 말씀해주신 것과 같이 users 전체를 도는 로직이 있다보니 dealer와 players를 분리해서 관리하는게 더 좋아보입니다.
따라서 이전 설계대로 한 번 players와 dealer를 분리해서 관리해보도록 수정해보겠습니다.

Q. player만의 메소드가 생겼다고 했는데, 추상클래스를 쓸 때 고려해야하는 부분이 어떤 부분이 있는지 고민해보시면 좋을거 같아요

A. 저는 우선 User와 Player, Dealer를 상속을 통해 구현한 이유는 다음과 같습니다..!
("개발자가 반드시 정복해야할 객체지향과 디자인패턴" 스터디를 하며 정리한 상속의 개념입니다.)

"상속이란 "코드의 재활용"을 위한 즉 "재사용"을 위한 개념은 아니다. 상속은 "연관된 일련의 클래스들에 공통적인 규약을 정의"하기 위해 사용하는 것이고 재사용의 관점보다는 "기능의 확장(extends)의 관점"이다.
따라서 명확한 IS-A 관계이고, 상위 클래스의 기능을 확장해 나가는 경우 사용하면 좋다."

이러한 점에서 User는 "연관된 일련의 클래스들에 공통적인 규약을 정의" 한다는 내용에 부합하고있다고 생각하고, Player만의 메소드가 생긴 부분은 "기능의 확장"의 관점으로 볼 수 있다고 생각합니다. 또 다른 부분들도 고민을해보았는데 LSP 원칙도 잘 지키고 있고..저는 Player에 getBettingMoney()와 같이 Player만의 메소드가 생긴 것에 문제가 없는 것 같은데요...혹시 "추상클래스"를 쓸 때 고려할 수 있는 부분이 어떤 부분이 있을지 힌트를 주실 수 있을까요..ㅠㅠㅠ

이전에 다운캐스팅 사용과 같은 문제를 직면하면서 User에 getMoney()라는 추상메소드를 두고 Dealer에서는 0을 반환하도록 구현을 할까 하다가도 "Dealer에서 0을 반환"이라는 메소드(기능)는 제대로된 구현이 아니라는 생각에 도입하지 않았습니다. 이번에 수정한 코드에서 또한 딜러에서 BettingMoney 대신 null을 반환한다고 해도 적절하지 않은 "기능 구현"이라는 생각도 들어 시도하지 않았습니다..🥲

이에 대한 리뷰어님의 답변: 캐스팅을 하는 부분이 생기면 IS-A가 맞는지 생각해보게되는거 같은데요.
추상클래스를 쓰다보면 상속받는 클래스들에서 각자만 사용하는 메서드가 생기고 역할이 커지게 되는 경우들이 있는데요.
이때는 정말 추상클래스와 같은 클래스인가? 역할과 책임이 같은가? 라는 생각을 하면서 클래스를 분리하는 것도 고려를 하게 되는거 같아요.
이런 부분을 한번 생각해보시라고 코멘트 달았던 것이었습니다.
문제가 있어서는 아니였어요!
아래 의견은 개인적인 생각입니다. 반영은 안하셔도 돼요.
말씀하신대로 User에 추상메서드를 두는 것은 둘 모두에서 사용하지 않기 때문에 맞지 않다고 생각하는데요.
다른 관점으로 money를 player가 가지고 있어야 하나라고 생각할 수도 있을 것 같습니다~

  • 무조건 forEach가 좋은게 아니다!
    users.getPlayers()
                    .stream()
                    .forEach(player -> result.put(UserDto.from(player), player.getScore()));
    위와 같은 경우에는 forEach 보다는 for문이 더 명확하다.
    그리고 이 경우에는 stream으로 map을 반환해서 putall을 할 수도 있다.
    참고자료: Stream의 forEach와 for-loop는 다르다.
    위의 참고자료를 통해서도 알 수 있듯이 Stream.forEach() 를 사용할 때, 로직이 들어가 있는 경우 자신이 Stream을 잘 활용하고 있는건지에 대해서 고민해보아야 한다.
    그 이유에 대해서 간략히 정리하면 다음과 같다.
    무언가 강제적인 종료 조건에 대한 로직이 있는 for-loop를 stream.forEach()로 구현하게 되면 기존 for-loop에 비해 비효율이 발생하기 때문이다.
    또한 스트림 병렬화에 대한 공식 문서의 side-effect 항목을 참고하면, forEach 내부에 로직이 하나라도 추가되는 경우 동시성 보장이 어려워지고 가독성이 떨어질 위험이 있다고 언급하고 있다.
    마지막으로 Stream의 의도를 벗어나게 된다. 원래 로직을 수행하는 역할은 Stream을 반환하는 중간 연산이 해야하는 일인데, 최종연산인 forEach에서 수행하는 것은 적절하지 않다.
    이와 관련하여 이펙티브 자바에서는 다음과 같이 이야기 하고 있다.

    forEach 연산은 최종 연산 중 기능이 가장 적고 가장 '덜' 스트림 답기 때문에, forEach 연산은 스트림 계산 결과를 보고할 때(주로 print 기능)만 사용하고 계산하는 데는 쓰지 말자.

profile
꾸준함에서 의미를 찾자!

0개의 댓글