Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[1단계 - 블랙잭 구현] 조엘(조영상) 미션 제출합니다. #146

Merged
merged 85 commits into from
Mar 9, 2021

Conversation

joelonsw
Copy link

@joelonsw joelonsw commented Mar 4, 2021

안녕하세요 지노! 우테코 3기 조엘입니다.
블랙잭 미션을 페어들과 함께 작성하고, 추가적인 개인 리팩토링은 아직 많이 하지 않았어요.
지노의 의견을 들어보고 리팩토링에 들어갈 생각입니다.

제가 이번 미션을 진행하면서 고민한 부분은 다음과 같아요

1. Controller에서 처리해줄 로직이 너무 많다.
TDD를 통해 객체를 설계하고, 이를 블랙잭 게임에 알맞게 컨트롤러에서 조립하다보니 컨트롤러가 이전 미션에 비해 길어지게 됐어요.
이를 해결할 방법으로 도메인에 게임을 진행해주는 클래스를 생성하거나, 컨트롤러를 역할 별로(초기화/게임진행/승패결정) 나누자라는 의견이 나왔어요.
하지만 게임을 진행하는 클래스를 도메인에 생성하는 것은 컨트롤러의 역할을 위임한다고 밖에 생각이 들지 않았고,
컨트롤러를 잘게 나누는 것은 그저 컨트롤러가 길기 때문에 나누는 느낌이 강하게 들었어요.

다른 크루가 컨트롤러가 너무 길어진 것은 어쩌면 domain에서 처리해줘야할 일을 컨트롤러에서 해주는 거일 수 있다고 했어요.
리팩토링하면서 이에 대해 더 생각해볼 예정이에요.

2. Participant 클래스의 List<Card>
Participant 클래스는 인스턴스 변수로 List<Card>를 가지고 있어요.
페어들과 함께 List<Card>를 일급 컬렉션으로 만들까에 대한 의논을 했었는데, 결과적으로 현재는 따로 빼지 않기로 했어요.

Participant 클래스 내의 로직들이 주로 List<Card>를 관리하는 로직들이였어요.
카드를 받고, 현재 카드들의 가치를 계산하고, Bust가 되었는지 확인하는,
일급 컬렉션이였으면 아마 같이 분리되었을 로직들을 Participant가 가지고 있더라고요.

이를 따로 일급컬렉션으로 분리하려고 하니까, Participant에서 실질적으로 처리하는 로직은 자신의 이름을 받고, getter로 넘겨주는 것 밖에 안남더라고요.

물론 따로 일급컬렉션으로 List<Card>를 분리했다면, 재활용이 더 용이하고, List<Card>에 대한 상태/행위만 관리하기 때문에 장점도 분명히 있을 것 같아요.
어쩌면 이름이 생겨 가독성이 더 좋아질 수도 있고요.

하지만 참가자의 카드 목록을 컨트롤러로 가져오기 위해, 두 번의 getter가 필요하고 (DTO를 도입한다면 해결이 될 듯 하기도 하네요)
참가자 별로 List<Card>를 관리하는 객체를 하나 더 생성하는 것이 필수적인가에 대해 의문이였던것 같아요.
(사실... 생각을 글로 정리하고 나니, DTO를 도입해 getter에 대한 문제를 해결하고, 일급컬렉션으로 List<Card>를 포장하면 장점만 취하는 좋은 방안이 될 수 있을것 같네요)


제가 리팩토링 하려고 생각하고 있는 부분은 다음과 같아요

1. DTO 도입
사실 이전 미션까지는 DTO의 필요성을 느끼지 못했고, 그에 따라 DTO를 도입하지 않았었어요.
불변 객체를 만들면 레이어 간에 안전한 운반이 가능하다고 생각했어요.
하지만 이번에 진행한 블랙잭 미션은 그렇지 않았어요.
우선 participant내에 Dealer와 Player같은 경우, 승패를 가리는 필드가 유동적이에요.
그렇기 때문에 불변성을 보장할 수 없었고, DTO가 이를 해결해줄 수 있지 않을까 생각하고 있어요.
지금은 우선 controller에서 필요한 데이터를 getter를 통해 가져온 다음, view에 보내주고 있어요.

2. 게임 결과를 만들고 관리하는 클래스 생성
현재는 controller에서 플레이어와 딜러간의 결과를 비교하고, 결과를 필드값으로 지정해줬어요.
하지만 게임의 결과를 관리해주는 클래스를 만들어줘야 할 필요성을 느껴요.
우선 현재 코드의 문제점은 controller에서 플레이어와 딜러의 결과값을 하나하나 비교해주고,
플레이어와 딜러에게 각각 승패를 지정해주는 것이라고 생각합니다.

