본 회고는 2주차 미션 수행에 대한 코드 설명보다는 2주차 미션의 핵심 키워드인
TDD
와설계
를 어떻게 하려고 노력했는지 작성한 글입니다.
TDD
는 이번 생에서 처음이라 2주차 미션인 숫자 야구 게임 미션을 통해서 TDD
와 테스트 코드
와 최대한 친해지고 익숙해지는 데 최대한 노력하려고 했습니다.
TDD와 친해지기 위한 나만의 약속
- 무조건
테스트 코드 작성 -> 기능 구현 -> 테스트 -> 커밋
순으로 기능을 구현하자. (기능 구현 먼저 X)- 이번
TDD 개발 방식
으로 개발하면서 TDD 개발방식에 익숙해지고TDD의 장단점
을 최대한 이해해보자.
사실 위의 과제 요구사항을 보고 이번 숫자야구에 대한 기능들을 어떻게 나눌까 고민한 끝에 순서도
를 한번 그려보았다.
위의 그림을 토대로 기능 구현 목록을 한 번 추려보았다.
메인 로직
- 컴퓨터 랜덤 숫자 구현
- 출력함수 구현(원하는 메시지 출력) - (ex) 종료, 시작 메시지)
- 입력함수 구현(숫자 입력, 재시작 입력)
- 컴퓨터 랜덤 값과 입력값 비교 함수
- 비교 결과값 문자로 변환 함수(ex) 1볼 1스트라이크)
에러 관련 로직
- (숫자야구 상황) 입력값이 숫자인지 아닌지 확인
- (숫자야구 상황) 서로 다른 3자리 수인지 확인하는 함수
- (재시작/종료 상황) 입력값이 1아니면 2인지 확인하는 함수
기능 구현 목록 중 에러로직을 테스트 코드 작성 -> 기능 구현 -> 테스트 -> 커밋
같은 방식으로 구현을 하다가 너무 세세한 부분?까지 테스트 코드를 작성하다보니 기능 구현보다 테스트 코드만을 위한 코드가 되어버린다는 느낌이 들었다. ( 내가 무엇을 하고 있는지 감이 안잡히고 뭔가 주객전도가 된 느낌이었다....)
그래서 이번에는 내가 테스트할 값들이 무엇인지 알아보면서 세세한 기능들로 기능 구현 목록들을 나누는 것이 아니라 최대한 추상화해서 내가 테스트해야할 코드들만 기능 구현 목록들에 추가해 보았다.
내가 테스트할 코드들은
순서도에 있는 마름모 모양의 로직
이라고 생각하였고 아래와 같이 기능 구현 목록들을 구현했다.메인 로직
- (숫자야구 상황) 랜덤 값과 입력값를 비교 -> 같거나 다를 때의 결과값을 테스트
- (숫자야구 상황) 컴퓨터 랜덤 숫자 구현
- (재시작/종료) 입력값이 1일때 재시작, 2일때 종료하는 함수
에러 관련 로직
- 숫자야구 게임할 때 입력 시 발생하는 에러
- 재시작/종료 할 때 입력 시 발생하는 에러
1차 수정 후 작성한 기능 구현 목록을 토대로 코드를 작성하려고 보니 추상화한 기능 목록들에 필요한 작은 함수들이 생각이 나 새로 기능 구현 목록을 작성했다.
메인 로직
- (숫자야구) 랜덤 값과 입력값를 비교 -> 같거나 다를 때의 결과값을 테스트
- (숫자야구) 스트라이크인지 확인
- (숫자야구) 볼인지 확인
- (숫자야구) 전체 입력값이 스트라이크와 볼 배분하는 함수(개수 세기)
- (숫자야구) 잘 배분된 스트라이크와 볼이 문자로 잘 변환되었는지 확인
- (숫자야구) 삼진아웃됬는지 확인하는 함수
- (숫자야구) 컴퓨터 랜덤 숫자 구현
- (재시작/종료) 입력값이 1일때 재시작, 2일때 종료하는 함수
- (입출력) 입력함수 구현 -> 숫자야구 함수 조합해보기
- (입출력) 출력함수 구현
- (재시작) 입력값이 1일때 재시작하는 함수
- (재시작) 입력값이 2일때 종료하는 함수
에러 관련 로직
- 숫자야구 게임할 때 입력 시 발생하는 에러
- 재시작/종료 할 때 입력 시 발생하는 에러
따라서 위의 기능 구현 목록을 토대로 테스트 코드 작성 - 기능 구현 - 테스트 - 커밋
순으로 기능을 모두 구현하였습니다.
앞서 작성한 기능 구현 목록들을 한 번 살펴보자
이처럼 세부적인 기능(함수)들을 각각의 클래스로 분류, 세분화
하면 좋을 거 같다고 판단하여 다음과 같이 설계하였습니다.
에러 객체의 경우 기능 추가, 수정함에 있어서 언제든지 에러 관련 로직을 추가, 삭제가 가능하게끔 객체 설계 5원칙 중
DIP
를 지키면서 설계하였습니다. (APP클래스와 BaseBallException, NextException 클래스의 종속성을 제거하기)
DIP
를 어떻게 구현하는지 알고 싶으신 분들은 객체지향 5원칙 SOLID 글을 읽어보세요~ (제가 정리한 글입니다 ㅎㅎ)
1주차 코드 리뷰를 하면서 변수를 상수화하여 가독성을 높이면 좋다는 의견이 있어서 이번 2주차 미션에 적용하였습니다. 추가적으로 객체 동결함수인
Object.freeze()
를 사용하여 수정이 불가능하도록 만들었습니다.
utils/constants.js
...
const COMMAND = Object.freeze({
START_MESSAGE: '숫자 야구 게임을 시작합니다.',
QUESTION: '숫자를 입력해주세요 : ',
NEXT_QUESTION: '게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요',
STRIKEOUT: '3개의 숫자를 모두 맞히셨습니다! 게임 종료',
CLOSE: '게임 종료',
RESTART: '1',
EXIT: '2',
});
...
1주차 코드리뷰를 하면서 class객체 내 변수를
private 변수
를 한 코드를 보고 객체 외부에 사용하는 메서드와 내부에 사용하는 메서드를 구분 하여 가독성을 엄청 높일 수 있겠다고 판단하여 이번 2주차 미션에 바로 적용해보았습니다.
(저는 1주차 때는 protected 변수 선언인_
를 사용했는데 이는 airbnb 코드 컨벤션에서 지양한다고 하더라구요 ㅜㅜ)
생각해보니 private메서드는 클래스 외부에서는 사용이 불가능, 이때까지 내가 작성한 테스트를 동작시킬 수 없다는 것이다...
심지어 JAVA나 코틀린의 경우 리플렉션을 통해 private 함수가 테스트 가능하지만 Jest는 이 private 함수 테스트를 전혀 지원하지 않았다.
일단은 #private 함수의 결과값을 getter 함수
로 가져와 테스트하였습니다.
class BaseBall {
...
#isStrike(randomItem, inputItem) {
return randomItem === inputItem;
}
#countStrike(random, input) {
return input.filter((inputItem, index) => this.#isStrike(random[index], inputItem)).length;
}
...
/* tho code below are getter functions for only testing private method */
getIsStrikeResult(randomItem, inputItem) {
return this.#isStrike(randomItem, inputItem);
}
getCountStrikeResult(random, input) {
return this.#countStrike(random, input);
}
...
private 관련 테스트에 대한 고민을 하다가 저와 같은 고민을 하시는 분들이 많이 계셔서 거기서 많은 인사이트를 얻었습니다.
나의 생각은 ??
이번 과제에서 TDD를 겪으면서 세분화된 함수들 모두 테스트하면서 제일 신경쓰였던 점이 중복된 케이스에 대한 유사한 테스트
를 할 때가 참 많았다는 것입니다.
describe('App 클래스 - countStrike()', () => {
test('2볼 1스트라이크 상황', () => {
const random = [2, 1, 3];
const input = [1, 2, 3];
const app = new App();
expect(app.countStrike(random, input)).toEqual(1);
});
test('노볼 노스트라이크 상황', () => {
const random = [1, 2, 3];
const input = [4, 5, 6];
const app = new App();
expect(app.countStrike(random, input)).toEqual(0);
});
test('3스트라이크 상황', () => {
const random = [1, 2, 3];
const input = [1, 2, 3];
const app = new App();
expect(app.countStrike(random, input)).toEqual(3);
});
});
describe('App 클래스 - isStrikeOut()', () => {
test('3볼일 때', () => {
const random = [2, 1, 3];
const input = [3, 2, 1];
const app = new App();
expect(app.isStrikeOut(random, input)).toBeFalsy();
});
test('2스트라이크 노볼 상황', () => {
const random = [1, 2, 3];
const input = [1, 4, 3];
const app = new App();
expect(app.isStrikeOut(random, input)).toBeFalsy();
});
test('3스트라이크일 때', () => {
const random = [1, 2, 3];
const input = [1, 2, 3];
const app = new App();
expect(app.isStrikeOut(random, input)).toBeTruthy();
});
});
핵심은 private 메서드를 모두 사용하는 public 함수의 테스트 결과이며 이 public 함수만 모든 케이스에 대한 통과를 한다면 전혀 문제가 없을 거라는 내 개인적인 생각이다.
추가적으로 public함수 테스트 제외하고 로직 상 private 함수 테스트를 해야할 경우 이는 객체 설계가 잘못되었다고 할 수 있다. (따라서 저는 private 메서드 테스트를 위한 getter함수를 모두 제거하였다.)
private함수를 모두 테스트하는 것보다 public함수만을 테스트하는 것이 기능개발 속도, 테스트 모두 다 챙겨갈 수 있는 내가 생각하는 이상적인 개발방식인 거 같다.
- 만들려고 하는 앱에서 내가 테스트를 해야할 값들이 무엇이 있는지 확인한다 (ex. 순서도)
- 테스트를 할 값들을 구현하기 위한 세부 함수들을 작성해본다.
1,2 과정
을 통해서 내가 구현해야할기능 구현 목록
들을 작성해본다.- 이 기능 구현 목록들을 토대로
객체 설계
를 해본다.- 해당 객체의
public 함수
의 테스트 코드를 작성
(만약 private 함수에 대한 결과값이 미심쩍다면 테스트 코드를 작성하자 -> 불안하면 테스트 작성 gogo)feat:기능 구현
test:테스트
refactor:리팩토링
commit:커밋
설계가 끝났다면
5-6-7-8
을 반복하면서 개발을 하자
1. 조금은 믿음이 가는 내 코드
사실 저번 1주차같은 경우 문제 1 ~ 문제 7번까지 코드를 작성하면서 내 코드 "다른 케이스에 대해서도 테스트가 과연 통과할까"라는 의문을 품으면서 제출 전까지도 계속해서 코드를 보고 테스트케이스를 만들었다.
하지만 이번 2주차에는 TDD를 적용해보면서 각 함수에 대한 경우의 수를 미리 테스트해보면서 좀 더 통합된 기능을 만들 때도 오류에 대한 걱정을 덜게 되었습니다.
2. 리팩토링 시간
사실 이번에 리팩토링을 하면서 객체 구조를 바꾸고 생성한 객체에 따라 파일을 분리하는 작업을 거쳤습니다. 그 과정을 통해서 테스트 코드가 있다는 것만으로도 리팩토링 작업이 굉장히 빠르게 소요되어서 너무 편했고 코드를 부담없이 수정할 수 있다는 점이 너무나 좋았다.
1. 기능 구현 개발 속도
아무래도 똑같은 기능에 대한 코드를 두 개나 작성하다보니 개발 속도 측면에서는 크게 뒤쳐질 수 밖에 없다.
2. 테스트 코드 메서드에 대한 이해
jest에 대한 다양한 함수에 대한 이해가 필요하고 이를 잘 사용할 수 있어야 합니다. jest에 대한 어느 정도 학습이 필요하다는 점을 단점으로 뽑고 싶다.
빠른 기능 구현을 목적으로 한다면 TDD 사용은 힘들 수 있지만 유지보수가 중요하고 안정적인 웹을 만들고 싶다면 TDD는 무조건적으로 시행하면 좋을 것 같다.
객체 설계를 위한 객체지향에 대한 깊은 이해와 다양한 디자인 패턴에 대해 잘 알고 있었다면 좀 더 폭넓은 시각으로 객체 설계를 했을 것이라는 아쉬움이 남는다.
TDD학습에 유용한 자료들
전반적인 TDD과정에 대한 예시
자바지기님 TDD강의
TDD와 애자일
좋은 테스트 코드와 TDD를 하는 이유
와우 정리 너무 깔끔하네요
테스트 코드 짜는게 참 어렵더라구요 ... 익숙해지려 많은 노력해야겠습니다
잘보고갑니다 !