Skip to content

Latest commit

 

History

History
487 lines (398 loc) · 30.1 KB

tdd_lecture.md

File metadata and controls

487 lines (398 loc) · 30.1 KB

TDD with cleanCode 공부를 하면서 배운 것들을 정리해봤다.

TDD의 의미와 의의

  • TDD랑 단위테스트는 다르다
    • TDD는 그 과정을 얘기하는 것이고 단위테스트는 독립적. 만약 유연한 코드가 아니라면 추후에 단위테스트를 추가하기가 힘듬
  • 테스트 코드를 프로덕션 코드보다 늦게 짜면 테스트 코드를 짜기 힘들다. 더 어렵다.
  • TDD를 하게 되면 설계를 고려하게 되어서 테스트하기 쉬운 코드를 만들게 한다. 강제한다. 왜냐하면 테스트하기 쉬운 구조가 아니면 테스트가 안된다 => 설계 역량을 높여준다(고민 => 어떻게 해야 테스트하기 쉬울까)
  • 유연성이 높다(요구사항이 변경되었을 때 설계 변동성 낮음)
  • 최종적으로 제품을 만들고 난 후에는 수동테스트는 당연히 무조건 필요하다.(최종테스트)
    • 하지만 자동화 테스트(단위 테스트)를 통해서 코드를 작성하게 된다면 테스트에 들어가는 시간이 1/10로 줄어들 것이다.
  • 우리가 하려고 하는 최종 목표는 레거시 리팩토링이다. 백지에서 하는 것은 아무나 쉽게할 수 있다.

TDD with cleanCode의 방법 및 요령

TDD 처음 시작하기

  1. 시작하기
  • 객체 설계를 어떻게 해야할지 모르겠다면 시작은 클래스 메소드 구현으로 시작한 후 지속적인 리팩토링
  • 리팩토링할 때는 객체지향 생활체조 원칙, 클린코드 원칙을 참고해 리팩토링
  1. 요구사항 분석을 통한 기능 목록 작성
  • 처음부터 너무 자세한 기능 목록을 만드는 것이 목적이 아니다. 처음에 도메인을 파악할 수 있는 만큼만의 todo 리스트 만들기. 작업하다가 점점 더 정교하게 적용.
  • 작업중에 갑자기 생각난 todo 리스트가 생각났을 때는 리스트에만 추가한 후에, 하던 작업 만들기
  • 처음부터 끝까지 계속 업데이트 하기
  • TDD로 구현할 기능 찾기
    • 구현 중간 부분을 자르는 연습을 해야 한다.
    • 즉, 프로그램이 실행되는 특정 시점의 상태 값으로 시작한다는 것을 의미한다.
    • ex) 자동차 경주 게임 - 우승자 구하기에서 경주를 완료한 시점의 자동차 상태 값을 테스트 코드에서 변경(또는 결정)할 수 있음을 의미한다.
  • 로또에서 TDD로 구현할 기능 찾기. ex)
    • 로또 구매 금액을 전달하면 구매할 수 있는 로또의 장수를 반환한다.
    • 구매할 로또의 장수만큼 자동 구매할 경우 자동 로또 생성해 반환한다.
    • 구매한 한장의 로또 번호와 당첨 번호를 넣으면 당첨 결과를 반환한다.
    • 구매한 전체 로또의 당첨 결과를 입력하면 당첨금 총액을 반환한다.
    • 당첨 금액과 구매 금액을 넣으면 수익률을 반환한다.
  1. 객체설계를 통해 어느 부분부터 구현을 시작할 것인지 결정
  • 클래스를 아예 생성도 안하고 바로 TDD 하는 것이 아니라 최소한, 적당한 클래스 설계를 한 후에 TDD를 시작하는 것이 수월하다. 아예 맨 바닥에서는 너무 어렵다.

