Skip to content

Conversation

mintcoke123
Copy link

@mintcoke123 mintcoke123 commented Sep 18, 2025

안녕하십니까! 김준수 리뷰어님. 2기 프론트에 이어 3기 백엔드로 합류하게 된 강동현 리뷰이입니다!
저번 리뷰어와의 만남 때 테이블이 달라 말씀을 못 나눴던걸로 기억하는데, 이렇게 리뷰이로 만나뵙게 되어 영광입니다!
이번 리뷰에서 좋은 티키타카가 오갔으면 좋겠습니다!
잘부탁드립니다~! 😁


🚗 3단계 - 게임 실행


📝 요구사항

  • 주어진 횟수 동안 n대의 자동차는 전진 또는 멈출 수 있다.
  • 각 자동차에 이름을 부여할 수 있다. 전진하는 자동차를 출력할 때 자동차 이름을 같이 출력한다.
  • 자동차 이름은 쉼표(,)를 기준으로 구분하며 이름은 5자 이하만 가능하다.
  • 사용자는 몇 번의 이동을 할 것인지를 입력할 수 있어야 한다.
  • 전진하는 조건은 0에서 9 사이에서 random 값을 구한 후 random 값이 4 이상일 경우 전진하고, 3 이하의 값이면 멈춘다.
  • 자동차 경주 게임을 완료한 후 누가 우승했는지를 알려준다. 우승자는 한 명 이상일 수 있다.

➕ 새로운 프로그래밍 요구사항

  • 메인 메서드를 추가하여 실행 가능한 애플리케이션으로 만든다.

💻 구현 과정

  1. 프로그레밍 요구사항에 맞는 view 작성(mvc 패턴 도입)
  2. 기존에 작성한 Race 클래스에서 controller 추출(modelcontroller로 분리)
  3. Main 컴포넌트 작동하게끔 수정

🤔 고민한 부분들

▶️ mvc 패턴에서 패키지 분리는 어떻게 진행하면 좋을까?

기존 클래스를 mvc 패턴으로 분리하는 과정에서, 패키지 분리를 둘 중 어떤 방식으로 진행할지 고민했습니다.

  1. 도메인 단위 페키지 분리
race/Race.java
race/RaceController.java
race/RaceView.java
  1. 계층별 패키지 분리
domain/Race.java
controller/RaceController.java
view/RaceView.java

처음에는 특정 도메인 관련 클래스가 한 군데에 모여있어 관리가 쉬울 것이라고 생각해, 1번 방법을 선택할까 했습니다.

그러나 아래와 같은 두 가지 이유로 2번을 선택했습니다.

  1. 데이터의 흐름

한 기능이 한 폴더에 모여있게 되어, 유저와의 상호작용과 데이터의 흐름을 생각했을 때 역할별로 분리하는 것이 유지보수가 쉽다고 생각했습니다.

  1. 테스트 단위의 용이성

modal 부분만 테스트를 진행한다고 가정했을 때, 기능 단위로 테스트를 묶기 좋습니다.

▶️ RaceControllerrunRace의 위치와 역할

runRace 는, 요구사항의 마지막 단계에 부합하는, 유저의 입력을 받아 지금까지 만들어놓은 메서드를 조립하여

  1. 유저에게 정보받기
  2. 받은 정보로 경주진행
  3. 결과출력

까지 진행하는 메서드입니다.

위 메서드에서 저는 2가지의 고민을 하였습니다.

1. runRace를 분리할 수는 없을까?

저는 메서드를 작성할 때 항상 단일 책임 원칙을 지키려고 노력했습니다.

그러나 runRace는 하는 역할이 많음에도 불구하고 나눌 각이 잘 보이지 않았습니다.

처음에는

raceSetting

  1. 유저에게 정보받기

runRace

  1. 받은 정보로 경주진행
  2. 결과출력

와 같이 분리하고자 했지만, runRace는 컨트롤러로써 유저에게서 받은 정보에 의존하고,

1,2와 3을 분리하자니 3의 inputView 파트는 controller로부터 받은 model의 정보에 의존합니다.

결국 runRace경주 절차를 오케스트레이션한다 라는 단일 책임을 맡았다고 정의하여, 지금과 같은 형태가 되었습니다.

2. runRacecontroller에서 관리하는 것이 맞을까?

runRace는 지금까지 작성한 메서드들을 완성 시킴과 동시에, viewmodel을 조립하는 역할을 합니다.
앞서 말씀드렸다시피 runRace는 많은 책임을 안고있기 때문에, main에서 마지막으로 조립하는 방식도 고려해 보았습니다만,
main앱 시작점의 역할만 담당하는 것이 바람직하다고 생각하기 때문에 runRacecontroller에 배치하였습니다.


🚗 4단계 - 리팩터링


지를 알려준다. 우승자는 한 명 이상일 수 있다.

➕ 새로운 프로그래밍 요구사항

  • 모든 로직에 단위 테스트를 구현한다. 단, UI(System.out, System.in) 로직은 제외한다.
    • 랜덤한 요소가 존재하는 코드는 어떻게 테스트할 수 있는지 경험한다.

