어느덧 프리코스의 마지막 미션이 주어졌습니다. 코수타 (코치와의 수다타임) 때 포비 님과 코치님들께서 말씀해 주셨듯이 쉽게 풀리지는 않았었습니다.
객체지향의 의미를 다시 생각하자. (현실 세계 반영 주의)
그동안 객체지향의 사실과 오해 책을 읽으며 블로그에도 기록했었는데요, 책에서는 다음과 같은 내용이 나옵니다.
객체지향이 단순한 현실 세계의 모방이 아닌 이유 중 하나는 현실 속에서 수동적인 존재가 객체지향 세계에서는 능동적인 존재가 될 수 있기 때문이다. 이는 객체를 의인화 (anthropomorphism) 하였기 때문이다.
즉, 진정한 객체지향을 구축하기 위해서 주의할 점은 현실 세계를 그대로 옮기는 행위는 하면 안 된다는 것입니다.
그러나 저는 날짜 (Day)와 주문 (Orders)를 만든 다음, 이들을 실제로 관리하는 Customer (고객) 객체를 만들면 어떨까라는 생각을 했었습니다. 하지만 이렇게 하니 할인 정책을 적용할 때 결국 Customer로부터 Day와 Orders를 getter 메서드로 호출하도록 했고, Customer가 스스로의 로직을 가지지 못했습니다.
작성했던 할인 정책은 Day와 Orders를 아래 코드처럼 받습니다. 따라서 객체가 더 자율적 이도록 하기 위해 Customer를 만들지 않고 Day와 Orders만 활용하도록 변경했습니다.
public interface DiscountPolicy {
int discount(final Day day, final Orders orders);
boolean canDiscount(final Day day, final Orders orders);
}
방어적 복사에 대한 깊은 고민
그동안의 미션들에서는 일급 컬렉션을 활용할 때 방어적 복사를 활용하지 못했습니다. 개념에 대해 이름만 들어본 수준이기도 했고, 이펙티브 자바에 있다는 내용이길래 이해가 잘 안 될 것 같다는 생각만 했었기 때문입니다.
그러다 우연히 일급 컬렉션에서 응답 DTO를 만들기 위해 getter 메서드를 정의한다면, 내부적으로 값이 변경될 수 있는 것 아닐까?라는 의문이 들었습니다. 그리고 바로 이 점 때문에 나온 개념이 방어적 복사임을 알았습니다.
예시로 로또 미션의 경우, 방어적 복사를 하지 않는다면 컬렉션의 요소 값이 변경될 수 있습니다. (getNumbers로 가져온 다음에 numbers.add를 시키면 됩니다.)
public class GoalNumbers {
private static final String NUMBER_SPLITTER = ",";
private final List<LottoNumber> numbers;
...
public List<LottoNumber> getNumbers() {
return numbers;
}
}
이에 대해 느낀 점은 다음과 같습니다.
- 배움은 내가 필요하다고 느낄 때 그 효과가 가장 크다.
- 방어적 복사가 무조건 좋은 것은 아니다. 그렇기 때문에 팀 단위라면 충분한 논의가 이루어져야 한다.
- 일급 컬렉션에 불변 속성을 붙이는 것은 주된 목적이 아니다.
일급 컬렉션에 대한 고민을 했던 것은 추후 다른 글에 작성하도록 하겠습니다. (이 내용 또한 코드를 직접 뜯어보며 탐구했기에..)
끝없는 JUnit의 기능
3주 차까지 진행하면서 두렵기만 했었던 JUnit에 대해 어느 정도 알게 되었다고 생각했었는데요, EnumSource, CsvSource 등 유용한 것들을 알게 되었습니다. 이런 것을 보면 배움에는 끝이 없는 것 같습니다.
EnumSource
EnumSource는 Enum이 가진 값들로 테스트를 돌릴 때 유용하게 활용할 수 있습니다.
@ParameterizedTest(name = "{0} 메뉴 기본 생성 시 1개씩 저장되는가?")
@EnumSource(value = Menu.class)
@DisplayName("메뉴 이름만으로 Order 생성")
void createOrderWithNameTest(final Menu menu) {
// given
Order order = Order.withMenu(menu);
int expectedSize = 1;
// when
String menuName = order.getName();
int orderSize = order.getSize();
// then
assertAll(
() -> assertThat(menu.isNameSame(menuName)).isTrue(),
() -> assertThat(orderSize).isEqualTo(expectedSize)
);
}
// Menu
public enum Menu {
SOUP(APPETIZER, "양송이수프", 6000),
TAPAS(APPETIZER, "타파스", 5500),
SALAD(APPETIZER, "시저샐러드", 8000),
STEAK(MAIN_DISH, "티본스테이크", 55_000),
BARBEQUE(MAIN_DISH, "바비큐립", 54_000),
SEA_PASTA(MAIN_DISH, "해산물파스타", 35_000),
CHRISTMAS_PASTA(MAIN_DISH, "크리스마스파스타", 25_000),
CAKE(DESSERT, "초코케이크", 15_000),
ICECREAM(DESSERT, "아이스크림", 5000),
COKE(DRINK, "제로콜라", 3000),
WINE(DRINK, "레드와인", 60_000),
CHAMPAGNE(DRINK, "샴페인", 25_000);
...
}
위 코드는 Menu 타입으로 Order를 만들었을 시, 정상적으로 1개씩 생성되는지 검증합니다. 즉 SOUP ~ CHAMPAGNE를 한 번씩 넣어보며 테스트하도록 해 줍니다.
ParameterizedTest의 name 속성
ParameterizedTest에 name 속성을 붙이면 아래와 같이 테스트를 더 직관적으로 드러낼 수 있습니다. 인자 위치에 따라 {0}, {1}을 작성하면 됩니다.
CsvSource
CsvSource는 복잡한 데이터 형태를 구분할 수 있습니다.
private static final String MENU_ORDERS =
"""
양송이수프, 2, SOUP
타파스, 2, TAPAS
시저샐러드, 2, SALAD
...
""";
@ParameterizedTest(name = "{0} {1}개 주문 생성이 문제 없는가?")
@CsvSource(textBlock = MENU_ORDERS)
@DisplayName("정상 Order 생성")
void validOrderTest(final String nameInput, final String sizeInput) {
// given
String orderInput = nameInput + "-" + sizeInput;
// when & then
assertDoesNotThrow(() -> Order.from(orderInput));
}
위와 같이 textBlock 형태로 데이터를 받을 수 있습니다. 이때 기본적으로는 콤마를 기준으로 구분됩니다. 또한 앞뒤 공백은 자동으로 제거되는 것 같습니다.
아래는 textBlock에 관한 설명입니다. (delimiter, delimiterString으로 구분자를 정의할 수 있습니다.)
또는 아래와 같은 방법도 가능합니다. 이 방법은 value를 활용합니다.
@ParameterizedTest(name = "{0}원이 들어올 때 {1} Badge인가?")
@CsvSource({
"3000, NOT_THING",
"6000, STAR",
"12000, TREE",
"25000, SANTA"
})
@DisplayName("각 입력값에 따른 적절한 Badge 조회")
void findEachBadgeByCostTest(final int cost, final Badge badge) {
// when
Badge findBadge = Badge.findByCost(cost);
// then
assertThat(findBadge).isEqualTo(badge);
}
assertAll
assertAll은 위의 EnumSource에서도 작성했듯, 여러 개의 assert를 함께 해야 할 때 적용할 수 있습니다.
결론
프리코스를 하면서 놓쳤던 점들이 조금 있어 아쉽기도 하지만 (로또 미션에서 보너스 번호는 2등만 관여해야 하지만 그러지 못한 점, static import를 다른 메서드들에도 한 점 등), 전체적으로 보면 이 당시에는 할 수 있었던 최선을 했던 것 같습니다. (놓친 점들에 대해서는 그래도 기본으로 주어진 테스트 결과들은 모두 성공했으니 괜찮을 것 같다는 위안을 하고 있습니다..)
프리코스를 4주 간 하면서 진짜 학교 수업을 다 버리고, 수업 시간에도 프리코스만 하는 등 온전히 몰입을 했는데요. (매일 4시에 잔 것은 비밀) 그만큼 좋은 결과가 있으면 좋겠습니다.
그럼에도 탈락될 확률이 훨씬 높은 것은 당연하니, 차츰 우테코에 탈락하게 된다면 어떻게 문제없이 성장할 수 있을지에 대해서도 고민해 볼 계획입니다.
프리코스 미션을 하면서 여러 고민들을 할 수 있어 좋았고, 블로그 글들을 유익하게 채울 수 있어서 너무 좋았습니다. 합불 여부를 떠나서 프리코스의 본질적인 목표는 이루게 된 것 같아 뿌듯하네요.
1차 선발까지 할 것
혹시 모를 1차 선발 대상 합격을 기대하면서 혹시 모를 최종 코딩테스트에 대비하기 위해, 12.11 (월)까지 남은 기간 동안 우테코의 다른 문제들 (자판기, 페어매칭, 점심 메뉴 추천, 지하철 노선도)을 풀어보며 연습해보려 합니다.
실제 코딩테스트처럼 5시간을 잡으며 진행해 보고, 테스트 코드는 시간이 남을 경우에 작성하는 것을 목표로 할 계획입니다. (1차 목표는 구현이 온전히 되도록, 한 메서드가 15라인을 넘지 않도록 관리하는 것입니다.)
12월 11일에 합격이 되든 불합격이 되든 블로그에 기록하겠습니다!
제출 링크
제출 링크는 처음에는 private 저장소로 제한되었지만 이제 public으로 수정해도 되어서 링크 남기겠습니다!
이곳에서 확인하실 수 있습니다.
'🚀 우아한테크코스 6기 지원 기록' 카테고리의 다른 글
[프리코스] 프리코스 3주차 후기 (로또 🎱) (0) | 2023.11.14 |
---|---|
[프리코스] 프리코스 2주차 후기 (자동차 경주 🚗) (0) | 2023.11.06 |
[프리코스] 프리코스 1주차 후기 (숫자 야구 ⚾️) (1) | 2023.10.31 |