TDD는 핵심 비즈니스 로직에만 하면 된다.

  • 정말 중요한 핵심 비즈니스 로직에 대해서만 TDD 하면 된다. 제일 중요한 부분임.

    • 정말 테스트하기 힘든 부분은 사실 꼭은 안해도 된다(ex. 데이터베이스 CRUD)
      • 핵심 비즈니스 로직이 포함되어 있는 부분이 아니므로.
    • 모키토을 하지 않고도 비즈니스 로직을 테스트하기 쉬운 구조로 설계 변경. 그러면 junit 으로도 테스트할 수 있다.
    • 모든 부분을 다 TDD로 구현하는 것이 목표가 아니다. DB에 의존하지 않도록 핵심 비즈니스 로직을 분리하고 그 부분들을 TDD로 구현하자는 것.
  • 그런데 비즈니스 로직은 엔티티 객체 안에 최대한 다 넣어야 한다.

In -> Out 방식으로 TDD 도전

  • 가장 작은 단위 leaf 를 찾아서 TDD 로 in -> out 방식으로 하는 것이 낫다.

  • Out -> In 방식으로 한다면?

    • 테스트하기 어려움.
  • 그래서 예를 들어 사다리(Ladder)를 만든다고 한다면 하나의 라인(LadderRow)부터, 즉 작은 것부터 찾아서 TDD로 구현후에, 여러 개의 LadderRow가 필요한 Ladder를 구현하는 것이 낫다.

  • 큰 단위를 작은 단위로 쪼갤 수는 없을까?를 고민해야한다.

    • LadderRow에서는 여러 개의 true-false-false-... 이 있는데, 하나의 점(boolean)을 어떤 식으로 만들어야 할지 고민해보면 된다..
  • 중간에 포기하고 모든 코드를 버리더라도 괜찮다. 도메인 지식이 쌓이므로 다시 시작할 때 더 좋은 도메인 객체 설계를 하는 것이 맞기 때문

  • TDD는 테스트하기 쉬운 코드를 처음부터 고민하는 것. 왜냐하면 테스트를 해야 하므로. 물론 테스트가 쉽다고 해서 유연한 코드는 아니지만 그럴 확률이 높다.

원시값 포장하기

  • 일급 콜렉션을 사용하자

    • 좋은 링크
  • 클래스 분리

    • 원시값을 클래스로 포장하면, 그 클래스 안에는 그 하나의 원시값을 가진다.
    • 일급 컬렉션 역시 마찬가지다.
    • 생성자를 많이 활용하자
      • 하나의 값을 가지는 것을 클래스로 래핑하고 그 안에서 메시지 받아서 로직 처리.
      • 그러면 클래스 안의 그 하나의 멤버변수 값은 메서드들이 다 사용하므로 좋은 객체 설계.
  • 모든 원시값과 문자열을 포장해라

    • value object임
    • 하나의 멤버 변수를 가지는 객체를 만들게 되면 그 안의 대부분의 메서드들은 그 멤버변수를 다루므로 좋은 설계가 되고, 그 멤버 변수의 조건(음수가 안되거나 문자열 길이 등) 등도 객체가 책임지고 관리할 수 있게 된다.
    • 로직이 이 객체의 내부로 이동하게 된다.
    • 값을 책임지므로 버그를 발생할 확률이 낮다. 그냥 단순히 int 값으로 돌아다니면 범위가 너무 크고 버그 발생 확률 높음.
    • 외부에 값을 변경하는 메서드를 공개 안하면(또는 만들지 않는다면) 불변 객체된다. 한번 초기화된 인스턴스는 변경되지 않는다.
    • 불변객체 단점) 매번 새로운 객체가 만들어지는데(가비지 컬렉션 문제), 그럴 경우에는 본인의 값을 변경하는 메서드를 만들어서 그대로 자신을 리턴하면 된다. -> 불변객체에서 가변객체로 변하게 함으로써 유연성으로 해결.
      • 하지만 처음부터 너무 성능을 생각하지 마라.
    public Position move() {
        position = position + 1;
        return this;
    }
    public Position move2() {
        return new Position(position + 1);
    }
  • 모든 원시값들을 포장할 수는 없지만 특정 원시값들은 포장하면 좋은 효과를 볼 수 있는 것들이 많다(관련된 로직들이 그 클래스 안으로 들어가게 된다). 당연히 모든 것을 다 포장하려고 하면 복잡도만 더 증가된다. 그 판단을 현명하게 잘해야 한다.