플레이어가 직접 딜러의 결과를 보고 스스로 승패를 정하는 방식으로 코드를 작성하는게 더 좋은 코드라는 생각이 드네요.
혹은 Map을 사용해 플레이어가 승리했는지, 패배했는지를 인스턴스 변수로 저장해 둘 수도 있을 것 같고요.
이는 추후에 더 고민해볼게요.

3. Dealer클래스와 Player 클래스의 추상화
이 부분이 개선이 되면 코드가 훨씬 간결해지지 않을까 싶어요.
사실 현재는 Participant 클래스로 공통된 메서드와 필드를 추출한 정도에 지나지 않아요.
추상화를 잘 해냈다면, Dealer와 Player를 한 번에 관리하는 클래스를 도메인에 만들수도 있을 것 같고,
controller에서 Dealer객체와 List<Player>를 따로따로 만들어줘 비슷한 로직을 컨트롤러에서 수행해준 부분도 개선이 가능할 것 같아요.

리뷰 기다리고 있겠습니다!
코로나 조심하세요 :)

@joelonsw
Copy link
Author

joelonsw commented Mar 8, 2021

안녕하세요 지노! 우테코 3기 조엘입니다.

다음과 같은 사안에 집중하며 리팩토링 해봤어요

  • 어느 클래스에 어떤 책임을 부여해야 하는가

    • 원래는 결과를 담당하는 클래스를 따로 구현했었어요.
    • 하지만 players 클래스에서 players에 대한 결과 생성을 담당하고, dealer 클래스에서 dealer에 대한 결과 생성을 담당하는 것이 자연스럽다고 생각했어요.
    • 점수의 높고 낮음을 비교해 승패를 정하는 것은 Result Enum 클래스로 역할을 위임했어요.
      • 각 Enum 객체 별로 로직을 정의해주어 if-else문을 줄여나갈 수 있었어요.
  • Dealer/Player의 추상화

    • Participant 클래스를 추상클래스로 정의해 인스턴스화를 방지했어요. 또한 Dealer/Player에서 각각 다르게 정의되야 할 부분을 추상 메서드로 정의해 두었어요.
    • Dealer/Player와 Participant 사이에 명확한 "is-a" 관계가 성립된다고 생각해요. 따라서, 인터페이스보다 추상클래스로 구현하는게 맞다고 생각했어요.
  • controller/domain 내의 view 로직 제거

    • 불변이 보장이 안되는 domain 객체더라도 우선적으로 view로 보내 getter를 사용하는 방향으로 리팩토링했어요.
    • DTO가 필요해진다면 2단계에서 공부해보고 적용해볼 생각이에요.
  • 의도에 알맞는 메서드명/변수명

    • 좋은 네이밍에 대해 고민해보고 적용하려 노력했어요. 부족한 네이밍은 지적해주세요!
  • ace가 들어있을 경우, 점수 계산 로직 간소화

    • List를 관리하는 일급컬렉션인 Hand 클래스에서, 카드패의 점수를 구하는 로직을 간소화 시켰어요. 또한 메서드 명만 보고도 어떤 작업을 해주는지 알수있도록 작성하려고 노력했어요.

피드백 기다리고 있겠습니다!
코로나 조심하세요~ 💪

@joelonsw
Copy link
Author

joelonsw commented Mar 8, 2021

다음과 같은 사안을 고민했어요

1. Domain 클래스로 Players VS Controller에서 사용하는 List

리팩토링 하면서 이 부분에 대해서 고민을 가장 많이 했던 것 같아요.
Players로 따로 참가자에 대한 클래스를 생성하자니, 처음에는 해당 클래스의 역할과 책임이 그저 Player들을 모아놨다고만 생각이 들더라고요.
또한 Controller에서 플레이어 개개인의 의사를 View단과 소통하면서 정해주는 로직이 필요한데,
해당 작업을 진행해주려면 일급 컬렉션인 Players에 정의된 List를 getter를 통해 가져왔어야 했어요.

private void progressEveryPlayerGame(final Players players, final CardDeck cardDeck) {
    for (final Player player : players.getPlayers()) {
        progressSinglePlayerGame(cardDeck, player);
    }
} 

이럴바엔 List를 Controller에서 정의해주고 사용하는게 낫겠구나 싶어,
Players 클래스를 만들지 않고 리팩토링을 계속 진행했습니다.