💻 구현 과정

  1. 클래스 전략 패턴으로 리팩토링
  2. 메서드별 단위테스트 작성

🤔 고민한 부분들

단위 테스트 시행 범위

저희 요구사항 중, UI(System.out, System.in) 로직은 제외한다 부분을 보고 단위 테스트의 범위를 생각해 보았습니다.

controller 역시 로직이 들어가지만, View 부분과의 연결로써 (System.out, System.in) 부분으로 반드시 입력을 받아야 하기 때문에 테스트코드를 작성하지 않았습니다.

위 요구사항을 생각해봤을 때, 단위 테스트는 model 부분에서만 이루어져야 한다고 생각합니다.


❓질문사항

▶️1. 현재 단위 테스트코드의 작성이 잘 이루어졌나요?

제가 controllermodal을 나눈 기준은. 사용자의 입력이 필연적인가 입니다.
그런데 controller 역시 로직이 들어가 있기 때문에, 충분히 테스트의 대상이 된다고 생각합니다.
이런 경우에는 어떻게 단위테스트를 진행하면 좋을까요?

▶️2. 현제 Car 내부에 있는 isValidCarNames의 위치

이번 미션에서 "이름이 5 이상을 넘으면 안된다" 라는 규칙이 새로 생겼습니다.
이런 경우, 이름이 5 이상을 넘어가지 않음을 판단하는 로직은 어디에 두면 될까요?

  1. "사용자의 입력이 5 이상을 넘어가면 안된다" 라고 생각하면 controller에 두는 것이 타당해 보입니다.
  2. "자동차의 이름이 5 이상을 넘어가면 안된다" 라고 생각하면 'model'에 두는 것이 타당해 보입니다.

위 문제에 관해 리뷰어님의 의견을 듣고 싶습니다!

mintcoke123 and others added 30 commits September 10, 2025 01:51
@mintcoke123 mintcoke123 changed the base branch from main to mintcoke123 September 18, 2025 10:15
Copy link

@gogo1414 gogo1414 left a comment

Choose a reason for hiding this comment

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

안녕하세요, 동현님!
이번 미션의 리뷰를 맡은 김준수라고 해요😊

프론트에 이어 백엔드 스터디까지 진행하시는 걸 보니, 그 열정에 저도 큰 힘을 얻고 가네요!

저도 백엔드 스터디 후 프론트 스터디를 진행했었는데, "가독성 좋은 네이밍" 이라는 공통된 지향점이 있다는 걸 느꼈어요. 동현님께서는 프론트와 백엔드 스터디의 공통점과 차이점을 어떻게 느끼셨는지 궁금하네요!

이번 리뷰에서는 동현님이 앞으로 만들어나갈 '나만의 코드 컨벤션' 을 정립하는 데 도움이 될 만한 저의 생각들을 담아봤어요. 단순히 기존 컨벤션을 적용하는 것을 넘어, 왜 그런 컨벤션이 필요한지 고민하고 자신만의 기준을 세우는 것이 미래의 나에게 정말 큰 도움이 되더라고요. (과거의 제 코드를 보면 저도 가끔 놀라곤 한답니다😅)

제가 제안 드리는 내용과 동현님의 생각이 다를 수도 있어요. 충분히 고민해보시고 동현님만의 방식을 체득하는 계기가 되었으면 좋겠네요!

앞으로 잘 부탁드려요😁

고민에 대한 코멘트

  1. MVC 패턴에서 패키지 분리는 어떻게 진행하면 좋을까?

좋은 고민이라고 생각해요. 패키지 구조는 정답이 없어서 많은 개발자들이 토론하는 주제이기도 하죠. 동현님께서 두 가지 방식을 두고 데이터의 흐름과 테스트 용이성까지 고려해서 계층별 분리를 선택하신 과정과 이유가 인상 깊었어요.

중요한 것은 "어떤 방식이 절대적으로 옳은가?"보다 "우리 팀(혹은 나)의 프로젝트에서는 어떤 방식이 더 효율적인가?" 를 판단하는 것이라고 생각해요. 지금은 선택하신 구조로 빠르게 개발하고, 만약 프로젝트가 커지면서 불편함이 느껴질 때 다른 구조로 리팩토링하는 경험을 해보는 것도 좋은 접근법이에요. 스스로의 판단을 믿고 진행해보시는 것을 추천해요!

  1. runRace의 위치와 역할

runRace 메서드의 역할에 대해 단일 책임 원칙(SRP) 관점에서 깊이 고민하신 점이 정말 좋아 보였어요.

동현님께서 말씀하신 대로 runRace는 여러 작업을 수행하는 것처럼 보이지만, 그 본질적인 책임을 "경주 시작부터 끝까지의 과정을 순서대로 조율하는 것"으로 볼 수 있을 것 같아요. 일반적으로 이런 역할을 오케스트레이션(Orchestration) 이라고 부르기도 해요. 이런 관점에서 보면, runRace는 "경주를 총괄한다" 는 단일 책임을 수행하고 있다고 해석할 수도 있겠네요.

제가 비슷한 고민을 할 때는, 해당 메서드의 역할을 문장으로 정의해보곤 해요.

runRace의 역할은? → "레이스를 실행한다."

