📜 제목으로 보기

final 키워드로 값의 변경을 막아라

값은 final로 재할당금지 -> 재사용 금지

image-20220221000810476

  • main 메소드 template 인 psvm의 인자에도 final을 붙혀준다.

    image-20220220230643712

  • 메서드 내부에서 코드 중간에서 가변 변수를 수정할 때, 위에 재할당 등의 수정이 되었었나 확인해야한다.
    • final로 재할당(재사용)이 불가능한 불변 값사용하다가, 변수의 재사용이 필요할 경우 새 변수를 만들어서 쓰자.
  • 모든 지역변수, 인자에 final을 붙이자.

    image-20220221090040904

final 객체의 내부 속성변화 메서드 -> 변한 속성으로 만든 새 객체로 응답

  • 객체의 경우는 final로는 완벽한 불변을 못만든다.

    • final 값와 달리 final 객체는 메소드 등에 의해 내부가 변할 수 있다.(포인터의 포인터에서 껍데기 포인터1)
  • 객체를 불변으로 만든 법상태변화를 일으키는 기능들은 새로운 객체를 반환하도록 정의하자.

    1. 상태변화 시켰던 메서드의 void returnType에 해당 객체를 응답(return)하도록 수정

      public void move() {
          this.position++;
      }
              
      public Car move() {
          this.position++;
      }
      
    2. 변화된 상태로 new생성자()로 호출하여 -> 새로운 객체를 반환시킨다.

       public Car move() {
           return new Car(this.position + 1);
       }
              
              
              
       public Car(final int position) {
           this.position = position;
       }
      

객체, 컬렉션(껍데기포인터1)등은 응답시 copy해서 응답하라

my) 포인터1의 포인터2에 의해 변경될 수 있으니, 껍데기 포인터1 자체를 copy해서 응답해라

컨밴션 중 하나가 된다.

  • final을 붙이는 것은 컨벤션이며 팀 안에서는 따르는게 좋을 수 도 있다.
    • 내 생각에 타당하지 않으면 얘기를 꺼내서 바꿀 수 있다.
  • 컨벤션이라면, 비효율적이라도 그대로 가는 경우가 많다.
    • 디자인 패턴 역시, 효율적이거나 좋은 코드라서 뿐만 아니라, 커뮤니케이션을 줄이는 컨밴션이기 때문에 좋기도 하다.

참고 글

객체를 객체스럽게 사용해라

  • 아래와 같이 getter와 setter만 있는 객체를 구현하지 않는다. image-20220221090207736
  • setPosition()같은 setter 대신 move()의 역할로 분리해야한다.
  • getter()는 안쓰는게 좋은데, setter()는 100% 확정적으로 안쓰는게 좋다.
    • setter() == 생성자 == pushed 로 외부에서 주입은 X
    • 특히 불변객체의 경우는 속성 변화 -> 새 객체로 응답해야하니, setter란 개념이 없다.
  • view에서 사용하는 getter()는 필수적이다.
    • view에서 getter()안쓰기 위해 toString() 쓰는 것은… 좀 아니다.
    • getter()쓰지마라라는 말은 도메인에서 getter() 쓰는 대신, 객체에 메세지를 던지면서 협력하라는 뜻이다.
    • 객체를 직접 사용하지말고, View에서는 dto로 값만 꺼내써라
  • Q. view에서 cars.get(0).getPosition()은 가능한가요?

    • my) 디미터법칙 위반이다 -> 책임을 2번째 놈에게 위임해야됌

    • index 0 은 무엇을 말하는지 모른다.

    • 가져오려는 이유가 무엇인지 부터 물어보고 싶다.

      • 우승자를 구하려는 이유라면 메세지를 던지는 식으로 바꿔보자.
        • cars.getWinners()
    • .get + .get이 이어지는 경우가 있는데 의심을 해봐야한다.

      • 점 1개로 줄이도록 해보자.

          cars.getFirst().getPosition();
                    
          cars.getFirstPosition();
        
      • 첫번째 포지션 -> 의미가 애매하다 -> 위너로 바꾸자.

          cars.getWinners();
        

