객체를 객체스럽게 사용하도록 리팩토링해라.

2017-10-25 10:25

자바 개발자들은 상태를 가지는 객체를 자주 만든다. 그런데 이 객체를 객체스럽게 사용하지 못하는 경우를 종종 본다.

오늘 리뷰할 코드는 초보 개발자 뿐만 아니라 현업에 있는 개발자들 또한 이와 같이 구현하는 경우를 종종 본다.

또 다시 자동차 경주 게임의 구현 코드에서 시작해 보자.

public class Car {
  private String name;
  
  private int position = 1;
  
  public Car(String name){
    this.name = name;
  }

  public int getPosition() {
    return position;
  }

  public void setPosition(int position) {
    this.position = position;
  }

  public String getName() {
    return name;
  }
}

위와 같이 Car 클래스를 추가한 후 name과 position을 상태 값으로 가지는 객체를 추가했다. 그런데 이 객체는 로직에 대한 구현은 하나도 없고, name과 position에 대한 setter와 getter 메소드만을 가진다.

그럼 Car를 활용해 로직을 구현하는 코드를 보면 다음과 같기 구현하고 있다.

public class Racing {

  [...]

  public int run(Car car) {
    int num = random.nextInt(11);
    int position = car.getPosition();
    if (num >= 4) {
      position++;
    }
    car.setPosition(position);
    position = car.getPosition();
    return position;
  }
}

어떤 개발자들은 이 작은 코드를 보면서 비웃을지도 모른다. 하지만 수 많은 객체가 협업하는 구조를 구현하다보면 비슷한 방식으로 구현하는 경우를 종종 본다.

  • 상태를 가지는 객체를 추가했다면 객체가 제대로 된 역할을 하도록 구현해야 한다.
  • 객체가 로직을 구현하도록 해야한다.
  • 상태 데이터를 꺼내 로직을 처리하도록 구현하지 말고 객체에 메시지를 보내 일을 하도록 리팩토링한다.

위 같은 원칙에 따라 위 코드를 리팩토링하면 지금 구현 코드보다 훨씬 더 깔끔한 결과물을 얻을 수 있으며, 테스트 또한 쉬워질 수 있다.

그리고 상태 데이터를 가진다고 무조건 setter, getter 메소드를 만드는 습관을 버리자. setter, getter 메소드는 정말 필요한 순간까지 뒤로 미루는 습관을 만들면 좋겠다. 아예 추가하지 않는 연습을 하면 더 좋겠다.

위 샘플 예제는 코드스쿼드 에서 새롭게 진행 중인 마스터즈 코스에서 발췌한 코드입니다. 코드스쿼드의 마스터즈 코스는 코드 리뷰 방식의 개인별 맞춤 학습 방법입니다.

1개의 의견 from FB

BEST 의견 원본위치로↓
2017-10-25 13:55

위 댓글의 예에 대해 말씀드리면, Random 개체 때문에 Car 개체의 테스트가 어렵다면 Random의 테스트 대역(test double, 위 예에서는 TestableRandom 클래스)을 사용하는 것보다 Car 개체에서 Random 개체의 종속성을 제거하는 것이 우선 고려사항이 아닐까 싶습니다. 테스트 하기 어렵다는 것은, 항상 그런 것은 아니지만, 디자인 개선의 여지가 있다는 신호인 경우가 많습니다. Car 개체는 position을 증가시키는 proceed() 메서드를 가지는 것으로 책임을 제한하고 임의의 자극을 만드는 책임(게임 환경)과 이 자극에 따라 Car를 이동시키는(proceed() 메서드를 호출하는) 책임(운전자)을 분배하는 것을 고려해 볼 수 있겠습니다.

그리고 읽기접근자(getter)와 쓰기접근자(setter) 작성을 정말 필요한 순간까지 미루자고 말씀하셨는데 테스트 대역의 작성 역시 정말 필요한 순간까지 미루는 연습이 필요하다고 생각합니다.

3개의 의견 from SLiPP

2017-10-25 11:06

학습하는 친구 중의 의견

Car 클래스는 아래와 같은 메소드를 가지면 어떨까요?

public void run(Random rnd){
  if (rnd.nextInt(10) > 4){
    this.position++;
  }
}

Random 값을 처리하는 코드는 어떻게 단위 테스트할 수 있을까? 테스트는 다음과 같이 구현

public class TestableRandom extends Random{
    @Override
    public int nextInt(10) {
        return 1;
    }
}

이와 같이 구현하는 것은 어떨까요? 위와 같이 테스트용 클래스를 별도록 만들기 보다 Random을 상속하는 익명 클래스를 두면 더 좋을 것 같네요.

    @Test
    public void run_random값_3() throws Exception {
        Car car = new Car();
        car.run(createRandom(3));
    }
    
    @Test
    public void run_random값_4() throws Exception {
        Car car = new Car();
        car.run(createRandom(4));
    }
    
    private Random createRandom(int returnValue) {
        return new Random() {
            public int nextInt(int bound) {
                return returnValue;
            };
        };
    }
2017-10-25 13:55

위 댓글의 예에 대해 말씀드리면, Random 개체 때문에 Car 개체의 테스트가 어렵다면 Random의 테스트 대역(test double, 위 예에서는 TestableRandom 클래스)을 사용하는 것보다 Car 개체에서 Random 개체의 종속성을 제거하는 것이 우선 고려사항이 아닐까 싶습니다. 테스트 하기 어렵다는 것은, 항상 그런 것은 아니지만, 디자인 개선의 여지가 있다는 신호인 경우가 많습니다. Car 개체는 position을 증가시키는 proceed() 메서드를 가지는 것으로 책임을 제한하고 임의의 자극을 만드는 책임(게임 환경)과 이 자극에 따라 Car를 이동시키는(proceed() 메서드를 호출하는) 책임(운전자)을 분배하는 것을 고려해 볼 수 있겠습니다.

그리고 읽기접근자(getter)와 쓰기접근자(setter) 작성을 정말 필요한 순간까지 미루자고 말씀하셨는데 테스트 대역의 작성 역시 정말 필요한 순간까지 미루는 연습이 필요하다고 생각합니다.

2017-10-25 20:13

Car 클래스에서 제어할수없는 random 값(movePoint)을 가지기 보다는 전달받도록 변경하면 더 좋을것 같습니다.

public class Car {
  private String name;
  
  private int position = 1;
  
  public Car(String name){
    this.name = name;
  }

  public void run(int movePoint){
    if (isMove(movePoint)){
      this.position++;
    }
  }
  private boolean isMove(int movePoint) {
    return movePoint > 4;
  } 
}

대신 Car 클래스를 관리하는 주체 (잘몰라서 여기선 Driver라고 예시를 들겠습니다.)에서 생성해서 주는건 어떨까요?

public class Driver {
  public void driving(Car car){
     car.run(createMovePoint());
  } 
  public int createMovePoint(){
     return new Random().nextInt(10);
  }
}

이렇게 되면 테스트 메소드에선 createMovePoint를 Mocking 하면 될것 같습니다.

의견 추가하기