
이번 주 목표
3주 차 목표는 단순히 동작하는 코드를 넘어서, 스스로의 기준을 세우는데 집중하는 것이다!
요구사항으로 사용하는 것을 명시한 enum과 입출력 static 메서드 사용에 대해
학습하면서 나만의 기준을 세우는 것을 이번 주의 핵심 목표로 삼았다.
10/28 ~ 29 (화, 수) Day 1, 2
오늘의 도전
- 근거 또는 이유를 가지고서 총 6명 코드 리뷰 남기기
- 코드 리뷰 하면서 얻은 피드백 정리
1. 다른 사람 리뷰에서 배운 점
▪️ Comparable 인테페이스
Comparable 인터페이스를 활용하면 compareTo 메서드를 통해 객체 간 비교가 가능해진다.
아래와 같이 작성하여 활용할 수 있다.
Comparable<T>를 구현하고, @compareTo를 오버라이드하여 정렬 기준을 정의하면 된다.
class Car implements Comparable<Car> {
...
@Override
public int compareTo(Car other) {
return this.position.compareTo(other.position);
}
}
🤔 이로 인한 장점은 무엇일까?
바로 게터를 노출하지 않고도 비교 및 정렬을 수행할 수 있다는 점이다!!
객체끼리 스스로 비교할 수 있는 힘이 생긴다.
나는 게터없이 자동차 위치를 통해 우승자를 비교하기 위해 메서드를 2개나 만들었는데,
@compareTo만 구현한다면 이 메서드들은 필요가 없어진다ㅎㅎ
public boolean hasSamePositionWith(Car other) {
return this.position == other.position;
}
public boolean isAheadOf(Car other) {
return this.position > other.position;
}
🤔 Comparable를 사용하는 것의 단점은 뭐가 있을까?
Comparable를 사용하면, 우승자를 비교할 때 단순히 sort후 car.getFirst()를 통해 가져올 것이다.
그렇다면 Car 클래스에 들어가 정렬 기준을 파악해야 하는 수고로움이 존재한다.
즉, 다른 개발자들이 Car implements Comparable<Car> 선언만 보고도,
어떤 기준으로 정렬되는지 파악하기 쉽지 않다는 것이다.
또한 정렬 기준이 객체 내부에 고정되기 때문에 확장성에서도 제약이 생긴다.
만약 Car 객체가 확장되거나 도메인 요구사항이 변경되어 “우승 기준이 이동 거리 → 속도 → 이름 순”처럼 바뀐다면, Car 클래스 자체를 수정해야 한다.
결국 Comparable은 객체의 간단한정렬 기준을 정의할 때 유용하지만,
도메인의 규칙이 자주 바뀌거나 상황에 따라 여러 정렬 기준이 필요한 경우에는 유연성을 해칠 수 있다는 트레이드 오프가 존재한다.
Comparable에 대해 공부하다가 Comparator에 대해서도 알게되었다.
▪️ Comparator 인터페이스
Comparator 인터페이스를 활용하면 객체 외부에서 정렬 기준을 정의할 수 있다.
즉, 객체가 스스로 정렬 기준을 가지도록 강제하는 것이 아니라,
정렬 로직을 필요할 때 외부에서 주입하는 방식이다.
아래와 같이 다양한 기준을 손쉽게 정의할 수 있다.
public class CarComparators {
public static final Comparator<Car> byPosition =
Comparator.comparingInt(Car::getPosition);
public static final Comparator<Car> byName =
Comparator.comparing(Car::getName);
public static final Comparator<Car> byPositionThenName =
Comparator.comparingInt(Car::getPosition)
.thenComparing(Car::getName);
}
이후 정렬 시점에 cars.sort(CarComparators.byPosition)와 같이 원하는 기준을 선택해서 사용할 수 있다.
🤔 이로 인한 장점은 무엇일까?
바로 정렬 기준을 객체로부터 분리할 수 있어 유연함이 생긴다.
객체(Car)의 역할은 “도메인 로직”에 집중하도록 두고, 정렬 기준은 외부에서 “전략(Strategy)”처럼 주입할 수 있다.
-> 정렬 기준이 여러 개여도 Car 클래스는 전혀 수정하지 않는다.
🤔 Comparator의 단점은 무엇일까?
물론 Comparator에도 고려해야 할 트레이드오프가 있다.
정렬 기준이 외부에 있으므로 객체 내부 상태에 직접 접근해야 하는 경우 게터가 필요하다는 점이다.
▪️ String.repeat(int count)의 count는 음수이면 예외를 반환한다
"-".repeat(Math.max(0, car.getDistance()))
리뷰 중 repeat의 인자인 count에 단순히 "-".repeat(car.getDistance()) 하지 않고 Math.max()를 활용한 코드를 보았다.
🤔 왜 사용했을까?
Java의 String.repeat(int count) 메서드는
반복 횟수(count)가 음수일 경우 IllegalArgumentException을 발생시킨다.
따라서, 이를 방지하기 위해 Math.max(0, car.getDistance()) 를 사용하여
반복 횟수가 최소 0 이상이 되도록 보장한 것이다.
repeat()에 음수가 들어갈 수 있다는 가능성 자체를 미리 차단하며 안전한 코드를 만들다니,,
▪️ 출력시, 포맷팅 하는 책임 분리: OutputFormatter