객체에게 메시지 보내기(getter 사용지양)

  • 값 비교할 때 객체의 값을 꺼내서(getter) 하는 것 자체가 절차지향적임.(객체 지향적 아님)
    • 값 말고 객체로 비교하는 것이 객체지향적. 클래스가 값을 감싸고 있다면(ex, Position, Name) equals와 hasCode도 Override 해서 equals로 비교하기.
    • 절차지향적으로 짜면 테스트 하기 힘들다.
    • getter 를 보는순간!!!! 그 객체한테 메시지를 보내는 것으로 해결한 후 getter 메서드 삭제.
    • 로직구현할 때는 getter 쓰지 말고 메시지를 보내라. 그리고 Getter는 로직수행한 다음에, 제일 마지막에 UI에 보낼 때나 View에 전달할 때 Dto로 변경할 때에 필요하다.
    • 사실 Getter 을 아에 안 쓸수는 없다...최대한 쓰지 말자.
    • 그래서 그 객체한테 비교하고자 하는 값을 보내야 한다.(메시지를 보내는 것임)
      • 장점1) 코드 깔끔(indent 줄임)
      • 장점2) 테스트코드 짜기 쉽다
    • 값을 비교하기 위해 Getter 보다 class로 변수를 래핑해서 그 안에서 처리하면 좋다.

public 메서드보다 생성자 사용하기

  • 메서드 수 < 생성자 수 (by 엘레강트 오브젝트)

    • 응집도 높고, 견고한 클래스에는 적은 수의 메서드와 상대적으로 더 많은 수의 생성자가 존재한다는 점.
    new Cash(30);
    new Cash(29.95d);
    new Cash(29.95f);
    
  • 주 생성자와 부 생성자

    • 주 생성자는 1개. 부 생성자는 주 생성자를 호출하도록 한다.
    • 주 생성자는 가장 마지막에 작성하는 것이 주 생성자.
    • 중복 코드를 줄일 수 있다.
  • 메서드의 반환타입도 Wrapping한 클래스로 반환하는 것이 좋다. 왜냐하면 int로 반환하게 되면 int 값은 사실 엄청 큰 수가 될 수 있으므로 제한할 필요가 있다.

  • 테스틀를 위한 생성자는 추가로 만들어도 된다. 타입이 다른 오버로딩 생성자를 만들어도 된다.

  • 메서드도 타입이 다른 오버로딩을 하면 사용자 입장에서는 훨씬 더 편하다.

  • public 메서드 수를 늘리는 것을 조심해라(단일책임 원칙을 어길 가능성 높아진다).

  • private 메서드 수는 많아도 무관.

