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단계 - 블랙잭 구현] 에어(김준서) 미션 제출합니다. #141

Merged
merged 70 commits into from
Mar 10, 2021

Conversation

KJunseo
Copy link

@KJunseo KJunseo commented Mar 4, 2021

안녕하세요 지노!! 이번 미션을 함께하게 된 에어라고 해요! 잘 부탁드립니다 😄
이번 미션을 진행하는데 시간에 맞춰서 하기가 정말 쉽지 않네요.. 시간 맞춰 하려 하다보니 스스로 고칠 것이 너무 많이 보이는 것 같아요ㅠㅠ

TDD가 감이 오는 듯 했는데 이번 미션 진행하면서 다시 감이 흐려진 느낌이네요 ㅠㅠ 이번 코드 리뷰를 통해서도 많이 배울 것을 기대하고 있습니다! ㅎㅎ 많은 피드백 부탁드려요~~

이해해주신 덕분에 코드를 조금 다듬긴 했는데 좋은 코드라는 생각은 들지 않네요 ㅠㅠ 리뷰를 통해 좋은 코드로 변화시켜보고 싶습니다. 많은 힌트와 리뷰 부탁드릴게요!! 👍🏻


궁금증

이번 미션을 진행하면서 페어랑 이야기해보면서 많은 토론을 했는데 물어보고 싶은게 많아요

Q1. 객체 생성 테스트가 궁금해요. 페어랑 생성 테스트에 대해 이야기해봤는데, isNotNull로 객체가 생성된 것만 확인해도 생성 테스트가 되지 않느냐는 의견을 들었어요. 여태까지는 isEqualTo를 사용해서 생성테스트를 했었는데 isNotNull로만 생성 테스트를 해줘도 괜찮을까요?

        Card card = new Card(Shape.SPADE, Denomination.ACE);
        assertThat(card).isEqualTo(new Card(Shape.SPADE, Denomination.ACE));
        Card card = new Card(Shape.SPADE, Denomination.ACE);
        assertThat(card).isNotNull();

Q2. 카드덱에서 카드를 뽑으면서 동시에 제거를 해주려고 했는데, List를 포장한 일급컬렉션인 Cards를 불변 객체로 하려다가 보니 CardDeck에서 카드를 한 장 뽑을 때, 뽑을 카드를 확인하는 메서드와 실제로 CardDeck에서 카드를 제거하는 메서드가 분리되게 되었어요. 이렇게 분리해서 사용하는 게 괜찮을까요?

Q3. 카드 점수 계산에 관해서 질문이있어요. 여러가지 방법이 있을 것 같은데

  1. Player나, Dealer의 카드가 업데이트 될 때마다 카드를 넘겨서 새로 점수를 계산한다.
  2. Player와 Dealer의 조상인 Person에 점수를 저장하는 변수를 만들어서 저장한다.

현재는 1)번 같이 구현했는데 계속해서 중복된 계산을 하게 될 것 같아서 지노의 의견을 듣고 싶어요

Q4. 마찬가지로 점수 계산 관련 질문인데요. 저는 cards에서 계산을 해줘도 된다고 생각했는데, cards는 카드를 뽑고, 확인하고 하는 기능만 가지고 있는게 맞는 것 같다는 의견을 들었어요. 현재는 카드에서 계산하게 구현했습니다! cards에서 점수계산 하는 것이 올바른 책임이 아닌 걸까요?

Q5. 만약 inputView 내에서 try-catch를 하게 된다면 InputView에서 에러 메세지를 출력하기 위해 OutputView를 호출해도 되는 걸까요?

Q6. 현재 컨트롤러 역할을 하는 BlackJackGame의 distributeCards()와 playersTurn()에서 getter를 사용해서 하고 있는데 getter를 사용하지 않는 방법이 있을까요? 있다면 힌트 부탁드려요!