이부분도 책임을 부여하여 분리할 수 있는지 몰랐다!!!
나는 단순히 OutputView에서 포맷팅 및 출력을 같이 해주었는데,
위의 사진과 같이 view와 formatter를 분리하면, 훨씬 코드가 훨씬 깔끔해지고 구조도 명확해진다는 걸 느꼈다.
출력은 OutputView가 담당하고,
“어떻게 문자열을 만들어낼지(포맷팅)”는 Formatter가 담당함으로써
각 객체가 자기 역할에만 집중할 수 있는 구조가 된다.
이런 작은 분리만으로도 유지보수성이 개선되고, 테스트하기도 쉬워지는 걸 체감할 수 있었다!
2. 내 리뷰에서 배운 점
▪️ Optional
처음에 Optional없이 아래 처럼 구현하였는데, Round 객체 생성 시 최소 범위를 1 이상으로 잡아두다 보니
decrease() 호출 시 마지막 라운드에서는 value가 0인 객체를 만들 수 없었다.
public boolean isLeft() {
return value > MIN_RANGE;
}
public Round decrease() {
return new Round(value - 1);
}
그래서 Optional을 사용하였고, 다음과 같은 리뷰를 받았다.

문제점: “empty가 의미하는 게 무엇인지” (게임 종료인지, 예외 상황인지 등) 명시적으로 보장되지 않기 때문에, 메서드를 사용하는 입장에서 불명확한 계약을 만듦
해결책
- 라운드 진행이라는 명확한 상태 전이를 다루려면 예외나 명시적 상태 객체가 더 나은 선택
- 아니면 정적 팩토리 메서드를 활용해서 처음 생성할 때만 최소 범위를 검증하는 정팩메 호출하고,
이후 decrease()할 때는 new를 호출해도 좋을듯!!
▪️ DTO 변환 위치
DTO 변환을 아래 코드처럼 도메인 객체 내부에서 처리했는데, 이 부분에서 여러 리뷰를 받았다.
public CarResult move() {
int number = numberGenerator.generate();
if (movementCondition.isSatisfiedBy(number)) {
this.position++;
}
return new CarResult(name.value(), position);
}



