나를 기록하다
article thumbnail
반응형

우아한테크세미나

1. 의식적인 연습이란?

의식적인 연습의 7가지 원칙
  1. 효과적인 훈련 기법이 수립되어 있는 기술 연마
  2. 개인의 컴포트 존을 벗어난 지점에서 진행, 자신의 현재 능력을 살짝 넘어가는 작업을 지속적으로 시도
  3. 명확하고 구체적인 목표를 가지고 진행
  4. 신중하고 계획적. 개인이 온전히 집중하고 의식적으로 행동할 것을 요구
  5. 피드백과 피드백에 따른 행동 변경을 수반
  6. 효과적인 심적 표상을 만들어내느 한편으로 심적 표상에 의존
  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

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)
  1. 한 메서드에 오직 한 단계의 들여쓰기만 한다.
  2. else 예약어를 쓰지 않는다.
  3. 모든 원시값과 문자열을 포장한다.
  4. 한 줄에 점을 하나만 찍는다.
  5. 줄여쓰지 않는다.(축약 금지)
  6. 모든 엔티티를 작게 유지한다.
  7. 3개 이상의 인스턴스 변수를 가진 클래스를 쓰지 않는다.
  8. 일급 컬렉션을 쓴다.
  9. getter/setter/property를 사용하지 않는다.

 

메서드 인자 개수(출처: Clean Code)
  • 메서드(함수)에서 이상적인 인자 개수는 0개(무항)이다.
  • 다음은 1개이고, 다음은 2개이다.
  • 3개는 가능한 피하는 편이 좋다.
  • 4개 이상은 특별한 이유가 있어도 사용하면 안된다.

참고자료

https://www.youtube.com/watch?v=bIeqAlmNRrA&t=2372s 

 

반응형
profile

나를 기록하다

@prao

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...