Q7. player와 dealer가 person을 상속하는데 person에서 protected 접근제한자를 사용중입니다. player와 dealer에서는 super.xx 으로 person의 멤버변수에 접근하여 사용중인데, person의 접근제한자를 private으로 두고 getter로 사용하는 것이 더 좋다는 말을 들었습니다. 어떻게 생각하시나요?

Q8. person과 같은 추상 클래스는 어떻게 테스트를 해야할까요?

Q9. view에서는 출력을 위해 getter를 사용할 수 밖에 없다고 알고 있습니다. 그런데 객체들이 너무 포장이 되어 있어서 원래 값을 꺼낼 때 getter가 너무 이어지게 되는 것 같아요 ㅠㅠ view라서 괜찮은가? 라는 생각이 들기도 하지만 너무 데메테르 법칙을 위반하는 것 같기도 하여 조언을 구하고 싶어요ㅠㅠ

// 예시(in OutputView)

dealer.getName().getName()

dealer.getCurrentCards().peekCard()

entrySet.getKey().getResult()

Q10. getter 사용을 지양하라는 말을 많이 들었어요. 그런데 stream을 사용할 때 .map으로 객체의 어떤 타입으로 매핑하는 경우가 있는데 이런 경우에는 get을 사용해도 되는것인가요?

이 예시는 이번 미션에서 사용한 코드는 아니지만 궁금해서 여쭤봐요!

int calories = menu.stream()
    .map(Dish::getCalories) // 이런 부분입니다!
    ....

학습로그

[OOP] 상속 - 3

내용

  • player와 dealer의 중복 코드를 상속을 이용해 제거
  • 상속을 사용하긴 했지만 아직 장점이 막 와닿지는 않는다.

링크

[TDD] TDD 적용 - 3

내용

  • todo list를 작성 후 가장 간단한 도메인부터 테스트 코드 작성 -> 프로덕션 코드 작성 -> 리팩토링 과정을 거쳐 코딩하려고 노력했다.
  • 몇 몇 부분에서 설계가 부족했는지 테스트 코드 부터 짜기가 힘들어 프로덕션 코드 작성 후 단위 테스트를 한 부분도 있어 아쉬웠다.
  • 나중에 코드가 변경되더라도 테스트를 돌려보며 제대로 변경했다는 것을 알 수 있어서 좋았다.

링크

[JDK] Stream API - 3

내용

  • 컬렉션을 가공해야할 때 스트림을 적용해보려고 했다.
  • 점수 계산, 최종 결과 계산, 캐싱

링크

[Java] Enum - 3

내용

  • 카드 모양, 카드 번호, 게임 결과에 enum적용
  • enum 내부에 추상 메서드로 각 상수 객체가 메서드를 override하도록 구현해봄

링크

[OOP] 일급콜렉션 사용 - 3

내용

  • Cards, Players로 일급콜렉션 적용

링크

[자료구조] LinkedHashMap - 1

내용

  • 최종 결과 출력 시 순서를 유지하기 위해 LinkedHashMap 사용

링크

[OOP] 원시값 포장 - 1

내용

  • Name으로 원시값 포장

링크

pika96 added 30 commits March 2, 2021 15:49
@KJunseo
Copy link
Author

KJunseo commented Mar 8, 2021

안녕하세요 지노! 말씀해주신 피드백 적용해보았습니다!!
몰랐던 것들을 알게 된 것 같아 좋네요 ㅎㅎ


답변해주신 것에 대한 추가 궁금증

1.답변 주신 VO 관련해서 고민해봤어요! Cards 일급 컬렉션의 경우 카드가 같아도 다른 Cards가 될 수 있다고 생각했지만, 이 블랙잭 게임의 경우 카드가 52장만 존재하니까 같은 카드를 가진 Cards는 서로 같게 봐야한다고 판단했어요.

그렇게 해야 Participant 추상 클래스가 Nickname과 Cards를 가지고 있는데, Nickname과 Cards가 둘 다 같아야지 같은 Participant로 볼 수 있다고 생각했어요.