- 도메인 내부에서 DTO를 생성하는 경우
- 도메인이 외부 계층인 DTO에 의존하게 되어 의존성 방향 역전 및 계층 결합도 증가
- 외부(서비스, 컨트롤러 등)에서 DTO를 생성하는 경우
- DTO는 외부에서만 사용하므로, 도메인은 DTO 존재를 전혀 몰라도 되어 의존성 방향이 올바름
- DTO에서 도메인을 받아 생성하는 경우
- 변환 로직을 DTO 내부로 모아둠으로써 코드 중복을 줄임
나의 결론:
외부에서 DTO 생성하는 방법은 의존성 방향을 올바르게 유지하면서 도메인의 순수성을 보존할 수 있고,
그 중 DTO가 도메인을 받아 생성하는 방법은 변환 로직을 DTO 내부로 모아 일관성을 유지할 수 있기 때문에
도메인과 DTO 간의 결합도를 낮추면서도, 변환 로직이 한 곳(DTO 내부)에 모여 유지보수성과 가독성이 모두 좋아질 것 같다.
솔직히 처음에는 “이게 그렇게 큰 문제인가?”라는 생각이 들었는데,
리뷰를 통해 DTO 변환 위치가 코드 구조와 책임 분리에 얼마나 큰 영향을 주는지 처음으로 깊게 고민해볼 수 있었다.
DTO 변환 위치라는 작은 결정 하나가 생각보다 넓은 범위에 영향을 줄 수 있다는 걸 알게 되었고,
앞으로는 이런 부분도 더 신중하게 바라봐야겠다고 느꼈다.
10/30 ~ 11/02 Day 3,4,5,6
View 클래스, 꼭 인스턴스로 만들어야 할까?
1주 차 코드 리뷰에서 “지현 님이 생각하시는 domain은 무엇인가요?”라는 피드백을 받으면서, 도메인과 유틸리티의 경계를 명확히 해야겠다는 필요성을 느꼈다. 그래서 2주 차 미션에서는 아래와 같은 static 사용 기준을 직접 세워보고, Parser 클래스를 유틸성 클래스로 분리해보았다.
- 비즈니스 규칙과 직접적인 연관이 없는 공통 기능 클래스일 것
- 인스턴스 변수를 가지지 않아 상태를 지니지 않을 것
- 범용적으로 사용될 것
2주 차 미션을 진행하면서 “언젠가 다른 입출력 방식으로 확장할 수도 있으니, static보다는 인스턴스가 더 낫지 않을까?”라는 생각을 가지고 View를 인스턴스로 설계해 왔다.
그런데 곰곰이 살펴보니 View는 입력을 받고 출력하는 역할만 담당하고, 상태를 가지지도 않고 비즈니스 로직도 전혀 포함하지 않았다.
그래서 “그렇다면 굳이 인스턴스로 둘 필요가 있을까?”라는 의문이 생겼고, 지난 미션에서 세운 기준을 바탕으로 직접 실험하면서 인스턴스 구조가 static보다 실제로 나은 점이 있는지 검증해보기로 했다.
나는 그동안 View를 만들 때 인터페이스 없이 구현체만 생성했었다.
즉, 다형성을 활용하는 구조가 아니었기 때문에 인스턴스로 설계했을 때 얻을 수 있는 장점(유연성, 테스트 용이성 등)을 제대로 활용하지 못하고 있었다. 그런 상태에서 객체를 유지하는 건 그냥 형식적일 뿐 실질적인 이득이 없었다.
인터페이스가 없는 구조에서는 결국 InputView나 OutputView 내부 메서드를 직접 수정해야 했고, 리팩토링을 진행해보니 static과 인스턴스 간의 변경 비용도 사실상 비슷하다고 느꼈다.
그래서 이후 고민은 ‘확장성을 위해 인터페이스를 도입할지, 아니면 단순화를 위해 static으로 전환할지’로 이어졌다.
결론적으로 이번 미션의 맥락에서는 static으로 단순화하는 쪽을 선택했다.
camp.nextstep.edu.missionutils.Console을 사용해야 한다는 요구사항이 있었고, 프로그램 규모도 크지 않았기 때문이다.
이런 상황에서 확장성을 고려한 인터페이스 도입은 오히려 불필요한 복잡성만 높인다고 판단했다.
한 가지 걱정은 있었다.
static 멤버는 프로그램 종료 시까지 메모리에 상주하니까 불필요한 메모리 점유나 GC 부담으로 이어지진 않을까 하는 의문이었다.
그래서 직접 입출력 클래스를 static으로 전환한 뒤 IntelliJ Profiler로 힙 메모리를 측정해보았다.
Heap Memory 그래프는 약 250MB 수준에서 일정하게 유지되었고, 최대 힙 4GB 대비 사용량 변화도 거의 없었다.
이 실험을 통해 “static은 프로그램 종료까지 메모리를 잡고 있으니 비효율적이다”라는 막연한 우려가 항상 실제 문제로 이어지는 건 아니라는 걸 확인했다.
오히려 이번처럼 상태 없는 클래스라면 static이 더 단순하고 안정적인 선택이라는 생각이 들었다.
물론 static 메서드는 다형성을 활용할 수 없기 때문에 테스트에서 Mock 객체를 사용하기 어렵다는 단점도 있다.
하지만 View는 비즈니스 로직보다는 사용자 입출력(UI)에 집중된 계층이기 때문에, View 자체를 테스트하기보다는 Controller나 Model의 동작을 검증하는 편이 더 적절하다고 생각했다. 그렇다면 불필요하게 객체를 생성하는 것보다 static으로 단순화하는 게 더 명료하다고 느꼈다.