하지만, 모든 플레이어의 이름이 중복되지 않게 초기화 되어야 함을 깨닫고 이를 책임질 클래스가 있어야 겠구나 생각이 들었고,
또한 모든 플레이어의 게임 결과를 한번에 관리해줄 클래스가 필요함을 깨달았어요.

결국 모든 플레이어들의 게임 진행 및 결과 생성을 책임지는 Players 클래스를 생성해주었습니다.
현재로써는 Players 클래스를 생성해주길 잘한 것 같아요! 지노 생각은 어떤지 궁금합니다.

2. View에서 많은 getter가 필요함

현재 코드 상에서는 view단에서 필요한 정보를 출력해주기 위해서는 많은 getter가 필요해요.

#146 (comment)
지노의 해당 피드백을 받고, getter는 최대한 인스턴스 필드 자체를 반환하도록 설계했어요.
그러다 보니 view단에서 두,세 단계의 getter를 통해 정보를 거의 끄집어 내오는 느낌의 코드가 작성이 되었어요.

해당 코드의 단점으로는, 도메인 객체가 어떻게 구성되어있는지 명확히 알아야 필요한 정보를 정확히 가져올 수 있지 않나 라는 생각이 들어요.
getter를 통해 가져오기 때문에 캡슐화가 깨진다고까지는 말을 못하겠으나,
내부 구현을 상세히 알지 않아도 필요한 응답을 받을 수 있는 객체 지향 및 캡슐화의 의도와 반한다는 생각이 들어요.

해결책으로는 두 가지가 생각나는데요,

[1 - 한 번에 내부 구현체를 가져오게 한다]
예를 들면 다음과 같아요. 현재 Dealer의 카드 목록을 가져오기 위해서는 Dealer.getHand().getCards() 를 통해 가져와야 해요.
Dealer.getCardsHolding() 과 같은 메서드를 Dealer 클래스에 새롭게 정의해주어 바로 Hand의 List를 반환하도록 하는 거에요.

하지만 이 방법은 좋지 않아보여요.
결국 Hand의 내부 구현을 알고 있기 때문에 작성할 수 있는 코드라서 이 역시 캡슐화에 반하는 것 같고,
지노의 피드백 대로 getter는 인스턴스 필드 자체를 반환해주는게 getter의 의도에 맞다고 생각하기 때문이에요.
해당 고민에 대해서는
#146 (comment)
여기에서 한 번더 언급했어요!

[2 - DTO를 도입한다]
DTO를 자세히는 모르지만, view단에 필요한 필드값을 지정해주고 채워 넣은 후에, Domain에서 View단으로 보내준 후 출력에 사용한다고 알고있어요.
view단에 필요한 것들만 골라서 보내주기 때문에 getter를 한 번씩만 써줄 수 있을 것 같고,
또한 불변을 보장하기에 thread-safe 등의 이점을 누리기 쉬울 것 같아요.
(다른 크루에게 물어보니까 DTO는 불변을 보장하지 않는다고 하네요...)
다만 유지 보수해줄 클래스가 그 만큼 늘어나고, 어쩌면 객체가 많아져 메모리 관리에도 신경을 써야하지 않을까라는 단점이 생각이 됩니다.
하지만 해당 방법을 도입해보고 싶은 욕심이 드네요! 직접 장단점을 느껴보고 싶습니다 :)

지노의 생각은 어떤가요? View의 getter 로직을 줄이기 위해 어떤 방법이 있을까요?
또한 DTO를 도입하는 것의 장단점은 제가 맞게 생각하고 있는가 궁금합니다 :)

@joelonsw
Copy link
Author

joelonsw commented Mar 8, 2021

학습로그

[JDK] Stream.forEach vs Collection.forEach - 2

내용

  • Stream.forEach

    • Stream 객체를 생성한 후에 forEach 메서드 호출
      • 따라서 중간 연산에 해당하는 로직과 함께 써야 유의미하게 Stream을 사용한 것
    • 반복 도중에 다른 쓰레드에 의해 수정될 수 있다
    • parallelStream을 통해 여러 쓰레드에서 스트림을 실행할 수 있다
    • 무조건 요소의 끝까지 반복을 돌게 된다.
  • Collection.forEach

    • 따로 객체를 생성하지 않고 forEach 메서드 호출
      • forEach 메서드는 Iterable 인터페이스의 default 메서드
      • Collection 인터페이스에서 Iterable 인터페이스를 상속하기에 바로 사용한다.
        • 인터페이스는 인터페이스를 상속 받을 수 있다 (다중 상속도 가능하다!)
    • 멀티쓰레드 환경에서 락이 걸려있기에 더 안전하게 사용할 수 있다