레이스를 실행하려면 무엇이 필요한가? → "자동차 이름도 받아야 하고, 시도 횟수도 받아야 하고, 실제 경주를 진행시키고, 마지막에 결과를 보여줘야 한다."

결론: runRace는 이 과정들을 직접 수행하기보다, 각 단계에 맞는 객체(View, Model)에게 메시지를 보내 전체 흐름을 조율하는 지휘자 역할을 하는군요.

지금처럼 메서드의 역할을 명확히 정의하고 코드를 작성하는 습관은 좋은 결과로 이어지는 경우가 많다고 생각해요.

  1. runRacecontroller에서 관리하는 것이 맞을까?

저도 동현님의 의견에 동의해요. runRaceRaceController에 배치하는 것은 MVC 패턴의 역할 분담 관점에서 자연스러운 접근이라고 생각해요.

일반적으로 main은 어플리케이션을 시작시키는 최소한의 책임만 갖도록 하고, Controller가 사용자의 입력을 받아 ModelView를 연결하는 다리 역할을 하도록 구현하는 경향이 있어요. runRace가 바로 그 다리 역할을 하고 있으니, Controller에 위치하는 것이 좋은 선택지 중 하나라고 생각해요.

질문 사항 답변

  1. 현재 단위 테스트코드의 작성이 잘 이루어졌나요?

테스트 코드를 작성하고 고민하는 습관, 정말 좋은 습관이라고 생각해요.

먼저 테스트 케이스가 조금 더 다양해지면 좋겠어요. 예를 들어 숫자가_4이상이면_전진한다() 테스트에서 4뿐만 아니라, 경계값인 9, 혹은 예상치 못한 극단적인 값(아주 큰 숫자, 0, 음수 등)을 고려해보면 어떨까요? 특히 성공 케이스만큼이나 실패하거나 예외가 발생하는 케이스를 테스트하는 것이 코드의 안정성을 크게 높여줄 수 있어요.

컨트롤러 테스트에 대한 고민도 좋았어요. 컨트롤러는 로직 자체보다는 다른 객체와의 상호작용을 검증하는 경우가 많아요. 예를 들어, getValidCarNames() 메서드에 유효하지 않은 자동차 이름을 입력했을 때, raceOutputView의 특정 메시지 출력 메서드가 올바르게 호출되는지를 확인하는 테스트를 작성해볼 수 있어요. 이처럼 "특정 상황에서 올바른 객체들과 제대로 소통하는가?" 를 검증하는 방향으로 테스트를 추가해보시는 것도 좋은 경험이 될 거예요.

  1. 현재 Car 내부에 있는 isValidCarNames의 위치

이 부분도 정말 좋은 고민이라고 생각해요! 동현님께서 분석하신 대로, 요구사항은 "자동차의 이름은 5자 이하"라고 명시하고 있어요.

이는 "자동차(Car)라는 도메인 객체와 관련된 비즈니스 규칙"이라고 해석할 수 있어요. 많은 객체지향 설계에서는 "자신의 데이터를 가장 잘 아는 객체가 스스로의 유효성까지 책임지게 하는 것" 을 좋은 설계 원칙 중 하나로 여겨요.

그런 관점에서, 동현님께서 2번(model)에 두는 것이 타당하다고 생각하신 것은 지금 말씀드린 설계 원칙과도 잘 맞는, 설득력 있는 선택이라고 생각해요!


Request Changes로 리뷰를 남긴 이유는 이번 미션에서 제가 언급드린 부분이 하나의 형태로 통합되었으면 하는 바람에서 남겼습니다! 코드를 작성하는 형태에 대해서는 답이 없지만, 그래도 일관된 코드를 작성하면 미래에 함께할 팀원 뿐만 아니라 본인에게도 도움이 되기 때문에 위에서 말했다시피 이번 미션이 좋은 계기가 되셨으면 좋겠네요!

동현님, 앞으로 남은 미션 모두 힘내시고 응원할게요!! 아자아자 파이팅~~🔥🔥🔥🔥🔥🔥

Choose a reason for hiding this comment

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

해당 부분 보시면 End Of Line 경고가 발생하고 있는데 확인 부탁드려요!

Copy link
Author

Choose a reason for hiding this comment

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

넵! 수정했습니다!

// then
assertEquals(3, car.getCarPosition());
}
}

Choose a reason for hiding this comment

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

여기도 보시면 End Of Line 경고가 발생하고 있는데 확인 부탁드려요!

Copy link
Author

Choose a reason for hiding this comment

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

수정했습니다!

제가 이런 실수를 자주 하기도 하고, 신경쓴다 해도 사람이 신경쓰는 코드는 실수가 날 수밖에 없다고 생각하기 때문에, intellij 설정이나 config 파일로 바로잡아야 한다고 생각합니다!

editorconfig 파일을 추가하여 자동으로 파일에 개행문자를 추가하도록 설정하였습니다!

Choose a reason for hiding this comment

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

좋네요👍👍