클래스 분리

  • 클래스 분리를 위한 정량적인 원칙

    • 소트웍스 앤솔리치 책(객체지향 생활 체조 원칙)
    • 엘레강트 오브젝트
    • 클린코드에서 클래스 분리 원칙
  • 3개 이상의 인스턴스 변수를 가진 클래스를 허용하지 않는다. 최대 2개까지 제한. (클린코드 책 - 함수에서 이상적인 인수 개수는 0개, 다음은 1개, 그 다음은 2개. 3개는 피하는 것이 좋다. 4개 이상은 특별한 이유가 있어도 사용하면 안된다. 함수 인자가 많다는 것은 그만큼 하나의 메서드가 여러 일을 한다는 것)

    • 인스턴스 변수의 수를 줄이는 좋은 방법
      • 중복된 값 또는 불필요한 인스턴스 변수가 있는지를 확인해 제거.서로 연관있는 인스턴스 변수가 있으면 에러 발생률이 높다.
        • ex) 사실 아래에서는 cars 하나만 있어도 되고, 그것 하나로 나머지 2개를 다 구할 수 있다.
        public class Winners {
            private List<Car> cars;
            private List<String> winners;
            private int maxDistinace;
        }
        • 위 코드 수정 후(Winners 클래스에 아래의 메서드로 처리 - winners 인스턴스 변수 삭제)
        public WinnerNames findWinners() {
            ...중략...
            new WinnerNames();
        }
      • 관련있는 인스턴스 변수를 새로운 클래스(객체)로 묶어 분리. 새로운 클래스는 그 할일들을 가지고 있는다. 메서드의 인자들은 서로 연관되어 있을 확률이 높기 때문에 클래스 분리하기 매우 편하다.
  • 클래스가 가지고 있는 인스턴스 변수도 많이 두지 말기

    public class ABC {
        private int a;
        private int b;
        private int c;
    }
    
    • 만약 위에서 a와 b가 연관이 있다면 a, b를 AB 클래스로 빼내기
    public class AB {
        private int a;
        private int b;
    }
    
    public class C {
        private int c;
    }
    
    • AB클래스에 a, b는 또 각각 A, B 클래스로 포장하기(포장하는 것이 의미가 있다고 생각하면!. 그런데 연습때는 무조건 다 포장하기)
    public class AB {    
        private A a;
        private B b;
    }
    
    public class A {
        private int a;
    }
    
    public class B {
        private int b;
    }
    
    public class C {
        private int c;
    }
    
    • 의문) 이렇게 하면 객체들의 뎁스가 깊어지는 것은 문제가 업을까? 없다. 원래 OOP가 그런 것임. 위임하는 것. Object 그래프가 깊어지면 참조를 나타내는 .(점) 을 2개 이상 쓰지말라고 한다. 디미터의 법칙(Object 그래프 중에서 옆 집 친구랑만 놀아라!) .(점)이 많아지면 많아질수록 에러발생확률이 높다.
  • 객체 간 의존관계 연결, 인자 수를 최소화

    • 인자가 3개 이상이라면, 2개를 묶어서 새로운 클래스로 만들어도 된다.
    • 클래스를 분리할 때 또는 분리한 후 클래스끼리는 어떻게 연결하는가. 상속보다는 조합을 사용하자. 코드의 재사용성(Reusability) 측면에서는 상속이 유리하지만, 유연성(Flexibility)측면에서는 조합이 더 유리하다. 변화에 빠르게 대응하는 것이 점점 더 중요해지고 있는 현재는 재사용성보다는 유연성이 훨씬 더 중요하다. 상속의 복잡도는 너무나 큰 단점. 사실 코딩량이 아주 조금 많아질 뿐, 조합도 코드를 재사용하는 것임.
      • 상속은 웬만하면 피하라(is-a관계). 왜냐하면 상속했을 때는 자식은 본인의 메서드 하나만 사용하면 되는데 상속하게 되면 부모의 모든 것을 다 오픈하게 된다. 아래는 ArrayList의 수많은 메서드들이 오픈
      public class Lottos extends ArrayList<Lotto> {
          public LottoResults match(Lotto winningLotto) {
              LottoResults lottoResults = new LottoResults();
              this.stream()
                  .map(lotto -> new LottoResult(
                          lotto.getCorrentCount(winningLotto.getNumbers())))
                      .forEach(lottoResults::add);
              return lottoResults;
          }
      }
      • 조합을 사용하자(has-a관계). List 컬렉션의 어떠한 api도 오픈하지 않고 단지 match하나만 오픈
      public class Lottos {
          private List<Lotto> lottos;
      
          public Lottos(List<Lotto> lottos) {
              this.lottos = lottos;
          }
      
          public LottoResults match(Lotto winningLotto) {
              LottoResults lottoResults = new LottoResults();
              this.stream()
                  .map(lotto -> new LottoResult(
                          lotto.getCorrentCount(winningLotto.getNumbers())))
                      .forEach(lottoResults::add);
              return lottoResults;
          }
      
      }
    • 메서드가 위치해야 하는 클래스의 위치를 잘 판단하고 계속 이동

생성자 대신 정적 팩토리 메서드를 사용하라.

  • (좀 더 알아보기)
  • 팩토리 메서드

