자동차 경주 게임 구현 - 객체를 객체스럽게 리팩토링 하자

PPakSSam·2022년 1월 17일
1
post-thumbnail

먼저 읽으면 좋은 것들

  1. 일급 컬렉션
  2. 원시값과 문자열 포장

순서

  1. 자동차 경주 게임 구현 - Code Convention
  2. 자동차 경주 게임 구현 - 객체를 객체스럽게 리팩토링 하자
  3. 자동차 경주 게임 구현 - 비즈니스 로직과 UI의 분리
  4. 자동차 경주 게임 구현 - 테스트 코드에서의 팁

이 포스트는 자바 플레이그라운드 with TDD, 클린코드를 들었다는 전제하에 쓴 글이며,
수업을 듣지 않았어도 전체 깃헙코드를 보고 어느정도 안다는 전제하에 쓴 글이다.


1. 원시값과 문자열 포장을 활용하자

public class Car {

    private static final int GO_FORWARD_DISTANCE = 1;
    private static final int DEFAULT_CAR_POSITION = 0;

    private CarName name;
    private Engine engine;
    private Position position;

    public Car(String name, Engine engine) {
        this.name = CarName.of(name);
        this.engine = engine;
        this.position = Position.of(DEFAULT_CAR_POSITION);
    }
    
    ...
}    

위의 코드에서 nameposition원시값과 문자열 포장의 예시이다.
원래는 String name, int position으로 작성을 해왔었다.
그런데 이 수업을 듣고 위와 같이 Wrapping Class를 사용하여 작성해보니 다음과 같은 장점들이 있었다.

(1) 책임이 명확해진다.

name, position 모두 검증로직이 필요하다.
그런데 둘 다 원시 타입이라면 검증로직이 모두 Car 객체에 있게된다.
이는 Car객체가 너무 많은 책임을 떠안게 되는 것이다.

그런데 CarName에서 name의 상태를, Position에서 position의 상태를 각각 관리하게 함으로써
Car에서는 nameposition에 대한 검증 로직이 필요없어졌을 뿐만 아니라
nameposition에 대한 비즈니스 로직을 각각의 클래스가 담당함으로써 책임이 명확하게 분리되는 효과를 가지게 되었다.

(2) 유지보수에 도움이 된다.

책임이 명확해진다와 일맥상통하는 이유이다.
만약 name, position이 원시값이었다면 이 둘에 대한 로직이 Car객체에 모두 있게 되고,
그만큼 유지보수는 힘들어진다.

그러나 이 둘을 포장하여 클래스를 만들면 책임이 명확하게 분리된다.
만약 position관련해서 문제가 생기면 Position클래스만 보면 되므로 유지보수에 도움이 된다.

2. 일급컬렉션을 활용하자

public class Cars {

    private final List<Car> cars;

    public Cars(List<Car> cars) {
        this.cars = new ArrayList<>(cars);
    }

    public void move() {
        cars.forEach(Car::move);
    }

    public List<Car> findWinners() {
        return cars.stream()
                .filter(car -> car.isWinner(findMaxPosition()))
                .collect(Collectors.toList());
    }

    private int findMaxPosition() {
        return cars.stream()
                .map(car -> car.getPosition().getValue())
                .max(Integer::compare)
                .orElseThrow(() -> new RuntimeException("No Winner"));
    }

    public List<Car> getCars() {
        return Collections.unmodifiableList(cars);
    }
}

위의 코드는 일급 컬렉션의 예시이다.
일급 컬렉션의 장점은 위의 먼저 읽으면 좋은 것들 중의 일급 컬렉션에서 자세히 써놓았다.

일급 컬렉션 포스트에서 일급 컬렉션의 장점 4가지를 소개했는데 여기서는 그 중 3가지가 포함된다.

  1. 비즈니스에 종속적인 자료구조
  2. 불변
  3. 상태와 행위를 한 곳에서 관리

이 중 불변에 대해서 좀 더 다뤄보고자 한다.
일급 컬렉션 포스트에서 불변의 장점에 대해 다음과 같이 설명한다.

요즘과 같이 소프트웨어 규모가 커지고 있는 상황에서 불변 객체는 아주 중요하다.
각각의 객체들이 절대 값이 바뀔일이 없다는게 보장되면 그만큼 코드를 이해하고 수정하는데 사이드 이펙트가 최소화되기 때문이다.

처음에 Cars객체는 다음 코드처럼 구현했었다.

public class Cars {

    private final List<Car> cars;

    public Cars(List<Car> cars) {
        this.cars = new ArrayList<>(cars);
    }

    public List<Car> getCars() {
        return cars;
    }
}

위의 코드를 언뜻 보면 setter가 없으므로 불변 객체처럼 보일 수 있겠으나 그렇지 않다.
왜냐하면 다음과 같은 것이 가능하기 때문이다.

Cars cars = new Cars(Arrays.asList(new Car(...), new Car(...));
cars.getCars().add(new Car(...)); 

즉 새로운 값을 넣을 수 있으므로 불변 객체가 아니었던 것이다.
그런데 비즈니스 로직상 List<Car>의 값을 꺼내오는 getCars()는 필요한 메소드였다.
그래서 나온 것이 다음의 코드이다.

public List<Car> getCars() {
    return Collections.unmodifiableList(cars);
}

Cars cars = new Cars(Arrays.asList(new Car(...), new Car(...));
cars.getCars().add(new Car(...)); // 예외 발생

3. setter 메소드 사용을 자제하자

가능한 setter메소드를 사용하지 않고 생성자를 사용해 초기화하는 것이 좋다.
값을 꼭 변경해야 하는 변수와 관련된 것만 setter메소드가 아닌 의미있는 메소드를 만들고,
나머지 변수들은 setter 메소드를 만들지 않는다.
이렇게 해야 불변 객체의 필요성처럼 사이드 이펙트가 줄어든다.

4. 상태 데이터를 get하지 말고 메시지를 보내라

이번에 수업을 들으면서 새롭게 얻은 지식 중 하나이다.
수업에서는 다음과 같이 가르친다.

  • 객체의 데이터를 꺼내 로직을 구현하면 중복 코드가 발생한다.
  • 객체에 메시지를 보내 상태 데이터를 가지는 객체가 일하도록 하라.

무슨말인가 싶었는데 다음의 코드를 보며 설명을 하겠다.

public class Car {

    private Position position;
    
    public void move() {
        this.position.setPosition(this.position.getPosition() + 1);
    }
}

보통 클래스를 만들면 getter, setter 메소드는 무의식적으로 만드는데,
그렇다면 position 값을 변경하는 로직이 있을 때마다
this.position.setPosition(this.position.getPosition() + 1) 형태의 코드를
중복해서 계속 사용할 것이다.

public class Position {

    private int position;
    
    public Position moveForward(int position) {
        return Position.of(this.position + position);
    }
} 

public class Car {

    private Position position;
    
    public void move() {
        this.position = position.moveForward();
    }
}

그러나 위와 같이 Position 객체에 움직이라는 메세지를 보내는 moveForward() 메소드를 만들면, 코드의 양이 줄 뿐만 아니라 중복코드도 없앨 수 있다.

profile
성장에 대한 경험을 공유하고픈 자발적 경험주의자

0개의 댓글