public class Main {
public static void main(String[] args) {
new RaceController(new RaceInputView(), new RaceOutputView(), new RandomDigitGenerator())

Choose a reason for hiding this comment

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

RaceController 생성자 부분에서 지금은 파라미터가 3개라 괜찮지만, 나중에 더 늘어날 가능성을 고려해서 가독성을 미리 확보하는 차원에서 아래와 같이 작성하는 건 어떨까요?

Suggested change
new RaceController(new RaceInputView(), new RaceOutputView(), new RandomDigitGenerator())
new RaceController(
new RaceInputView(),
new RaceOutputView(),
new RandomDigitGenerator()
)

이렇게 하면 나중에 의존성이 추가/변경될 때 동현님뿐만 아니라 다른 팀원들도 어떤 의존성이 필요한지 한눈에 파악할 수 있어 유지보수에 큰 도움이 될 거라고 생각하는데, 동현님의 생각은 어떠신가요?

Copy link
Author

Choose a reason for hiding this comment

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

좋은 생각입니다! 수정했습니다!

또한 자동으로 스타일을 맞추도록 위와 같이 코드 스타일 설정을 수정하였습니다!
image

Choose a reason for hiding this comment

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

의도가 궁금합니다⁉️

1. 같은 do-while의 다른 형태

  1. getValidCarNames() 메서드
        do{
            raceOutputView.printGetCarNameMessage();
            carNames = raceInputView.getCarNames();
            printExceptionWhenInvalidName(carNames);
        } while (!isValidCarNames(carNames));
  1. getValidTurn(), getValidTurn() 메서드
        do{
            raceOutputView.printGetRaceTurnMessage();
            raceTurns = raceInputView.getRaceTurnNumber();
            printExceptionWhenInvalidTurn(raceTurns);
        }while (raceTurns <= 0);

2. 메서드 간 줄바꿈

특정 부분에선 줄바꿈이 1줄로 되어 있지만, 특정 부분에서는 줄바꿈이 2줄, 3줄이 적용되어 있어요.

2가지 사항이 의도하신 바가 있으실까요? 없다면 하나로 통일해보는건 어떨까요!?

Choose a reason for hiding this comment

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

다른 파일도 메서드 간 줄바꿈 혹은 같은 로직이지만 작성한 형식이 다른 부분이 있어서 그 부분도 같이 통일해보시면 더 좋을 것 같아요!

Copy link
Author

Choose a reason for hiding this comment

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

1. 같은 do-while의 다른 형태

image image

만일 사용자가 예상치 못한 답을 입력할 경우, 바로 에러를 뱉는 것보다는 경고문을 띄우고 다시 기회를 주는 것이 더 자연스럽다고 생각했습니다.

자동차 이름과, 시도할 횟수는 독립적으로 사용자에게 기회를 주기 때문에 유사한do-while 문을 2번 사용했습니다!

2. 메서드 간 줄바꿈

특정 부분에선 줄바꿈이 1줄로 되어 있지만, 특정 부분에서는 줄바꿈이 2줄, 3줄이 적용되어 있어요.

앞서 말씀드렸듯 다른 환경에 설정을 정해놓고 신경을 못쓴 저의 실수입니다!
현재는 수정이 되어 있습니다!

Copy link

@gogo1414 gogo1414 Sep 21, 2025

Choose a reason for hiding this comment

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

로직에 고민이 담겨 있어서 좋네요👍

다만, 제가 1번 질문에서 말씀드리고 싶었던 내용은 1. }while, 2. } while 이렇게 같은 do-while문을 쓰는데 쓸 때 마다 1번과 2번 방식으로 쓰게 되면 통일성이 떨어져 보여요! 해당 부분을 여쭙고자 질문을 드렸던겁니다!

Copy link
Author

@mintcoke123 mintcoke123 Sep 22, 2025

Choose a reason for hiding this comment

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

전혀 인지를 못하고 있었습니다..!😂
해당부분은 2번 } while으로 통일하도록 하겠습니다!

Comment on lines 25 to 41
private void printExceptionWhenInvalidName(List<String> carNames){
if(!isValidCarNames(carNames)){
raceOutputView.printInvalidNameExceptionMessage();
}
}

public List<String> getValidCarNames() {
List<String> carNames;

do {
raceOutputView.printGetCarNameMessage();
carNames = raceInputView.getCarNames();
printExceptionWhenInvalidName(carNames);
}while (!isValidCarNames(carNames));

return carNames;
}

Choose a reason for hiding this comment

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

지금 해당 부분을 보시면 printExceptionWhenInvalidName() 메서드 내에서도 isValidCarNames() 메서드를 활용중이고 getValidCarNames() 메서드 내에서도 isValidCarNames()를 활용중인데, 이를 하나로 합쳐서 활용하는 방법은 없을까요?

Copy link
Author

Choose a reason for hiding this comment

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

생각해보니 printExceptionWhenInvalidName 가 검증로직을 가지고 있으니 view도 아닌 이상 void로 사용할 필요가 없네요!

printExceptionWhenInvalidNameboolean 함수로 바꿔서, '적절한 이름인가를 검증하는 함수' 로 변경하고, 적절하지 않은 함수일 경우에만 경고 메세지를 출력하는 것이 좋아보입니다!

    private boolean validateCarNames(List<String> carNames) {
        if (!isValidCarNames(carNames)) {
            raceOutputView.printInvalidNameExceptionMessage();
            return false;
        }
        return true;
    }