인터페이스, 추상클래스의 필요성

  • 인터페이스가 필요할 수 있는 부분

    • if(또는 if/else if/ else)가 반복되는 부분(switch 도 마찬가지)
    • 메서드가 길어지는 부분
  • 인터페이스 기반 설계를 잘하려면?

    • 내가 운영하는 서비스(도메인)에 대한 이해도가 높아야 한다.
    • 상당 기간의 운영 경험을 통해 요구사항의 변화가 자주 발생하는 부분을 찾아야 함.
    • 즉, 상당 기간의 유지보수 + 지속적인 리팩토링 경험이 핵심
    • TDD, OOP 에 익숙해지면 나중에 인터페이스로 주도 개발을 도전해보자
    • 그냥 TDD 즐겨서 하자.
  • 인터페이스로 설게를 한다면 패키지간 순환의존을 조심해야 한다. cycle dependency

    • 해결법
      • 구현클래스들이 있는 패키지(A) -> 인터페이스 있는 패키지(B)
        • factory 패키지를 만들어서 각각 ->A, ->B 를 바라보게 하면 된다.
      • 정적 분석 도구 사용하기. sonarqube 도구 사용하기. 사용하면 cyclic packages 수를 알려준다.
  • 그런데 인터페이스로 하게 되면 in -> out 방식이 아닌 끼워맞추기 식의 out -> in 식 개발이 될 가능성이 높다.

    • 도메인 지식이 쌓인 후에 인터페이스를 나중에 적용하자. 급하게 하지 말기.
  • 추상클래스

    • 인터페이스를 구현한 여러 클래스들 안에 중복코드가 발생할 때, 그 위에 추상클래스를 둔다.
  • DI를 잘해야 테스트 가능한 설계가 된다.

    • 스프링이 없는 자바코드로도 DI를 할 줄 알아야 한다.
  • Strategy 전략을 통한 테스트하기 쉬운 코드로 변경. 인터페이스를 통한 DI

    • 파라미터를 인터페이스를 받게 하고 상황에 따른 구현클래스를 주입
    • 꼭 스프링 컨테이너를 통한 DI가 존재하는 것이 아니다.

