[회고] 우아한 테크코스 백엔드 5기 프리코스 3주차 솔직한 회고

BlackBean99·2022년 11월 18일
4

회고

목록 보기
2/8
post-thumbnail

우테코 3주차도 무사히??? 우여곡절 끝에 마감됐습니다. 이번주차는 저번 2주차보다 더 복잡한 로직들과 함께 클래스적으로 제한사항들이 추가됐습니다. 이번에도 무사히 마감할 수 있어서 정말 감사감사감사~~~합니다.

우테코 리뷰

코드 작성 가이드

함수(메서드) 라인에 대한 기준

프로그래밍 요구사항을 보면 함수 15라인으로 제안하는 요구사항이 있다. 이 기준은 main() 함수에도 해당된다.
정상적인 경우를 구현하는 것보다 예외 상황을 모두 고려해 프로그래밍하는 것이 더 어렵다. 예외 상황을 고려해 프로그래밍하는 습관을 들인다. 예를 들어 로또 미션의 경우 아래와 같은 예외 상황을 고민해 보고 해당 예외에 대해 처리를 할 수 있어야 한다.

이 말은

  • 예시 예외 사항.
로또 구입 금액에 1000 이하의 숫자를 입력
당첨 번호에 중복된 숫자를 입력
당첨번호에 1~45 범위를 벗어나는 숫자를 입력
당첨 번호와 중복된 보너스 번호를 입력

비즈니스 로직과 UI 로직을 분리한다

비즈니스 로직과 UI 로직을 한 클래스가 담당하지 않도록 한다. 단일 책임의 원칙에도 위배된다.

public class Lotto {
    private List<Integer> numbers;

    // 로또 숫자가 포함되어 있는지 확인하는 비즈니스 로직
    public boolean contains(int number) {
        ...
    }

    // UI 로직
    private void print() {
        ...
    }
}

현재 객체의 상태를 보기 위한 로그 메시지 성격이 강하다면 toString()을 통해 구현한다. View에서 사용할 데이터라면 getter 메서드를 통해 데이터를 전달한다.

연관성이 있는 상수는 static final 대신 enum을 활용한다

Spring 에서 이런 상수같은 불변 값을 쓰는건 에러메시지나 제한사항에 쓰는 내용들이겠죠?

  public enum Rank {
    FIRST(6, 2_000_000_000),
    SECOND(5, 30_000_000),
    THIRD(5, 1_500_000),
    FOURTH(4, 50_000),
    FIFTH(3, 5_000),
    MISS(0, 0);

    private int countOfMatch;
    private int winningMoney;

    private Rank(int countOfMatch, int winningMoney) {
        this.countOfMatch = countOfMatch;
        this.winningMoney = winningMoney;
    }
}

final 키워드를 사용해 값의 변경을 막는다

  • 최근에 등장하는 프로그래밍 언어들은 기본이 불변 값이다. 자바는 final 키워드를 활용해 값의 변경을 막을 수 있다.

불변값을 불변스럽게 해야한다!

public class Money {
private final int amount;

public Money(int amount) {
    ...
}

}

객체의 상태 접근을 제한한다

  • 인스턴스 변수의 접근 제어자는 private으로 구현한다.
public class WinningLotto {
    private Lotto lotto;
    private Integer bonusNumber;

    public WinningLotto(Lotto lotto, Integer bonusNumber) {
        this.lotto = lotto;
        this.bonusNumber = bonusNumber;
    }
}

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

참 어려운 말인데.. 책임 이라는 키워드로 생각해보면 그렇게 어려운 일은 아닙니다.
Lotto 클래스는 numbers를 상태 값으로 가지는 객체이다. 그런데 이 객체는 로직에 대한 구현은 하나도 없고, numbers에 대한 getter 메서드만을 가진다. 이렇게 하지 마라.

public class Lotto {
    private final List<Integer> numbers;
    
    public Lotto(List<Integer> numbers) {
        this.numbers = numbers;
    }

    public int getNumbers() {
        return numbers;
    }
}

public class LottoGame {
    public void play() {
        Lotto lotto = new Lotto(...);
	
        // 숫자가 포함되어 있는지 확인한다.
        lotto.getNumbers().contains(number);
        
        // 당첨 번호와 몇 개가 일치하는지 확인한다.
        lotto.getNumbers().stream()...
    }
}
  • 이게 무슨말이냐 객체를 옮겨서 그 책임을 넘겨주지 말고 객체가 일하고 넘겨줘라 라는 뜻이니당.

Lotto에서 데이터를 꺼내지(get) 말고 메시지를 던지도록 구조를 바꿔 데이터를 가지는 객체가 일하도록 한다.
이런식으로 코드를 바꾸면? 아래에

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

    public boolean contains(int number) {
        // 숫자가 포함되어 있는지 확인한다.
        ...
    }
    
    public int matchCount(Lotto other) {
        // 당첨 번호와 몇 개가 일치하는지 확인한다.
        ...
    }
}

public class LottoGame {
    public void play() {
        Lotto lotto = new Lotto(...);
        lotto.contains(number);
        lotto.matchCount(...); 
    }
}

(참고. getter를 사용하는 대신 객체에 메시지를 보내자)
필드(인스턴스 변수)의 수를 줄이기 위해 노력한다
필드(인스턴스 변수)의 수가 많은 것은 객체의 복잡도를 높이고, 버그 발생 가능성을 높일 수 있다. 필드에 중복이 있거나, 불필요한 필드가 없는지 확인해 필드의 수를 최소화한다.

성공하는 케이스 뿐만 아니라 예외에 대한 케이스도 테스트한다

