2018.10.18
OKKYCON: 2018 《The Real TDD - TDD 제대로 알기》
박재성님의 <의식적인 연습으로 TDD, 리팩토링 연습하기> 발표자료입니다.
[연사 소개]
박재성 - SW 교육 전문가 전 NEXT 교수
5년 동안 NEXT에서 학생들을 가르치다 NEXT가 문을 닫으면서 1인 교육 사업을 하고 있다. TDD, 리팩토링 경험이 프로그래머의 삶에 큰 영향을 미칠 것으로 판단해 TDD, 리팩토링을 주제로 교육 과정을 개설했는데 좋은 반응을 얻고 있다.
[발표 소개]
TDD와 리팩토링 역량은 책 몇 권 읽고, 반복적인 연습만 한다고 해서 쌓을 수 있는 역량이 아닙니다. 의식적인 연습을 통해 꾸준히 수련해 나갈 때 점진적으로 향상시킬 수 있습니다. 의식적인 연습을 설계하고, 단계적인 수련을 통해 점진적으로 TDD, 리팩토링 역량을 키워가는 과정에 대해 다룹니다.
http://okkycon.com
15. 의식적인 연습의 7가지 원칙
• 첫째, 효과적인 훈련 기법이 수립되어 있는 기술 연마
• 둘째, 개인의 컴포트 존을 벗어난 지점에서 진행, 자신의 현재 능력
을
살짝 넘어가는 작업을 지속적으로 시도
• 셋째, 명확하고 구체적인 목표를 가지고 진행
16. 의식적인 연습의 7가지 원칙
• 넷째, 신중하고 계획적이다. 즉, 개인이 온전히 집중하고 '의식적'으로
행동할 것을 요구
• 다섯째, 피드백과 피드백에 따른 행동 변경을 수반
• 여섯째, 효과적인 심적 표상을 만들어내는 한편으로 심적 표상에 의존
• 일곱째, 기존에 습득한 기술의 특정 부분을 집중적으로 개선함으로
써
발전시키고, 수정하는 과정을 수반
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 연습이 목적
난이도가 낮거나 자신에게 익숙한 문제로 시작하는 것을 추천
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;
}
}
테스트 코드는 변경하지 말고
테스트 대상 코드(프로덕션 코드)를 개선하는 연습을 한다.
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라인으로 줄여가면
서 연습하는 것도 좋은 방법이다.
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개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
79. 객체지향 생활체조 규칙
• 규칙 1: 한 메서드에 오직 한 단계의 들여쓰기만 한다.
• 규칙 2: else 예약어를 쓰지 않는다.
• 규칙 3: 모든 원시값과 문자열을 포장한다.
• 규칙 4: 한 줄에 점을 하나만 찍는다.
• 규칙 5: 줄여쓰지 않는다(축약 금지).
• 규칙 6: 모든 엔티티를 작게 유지한다.
• 규칙 7: 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는
다.
• 규칙 8: 일급 콜렉션을 쓴다.
• 규칙 9: 게터/세터/프로퍼티를 쓰지 않는다.
80. 메소드 인수 개수
메소드(함수)에서 이상적인 인자 개수는 0개(무항)이다. 다음
은 1개이고, 다음은 2개이다.
3개는 가능한 피하는 편이 좋다.
4개 이상은 특별한 이유가 있어도 사용하면 안된다.
클래스
클래스를 만들 때 첫 번째 규칙은 크기다.
클래스는 작아야 한다.
두 번째 규칙도 크기다.
더 작아야 한다.
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(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();
}
}
89. 테스트 가능한 부분을 분리하고,
객체지향 설계 원칙에 따라 역할과 책임을 분리해 구현한다.
90. public void deleteQuestion(User loginUser, 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)