들어가며.
이번 3주차는 로또를 주제로 미션을 수행했는데요. 이전 2주차 미션보다 많은 요구 사항으로 인해 더 어렵다는 느낌을 받았습니다. 특히 이번 3주차 공통 피드백을 보니 TDD를 제대로 이해하고 처리하지 못했던 것 같습니다. 그래서 이번 회고록은 진행하면서 학습했던 내용도 좋지만 공통 피드백을 보면서 로또 미션에서 실수했던 부분이나 혹은 잘했던 부분에 대한 회고록을 작성하고자 합니다.
테스트 코드를 먼저 작성해야해.
어떻게 보면 TDD를 하는 의미를 지키기 위해서는 반드시 지켜야하는 사항이 아닌가 싶습니다. 이미 개발을 들어가고 테스트 코드를 작성하면 그것은 TDD를 하는 의미가 없겠죠. 이전 테스트 주도 개발의 의미와 방법에 대해서라는 글을 작성했음에도 지키기 힘든 것도 사실인 것 같습니다. 개발을 하면서 처음 설계대로 코드를 작성하고 동작한다면 지키기 쉽겠지만 개발이라는 것이 생각대로 되면 모든 개발자 분들이 고민할 이유가 없겠죠.
이럴수록 README 작성의 중요성이 부각되는 것 같습니다. 요구 사항을 세밀하게 나눌 수록 관련된 기능들을 한 클래스로 만들기 좋다는 것을 알 수 있었습니다. 너무 큰 요구 사항, 기능을 두면 테스트 코드 작성을 먼저 할 때 막막한 기분을 느낄 수 있는데요. 그것은 TDD를 할 때 작은 기능부터 테스트 작성이라는 전제가 깔려있기 때문이라 생각합니다.
그래서, 이번에 System.in, System.out은 테스트 하지 않도록 한다는 프로그래밍 요구 사항이 있어서 Input 관련된 클래스는 테스트를 하지 않았는데요. 문제는 제가 Input 클래스에 로또 금액과 관련된 validate를 넣어버리니 이에 대해서는 검증을 안하고 넘어가 버리는 상황을 마주하게 되었습니다.
private static void validateAmount(int amount) {
if (isAmountUnit(amount)) {
throw new CustomException(ErrorMessage.PURCHASE_AMOUNT_ERROR_MESSAGE.format(PURCHASE_UNIT));
}
if (isMaxPurchase(amount)) {
throw new CustomException(ErrorMessage.LOTTO_MAX_PURCHASE_ERROR_MESSAGE.toString());
}
}
이 코드에 대해 검증을 하지 못했던 것이었죠. 물론 통합 테스트를 통해 확인을 한 부분이지만 클래스 분리를 명확히 하지 못했다는 생각을 지울 수가 없었습니다. 추후 미션을 진행할 때는 System.in, System.out 을 완전 분리하고 다른 클래스를 제대로 검증해야 한다는 것을 알 수 있었습니다.
객체는 객체답게 사용하자.
제가 1주차부터 시작해서 2주차, 3주차 때 계속해서 고민하며 적용한 부분이었는데요. 이번에 공통 피드백으로 이 내용이 나와서 제가 제대로 된 길로 학습하고 있다는 생각을 가질 수 있었습니다. 객체가 상태를 변경할 때 데이터를 꺼내 외부에서 변경하는 것이 아닌 객체 스스로가 자신의 상태를 변경하도록 즉, 일을 하도록 만들어야 한다는 것입니다.
예를 들자면, 로또의 번호를 검증하는 것은 로또 번호를 들고 있는 Lotto 클래스가 해야하는 일이지 외부에서 numbers를 꺼내 변경하는건 옳지 못합니다.
public class Lotto {
private final List<Integer> numbers;
public Lotto(List<Integer> numbers) {
validate(numbers);
this.numbers = numbers;
}
private void validate(List<Integer> numbers) {
}
private int getMatchedCount(List<Integer> winningNumbers) {
HashSet<Integer> compareNumbers = new HashSet<>(numbers);
HashSet<Integer> compareWinning = new HashSet<>(winningNumbers);
compareNumbers.retainAll(compareWinning);
return compareNumbers.size();
}
private boolean hasBonusNumber(int bonusNumber) {
return numbers.contains(bonusNumber);
}
}
위 코드 처럼 numbers를 가지고 있으면 이를 명령하듯이 처리해야하는 것이죠. 만약 Getter를 사용해서 numbers를 외부에서 불러 처리하게 되면 다음과 같은 문제가 발생합니다.
public class LottoServie {
public int getMatchedCnt(List<Integer> winningNumbers) {
Lotto lotto = new Lotto(List.of(1,2,3,4,5,6));
int matched = 0;
for(int number : lotto.getNumbers()) {
if (winningNumbers.contains(number)) {
matched++;
}
}
return matched;
}
}
만약 LottoService라는 클래스에서 getter로 불러 쓰다가 Lotto 클래스의 numbers의 구조가 변경되면 어떻게 될까요? getter를 사용하는 곳을 전부 고치는 안타까운 일이 발생하게 됩니다. 이에 대한 내용은 Getter, Setter 를 지양하는 이유에 대해서에 정리해두었습니다.
Enum, 물론 좋은데... 이것도?
요구 사항 중 하나인, "Java Enum을 적용해 프로그램을 구현한다." 를 지키고자 연관성 있는 데이터인 로또 등수에 대한 집합을 Rank 라는 enum으로 관리하도록 구현했습니다.
public enum Rank {
NOTHING(0, 0),
FIFTH_PLACE(3, 5000),
FOURTH_PLACE(4, 50000),
THIRD_PLACE(5, 1500000),
SECOND_PLACE(5, 30000000),
FIRST_PLACE(6, 2000000000);
private final int matchedCount;
private final long winningAmount;
Rank(int matchedCount, long winningAmount) {
this.matchedCount = matchedCount;
this.winningAmount = winningAmount;
}
public static Rank findRank(int matchedCount, boolean isBonus) {
Rank find = Arrays.stream(Rank.values())
.filter(rank -> rank.matchedCount == matchedCount)
.findFirst()
.orElse(NOTHING);
if (find == Rank.THIRD_PLACE && isBonus) {
return Rank.SECOND_PLACE;
}
return find;
}
public int getMatchedCount() {
return matchedCount;
}
public long getWinningAmount() {
return winningAmount;
}
}
등수마다 맞춘 개수, 당첨금을 묶어서 관리하기 수월하게 구현했으며 Rank를 반환하는 메서드를 내부에 구현해 둠으로써 이전 내용인 "객체는 객체답게 사용하자"라는 것 또한 지킬 수 있었습니다. 이는 가독성과 유지보수를 챙겨주었으며, 확실히 개발 이전에 Enum에 대해 정리해 둔 내용(Enum 열거 타입에 대해 알아보기)이 저에게 많은 도움이 되었습니다.
하지만 개발하면서 의문이 든 부분이 있습니다. 그것은 "지극히 단순한 상수에 대해서도 Enum으로 관리하는 게 맞을까?"라는 의문이었습니다. 저는 이번 로또 미션을 하면서 상수를 클래스로 묶어 static final로 선언하여 사용하였는데요. 이는 Enum을 사용하는 것이 가독성에서 불편한 점이 많았기 때문입니다.
public class Constant {
public static final int MIN = 1;
public static final int MAX = 45;
public static final int LOTTO_SIZE = 6;
public static final int PURCHASE_UNIT = 1000;
}
public enum ConstantEnum {
MIN(1),
MAX(45),
LOTTO_SIZE(6),
PURCHASE_UNIT(1000);
private final int number;
ConstantEnum(int number) {
this.number = number;
}
public int getNumber() {
return number;
}
}
위 두 코드를 비교하면 어떤 게 작성하기 편할까요? 그리고 사용하기에 적절할까요? Enum이 확실히 IDE의 이점을 챙길 수 있겠지만 막상 사용하는 코드를 보면 길어지는 것을 알 수 있습니다. 한 개 아닌 여러 상수를 비교해야 하는 경우가 생기면 더 길어지지 않을까 하는 생각이 들기도 했습니다.
private boolean isNonSize(List<Integer> numbers) {
return numbers.size() != Constant.LOTTO_SIZE;
}
private boolean isNonSize(List<Integer> numbers) {
return numbers.size() != ConstantEnum.LOTTO_SIZE.getNumber();
}
그래서 이번 미션을 할 때는 연관된 데이터 집합이 두 개 이상이면 Enum으로 처리하고 한 개인 경우에는 상수 클래스로 하자고 마음먹고 작성했습니다. 하지만 이번 3주차 코드 리뷰를 진행하면서 다른 지원자분들과 얘기를 나눠보니 연관된 값이 한 개인 경우도 추후 두 개로 변경될 가능성도 있기에 Enum이 맞다는 생각이 들었습니다.
마치며.
이번 프리코스 3주차는 이전 기수 분들이 했던 내용을 보지 않고 처음부터 끝까지 개발했는데요. 확실히 1, 2주차를 경험하니 기능 구현을 완성할 수 있었고 정말 뿌듯했습니다. :) (제가 지키고자 노력했던 내용이 공통 피드백에 나오니 더 뿌듯하기도 했네요) 아직 부족한 점도 많지만 더욱 배우고 성장할 수 있다는 점이 몰입할 수 있게 만드는 것 같습니다.
이제 프리코스 마지막 4주차 미션은 "편의점" 인데요. 마지막이다보니 더 많은 요구 사항이 저를 기대하게 만드는 것 같습니다. 3주차는 시간 제한 없이 완성이 목표였다면 4주차는 돌아가는 쓰레기를 만든다는 목표로 5시간 내에 완성을 노리고자 합니다.
프리코스도 1주차를 남겨두고 있네요. 아쉬운 시간이 다가오고 있지만 다른 지원자 분들도 힘내서 좋은 경험을 얻고가면 좋겠습니다. 읽어주셔서 감사합니다! (_ _)
'회고록' 카테고리의 다른 글
[우아한 테크코스] 프리코스 4주 차 회고록 (9) | 2024.11.12 |
---|---|
[우아한 테크코스] 프리코스 2주 차 회고록 (4) | 2024.10.28 |
[우아한 테크코스] 프리코스 1주 차 회고록 (7) | 2024.10.22 |
[Goormton] 구름톤 트레이닝 풀스택 7회차를 마치며. (0) | 2024.08.16 |