그렇게 고민을 마무리하고 있던 찰나, 커뮤니티에 View에서 static 메서드를 사용하는 것에 대한 토론 주제가 올라왔다. 며칠 동안 고민해온 내용이라 반가운 마음이 들었고, 내가 정리한 결론을 공유했다. 예상보다 공감이 많이 달려서(좋아요 8개!), 단순히 숫자를 떠나 내가 깊이 고민한 주제를 나만의 언어로 정리해 다른 사람들에게 설득력 있게 전달할 수 있었다는 점에서 큰 보람을 느꼈다.
이번 고민을 통해 “static 사용은 지양해야 한다”라는 단정적인 생각이 사실은 맥락에 따라 달라질 수 있다는 걸 다시 한번 느꼈다. 특히 이번 미션처럼 규모가 작고, 입출력의 변경 가능성이 낮고, 상태를 가지지 않는 클래스라면 오히려 static 메서드가 더 단순하고 명확한 선택이 될 수 있다는 점을 배웠다.
앞으로는 “확장성”을 고려하되, 그게 현재 맥락에서 정말 필요한 선택인지 한 번 더 고민하는 습관을 갖고자 한다.
실패를 통해 확립한 Enum 활용 기준
enum은 나에게 꽤 익숙하다고 생각했던 개념이었는데, 이번에 다시 보니까 그냥 무심코 써왔던 부분이 많았다.
그래서 이번 기회에 명확한 기준을 세우고자 학습과 실험을 병행하며 enum을 다시 바라보게 되었다.
사실 이전 미션들에서도 enum을 여러 번 사용했지만, 지금 생각해보면 명확한 기준 없이 사용한 경우가 많았다.
예를 들어 1주 차 미션에서는 콤마, 콜론, 커스텀 구분자 등 관심사가 전혀 다른 상수들을 하나의 enum에 모아두었다가
“관심사가 다른 상수들을 한 곳에 묶었다”는 리뷰를 받았다.
2주 차에는 예외 메시지를 enum으로 만들었는데, 생성 시점에 접두사를 붙이는 방식이 불변성을 깨뜨린다는 리뷰도 받았다.
이런 경험을 통해 “enum을 언제, 어떤 기준으로 사용해야 하지?”라는 근본적인 질문이 생겼다.
마침 이번 미션 요구사항에도 enum을 활용하라는 문구가 있었고, 이 기회에 enum을 제대로 이해해보자는 마음으로 탐구하기 시작했다.
당첨 순위를 표현하기 위해 Rank enum을 만들 때, 제일 먼저 “어떤 값을 enum으로 정의해야 할까?”라는 질문을 던졌다.
1주 차 피드백을 되새기며, 하나의 열거형에 속한 값들은 서로 같은 성격과 역할을 가져야 한다는 원칙을 세워보았다.
즉, 모든 멤버가 동일한 기준 아래에 있어야 하고, 어떤 값이 다른 값보다 상위·하위 개념이 되어서는 안 된다는 것이다.
이 기준에 비춰보니 각 당첨 순위는 ‘일치 개수, 보너스 여부, 상금’처럼 변하지 않는 규칙을 기반으로 동등하게 존재하는 값들이었다.
‘당첨 결과’라는 하나의 기준 아래에서 나란히 존재하고, 단지 조건의 차이로 구분될 뿐 역할의 우열이 없었다.
따라서 Rank를 enum으로 정의하였다.
이후 출력 기능을 구현하면서 또 다른 고민이 생겼다.
출력 순서가 정해져 있고 Rank와 1:1로 대응되다 보니, "Rank로부터 view와 관련된 책임을 분리하기 위해 DisplayRank라는 출력용 enum을 만들어도 괜찮지 않을까?"라는 생각을 했다.
그래서 DisplayRank를 만들고 formatLine() 메서드를 두어 당첨 순위에 맞춰 당첨 내역을 출력하는 문자열을 만들도록 했다.
처음에는 꽤 괜찮아 보였는데, 구현을 마치고 보니 출력 로직은 enum의 상태와 직접 관련이 없고,
출력 환경이나 요구사항에 따라 충분히 변할 수 있는 부분이라는 점을 깨달았다.
enum은 본질적으로 변하지 않는 상태를 표현하는 타입인데, 이렇게 변화에 취약한 행위를 내부에 두는 건 어색하다고 느꼈다.
이 과정을 통해 “enum의 멤버는 서로 같은 성격과 역할을 가져야 한다”라는 기준만으로는 충분하지 않다는 걸 느꼈다.
처음 세운 기준이 값의 구조적 동등성을 구분하는 데에는 도움이 되었지만, ‘행위나 책임의 분리’라는 관점까지는 포괄하지 못했다.
그래서 enum의 활용 기준을 다시 정의해보게 됐다.
결론적으로 enum은 변하지 않는 상태를 표현하고, 확장 가능성이 낮은 값들을 다룰 때 가장 적합하다는 결론에 도달했다.
그래서 출력 포맷팅 책임을 RankFormatter라는 별도 클래스로 분리했고, 결국 Rank는 상태를, RankFormatter는 행위를 담당하도록 설계를 바꿨다.
이 과정에서 역할이 명확하게 분리되었고, enum이 가진 장점을 더 잘 살릴 수 있었다.
이 경험은 단순한 설계 수정이 아니라, 내가 스스로 세웠던 기준의 한계를 마주하고 그 원칙을 다시 정의해 나간 과정이라고 느꼈다.
처음엔 명확한 기준이 없었기 때문에 시행착오를 겪었지만, 그 실패를 통해 사고의 폭이 넓어졌다.
무너진 기준 위에서 새로운 기준을 다시 세워가며 배우는 과정은 어렵지만 정말 재미있었다.
앞으로도 기준을 세우고 무너뜨리며 계속 성장해 나가고 싶다!
'Reflection' 카테고리의 다른 글
| [Gemini 웹앱 출시] 미션 회고 (2) | 2026.02.28 |
|---|---|
| 우아한테크코스 8기 최종 코딩테스트 회고 (4) | 2026.01.19 |
| 우아한테크코스 프리코스 - 2주 차 회고 (0) | 2025.10.28 |
| 우아한테크코스 프리코스 - 1주 차 피드백 정리 (0) | 2025.10.23 |
| 우아한테크코스 프리코스 - 1주 차 회고 (0) | 2025.10.21 |