객체.getXXX()대신 new XXX( 객체 )를 던지는 클래스로 만드는 방법도

  • **getter()대신 객체를 생성자로 던져 class를 만들어볼까? **

      new Winner(cars);
    

객체를 던졌어도, 객체.setter() 등의 주입 대신 역할위임을 하자.

public class Racing {

    ...

    public int run(Car car) {
        int num = random.nextInt(11);
        int position = car.getPosition();
    
        if (num >= 4) {
            position++;
        }
    
        car.setPosition(position);
        return position;
    }
}
  • car.setPosition(position);대신에 car 내부로 역할이 위임되어서 내부에서 했으면 좋겠다.

인변의 접근제한자는 private으로 고정 -> 객체상태에 대한 접근은 제한무조건>직접>

  • getter()도 지금 허용할까말깐데.. 인변에 접근은 절대 안시킨다.

image-20220221093035905

  • 그렇다면 public, protected는 왜 있는 걸까?
    • 과거에는 의도가 있어서 만들었는데, 요즘에는 필요없다고 느껴졌을 수 가 있음.
    • 과거에는 왜 여러 접근제한자를 만들었을까?
    • public으로 열여있으면, getter/setter 동시허용이나 마찬가지다.
      • 어디서든 접근하면 -> 불안해야 한다.
      • 이 역할만 하면 좋겠다.
      • 변수를 public으로 열어두면, 변했는지 안했는지 직접 보고 확인해야하는 비용이 든다.
      • 변수를 public으로 열어두지말고, method를 열어주어 캡슐화된 속성변화를 만들어내자.

(내부)랜덤요소는 단위 테스트하기 힘들다.

  • 내부에 random 테스트하기 힘들다.

    20220728030556

밖에서 받아오는 메서드로 변경한다.

  1. 테스트하기 힘든 내부 랜덤은 일단 밖에서 주어지는 파라미터로 변경 한다.

    • 파라미터 추출 단축키 ctrl+shit+P
     public void move() {
         if (getRandomNo() >= 4) {
             this.position++;
         }
     }
        
        
     car.move();
    
     public void move(final int number) {
         if (number >= 4) {
             this.position++;
         }
     }
        
     car.move(car.getRandomNo());
     // 자동으로 메서드의 인자도 바뀐다.
    
  2. 문제점: 밖에서 받아오는 랜덤이

    • 단일객체.move( 밖 )
    • 일급컬렉션.move( 밖 )
      • cars.forEach( car -> car.move( 밖 ) ) -> ..
    • 일급컬렉을 사용하는 클래스에서도 ( 밖 )
    • 어디 밖까지 빼서 받아올지가 끝이 없다 -> 적절한 경계를 잡아준다.
  3. 질문:

    • car.move ( 밖 )도 테스트 했는데, cars.move ( 밖 ) 도 테스트해야할까?
      • 마음의 평화가 중요함. 자기가 생각해 봐야함.
    • 리뷰어에게는 설계가 담근 readme의 링크와 같이 보낸다.

받아오는 인자를 전략메소드를 가진 인터페이스로 변경하여 -> 랜덤외에 구상 전략객체들도 입력가능하게 만든다.

public interface MovableStrategy {
    boolean isMove();
}

인변 수는 최소화한다.

  • 최대 2개 정도로…

  • 기존 인변으로 만들어낼 수 있는 것은, 변수로 선언하지 않는다.

      public class RacingGame {
          private List<Car> cars;
          private List<String> winners;
    
  • my) 인변으로 둔다 -> 바깥에서(or default값으로) 받아와 알고있고, 의존해야하는 것

    • 그게 아니라면, 시간이 좀 걸리더라도, 기존 인변들도 만든다.
    • 관리포인트를 늘리면 좋은가? 그냥 그때그때 실시간으로 만드는게 좋은 것 같다.
    • 현대에는 메모리/컴퓨팅 비용성능 «< 인력의 유지보수성
    • DB를 이용하는 시점에서 변수 -> 성능을 위한 캐싱을 하기도 한다.

