23.10.30 ~ 23.12.14동안 진행한 TDD, 클린 코드 with Java 17기의 교육을 무사히 완주했다. 약 한달 반정도 교육을 진행하며 새롭게 학습하고, 부족했던 부분들을 미션별로 정리해보자.
1. 자동차 경주 - 단위 테스트
이전에 자바 플레이 그라운드 With TDD, 클린 코드에서 한 번 경험했던 미션이라 어렵지 않게 수행할 수 있었다. 다만 앞서 말한 교육을 경험하지 않은 상태에서 TDD, 클린 코드 With Java과정을 들었다면 교육을 무사히 끝마칠 수 있었을 까 생각이 든다.
1-1. 학습 테스트
- 테스트를 통해 다양한 API를 학습할 수 있다. 보통 학습 테스트라고 불려진다.
1-2. 문자열 덧셈 계산기
- 인자로 받은 매개변수의 값을 변경해 반환하는 것은 피하도록하자.
public int sample(int a) {
if (a%2 == 0) {
a *= 2;
} else {
a -= 1;
}
return a;
}
1-3. 자동차 경주
- 테스트는 하나의 목적을 가지고 인풋, 아웃풋이 명확해야한다. 아래와 같은 경우 b의 값을 랜덤 값으로 초기화하는 줄을 주석처리해도 테스트는 통과하게 된다. 또 랜덤 값이기 때문에 결과 값이 명확하지가 않아 테스트를 범위로 할 수 밖에 없다.
@Test
void sample() {
int a = 5;
int b = 0;
b = Math.random * a;
assertThat(b).isBetWeen(0, 5);
}
- getter지양 - 디미터 법칙, 객체의 상태를 꺼내는 것은 중복되는 코드, 결합도 증가, 캡슐화 깨짐 등의 문제가 있다. getter를 써야만 하는 경우도 있지만 가급적 getter보단 객체에 메세지를 보내 해결하자.
- 도메인은 UI에 의존하면 안된다. 반대는 어느정도 허용가능
- 메서드 선언에 throws를 사용하는 것은 해당 메서드를 호출한 객체에게 예외의 책임을 떠넘기기 때문에 가능한 지양하자.
2. 로또 - TDD
1-1 문자열 계산기
- 반복되는 부분은 추상화, 람다를 활용 하자.
public int calculate(int x, int y, String operator) {
if ("+".equals(operator)) {
return x + y;
}
if ("-".equals(operator)) {
return x - y;
}
if ("*".equals(operator)) {
return x * y;
}
}
// 위와 같은 코드 보단
// 아래와 같은 코드를 지향.
public class StringCalculator {
public int calculate(int x, int y, String operator) {
IntBinaryOperator folmula = OperationSymbols.findFolmula(operator);
return folmula.applyAsInt(x, y);
}
}
public enum Operator {
PLUS("+", (a, b) -> a + b),
MINUS("-", (a, b) -> a - b),
MULTIPLICATION("*", (a, b) -> a * b),
...
}
// 혹은 folmula자체를 인터페이스로 만들어 각 연산기호 별로 구현하여 만들 수도 있음
1-2 로또 게임
- 일급 컬렉션, 원시 값 포장 클래스의 장점을 활용 하자 - 객체 생성 자체만으로도 해당 객체의 상태를 보장할 수 있다. 더불어 객체 또한 자신의 상태를 검증하는 것이 최우선이다.
- 한 번에 여러개를 테스트할 때는 AssetJ의 assertThat()보단 JUnit의 assertAll()을 사용하자 - 전자는 테스트가 실패하면 뒤의 테스트가 실행안됨
assertAll(
() -> assertEquals(2,rankCountGroup.findWinningCountBy(FIRST)),
() -> assertEquals(1, rankCountGroup.findWinningCountBy(SECOND)),
() -> assertEquals(1, rankCountGroup.findWinningCountBy(THIRD)),
() -> assertEquals(2, rankCountGroup.findWinningCountBy(FOURTH)),
() -> assertEquals(1, rankCountGroup.findWinningCountBy(FIFTH))
);
- 일반적으로 check, validate, verify 등의 메서드 명은 주로 검증 처리를 뜻하며 void 반환타입을 가진다. 해당 메서드명에 반환 타입이 void가 아니면 오해가 생길 수 있다.
- 상황에 따라 적절한 자료구조를 사용하자. ex) 중복을 없애고 싶을 땐 Set, enum을 활용한 Map이 필요할 땐 EnumMap 등
- 이펙티브 자바 아이템 46 - 스트림에서는 부작용 없는 함수를 사용
- 위 내용과 더불어 Stream의 forEach()에선 상태 값을 변경 하면 안되고 결과를 가져올 때만 사용하도록 하자.
- 이펙티브 자바 아이템 32 - 제네릭과 가변인수를 함께 쓸 때 신중하게
- 다만, 타입 안전한 부분에서는 사용해도 OK
- while, for문 같은 반복문 대신 재귀함수를 활용해보자
- @MethodSource, @EnumSource 등 다양한 테스트 방식을 활용해 테스트 해보자
@ParameterizedTest
@MethodSource("provideArguments")
void countMatchingNumbersWithBonus(LottoNumbers lotto, int expectedResult) {
// given
LottoNumbers winningNumber = new LottoNumbers(Set.of(1, 5, 12, 21, 32, 43));
// when
int count = lotto.countMatchingNumbers(winningNumber);
// then
assertThat(count).isEqualTo(expectedResult);
}
private static Stream<Arguments> provideArguments() {
return Stream.of(
Arguments.of(new LottoNumbers(Set.of(1, 5, 12, 21, 32, 43)), FIRST.matchingCount()),
Arguments.of(new LottoNumbers(Set.of(1, 5, 12, 21, 32, 42)), THIRD.matchingCount()),
Arguments.of(new LottoNumbers(Set.of(1, 5, 12, 21, 31, 42)), FOURTH.matchingCount()),
Arguments.of(new LottoNumbers(Set.of(1, 5, 12, 20, 31, 42)), FIFTH.matchingCount())
);
}
- 정적 팩토리 메서드와 인스턴스 캐싱을 사용해보자. - 로또 게임에서 로또 번호 같은 객체는 반복적으로 생성될 수밖에 없다.
- 정적 팩토리 메서드는 생성자에 네이밍을 해줄 수 있어 의도를 명확하게 만들어준다. 인스턴스 캐싱은 반복적으로 생성되는 객체를 미리 생성해 재활용하므로 메모리 낭비를 줄일 수 있다.
3. 사다리 - FP, OOP
TDD의 본질은 객체를 테스트하기 쉬운 상태로 만들어 유지보수성이 높은 코드를 작성하는 것이다. 보통 TDD를 '테스트 코드 작성 후 프로덕트 코드 작성'과 동일시 하는데 이는 오해가 생길 수 있는 표현이다. 앞서 말했 듯 TDD의 본질은 유지보수성을 높이는 개발 방법론이고, '테스트 코드 작성 후 프로덕트 코드 작성'의 방법은 이를 실현하기 위해 많이 사용되는 방법일 뿐이다. 그렇기 때문에 꼭 테스트를 먼저 작성하는 것에 집착하지 않아도 된다.
3-1. 사다리 게임
- 요구 사항을 확대 해석해 오버 프로그래밍하지 않기
- 로그는 모아서 출력하는 습관 길들이기
- IntStream().range() or Stream.generate() or Stream.iterator()을 상황에 맞게 적절히 사용하기 - 좀 더 많은 상황을 경험해야 할 듯하다. 아직은 어떤 상황에 어떤 것을 써야할 지 애매하다.
- Stream을 사용할 때 상태를 저장하고 싶다면 객체를 만들어 이전 상태를 저장해보자.
- Map타입의 변수명을 네이밍할 때는 'value'By'Key'를 활용하자. ex) heightByName
- 반복되는 부분은 Enum, 람다, 추상화를 하자.
4. 수강 신청 - 레거시 코드 리팩토링
이전 단계들을 통해 학습하고 배운 것을 실무와 비슷한 환경에 적용하는 연습을 할 수있는 단계다. 가장 충격으로 다가왔던 점은 서비스 레이어에서 작성했던 핵심 로직을 도메인으로 옮기는 것이다. 즉, 객체를 테스트하기 쉬운 상태로 만들어 단위테스트를 진행하려면 각각의 객체가 자신의 역할에 맞는 행위를 가져야 한다. 이를 통해 서비스 레이어의 테스트를 통합테스트가 아닌 단위 테스트로 만들 수도 있다.
도메인과 엔티티의 구분에 대해 몸소 느끼게 되고, RDB와 OOP의 패러다임에 따른 차이를 JPA와 같은 기술로 어느정도 메꿀순 있지만 분명한 한계점이 존재한다는 것을 알게 됐다. 하지만 아직은 더 많은 경험을 해야 이 부분에 대해 확실하게 인지할 수 있을 듯하다.
4-1. 레거시 코드 리팩토링
- 예외를 던질 때 추적이 용이하도록 예외의 원인이 되는 변수를 같이 남기자.
- Checked, Unchecked Exception의 상황에 맞는 활용 - Checked Exception을 사용할 땐 신중하게 결
4-2. 수강 신청
- DB의 테이블을 고려하지 않고 도메인을 먼저 설계하는 것이 핵심
- JPA에 익숙하다 보니 도메인 == 엔티티라는 생각을 가져 이를 풀어 나가는 것이 어려웠음
- '강의'라는 도메인과 '수강신청'이라는 도메인을 분리
- 강의는 DB와 직접적으로 매핑되는 객체, 수강신청은 DB와 매핑되지 않고 비즈니스 로직을 담당하는 객체
- 강의가 가지는 역할과 행위를 가독성, 유지보수성이 용이하게 만들기 위해 분리
- 객체의 역할과 책임에 맞는 행위(메서드)를 가지자.
- 테스트 검증에 필요하다고 해서 메서드를 만드는 것은 반드시 지양해야 한다. 단, 생성자는 ok
5. 정리
TDD, 클린 코드 with Java(리뷰 O, 이하 A)와 자바 플레이 그라운드 With TDD, 클린 코드(리뷰 X, 이하 B) 두 교육의 최대 장점은 정량적인 기준을 통해 자연스럽게 클린한 코드의 작성과 테스트 코드의 중요성 깨우치기다. A교육을 수강하며 올해 초 B 교육을 수강했을 때 만큼 충격적으로 인사이트를 얻은 부분은 없었다. 다만 리뷰가 없는 B교육이다 보니 중간 중간 부실한 부분이 있는 느낌이었고, 이번 A교육을 통해 빈틈 있는 부분을 충분히 메꿀 수 있었다. 이번 교육을 통해 공통적으로 나온 리뷰들과 앞으로의 학습 방향에 대해 정리해보자.
1. 역할과 책임에 맞는 적절한 클래스, 메서드, 변수의 네이밍
- 메서드명만으로 반환타입, 매개변수의 유무가 어느정도 예측이 되어야 한다.
- 클래스, 변수의 네이밍도 마찬가지로 해당 객체의 역할과 책임을 명확하게 나타내어야 한다.
- 이 부분은 많은 경험을 통해 체득할 수 밖에 없을 듯하다.
2. 요구사항을 확대 해석하여 오버 프로그래밍
- 익숙한 도메인인 경우 다음 단계의 요구사항을 미리 적용하거나 요구 사항을 확대 해석해 오버 프로그래밍을 한 경우가 종종 있었다. 좋지 않은 습관이니 의식적으로 해당 기능이 요구 사항에 부합하는 지 고민하자.
3. 클래스의 역할과 책임 구분에 따른 분(엔티티와 도메인의 분리)
- 마지막 단계에서 가장 애를 먹었던 부분이다. 아래의 학습 방향을 고려해보자.
- 처음엔 단순히 기능위주로 구현 -> 리팩토링하면서 클래스 역할에 따른 분리.
- 만들면서 배우는 클린 아키텍처
- 도메인, 도메인 서비스 등 DDD와 관련된 학습
마지막으로 재밌는 주제가 있다. 이번 교육을 수강하며 우연하게 유튜브 알고리즘에 떠서 보게 된 영상이다.
위 영상을 간단하게 요약하자면 'TDD에 대한 비판' 이다. 해당 영상의 옳고, 그름을 따지기 보단 이러한 관점도 존재하는 구나로 받아드렸다. 확실한 것은 이 영상을 보기 전까진 TDD에 대한 맹목적인 믿음이 존재 했다는 것이다. 아직은 경험이 적기에 확실한 판단을 세우기 어렵다. 당장의 생각을 정리하자면, 중요한 것은 좋은 테스트 코드를 작성하는 것이다. 그 시점이 언제인지는 당장 중요한 문제는 아니다.
TDD관련 교육을 수강 중에 유튜브에 우연하게 뜬 영상의 내용이 TDD 비판인 것이 참 웃픈 상황이다. 덕분에 아주 좋은 타이밍에 여러 관점으로 생각을 할 계기가 된 것 같아 기분이 좋다. 배우는 것을 당연하다 생각하며 받아드리기 보단 비판적으로 타당한 지에 대한 고민을 충분히 하도록 노력하자.