의식적인 연습으로
TDD, 리팩토링 연습하기
박재성(자바지기)
넥스트스텝(NextStep) 이름으로 온/오프라인 코드리뷰 중심 교육
SLiPP(https://www.slipp.net) 커뮤니티 운영
이 발표에서는
TDD와 리팩토링을 왜 해야 하는지 알고 있다.
는 가정 하에 진행한다.
이 발표에서는
TDD와 리팩토링을 비슷한 비중으로 다룬다.
어쩌면 TDD < 리팩토링 일지도 모른다.
개발 현장을 떠나 교육자로 산지 6년.
이 발표의 상당 수가 구라일 수 있으니 주의해야 한다.
발표 내용
• 의식적인 연습이란?
• 의식적인 연습으로 TDD, 리팩토링 연습 과정
의식적인 연습이란?
TDD, 리팩토링을 잘 하려면…
연습, 연습, 연습 …
무조건 연습을 많이 한다고 잘할 수 있을까?
TDD, 리팩토링과 관련해 5, 6년을 도전한 후에야
테스트하기 쉬운 코드와 테스트하기 어려운 코드를 보는 눈
테스트하기 어려운 코드를 테스트하기 쉬운 코드로 설계하는 감
(sense)
TDD, 리팩토링. 멋져 보인다.
하지만 생각만큼 쉽게 연습할 수 있는 녀석이 아니다.
교육자로서 최근의 고민
좀 더 효과적으로 연습할 수 있는 방법은 없을까?
아마추어와 프로의 결정적 차이
목적 의식 있는 연습
에 얼마나 많은 시간을 투자했느냐?
의식적인 연습의 7가지 원칙
• 첫째, 효과적인 훈련 기법이 수립되어 있는 기술 연마
• 둘째, 개인의 컴포트 존을 벗어난 지점에서 진행, 자신의 현재 능력
을
살짝 넘어가는 작업을 지속적으로 시도
• 셋째, 명확하고 구체적인 목표를 가지고 진행
의식적인 연습의 7가지 원칙
• 넷째, 신중하고 계획적이다. 즉, 개인이 온전히 집중하고 '의식적'으로
행동할 것을 요구
• 다섯째, 피드백과 피드백에 따른 행동 변경을 수반
• 여섯째, 효과적인 심적 표상을 만들어내는 한편으로 심적 표상에 의존
• 일곱째, 기존에 습득한 기술의 특정 부분을 집중적으로 개선함으로
써
발전시키고, 수정하는 과정을 수반
의식적인 연습으로 효과적으로 연습하자.
의식적인 연습으로
TDD, 리팩토링 연습하는 과정
1단계 - 단위 테스트 연습
내가 사용하는 API 사용법을 익히기 위한 학습 테스트에서 시
작
• 자바 String 클래스의 다양한 메소드(함수) 사용법
• 자바 ArrayList에 데이터를 추가, 수정, 삭제하는 방법
import static org.assertj.core.api.Assertions.assertThat;
public class StringTest {
@Test
public void split() {
String[] values = "1".split(",");
assertThat(values).contains("1");
values = "1,2".saplit(",");
assertThat(values).containsExactly("1", "2");
}
@Test
public void substring() {
String input = "(1,2)";
String result = input.substring(1, input.length() - 1);
assertThat(result).isEqualTo("1,2");
}
}
import static org.assertj.core.api.Assertions.assertThat;
public class CollectionTest {
@Test
public void arrayList() {
ArrayList<String> values = new ArrayList<>();
values.add("first");
values.add("second");
assertThat(values.add("third")).isTrue();
assertThat(values.size()).isEqualTo(3);
assertThat(values.get(0)).isEqualTo("first");
assertThat(values.contains("first")).isTrue();
assertThat(values.remove(0)).isEqualTo("first");
assertThat(values.size()).isEqualTo(2);
}
}
연습 효과
• 단위테스트 방법을 학습할 수 있다.
• 단위테스트 도구(xUnit)의 사용법을 익힐 수 있다.
• 사용하는 API에 대한 학습 효과가 있다.
내가 구현하는 메소드(함수) 중
Input과 Output이 명확한 클래스 메소드(보통 Util 성격의 메소
드)
에 대한 단위 테스트 연습
알고리즘을 학습한다면 알고리즘 구현에 대한 검증을 단위 테스트로 한다.
알고리즘은 Input, Output이 명확하기 때문에 연습하기 좋다.
2단계 - TDD 연습
지켜야 할 원칙 1
회사 프로젝트에 연습하지 말고 장난감 프로젝트를 활용해 연습하자.
지켜야 할 원칙 2
웹, 모바일 UI나 DB에 의존관계를 가지지 않는 요구사항으로 연습한다.
문자열 덧셈 계산기 요구사항
쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우
구분자를 기준으로 분리한 각 숫자의 합을 반환
문자열 덧셈 계산기 요구사항
쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우
구분자를 기준으로 분리한 각 숫자의 합을 반환
입력(input) 출력(output)
null 또는 “” 0
“1” 1
“1,2” 3
“1,2:3” 6
이미지 출처: https://mynetdev.wordpress.com/2016/01/05/tdd-to-bdd/
public class StringCalculatorTest {
@Test
public void null_또는_빈값() {
assertThat(StringCalculator.splitAndSum(null)).isEqualTo(0);
assertThat(StringCalculator.splitAndSum("")).isEqualTo(0);
}
@Test
public void 값_하나() {
assertThat(StringCalculator.splitAndSum("1")).isEqualTo(1);
}
@Test
public void 쉼표_구분자() {
assertThat(StringCalculator.splitAndSum("1,2")).isEqualTo(3);
}
@Test
public void 쉼표_콜론_구분자() {
assertThat(StringCalculator.splitAndSum("1,2:3")).isEqualTo(6);
}
}
public class StringCalculator {
public static int splitAndSum(String text) {
int result = 0;
if (text == null || text.isEmpty()) {
result = 0;
} else {
String[] values = text.split(",|:");
for (String value : values) {
result += Integer.parseInt(value);
}
}
return result;
}
}
어려운 문제를 해결하는 것이 목적이 아니라 TDD 연습이 목적
난이도가 낮거나 자신에게 익숙한 문제로 시작하는 것을 추천
3단계 - 리팩토링 연습
리팩토링 연습 – 메소드 분리
public class StringCalculator {
public static int splitAndSum(String text) {
int result = 0;
if (text == null || text.isEmpty()) {
result = 0;
} else {
String[] values = text.split(",|:");
for (String value : values) {
result += Integer.parseInt(value);
}
}
return result;
}
}
테스트 코드는 변경하지 말고
테스트 대상 코드(프로덕션 코드)를 개선하는 연습을 한다.
막막함
멘붕
막막함
멘붕
의식적인 연습 7가지 원칙
셋째, 명확하고 구체적인 목표를 가지고 진행
다섯째, 피드백과 피드백에 따른 행동 변경을 수반
막막함
멘붕
정성적인 기준보다는 정량적이고 측정 가능한 방법으로 연습
public class StringCalculator {
public static int splitAndSum(String text) {
int result = 0;
if (text == null || text.isEmpty()) {
result = 0;
} else {
String[] values = text.split(",|:");
for (String value : values) {
result += Integer.parseInt(value);
}
}
return result;
}
}
한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
public class StringCalculator {
public static int splitAndSum(String text) {
int result = 0;
if (text == null || text.isEmpty()) {
result = 0;
} else {
String[] values = text.split(",|:");
for (String value : values) {
result += Integer.parseInt(value);
}
}
return result;
}
}
한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
들여쓰기가 2인 곳
public class StringCalculator {
public static int splitAndSum(String text) {
int result = 0;
if (text == null || text.isEmpty()) {
result = 0;
} else {
String[] values = text.split(",|:");
result = sum(values);
}
return result;
}
private static int sum(String[] values) {
int result = 0;
for (String value : values) {
result += Integer.parseInt(value);
}
return result;
}
}
public class StringCalculator {
public static int splitAndSum(String text) {
int result = 0;
if (text == null || text.isEmpty()) {
result = 0;
} else {
String[] values = text.split(",|:");
result = sum(values);
}
return result;
}
private static int sum(String[] values) {
int result = 0;
for (String value : values) {
result += Integer.parseInt(value);
}
return result;
}
}
else 예약어를 쓰지 않는다.
public class StringCalculator {
public static int splitAndSum(String text) {
if (text == null || text.isEmpty()) {
return 0;
}
String[] values = text.split(",|:");
return sum(values);
}
private static int sum(String[] values) {
int result = 0;
for (String value : values) {
result += Integer.parseInt(value);
}
return result;
}
}
else 예약어를 쓰지 않는다.
public class StringCalculator {
public static int splitAndSum(String text) {
if (text == null || text.isEmpty()) {
return 0;
}
String[] values = text.split(",|:");
return sum(values);
}
private static int sum(String[] values) {
int result = 0;
for (String value : values) {
result += Integer.parseInt(value);
}
return result;
}
}
메소드가 한 가지 일만 하도록 구현하기
public class StringCalculator {
[…]
private static int[] toInts(String[] values) {
int[] numbers = new int[values.length];
for (int i = 0; i < values.length; i++) {
numbers[i] = Integer.parseInt(values[i]);
}
return numbers;
}
private static int sum(int[] numbers) {
int result = 0;
for (int number : numbers) {
result += number;
}
return result;
}
}
메소드가 한 가지 일만 하도록 구현하기
public class StringCalculator {
public static int splitAndSum(String text) {
if (text == null || text.isEmpty()) {
return 0;
}
String[] values = text.split(",|:");
int[] numbers = toInts(values);
return sum(numbers);
}
private static int[] toInts(String[] values) {
[…]
}
private static int sum(int[] numbers) {
[…]
}
}
메소드가 한 가지 일만 하도록 구현하기
public class StringCalculator {
public static int splitAndSum(String text) {
if (text == null || text.isEmpty()) {
return 0;
}
String[] values = text.split(",|:");
int[] numbers = toInts(values);
return sum(numbers);
}
private static int[] toInts(String[] values) {
[…]
}
private static int sum(int[] numbers) {
[…]
}
}
로컬 변수가 정말 필요한가?
public class StringCalculator {
public static int splitAndSum(String text) {
if (text == null || text.isEmpty()) {
return 0;
}
return sum(toInts(text.split(",|:")));
}
private static int[] toInts(String[] values) {
[…]
}
private static int sum(int[] numbers) {
[…]
}
}
로컬 변수가 정말 필요한가?
compose method 패턴 적용
메소드(함수)의 의도가 잘 드러나도록 동등한 수준의 작업을 하는 여러
단계로 나눈다.
public class StringCalculator {
public static int add(String text) {
if (isBlank(text)) {
return 0;
}
return sum(toInts(split(text)));
}
private static boolean isBlank(String text) {
}
private static String[] split(String text) {
}
private static int[] toInts(String[] values) {
}
private static int sum(int[] numbers) {
}
}
compose method 패턴 적용
public class StringCalculator {
public static int add(String text) {
int result = 0;
if (text == null || text.isEmpty()) {
result = 0;
} else {
String[] values = text.split(",|:");
for (String value : values) {
result += Integer.parseInt(value);
}
}
return result;
}
}
public class StringCalculator {
public static int add(String text) {
if (isBlank(text)) {
return 0;
}
return sum(toInts(split(text)));
}
private static boolean isBlank(String text) {
}
private static String[] split(String text) {
}
private static int[] toInts(String[] values) {
}
private static int sum(int[] numbers) {
}
}
add() 메소드를 처음 읽는 사람에게 어느 코드가 더 읽기 좋을
까?
한 번에 모든 원칙을 지키면서 리팩토링하려고 연습하지 마라.
한 번에 한 가지 명확하고 구체적인 목표를 가지고 연습하라.
연습은 극단적인 방법으로 연습하는 것도 좋다.
예를 들어 한 메소드의 라인 수 제한을 15라인 -> 10라인으로 줄여가면
서 연습하는 것도 좋은 방법이다.
리팩토링 연습 – 클래스 분리
다시 문자열 덧셈 계산기 요구사항
쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리
한
각 숫자의 합을 반환
문자열 계산기에 숫자 이외의 값 또는 음수를 전달하는 경우
RuntimeException 예외를 throw한다.
입력(input) 출력(output)
null 또는 “” 0
“1” 1
“1,2” 3
“1,2:3” 6
“-1,2:3” RuntimeException
public class StringCalculatorTest {
[…]
@Test
public void 쉼표_구분자() {
assertThat(StringCalculator.splitAndSum("1,2")).isEqualTo(3);
}
@Test
public void 쉼표_콜론_구분자() {
assertThat(StringCalculator.splitAndSum("1,2:3")).isEqualTo(6);
}
@Test(expected = RuntimeException.class)
public void 음수값() {
StringCalculator.splitAndSum("-1,2:3");
}
}
public class StringCalculator {
public static int splitAndSum(String text) {
[…]
}
private static int[] toInts(String[] values) {
int[] numbers = new int[values.length];
for (int i = 0; i < values.length; i++) {
numbers[i] = toInt(values[i]);
}
return numbers;
}
private static int toInt(String value) {
int number = Integer.parseInt(value);
if (number < 0) {
throw new RuntimeException();
}
return number;
}
}
public class StringCalculator {
[…]
private static int[] toInts(String[] values) {
int[] numbers = new int[values.length];
for (int i = 0; i < values.length; i++) {
numbers[i] = toInt(values[i]);
}
return numbers;
}
private static int toInt(String value) {
int number = Integer.parseInt(value);
if (number < 0) {
throw new RuntimeException();
}
return number;
}
}
모든 원시값과 문자열을 포장한다.
public class Positive {
private int number;
public Positive(String value) {
int number = Integer.parseInt(value);
if (number < 0) {
throw new RuntimeException();
}
this.number = number;
}
}
모든 원시값과 문자열을 포장한다.
public class Positive {
private int number;
public Positive(String value) {
this(Integer.parseInt(value));
}
public Positive(int number) {
if (number < 0) {
throw new RuntimeException();
}
this.number = number;
}
}
모든 원시값과 문자열을 포장한다.
public class StringCalculator {
이전 코드와 같음
private static Positive[] toInts(String[] values) {
Positive[] numbers = new Positive[values.length];
for (int i = 0; i < values.length; i++) {
numbers[i] = new Positive(values[i]);
}
return numbers;
}
private static int sum(Positive[] numbers) {
Positive result = new Positive(0);
for (Positive number : numbers) {
result = result.add(number);
}
return result.getNumber();
}
}
모든 원시값과 문자열을 포장한다.
public class Positive {
private int number;
[…]
public Positive add(Positive other) {
return new Positive(this.number + other.number);
}
public int getNumber() {
return number;
}
}
모든 원시값과 문자열을 포장한다.
클래스 분리 연습을 위해 활용할 수 있는 원칙
• 일급 콜렉션을 쓴다.
• 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
public class Lotto {
private static final int LOTTO_SIZE = 6;
private final Set<LottoNumber> lotto;
private Lotto(Set<LottoNumber> lotto) {
if (lotto.size() != LOTTO_SIZE) {
throw new IllegalArgumentException();
}
this.lotto = lotto;
}
}
일급 콜렉션을 쓴다.
public class WinningLotto {
private final Lotto lotto;
private final LottoNumber no;
public WinningLotto(Lotto lotto, LottoNumber no) {
if (lotto.contains(no)) {
throw new IllegalArgumentException();
}
this.lotto = lotto;
this.no = no;
}
public Rank match(Lotto userLotto) {
int matchCount = lotto.match(userLotto);
boolean matchBonus = userLotto.contains(no);
return Rank.valueOf(matchCount, matchBonus);
}
}
3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
4단계 - 장난감 프로젝트
난이도 높이기
점진적으로 요구사항이 복잡한 프로그램을 구현한다.
앞에서 지켰던 기준을 지키면서 프로그래밍 연습을 한다.
TDD, 리팩토링 연습하기 좋은 프로그램 요구사항
• 게임과 같이 요구사항이 명확한 프로그램으로 연습
• 의존관계(모바일 UI, 웹 UI, 데이터베이스, 외부 API와 같은 의존관계)
가 없이 연습
• 약간은 복잡한 로직이 있는 프로그램
연습하기 좋은 예
• 로또(단, UI는 콘솔)
• 사다리 타기(단, UI는 콘솔)
• 볼링 게임 점수판(단, UI는 콘솔)
• 체스 게임(단, UI는 콘솔)
• 지뢰 찾기 게임(단, UI는 콘솔)
5단계 - 의존관계 추가를 통한
난이도 높이기
웹, 모바일 UI, 데이터베이스와 같은 의존관계를 추가
이때 필요한 역량은
테스트하기 쉬운 코드와 테스트하기 어려운 코드를 보는 눈
테스트하기 어려운 코드를 테스트하기 쉬운 코드로 설계하는 감
(sense)
앞 단계 연습을 잘 소화했다면 테스트하기 쉬운 코드와 어려운
코드를 분리하는 역량이 쌓였을 것이다.
한 단계 더 나아간 연습하기
한 단계 더 나아간 연습을 하고 싶다면
• 컴파일 에러를 최소화하면서 리팩토링하기
• ATDD 기반으로 응용 애플리케이션 개발하기
• 레거시 애플리케이션에 테스트 코드 추가해 리팩토링하기
구체적인 연습 목표 찾기
객체지향 생활체조 규칙
• 규칙 1: 한 메서드에 오직 한 단계의 들여쓰기만 한다.
• 규칙 2: else 예약어를 쓰지 않는다.
• 규칙 3: 모든 원시값과 문자열을 포장한다.
• 규칙 4: 한 줄에 점을 하나만 찍는다.
• 규칙 5: 줄여쓰지 않는다(축약 금지).
• 규칙 6: 모든 엔티티를 작게 유지한다.
• 규칙 7: 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는
다.
• 규칙 8: 일급 콜렉션을 쓴다.
• 규칙 9: 게터/세터/프로퍼티를 쓰지 않는다.
메소드 인수 개수
메소드(함수)에서 이상적인 인자 개수는 0개(무항)이다. 다음
은 1개이고, 다음은 2개이다.
3개는 가능한 피하는 편이 좋다.
4개 이상은 특별한 이유가 있어도 사용하면 안된다.
클래스
클래스를 만들 때 첫 번째 규칙은 크기다.
클래스는 작아야 한다.
두 번째 규칙도 크기다.
더 작아야 한다.
마치며
TDD, 리팩토링 연습을 위해 필요한 것은?
• 조급함 대신 마음의 여유
• 나만의 장난감 프로젝트
• 같은 과제를 반복적으로 구현할 수 있는 인내력
가장 필요한 것은 가보지 않는 길에 꾸준히 도전할 수 있는 용기
부록 - 예제를 통해
테스트하기 쉬운 코드와
어려운 코드 분리
Q&A 게시판에서 질문 삭제 요구사항
• 질문 데이터를 완전히 삭제하는 것이 아니라 데이터의 상태를 삭제 상태
(deleted 상태를 false -> true)로 변경한다.
• 로그인 사용자와 질문한 사람이 같은 경우 삭제 가능하다.
• 답변이 없는 경우 삭제가 가능하다.
• 질문자와 답변 글의 모든 답변자 같은 경우 삭제가 가능하다.
• 질문을 삭제할 때 답변 또한 삭제해야 하며, 답변의 삭제 또한 삭제 상태
(deleted)를 변경한다.
• 질문자와 답변자가 다른 경우 답변을 삭제할 수 없다.
Q&A 게시판에서 질문 삭제 요구사항
• 질문 데이터를 완전히 삭제하는 것이 아니라 데이터의 상태를 삭제 상태
(deleted 상태를 false -> true)로 변경한다.
• 로그인 사용자와 질문한 사람이 같은 경우 삭제 가능하다.
• 답변이 없는 경우 삭제가 가능하다.
• 질문자와 답변 글의 모든 답변자 같은 경우 삭제가 가능하다.
• 질문을 삭제할 때 답변 또한 삭제해야 하며, 답변의 삭제 또한 삭제 상태
(deleted)를 변경한다.
• 질문자와 답변자가 다른 경우 답변을 삭제할 수 없다.
Q&A 게시판에서 질문 삭제 요구사항
질문 답변 삭제 여부
로그인 사용자 != 질문자 답변 유무와 무관 X
로그인 사용자 == 질문자 답변이 없음 O
로그인 사용자 == 질문자 로그인 사용자 == 모든 답변자 O
로그인 사용자 == 질문자 로그인 사용자 != 모든 답변자 X
public void deleteQuestion(User loginUser, long questionId) throws
CannotDeleteException {
Question question = questionRepository.findOne(questionId);
if (question == null) {
return;
}
if (!loginUser.equals(question.getWriter())) {
throw new CannotDeleteException("질문을 삭제할 수 없습니다.");
}
List<Answer> answers = question.getAnswers();
boolean canDelete = true;
for (Answer answer : answers) {
if (!loginUser.equals(answer.getWriter())) {
throw new CannotDeleteException("답변을 삭제할 수 없습니다.");
}
}
question.delete();
for (Answer answer : answers) {
answer.delete();
}
}
테스트 가능한 부분을 분리하고,
객체지향 설계 원칙에 따라 역할과 책임을 분리해 구현한다.
public void deleteQuestion(User loginUser, long questionId) throws
CannotDeleteException {
Question question = questionRepository.findOne(questionId);
if (question == null) {
return;
}
question.delete(loginUser);
}
public class Question {
public void delete(User loginUser) throws CannotDeleteException {
if (!isOwner(loginUser)) {
throw new CannotDeleteException("다른 사람의 글은 삭제할 수 없다.");
}
answers.delete(loginUser);
this.deleted = true;
}
}
public class Answers {
private List<Answer> answers = new ArrayList<>();
public void delete(User loginUser) throws CannotDeleteException {
for (Answer answer : answers) {
answer.delete(loginUser);
}
}
}
일급 콜렉션을 쓴다.
단위 테스트를 먼저 만드느냐, 뒤에 만드느냐는 중요하지 않다.
단위 테스트 하기 쉬운 구조로 설계하고, 단위 테스트를 추가하
는 것이 핵심이다.
단위 테스트를 먼저 만드느냐, 뒤에 만드느냐는 중요하지 않다.
단위 테스트 하기 쉬운 구조로 설계하고, 단위 테스트를 추가하
는 것이 핵심이다.
테스트하기 쉬운 코드와 테스트하기 어려운 코드를 보는 눈
테스트하기 어려운 코드를 테스트하기 쉬운 코드로 설계하는 감
(sense)

[OKKYCON] 박재성 - 의식적인 연습으로 TDD, 리팩토링 연습하기

  • 1.
    의식적인 연습으로 TDD, 리팩토링연습하기 박재성(자바지기)
  • 2.
  • 3.
  • 4.
    이 발표에서는 TDD와 리팩토링을왜 해야 하는지 알고 있다. 는 가정 하에 진행한다.
  • 5.
    이 발표에서는 TDD와 리팩토링을비슷한 비중으로 다룬다. 어쩌면 TDD < 리팩토링 일지도 모른다.
  • 6.
    개발 현장을 떠나교육자로 산지 6년. 이 발표의 상당 수가 구라일 수 있으니 주의해야 한다.
  • 7.
    발표 내용 • 의식적인연습이란? • 의식적인 연습으로 TDD, 리팩토링 연습 과정
  • 8.
  • 9.
    TDD, 리팩토링을 잘하려면… 연습, 연습, 연습 …
  • 10.
    무조건 연습을 많이한다고 잘할 수 있을까?
  • 11.
    TDD, 리팩토링과 관련해5, 6년을 도전한 후에야 테스트하기 쉬운 코드와 테스트하기 어려운 코드를 보는 눈 테스트하기 어려운 코드를 테스트하기 쉬운 코드로 설계하는 감 (sense)
  • 12.
    TDD, 리팩토링. 멋져보인다. 하지만 생각만큼 쉽게 연습할 수 있는 녀석이 아니다.
  • 13.
    교육자로서 최근의 고민 좀더 효과적으로 연습할 수 있는 방법은 없을까?
  • 14.
    아마추어와 프로의 결정적차이 목적 의식 있는 연습 에 얼마나 많은 시간을 투자했느냐?
  • 15.
    의식적인 연습의 7가지원칙 • 첫째, 효과적인 훈련 기법이 수립되어 있는 기술 연마 • 둘째, 개인의 컴포트 존을 벗어난 지점에서 진행, 자신의 현재 능력 을 살짝 넘어가는 작업을 지속적으로 시도 • 셋째, 명확하고 구체적인 목표를 가지고 진행
  • 16.
    의식적인 연습의 7가지원칙 • 넷째, 신중하고 계획적이다. 즉, 개인이 온전히 집중하고 '의식적'으로 행동할 것을 요구 • 다섯째, 피드백과 피드백에 따른 행동 변경을 수반 • 여섯째, 효과적인 심적 표상을 만들어내는 한편으로 심적 표상에 의존 • 일곱째, 기존에 습득한 기술의 특정 부분을 집중적으로 개선함으로 써 발전시키고, 수정하는 과정을 수반
  • 17.
  • 18.
  • 19.
    1단계 - 단위테스트 연습
  • 20.
    내가 사용하는 API사용법을 익히기 위한 학습 테스트에서 시 작 • 자바 String 클래스의 다양한 메소드(함수) 사용법 • 자바 ArrayList에 데이터를 추가, 수정, 삭제하는 방법
  • 21.
    import static org.assertj.core.api.Assertions.assertThat; publicclass StringTest { @Test public void split() { String[] values = "1".split(","); assertThat(values).contains("1"); values = "1,2".saplit(","); assertThat(values).containsExactly("1", "2"); } @Test public void substring() { String input = "(1,2)"; String result = input.substring(1, input.length() - 1); assertThat(result).isEqualTo("1,2"); } }
  • 22.
    import static org.assertj.core.api.Assertions.assertThat; publicclass CollectionTest { @Test public void arrayList() { ArrayList<String> values = new ArrayList<>(); values.add("first"); values.add("second"); assertThat(values.add("third")).isTrue(); assertThat(values.size()).isEqualTo(3); assertThat(values.get(0)).isEqualTo("first"); assertThat(values.contains("first")).isTrue(); assertThat(values.remove(0)).isEqualTo("first"); assertThat(values.size()).isEqualTo(2); } }
  • 23.
    연습 효과 • 단위테스트방법을 학습할 수 있다. • 단위테스트 도구(xUnit)의 사용법을 익힐 수 있다. • 사용하는 API에 대한 학습 효과가 있다.
  • 24.
    내가 구현하는 메소드(함수)중 Input과 Output이 명확한 클래스 메소드(보통 Util 성격의 메소 드) 에 대한 단위 테스트 연습
  • 25.
    알고리즘을 학습한다면 알고리즘구현에 대한 검증을 단위 테스트로 한다. 알고리즘은 Input, Output이 명확하기 때문에 연습하기 좋다.
  • 26.
  • 27.
    지켜야 할 원칙1 회사 프로젝트에 연습하지 말고 장난감 프로젝트를 활용해 연습하자.
  • 28.
    지켜야 할 원칙2 웹, 모바일 UI나 DB에 의존관계를 가지지 않는 요구사항으로 연습한다.
  • 29.
    문자열 덧셈 계산기요구사항 쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환
  • 30.
    문자열 덧셈 계산기요구사항 쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환 입력(input) 출력(output) null 또는 “” 0 “1” 1 “1,2” 3 “1,2:3” 6
  • 31.
  • 32.
    public class StringCalculatorTest{ @Test public void null_또는_빈값() { assertThat(StringCalculator.splitAndSum(null)).isEqualTo(0); assertThat(StringCalculator.splitAndSum("")).isEqualTo(0); } @Test public void 값_하나() { assertThat(StringCalculator.splitAndSum("1")).isEqualTo(1); } @Test public void 쉼표_구분자() { assertThat(StringCalculator.splitAndSum("1,2")).isEqualTo(3); } @Test public void 쉼표_콜론_구분자() { assertThat(StringCalculator.splitAndSum("1,2:3")).isEqualTo(6); } }
  • 33.
    public class StringCalculator{ public static int splitAndSum(String text) { int result = 0; if (text == null || text.isEmpty()) { result = 0; } else { String[] values = text.split(",|:"); for (String value : values) { result += Integer.parseInt(value); } } return result; } }
  • 34.
    어려운 문제를 해결하는것이 목적이 아니라 TDD 연습이 목적 난이도가 낮거나 자신에게 익숙한 문제로 시작하는 것을 추천
  • 35.
  • 36.
    리팩토링 연습 –메소드 분리
  • 37.
    public class StringCalculator{ public static int splitAndSum(String text) { int result = 0; if (text == null || text.isEmpty()) { result = 0; } else { String[] values = text.split(",|:"); for (String value : values) { result += Integer.parseInt(value); } } return result; } } 테스트 코드는 변경하지 말고 테스트 대상 코드(프로덕션 코드)를 개선하는 연습을 한다.
  • 38.
  • 39.
    막막함 멘붕 의식적인 연습 7가지원칙 셋째, 명확하고 구체적인 목표를 가지고 진행 다섯째, 피드백과 피드백에 따른 행동 변경을 수반
  • 40.
  • 41.
    public class StringCalculator{ public static int splitAndSum(String text) { int result = 0; if (text == null || text.isEmpty()) { result = 0; } else { String[] values = text.split(",|:"); for (String value : values) { result += Integer.parseInt(value); } } return result; } } 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다.
  • 42.
    public class StringCalculator{ public static int splitAndSum(String text) { int result = 0; if (text == null || text.isEmpty()) { result = 0; } else { String[] values = text.split(",|:"); for (String value : values) { result += Integer.parseInt(value); } } return result; } } 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다. 들여쓰기가 2인 곳
  • 43.
    public class StringCalculator{ public static int splitAndSum(String text) { int result = 0; if (text == null || text.isEmpty()) { result = 0; } else { String[] values = text.split(",|:"); result = sum(values); } return result; } private static int sum(String[] values) { int result = 0; for (String value : values) { result += Integer.parseInt(value); } return result; } }
  • 44.
    public class StringCalculator{ public static int splitAndSum(String text) { int result = 0; if (text == null || text.isEmpty()) { result = 0; } else { String[] values = text.split(",|:"); result = sum(values); } return result; } private static int sum(String[] values) { int result = 0; for (String value : values) { result += Integer.parseInt(value); } return result; } } else 예약어를 쓰지 않는다.
  • 45.
    public class StringCalculator{ public static int splitAndSum(String text) { if (text == null || text.isEmpty()) { return 0; } String[] values = text.split(",|:"); return sum(values); } private static int sum(String[] values) { int result = 0; for (String value : values) { result += Integer.parseInt(value); } return result; } } else 예약어를 쓰지 않는다.
  • 46.
    public class StringCalculator{ public static int splitAndSum(String text) { if (text == null || text.isEmpty()) { return 0; } String[] values = text.split(",|:"); return sum(values); } private static int sum(String[] values) { int result = 0; for (String value : values) { result += Integer.parseInt(value); } return result; } } 메소드가 한 가지 일만 하도록 구현하기
  • 47.
    public class StringCalculator{ […] private static int[] toInts(String[] values) { int[] numbers = new int[values.length]; for (int i = 0; i < values.length; i++) { numbers[i] = Integer.parseInt(values[i]); } return numbers; } private static int sum(int[] numbers) { int result = 0; for (int number : numbers) { result += number; } return result; } } 메소드가 한 가지 일만 하도록 구현하기
  • 48.
    public class StringCalculator{ public static int splitAndSum(String text) { if (text == null || text.isEmpty()) { return 0; } String[] values = text.split(",|:"); int[] numbers = toInts(values); return sum(numbers); } private static int[] toInts(String[] values) { […] } private static int sum(int[] numbers) { […] } } 메소드가 한 가지 일만 하도록 구현하기
  • 49.
    public class StringCalculator{ public static int splitAndSum(String text) { if (text == null || text.isEmpty()) { return 0; } String[] values = text.split(",|:"); int[] numbers = toInts(values); return sum(numbers); } private static int[] toInts(String[] values) { […] } private static int sum(int[] numbers) { […] } } 로컬 변수가 정말 필요한가?
  • 50.
    public class StringCalculator{ public static int splitAndSum(String text) { if (text == null || text.isEmpty()) { return 0; } return sum(toInts(text.split(",|:"))); } private static int[] toInts(String[] values) { […] } private static int sum(int[] numbers) { […] } } 로컬 변수가 정말 필요한가?
  • 51.
    compose method 패턴적용 메소드(함수)의 의도가 잘 드러나도록 동등한 수준의 작업을 하는 여러 단계로 나눈다.
  • 52.
    public class StringCalculator{ public static int add(String text) { if (isBlank(text)) { return 0; } return sum(toInts(split(text))); } private static boolean isBlank(String text) { } private static String[] split(String text) { } private static int[] toInts(String[] values) { } private static int sum(int[] numbers) { } } compose method 패턴 적용
  • 53.
    public class StringCalculator{ public static int add(String text) { int result = 0; if (text == null || text.isEmpty()) { result = 0; } else { String[] values = text.split(",|:"); for (String value : values) { result += Integer.parseInt(value); } } return result; } } public class StringCalculator { public static int add(String text) { if (isBlank(text)) { return 0; } return sum(toInts(split(text))); } private static boolean isBlank(String text) { } private static String[] split(String text) { } private static int[] toInts(String[] values) { } private static int sum(int[] numbers) { } } add() 메소드를 처음 읽는 사람에게 어느 코드가 더 읽기 좋을 까?
  • 54.
    한 번에 모든원칙을 지키면서 리팩토링하려고 연습하지 마라. 한 번에 한 가지 명확하고 구체적인 목표를 가지고 연습하라.
  • 55.
    연습은 극단적인 방법으로연습하는 것도 좋다. 예를 들어 한 메소드의 라인 수 제한을 15라인 -> 10라인으로 줄여가면 서 연습하는 것도 좋은 방법이다.
  • 56.
    리팩토링 연습 –클래스 분리
  • 57.
    다시 문자열 덧셈계산기 요구사항 쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리 한 각 숫자의 합을 반환 문자열 계산기에 숫자 이외의 값 또는 음수를 전달하는 경우 RuntimeException 예외를 throw한다. 입력(input) 출력(output) null 또는 “” 0 “1” 1 “1,2” 3 “1,2:3” 6 “-1,2:3” RuntimeException
  • 58.
    public class StringCalculatorTest{ […] @Test public void 쉼표_구분자() { assertThat(StringCalculator.splitAndSum("1,2")).isEqualTo(3); } @Test public void 쉼표_콜론_구분자() { assertThat(StringCalculator.splitAndSum("1,2:3")).isEqualTo(6); } @Test(expected = RuntimeException.class) public void 음수값() { StringCalculator.splitAndSum("-1,2:3"); } }
  • 59.
    public class StringCalculator{ public static int splitAndSum(String text) { […] } private static int[] toInts(String[] values) { int[] numbers = new int[values.length]; for (int i = 0; i < values.length; i++) { numbers[i] = toInt(values[i]); } return numbers; } private static int toInt(String value) { int number = Integer.parseInt(value); if (number < 0) { throw new RuntimeException(); } return number; } }
  • 60.
    public class StringCalculator{ […] private static int[] toInts(String[] values) { int[] numbers = new int[values.length]; for (int i = 0; i < values.length; i++) { numbers[i] = toInt(values[i]); } return numbers; } private static int toInt(String value) { int number = Integer.parseInt(value); if (number < 0) { throw new RuntimeException(); } return number; } } 모든 원시값과 문자열을 포장한다.
  • 61.
    public class Positive{ private int number; public Positive(String value) { int number = Integer.parseInt(value); if (number < 0) { throw new RuntimeException(); } this.number = number; } } 모든 원시값과 문자열을 포장한다.
  • 62.
    public class Positive{ private int number; public Positive(String value) { this(Integer.parseInt(value)); } public Positive(int number) { if (number < 0) { throw new RuntimeException(); } this.number = number; } } 모든 원시값과 문자열을 포장한다.
  • 63.
    public class StringCalculator{ 이전 코드와 같음 private static Positive[] toInts(String[] values) { Positive[] numbers = new Positive[values.length]; for (int i = 0; i < values.length; i++) { numbers[i] = new Positive(values[i]); } return numbers; } private static int sum(Positive[] numbers) { Positive result = new Positive(0); for (Positive number : numbers) { result = result.add(number); } return result.getNumber(); } } 모든 원시값과 문자열을 포장한다.
  • 64.
    public class Positive{ private int number; […] public Positive add(Positive other) { return new Positive(this.number + other.number); } public int getNumber() { return number; } } 모든 원시값과 문자열을 포장한다.
  • 65.
    클래스 분리 연습을위해 활용할 수 있는 원칙 • 일급 콜렉션을 쓴다. • 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  • 66.
    public class Lotto{ private static final int LOTTO_SIZE = 6; private final Set<LottoNumber> lotto; private Lotto(Set<LottoNumber> lotto) { if (lotto.size() != LOTTO_SIZE) { throw new IllegalArgumentException(); } this.lotto = lotto; } } 일급 콜렉션을 쓴다.
  • 67.
    public class WinningLotto{ private final Lotto lotto; private final LottoNumber no; public WinningLotto(Lotto lotto, LottoNumber no) { if (lotto.contains(no)) { throw new IllegalArgumentException(); } this.lotto = lotto; this.no = no; } public Rank match(Lotto userLotto) { int matchCount = lotto.match(userLotto); boolean matchBonus = userLotto.contains(no); return Rank.valueOf(matchCount, matchBonus); } } 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  • 68.
    4단계 - 장난감프로젝트 난이도 높이기
  • 69.
    점진적으로 요구사항이 복잡한프로그램을 구현한다. 앞에서 지켰던 기준을 지키면서 프로그래밍 연습을 한다.
  • 70.
    TDD, 리팩토링 연습하기좋은 프로그램 요구사항 • 게임과 같이 요구사항이 명확한 프로그램으로 연습 • 의존관계(모바일 UI, 웹 UI, 데이터베이스, 외부 API와 같은 의존관계) 가 없이 연습 • 약간은 복잡한 로직이 있는 프로그램
  • 71.
    연습하기 좋은 예 •로또(단, UI는 콘솔) • 사다리 타기(단, UI는 콘솔) • 볼링 게임 점수판(단, UI는 콘솔) • 체스 게임(단, UI는 콘솔) • 지뢰 찾기 게임(단, UI는 콘솔)
  • 72.
    5단계 - 의존관계추가를 통한 난이도 높이기
  • 73.
    웹, 모바일 UI,데이터베이스와 같은 의존관계를 추가
  • 74.
    이때 필요한 역량은 테스트하기쉬운 코드와 테스트하기 어려운 코드를 보는 눈 테스트하기 어려운 코드를 테스트하기 쉬운 코드로 설계하는 감 (sense)
  • 75.
    앞 단계 연습을잘 소화했다면 테스트하기 쉬운 코드와 어려운 코드를 분리하는 역량이 쌓였을 것이다.
  • 76.
    한 단계 더나아간 연습하기
  • 77.
    한 단계 더나아간 연습을 하고 싶다면 • 컴파일 에러를 최소화하면서 리팩토링하기 • ATDD 기반으로 응용 애플리케이션 개발하기 • 레거시 애플리케이션에 테스트 코드 추가해 리팩토링하기
  • 78.
  • 79.
    객체지향 생활체조 규칙 •규칙 1: 한 메서드에 오직 한 단계의 들여쓰기만 한다. • 규칙 2: else 예약어를 쓰지 않는다. • 규칙 3: 모든 원시값과 문자열을 포장한다. • 규칙 4: 한 줄에 점을 하나만 찍는다. • 규칙 5: 줄여쓰지 않는다(축약 금지). • 규칙 6: 모든 엔티티를 작게 유지한다. • 규칙 7: 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는 다. • 규칙 8: 일급 콜렉션을 쓴다. • 규칙 9: 게터/세터/프로퍼티를 쓰지 않는다.
  • 80.
    메소드 인수 개수 메소드(함수)에서이상적인 인자 개수는 0개(무항)이다. 다음 은 1개이고, 다음은 2개이다. 3개는 가능한 피하는 편이 좋다. 4개 이상은 특별한 이유가 있어도 사용하면 안된다. 클래스 클래스를 만들 때 첫 번째 규칙은 크기다. 클래스는 작아야 한다. 두 번째 규칙도 크기다. 더 작아야 한다.
  • 81.
  • 82.
    TDD, 리팩토링 연습을위해 필요한 것은? • 조급함 대신 마음의 여유 • 나만의 장난감 프로젝트 • 같은 과제를 반복적으로 구현할 수 있는 인내력
  • 83.
    가장 필요한 것은가보지 않는 길에 꾸준히 도전할 수 있는 용기
  • 84.
    부록 - 예제를통해 테스트하기 쉬운 코드와 어려운 코드 분리
  • 85.
    Q&A 게시판에서 질문삭제 요구사항 • 질문 데이터를 완전히 삭제하는 것이 아니라 데이터의 상태를 삭제 상태 (deleted 상태를 false -> true)로 변경한다. • 로그인 사용자와 질문한 사람이 같은 경우 삭제 가능하다. • 답변이 없는 경우 삭제가 가능하다. • 질문자와 답변 글의 모든 답변자 같은 경우 삭제가 가능하다. • 질문을 삭제할 때 답변 또한 삭제해야 하며, 답변의 삭제 또한 삭제 상태 (deleted)를 변경한다. • 질문자와 답변자가 다른 경우 답변을 삭제할 수 없다.
  • 86.
    Q&A 게시판에서 질문삭제 요구사항 • 질문 데이터를 완전히 삭제하는 것이 아니라 데이터의 상태를 삭제 상태 (deleted 상태를 false -> true)로 변경한다. • 로그인 사용자와 질문한 사람이 같은 경우 삭제 가능하다. • 답변이 없는 경우 삭제가 가능하다. • 질문자와 답변 글의 모든 답변자 같은 경우 삭제가 가능하다. • 질문을 삭제할 때 답변 또한 삭제해야 하며, 답변의 삭제 또한 삭제 상태 (deleted)를 변경한다. • 질문자와 답변자가 다른 경우 답변을 삭제할 수 없다.
  • 87.
    Q&A 게시판에서 질문삭제 요구사항 질문 답변 삭제 여부 로그인 사용자 != 질문자 답변 유무와 무관 X 로그인 사용자 == 질문자 답변이 없음 O 로그인 사용자 == 질문자 로그인 사용자 == 모든 답변자 O 로그인 사용자 == 질문자 로그인 사용자 != 모든 답변자 X
  • 88.
    public void deleteQuestion(UserloginUser, long questionId) throws CannotDeleteException { Question question = questionRepository.findOne(questionId); if (question == null) { return; } if (!loginUser.equals(question.getWriter())) { throw new CannotDeleteException("질문을 삭제할 수 없습니다."); } List<Answer> answers = question.getAnswers(); boolean canDelete = true; for (Answer answer : answers) { if (!loginUser.equals(answer.getWriter())) { throw new CannotDeleteException("답변을 삭제할 수 없습니다."); } } question.delete(); for (Answer answer : answers) { answer.delete(); } }
  • 89.
    테스트 가능한 부분을분리하고, 객체지향 설계 원칙에 따라 역할과 책임을 분리해 구현한다.
  • 90.
    public void deleteQuestion(UserloginUser, long questionId) throws CannotDeleteException { Question question = questionRepository.findOne(questionId); if (question == null) { return; } question.delete(loginUser); }
  • 91.
    public class Question{ public void delete(User loginUser) throws CannotDeleteException { if (!isOwner(loginUser)) { throw new CannotDeleteException("다른 사람의 글은 삭제할 수 없다."); } answers.delete(loginUser); this.deleted = true; } }
  • 92.
    public class Answers{ private List<Answer> answers = new ArrayList<>(); public void delete(User loginUser) throws CannotDeleteException { for (Answer answer : answers) { answer.delete(loginUser); } } } 일급 콜렉션을 쓴다.
  • 93.
    단위 테스트를 먼저만드느냐, 뒤에 만드느냐는 중요하지 않다. 단위 테스트 하기 쉬운 구조로 설계하고, 단위 테스트를 추가하 는 것이 핵심이다.
  • 94.
    단위 테스트를 먼저만드느냐, 뒤에 만드느냐는 중요하지 않다. 단위 테스트 하기 쉬운 구조로 설계하고, 단위 테스트를 추가하 는 것이 핵심이다. 테스트하기 쉬운 코드와 테스트하기 어려운 코드를 보는 눈 테스트하기 어려운 코드를 테스트하기 쉬운 코드로 설계하는 감 (sense)