첫 번째 java 미션은 프리코스 때도 진행했던 로또 문제였다.
페어 프로그래밍을 처음 해봐서 스스로 부족했던 부분들이 많았던 것 같다ㅜ
👀 미션 진행 방식
첫 미션이다보니 미션 진행 방식이 어렵게 느껴졌다.
일단 미션은 두 단계로 나뉜다.
1단계
https://github.com/woowacourse/java-lotto/pull/579
페어 프로그래밍으로 진행한다. 페어와 네비게이터 / 드라이버 역할을 번갈아서 맡으며 코드를 작성한다.
처음에는 제한 시간을 따로 정하지 않고 진행했는데, 하다보니 좋은 방법이 아닌 것 같아서 20분 타이머를 맞춰놓고 알람이 울리면 현재 진행중인 기능까지 완료한 다음에 넘기는 방법으로 바꿨다.
로또 피드백 과정에서 페어 프로그래밍 진행 방식에 대해서도 질문이 나왔는데, 코치님은 제한 시간을 철저하게 지키는 방법을 제안해주셨다. (현재 진행중인 것과 상관 없이 바로 역할 교환)
+ 바로 다음 출석 미션에서는 코치님이 제안해주신 방법대로 했는데, 개인적으로 코치님 말씀대로 진행하는게 더 잘 맞았던 것 같다!
1단계 코드에 대해서 리뷰어에게 리뷰를 받고 피드백하는 과정을 거친다. 그리고 1단계 코드가 merge 되면 2단계를 시작한다.
2단계
https://github.com/woowacourse/java-lotto/pull/619
2단계는 페어와 헤어지고 혼자서 진행한다.
이번 로또 미션 2단계는 기존 코드를 리팩터링 하는 것이었다.
🛠️ 리팩터링
테스트하기 어려운 코드 개선하기
로또 피드백 강의에서 다뤘던 '테스트가 어려운 코드 개선하기' 내용을 참고해서, RandomNumberGenerator를 인터페이스로 추상화하고 의존성을 주입하는 방식으로 리팩터링 했다.
테스트하기 어려운 코드란?
객체 내부에 통제 불가능한 로직이 포함되어 있을 때 (ex. 실행 시점마다 테스트 결과가 달라짐)
일단 테스트가 어려운 코드 예제를 살펴보자.
public List<Integer> getRandomNumbers(int minNumber, int maxNumber) {
Set<Integer> numbers = new HashSet<>();
Random random = new Random(); // 통제할 수 없는 로직
while (numbers.size() != LOTTO_SIZE) {
int number = random.nextInt(maxNumber - minNumber + 1) + minNumber;
numbers.add(number);
}
return new ArrayList<>(numbers);
}
이와 같이 코드가 작성된 경우, 로또의 당첨 결과를 테스트하는 과정에서 어려움을 겪을 가능성이 높다.
예를들어 통합 테스트 과정에서 '생성된 로또 번호를 정해두고, 이를 기반으로 당첨 결과가 잘 나오는지 확인하고 싶은 경우'가 있을 수 있다.
이때, 위의 코드대로 테스트를 진행하면 생성 로또 번호를 정해둘 수 없기 때문에 테스트가 어려워진다.
interface RandomGenerator {
int generate();
}
// Fake 객체
class TestRandomGenerator implements RandomGenerator {
int generate() {
// ...
}
}
class LottoNumberGenerator {
private final RandomGenerator randomGenerator;
// 의존성 주입
public LottoNumberGenerator(RandomGenerator randomGenerator) {
this.randomGenerator = randomGenerator;
}
public static List<Integer> getRandomNumbers(int minNumber, int maxNumber) {
Set<Integer> numbers = new HashSet<>();
while (numbers.size() != LOTTO_SIZE.getValue()) {
int number = generator.generate();
numbers.add(number);
}
return new ArrayList<>(numbers);
}
}
위 코드는 테스트를 어렵게 하는 부분을 인터페이스로 추출하고, 외부에게 책임을 빼내어 문제를 개선한 것이다.
이렇게 되면 테스트에 필요한 가짜 객체를 직접 생성해서, 원하는 값으로 테스트를 진행할 수 있다.
지난번 테스트 코드 세미나에서 들었던 내용이기도 하고, 오브젝트 아니면 좋은코드 나쁜코드 책에서도 나왔던 내용이다.
✍🏻 느낀점 정리
⭐️ 자신만의 명확한 근거를 기반으로 코드를 작성해야 한다
리뷰어에게 리뷰를 받으면서 가장 크게 와닿았던 부분은, 본인 스스로 납득할만한 근거에 기반한 코드를 작성해야 한다는 점이다.
실제로 페어 프로그래밍 과정에서 어영부영 작성했던 코드에 대해서 '왜 이렇게 작성하셨나요?' 라는 질문을 받을 때마다, 그 이유를 설명하기 어려워서 당황스럽기도 했다.
질문을 받는 시점이 되어서야 '진짜 왜 이렇게 짰지?' 라고 생각했던 적이 매우 많았다.
따라서 페어 프로그래밍 과정에서 서로 의문이 드는 지점에 대해서는 바로 질문하고 토론해야 한다는 생각이 들었다.
이 과정에서 내 생각을 상대방에게 설득할 수도 있고, 내가 설득 당할 수도 있다.
하지만 누구의 의견이 맞냐가 중요한게 아니라, 서로가 납득할만한 근거로 코드를 작성해야 한다는 점이다!
그리고 지금까지 해보면서 느낀 것은 코드에는 정답이 없다는 것이다. 자신만의 기준으로 코드에 명확한 근거를 가지고 있냐 가지고 있지 않느냐가 큰 차이를 만드는 것 같다.
테스트가 어렵다 👉🏻 역할 분리의 신호
난수 생성기 분리 과정을 통해, 테스트가 어려운 경우에는 해당 객체가 너무 많은 책임을 맡은게 아닌지 점검해 볼 필요성이 있다는 생각이 들었다.
getter를 쓸 수 밖에 없는 상황에서는 getter를 써야한다
객체지향 관련 도서 / 포스팅을 보다보면, "getter 대신 메시지를 전달하라"는 내용을 많이 접한다.
나도 이 내용에 집착(?)하다보니 당첨 상금 객체(WinningInfo)에 위와 같은 메서드를 정의했다.
- getter를 사용하지 않기 위해서는 WinningInfo 내부에서 출력 포맷을 결정해야 한다고 생각했기 때문이다.
그리고 위와 같이 코멘트를 받은 후, getter를 사용해서 출력 포맷을 정하는 방식과 getter를 사용하지 않고 getInfo 메서드를 사용해서 출력 포맷을 정하는 방식을 비교하게 되었다.
일단 뷰는 도메인에 비해서 변경에 매우 취약한 계층이다.
하지만 getInfo 메서드를 정의하는 순간 WinningInfo 도메인 객체는 뷰에 대한 책임을 떠안게 된다. 이는 결국 WinningInfo는 출력 요구사항이 변경될 때마다 바뀌어야 하므로, 변경에 취약한 도메인이 될 수 밖에 없다.
이러한 생각을 거쳐 getter를 사용하는 단점이 getInfo를 사용하는 단점보다 훨씬 작다는 생각이 들었고, WinningInfo에 getter를 추가하여 출력 책임을 전적으로 뷰에게 넘기는 방식으로 리팩터링했다.
위 코멘트를 보고, getter 대신 메시지를 사용하라는 법칙에 너무 집착하지 말자는 생각이 들었다.
값을 꺼내야할 때는 꺼내야 한다!
객체가 관리할 데이터는 클래스명이 아닌 역할에 따라서 결정해야 한다
'로또 가격을 어디서 관리해야할까?' 를 생각해봤을 때, 단순히 Lotto에서 관리하는게 자연스럽겠다는 생각이 들었다.
지금 다시 생각해보면 Lotto 객체의 역할을 고려한 것이 아닌, 클래스명만 보고 데이터 관리의 책임을 결정지었던 것 같다.
그리고 리뷰어가 관련 코멘트를 남겨주셨다.
코드를 작성하면서 고민했던 부분이라 당연히 쉽게 답변할 수 있다고 생각했는데, 답변을 작성하다보니 뭔가 이상했다.
'진짜 로또 가격을 Lotto 객체가 알고 있는게 맞을까?' 라는 의문이 계속 들었다.
사실상 Lotto 객체는 자신의 가격을 외부에 알려주기만 할 뿐, 가격과 관련된 행동은 전혀 하지 않기 때문이다.
그리고 Lotto 객체와 Money 객체의 책임이 각각 무엇인지 다시 생각해봤다.
생각해보니 Lotto 객체는 로또 번호와 관련된 책임만 가질 뿐, 금액 계산과 관련한 책임을 생각하고 만든 객체가 아니었다.
따라서, 금액 관련 책임을 떠올리며 만들었던 Money 객체에게 로또 금액 관리의 책임을 옮기는 것이 타당하다는 생각이 들었다.
책임을 결정하는 과정에서, 단순히 클래스명으로만 책임의 주체를 판단하는 것을 경계해야겠다고 느낀 순간이었다.
클래스명이 주는 이미지가 매우 강하지만, 그보다 객체의 역할이 무엇인지 떠올린 후 해당 역할에 필요한 데이터만 관리하도록 하는 것이 중요하다고 다시금 느꼈다.
'우아한테크코스 7기 > 회고' 카테고리의 다른 글
[레벨1] 장기 미션 회고록 2 - 1단계 (추상 클래스, 합성) (1) | 2025.04.20 |
---|---|
[레벨1] 레벨 인터뷰 정리 (4) | 2025.04.14 |
[레벨1] 장기 미션 회고록 1 - 우테코가 바라는 것 (2) | 2025.04.12 |
[레벨1] 블랙잭 미션 회고록 (4) | 2025.04.08 |
[레벨1] 출석 미션 회고록 그리고 TDD (6) | 2025.03.11 |