위와 같이 코드를 수정하고, getValidCarNames 메서드는 위 함수를 통해 검증 로직을 수행하도록 변경했습니다.
유사한 getValidTurn에서도 같은 리팩토링을 진행하였습니다.

Comment on lines 8 to 9
private static final int carMoveBaseline = 4;
private static final int validCarNameSize = 5;

Choose a reason for hiding this comment

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

static final로 선언된 불변 상수(constant)는, 일반 변수와 구분하기 위해 대문자와 언더스코어(_)를 사용하는 UPPER_SNAKE_CASE로 작성하는 경우가 일반적인데 가독성을 높이는 차원에서 아래와 같이 수정해보는 건 어떨까요?

Suggested change
private static final int carMoveBaseline = 4;
private static final int validCarNameSize = 5;
private static final int CAR_MOVE_BASE_LINE = 4;
private static final int VALID_CAR_NAME_SIZE = 5;

Copy link
Author

@mintcoke123 mintcoke123 Sep 21, 2025

Choose a reason for hiding this comment

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

불변 상수의 네이밍 컨벤션은 생각못했네요! 수정했습니다!

Choose a reason for hiding this comment

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

코드를 읽다가 더 좋은 방향이 떠올라서 의견 드려요!

코드는 위에서 아래로, 마치 책을 읽는 것처럼 자연스럽게 흐름이 이어질 때 가독성이 정말 좋아지는 것 같아요.

지금 move() 메서드가 내부에서 go()를 사용하고 있는데, go()가 클래스 맨 아래에 있다 보니 전체 흐름을 파악하기 위해 스크롤을 조금 내려가야 하더라고요. 😅

move() 바로 아래에 go()를 배치하는 것처럼, 서로 호출하는 메서드들을 가까이 두는 방식으로 수정해보는 건 어떨까요? 이렇게 하면 코드의 전체적인 흐름을 파악하기가 더 수월해질 것 같아요!

Copy link
Author

@mintcoke123 mintcoke123 Sep 21, 2025

Choose a reason for hiding this comment

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

스크린샷 2025-09-13 220123

현재 위와 같은 코드 컨벤션 설정을 적용하고 있어 private 메서드인 go 가 밑으로 내려갔습니다! 위 컨벤션은 외부에 드러나는 것 → 내부에서만 쓰이는 것 순서로 매서드를 표기하도록 작성하였습니다.
리뷰어님의 말씀이 일리가 있다고 생각합니다만, 이 파일 하나뿐만이 아니라 프로젝트, 혹은 패키지 단위로 보았을 때 쓰이는 기능끼리 묶기보다는 "외부에 드러나는 것 → 내부에서만 쓰이는 것"의 순서로 public 메서드가 위에 위치하는 컨벤션이 더 적합할 수 있다고 생각합니다!

리뷰어님 생각은 어떠신가요??🤔

Choose a reason for hiding this comment

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

네, 동현님이 제안해주신 public 메서드를 먼저 배치하는 컨벤션은 아주 좋은 접근 방식이에요👍
클래스의 '대표 기능'이 무엇인지 한눈에 파악할 수 있게 해주죠.

여기에 코드를 읽는 흐름이라는 관점을 더하면 훨씬 더 강력한 컨벤션이 될 거라고 생각해요.

동현님께서 말씀해주신 예시를 조금 더 발전시켜 볼게요.
만약, public 메서드인 startRace()가 내부적으로 아래 순서대로 private 메서드를 호출한다고 가정해볼게요.

public void startRace() {
    prepareCars();   // 1. 자동차 준비
    runTurns();      // 2. 턴 진행
    showResult();    // 3. 결과 보여주기
}

이때 private 메서드들을 단순히 클래스 아래쪽에 모아두는 것보다, startRace() 바로 아래에 호출되는 순서대로 배치해주면 어떨까요?

// "외부에 드러나는 것"을 맨 위에!
public void startRace() {
    prepareCars();
    runTurns();
    showResult();
}

"내부에서만 쓰이는 것"들을 호출 순서대로 바로 아래에!

private void prepareCars() {
    // ... 1번 세부 구현
}
private void runTurns() {
    // ... 2번 세부 구현
}
private void showResult() {
    // ... 3번 세부 구현
}

이렇게 하면 IDE의 도움 없이도, 코드를 위에서 아래로 한번만 쭉 읽는 것만으로도 전체 동작 방식을 쉽게 이해할 수 있어요. 마치 신문 기사를 읽을 때 헤드라인을 먼저 보고, 이어서 나오는 본문 내용을 순서대로 읽는 것과 비슷하죠.

결론적으로, 동현님이 제안해주신 '외부 공개 → 내부 사용' 규칙과 제가 제안하는 '호출 순서대로 배치' 규칙을 함께 사용하면 가장 이상적일 거라고 생각하는데 어떻게 생각하시나요?

Copy link
Author

@mintcoke123 mintcoke123 Sep 22, 2025

Choose a reason for hiding this comment

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