다만 Players 같은 경우에는 같은 Player들이 있더라도 각각 다른 모임?으로 볼 수 있을 것 같아서 VO로 판단하지 않았습니다!

2.의존성 주입에 대해 공부해봤습니다. 외부에서 객체를 생성해서 주입해주면 클래스를 객체 생성과 독립적으로 만들 수 있습니다. 클래스 내부에서 객체를 생성하게 된다면 클래스의 수정없이 독립적으로 인스턴스 생성을 변경할 수 없게되어 유연하지 못하고 재사용할 수도 없게됩니다!

따라서 Players 외부에서 List를 만들어 준 후, Players에 주입해주면 후에 다른 형식의 input이 들어와도 Players의 코드는 변경 없이 List로 만들어주는 부분만 수정하면 된다고 생각해요!

3.view에서 도메인 객체의 메서드를 호출하는 식으로 작성된 코드를 어떻게 바꿔야하나 고민하다가 dto를 적용해보라는 뜻으로 이해를 해서 적용해보았습니다. dto를 한 번도 써본 적이 없어서 제대로 적용한지는 잘 모르겠네요 ㅠㅠ

dto의 멤버변수를 어떤 타입으로 둬야할지를 잘 모르겠더라고요.
1)view에서 바로 출력을 할 수 있게 dto 객체의 필드를 다 원시값이나 String 값으로 두고 dto를 생성할 때 포장된 객체에서 값을 꺼낸 후 dto 생성자에 넣어줘야할지,
2)아니면 포장된 값 자체를 필드로 가지게 해서 view에서 꺼내 써야할지를 잘 모르겠더라고요.

1)번 방법으로 하려니 도메인 객체에서 getter를 써야해서 현재는 2)번 방법으로 했어요!

또 현재 각 도메인에서 toXXXDto 라는 메서드를 만들어서 Dto로 변환해주고 있는데 이렇게 사용해도 되는건지 궁금해요!

4.테스트가 더 필요한 부분에 대하여 고민해보고 몇가지 테스트들을 추가했어요! 혹시 부족한 부분이 있으면 말해주세요!


새로운 궁금증

추가적으로 궁금한 것이 생겼어요!

Q1. 처음 service를 분리한 것이 controller가 너무 커지는 것 같아서였어요! 그래서 View와 연결되지 않은 메서드들만 service로 분리했었는데, 이런 로직을 controller에서 처리해주면 controller가 너무 길지는 않을까요?

페어에게서 Controller를 게임 세팅 컨트롤러, 게임 진행 컨트롤러 등으로 나누는 것은 어떠냐는 의견도 들었는데 어떻게 생각하시나요?? 아니면 아에 다른 도메인 객체를 만들어야할까요?(이 경우는 시도해보지 않아서 잘 모르겠지만 controller에서 get을 사용해야할거 같다는 생각도 들긴하네요ㅠㅠ)

Q2. 현재 CardDeck의 변수가 Cards 하나 밖에 없는 상태에요. 이러다보니 굳이 클래스를 나눠야하나? 라는 생각이 들기도 해요. Cards deck = new Cards() 이런식으로 따로 클래스를 만들지않고 Cards 객체로 만들어도 별 차이가 없지 않나라는 생각이 들어서 어떻게 생각하시는지 듣고 싶네요 ㅎㅎ

Q3. 코딩 컨벤션을 찾아봤는데 이렇게 나와있었어요.

  1. Static 변수
  2. 인스턴스 변수
  3. 생성자
  4. 메서드(기능 별로 분리. public 사이에 private 메서드가 존재할 수 있다.)

보통 toString, equals, hashCode가 제일 하단. 그 위에 getter까지는 들은 적이 있는데 static 메서드와 일반 메서드가 같이 존재할 경우 static 메서드는 따로 위로 빼줘야할까요 아니면 상관없이 기능에 따라 배치하면 될까요?

Q4. 만약 정적 팩토리 메서드를 사용한다면 정적 팩토리 메서드 내부에서 검증을 해줘야 할까요? 아니면 생성자에서 검증을 해줘야할까요?

