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

CupRaccoon·2022년 11월 25일
0

0. 복기의 목표

3주차같은 경우는 2주차 피드백과 3주차 요구사항이 지난 주차에 비해 더욱 구체적이었기 때문에 이런 조건들의 의도가 무엇인지를 생각하면서 개발했다.
이번에도 역시 개발할 당시에 왜 이렇게 짰는지, 그리고 다시 봤을 때 무엇이 문제였고 어떤 식으로 고칠 수 있는지와 같은 피드백들을 위주로 다뤄본다.

1. 3주차-로또

3주차 코드 : https://github.com/CupRaccoon/java-lotto

3주차같은 경우 함수의 길이(15행)이라는 조건이 새로 생겼고, 전주차 피드백에서도 기능 단위의 구현과 테스트 작성을 강조했었던 만큼 시작할 때 그런 부분들에 집중해서 구현하고자 했다. 따라서 1. 코드를 어떻게 잘 쪼갤지 그리고 2. 객체지향적 설계의 두 가지를 중심으로 개발하는 것을 목표로 했다.

2. Lotto와 WinningLotto, 그리고 상속

2-1. 설계

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

    public Lotto(List<Integer> numbers) {
        validate(numbers);
        this.numbers = numbers;
    }

    private void validate(List<Integer> numbers) {
        if (numbers.size() != 6) {
            throw new IllegalArgumentException();
        }
    }

    // TODO: 추가 기능 구현
}
  1. 제공된 Lotto 클래스를 활용해 구현해야 한다.
  2. Lotto에 매개 변수가 없는 생성자를 추가할 수 없다.
  3. numbers의 접근 제어자인 private을 변경할 수 없다.
  4. Lotto에 필드(인스턴스 변수)를 추가할 수 없다.
  5. Lotto의 패키지 변경은 가능하다.

와 같이 코드와 요구사항으로 Lotto 클래스가 주어졌다. 문제를 풀려고 요구사항들을 정리할 때 Lotto 클래스가 이미 numbers 리스트와 validate 함수가 작성되어 있었기 때문에 코드를 재사용하기 위해서 Lotto 클래스를 상속한 WinningLotto 클래스를 만들어 Lotto 클래스와 비교할 수 있도록 만들고자 했다.

public class WinningLotto extends Lotto {
    private int bonusNumber;
    public int getBonusNumber() {
        return bonusNumber;
    }

    public WinningLotto(List<Integer> numbers, int bonusNumber) throws IllegalArgumentException {
        super(numbers);
        validateBonusNumber(bonusNumber);
        this.bonusNumber = bonusNumber;
    }

따라서 WinningLotto 클래스를 이런 형태로 코드를 작성하게 됬다.

2-2. 문제점

    public MatchPair calculateMatchNumber(WinningLotto winningLotto) {
        int matchNumbers = (int) this.numbers.stream()
                .filter(lotto -> winningLotto.getNumbers().stream()
                        .anyMatch(Predicate.isEqual(lotto))).count();
        boolean matchBonusNumber = this.numbers.contains(winningLotto.getBonusNumber());
        return new MatchPair(matchNumbers, matchBonusNumber);
    }

제출할 당시의 Lotto 클래스의 calculateMatchNumber 메소드

문제는 Lotto 클래스가 WinningLotto 클래스와 비교해서 같은 숫자를 몇 개 가지고 있는지 계산해야 했으므로 이런 형태의 calculateMatchNumber 메소드를 Lotto 클래스 내부에 작성하게 되었다.
다시 생각해보면 크게 2 가지 문제를 가지고 있는 코드라고 생각한다.
첫번째로 Lotto 클래스에서 Lotto 클래스를 상속받은 구체적인 클래스인 WinningLotto 클래스를 직접적으로 의존하고 있다는 것이다. 따라서 객체지향적으로 좋지 못한 코드라고 생각했다.
두번째로는 이 메소드가 WinningLotto 클래스가 상속받은 Lotto 클래스 내부에 작성된 함수이기 때문에 WinningLotto 클래스도 마찬가지로 이 메스드를 호출이 가능하다. 하지만 Winning 클래스는 비교를 당하는 추첨번호이므로 이러한 메소드를 사용할 필요가 없다. 역시 객체지향적으로 좋지 못한 설계라고 생각했다.

2-3. 수정 및 피드백

수정한 커밋 : refactor: terminate the inheritance of the WinningLotto class

따라서 Lotto 클래스와 WinningLotto 클래스의 상속을 끊고 각각 클래스를 작성하기로 했다.

public class WinningLotto {

    private final List<Integer> numbers;
    private int bonusNumber;

