3주차 프리코스를 진행하는 과정에서 내가 올바른 방식으로 회고를 하고 있는지에 대한 의문이 들었다.
지금까지 나는 회고를 위해 메타인지를 하는 과정에서 내가 안다고 생각했던 것과 고민했던 점들, 의사결정 이유만을 정리하고 있었다.
이것들을 정리하는 과정에서 앞으로의 계획이나 내가 부족했던 점을 잘 정리하고 있지 않은 것 같다는 생각이 들어 우선 "회고" 란 무엇인지 찾아보기로 마음먹었다.
회고란?
지난 일을 순서대로 정리하고, 방식을 복기하면서 아쉬운 점과 앞으로 적용할 수 있는 점들을 기록하는 것.
제대로 된 회고를 하기 위해 나는 KPT 회고 템플릿을 사용해 회고록을 작성하기로 마음먹었다.
KPT 회고 템플릿이란?
Keep : 잘하고 있는 점. 계속 했으면 좋겠다 싶은 점.
Problem : 뭔가 문제가 있다 싶은 점. 변화가 필요한 점.
Try : 잘하고 있는 것을 더 잘하기 위해서, 문제가 있는 점을 해결하기 위해서 시도해 볼 것들
위 사항들을 정리하는 것이다. Problem 을 점차 개선해나가고, Try 를 시도해나가며 내가 성장하는 것을 느낄 수 있다고 생각한다.
Keep - 잘하고 있는 점. 계속 했으면 좋겠다 싶은 점
Keep1 - 일급 컬렉션 사용
일급 컬렉션을 사용하며 객체지향의 핵심 내용 중 하나인 "객체에 메시지를 던져라" 를 더 깊게 이해하고 실천할 수 있었다.
객체의 컬렉션을 직접 다루지 않고 일급 컬렉션을 사용하는 과정에서 의미 없는 getter 를 사용하는 것을 왜 지양해야 하는지 피부로 느낄 수 있었고, 캡슐화에 대해 깊게 고민해볼 수 있었다.
LottoNumber 의 일급 컬렉션인 LottoNumbers
public class LottoNumbers {
private final List<LottoNumber> lottoNumbers;
private LottoNumbers(List<LottoNumber> lottoNumbers) {
this.lottoNumbers = lottoNumbers;
}
public static LottoNumbers from(List<LottoNumber> numbers) {
return new LottoNumbers(numbers);
}
public static LottoNumbers generate(int size, RandomNumberGenerator randomNumberGenerator) {
validateSize(size);
List<Integer> numbers = randomNumberGenerator.pickUniqueNumbersInRange(
LottoNumber.MIN_NUMBER,
LottoNumber.MAX_NUMBER,
size
);
return new LottoNumbers(numbers.stream()
.map(LottoNumber::from)
.toList()
);
}
private static void validateSize(int size) {
if (size > LottoNumber.MAX_NUMBER - LottoNumber.MIN_NUMBER + 1) {
throw LottoNumberInvalidException.tooManyLottoNumbersSize();
}
}
public List<Integer> mapToInt() {
return lottoNumbers.stream()
.map(LottoNumber::getNumber)
.toList();
}
public boolean hasSize(long size) {
return lottoNumbers.size() == size;
}
public boolean hasUniqueElements() {
return hasSize(lottoNumbers.stream()
.distinct()
.count()
);
}
// .......
}
대표적으로 hasSize() 와 hasUniqueElements() 를 사용함으로써, 객체에 메시지를 던진다는 의미를 이해할 수 있었고.
getter 사용을 지양하며 프로그래밍하니 객체의 캡슐화에 대해 이해할 수 있었다.
Keep2 - 이름을 가지는 정적 팩토리 메서드
정적 팩토리 메서드와 생성자의 차이점을 잘 이해하고 있지 못하다가, 2주차 코드리뷰에서 정적 팩토리 메서드를 사용한 다른 지원자분의 코드를 접했다.
차이점을 몰라 해당 지원자분의 코드를 이해하는 데에 어려움을 겪었고, 찾아봐야겠다는 생각이 들었다.
정적 팩토리 메서드의 장점 중 하나인 객체 생성에 이름을 가질 수 있어 객체의 생성 목적을 담을 수 있다는 점에 깊이 공감할 수 있었고, 복잡한 생성 로직을 추상화시킬 수 있다는 점을 이해할 수 있었다.
다만, 상속이 되지 않으니 이 점은 주의해서 사용해야겠다는 생각을 했다.
여러 군데에서 적용했지만, 대표적으로 LottoNumber 의 일급 컬렉션인 LottoNumber 를 랜덤으로 생성한다는 의미를 가진 정적 팩토리 메서드를 언급하고 싶다.
public static LottoNumbers generate(int size, RandomNumberGenerator randomNumberGenerator) {
validateSize(size);
List<Integer> numbers = randomNumberGenerator.pickUniqueNumbersInRange(
LottoNumber.MIN_NUMBER,
LottoNumber.MAX_NUMBER,
size
);
return new LottoNumbers(
numbers.stream()
.map(LottoNumber::from)
.toList()
);
}
LottoNumbers 의 기본 생성자가 아닌 generate() 라는 이름을 가진 정적 팩토리를 호출함으로써, size에 대한 검증이나 랜덤 번호를 생성한다는 생성 로직을 내부로 감출 수 있었고, 명확한 이름을 통해 가독성을 높일 수 있었다.
Keep3 - View 와 Model 의 의존성 분리를 위한 Response 클래스 사용
지금까지 View 에서 Model 을 직접 전달받아 getter 를 통해 데이터를 출력하고 있었다. 이 방식을 사용한다면, View 는 Model 을 다룰 권한이 생긴다. 나는 이것이 올바른 설계가 아니라고 판단했고, 지인과 토론을 가졌다.
다음과 같은 의견이 나왔고
의견 1: View는 Model을 매개변수로 받아 데이터를 출력하되, Model을 수정하지 않으면 괜찮다.
의견 2: View는 Model을 매개변수로 받으면 안 되고, 필요한 데이터만 담긴 객체를 매개변수로 받아 사용해야 한다.
나는 의견 2를 채용하기로 결정했다. View 가 Model 을 매개변수로 받는 이상, View 가 Model 의 수정을 하지 못하도록 설정할 수 있는 방법이 없다고 판단했기 때문이다.
이를 구현하기 위해 각 View에 출력할 데이터를 담기 위해 여러 개의 Response 클래스를 만들었다.
하지만, 이 Response 클래스들에 데이터를 넣기 위해 Model에서 데이터를 추출하는 역할을 누가 담당해야 하는가에 대한 고민이 들었다.
MVC 원칙 상 이는 Controller 가 담당해야 하지만, 그렇게 된다면, Response 의 수가 많아질수록 Controller가 너무 많은 책임을 지고, 코드가 뚱뚱해질 것 같다는 생각이 들었다.
그렇다고 Response 에서 Model 을 받아 자신을 초기화하자니 View 클래스 내부에서 Model 에 대한 import 가 생겨 찜찜한 기분이 들었다.
결과적으로는, Controller의 책임을 줄이기 위해 Response 클래스가 Model 에서 반환된 결과를 받아 스스로 초기화하는 방식을 선택했다. View 패키지에서 Model의 의존성을 완전히 제거할 수는 없었지만, Controller 가 너무 많은 책임을 지지 않게 되었고, Response 가 Model 을 직접적으로 저장하지 않기에 최소한의 의존성은 분리되었다고 생각한다.
public class LottoNumberResponse {
private final List<Integer> lottoNumbers;
private LottoNumberResponse(List<Integer> lottoNumbers) {
this.lottoNumbers = lottoNumbers;
}
public static LottoNumberResponse from(LottoNumbers lottoNumbers) {
return new LottoNumberResponse(lottoNumbers.mapToInt());
}
public List<Integer> getLottoNumbers() {
return lottoNumbers;
}
}
Problem : 뭔가 문제가 있다 싶은 점. 변화가 필요한 점
Problem1 - 구입 금액의 자료형 (AtomicInteger)
요구사항 중 하나인 "사용자가 잘못된 값을 입력할 경우 그 부분부터 다시 입력받는다" 를 구현하기 위해, Supplier를 사용해 구현했다.
public static <T> T retryOnCustomException(Supplier<T> supplier) {
try {
return supplier.get();
} catch (RuntimeException e) {
handleException(e);
return retryOnCustomException(supplier);
}
}
하지만, Controller 코드에서 문제가 발생했는데, 상황은 다음과 같다.
int purchaseMoney;
Lottos lottos = Retryer.retryOnCustomException(() -> {
purchaseMoney = inputView.inputPurchaseMoney();
return lottoShop.purchaseRandomLottos(purchaseMoney.get(), randomNumberGenerator);
});
하지만, 람다식에서 사용되는 변수는 final 이어야 하기 때문에, purchaseMoney 의 값을 변경할 수는 없었다.
이 문제를 해결하기 위해, AtomicInteger 를 사용하도록 수정했고, 해결할 수 있었다.
그러나, AtomicInteger 를 사용하는 것이 좋다고 생각하지는 않는다. AtomicInteger 를 사용하는 이유는 멀티쓰레드 환경에서 동시성을 보장하기 위함이 제일 큰데, 현재 AtomicInteger 를 사용한 이유는 해당 이유가 아니기 때문이다.
AtomicInteger 를 사용하지 않고 구현할 방법을 찾았어야 한다고 생각한다. 이번 주차 과제에서도 재시도 요구사항이 있기에, AtomicInteger 를 사용하지 않는 다른 방법을 찾아야 한다.
Problem2 - View 에서 사용하는 Response 클래스에 정의된 equals()
View 에서의 사용을 위한 Response 클래스를 테스트하던 도중, Map 을 사용해야 하는 상황이 생겼다.
public class LottoScoreResponses {
private final Map<LottoScoreResponse, Integer> lottoScoreResponses;
private LottoScoreResponses(Map<LottoScoreResponse, Integer> lottoScoreResponses) {
this.lottoScoreResponses = lottoScoreResponses;
}
public static LottoScoreResponses from(Map<Score, Integer> scores) {
Map<LottoScoreResponse, Integer> lottoScoreResponses = new LinkedHashMap<>();
scores.forEach(
(score, count) -> lottoScoreResponses.put(LottoScoreResponse.from(score), count)
);
return new LottoScoreResponses(lottoScoreResponses);
}
public Map<LottoScoreResponse, Integer> getLottoScoreResponses() {
return lottoScoreResponses;
}
}
로또 결과 Enum 인 Score Map 을 받아 LottoScoreResponse Map 으로 변환해주는 코드인데, 이 코드를 테스트하는 과정에서 Map 에 제대로 들어갔는지 테스트할 수 없었다.
Enum 은 유일 인스턴스를 가지기 때문에, equals 를 재정의할 필요 없이 Map 에 잘 들어가지만, LottoScoreResponse 는 equals 를 재정의하지 않으면 테스트 시, Map 에 잘 들어갔는지 확인할 수 없었다.
이 문제를 해결하기 위해 어쩔 수 없이 LottoScoreResponse 에 equals() 와 hashCode() 를 재정의하였다.
내가 가장 싫어하는 상황이 발생했다. 테스트 코드만을 위한 함수를 만들게 된 것이다.
이를 해결하려면 LottoScoreResponse 의 구조를 변경하거나 더 나은 방법을 찾았어야 했다. 하지만, 시간이 촉박해 그렇게 할 수 없었다.
이번 주차 미션에서는 절대 이런 일이 발생하지 않게 노력할 생각이다.
Try : 잘하고 있는 것을 더 잘하기 위해서, 문제가 있는 점을 해결하기 위해서 시도해 볼 것들
Try1 - 일급 컬렉션을 사용할 때의 규칙을 만들자
일급 컬렉션은 컬렉션에게 메시지를 던지기 위해 사용한다고 생각한다. 이 철학을 지키기 위해 일급 컬렉션을 사용할 때에 지켜야 하는 규칙을 만들어야 한다.
현재 생각하고 있는 것은
1. 일급 컬렉션은 private 메서드와 정적 팩토리 메서드를 제외한 모든 public 메서드에서 컬렉션 필드를 사용해야 한다.
2. getter, setter 는 사용하지 않는다.
3. 정적 팩토리 메서드를 사용해 메지시를 던져 생성되게 한다.
정도이다. 이 규칙을 통해 일급 컬렉션을 명확하고 구조적으로 사용할 수 있을 것이라 기대한다.
Try2 - 정적 팩토리를 사용할 때, 더욱 신중하게 고려한다
현재, 정적 팩토리를 사용해 메시지를 던지는 방식으로 객체를 초기화하도록 구현했다. 좋은 방법이지만, 고려해야 할 점들도 분명하게 존재한다.
정적 팩토리를 사용할 때 고려해야 하는 점들.
1. 정적 팩토리 메서드는 static 이므로, 상속을 통한 오버라이딩이 되지 않는다. 상속 가능성이 있다면, 이 점을 고려해야 한다.
2. 아무런 의미 없이 습관적으로 사용하는 것을 경계해야 한다.
모든 의사결정에는 근거가 있어야 한다. 정적 팩토리를 사용하는 것을 습관화하기보다는, 의식적으로 고려해야 한다고 생각한다.
Try3 - View 패키지 내부의 Response 클래스들에서 Model을 다룰 때 fianl 을 사용하자
현재, Response 클래스들은 Model 을 받아 자신의 필드들을 초기화한다. View는 이 Response 클래스들을 사용해 데이터를 출력한다.
하지만, Response 가 Model 의 수정 권한을 가진다는 문제점은 여전히 남아 있다.
이를 명시적으로 방지하고, 최소한의 변경을 방지하기 위해, Response 가 Model 을 매개변수로 받을 때 final 키워드를 사용하게 한다.
'우아한테크코스 7기 프리코스' 카테고리의 다른 글
7기 프리코스 4주차 회고 (KPT 회고) (2) | 2024.11.12 |
---|---|
7기 프리코스 2주차 회고 (2) | 2024.10.29 |
7기 프리코스 1주차 회고(알고 있다고 생각했던 것들) (1) | 2024.10.23 |