### setter는 사용을 자제해라 -> 생성자에서 초기화 후 더이상 변경안한다.

  • 인스턴스 변수는 생성자 초기화 후불변을 유지하기 위해 setter는 사용을 자제한다.
  • setter의 문제점은?
    • 역할을 하지 못하게 된다.
    • 어디서든 변경되어 -> 내 의도와 다르게 동작한다.
    • 유지보수성이 떨어진다.

비즈니스 로직과 UI 로직의 분리

  • View관련 코드는 도메인에 넣지말라는 소리

    • 90%는 도메인에 View관련 코드를 넣었었다.

        public class Car {
            private int position;
              
            ...
              
            private void print(int position) {
                StringBuilder sb = new StringBuilder();
                for (int i = 0; i < position; i++) {
                      sb.append("-");
                }
                System.out.println(sb.toString());
            }
        }
      
  • 리뷰를 받으면서 점점 분리한다.

  • 왜 분리할까? 그냥 출력하도록 도메인에서 메서드 만들면 되는거 아닌가?

    • getter쓰지말라고 해서 -> 객체.역할부여()로 출력역할 만들었더니
    • 역할부여는 도메인 && getter없이 하는게 맞지만, 변경이 자주 일어나는 view로직을 예외다.
  • View관련 로직

    • 출력관련 역할부여(책임)를 도메인에서 안시킨다. 아예 정의하지말고 도메인은 객체만 넘겨라.

      • 출력 책임은 객체가 가지게 하지마라
      • <변경 빈번> UI변경 -> <변경없어야할 소중한, 중요도가 높은> 도메인에 여파를 일으키면 큰일이 나기 때문
    • dto로 getter로 값만 꺼내쓰게 할 것이다

Collection 활용 로직 처리

  • Car 목록에서 최종 우승자를 구하는 로직이다. 이 코드를 Collection 기능을 사용해 어떻게 리팩터링할 것인가?

      public class ResultView {
          private Cars cars = null;
        
          private String getTopRankedCar(List<Car> carList) {
              String topCarString = "";
              cars = new Cars(carList);
              int maxPosition = getMaxPosition(carList);
        
              for (int i = 0; i < carList.size(); i++) {
                  if (cars.getPosition(i) == maxPosition) {
                      topCarString += cars.getCarName(i) + ", ";
                  }
              }
              return topCarString.substring(0, topCarString.length() - 2);
          }
          
          private int getMaxPosition(List<Car> carList) {
              int maxPosition = 0;
              cars = new Cars(carList);
        
              for (int i = 0; i < carList.size(); i++) {
                  if (maxPosition < cars.getPosition(i)) {
                      maxPosition = cars.getPosition(i);
                  }
              }
              return maxPosition;
          }
      }
    

기본 api를 이용해서 다짜지 말자

Collection을 활용해 로직을 구현할 때 직접 구현하려 하지 말고 먼저 Collection API를 통해 해결할 수 있는 방법이 있는지 찾는다. 방법을 찾았는데 해결 방법을 찾지 못하는 경우만 직접 구현한다.

image-20220221104440307

경계값을 테스트한다.

  • 다 하는게 아니라 경계값만 테스트

    • 모든 테스트 X
      @Test
      public void 랜덤숫자가_4이상일때만_움직인다() {
          Car car = new Car();
        
          assertFalse(car.shouldMove(0));
          assertFalse(car.shouldMove(1));
          assertFalse(car.shouldMove(2));
          assertFalse(car.shouldMove(3));
        
          assertTrue(car.shouldMove(4));
          assertTrue(car.shouldMove(5));
          assertTrue(car.shouldMove(6));
          assertTrue(car.shouldMove(7));
          assertTrue(car.shouldMove(8));
          assertTrue(car.shouldMove(9));
      }
    

