
3주차 미션은 로또 게임이었다.
이번 주차는 설계 과정뿐만 아니라 코딩 과정에서도 유독 많은 고민을 했다.
사람들과 코드 리뷰를 통해 다양한 시각을 접하고, ‘함께-나누기’와 ‘토론하기’를 적극적으로 활용하며 생각의 폭이 한층 넓어졌다. 또한 2주차에 받았던 피드백을 되짚어보며 나의 부족했던 부분이 조금씩 보이기 시작했다.
그 덕분에 설계에 더 많은 시간을 들이게 되었고 구현을 하면서도 “이렇게 하면 안 될 것 같은데…” 하는 의문이 계속 따라다녔다.
이번 회고는 그 고민에 대한 나의 답을 찾아가는 과정의 기록이다.
먼저 2주차 미션에서 느꼈던 아쉬움을 3주차에서는 어떻게 행동으로 옮겼는지부터 이야기해보겠다.
2주차 때는 에러가 발생하면 약 5분 정도 코드를 살펴보며 문제를 찾았다.
하지만 솔직히 “운이 좋았다”는 느낌이 컸다.
그래서 3주차에는 에러가 발생하면 무조건 먼저 디버깅을 통해 에러를 찾고 해결하자라는 목표를 세웠다.
그 결과 이번 주차에서는 모든 에러를 디버깅으로 추적하며 원인을 정확히 파악하고 해결했다.
그중에서도 가장 처음 테스트 오류가 일어나 디버깅을 시도했던 예시를 공유하고 싶다.
private void validateLottoRange(List<Integer> numbers) {
if ((numbers.size() < LOTTO_NUMBER_MIN) || (numbers.size() > LOTTO_NUMBER_MAX)) {
throw new IllegalArgumentException("[ERROR] 로또 번호의 범위는 " + LOTTO_NUMBER_MIN
+ " ~ " + LOTTO_NUMBER_MAX + " 입니다.");
}
}
위 코드는 Lotto 객체에서 로또 번호의 범위를 검증하는 메서드이다.
테스트 코드는 다음과 같았다.
@Test
@DisplayName("로또 번호의 범위를 벗어나면 예외가 발생한다.")
void lottoNumberRangeTest() {
// given & when & then
assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 46)))
.isInstanceOf(IllegalArgumentException.class)
.hasMessageContaining("로또 번호의 범위는");
}
그런데 테스트가 통과하지 않았다.
분명히 검증 메서드는 문제가 없어 보였는데 왜 실패하지..?

이때 처음으로 디버깅 모드를 실행했다.
if문에 브레이크포인트를 걸고 조건을 살펴보니 46이 들어왔는데도 true가 아닌 false가 반환되고 있었다.
자세히 보니 numbers.size()를 비교하고 있었던 것!
번호의 범위를 검사하는게 아니라 리스트 크기를 검사하고 있었던 것이었다.. 😅
그래서 다음과 같이 코드를 수정했다.
private void validateLottoRange(List<Integer> numbers) {
for (int number : numbers) {
if ((number < LOTTO_NUMBER_MIN) || (number > LOTTO_NUMBER_MAX)) {
throw new IllegalArgumentException("[ERROR] 로또 번호의 범위는 " + LOTTO_NUMBER_MIN
+ " ~ " + LOTTO_NUMBER_MAX + " 입니다.");
}
}
}
수정 후 테스트를 다시 실행하니 바로 통과했다!!
이 예시는 아주 단순해 보일 수 있지만 나에게는 정말 큰 도전이었다.
‘코드의 동작을 눈으로 확인하며 문제를 추적한다’는 경험을 처음으로 했기 때문이다.
이후로 더 어려운 에러들을 마주했지만 이 첫 경험 덕분에 두려움 없이 차근차근 원인을 찾고 해결할 수 있었다.
앞으로도 에러를 두려워하지 않고 코드를 이해하며 디버깅으로 차근차근 해결하는 개발자로 성장하고 싶다.
3주차 미션을 시작하기 전 2주차 때 받았던 피드백을 천천히 다시 읽어보았다.
그중에서도 유난히 마음에 깊게 남은 피드백 세 가지가 있었다.