생성자와 정적 팩토리 메서드의 차이에 대해 공부하다가 크루에게 new 키워드가 불리는 순간 생성자 내부에서 값이 할당되지 않은 상태여도 이미 생성된 상태라는 말을 들었어요. 그래서 정적팩토리 메서드에서 검증을 해준 후 private 생성자를 호출해야 된다는 말을 들었어요.

그런데 만약 인수가 다른 두 정적팩토리 메서드가 있을 때 각각 정적 팩토리 메서드에서 검증을 해주게되면 중복이 발생한다고 생각이 들어서 생성자에서 검증을 해주는 것이 좋지 않나라는 생각이 들었어요. 혹시 지노는 어떻게 생각하시나요?

예시(이번 미션 코드와 상관없는 그냥 예시입니다!)

// 정적 팩토리 메서드에서 검증
private Card(String name, int number) {
    this.name = name;
    this.number = number;
}

public static Card valueOf(String name) {
    validateName();
    return new Card(name, 0);
}

public static Card valueOf(String name, int number) {
    validateName();
    return new Card(name, number);
}
// 생성자에서 검증
private Card(String name, int number) {
    validateName();
    this.name = name;
    this.number = number;
}

public static Card valueOf(String name) {
    return new Card(name, 0);
}

public static Card valueOf(String name, int number) {
    return new Card(name, number);
}

Q5. 추상 클래스를 상속받는 것과 디폴트 메서드를 가지는 인터페이스를 상속받는 것 두 경우를 각각 언제 사용해야하는지가 잘 와닿지 않아요. 필드의 유무, 다중 상속 여부, 등의 차이를 보긴 했는데, 어떨 때 추상 클래스를 쓰고, 어떨 때 디폴트 메서드를 가지는 인터페이스를 써야할지 잘 모르겠어요. 큰 차이가 없는 느낌이에요.

쓰다보니 질문이 너무 많아진 것 같네요 😢 😢
늘 감사합니다!

@KJunseo
Copy link
Author

KJunseo commented Mar 9, 2021

+) 위에서 피드백 적용한 것과 별개로 오늘 수업 시간에 배운 상태 패턴을 추가로 적용해 봤어요.
상태 객체들을 많이 만들었는데 컨트롤러 부분에서 크게 달라진 게 없는 것을 보면 제대로 구현하지 못한 것 같기도하네요.. 상태를 만들긴 했는데 제대로 사용하지 못한 느낌?이 드는 것 같아요 😢
이 부분도 한 번 봐주시면 감사하겠습니다!!

[디자인패턴] 상태패턴 - 3

내용

  • 카드 값에 따라 상태가 결정되고 상태가 카드 뽑기, 수익률 계산 등의 행위를 하도록 함

링크

[디자인패턴] 퍼사드패턴 - 2

내용

  • 컨트롤러에서 start만 요청해도 게임이 알아서 진행된다.
    -수익률 계산등의 요청을 보내면 내부 구현은 알아서 진행된다

링크

[DI] 의존성 주입 - 3

내용

  • Players 생성시 내부에서 List를 만들지 않고 외부에서 만든 다음 주입해줬다.

링크

@hyunssooo
Copy link

hyunssooo commented Mar 10, 2021

안녕하세요 에어 지노입니다!

Dto를 적용해보자 라는 말은 아니였는데...🙄 개인적으로 레벨 1에서 dto까지는 필요 없다고 느껴요!

dto 객체의 필드를 다 원시값이나 String 값으로 두고 dto를 생성할 때 포장된 객체에서 값을 꺼낸 후 dto 생성자에 넣어줘야할지, 아니면 포장된 값 자체를 필드로 가지게 해서 view에서 꺼내 써야할지를 잘 모르겠더라고요.

VO를 Dto의 필드에 뒀다는 말씀 같아요! VO는 불변이겠죠? 불변객체는 상태가 변하지 않기 때문에 사이드 이펙트가 없습니다! view로 나가도 좋습니다 :) 굳이 모든 원시값을 dto의 필드에 둘 필요는 없어요!

