자바 개발자들은 상태를 가지는 객체를 자주 만든다. 그런데 이 객체를 객체스럽게 사용하지 못하는 경우를 종종 본다.
오늘 리뷰할 코드는 초보 개발자 뿐만 아니라 현업에 있는 개발자들 또한 이와 같이 구현하는 경우를 종종 본다.
또 다시 자동차 경주 게임의 구현 코드에서 시작해 보자.
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
위 댓글의 예에 대해 말씀드리면, Random 개체 때문에 Car 개체의 테스트가 어렵다면 Random의 테스트 대역(test double, 위 예에서는 TestableRandom 클래스)을 사용하는 것보다 Car 개체에서 Random 개체의 종속성을 제거하는 것이 우선 고려사항이 아닐까 싶습니다. 테스트 하기 어렵다는 것은, 항상 그런 것은 아니지만, 디자인 개선의 여지가 있다는 신호인 경우가 많습니다. Car 개체는 position을 증가시키는 proceed() 메서드를 가지는 것으로 책임을 제한하고 임의의 자극을 만드는 책임(게임 환경)과 이 자극에 따라 Car를 이동시키는(proceed() 메서드를 호출하는) 책임(운전자)을 분배하는 것을 고려해 볼 수 있겠습니다.
그리고 읽기접근자(getter)와 쓰기접근자(setter) 작성을 정말 필요한 순간까지 미루자고 말씀하셨는데 테스트 대역의 작성 역시 정말 필요한 순간까지 미루는 연습이 필요하다고 생각합니다.
3개의 의견 from SLiPP
학습하는 친구 중의 의견
Car 클래스는 아래와 같은 메소드를 가지면 어떨까요?
Random 값을 처리하는 코드는 어떻게 단위 테스트할 수 있을까? 테스트는 다음과 같이 구현
이와 같이 구현하는 것은 어떨까요? 위와 같이 테스트용 클래스를 별도록 만들기 보다 Random을 상속하는 익명 클래스를 두면 더 좋을 것 같네요.
위 댓글의 예에 대해 말씀드리면, Random 개체 때문에 Car 개체의 테스트가 어렵다면 Random의 테스트 대역(test double, 위 예에서는 TestableRandom 클래스)을 사용하는 것보다 Car 개체에서 Random 개체의 종속성을 제거하는 것이 우선 고려사항이 아닐까 싶습니다. 테스트 하기 어렵다는 것은, 항상 그런 것은 아니지만, 디자인 개선의 여지가 있다는 신호인 경우가 많습니다. Car 개체는 position을 증가시키는 proceed() 메서드를 가지는 것으로 책임을 제한하고 임의의 자극을 만드는 책임(게임 환경)과 이 자극에 따라 Car를 이동시키는(proceed() 메서드를 호출하는) 책임(운전자)을 분배하는 것을 고려해 볼 수 있겠습니다.
그리고 읽기접근자(getter)와 쓰기접근자(setter) 작성을 정말 필요한 순간까지 미루자고 말씀하셨는데 테스트 대역의 작성 역시 정말 필요한 순간까지 미루는 연습이 필요하다고 생각합니다.
Car 클래스에서 제어할수없는 random 값(movePoint)을 가지기 보다는 전달받도록 변경하면 더 좋을것 같습니다.
대신 Car 클래스를 관리하는 주체 (잘몰라서 여기선 Driver라고 예시를 들겠습니다.)에서 생성해서 주는건 어떨까요?
이렇게 되면 테스트 메소드에선 createMovePoint를 Mocking 하면 될것 같습니다.
의견을 남기기 위해서는 SLiPP 계정이 필요합니다.
안심하세요! 회원가입/로그인 후에도 작성하시던 내용은 안전하게 보존됩니다.
SLiPP 계정으로 로그인하세요.
또는, SNS 계정으로 로그인하세요.