메서드 호출의 순서는 전혀 생각치 못했습니다!
startRace 같은 전체 흐름을 책임지는 메서드는 호출순서를 컨벤션에 적용했을 때 가독성이 많이 올라가겠네요.
정말 좋은 생각인것 같습니다! 이후 코드에 적용하도록 하겠습니다!

@gogo1414
Copy link

gogo1414 commented Sep 20, 2025

실행 후 테스트를 해봤어야 했는데 까먹고 넘어갔었네요.. 죄송합니다😅

실행해보니 이미지와 같이 고려가 되지 않은 입력의 경우 에러가 발생하거나 의도한 출력이 나오지 않고 있어요!
리뷰 반영해주실 때 같이 고민해보시면 좋을 것 같아서 추가 코멘트 드립니다!

1. 자동차 이름의 입력을 넣지 않고 ,,,로 입력

스크린샷 2025-09-20 오후 4 52 22

2. int의 범위를 넘어선 숫자 입력

스크린샷 2025-09-20 오후 4 51 31 스크린샷 2025-09-20 오후 4 51 52

@mintcoke123
Copy link
Author

mintcoke123 commented Sep 21, 2025

▶️테스트케이스의 케이스 분리

먼저 테스트 케이스가 조금 더 다양해지면 좋겠어요. 예를 들어 숫자가_4이상이면_전진한다() 테스트에서 4뿐만 아니라, 경계값인 9, 혹은 예상치 못한 극단적인 값(아주 큰 숫자, 0, 음수 등)을 고려해보면 어떨까요? 특히 성공 케이스만큼이나 실패하거나 예외가 발생하는 케이스를 테스트하는 것이 코드의 안정성을 크게 높여줄 수 있어요.

좋은 생각인것 같습니다!
말씀드린 부분에서 기준이 있을 것 같아 찾아봤습니다.
보통 예상 범위 내에서 케이스를 4개의 기준으로 나누는 것을 확인했습니다.

  1. 정상 동작
  2. 경계값
  3. 예외/에러 케이스
  4. 누적/상태 변화 확인

제가 작성한 domain에 넣을 케이스는 빌드 시 이전 단계에서 예외사항이 걸러지는 것을 전재로 하기 때문에, 3번째 케이스는 컨트롤러 테스트에 적용해야 할 것으로 생각됩니다.

CarTest에서 기존의 경계값 말고도, 극대, 극소의 케이스도 있을 것이라 생각해 추가했습니다! 🚀

▶️RaceController에 관한 테스트 RaceControllerTest

컨트롤러 테스트에 대한 고민도 좋았어요. 컨트롤러는 로직 자체보다는 다른 객체와의 상호작용을 검증하는 경우가 많아요. 예를 들어, getValidCarNames() 메서드에 유효하지 않은 자동차 이름을 입력했을 때, raceOutputView의 특정 메시지 출력 메서드가 올바르게 호출되는지를 확인하는 테스트를 작성해볼 수 있어요. 이처럼 "특정 상황에서 올바른 객체들과 제대로 소통하는가?" 를 검증하는 방향으로 테스트를 추가해보시는 것도 좋은 경험이 될 거예요.

controller테스트와 같은 유저와의 상호작용이 복잡하지만 가능했군요!
몇몇 레퍼런스들을 참고해서, gradle 설정을 바꾸고 테스트코드를 작성해보았습니다.

  1. mock 객체를 만들고, Stub 동작을 정의하여 유저의 입력을 mock 데이터로 구현하고. 테스트 데이터를 생성했습니다. => given
  2. 실행 대상을 호출합니다 => when
  3. MockitoInOrder 로 호출 순서를 정하고, 입력이 들어왔을 때 해당 입력에 맞는 메세지를 출력하도록 합니다.
  4. verify로 해당 메세지가 얼마나 출력되었는지 확인합니다. => then

추가로 해당 단계를 진행하던 중, when-thenReturn 문으로 가짜 메서드없이 randomDigit의 출력을 조절할 수 있다는 걸을 알게 되어, runRace 테스트코드에 사용하였습니다!

테스트코드를 작성하면서 느낀 사항입니다만, 해당 테스트코드는 정제되지 않은 사용자의 여러 입력들을 고려하고, 그에 맞는 사용자에게 보여지는 메세지를 출력한다는 점에서 프론트엔드의 테스트코드와 매우 유사하다고 생각이 들었습니다!
그래서 처음 배우는 개념이지만 비교적 쉽게 이해가 된 것 같습니다!
노파심에 드리는 질문일 수도 있지만, controller의 테스트코드를 작성하면서 위와 같은 생각이 들었습니다.

verify(raceOutputView, times(3)).printRaceOneTurn(anyList());

위 코드처럼, Car 객체를 만들지 않고 "이러한 메세지가 출력된다" 라는 의미를 가진 테스트코드를 작성하는 것은 객체의 상태 변화를 정확히 확인하지 않고, 메세지가 몇번 호출되었는가를 따진다는 부분에서 "백엔드스럽지 않다"라는 생각이 들었습니다.
그렇다고 해서 상태를 정확히 추적하게 된다면 사용자의 상호작용 부분에서 입력을 폭넓게 확인할 수 없게 됩니다.

질문

