우아한 테크코스 프리코스 4주차 복기

CupRaccoon·2022년 11월 28일
0

1. 4주차-다리 건너기

4주차 코드 : https://github.com/CupRaccoon/java-bridge

4주차 미션인 다리 건너기에서 중요한 점은 세가지 정도라고 생각했다.
먼저 첫번째로 10행밖에 되지 않는 함수의 길이 요구사항, 두번째로 훨씬 디테일해진 각각의 클래스에 대한 요구사항, 그리고 마지막으로 최종 미션인 만큼 전주차들에서의 피드백들을 잘 반영하는 것, 이 세 가지의 목표를 지키면서 개발하고자 했다.

이에 따라서 목표에 맞춰 구체적인 가이드를 스스로 설정했는데 각각 함수를 기능단위로 분리할 것, 객체지향적 설계를 할 것, 전 주차들의 피드백들을 반영할 것으로 정했다.

2. 함수를 기능단위로 분리할 것

3주차의 15행 조건은 실제로 개발을 할 때에는 크게 문제가 되지 않았었다. 하지만 4주차의 10행같은 경우에는 함수를 작성하면서 계속 라인을 확인하면서 개발하게 되었다. 그리고 그런 식으로 개발하게 되면 자연스럽게 함수를 기능단위로 쪼갤 수 밖에 없다는 것을 느꼈다.

// in OutputView class

    public int readBridgeSize() throws IllegalArgumentException {
        String input = Console.readLine();
        int bridgeSize = inputToNumber(input);
        if (bridgeSize < 3 || bridgeSize > 20) {
            throw new IllegalArgumentException("다리 길이는 3부터 20 사이의 숫자여야 합니다.");
        }
        return bridgeSize;
    }
    private int inputToNumber(String input) throws IllegalArgumentException {
        int number;
        try {
            number = Integer.parseInt(input);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("숫자를 입력해야 합니다.");
        }
        return number;
    }

예를 들자면 이런 코드를 작성할 때에 이전까지라면 형변환도 readBridgeSize() 메소드 내에서 작성했겠지만―실제로 3주차에서 그런 형태로 작성했다―반면에 4주차에서는 inputToNumber()를 만들어 기능을 분리했다.
10행이라는 조건에 맞추기 위해서 분리했지만 이렇게 분리하게 되면 다른 입력 값을 받는 함수를 추가로 작성할 때 inputToNumber() 함수를 재사용할 수 있는 장점이 있음을 알게 되었다.

// in BridgeGame class

    private void toStringBodyBuilder(StringBuilder upBridge, StringBuilder downBridge) {
        for (int i = 0; i < this.nowPosition; i++) {
            if (this.bridge.get(i).equals("U")) {
                upBridge.append("O | ");
                downBridge.append("  | ");
                continue;
            }
            upBridge.append("  | ");
            downBridge.append("O | ");
        }
    }

    private void toStringTailBuilder(StringBuilder upBridge, StringBuilder downBridge, String lastResult) {

        if (this.bridge.get(this.nowPosition).equals("U")) {
            upBridge.append(lastResult).append(" ]");
            downBridge.append("  ]");
            return;
        }
        upBridge.append("  ]");
        downBridge.append(lastResult).append(" ]");
    }

    @Override
    public String toString() {
        StringBuilder upBridge = new StringBuilder("[ ");
        StringBuilder downBridge = new StringBuilder("[ ");
        toStringBodyBuilder(upBridge, downBridge);
        String lastResult = "O";
        if (this.moveResult == -1) {
            lastResult = "X";
        }
        toStringTailBuilder(upBridge, downBridge, lastResult);
        return upBridge.toString() + System.lineSeparator() + downBridge.toString();
    }

반면에 이 부분의 코드 같은 경우에는 좀 길어지더라도 toString() 메소드 안에 전부 작성하는게 (제한사항이 없다면) 더 좋은 설계가 아닐까 생각한다. 물론 Bridge의 상태를 나타내는 기능을 가진 BridgeMapResult 같은 이름의 클래스를 생성해서 해결할 수도 있다.

하지만 Bridge 정보를 가지고 있는 BridgeGame 클래스에서 직접 toString() 을 오버라이딩하거나 굳이 toString()을 오버라이딩하지 않더라도 getMapResult()과 같은 메소드를 작성하는 방법이 더 낫다고 생각한다.

