[TDD, 클린 코드 with Java] 과정 소개, 학습 테스트 구현, 메서드 분리
by Hi.Claire🐣 TDD, 클린 코드 with Java 19기 (박재성, 넥스트스텝)
과정 소개, 학습 테스트 구현, 메서드 분리
학습 목표 및 커리큘럼
커리큘럼
- TDD, 리팩터링, 클린 코드
- 순수 자바로 객체지향 프로그래밍 (미션1-3)
- 아직 TDD, OOP, 클린 코드 등에 익숙하지 않기 때문에 DB없이 순수 자바로 진행
- 레거시 코드 리팩터링 (미션4)
- 우리의 최종 목표! 실제 업무에서 마주할 웹 프로젝트 레거시 코드 리팩터링
미션별 학습 목표
미션1. 초간단 자동차 경주 게임
- Github 기반으로 온라인 코드 리뷰하는 경험
- 오픈 소스의 코드 리뷰 방식
- JUnit 사용법을 익혀 단위 테스트하는 경험
- 자바 code convention을 지키면서 프로그래밍하는 경험
- 메소드를 분리하는 리팩터링 경험
미션2. 로또
- TDD 기반으로 프로그래밍하는 경험
- 메소드 분리 + 클래스를 분리하는 리팩터링 경험
- 객체 지향 설계 연습
- 점진적으로 리팩터링하는 경험
미션3. 사다리 타기
- 자바8의 스트림, 람다를 사용해 함수형 프로그래밍(FP)하는 경험
- In -> Out, Out -> In 방식으로 도메인 객체를 설계하는 경험
- 책임주도설계 기반으로 인터페이스 활용해 프로그래밍하는 연습(OOP)
- 책임을 찾으면 그걸 인터페이스의 메소드로 사용하기
미션4. LMS - 수강신청
- 레거시 코드를 리팩터링할 때 테스트 코드를 통해 보호하는 경험
- Q&A 서비스의 질문 삭제하기 기능의 레거시 코드를 리팩터링하는 경험
- DB 테이블보다 도메인 모델을 TDD 기반으로 먼저 개발해 보는 경험
- 레거시 코드를 점진적으로 리팩터링해보는 경험
- 현장과 유사한 기능 요구사항을 지금까지 학습한 TDD, OOP, 클린 코드 기반으로 개발해 보는 경험
과정을 슬기롭게 소화하는 방법
1. 변화를 위해서는 의지력보다는 환경(상황)이 중요하다.
2. 주변 환경을 정리해 꾸준히 연습할 시간 확보
3. 매일 1시간씩 미션 진행에 투자할 수 있는 환경 만들기
4. 매일 1, 2시간씩 미션 진행하기
- 한번에 모두 구현하기 보다 매일 일정한 시간을 투자하는 것이 중요하다.
- 일주일에 최소 4회 이상 코드 리뷰 요청을 보내 코드 리뷰 받기
5. 가진 것을 비우기
- 자신이 가진 것을 비울 때 가장 많은 것을 배울 수 있다.
6. 리뷰어 피드백이 상반되는 경우
- 상반되는 의견을 즐겨라.
- 리뷰어에게 계속 물어본다.
- "저번 리뷰어님의 의견은 이러이러해서 지금 리뷰어님의 의견과 상반되는데, 왜 이렇게 생각하시나요?"
7. 정답에 집착하지 마라.
- 정답은 없다. 한 가지 정답만을 찾지 말자.
- 상황이 바뀜에 따라 최선의 선택지가 있는 것이지 정답은 없다. 현재 상황에서 최선의 답을 찾으려고 끊임없이 노력한다.
- 지금보다 더 나은 설계, 더 나은 코드, 더 나은 해결책이 있지 않을까 끊임없이 생각하고 리팩터링 한다.
8. 학습의 가장 큰 적은?
- 조급함 -> 우리나라의 교육은 너무 경쟁적이다.
- 마음의 여유를 가지고 즐겨보자.
- 부끄러워하지 말고 질문하고, 리뷰어 피드백을 받아 개선한다.
책 추천
- 자바 문법에 익숙하지 않다. -> 이펙티브 자바
- 미션과 병행하면서 읽을 책 추천 -> 클린 코드, 오브젝트
얼마 전에 인프런에서 조영호님의 오브젝트 기초 강의를 샀는데 이 참에 바로 책까지 결제 완료해버렸다.
라이브 코딩 공부 방법
- 오늘 수업 때 라이브 코딩 보기
- 내일 혼자 코딩해보기
- (막히면) 영상 다시 보기
- 다시 혼자 코딩해보기
- (막히면) 영상 다시 보기
그냥 무작정 영상을 보면서 순차적으로 따라하는 것은 의미가 없다.
내가 TDD, 리팩토링에 집착하는 이유
클린 코드
처음에는 클린 코드를 적용하는 것이 생산성이 더 떨어진다.
하지만 멀리 보고 클린 코드를 추구해야 한다.
좋은 회사가 요구하는 개발자 역량
다른 사람이 읽기 좋은 클린 코드, 유지보수하기 좋은 코드를 구현하는 능력
유지보수하기 좋은 코드를 구현하려면 지속적인 리팩터링이 필요하다. (우리는 신이 아니기 때문에 한번에 구현할 수 없다.)
지속적인 리팩터링이 가능하려면 테스트가 지탱하고 있어야 한다.
TDD(Test Driven Development by Kent Beck)
TDD란 프로그래밍 의사결정과 피드백 사이의 간극을 의식하고 이를 제어하는 기술이다.
엄청 빠른 사이클로 구현한 뒤 피드백을 받을 수 있는 기술이다.
TDD는 테스트 기술이 아니라 분석 기술이자 설계 기술이다.
TDD = Test First Development + Refactoring
TDD Cycle
TDD는 한 번에 한 가지에만 집중할 수 있게 한다.
즉, 구현과 설계를 철저히 분리한다.
- 구현에 집중 (1, 2단계)
- 실패하는 테스트 코드를 만든다. 1단계에서는 컴파일 에러에 익숙해져야 한다.
- 실패한 테스트를 성공시키기 위해 프러덕션 코드를 구현한다. TDD에서는 원칙적으로 프러덕션 코드부터 구현할 수 없다. 테스트를 통과하기 위해 어떠한 악행을 저질러도 된다. 쓰레기 코드는 나중에 리팩터링한다.
- 실패하는 테스트 코드 -> 테스트 성공 후에는 실패하는 테스트 코드를 계속 추가하며 반복한다.
- 설계에 집중 (3단계)
- 리팩터링
- 메서드, 클래스 설계
- 클린 코드 구현
- 리팩터링
의식적인 연습과 학습 테스트
https://edu.nextstep.camp/s/KIXpLZ6i/ls/sz0ELLV2
NEXTSTEP
edu.nextstep.camp
의식적인 연습의 7가지 원칙
- 효과적인 훈련 기법이 수립되어 있는 기술 연마
- 개인의 컴포트 존을 벗어난 지점에서 진행, 자신의 현재 능력을 살짝 넘어가는 작업을 지속적으로 시도
- 명확하고 구체적인 목표를 가지고 진행
- 신중하고 계획적이다. 즉, 개인이 온전히 집중하고 '의식적'으로 행동할 것을 요구
- 성장하는 단계에서는 고통스러운 게 당연하다. 성장한 이후에 결과물을 볼 때 행복이 찾아온다. 고통을 즐기자. 고통이 길면 길수록 그 뒤에 찾아오는 행복감이 더 크다.
- 우리는 1:1 피드백을 통해 의식적인 행동이 가능하다.
- 피드백과 피드백에 따른 행동 변경을 수반
- 효과적인 심적 표상을 만들어내는 한편으로 심적 표상에 의존
- 기존에 습득한 기술의 특정 부분을 집중적으로 개선함으로써 발전시키고 수정하는 과정을 수반
- 같은 미션을 반복하면서 이전에 내가 부족했던 부분을 집중적으로 연습하고, 다음 단계로 가도록 만드는 게 중요하다.
객체지향 생활 체조 원칙
https://edu.nextstep.camp/s/KIXpLZ6i/ls/TOMG8TDx
NEXTSTEP
edu.nextstep.camp
주니어 때는 이런 세부 지침들을 따르며 연습하는 것이 좋다.
- 규칙 1: 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
- 규칙 2: else 예약어를 쓰지 않는다.
- 규칙 3: 모든 원시값과 문자열을 포장한다.
- 규칙 4: 한 줄에 점을 하나만 찍는다.
- 규칙 5: 줄여쓰지 않는다(축약 금지).
- 규칙 6: 모든 엔티티를 작게 유지한다.
- 규칙 7: 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
- 규칙 8: 일급 콜렉션을 쓴다.
- 규칙 9: 게터/세터/프로퍼티를 쓰지 않는다.
미션1. 2단계 : 문자열 덧셈 계산기를 통한 TDD/리팩토링 실습 라이브 코딩
미션1. 자동차 경주 - 단위테스트
https://edu.nextstep.camp/s/KIXpLZ6i/lt/kAzHiUHH
NEXTSTEP
edu.nextstep.camp
2단계 기능 요구사항 & 프로그래밍 요구사항
https://edu.nextstep.camp/s/KIXpLZ6i/ls/s9PIIgE6
NEXTSTEP
edu.nextstep.camp
이번 2단계는 아주 간단하고 쉽지만 앞으로 3단계에서부터는 TDD로 구현하려고 하면 처음 시작이 너무 막막할 거다.
그 이유는 첫째, 요구사항 분석을 꼼꼼히 하지 않아서이고, 둘째, 최소한의 설계도 없이 시작해서이다.
1. 의식적인 연습
- 기능 목록 만들기
- 기능 요구사항을 작은 단위로 분리하여 기능 목록을 만든 후 제일 만만한 것부터 TDD를 시작해본다. 빠진 것이 있다면 추가한다. (기능 목록 요소 하나가 테스트케이스 하나가 된다.)
- 단위 테스트 구현하기
- 한 번도 단위 테스트 코드를 만들어보지 않은 사람은 먼저 프로덕션 코드 구현 -> 테스트 코드 구현 순서로 가도 괜찮다.
- 익숙해진 후에 TDD 사이클에 따라 실패하는 테스트 코드 만들기 -> 테스트 성공의 순서로 간다.
- 아래 예시처럼 TODO와 DONE 목록을 구분해서 관리해도 좋다. 실패하는 테스트 -> 테스트 성공하면 해당 task를 TODO 영역에서 DONE 영역으로 이동시킨다.
- 테스트 실행 : Control + Shift + R
- 이전에 실행한 테스트 실행 : Control + R
- 리팩터링
- 클래스, 메서드, 파라미터 이름 변경 : Rename(Shift + F6)
- 메서드 분리 : Extract Method(Option + Command + M)
- 메서드 하나는 한 가지의 일을 해야 한다. 그래야 메서드의 재사용이 가능하다. 메서드의 재사용성이 떨어진다는 것은 메서드가 두 가지 이상의 일을 하고 있다는 뜻이다.
- 하지만 주니어 때는 이런 생각으로 메서드를 분리하는 것이 어렵다.
- 일단은 indent가 2 이상이면 indent의 depth를 줄이는 것으로 시작하자. 메서드를 분리하면 indent가 1 줄어든다.
- 추상화 레벨 맞추기
- else문 제거 : if문 내에서 return하기 = early return
- 매직넘버 제거 : 정적 상수 추출, Introduce Constant(Option + Command + C)
- 재사용하지 않는 로컬변수 : Inline Method(Option + Command + N)
- static import
- 클래스 분리 (다음에 연습)
- Java8의 스트림, 람다 사용 (다음에 연습)
- 학습 테스트 구현하기
- 하다가 궁금증이 생기면 study 패키지를 만들어서 학습 테스트 구현해보기
(예시) ToDo.md
## TODO
---
- 빈 문자열 또는 null 값을 입력할 경우 0을 반환해야 한다.(예 : “” => 0, null => 0)
- 숫자 두개를 컴마(,) 구분자로 입력할 경우 두 숫자의 합을 반환한다.(예 : “1,2”)
- 구분자를 컴마(,) 이외에 콜론(:)을 사용할 수 있다. (예 : “1,2:3” => 6)
- “//”와 “\n” 문자 사이에 커스텀 구분자를 지정할 수 있다. (예 : “//;\n1;2;3” => 6)
- 음수를 전달할 경우 RuntimeException 예외가 발생해야 한다. (예 : “-1,2,3”)
- 구글에서 “junit4 expected exception”으로 검색해 해결책을 찾는다.
## DONE
---
- 숫자 하나를 문자열로 입력할 경우 해당 숫자를 반환한다.(예 : “1”)
Q&A
- 실패 테스트 작성 후에 커밋 찍고 통과 시키는 작업으로 가나요? 아님 통과 코드까지 커밋 찍고 이후 리팩토링 하나요?
- TDD Cycle 하나를 커밋의 단위로 잡는다. 즉, 기능 목록 요소 하나에 대해서 실패하는 테스트 코드 만들기 -> 테스트 성공 -> 리팩터링까지 끝낸 후 커밋하는 것을 추천한다.
- 그렇게 하실 경우 커밋 메시지를 feature로 구분하시는지 궁금합니다
- feat : 새로운 기능(테스트케이스) 추가
- fix : 기능 개발하다가 버그 발견
- refactor : 약간의 rename, extract method 등 리팩터링이 들어감
- docs
2. 학습 테스트 구현
"1"을 ","로 split할 수 있나요?
궁금하면 테스트 코드를 구현한다.
이때 study 패키지를 따로 만들어서 학습 테스트를 구현하는 것이 정말 좋은 팁인 것 같다.
StudyTest.java
public class StudyTest {
private static final String DELIMITER = ",|:";
@Test
void split_숫자_하나() {
String[] result = "1".split(DELIMITER);
assertThat(result).contains("1");
assertThat(result).hasSize(1);
}
}
3. 메서드 분리
3-1. 메서드는 한 가지 일을 해야 한다.
(예시) toInts()와 sum() 메서드 분리
메서드 분리 전 : sum() 메서드는 한 가지 이상의 일을 하고 있다.
- 문자열을 int로 바꾼다.
- 합을 계산해서 리턴한다.
private static int sum(String[] strings) {
int result = 0;
for (String s : strings) {
result += Integer.parseInt(s);
}
return result;
}
메서드 분리 후 : toInts() 메서드와 sum() 메서드는 각각 한 가지의 일만 한다.
- toInts() : 문자열 배열을 int형 배열로 바꾼다.
- sum : 인수로 들어온 int형 배열 요소의 합을 리턴한다.
private static int[] toInts(String[] strings) {
int[] numbers = new int[strings.length];
for (int i = 0; i < strings.length; i++) {
numbers[i] = Integer.parseInt(strings[i]);
}
return numbers;
}
private static int sum(int[] numbers) {
int result = 0;
for (int number : numbers) {
result += number;
}
return result;
}
3-2. 추상화 레벨 맞추기
컴포즈 메서드 패턴 : 메서드의 각각의 영역에 대한 추상화 레벨이 같으면 가독성이 높다.
(예시1) isNullOrEmpty(), splitStringsByDelimiter() 메서드 분리
리팩터링 전
sum()과 toInts()는 각각의 메서드를 통해 추상화를 한 번 했으므로 추상화 레벨이 1이다.
text == null || text.isEmpty() 부분과 text.split(DELIMITER)는 추상화 레벨이 0이다.
public static int splitAndSum(String text) throws IllegalArgumentException {
if (text == null || text.isEmpty()) {
return 0;
}
return sum(toInts(text.split(DELIMITER)));
}
리팩터링 후
isNullOrEmpty(), splitStringsByDelimiter() 메서드를 분리하여 추상화 레벨을 1로 맞춘다.
public static int splitAndSum(String text) throws IllegalArgumentException {
if (isNullOrEmpty(text)) {
return 0;
}
return sum(toInts(splitStringsByDelimiter(text)));
}
private static String[] splitStringsByDelimiter(String text) {
return text.split(DELIMITER);
}
private static boolean isNullOrEmpty(String text) {
return text == null || text.isEmpty();
}
이어서 실패하는 테스트 코드 추가 -> 테스트 성공 -> 리팩터링의 사이클을 반복한다.
이제 처음 splitAndSum() 메서드를 본 사람도 메서드의 로직을 파악하기 수월해졌다.
(처음에는 로직 파악이 우선이지, 세부 구현은 궁금하지 않다.)
만약에 다음 기능 요구사항인 커스텀 구분자를 추가하게 된다고 가정하자.
이 요건은 텍스트를 split하는 규칙이 바뀌는 것이므로 다른 곳은 볼 필요도 없이 splitStringsByDelimiter() 메서드에 들어가서 코드를 추가하거나 수정하면 된다.
(예시2) throwIfNegative() 메서드 분리
"음수를 전달할 경우 RuntimeException 예외가 발생해야 한다."는 요구사항을 봤을 때에도 어느 메서드에 해당 기능을 추가해야할지 파악하기 쉽다.
문자열을 숫자로 바꾸는 toInts() 메서드에 들어가서 수정한다.
리팩터링 전
for문 안에 if문이 있어 indent가 2이다.
private static int[] toInts(String[] strings) {
int[] numbers = new int[strings.length];
for (int i = 0; i < strings.length; i++) {
int number = Integer.parseInt(strings[i]);
if (number < 0) {
throw new IllegalArgumentException("음수는 허용하지 않습니다.");
}
numbers[i] = number;
}
return numbers;
}
리팩터링 후
throwIfNegative() 메서드를 분리하여 indent를 1 줄인다.
private static int[] toInts(String[] strings) {
int[] numbers = new int[strings.length];
for (int i = 0; i < strings.length; i++) {
int number = Integer.parseInt(strings[i]);
throwIfNegative(number);
numbers[i] = number;
}
return numbers;
}
private static void throwIfNegative(int number) {
if (number < 0) {
throw new IllegalArgumentException("음수는 허용하지 않습니다.");
}
}
Q&A
- 리뷰 횟수가 정해져 있나요?
- 없다. 이 강의의 핵심은 미션을 수행하고 코드 리뷰를 받는 것이므로 리뷰어를 쓰러뜨리겠다는 각오로 코드 리뷰 신청을 최대한 많이 하자!
- private 메서드도 테스트해야 하나요?
- 아니다. 그러면 테스트가 중복될 수 있다. private 메서드는 public 메서드를 통해서만 테스트한다.
- 뭔가 저는 final 키워드로 제약을 걸어서 혹시나 하는 실수를 방지하도록 하고 (파라미터 같은 변수들) 기능상 재할당이 필요한 부분만 final 키워드를 지우는 식으로 하는데 재성님은 어떻게 생각하시는지! 또 final 키워드를 어떻게 사용하시는지 궁금합니다.
- 좋은 습관이다! Java는 오래된 언어이다 보니 직접 해줘야 한다. 최근 언어 트렌드를 파악하기 위해 Kotlin 같은 언어를 공부해보는 것도 추천한다.
- 현업에서는 재사용성이 높은 메서드는 해당 클래스가 아니라 lib같은 다른 곳에 위치해야 할 것 같은데 이러다보면 필요한 메서드를 찾는데에도 시간이 걸리지 않나요?
- 유틸성 메서드는 중복 구현하는 것보다 분리하는 것이 유지보수 측면에서 좋다. 특히 인텔리제이는 검색 성능도 좋으니까.. (이클립스 쓰는 나만 슬프지🥹)
- 같은 함수내에서 추상화 레벨 맞추는 부분이 상당히 인상적이었는데요. 혹시 추상화 레벨에 대한 명확한 정의가 있을까요? 이 개념을 모르는 사람한테 설명하는 걸 상상하니까 제가 개념 정의가 명확치 않은 거 같아서요.
- 추상화 레벨
- 메서드의 책임의 관점을 어디까지 보아야할지 모르겠습니다.
현재 toInt메서드의 경우 음수의 예외 처리와 형변환 이렇게 2가지 책임을 갖는다고 봐야하는지, 큰 관점에서 형변환 하나의 역할만을 한다고 봐야하는지 궁금합니다.
위와 같이 메서드 책임의 범위를 어디까지 봐야 할까요?
- 계속 고민해봐야 한다. 계속 고민한 후에 피드백 강의를 듣다가 깨달음을 얻는 순간 짜릿하다.
2단계 라이브코딩 듣고 수정 · coongya/java-racingcar@3a43afb
coongya committed Oct 3, 2024
github.com
'☕️ Java > TDD, 클린 코드 with Java' 카테고리의 다른 글
[TDD, 클린 코드 with Java] 자동차 경주 미션 피드백 : 테스트 코드 작성, 전략패턴, 일급컬렉션 (0) | 2024.10.06 |
---|---|
[TDD, 클린 코드 with Java] 자동차 경주 미션 피드백 : 예외 처리, 매직넘버 치환 (0) | 2024.10.05 |
[TDD, 클린 코드 with Java] 미션과 상관없는 이야기 (2) | 2024.10.01 |
[TDD, 클린 코드 with Java] 개발 환경 세팅, AssertJ, JUnit5 가이드 (0) | 2024.09.26 |
[TDD, 클린 코드 with Java] 시작하며 (2) | 2024.09.25 |
블로그의 정보
Claire's Study Note
Hi.Claire