지금 제 ControllerTest 테스트코드는 좋은 테스트코드일까요? 아니면 사용자와의 상호작용보다는 상태를 추적하는 방향으로 가야 할까요?🤔🤔

▶️예외사항 추가

실행해보니 이미지와 같이 고려가 되지 않은 입력의 경우 에러가 발생하거나 의도한 출력이 나오지 않고 있어요!

제가 예외사항에 대해 조금 느슨하게 접근했던 것 같습니다!
validateCarNames, validateRaceTurns를 strict하게 설정해 예외사항을 여러 분기로 나누었습니다!
이제 해당 예외사항뿐만이 아니라,

차 이름

  1. 차 이름이 입력되지 않았을 때
  2. 5자를 넘는 이름이 입력되었을 때
  3. 영어가 아닌 이름이 입력되었을 때

턴 수

  1. 양의 정수가 아닐 때(문자, 특수문자도 포함)
  2. int 범위 밖의 값이 입력되었을 때

위와 같은 경우에 오류메세지와 함께 사용자가 해당 데이터를 재입력할 수 있게 됩니다!🚀

Copy link

@gogo1414 gogo1414 left a comment

Choose a reason for hiding this comment

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

반영해주신 피드백 모두 잘 확인했습니다! 고생 많으셨어요😄

위 코드처럼, Car 객체를 만들지 않고 "이러한 메세지가 출력된다" 라는 의미를 가진 테스트코드를 작성하는 것은 객체의 상태 변화를 정확히 확인하지 않고, 메세지가 몇번 호출되었는가를 따진다는 부분에서 "백엔드스럽지 않다"라는 생각이 들었습니다. 그렇다고 해서 상태를 정확히 추적하게 된다면 사용자의 상호작용 부분에서 입력을 폭넓게 확인할 수 없게 됩니다.
질문
지금 제 ControllerTest 테스트코드는 좋은 테스트코드일까요? 아니면 사용자와의 상호작용보다는 상태를 추적하는 방향으로 가야 할까요?🤔🤔

동현님이 고민하시는 부분은 많이 공감이 되네요. "어디까지 테스트해야 좋은 테스트일까?"는 대부분의 개발자가 하는 고민이라고 생각해요.

먼저 질문에 대한 제 생각을 말씀드리면, 지금 작성하신 Controller 테스트는 현재 구조에서 충분히 의미 있는 좋은 테스트라고 생각해요.

컨트롤러를 ‘레이스 경기 진행 요원’ 이라고 생각해볼게요. 진행 요원의 중요한 역할 중 하나는 경주 시작 전, 참가 등록(사용자 입력)이 올바른지 확인하고, 문제가 생기면 안내 방송(View 호출)을 하는 것이죠. 지금 테스트는 바로 그 ‘안내 방송이 제때 잘 나오는가?’ 와 같은 진행 요원의 행동을 잘 검증하고 있어요.

다만, "백엔드스럽지 않다"고 느끼신 이유는 아마 ‘경기 진행 요원(Controller)’ 이 안내 방송뿐만 아니라, 직접 자동차를 한 칸씩 앞으로 밀어주는 ‘핵심 경주 규칙(Business Logic)’ 까지 함께 처리하고 있기 때문일 거예요.

이럴 때 RaceService 라는 계층을 도입해서 역할을 분리하면 테스트의 목적이 명확해져요.

RaceController (경기 진행 요원)

  • 역할: 참가자 등록(입력)을 받고, RaceService에게 경주 시작을 지시하며, 끝난 후 결과를 방송(View 호출)하는 역할에만 집중해요.
  • 컨트롤 테스트: "참가 등록이 잘못되면 경주 시작을 막고 안내 방송을 하는가?"처럼 행위 검증에 집중하게 되죠.

RaceService (경주 규칙 그 자체)

  • 역할: 자동차들을 실제로 전진시키고, 차례를 반복하며, 최종 우승자를 가려내는 핵심 로직을 책임져요.
  • 서비스 테스트: "전진 조건(4 이상)에 따라 자동차의 위치가 정말 변했는가?"처럼 객체의 상태 검증에 집중하게 됩니다.

결론적으로, 현재 프로젝트는 컨트롤러가 진행 요원과 경주 규칙 역할을 모두 하고 있는 셈이에요.

다음 미션을 진행하실 때는 지금처럼 컨트롤러의 행위를 검증하는 경험을 살리시면서 RaceService 계층을 분리해서 그곳에서는 자동차의 상태를 검증하는 테스트를 작성해보시는 건 어떨까요?

이렇게 역할을 분리하면 각각의 테스트 목적이 더 명확해져서, 동현님이 지금 느끼는 고민을 자연스럽게 해결할 수 있을 거라고 생각해요! 😊

Comment on lines 65 to 77
private int RaceTurnsToInt(String getRaceTurnNumber) {
if (!getRaceTurnNumber.matches("^[0-9]+$")) {
return -1;
}

java.math.BigInteger v = new java.math.BigInteger(getRaceTurnNumber);
java.math.BigInteger max = java.math.BigInteger.valueOf(Integer.MAX_VALUE);
if (v.compareTo(max) > 0) {
return -2;
}

return v.intValue();
}

Choose a reason for hiding this comment

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