첫번째로 단일책임원칙을 크게 위반하지도 않고(직접 출력하는게 아니고 String을 반환하니까), 둘째로 메소드 또한 기능 단위로 구현되어있고(멤버 변수인 ArrayList<Integer> bridge를 String 형식으로 표현하는 기능만 가지고 있음으로), 세번째로 이 방법이 클래스를 추가로 만드는 방법보다 가독성이 좋고 직관적이다.

사실 이런 부분은 아무래도 함수를 기능단위로 분리하는 연습을 위해서 함수당 10행이라는 약간은 과할 정도로 빡빡한 조건이 주어졌다고 생각한다.

3. 객체지향적 설계를 할 것

객체지향적 설계같은 경우 이번에는 무엇보다 주어진 클래스와 그에 대한 요구사항들을 잘 이해하고 분석하는게 중요했던 것 같다.
요구사항과 이미 주어진 클래스들을 분석하면서 헤맸던 부분은 크게 두 가지인데 첫 번째는 BridgeNumberGenerator 클래스와 BridgeRandomNumberGenerator 클래스이었고 두 번째는 BridgeGame 클래스였다.

첫번째를 분석한 결과 BridgeNumberGenerator 클래스와 BridgeRandomNumberGenerator 클래스같은 경우 Interface구체 클래스의 관계였는데 DIP(의존 역전 원칙)을 고려한 설계였다(사실 처음에 코드를 봤을 때는 한 번에 이해가 되진 않았다).

// in BridgeGameService class
BridgeNumberGenerator bridgeNumberGenerator = new BridgeRandomNumberGenerator();
BridgeMaker bridgeMaker = new BridgeMaker(bridgeNumberGenerator);

이런 형태의 설계를 의도했다고 생각한다. 이런 식으로 구현하게 되면 예를 들어 BridgeRandomNumberGenerator 클래스말고 0과 1을 비율을 다르게 하고 싶다면 BridgeRandomNumberRatioGenerator 클래스와 같은 클래스를 만들고 주입하기만 한다면 다른 부분의 코드를 거의 고칠 필요가 없다는 장점이 생긴다.

두번째를 분석한 결과 BridgeGame 클래스가 Bridge의 정보를 저장하는 클래스인 것 같았다.
move()retry() 가 자동사 형태라서 아! Bridge를 저장하는 일종의 도메인과 비슷한 클래스이구나. 라는 식으로 깨달았던 것 같다.
나는 보통 그러한 클래스라면 Bridge 라고 작성하고 반대로 BridgeGame 같은 경우는 전체적인 BridgeGame 자체를 다루는 클래스에 명명하는 일종의 네이밍 습관을 가지고 있어서 좀 이해하기가 어려웠었다.

4. 전 주차들의 피드백들을 반영할 것

전주차들의 피드백같은 경우 1~3주차 동안 배웠던 부분들을 최대한 반영해서 좋은 코드를 짜려고 노력했다. 개중에서 꼽자면 enumtoString()을 꼽을 수 있을 것 같다.

public enum UpDown {
    DOWN(0, "D"),
    UP(1, "U");
    
    private final int number;
    private final String letter;

    UpDown(int number, String letter) {
        this.number = number;
        this.letter = letter;
    }
    private int number() {
        return this.number;
    }
    private String letter() {
        return this.letter;
    }
    private static final Map<Integer, UpDown> BY_NUMBER =
            Stream.of(values()).collect(Collectors.toMap(UpDown::number, Function.identity()));
    public static String numberToLetter(int number) {
        return BY_NUMBER.get(number).letter();
    }
}

이런 식으로 0과 1을 "D"와 "U"를 매핑하는 용도로 enum을 사용했다.

toString() 같은 경우 2번 문단에 나와있는대로 BridgeGame의 상태를 반환할 수 있도록 오버라이딩해 사용했다.

5. 4주차에서 배운 것

4주차같은 경우 되돌아 보니, 10행 제한을 제외한다면 개인적으로 난이도도 3주차 미션이 더 높았다고 생각하고 또한 4주차는 피드백도 따로 없었기 때문에, 이런 부분들을 보면 1~3주차들에서 배운 것들이나 피드백들을 최대한 써 볼 수 있도록 만든 미션이라는 생각이 들었다.
그래서 4주차에서 배운 것이라고 한다면 1~3주차에서 배운 것들을 다시 한 번 더 배웠다고 말할 수 있을 것 같다.

profile
github : https://github.com/CupRaccoon

0개의 댓글