테스트를 작성하면 성공하는 케이스에 대해서만 고민하는 경우가 있다. 하지만 예외에 대한 부분 또한 처리해야 한다. 특히 프로그램에서 결함이 자주 발생하는 부분 중 하나는 경계값이므로 이 부분을 꼼꼼하게 확인해야 한다.

테스트 코드도 코드다

테스트 코드도 코드이므로 리팩터링을 통해 개선해나가야 한다. 특히 반복적으로 하는 부분을 중복되지 않게 만들어야 한다. 예를 들어 단순히 파라미터의 값만 바뀌는 경우라면 아래와 같이 테스트할 수 있다.

@DisplayName("천원 미만의 금액에 대한 예외 처리")
@ValueSource(strings = {"999", "0", "-123"})
@ParameterizedTest
void underLottoPrice(Integer input) {
    assertThatThrownBy(() -> new Money(input))
            .isInstanceOf(IllegalArgumentException.class);
}

테스트를 위한 코드는 구현 코드에서 분리되어야 한다

테스트를 위한 편의 메서드를 구현 코드에 구현하지 마라. 아래의 예시처럼 테스트를 통과하기 위해 구현 코드를 변경하거나 테스트에서만 사용되는 로직을 만들지 않는다.

테스트를 위해 접근 제어자를 바꾸는 경우
테스트 코드에서만 사용되는 메서드
단위 테스트하기 어려운 코드를 단위 테스트하기
아래 코드는 Random 때문에 Lotto에 대한 단위 테스트를 하기 힘들다. 단위 테스트가 가능하도록 리팩터링한다면 어떻게 하는 것이 좋을까?

import camp.nextstep.edu.missionutils.Randoms;

public class Lotto {
    private List<Integer> numbers;

    public Lotto() {
        this.numbers = Randoms.pickUniqueNumbersInRange(1, 45, 6);
    }
}

——————

public class LottoMachine {
    public void execute() {
        Lotto lotto = new Lotto();
    }
}

올바른 로또 번호가 생성되는 것을 테스트하기 어렵다. 테스트하기 어려운 것을 클래스 내부가 아닌 외부로 분리하는 시도를 해 본다.

private 함수를 테스트 하고 싶다면 클래스(객체) 분리를 고려한다

  • 아니다 그냥 private 함수를 테스트하는건 그냥 니가 설계를 잘못한거다. 하지마라!

가독성의 이유만으로 분리한 private 함수의 경우 public으로도 검증 가능하다고 여겨질 수 있다.

public 함수가 private 함수를 사용하고 있기 때문에 자연스럽게 테스트 범위에 포함된다. 하지만 가독성 이상의 역할을 하는 경우, 테스트하기 쉽게 구현하기 위해서는 해당 역할을 수행하는 다른 객체를 만들 타이밍이 아닐지 고민해 볼 수 있다.

다음 단계를 진행할 때에는 너무 많은 역할을 하고 있는 함수나 객체를 어떻게 의미 있는 단위로 분할할지에 초점을 맞춰 진행한다.

내가 만든 쿠.. 아니 피드백

1. 제네릭

이전에는 디자인 패턴 접목을 통해 효율적으로 인스턴스를 생성하는 방법을 고려했지만 확장 가능성을 고려하여 설계하는 부분에 집중했습니다.

특히 제네릭을 사용하여 점수와 수익률 계산하는 기능은 제네릭으로 확장할 수 있게 설계했던 부분은 중간에 수정하는 경우도 있었습니다. 이런 설계를 하면서 제네릭이 확장을 고려한 개념이지만 생각보다 제약이 많았습니다.

반환형을 너무 자유롭게 사용하는데 제한이 있었습니다. 실제 개발하는데 제약이 있어서 무분별한 확장은 제한하는 느낌을 받았습니다.
제네릭을 잘 쓰면 정말 좋은 설계가 나오지만, 오히려 확장이 아니라 제약을 걸어주는 도구일 수도..?

2. 네이밍

이번에는 로또 번호를 저장하고 바인딩하는 과정들에 있어서 많은 변수도 등장했는데, 이 변수들의 네이밍을 짓는 것이 정말 어려웠습니다.

기존 피드백에서 자료형을 변수명으로 사용하지 말라는 피드백이 있어서 numberList를 numbers라고 하는 등 기존의 코드와는 다르게 이름을 지었습니다.

하지만 그런데도 네이밍하기 어려운 변수들을 어떻게 작성하면 좋을지 고민하는 과정을 통해 유지보수하기 좋은 클린코드란 무엇인지에 대해서도 다시 생각해 볼 수 있는 기회가 됐습니다.

  • 빨리 클린코드 읽으러 가야겠습니다 호닥닥

3. 총평

이번 미션에서 IllegalException 을 요구사항으로 받았었는데, 테스트 코드가 동작하지 않아 내부 동작을 확인해보았을 때 NoSuchElementException을 받아야 통과되게 설계됨을 확인했습니다. 많은 의구심이 들었지만 미션을 수행해야겠다는 생각이 들어 Exception을 수정했습니다. 내부 코드를 확인해보는 경험을 주기 위한 의도로 이렇게 설계했다고 생각했습니다.

전체적으로 요구사항이 많았고, 반복적으로 동작하는 연산을 클래스 분리를 통해 어떻게 구현해낼 수 있을지 고민해보는 시간을 통해 실제 발생하는 문제들에 적용해볼 수 있겠다는 생각을 할 수 있었습니다.

다양한 문제상황에 노출되는 것은 다양한 문제들을 해결할 수 있는 유연한 사고를 가질 수 있게 도와주는 것 같습니다. 이번 3주 차 때 배운 내용들 잊지 않고 피드백 반영해서 마지막 4주차 미션도 많이 배워나가고 싶습니다.

오늘의 회고 끝!

profile
like_learning

0개의 댓글