테스트 픽스처 생성

  • 테스트를 반복적으로 수행할 수 있게 도와주고 매번 동일한 결과를 얻을 수 있게 도와주는 ‘기반이 되는 상태나 환경’을 의미한다. 여러 테스트에서 공용으로 사용할 수 있는 테스트 픽스처는 테스트의 인스턴스 변수 혹은 별도의 클래스에 모아 본다.

    • 중복제거를 위해 하면 된다.
    • junit의 @beforEach를 사용하면 된다. 테스트 픽스쳐의 일종이다.
      public class RacingGameTest {
          private final String[] testNames = {"a", "b", "c"};
          private final int testPosition = 5;
          private final String resultSamePositionString = ", b";
          private final String resultWinnersString = "a, b";
          RacingGame racingGame;
          private final Car firstWinner = new Car(testPosition, "a");
          private Car secondWinner;
          private final List<Car> cars = new ArrayList<Car>();
        
          ...
        
      }
    

과정 실습

  • 추후 미션이 진행 되면, 복잡해지고 객체가 많아질 경우, 코드를 분리해야됨.

      @Test
          void move() {
              final Car car = new Car("재성");
          }
    
  • 비슷한 환경의 테스트를 복사해서 case를 나눈다고 가정하자.

      @Test
      void move_go() {
          final Car car = new Car("재성");
      }
        
      @Test
      void move_stop() {
          final Car car = new Car("재성");
      }
    
  • 객체의 생성이 반복된다면, Generate > Setup method를 선택해서 @BeforeEach를 생성하자.

      @BeforeEach
      void setUp() {
        
      }
        
      @Test
      void move_go() {
          final Car car = new Car("재성");
      }
        
      @Test
      void move_stop() {
          final Car car = new Car("재성");
      }
    
  • 공통되는 객체생성코드를 beforeEach로 뺀다

      @BeforeEach
      void setUp() {
          final Car car = new Car("재성");
      }
        
      @Test
      void move_go() {
      }
        
      @Test
      void move_stop() {
      }
    
  • ctrl+.(show context action)을 눌러서 split declation and assignment를 통해

    • 변수 선언은 변수재활용을 위해 클래스변수
      • 클래스변수에 넣고 매번 초기화한다 -> 변수 재활용한다 -> 변수 재할당한다 -> final은 안된다.
    • 할당만 beforeEach에서 매 테스트마다 새로운 것으로 분리한다.
      @BeforeEach
      void setUp() {
          final Car car;
          car = new Car("재성");
      }
    
      class ApplicationTest {
          //final Car car;  // 매 테스트마다의 재활용 = 재할당
          Car car;
        
          @BeforeEach
          void setUp() {
              car = new Car("재성");
          }
    
  • 픽스쳐가 많아지면, 공통이 맞는지 확인한다.

특정상태를 만들어주기 위한 반복코드 -> 생성자로 상태 바로 만들기

  • 과도하게 반복해서 만든다?

      public class RacingGameResultTest {
          @Test
          public void check_ranking_if_correct() {
              List<Car> cars = new ArrayList<>();
              Car car1 = new Car("pobi");
              Car car2 = new Car("crong");
              Car car3 = new Car("honux");
        
              car1.move();
              car1.move();
              car2.move();
              car2.move();
              car2.move();
              car3.move();
                
              cars.add(car1);
              cars.add(car2);
              cars.add(car3);
                
              ...
        
          }
      }
        
    
    • 단순반복의 경우, 반복문+메서드 호출로 해결할 수도 있지만, 그렇지 않은 경우가 많다.
    • 상태를 만들기 위해 복잡한 과정이 많은 경우, 상태를 바로 만들어주는 생성자를 만들 수 있다.
      • 여기선, move()테스트가 아니라 우승자를 찾는게 목적이기 때문에, 목적 달성을 위해, 다른 것은 바로 상태를 만드는 생성자를 만들수 도 있음.
      • 현재는 move()호출방식에 따라 테스트가 의존하여, 변하면 테스트코드가 깨질 수 도 있다.
      • 테스트를 위해 생성자를 만들지만, 그게 진짜 테스트만을 위한 코드인지 생각해봐야한다.
      public Car(final String name, final int position) {
          this.name = name;
          this.position = position;
      }
        
        
        
      List<Car> cars = new ArrayList<>();
        
      //        Car car1 = new Car("pobi");
      //        Car car2 = new Car("crong");
      //        Car car3 = new Car("honux");
      Car car1 = new Car("pobi", 2);
      Car car2 = new Car("crong", 3);
      Car car3 = new Car("honux", 1);
        
      //        car1.move();
      //        car1.move();
      //        car2.move();
      //        car2.move();
      //        car2.move();
      //        car3.move();
        
      cars.add(car1);
      cars.add(car2);
      cars.add(car3);
    
    • 이 코드가 프로덕션 코드에서 사용되지 않더라도, 객체관점+객체역할에서 봤을 때, 테스트를 위한 기능이 아닐 수 있다.
      • 객체 관점에서 자연스러우면 괜찮다.
      • position default값을 지정해주는 관점에서 괜찮다.
    • 테스트만을 위한 메서드추가는 문제가 더 큰데, 생성자는 객체를 만드는 방법일 뿐이므로 생성자는 많아도 괜찮다.
      • 충분히 납득이 가능하다.
  • 생성자는 여러개 만들면, 인자 많은 것을 주생성자로 취급하고 인자 적은 생성자 -> 위쪽에 위치하고 default값을 this에 넣어주는 부생성자

      // 부 생성자는 주 생성자(name, position)보다 더 앞에 위치하며, 뒤쪽의 주 생성자를 this( , )로 사용한다.
      public Car(final String name) {
          this(name, 0);
      }
        
      public Car(final String name, final int position) {
          this.name = name;
          this.position = position;
      }
    
  • 생성자가 너무 많아지거나, 같은 시그니쳐 or 이름을 주고 싶을 때 -> 정팩메로 분리 고려

getter없이 구현 가능?

  • setter/getter 메서드를 사용하지 말라는 것은 핵심 비지니스 로직을 구현하는 도메인 객체를 의미한다. 도메인 Layer -> View Layer, View Layer -> 도메인 Layer로 데이터를 전달할 때 사용하는 DTO(data transfer object)의 경우 setter/getter를 허용한다.

  • 아예 없이 toString으로 구현하는 분들도 있지만, 도메인 Layer에서 사용하는 getter를 만들지마라 == 비즈니스 로직를 위한 getter를 이용하지마라

    • 도메인 대신 DTO 구현시 getter/setter는 허용한다.

    • DTO라도 setter생성자에서 초기화하는 것으로 대신한다.
    • view에서는 열어야한다.
  • 접근제한자는 최소한으로 열고 시작한다.

  • getter로 받아와서 하지말고, 역할을 부여해서 시키는 것 -> 그래야 협력한다.

객체안에서 toDTO를 만들지마라

  • 내부에서 toDto()를 만든다?
    • 모델에서 DTO를 알면 왜 안좋을까?
    • DTO는 뷰에 종속적인 객체 -> view처럼 변경이 많다 -> 도메인에 영향을 줌
    • 객체 안에서 DTO를 생성하지마라
    • 객체는 밥줄이라 아껴야하고, DTO는 막쓴다.

테스트를 위한 코드는 구현코드에 분리

  • 테스트용 편의 메서드를 구현 코드에서 구현하지마라

생성자로 주입하는 건 괜찮고, setter는 안좋냐?

  • 똑같은 내부 코드(행위)보다는 역할이나 시점을 봐야한다.
  • setter가 하나라도 존재하면 내부 상태변화 기능.move()을 따로 가지는 객체지만, 객체를 믿을 수 없게 된다.
    • 기능으로 내부 변하는 것도 좀 그런데… 언제든지 변화시키는 믿을 수 없는 객체가 된다.