1. 의식적인 연습이란?
의식적인 연습의 7가지 원칙
- 효과적인 훈련 기법이 수립되어 있는 기술 연마
- 개인의 컴포트 존을 벗어난 지점에서 진행, 자신의 현재 능력을 살짝 넘어가는 작업을 지속적으로 시도
- 명확하고 구체적인 목표를 가지고 진행
- 신중하고 계획적. 개인이 온전히 집중하고 의식적으로 행동할 것을 요구
- 피드백과 피드백에 따른 행동 변경을 수반
- 효과적인 심적 표상을 만들어내느 한편으로 심적 표상에 의존
- 기존에 습득한 기술의 특정 부분을 집중적으로 개선함으로써 발전시키고, 수정하는 과정을 수반
의식적인 연습 예시 - 우테코 프리코스
- 3주 동안 진행. 매주 해결해야할 미션 부여
- 미션 완료 후 Github의 Pull Request를 제출
- 공통 피드백 진행
[피드백 예시]
- space(공백)도 convention
- for, while, if문 사이의 space도 convention
- 불필요하게 공백 라인을 만들지 않는다.
- 공백 라인을 띄우는 것도 코드상에 문맥이 달라지는 부분에 의도를 가지고 하라.
- git commit 메시지를 의미있게 작성한다.
- commit 메시지에 해당 commit에서 작업한 내용에 대한 이해가 가능하도록 작성
- 자바 코드 컨벤션을 지키면서 프로그래밍한다.
- indent(인덴트, 들여쓰기) depth를 3이 넘지 않도록 구현한다.(2까지)
- java api를 적극 활용한다.
- 함수(또는 메서드)의 길이가 10라인을 넘지 않도록
- indent depth를 2가 넘지 않도록 구현
- 함수(또는 메서드)의 인자 수를 3개까지만 허용.
- 객체에 메시지를 보내라
- 상태 데이터를 가지는 객체에서 데이터를 꺼내려(get)하지 말고 객체에 메시지를 보내라.
private boolean isMaxPosition(Car car) {
return car.getPosition() == maxDistance;
}
//=========================================//
private boolean isMaxPosition(Car car) {
return car.isMaxPosition(maxDistance);
}
2. TDD, 리팩토링 적용 - 개인
TDD, 리팩토링 == 운동
- 평생동안 연습하겠다는 마음가짐으로 시작하라
- 시간을 확보하라 - 매일, 꾸준히 하라
- 토이 프로젝트를 찾아라 - 주변 환경에 영향을 받지 않고 꾸준히 연습하기 위함
1단계 - 단위테스트 연습
내가 사용하는 API 사용법을 익히기 위한 학습 테스트에서 시작
- 자바 String 클래스의 다양한 메서드(함수) 사용법
- 자바 ArrayList에 데이터를 추가, 수정, 삭제하는 방법
public class StringTest{
@Test
public void split() {
String[] values = "1".split(",");
assertThat(values).contains("1");
values = "1,2".split(",");
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")
}
}
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()).isEqualsTo(3);
assertThat(values.get(0)).isEqualsTo("first");
assertThat(values.contains("first")).isTrue();
assertThat(values.remove(0)).isEqualTo("first");
assertThat(values.size()).isEqualTo(2);
}
}
연습 효과
- 단위테스트 방법 학습
- 단위테스트 도구(xUnit)의 사용법 학습
- 내가 구현하는 메서드(함수) 중 Input과 Output이 명확한 클래스 메서드(보통 Util 성격의 메서드)에 대한 단위테스트 연습
2단계 - TDD 연습
- 어려운 문제를 해결하는 것이 목적이 아니라 TDD 연습이 목적
- 난이도가 낮거나 자신에게 익숙한 문제로 시작하라
- 웹, 모바일 UI나 DB에 의존관계를 가지지 않는 요구사항으로 연습하라
[예시] 문자열 덧셈 계산기
- 요구사항: 쉼표(,) 또는 콜론(:)을 구분자로 가지는 문자열을 전달하는 경우 구분자를 기준으로 분리한 각 숫자의 합을 반환
TDD Cycle
3단계 - 리팩토링 연습
리팩토링 연습 - 메서드 분리
테스트 코드
테스트코드와 프로덕션코드를 만들고 리팩토링하는 과정으로 코드를 발전시킨다.
public class StringCalculatorTest {
@Test
public void null_또는_빈값() {
assertThat(StringCalculator.splitAndSum(null)).isEqualsTo(0);
assertThat(StringCalculator.splitAndSum("")).isEqualsTo(0);
}
@Test
public void 값_하나() {
assertThat(StringCalculator.splitAndSum("1")).isEqualsTo(1);
}
@Test
public void 쉽표_구분자() {
assertThat(StringCalculator.splitAndSum("1,2")).isEqualsTo(3);
}
@Test
public void 쉼표_콜론_구분자() {
assertThat(StringCalculator.splitAndSum("1,2:3")).isEqualsTo(6);
}
}
다음의 프로덕션 코드를 아래와 같이 만든다.
프로덕션 코드
public class StringCalculator {
public static int splitAndSum(String text){
int result = 0;
if(text == null || text.isEmpty()) {
return 0;
} else {
String[] values = text.split(",|:");
for(String value : values) {
result += Integer.parseInt(value);
}
}
return result;
}
}
리팩토링 연습
정량적이고 측정 가능한 방법으로 연습하라.
1. 한 메서드에 오직 한 단계의 들여쓰기(indent)만 한다. → 메서드 분리
public class StringCalculator {
public static int splitAndSum(String text){
if(text == null || text.isEmpty()) {
return 0;
} else {
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;
}
}
2. 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;
}
}
3. 메서드가 한 가지 일만 하도록 구현하라.
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){
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;
}
}
4. 로컬 변수가 정말 필요한가?
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){
[...]
}
}
5. compose method 패턴 적용
- 메서드(함수)의 의도가 잘 드러나도록 동등한 수준의 작업을 하는 여러 단계로 나눈다.
public class StringCalculator {
public static int splitAndSum(String text){
if(isBlank(text)) {
return 0;
}
return sum(toInts(split(text)));
}
private static boolean isBlank(String text) {
return text == null || text.isEmpty();
}
private static String[] split(String text) {
return text.split(",|:");
}
private static int[] toInts(String[] values){
[...]
}
private static int sum(int[] numbers){
[...]
}
}
한번에 모든 원칙을 지키면서 리팩토링하려고 연습하지 마라.
한번에 한 가직 명확하고 구체적인 목표를 가지고 연습하라.
컴포트존을 제거하고 리팩토링을 통해 좋은 코드를 작성할 수 있도록 노력하라!
리팩토링 연습 - 클래스 분리
숫자 이외의 값이나 음수 입력 시 RuntimeException이 발생하도록 적용. 테스트 코드 추가
public class StringCalculatorTest {
[...]
@Test
public void 쉽표_구분자() {
assertThat(StringCalculator.splitAndSum("1,2")).isEqualsTo(3);
}
@Test
public void 쉼표_콜론_구분자() {
assertThat(StringCalculator.splitAndSum("1,2:3")).isEqualsTo(6);
@Test(expected = RuntimeException.class)
public void 음수값() {
StringCalculator.splitAndSum("-1,2:3");
}
}
테스트에 대한 프로덕션 코드 추가
public class StringCalculator {
public static int splitAndSum(String text){
[...]
return sum(toInts(split(text)));
}
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 toInt(String value) {
int number = Integer.parseInt(value);
if(number < 0) {
throw new RuntimeException();
}
return number;
}
}
모든 원시값과 문자열을 포장한다.
number < 0일 때, RuntimeException을 던지도록 한다.
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;
}
}
int와 String으로 처리하던 코드를 Positive 클래스의 메서드로 변경
public class StringCalculator {
public static int splitAndSum(String text){
[...]
private static Positive[] toPositives(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(String value) {
this(Integer.parseInt(value));
}
public Positive(int number) {
if(number < 0){
throw new RuntimeException();
}
this.number = number;
}
public Positive add(Positive other) {
return new Positive(this.number + other.number);
}
public int getNunber() {
return number;
}
}
클래스를 분리하는 원칙
일급 컬렉션(예시의 Positive는 primitive type을 포장한 클래스)으로 분리가 가능할 경우 클래스로 분리가 가능하다.
일급 컬렉션이란 하나의 컬렉션을 주된 멤버로 가지는 클래스. 모든 연습은 정량적 기준으로 진행한다.
[예시]
Set 컬렉션 하나만을 인스턴스 변수로 가지는 클래스를 분리
Set 클래스 관련 로직들이 이 클래스로 들어오게 된다.
import java.util.Set;
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;
}
}
3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
[예시]
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);
}
}
4단계 - 토이 프로젝트 난이도 높이기
리팩토링 연습하기 좋은 프로그램 요구사항
- 게임과 같이 요구사항이 명확한 프로그램으로 연습
- 의존관계(모바일 UI, 웹 UI, 데이터베이스, 외부 API와 같은 의존관계)가 없이 연습
- 약간은 복잡한 로직이 있는 프로그램
[예시]
UI는 콘솔로 진행할 것
- 로또
- 사다리 타기
- 볼링 게임 전수판
- 체스 게임
- 지뢰 찾기 게임
5단계 - 의존관계 추가를 통한 난이도 높이기
- 테스트하기 쉬운 코드와 테스트하기 어려운 코드를 보는 눈
- 테스트하기 어려운 코드를 테스트 하기 쉬운 코드로 설계하는 센스
- 컴파일 에러를 최소화하면서 리팩토링하기
- ATDD 기반으로 응용 애플리케이션 개발하기
- 레거시 애플리케이션에 테스트 코드 추가해 리팩토링하기
→ TDD, 리팩토링 연습이 충분하지 않은 상태에서 높은 난이도에 바로 도전하면 리팩토링 적용에 실패한다.
객체지향 생활체조 원칙(The ThoughtWorks Anthology)
- 한 메서드에 오직 한 단계의 들여쓰기만 한다.
- else 예약어를 쓰지 않는다.
- 모든 원시값과 문자열을 포장한다.
- 한 줄에 점을 하나만 찍는다.
- 줄여쓰지 않는다.(축약 금지)
- 모든 엔티티를 작게 유지한다.
- 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
- 일급 컬렉션을 쓴다.
- getter/setter/property를 사용하지 않는다.
메서드 인자 개수(출처: Clean Code)
- 메서드(함수)에서 이상적인 인자 개수는 0개(무항)이다.
- 다음은 1개이고, 다음은 2개이다.
- 3개는 가능한 피하는 편이 좋다.
- 4개 이상은 특별한 이유가 있어도 사용하면 안된다.
참고자료
https://www.youtube.com/watch?v=bIeqAlmNRrA&t=2372s
'Review > Seminar' 카테고리의 다른 글
[KSUG Seminar] Growing Application - 애플리케이션과 객체지향 (0) | 2023.09.30 |
---|