처음 service를 분리한 것이 controller가 너무 커지는 것 같아서였어요! 그래서 View와 연결되지 않은 메서드들만 service로 분리했었는데, 이런 로직을 controller에서 처리해주면 controller가 너무 길지는 않을까요?

controller가 길어진다면 도메인 설계가 잘 되었는지 확인해보면 좋을 것 같아요! 적절한 도메인 객체 만들어 책임과 역할을 부여하고 컨트롤러에서는 그 도메인 객체를 사용해 비즈니스 요구사항을 풀어내면 됩니다. 그러다 도저히 안되면 controller에서 처리하면 될 것 같아요!
controller 가 분리된다면 controller가 controller를 사용하는 모습일 것 같아요! 그 모습은 에어가 만든 서비스랑 비슷한 모습일 것 같아요!

현재 CardDeck의 변수가 Cards 하나 밖에 없는 상태에요. 이러다보니 굳이 클래스를 나눠야하나?

Deck도 List 를 가지는 일급컬렉션으로도 구현이 가능할 것 같아요 :) 그렇다면 Deck과 Cards의 역할이 좀 더 분명할 것 같네요!

static 메서드는 따로 위로 빼줘야할까요 아니면 상관없이 기능에 따라 배치하면 될까요?

static 필드, static 메서드, 인스턴스 필드, 생성자, 메서드 순으로 배치하면 될 것 같습니다!

찾아보니 딱히 순서는 정해진게 없나봐요!
https://google.github.io/styleguide/javaguide.html

class Test {
  상수
  클래스 변수
  인스턴스 변수
  생성자
  팩토리 메서드
  메서드
  기본 메서드 (equals, hashcode, toString...)
}

정적 팩토리 메서드 내부에서 검증을 해줘야 할까요? 아니면 생성자에서 검증을 해줘야할까요?

static 메서드는 어플리케이션이 종료될 때까지 메모리에 남아있습니다. 정적 팩터리 메서드에서 검증을 하게된다면 검증에 대한 메서드는 private static일 가능성일 높을 것 같아요! 😀 그렇다면 생성자에서 검증을 하는 편이 저는 더 나은 방법이라고 생각해요.

어떨 때 추상 클래스를 쓰고, 어떨 때 디폴트 메서드를 가지는 인터페이스를 써야할지 잘 모르겠어요. 큰 차이가 없는 느낌이에요.

상속은 확장 이고 인터페이스는 구현입니다. 각각의 키워드가 extends, implements로 다르구요.
상속은 확실한 is a 관계에서 사용하시면 됩니다.
사실 요구사항에서 확실한 is a 관계를 찾기는 힘들거라고 생각해요. 저도 마찬가지구요. 그래서 상속보다 조합을 사용하라는 말도 있는 것 같습니다. 하지만 확실한 것은 상속이 나쁜 것은 아닙니다. 끊임없이 변경되는 요구사항에서 is a 관계를 찾기가 힘들뿐이죠.
디폴트 메서드를 가지는 인터페이스는 인터페이스를 구현하는 구현체들에게 공통적인 메서드를 제공할 때 사용합니다. 각 구현체에서 override를 할 수도 있어요! 인터페이스와 상속을 어떨때 사용하는지 먼저 고민해보면 더 좋을 것 같네요!

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.

안녕하세요 에어! 지노입니다.
pr 확인이 늦어져서 죄송해요.
step1 진행하신다고 수고 많으셨습니다!
몇 가지 코멘트 남겼어요! step2 진행하시면서 고민해보시면 좋을 것 같아요

Comment on lines +54 to +65
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
Participant that = (Participant) o;
return Objects.equals(nickname, that.nickname) && Objects.equals(state, that.state);
}

@Override
public int hashCode() {
return Objects.hash(nickname, state);
}

Choose a reason for hiding this comment

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