링크

[OOP] Enum 활용 - 1

내용

  • 정의
    • 관련한 상수 Singleton 객체의 모음집
    • 컴파일 당시, 가능한 모든 값을 안다면 Enum 활용을 고려할 것
  • 장점
    • Java의 Enum은 상태와 행위를 동시에 관리할 수 있다
    • 컴파일 시에 정의한 타입 이외의 타입에 대해 오류를 발생시킨다

링크

@joelonsw
Copy link
Author

joelonsw commented Mar 9, 2021

+ 추가고민)
현재 Dealer, Players 클래스에서 결과를 확인하고 필요한 데이터를 반환하는 메서드의 이름을 check~Result 형식으로 작성했어요.

check라는 네이밍이 근데 좋은지 모르겠네요ㅜㅜ 괜히 메서드명만 봤을때에는 boolean을 리턴 받아야 할 것 같거든요.

게임 결과를 확인하고 필요한 자료구조를 반환하는 메서드의 이름은 무엇으로 지으면 좋을까요?

+ 리팩토링)
참가자의 결과를 확인하는 메서드의 네이밍은 check~Result에서 generate~Result로,
Enum객체로 결과를 반환하는 메서드의 네이밍은 chekResult에서 findResult로 변경했습니다!

@joelonsw
Copy link
Author

joelonsw commented Mar 9, 2021

학습로그

[JDK] new ArrayList<>() vs Arrays.asList() - 2

내용

  • new ArrayList<>()

    • java.util.ArrayList 소속
    • List에 대한 모든 operation을 쓸 수 있음
    • new로 생성해주기에, 넘겨받은 원본 리스트에 대한 참조는 끊는다
      • 여기서 참조를 끊는다는 것은, 새로운 주소에 List를 만들고, 넘겨받은 List들의 원소를 하나하나 복사해주어 만들어 반환한다는 것
      • 넘겨준 원본 List에 add, remove등의 연산을 해도 새롭게 만든 ArrayList에는 영향이 가지 않는다
      • 하지만 List안에 들어있는 객체들은 그대로 얕은 복사가 되기 때문에 넘겨준 원본 List에서 원소로 가지고 있는 객체의 필드값을 조작하면, 새롭게 만든 ArrayList 내의 원소로 포함된 객체에도 영향을 미친다.
  • Arrays.asList()

    • java.util.Arrays.ArrayList 소속
    • 읽기만 가능한 List
    • add(), remove() 메서드를 통해 해당 list에 원소를 추가/삭제하려고 하면...
      • "UnsupportedOperationException"을 반환
    • 원본 배열의 참조도 끊지 않는다. 즉 얕은 복사로 주소를 공유한다.
    • List 형태로 배열을 포장한 것이라고 보면 됨

링크

[JDK] HashMap vs LinkedHashMap - 3

내용

  • HashMap

    • 중복된 원소를 허용하지 않으며, 순서도 고려되지 않음
    • HashMap은 key와 value를 저장하는 두개의 배열로 구성되어 있음
    • Key값이 어디에 저장될지는 다음과 같이 정의됨
      • Key값으로 저장할 Object의 hashCode()를 구한다
      • 정수로 반환된 hashCode() 값을 버킷의 수로 나눈 나머지를 구한다 (초기 버킷 수는 16개)
      • 그렇게 구한 값이 배열의 index가 된다
      • 해당 index에 Object를 저장한다
  • LinkedHashMap

    • HashMap과 동일하게 데이터를 저장
    • 하지만 Doubly-Linked-List를 통해 데이터 저장 순서를 동시에 기록함
    • 따라서, 데이터가 저장된 순서대로 접근할 수 있음

링크

@hyunssooo
Copy link

hyunssooo commented Mar 9, 2021

안녕하세요 조엘! 질문 확인이 늦었네요! 😭

현재로써는 Players 클래스를 생성해주길 잘한 것 같아요!

player에 대한 일급컬랙션은 아주 좋은 방법이라고 생각합니다! :) 일급컬렌션이 필수는 아니지만 저는 얻는 이점이 많다고 생각해요!
이렇게 구현한 일급컬렉션은 도메인 객체이니 적절한 책임과 역할을 부여해주면 좋을 것 같네요! 👍

한 번에 내부 구현체를 가져오게 한다