불변객체

  • immutable vs mutable
    • mutable 객체의 단점

      • 버그 발생확률이 높다.
        • ex) Positive 클래스(양수)의 값인 int number 멤버변수가 있는데, minusOne() 이라는 함수로 계속 마이너스 하게 되면 number값이 결국 음수를 갖게 되는 경우가 있다.
        public class Positive {
            private int number;
        
            public Positive(int number) {
                if (number < 0) {
                    // 에러 발생
                }
                this.number = number;
            }
        
            public void minusOne() {
                this.number--;
            }
        
            ..(중략)..
        }
        • 코드가 많아지면 어디선가(누군가가) 그 값을 바꾸거나 하면 버그 발생 확률이 높다.
    • 가능한 immutable 객체로 생성하는 것이 좋다.

    • 극단적이긴 하지만 앞으로의 객체들은 다 immutable 로 변경하는 것이 좋다.

      • 새로운 객체를 생성해서 반환한다.
      public class Positive {
          private final int number;
      
          public Positive(int number) {
              if (number < 0) {
                  // 에러 발생
              }
              this.number = number;
          }
      
          public Positive minusOne() {
              return new Positive(number - 1);
      }
      • 이렇게 되면 Positive 클래스를 사용하는 클래스도 불변으로 만들어야 한다. 만약 Positive를 사용하는 Car 객체도 위치가 이동할 때마다 새로운 위치값을 가지는 새로운 new Car()를 반환해야 한다.
    • immutable의 단점

      • 매번 객체를 생성해야 한다. 인스턴스를 캐싱하면 된다. 캐싱은 프로그래밍을 통해서. 만약 보통 사람들이 최대 100 번 정도만 car를 움직이는 시도를 한다면.
      public class Positive {
          private static Map<Integer, Positive> numberes = new HashMap<>();
      
          static {
              for (int i = 0 ; i < 100 ; i++) {   // 미리 만들어둬서캐싱
                  numbers.put(i, new Positive(i));   
              }
          }
      
          private final int number;
      
          private Positive(int number) { // public -> private 으로 막음.
              if (number < 0) {
                  // 에러
              }
              this.number = number;
          }
      
          public static Positive of(int number) { // 정적 팩토리 메서드
              Positive positive = numbers.get(number);
              if (positive == null) {
                  Positive newPositive = new Positive(number);
                  numbers.put(number, newPositive);
                  return newPositive;
              }
              return positive;
          }
      }
      • 캐싱에 대한 단위테스트
      @Test
      void cache() {
          assertThat(Positive.of(3) == Positive.of(3)).isTrue();  // 모든 번호에 대해서 다해볼 필요는 없다.
      }
      
      • 정적 팩토리 메서드는 특별한 경우 말고는 너무 남발하는 것은 좋지 않다.

Service Layer 에 로직을 넣으면 안된다.

  • service Layer의 단점

    • TDD로 단위테스트 하기 힘들다
    • 객체 지향 설계가 안된다.
  • service Layer의 유일한 3가지 역할

    1. 도메인 객체 로딩하는 역할(DB 데이터 + 비즈니스 로직을 위해)
    2. 도메인 객체에게 메시지를 보내는 역할
    3. 도메인 객체의 바뀐 상태값을 데이터베이스에 반영하는 역할
  • 서비스에 있는 비즈니스 로직을 도메인으로 옮기는 것만 해도 정말 크다. 제일 큰 부분

    • 도메인 객체가 db 테이블과 매칭되는 상황이라면 로직까지 들어갔을때 규모가 너무 커짐
      • 이 때 클래스를 분리한다(일급 컬렉션을 적용한다거나 등). N개의 클래스로 나뉘면서 많은 인스턴스 변수들이 다 따라가게 됨
    • 서비스 비즈니스 로직 -> 도메인으로 옮긴 후, 리팩토링은 계속 도메인 안에서 이루어져야 한다. 서비스 층에 있는 것들을 그 자체로 꼐속 리팩토링 하는 것은 의미가 없다.
    • 즉, 서비스 레이어의 도메인 로직을 각 도메인 객체로 옮기고 테스트 하기 쉬운코드와 어려운 코드를 분리해서 지속적으로 리팩토링하자!
    • 두 개 이상의 도메인 객체가 필요한 로직은 도메인 레이어에 서비스를 만들고 서비스 레이어에서 호출해서 처리
      • 도메인 서비스!! 도메인 서비스는 그냥 서비스(service layer)와는 다르다.
    • 우선 로직의 복잡도가 낮은 서비스를 선정해서 도메인 객체로 이동하면서 TDD 를 시행한다.
  • 로직의 복잡도가 높으면 높을수록 비즈니스 로직을 도메인에게 넘겨라

  • 서비스에서의 비즈니스 로직을 최대한 도메인으로 넘기기

도메인 객체와 엔티티

  • 도메인 객체를 먼저 설계해야 한다. 보통은 도메인과 테이블이 1:1 매핑이 되는 경우가 많은데 잘못된 것

    • 제대로 된 설계는 데이터베이스 테이블 1개당 N개의 객체다.
  • 선 도메인 객체 설계 -> 후 DB 적용 개발 방식

    • 기능 요구사항이 명확하지 않거나 변경이 많은 시점
      • 새로운 기능 개발을 시작하는 시점이 유리
    • 객체 설계 변경에 대한 부담이 적어 지속적인 리팩터링이 가능함
    • 요구사항 분석, 도메인 로직 구현, 단위 테스트에 집중할 수 있음
    • 경험이 쌓이면서 DB 적용을 고려한 객체 설계가 가능해짐
  • 도메인 객체 설계할 때는 후에 primary key가 될 수 있는 변수를 미리 선언하지 않는다. 추후에 필요할 때 하면 된다.

  • 서비스 레이어 말고 도매인 객체에서 DB에 접근해도 괜찮은가?

    • 추천하지 않는다. 하지 마라
    • 하지 않으면서 얼마든지 TDD, DDD 가능하다
  • 도메인에 모든 비즈니스 로직을 다 넣고 TDD 개발을 해라! -> 이것은 무조건 정답

    • 단점은 없다.
  • 도메인 객체와 entity 를 분리하는 것이 맞나?

    • 꼭 정답은 없다. 엄격한 원칙주의자들은 분리해야 된다고 하긴 하는데 어느정도 실용적인 면을 생각안할 수가 없다.
  • 좋은 객체는 내가 가지고 있는 멤버변수가 대부분의 public 변수에 쓰일 때.

  • DB적용할 때 고려사항

    • 객체를 테이블과 어떻게 매핑할 것인가?
      • n개의 객체를 1개의 테이블로 매핑하는 것이 일반적이다.
    • 객체(Object) 간의 의존 관계를 어떻게 유지할 것인가?
      • DB 테이블을 고려하지 않고 로직을 구현할 경우 객체로 의존 관계를 유지한다.
      • DB 테이블을 고려할 경우 객체로 유지할 수 있는 부분과 값(예로 들어 primary key) 으로 유지할 부분을 식별해야 한다.
    • 테이블을 설계하면서 추가되는 칼럼(pk, fk, 데이터 생성/수정 시간 등) 과 역정규할 데이터가 있는지 확인한다.
  • 로직의 복잡도에 맞게 도메인 객체의 크기, 범위가 달라진다.

  • TDD는 DB에 의존하는 부분과 비즈니스 로직을 분리해야 한다.

    • 그 후에 비즈니스 로직에 집중해서 단위테스트를 많이 해야한다.
  • 아래 2가지를 병행해서 하면 된다.

    1. Session : 테이블 매핑 + 도메인 로직 담당 (주로 이렇게!!)
      • 매핑도 도메인 역할을 하는 객체들은 다 jpa에서 @Embeddable 로 연결한다.
    2. Session2 : 테이블 매핑 (수강신청에 대한 도메인 로직 수행은 Enrollment 객체에게 위임).

TDD하면서 리팩토링 방법

  • 리팩토링 할 때는 메서드의 시그니처(정의 부분) 는 최대한 안 건드리고 해야 한다. 왜냐하면 현업에서 수많은 곳에서 이미 그 메서드를 사용하고 있으므로 쉽게 변경하면 안된다. 시그니처 안 바꾸고 테스트 만들기.

    • 함수명을 동일하게 해서 as-is 와 to-be를 변경하게 한다.
    • 기존의 테스트 부분 등에서 to-be 함수로 변경해서 바로 테스트해본다.
    • 이렇게 하면 중간에 작업이 끊겨도 일단 배포하고, 틈날 때마다 계속한다. 그래도 괜찮다. 왜냐하면 as-is와 to-be 코드 모두 현재 동작중이므로, 나는 만약 10개 중에 4개만 완료한 상태면 급하게 HotFix 해서 내보내도 된다.
    • 짬을 내서 리팩토링 하는 것인데 한번에 모든 것을 다 리팩토링 하려고 하면, 더 우선순위가 높은 것이 나와서 원복 하고 다시 반복... 이것은 너무 힘들다.
  • 안전한 리팩토링

    • 과도기적인 리팩토링
    • 함수1를 바로 바꾸면 컴파일 에러가 난다. 그래서 함수1을 그대로 복붙해서 함수2로 변경해서 만든 후, 함수1을 사용하는 곳에 가서 함수2로 변경해서 테스트하며 하나씩 바꿔나간다. 그래서 함수1을 사용하는 모든 곳에서 함수2로 변경한 후 테스트가 다 성공하면 함수1을 삭제. 함수2 메서드명 -> 함수1로 변경
    • 만약에 함수1을 사용하는 곳이 10곳인데 한 6군데만 바꾸는 중에 시간이 없다면?
      • 함수2로 바꾸지 못한 함수1의 사용 4곳은 그대로 둔다.
      • 이것이 바로 공존상태.
  • 객체, 테스트 중심으로 코드를 작성한다면 Controller, service, Repository에 대한 리팩토링은 우선순위가 매우 낮다. 도메인 객체에 대한 지속적인 리팩토링 & 단위테스트가 중요하다. 도에인 객체에 대한 인터페이스 추출 등. 객체지향 설계도 도메인 객체에 먼저 집중한 후 Controller, Service, Repository로 넘어간다.

  • 가장 먼저 해야할 일

    • Service Layer에 단위 테스트를 추가한 후 비지니스 로직을 도메인 객체로 이동하는 리팩토링
    • Service layer에 비즈니스 로직이 있으면 TDD로 구현하는 것이 많이 어렵다.

private 메서드 테스트 방법

  • private 메서드 안에 테스트할만한 것들이 꽤 있다면?

    • 먼저 다른 클래스로 분리할 수 있진 않을까 고민해본다.
  • 해당 private 메서드를 포함하고 있는 public 메서드를 통해서 테스트를 하는 것이 맞다. public 메서드를 테스트하면 속해있는 private 메서드가 자동으로 테스트가 될 수 있도록. private 테스트를 하기 위해서 public 으로 변경하는 것은 좋지 않다.

    • 그리고 private 메서드를 계속 테스트해보고 싶은 경우에는 해당 경우를 별도의 enum이나 클래스로 뺄 수 있는지 고민해본다. 보통 테스트코드 해보고 싶은 private 메서드 안에서 여러가지 상수들이 많은 경우가 있는데 그것을 enum으로 정의해보면 더 간결해진다. 그리고 별도의 클래스로 뺀다면 해당하는 메서드는 public 으로 열리게 된다.
    • Enum으로 변경하게 되면 해당하는 값을 제한할 수 있다(예를 들어 로또 1등 ~ 6등까지 있으면 각각에 해당하는 금액까지.)

함수형 프로그래밍

  • 자가 개발자로서 함수형 프로그래밍을 대하는 자세

    1. 프로그래밍이 기본 틀은 OOP 기반.
    2. 메소드 내부 구현은 FP(Functional Programming)를 지향.
    3. 객체의 상태 관리는 immutable object를 지향.
  • stream을 쓰게 되면 indent 줄일 수 있다.

    • 그런데 바로 쓰지 말고 최대한 함수분리 등을 통해 indent 없애고 그 후에 stream 쓰기
    • 바로 stream 쓰면 분리 연습이 잘 안되므로, 최대한 제일 마지막에 쓰기

기타

  • 기능목록을 잘 작성하면 TDD하기 쉽다.

    • 난이도가 낮은것부터 작성
  • 테스트를 위한 임의의 메서드를 절대 만들면 된다. 테스트를 가능하게 하기 위한 설계를 변경해야 한다.

    • 테스트를 위한 메서드는 안되지만 생성자는 괜찮다.
  • 싱글톤 패턴을 쓰지마라. 구현하지 마라. 남용하지 마라.

    • 객체지향의 안티 패턴이다.
    • 안 좋은 패턴이다.
    • 테스트하기 너무 힘들다.
    • 별도의 생성자 오버로딩이 불가능한데 그럴때는 어떻게 해결해야할까요?
    • 스프링에서도 싱글톤은 캐시처럼 사용하는 느낌이다.
  • 상태값(멤버변수) 이 없을 경우에는 static 을 그대로 써도 된다.

  • Object Graph 는 Object 의존관계를 말함.

  • 테스트할 때의 값은 최대한 경계값으로 하면 좋다.

    • 예를 들어 4이상이면 통과라고 한다면, 4일 경우와 3일 경우 등으로 구분해서 값 넣기
  • 테스트는 항상 성공해야 한다.

  • 객체지향적으로 사용하게 되면 값을 꺼내기보다 클래스끼리 비교하기 때문에 hashCode()와 equals()를 자주하게 된다. 멤버변수가 1개밖에 없을 때는 equals() 메서드가 더 명확. 인스턴스 변수가 많으면 equals() 메서드는 더 혼란.

  • Mock 프레임워크를 사용안하면서 하는 사람이 진짜 TDD 고수

    • 제일 마지막에 Mock 프레임워크를 사용하라. 최대한 사용을 지양(켄트 벡)