다음주에도 피드백이 있을까? 이번 3주차 피드백을 피드백 해본다...
공통 피드백
함수(메서드) 라인에 대한 기준
프로그래밍 요구사항을 보면 함수 15라인으로 제안하는 요구사항이 있다. 이 기준은 main() 함수에도 해당된다. 공백 라인도 한 라인에 해당한다. 15라인이 넘어간다면 함수 분리를 위한 고민을 한다.
📝 정말 이걸 지키는 것이 고통스러웠다. 15줄 안에 공백줄도 포함이 되어있기 때문에 이것도 고려해서 코딩을 해야해서 난이도가 높았다. 문제는 다음주는 10줄 제한이다...
발생할 수 있는 예외 상황에 대해 고민한다
정상적인 경우를 구현하는 것보다 예외 상황을 모두 고려해 프로그래밍하는 것이 더 어렵다. 예외 상황을 고려해 프로그래밍하는 습관을 들인다. 예를 들어 로또 미션의 경우 아래와 같은 예외 상황을 고민해 보고 해당 예외에 대해 처리를 할 수 있어야 한다.
● 로또 구입 금액에 1000 이하의 숫자를 입력
● 당첨 번호에 중복된 숫자를 입력
● 당첨번호에 1~45 범위를 벗어나는 숫자를 입력
● 당첨 번호와 중복된 보너스 번호를 입력비즈니스 로직과 UI 로직을 분리한다
📝 예외 상황을 고려한다고 하는데에도 리팩토링을 하다보면 생각이 난다. 그렇지만 예외처리를 하면서 이걸 입력 메시지딴에서 제한을 두는 것도 하나의 방법이 될 것 같다는 생각을 지울수가 없다.
비즈니스 로직과 UI 로직을 분리한다.
비즈니스 로직과 UI 로직을 한 클래스가 담당하지 않도록 한다. 단일 책임의 원칙에도 위배된다.
public class Lotto {
private List<Integer> numbers;
// 로또 숫자가 포함되어 있는지 확인하는 비즈니스 로직
public boolean contains(int number) {
...
}
// UI 로직
private void print() {
...
}
}
현재 객체의 상태를 보기 위한 로그 메시지 성격이 강하다면 toString()을 통해 구현한다. View에서 사용할 데이터라면 getter 메서드를 통해 데이터를 전달한다.
📝 이 부분 이해가 잘 가지 않았다. 먼저 비즈니스 로직과 UI 로직이 뭔지 몰라 검색해보니 비즈니스 로직은 기본 기능 구현과 관련된 부분이고, UI로직은 view 영역을 말하는 듯하다.
두 번째로는 '객체의 상태를 보기 위한 로그 메시지 성격이 강하다면 toString()' 이 부분이다.
print 메서드를 사용하지 않고 toString을 통하여 객체의 상태를 보라는 것 같은데, 어떤 때에 쓰이는지 정확히 모르겠다.
조금 더 공부가 필요할 듯 하다.
연관성이 있는 상수는 static final 대신 enum을 활용한다.
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;
}
}
📝 ENUM을 지난번에 적용한다고 머리를 꽤 싸맸던 기억이 있다.
ENUM은 클래스 안에서 구현하는 방법도 있지만 그것보다 클래스를 따로 빼내어 보통 구현하는 듯 하여 위와 비슷한 코드 형태로 진행했다.
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;
}
}
📝IV(instance variable) 사용은 private가 기본인 것 같다.
객체는 인스턴스 변수를 가지고 외부에서는 변경이 불가능하게 만드는 것이 기본인 듯하다.
객체는 객체스럽게 사용한다
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(...);
}
}
📝 참 멋진 과제다. 3주차 과제 시작에서 lotto 클래스에서 get을 통해 불러오는 법을 익히게 해주고, 피드백에서 getter 를 응용한 방법을 알려줬다. 객체가 직접 일하게 하는 방식이 있다는 걸 왜 몰랐을까??
필드(인스턴스 변수)의 수를 줄이기 위해 노력한다
필드(인스턴스 변수)의 수가 많은 것은 객체의 복잡도를 높이고, 버그 발생 가능성을 높일 수 있다. 필드에 중복이 있거나, 불필요한 필드가 없는지 확인해 필드의 수를 최소화한다.
예를 들어 총 상금 및 수익률을 구하는 다음 객체를 보자.
public class LottoResult {
private Map<Rank, Integer> result = new HashMap<>();
private double profitRate;
private int totalPrize;
}
위 객체의 profitRate와 totalPrize는 등수 별 당첨 내역(result)만 있어도 모두 구할 수 있는 값이다. 따라서 위 객체는 다음과 같이 하나의 필드만으로 구현할 수 있다.
public class LottoResult {
private Map<Rank, Integer> result = new HashMap<>();
public double calculateProfitRate() { ... }
public int calculateTotalPrize() { ... }
}
📝 필드를 줄이고 이걸 메소드화하는 방법이다. 리턴값에 대한 부분도 고려해서 메소드를 짜야한다.
물론 해당 클래스 말고 컨트롤 영역에서 로직을 짤 때까지 매개변수까지 고려하는것이 나중에 스파게티 코드를 만들지 않는 비결이지 않을까 싶다.
성공하는 케이스 뿐만 아니라 예외에 대한 케이스도 테스트한다
테스트를 작성하면 성공하는 케이스에 대해서만 고민하는 경우가 있다. 하지만 예외에 대한 부분 또한 처리해야 한다. 특히 프로그램에서 결함이 자주 발생하는 부분 중 하나는 경계값이므로 이 부분을 꼼꼼하게 확인해야 한다.
@DisplayName("보너스 번호가 당첨 번호와 중복되는 경우에 대한 예외 처리")
@Test
void duplicateBonus() {
assertThatThrownBy(() ->
new WinningLotto(new Lotto(List.of(1, 2, 3, 4, 5, 6), 6))
).isInstanceOf(IllegalArgumentException.class);
}
📝 이번주에는 테스트 junit 라이브러리에 대한 조금 더 깊은 공부가 필요할 것 같다.
빨리 익혀서 원하는대로 짜보고 싶다.
테스트 코드도 코드다
테스트 코드도 코드이므로 리팩터링을 통해 개선해나가야 한다. 특히 반복적으로 하는 부분을 중복되지 않게 만들어야 한다. 예를 들어 단순히 파라미터의 값만 바뀌는 경우라면 아래와 같이 테스트할 수 있다.
@DisplayName("천원 미만의 금액에 대한 예외 처리")
@ValueSource(strings = {"999", "0", "-123"})
@ParameterizedTest
void underLottoPrice(Integer input) {
assertThatThrownBy(() -> new Money(input))
.isInstanceOf(IllegalArgumentException.class);
}
📝 람다식을 이용해서 값을 모조리 던지고 있다. 람다식을 빨리 익혀야겠다.
테스트를 위한 코드는 구현 코드에서 분리되어야 한다
테스트를 위한 편의 메서드를 구현 코드에 구현하지 마라. 아래의 예시처럼 테스트를 통과하기 위해 구현 코드를 변경하거나 테스트에서만 사용되는 로직을 만들지 않는다.
● 테스트를 위해 접근 제어자를 바꾸는 경우
● 테스트 코드에서만 사용되는 메서드
📝 대부분 public으로 이용해서 아직 이부분에 대한 깊은 고민은 없었던 것 같다.
단위 테스트하기 어려운 코드를 단위 테스트하기
아래 코드는 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();
}
}
올바른 로또 번호가 생성되는 것을 테스트하기 어렵다. 테스트하기 어려운 것을 클래스 내부가 아닌 외부로 분리하는 시도를 해 본다.
public class Lotto {
private List<Integer> numbers;
public Lotto(List<Integer> numbers) {
this.numbers = numbers;
}
}
——————
import camp.nextstep.edu.missionutils.Randoms;
public class LottoMachine {
public void execute() {
List<Integer> numbers = Randoms
.pickUniqueNumbersInRange(1, 45, 6);
Lotto lotto = new Lotto(numbers);
}
}
위 코드는 A 상황을 B로 바꾼 것이다.
A.
Application(테스트하기 어려움)
⬇️
LottoMachine(테스트하기 어려움)
⬇️
Lotto(테스트하기 어려움) ➡️ Randoms(테스트하기 어려움)
B.
Application(테스트하기 어려움)
⬇️
LottoMachine(테스트하기 어려움) ➡️ Randoms(테스트하기 어려움)
⬇️
Lotto(테스트하기 쉬움)
이처럼 단위 테스트를 할 때 테스트하기 어려운 부분은 분리하고 테스트 가능한 부분을 단위 테스트한다. 테스트하기 어려운 부분은 단위 테스트하지 않아도 된다. 남은 LottoMachine은 어떻게 테스트하기 쉽게 바꿀 수 있을지 고민해 본다.
📝 외부에서 랜덤값을 넣어서 인스턴스 변수에 넣는 방식을 취하고 있다. 테스트코드에서 랜덤 값을 추출해서 넣거나 메뉴얼로 넣었을 때 정상 추출되는지 테스트가 되니 좋은 것 같다. 멋진방식.👍👍
private 함수를 테스트 하고 싶다면 클래스(객체) 분리를 고려한다
가독성의 이유만으로 분리한 private 함수의 경우 public으로도 검증 가능하다고 여겨질 수 있다. public 함수가 private 함수를 사용하고 있기 때문에 자연스럽게 테스트 범위에 포함된다. 하지만 가독성 이상의 역할을 하는 경우, 테스트하기 쉽게 구현하기 위해서는 해당 역할을 수행하는 다른 객체를 만들 타이밍이 아닐지 고민해 볼 수 있다. 다음 단계를 진행할 때에는 너무 많은 역할을 하고 있는 함수나 객체를 어떻게 의미 있는 단위로 분할할지에 초점을 맞춰 진행한다
📝 객체의 분리를 어디까지 해야 하는지에 대한 고민이 많아 질 것 같다. 여기서 실력차가 나는 것이겠지...
'성장기록 > 우테코(프리코스)' 카테고리의 다른 글
우아한 테크코스 5기 탈락 (1) | 2022.12.15 |
---|---|
우아한테크코스 5기 우테코 프리코스 4주차 회고 (0) | 2022.11.24 |
우아한테크코스 5기 우테코 프리코스 3주차 회고 (0) | 2022.11.16 |
프리코스 2주차 공통 피드백 (0) | 2022.11.16 |
2주차 피드백 강의 수강하며 몰랐던 부분 정리 (0) | 2022.11.10 |
남에게 설명할 때 비로소 자신의 지식이 된다.
포스팅이 도움되셨다면 하트❤️ 또는 구독👍🏻 부탁드립니다!! 잘못된 정보가 있다면 댓글로 알려주세요.