Participant는 nickname, state가 모두 같아야 같은 객체로 취급하나요??

Copy link
Author

Choose a reason for hiding this comment

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

아 state는 달라도 nickname만 같다면 동일 객체로 볼 수 있겠네요!

VO가 같은 값일 경우 같은 객체로 취급하는건데, participant는 state가 달라도 nickname만 같으면 같은 객체로 취급할 수 있잖아요. 이렇게 필드의 일부분만으로 같은 객체 판단을 하는 경우도 VO로 볼 수 있는 건가요??

VO판단 핵심을 어떤 클래스의 일부 필드가 같을 때 같은 객체로 볼 수 있는 경우로 생각해도 될까요? 🤯

Choose a reason for hiding this comment

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

아닙니다! VO의 동등성은 모든 필드의 값이 같아야 합니다.

주소로 비교를 하자면 아파트 이름이 같으면 다 같다고 볼 수 없는 것과 같습니다.
아파트 이름, 동, 호수 까지 같아야 같은 주소로 취급합니다 :)

Comment on lines +34 to +36
boolean isDuplicate = players.stream()
.distinct()
.count() != players.size();

Choose a reason for hiding this comment

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

set을 사용하는 방법도 있겠네요!

Comment on lines +41 to +44
public final ParticipantDto toParticipantDto() {
Cards cards = state.getCards();
return new ParticipantDto(this.nickname, cards, cards.calculateScore());
}

Choose a reason for hiding this comment

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

도메인 모델이 dto의 존재를 알고 있네요!
dto에 변화가 생기면 도메인 모델도 변경 해줘야 할 것 같아요. 도메인 모델은 Dto를 모르는 편이 나을 것 같습니다! :)

Comment on lines +60 to +64
public List<ParticipantDto> toPlayersDto() {
return players.stream()
.map(Player::toParticipantDto)
.collect(Collectors.toList());
}

Choose a reason for hiding this comment

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

여기도 도메인 모델이 DTO를 모르는 편이 나을 것 같아요!

Comment on lines +17 to +23
public Map<MatchResult, Integer> getDealerResult() {
Map<MatchResult, Integer> dealerResult = new EnumMap<>(MatchResult.class);
for (MatchResult matchResult : MatchResult.values()) {
dealerResult.put(matchResult, dealerResultCount(matchResult));
}
return dealerResult;
}

Choose a reason for hiding this comment

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

테스트를 제외한다면 이 클래스 내부에서만 사용하는 메서드네요. private이 더 어울릴거 같아요.
그리고 테스트에선는 이 메서드를 사용하는 다른 메서드를 통해 간접적으로 테스트하면 더 좋을 것 같습니다 :)

Choose a reason for hiding this comment

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

toDealerResultDto() 에서만 사용되네요.
BlackJackResult를 playerResult, dealerResult로 분리하는 것도 방법일 것 같아요 :)
고민만 해주세요!

Comment on lines +15 to +20
if (playerState instanceof BlackJack && !(dealerState instanceof BlackJack)) {
return true;
}
if (playerState instanceof Stay && dealerState instanceof Bust) {
return true;
}

Choose a reason for hiding this comment

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

instanceof는 OOP에서 다형성으로 해결 가능합니다. isBlackJack() 같은 메서드도 만들수 있을 것 같네요. 다형성과 OCP에 대해서 학습해보면 좋을 것 같습니다 :)

Comment on lines +16 to +19

public class DealerTest {
private Dealer dealer;

Choose a reason for hiding this comment

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

이 클래스 내부에 있는 주석은 지워주세요! :)

state.draw(new Card(Shape.DIAMOND, Denomination.KING));
}).isInstanceOf(IllegalStateException.class);
}
}

Choose a reason for hiding this comment

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

Suggested change
}
}

마지막줄은 개행으로 끝납니다 :) 다른 클래스들도 확인 부탁드려요!

@hyunssooo hyunssooo merged commit ebd09b7 into woowacourse:kjunseo Mar 10, 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