    public WinningLotto(List<Integer> numbers, int bonusNumber) throws IllegalArgumentException {
        validateNumbers(numbers);
        this.numbers = numbers;
        validateBonusNumber(bonusNumber);
        this.bonusNumber = bonusNumber;
    }

이런 형태로 WinningLotto 클래스를 Lotto 클래스를 상속하지 않도록 하고 겹치는 내부 메소드같은 경우는 복붙을 통해 그대로 사용했다.
사실 처음에 WinningLotto 클래스를 상속으로 구현한 이유가 이러한 복붙을 피하고 코드를 그대로 재사용하기 위함이었는데 끝까지 메소드를 작성하고보니 calculateMatchNumber와 같은 메소드때문에 직접적인 상속관계를 가지게 되면 좋지 못하 설계가 됨을 알 수 있었다.
따라서 이런 경우에는 직접적으로 구체적인 클래스를 상속하기 보다는 두 클래스 사이에 공통부분을 묶어서 abstract classinterface를 통해 정의하고 Lotto, WinningLotto 클래스 모두 추상적인 상위 클래스를 상속받아서 중복을 피하고 더 좋은 객체지향적 설계를 꾀할 수 있으리라 생각한다.
다만 이번 문제에서는 Lotto 클래스에 대해 요구사항이 빡빡했으므로 상위클래스를 만들어 상속받는 대신 Lotto, WinningLotto 클래스를 각각 작성했다.

3. 입력값 검증과 예외처리

3-1. 문제점

    public static int readBuyingMoney() throws IllegalArgumentException {
        System.out.println("구입금액을 입력해 주세요.");
        String input = Console.readLine();
        int buyingMoney;
        try {
            buyingMoney = Integer.parseInt(input);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("로또 구입 금액은 숫자입니다.");
        }
        if (buyingMoney % 1000 != 0) {
            throw new IllegalArgumentException("로또 구입 금액은 1000원 단위입니다.");
        }
        return buyingMoney / 1000;
    }

제출할 당시의 IOController 클래스 내의 readBuyingMoney 메소드

1장에 1000원인 로또 구입금액을 입력받는 메소드로 readBuyingMoney라는 메소드를 구현했다. 이 때 입력값이 숫자인지, 천원 단위인지는 정상적으로 검증했지만 음수값이 들어오게 되도 예외없이 함수가 동작한다. 즉 -2000이나 -3000을 입력값으로 들어왔다면 로또를 2장, 3장 살 수 있게 된다.
코드를 작성할 당시에는 음수 값 입력을 전혀 생각하지 못했기 때문에 이런 문제가 발생했다.

3-2. 수정 및 피드백

수정한 커밋 : fix: Set range of input value

    public static int readBuyingMoney() throws IllegalArgumentException {
        System.out.println("구입금액을 입력해 주세요.");
        String input = Console.readLine();
        int buyingMoney;
        try {
            buyingMoney = Integer.parseInt(input);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("로또 구입 금액은 숫자입니다.");
        }
        if (buyingMoney % 1000 != 0) {
            throw new IllegalArgumentException("로또 구입 금액은 1000원 단위입니다.");
        }
        if(buyingMoney >= 0 && buyingMoney <= MoneyConstant.MAX_LOTTO_PRIC.getValue())
        return buyingMoney / 1000;
    }

이번 문제같은 경우에는 (전부 뜯어고치기 애매/귀찮+else 사용불가라서) 단순히 입력값에 범위를 추가하는 것으로 해결했다. 하지만 이런 예외같은 경우라면 언제나 놓치는 예외가 생길 수 있음을 알게 되었다.
따라서 "예외처리"라는 표현을 쓰기는 하지만 예외에 집중하는 것이 아니라 유효한 입력 값을 strict하게 검증하고 그 외에는 전부 예외로 던진 후 그 안에서 예외를 나누는 식으로 코드를 작성하는게 유리하다고 생각하게 됬다.

    public static int readBuyingMoney() throws IllegalArgumentException {
        System.out.println("구입금액을 입력해 주세요.");
        String input = Console.readLine();
        int buyingMoney;
        try {
            buyingMoney = Integer.parseInt(input);
        } catch (NumberFormatException e) {
            throw new IllegalArgumentException("로또 구입 금액은 숫자입니다.");
        }
        if(buyingMoney % 1000 == 0 && buyingMoney >= 0 
        && buyingMoney <= MoneyConstant.MAX_LOTTO_PRIC.getValue()){
        	return buyingMoney/1000;
        }
        else{
        	throw new IllegalArgumentException1("에러1");
        	throw new IllegalArgumentException2("에러2");
        .
        .
        }
    }

위의 코드를 그러한 규칙에 입각해서 리팩토링한다면 이런 식으로 짤 수 있을 것 같다.
3주차에서 이렇게 고치지 않은 대신 4주차에서는 이런 방식으로 최대한 구현하려고 했다.

    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;
    }

    public String readMoving() throws IllegalArgumentException {
        String input = Console.readLine();
        if (!input.equals("U") && !input.equals("D")) {
            throw new IllegalArgumentException("이동할 칸은 \"U\"또는 \"D\"를 입력해야 합니다.");
        }
        return input;
    }

4주차의 입력값 검증 메소드

10행 제한과 else 사용불가 때문에 예외 값을 먼저 검증하긴 했지만 예외를 놓친 첫번째 코드보다는 훨씬 입력 값 검증과 예외처리가 잘 된 코드라고 생각한다.

4. 3주차에서 배운 것

  1. 상속(Abstract Class, Interface...)
  2. 예외처리와 입력값 검증

왜 구체적인 클래스에 의존하지말고 추상적인 클래스에 의존하라고 하는지 확실히 알게 되었다.
또한 예외처리에 대한 방법론에 대해서도 많이 깨달을 수 있었다.

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

0개의 댓글