해당 메서드에서 v는 어떤걸 의미하나요?
그리고 java.math.BigInteger로 표기하신 이유가 있을까요?

Copy link
Author

@mintcoke123 mintcoke123 Sep 22, 2025

Choose a reason for hiding this comment

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

해당 메서드에서 v는 어떤걸 의미하나요?

v는 value를 줄여 표기한 것입니다만, 다시 보니 네이밍에 대한 고민이 부족했다는 생각이 듭니다..
수정하였습니다!

그리고 java.math.BigInteger로 표기하신 이유가 있을까요?

처음에는 int 범위를 넘기는 long 자료형을 사용하고자 했습니다만,
long 역시 정말 큰 숫자(약 9경)을 넘어가면 사용자에게 기회를 주지 않고 에러를 뱉습니다.
그에 비해 BigInteger 자료형은 메모리가 남아있는 이상 크기 제한이 없기 때문에, BigInteger를 사용했습니다!

Choose a reason for hiding this comment

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

이전에 저희가 나눴던 이야기의 핵심은 "도메인 객체는 스스로의 유효성을 책임져야 한다" 는 것이었죠. Car 내부에 검증 로직을 두신 것을 보니 이 원칙을 적용하려 하신 점이 잘 보여요👍👍

여기서 한 단계 더 나아가, '어떻게 하면 도메인이 더 적극적으로 책임을 질 수 있을까?' 를 고민해보면 좋을 것 같아요!

지금 validateCarNames 메서드는 ControllerCarstatic 메서드를 하나씩 호출하며 검증 흐름을 직접 제어하고 있어요. 즉, 검증의 책임과 순서가 Controller에 있는 셈이죠. 이러면 도메인 내부에 검증 메서드를 정의한 의미가 조금 약해질 수 있습니다.

이 책임을 도메인에게 완전히 넘겨주는 방법은 어떨까요?

Controller는 도메인에게 "이 이름 괜찮아?"라고 묻는 대신, "이 이름으로 자동차를 만들어 줘!" 라고 생성을 시도하는 겁니다.

Car 객체는 자신의 생성자(Constructor)에서 이름에 대한 모든 검증(null, 길이, 언어 등)을 직접 수행해요. 만약 유효하지 않은 이름이 들어오면, 생성 자체를 거부하고 구체적인 이유를 담은 예외(Exception)를 던지는 거죠.

예시코드

    public Car(String name) {
        // 생성자(Gatekeeper)가 스스로의 유효성을 모두 검증해요.
        validate(name);
        this.name = name;
    }

    private void validate(String name) {
        // 자동차 이름을 검증해요..
        if (name.length() > 5) {
            throw new IllegalArgumentException("[ERROR] 자동차 이름은 5자를 초과할 수 없습니다.");
        }
    }

이 방식에서 Car 객체는 생성되는 시점부터 항상 유효한 상태임을 스스로 보장하고, 컨트롤러는 복잡한 검증 흐름 대신, 도메인 생성을 시도하고 발생하는 예외를 처리하는 역할에만 집중할 수 있어요.

이처럼 검증 책임을 도메인 객체의 생성 시점으로 완전히 넘기고 컨트롤러는 발생하는 예외를 받아 사용자에게 보여주는 역할에 집중하는 방식은 어떨까요?

이번 미션에서 바로 적용하시기엔 고민할 시간이 부족할 수 있으니, 다음 미션을 진행하실 때 이런 방식을 한번 떠올려보시면 더 깊이 있는 설계에 큰 도움이 될 거예요!😄

@mintcoke123
Copy link
Author

mintcoke123 commented Sep 22, 2025

남겨주신 리뷰 모두 정독했습니다!
덕분에 읽기 좋은 코드를 짜려면 어떻게 해야 하는지 방향성이 더욱 또렷히 정해진 것 같습니다!😀

코멘트 남겨주신 부분에 질문이 있습니다!

RaceService (경주 규칙 그 자체)

  • 역할: 자동차들을 실제로 전진시키고, 차례를 반복하며, 최종 우승자를 가려내는 핵심 로직을 책임져요.
  • 서비스 테스트: "전진 조건(4 이상)에 따라 자동차의 위치가 정말 변했는가?"처럼 객체의 상태 검증에 집중하게 됩니다.

리뷰어님께서는 RaceService를 "경주 규칙 그 자체" 라고 정의하시고 핵심 로직을 책임지도록 말씀하신 것 같은데,
사용자와의 상호작용이 없는 RaceService는 model 인Race와 별 차이가 없을 것 같다는 생각이 듭니다!
컨트롤러에서 로직을 더욱 분리하여, RaceSeviceRaceController를 분리하고 ModelController의 중간다리역할을 하게 만들까 생각했습니다만, 현재 컨트롤러에서 처리하는 로직은 사용자의 입력에 크게 의존하기 때문에 만일 위 코드를 분리하게 된다면 책임이 불필요하게 분리되는 문제가 생기지 않을까 생각합니다.
만약 분리를 하게 된다면 RaceServiceController의 역할을 하는게 아니라 Model내부의 Race에 작성되어야 하지 않을까 싶습니다!

리뷰어님의 생각은 어떠신가요??🤔

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.

2 participants