나는 지금까지 README에는 기능 구현 목록만 작성해왔다. 그 기능 목록 안에 클래스 설계와 구현 내용이 섞여 있었다.
게다가 이름을 짓기 애매하면 변수명에 자료형을 그대로 붙이는 습관도 있었다... 😅
이 피드백을 마음에 새기고 3주차 미션을 시작할 때 가장 먼저 이를 직접 개선해보기로 목표를 세웠다.
README 구성 방식 개선
설계를 시작하면서 README를 새롭게 작성했다.
먼저 프로젝트 소개와 설계 목표를 정리하고 이후 실행 예시 → 기능 구현 목록 → 설계 구조 및 책임 분리 순으로 구성했다.
모든 내용을 한눈에 보기 어려워 README 링크를 첨부한다.
의미 중심 네이밍으로 개선
변수명과 메서드명에 자료형을 전혀 사용하지 않았다.
예를 들어 NumberList 같은 이름 대신 winningNumbers처럼 의미 중심으로 표현했다.
README의 중요성
README를 상세하게 작성하면 코드를 처음 보는 사람도 프로젝트의 흐름을 빠르게 파악할 수 있다.
단순한 정리 문서가 아니라 “이 프로젝트가 어떤 문제를 해결하고 어떤 구조로 접근했는가”를 보여주는 프로젝트의 얼굴이라는 걸 느꼈다.
기능 목록과 설계 분리의 효과
기능 구현 목록에는 무엇을 해야 하는가가 명확히 보이고 구조 설계 부분에서는 어떻게 설계했는가가 드러나면서 전체 구조가 한눈에 정리되었다.
이 분리가 코드 작성 방향을 잡는 데 큰 도움이 되었다.
의미 중심 이름의 힘
자료형 대신 의미로 이름을 짓다 보니 변수명만 봐도 역할이 명확하게 드러나고 코드의 가독성이 확연히 높아졌다.
단순한 네이밍 규칙의 문제가 아니라 의도를 표현하는 코드로 가는 과정이었다.
이 피드백들을 단순히 “하지 말라니까 안 해야지”라고 받아들이지 않았다.
“왜 이런 피드백이 나왔는지”를 이해하고 그 이유를 내 것으로 만드는 과정에서 진짜 성장이 있었다.
앞으로도 매주 주어지는 피드백을 단순히 수정 지시로 보지 않고 그 속에 담긴 의도를 해석하고 체화하는 개발자가 되고 싶다.
3주차 미션 프로그래밍 요구 사항 3번에는
Java Enum을 적용하여 프로그램을 구현한다.
라는 요구 사항이 있었다.
‘이걸 안 쓰면 문제를 못 푸는 건가?’라는 생각이 들 정도로 왜 꼭 Enum을 써야 하는지 이해가 잘 되지 않았다.
하지만 일단 도전을 해보는 게 배우는 길이라고 생각했다.
“일단 써보자. 대신 왜 써야 하는지는 직접 느껴보자!”
그렇게 마음을 먹고 설계를 시작했다.
처음에는 단순히 상수들을 Enum에 넣으려고 했다.
public enum Rank {
FIRST, SECOND, THIRD, FOURTH, FIFTH, NONE
}
그런데 이렇게 만들고 나니 전혀 쓸모가 없었다.
등수를 계산하려면 여전히 if문으로 개수를 비교해야 했고 Enum을 왜 써야 하는지 더 혼란스러웠다.
그래서 하나씩 찾아보며 사용법을 익혔다.
Enum도 필드, 생성자, 메서드를 가질 수 있다는 걸 알게 되었고 그제서야 조금 감이 오기 시작했다.
"아 단순한 상수의 집합이 아니라 값과 행동을 함께 담는 객체처럼 쓸 수 있구나!"
이걸 깨닫고 나서 Rank를 이렇게 다시 설계했다.
public enum Rank {
FIFTH(3, false, 5000),
FOURTH(4, false, 50000),
THIRD(5, false, 1500000),
SECOND(5, true, 30000000),
FIRST(6,false, 2000000000),
NONE(0, false, 0);
private final int winningNumberCount;
private final boolean isBonusNumber;
private final long prizeMoney;
Rank(int winningNumberCount, boolean isBonusNumber, long prizeMoney) {
this.winningNumberCount = winningNumberCount;
this.isBonusNumber = isBonusNumber;
this.prizeMoney = prizeMoney;
}
}
여기서 한 가지 생각이 떠올랐다.
"순위를 계산하는 책임은 Rank가 갖는 게 맞지 않을까?"
그렇게 Rank안에 등수를 판별하는 행동을 넣었다.
public static Rank from(int winningNumbersMatchCount, boolean bonusNumberMatchResult) {
if (winningNumbersMatchCount == 6) return FIRST;
if (winningNumbersMatchCount == 5 && bonusNumberMatchResult) return SECOND;
if (winningNumbersMatchCount == 5) return THIRD;
if (winningNumbersMatchCount == 4) return FOURTH;
if (winningNumbersMatchCount == 3) return FIFTH;
return NONE;
}
이걸 작성하면서 처음으로 "Enum이 진짜 객체처럼 행동할 수 있구나"라는 걸 체감했다.
예전 같았으면 '등수를 판별하는 심판 클래스'를 따로 만들어서 수많은 if문으로 조건을 검사했을 것이다.
하지만 Enum을 쓰니 각 Rank 상수가 자신의 규칙을 직접 알고 있어서 등수 판단 이라는 책임이 자연스럽게 Rank 내부로 모였다.
이번 도전을 통해 Enum은 단순한 상수 모음이 아니라 의미 있는 상태와 행동을 함께 가진 객체라는 걸 배웠다.
그리고 그 덕분에 다음과 같은 장점을 직접 느꼈다.
만약 Enum을 쓰지 않았다면 ResultCalculator 안에서 등수를 일일이 if문으로 비교하고 상금 계산을 별도의 메서드에서 따로 처리했을 것이다.
그랬다면 규칙이 조금만 바뀌어도 여러 곳의 코드를 수정해야 했을 것이고 등수 정보와 로직이 흩어져 구조가 훨씬 복잡해졌을 것이다.
하지만 Enum으로 Rank를 정의하니 당첨 규칙과 상금 정보가 한곳에 모여 응집도가 높아졌고 코드가 스스로 규칙을 설명하는 구조가 되었다.
이 경험을 통해 Enum은 단순히 문법적인 요구사항이 아니라
설계의 의도를 더 명확히 드러내고 유지보수성을 높여주는 도구라는 걸 확실히 느꼈다.
디스코드 ‘토론하기’ 채널을 둘러보다가 “getter를 지양해야 한다”는 글을 우연히 읽게 되었다.
처음에는 단순히 “값을 가져오는 메서드일 뿐인데, 왜 지양해야 하지?” 하는 생각이 들었다.
하지만 글을 읽어 내려가면서 “객체의 내부 상태를 꺼내 계산하는 행위 자체가 결국 캡슐화를 깨뜨리고 책임이 흩어지게 만든다”는 설명이 마음에 깊게 남았다.
그 내용을 떠올리며 내 코드를 천천히 살펴보던 중 LottoIssuer 클래스의 다음 코드가 눈에 들어왔다.
int count = money.getMoney() / LottoPrice.UNIT;
처음에는 아무 문제 없어 보였지만 곰곰이 생각해보니 ‘로또를 살 수 있는 개수를 계산하는 책임’은 Money의 몫이었다.
LottoIssuer는 단지 “돈으로 로또를 발행하는 역할”만 하면 되는 객체다.
그래서 Money 객체에 아래 메서드를 추가했다.
public int purchasableCount() {
return money / LottoPrice.UNIT;
}
그리고 LottoIssuer에서는 이렇게 수정했다.
int count = money.purchasableCount();
이제 LottoIssuer는 Money의 내부 상태를 알 필요가 없어졌다.
그저 "너로 몇 장 살 수 있니?"하고 메시지를 보내는 형태로 바뀐 것이다.
이 과정에서 깨달은 또 하나의 중요한 점은 getter를 완전히 없애야 한다는 건 아니라는 것이다.
객체가 단순히 상태를 외부에 전달해야 하는 역할이라면 예를 들어 View나 DTO로 데이터를 전달할 때는 getter가 자연스럽고 필요하다.
“비즈니스 로직을 위해 객체의 내부 상태를 끌어다 쓰는 getter”는 지양해야 하지만,
“표현 계층으로 데이터를 전달하기 위한 getter”는 오히려 명확한 의도를 드러낸다.
결국 중요한 건 “getter를 쓰느냐 안 쓰느냐”가 아니라 “이 getter가 객체의 책임을 침범하는가?”를 판단할 수 있는 감각이었다.
이 경험을 통해 “getter를 지양하라”는 말의 진짜 의미를 이해했다.
처음엔 단순히 “getter를 쓰지 말자”는 문법적 조언으로만 들렸지만 사실 그 말은 “객체의 상태를 외부에서 읽어 계산하지 말고 그 행동을 객체 스스로 하게 만들어라.”라는 철학적인 메시지였다.
getter를 썼다는 건 “이 객체의 상태를 내가 알아야 한다”는 뜻이고 그 순간 이미 객체의 자율성을 침범하고 있는 셈이다.
이번 경험으로 Money와 LottoIssuer의 역할이 훨씬 명확해졌고 서로의 의존도도 줄어들었다.
getter를 없애는 게 목적이 아니라 객체가 스스로 행동하도록 만들고 필요할 때만 상태를 안전하게 드러내는 것이 진짜 객체지향이라는 걸 배웠다.
앞으로는 "왜 getter를 써야 하는가"를 스스로 설명할 수 있는 개발자가 되고싶다.
우아한테크코스 3주차 미션을 통해 이번에도 많은 성장을 경험했다.
물론 아직 내가 보지 못한 문제나 약점도 분명 있을 것이다.
하지만 회고와 메타인지를 반복하며 나는 분명 한 걸음씩 성장하고 있다고 믿는다.
4주차에는 어떤 미션이 주어질지 모르지만 또 어떤 문제를 마주하고 어떤 배움을 얻게 될지 기대가 된다.
이 마음을 유지하며 배움이 주는 즐거움을 잊지 않고 읽기 쉬운 코드와 변화에 강한 설계를 만들어가는 개발자가 되고 싶다.
앞으로도 계속 도전하고 실패하더라도 다시 도전하며 성장의 과정을 멈추지 않을 것이다.
이상으로 3주차 회고를 마치겠다.