현재는 view에서 어떠한 방법을 쓰셔도 괜찮습니다! :) 개인적으로 뷰에서는 get get도 나쁘지 않다고 생각해요!
만약 get 한 필드가 컬렉션이라면 for문 혹은 메서드로 분리해 각각의 element를 전달해 출력할 수도 있을 것 같네요! :)

List<player> elements = players.getElements();

for(player element : elements) {
  ..
}

private void print(Player player) {
  ..
}

DTO를 도입한다.

현재는 저는 그다지 추천하지는 않습니다. DTO는 date transfer object로 데이터 전달 오브젝트를 뜻해요!
DTO를 자세히 모른다고 하셨지만 충분히 자세히 아시는 것 같은데요?? 🙂
유지 보수해줄 클래스가 늘어나는 것도 사실입니다! 객체 생성이 많아진다면 메모리도 더 차지하는 것은 맞지만 dto 는 1회성이기 때문에 메모리 관리까지는 크게 신경 써줄 필요는 없다고 생각이 드네요! 또한, DTO는 불변을 보장하지는 않지만 setter 메서드를 제거하고 생성자로 필요한 데이터들을 받는다면 충분히 불변을 보장할 수 있다고 생각해요 :)

전에도 말했지만 조엘이 DTO를 도입한다는 걸 저는 말리지는 않을 거지만 현재는 그다지 필요는 없을 것 같습니다 :)

Copy link

@hyunssooo hyunssooo left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

안녕하세요 조엘!
step1 진행한다고 수고 많으셨어요!
pr에 코멘트를 보니 제가 너무 오래 기다리게 한 거 같아요.😭
step2 진행하면서 궁금한 점은 언제든 DM 주세요! 감사합니다!

OutputView.showDealerGameResult(dealer, dealer.generateEveryResult(players));
OutputView.showPlayerGameResult(players.generateEveryPlayerResult(dealer));
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
}
}

마지막 줄은 개행으로 끝납니다 :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 적용했습니다! 코드 컨벤션 배워갑니당 💪

Comment on lines +11 to +22
public CardDeck() {
this.cards = new LinkedList<>();
initialize();
}

private void initialize() {
for (final CardSuit type : CardSuit.values()) {
Arrays.stream(CardLetter.values())
.forEach(number -> cards.add(new Card(number, type)));
}
Collections.shuffle(cards);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

cards를 내부에서 초기화하는 것과 생성자로 주입 받는 것은 어떤 차이가 있을까요??

Comment on lines +25 to +32
public void receiveInitialCard(final CardDeck cardDeck) {
hand.add(cardDeck.distribute());
hand.add(cardDeck.distribute());
}

public void receiveAdditionalCard(final Card card) {
hand.add(card);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

추상클래스의 메서드를 override 못하게 하려면 메서드에 final 키워드를 붙여 주면 됩니다!

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 적용했습니다! 감사합니다 :)

Comment on lines +22 to +35
public static Players of(final List<String> playerName) {
validateDuplicate(playerName);
final List<Player> players = playerName.stream()
.map(Player::new)
.collect(Collectors.toList());
return new Players(players);
}

private static void validateDuplicate(final List<String> playerName) {
final int distinctName = (int) playerName.stream().distinct().count();
if (distinctName != playerName.size()) {
throw new IllegalArgumentException("입력된 플레이어의 이름이 중복됩니다.");
}
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

player는 이름이 같으면 같은 객체로 취급하나요?
그렇다면 player에 equals(), hashCode()를 재정의하고 player를 set에 넣어보는 것도 방법이겠네요! :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이름이 같으면 같은 객체로 고려하도록 equals(), hashCode()를 정의해주었고, 테스트코드 작성을 완료했습니다!

Comment on lines +7 to +10
@Override
boolean condition(int score, int opponentScore) {
return score > opponentScore;
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

functional interface도 적용할 수 있을 것 같네요! :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

해당 글을 참고하여 수정해봤습니다! 맞게 한건지는 잘 모르겠으나 테스트케이스가 통과하여 우선 commit 했습니다 :)

joelonsw@d472685

Comment on lines +39 to +41
private int countAce() {
return (int) cards.stream().filter(Card::isAce).count();
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private int countAce() {
return (int) cards.stream().filter(Card::isAce).count();
}
private int countAce() {
return (int) cards.stream()
.filter(Card::isAce)
.count();
}

개행으로 가독성을 높일 수 있겠네요! :)

Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

네 적용했습니다~

@hyunssooo hyunssooo merged commit d7fbc8d into woowacourse:papimonlikelion Mar 9, 2021
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants