우아한 프리코스 4주차

GGOMG·2022년 11월 16일
0

우아한 프리코스

목록 보기
3/4

1. 시작

1. 공통 피드백

객체는 객체스럽게 사용한다.

Lotto에서 데이터를 꺼내지(get) 말고 메시지를 던지도록 구조를 바꿔 데이터를 가지는 객체가 일하도록 한다.

3주차에서도 고민했던 내용입니다. Get메서드를 통해 private 필드를 요청할 때가 많았는데, 다른 계층에서 무분별한 get으로 데이터를 요구하는 것이 객체지향스러운가? 라는 고민이었습니다.

피드백에서는 메시지를 제안하고 있습니다. 고민 과정에서 레퍼런스를 찾아보며 객체간 메시지 전달에 대한 내용이 있었는데 제대로 이해하지 못했습니다.

public class Lotto {
    private final List<Integer> numbers;

    public boolean contains(int number) {
        // 숫자가 포함되어 있는지 확인한다.
        ...
    }

피드백의 예시를 보며 적용 방법에 대해 조금이나마 이해할 수 있었습니다.

단위 테스트하기 어려운 코드를 단위 테스트하기

테스트하기 어려운 것을 클래스 내부가 아닌 외부로 분리하는 시도를 해 본다.

가독성의 이유만으로 분리한 private 함수의 경우 public으로도 검증 가능하다고 여겨질 수 있다. public 함수가 private 함수를 사용하고 있기 때문에 자연스럽게 테스트 범위에 포함된다. 하지만 가독성 이상의 역할을 하는 경우, 테스트하기 쉽게 구현하기 위해서는 해당 역할을 수행하는 다른 객체를 만들 타이밍이 아닐지 고민해 볼 수 있다. 다음 단계를 진행할 때에는 너무 많은 역할을 하고 있는 함수나 객체를 어떻게 의미 있는 단위로 분할할지에 초점을 맞춰 진행한다.

제가 작성한 3주차 코드에도 적용되는 피드백입니다.

코드를 작성하다보면, 다른 객체에서 호출하는 부분을 제외하면 모두 private으로 구현하게 됩니다. 코드를 가능한 노출하지 않는 것이 원칙이라고 알고있었고, 그렇게 구현했습니다.

그러다보면 단위 테스트에 어려움이 생깁니다. 많은 메서드를 구현했지만, 결국 테스트 코드의 대상이 되는 건 다른 계층에서 사용하기 위해 public으로 노출해 놓은 코드 뿐 입니다.

"private 코드 테스트하는 방법" 레퍼런스들을 찾아보며, private 코드들은 테스트하지 않는 것이 좋다는 의견이 대부분이었습니다. 그래서 결국 public 메서드들만 제한적으로 테스트는데, 이게 제대로 된 테스트가 맞을까? 하는 의문이 들었습니다. 테스가 어렵다면, 클래스 분리가 잘못되었다고 생각하고 클래스 분리를 시도해야겠습니다.

2. 구현

1. BridgeMaker - Enum 활용

요구사항
다리의 길이를 숫자로 입력받고 생성한다.
다리를 생성할 때 위 칸과 아래 칸 중 건널 수 있는 칸은 0과 1 중 무작위 값을 이용해서 정한다.
위 칸을 건널 수 있는 경우 U, 아래 칸을 건널 수 있는 경우 D값으로 나타낸다.
무작위 값이 0인 경우 아래 칸, 1인 경우 위 칸이 건널 수 있는 칸이 된다.

처음 고려한 코드는 다음과 같습니다.

List<String> bridgeStructure = Arrays.asList("D", "U")

// bridgeStruceture.get(0) = "D"
// bridgeStruceture.get(1) = "U"

그러나 리스트 선언 한줄 (Arrays.asList("D", "U")) 만으로 0,1 과 "D","U"의 연관관계를 충분히 설명하지 못한다고 생각했습니다.

따라서 0, 1과 D,U를 매칭할 수 있도록 enum을 이용했습니다.

public enum BridgeComponent {
    D(0, "D"),
    U(1, "U");

    private final int number;
    private final String symbol;
}

2. 테스트하기 쉬운 코드

BridgeGame 객체의 move메서드는 반환값이 없습니다.
controller에 의해 사용되야 하기 때문에 public으로 열려있고, 핵심 로직이기 때문에 테스트가 필요합니다.

BridgeGame.java

public void move(String step) {
        stepValidate(step);
        currentLocation += 1;
        boolean success = bridge.checkStep(currentLocation, step);
        records.add(MoveRecord.addRecord(step, success));
	}

그러나 코드를 잘 보면, BridgeGame 내부 필드만 변화값을 줄 뿐 반환하는 값이 없습니다. 반환하는 값이 없다면, 테스트를 실행할 때, move() 메서드가 영향을 주는 객체를 생성하고, 메서드 실행으로 인해 영향을 주는 필드를 추적해야 합니다.

getter 메서드를 필요에 의해 구현했다면 괜찮겠지만, 없는 상황이라면 테스트가 난감해집니다. 외부 기능 테스트를 위해 필드값을 꺼내는 getter를 새로 구현해야 한다면, 생산성이 떨어지고, 캡슐화의 원칙에 위배됩니다.

따라서, move 메서드를 테스트하기 쉬운 코드로 리팩터링 하겠습니다.
먼저, 메서드의 영향이 미치는 범위를 보기위해, move()가 사용되는 도메인을 살펴보겠습니다.

  • BridgeController.chooseStep( )

    bridgeGame.move() 메서드는 BridgeControllerchooseStep()에서 사용되며, 역시 void를 반환하는 함수입니다.
BridgeController.java

    private void chooseStep() {
        while (true) {
            try {
                String step = inputView.readMoving();
                bridgeGame.move(step);
                break;
            ...
        }
    }
  • BridgeController.goForward()

    BridgeController.chooseStep() 메서드는 BridgeControllergoForward()에서 사용되며, 역시 void를 반환하는 함수입니다.
BridgeController.java
    private void goForward() {
        do {
            chooseStep();
            showResult();
        } while (!bridgeGame.isOver());
    }

while 문의 탈출 조건에 내부 메서드가 직접적으로 관여하지 않는 모습입니다.
Bridge.move()를 개선함으로서 코드를 리팩터링 해보겠습니다.

리펙터링

Bridge.move( )

Bridge.java

    public boolean move(String step) {
        stepValidate(step);
        currentLocation += 1;
        
        boolean success = bridge.checkStep(currentLocation, step);
        records.add(MoveRecord.addRecord(step, success));
        
        return success;
    }

return void->boolean 함으로써 메서드 실행 결과를 상위로 전달합니다.
또한, 코드 테스트가 쉬워집니다.

BridgeController.chooseStep( )

BridgeController.java

    private boolean chooseStep() {
        while (true) {
            try {
                String step = inputView.readMoving();
                return bridgeGame.move(step);
            } catch (IllegalArgumentException e) {
                outputView.printError(e.getMessage());
            }
        }
    }

whilebreak하고 void 종료하는 대신,
boolean을 리턴하며 while를 탈출하며 클린코드에 가까워졌습니다.

BridgeController.goForward( )

BridgeController.java

    private void goForward() {
        while (true) {
            boolean moveSuccess = chooseStep();
            showResult();
            if (!moveSuccess || bridgeGame.isOver()) {
                break;
            }
        }
    }

goForward까지 리팩터링하니 if (!moveSuccess || bridgeGame.isOver()) 이 부분이 명료하지 못합니다. 탈출조건을 개선해보겠습니다.

BridgeGame.isOver( ) - before

BridgeController.java

    public boolean isOver() {
        if (bridge.isEnd(currentLocation)) {
            return true;
        }
        return !checkFinalStep();
    }

bridgeGame.isOver() 메서드는

다리를 끝까지 건너거나bridge.isEnd(currentLocation)
마지막 다리건너기 시도checkFinalStep()false(실패)라면
게임의 종료를 알리는 메서드입니다.

앞서 리팩터링한 move()메서드는 boolean을 반환함으로써
이미 다리 건너기 시도의 성공 여부의 정보를 담고있습니다.

BridgeGame.isOver( ) - after

BridgeController.java

    public boolean isOver() {
        return bridge.isEnd(currentLocation);
    }

마지막 시도 성패확인 코드를 제거함으로써 코드가 훨씬 간결해졌습니다.

결과

테스트하기 쉬운 코드를 작성하기 위해, move( ) 메서드 하나를 리팩터링 했을 뿐인데,
연쇄적으로 다른 코드 또한 개선되는 것을 알 수 있었습니다.

반대로 생각해보자면, 처음부터 테스트하기 쉬운 단위 기능을 작성한다면,
해당 코드를 사용하는 상위 코드 또한 클린하게 작성할 수 있습니다.

0개의 댓글