[워밍업 클럽 4기 백엔드] 2주차 발자국
2주차에 배운 것들
📌자동화된 테스트를 만들자
지금까지 개발을 하면서 테스트 코드를 만들어서 검증을 한적이 한번도 없었다. 강의에서도 언급된 콘솔을 통한 검증 방법을 통해 원하는 값이 잘 나오는지 직접 눈으로 확인했다.
@Test
void add() {
// 옳바른 테스트일까?
CafeKiosk cafeKiosk = new CafeKiosk();
cafeKiosk.add(new Americano());
System.out.println(">>> 담긴 음료 수 : " + cafeKiosk.getBeverages().size());
System.out.println(">>> 담긴 음료 : " + cafeKiosk.getBeverages().get(0).getName());
}이 방식에는 두 가지 문제점이 있었다.
테스트 최종 단계에서 사람이 개입된다.
다른 사람이 테스트 코드를 봤을 때, 무엇을 검증하고 어떤 것이 맞는 상황인지 틀린 상황인지 알 수 없다는 것이다.
이런 문제를 해결해주는 것이 바로 자동화된 테스트이다. 위 코드는 사용자가 음료를 추가하면 키오스크에 정상적으로 담기는지 확인하는 코드로 단위 테스트를 통해 검증할 수 있다. JUnit5와 AssertJ를 사용하여 자동 테스트를 만들어보자.
단위 테스트 : 작은 코드 단위(클래스 or 메서드)를 독립적으로 검증하는 테스트로 검증 속도가 빠르고 안정적이다.
@Test void add() { CafeKiosk cafeKiosk = new CafeKiosk(); cafeKiosk.add(new Americano()); assertThat(cafeKiosk.getBeverages()).hasSize(1); assertThat(cafeKiosk.getBeverages().get(0).getName()).isEqualTo("아메리카노"); }이렇게 테스트 코드를 구성하게 되면 테스트 최종 단계에서 사람의 개입 없이 검증할 수 있고, 기능이 정상적으로 동작했을 때의 결과를 통해 기능을 검증하므로 어떤 것을 검증하는지 명확하다.
📌 테스트하기 어려운 영역을 분리하자
public Order createOrder() {
LocalTime currentTime = LocalDateTime.now().toLocalTime();
if(currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) {
throw new IllegalArgumentException("주문 시간이 아닙니다. 관리자에게 문의하세요.");
}
return new Order(beverages);
}이 코드는 Order 객체를 생성하는 코드이다. 코드 내부에서 현재 시간을 기준으로 주문 시간이 아닌 경우 예외를 던지도록 해두었다. 이 코드를 단위 테스트를 통해 검증할 수 있을까?
그럴 수 없다. 테스트 코드를 실행하는 시점에 따라서 결과가 계속 달라지기 때문에 테스트의 정합성을 보장할 수 없기 때문이다. 그렇다면 어떻게 테스트를 해야할까?
💡시간을 매개변수를 통해서 외부에서 받으면 된다!
public Order createOrder(LocalDateTime currentDateTime) {
LocalTime currentTime = currentDateTime.toLocalTime();
if(currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) {
throw new IllegalArgumentException("주문 시간이 아닙니다. 관리자에게 문의하세요.");
}
return new Order(currentDateTime, beverages);
}@Test
void createOrderWithCurrentTime() {
CafeKiosk cafeKiosk = new CafeKiosk();
Americano americano = new Americano();
cafeKiosk.add(americano);
// 시작 시간의 경계값
Order order = cafeKiosk.createOrder(LocalDateTime.of(2025, 5, 28, 10, 0));
assertThat(order.getBeverages()).hasSize(1);
assertThat(order.getBeverages().get(0).getName()).isEqualTo("아메리카노");
}이 방식을 통해 시간을 외부에서 입력 받으면 테스트를 실행하는 시점과 관계 없이 우리가 원하는 값으로 테스트를 진행할 수 있게 된다. 게다가 영업 시작 시간(10시), 영업 종료 시간(22시)과 같은 경계값 테스트도 수행할 수 있어 유연한 테스트 코드를 작성할 수 있게 된다.
경계값 : 범위(이상, 이하, 초과, 미만), 구간, 날짜 등
물건 5개 이상 구매 시 할인 적용
물건을 5개로 설정하고, 할인이 적용되는지 확인
물건을 4개로 설정하고, 할인이 적용되지 않는지 확인
그렇다면 구현 단계에서 이런 부분까지 신경쓸 수 있을까? TDD를 통해 기능 구현 전 테스트를 먼저 작성하여 테스트가 어려운 영역을 인지할 수 있고, 테스트에 유연한 코드를 작성할 수 있다!
📌 TDD를 통해 테스트에 유연한 코드를 작성하자
TDD란? 프로덕션 코드보다 테스트 코드를 먼저 작성하여 테스트가 구현 과정을 주도하도록 하는 방법론
🔄TDD 사이클
실패하는 테스트를 작성하여 빨간불 보기 -> 테스트가 아직 성공하지 않았다고 명시적으로 표현
주먹구구식으로 엉터리 구현을 해서라도 테스트를 통과하는 기능 구현하기 -> '기능 구현'에 초점을 두고 코드 작성
초록불을 유지하면서 구현 코드 개선하기 -> 기능에 대한 검증은 끝났으므로 그 틀안에서 '읽기 좋은 코드'에 초점을 두고 리팩토링
🚨선 기능 구현, 후 테스트 작성의 문제점
테스트 코드 자체를 까먹고 작성하지 않을 수 있음
특정 테스트 케이스(해피 케이스)만 검증할 가능성
잘못된 구현을 다소 늦게 발견할 가능성
💡 TDD는 빠른 피드백을 받을 수 있다!
테스트에 유연하며 유지보수가 쉬운 코드로 구현할 수 있게 한다.
위에서 언급한 테스트하기 어려운 영역을 미리 생각하여 구현 단계에서 해당 영역을 의식할 수 있음
발견하기 어려운 엣지 케이스를 놓치지 않게 해준다.
구현에 대한 빠른 피드백이 가능하다.
과감한 리팩토링이 가능하다.
초록불을 보았으면, 기능에 대한 검증이 된 테스트 코드로 인해 리팩토링 중 잘못된 구현을 해도 리팩토링이 잘못되고 있다는 것을 빠르게 알 수 있음
📌 테스트는 문서다!
내가 작성한 코드와 문서가 다른 팀원에게는 어떻게 비춰질지 생각하면서 작성하는 것이 중요하다!
프로덕션 기능을 설명한다.
다양한 테스트 케이스를 통해 프로덕션 코드를 이해하는 시각과 관점을 보완한다.
어느 한 사람이 과거에 경험했던 고민의 결과물을 팀 차원으로 승격시켜서, 모두의 자산으로 공유할 수 있다.
그렇다면 이 문서를 어떻게 잘 작성할 수 있을까❓
💡@DisplayName 을 섬세하게 작성하자
음료 1개 추가 테스트 ❌ -> ~'테스트' 를 지양
음료를 1개 추가할 수 있다. ⭕ -> 명사의 나열보다는 문장으로 구성
음료를 1개 추가하면 주문 목록에 담긴다. ⭕ -> 테스트 행위에 대한 결과까지 기술하면 다른 팀원 또는 미래의 내가 봤을 때 이해하기 쉬워짐
특정 시간 이전에 주문을 생성하면 실패한다. ❌ -> 보는 사람 입장에서 '특정 시간'이 언제인지 불분명함
영업 시작 시간 이전에는 주문을 생성할 수 없다. ⭕ -> 도메인 용어인 '영업 시작 시간'을 사용하여 한층 추상화된 내용을 담아 테스트를 진행할 명확한 시간대를 알 수 있게됨
메서드 자체의 관점보다 도메인 정책에 관점을 두고 작성
테스트 현상을 중점으로 기술하기 보다, "어떤 행동이 어떤 결과를 만든다" 식의 표현으로 작성
💡 BDD 스타일로 작성하자
BDD(Behavior Driven Development)는 TDD에서 파생된 개발 방법으로 함수 단위의 테스트에 집중하기보다, 시나리오에 기반한 테스트케이스 자체에 집중하여 테스트하는 방법을 말한다. 이 방법은 개발자가 아닌 사람이 봐도 이해할 수 있을 정도의 추상화 수준을 권장한다.
Given : 시나리오 진행에 필요한 모든 준비 과정 (객체, 값, 상태 등) → 어떤 환경에서
When : 시나리오 행동 진행 → 어떤 행동을 진행했을 때
Then : 시나리오 진행에 대한 결과 명시, 검증 → 어떤 상태 변화가 일어난다
'음료를 1개 추가하면 주문 목록에 담긴다' 테스트를 BDD 스타일로 구성해보자
@Test
@DisplayName("음료 1개 추가하면 주문 목록에 담긴다.")
void add() {
// given
CafeKiosk cafeKiosk = new CafeKiosk();
Americano americano = new Americano();
// when
cafeKiosk.add(americano);
// then
assertThat(cafeKiosk.getBeverages()).hasSize(1);
assertThat(cafeKiosk.getBeverages().get(0).getName()).isEqualTo("아메리카노");
}✅ 이 방법을 통해 @DisplayName 을 명확하게 작성할 수 있게 된다!
미션을 통해 배운 것들
📌 Day 7
여유롭게 리팩토링을 진행하기 위해서 진도를 미리 나간 후 3일 동안 리팩토링을 진행했다. 처음 시작했을 때 어떤 것부터 리팩토링을 해야할지 막막했었다. 그리고 처음부터 '완벽한 리팩토링'을 해야한다고 생각했기 때문에 고려할 요소들을 한번에 적용하려고 했다. 1시간 동안 어떻게 할지 생각만 했고, 생각만 하고 있으면 아무것도 안되기에 한 가지 요소만 집중해서 차근 차근 바꿔나갔다.
한 가지 요소에만 집중하니까 확실히 수월하게 리팩토링을 진행할 수 있었다. 한 가지 요소를 해결하니 전에는 보이지 않던 문제들이 보이기 시작했고, 다른 요소들을 적용할 수 있는 방법들이 떠올랐다. 이를 통해 한 번에 해결하려는 욕심을 버리고, 단계적으로 나아가는 방법에 대해서 깨닫게 되었다.
그리고 강의를 통해 이론을 배우고, 코드를 따라 치는 것만 해서는 내용을 제대로 '체득'하는 것이 어렵다는 것을 알게 되었다. 강의를 수강하고, 이제부터는 읽기 좋은 코드를 작성할 수 있을 것 같다는 자신감이 생겼는데 직접 코드에 적용시키려니 잘 되지 않았다. '읽기 좋은 코드를 작성하는 능력'은 스스로 생각하고, 많은 시행착오를 거쳐야 비로소 얻을 수 있는 것이구나 깨닫게 되었다.
📌 중간 점검
중간 점검에는 다른 사람들의 코드와 고민했던 점을 볼 수 있어서 좋았다. 같은 코드, 같은 언어, 같은 기능을 리팩토링 하는데도 접근 방식과 구현 방법이 다르고, 내가 생각하지 못한 것들을 보고 많이 배울 수 있었다. 그 중에서 코치님께서 좋은 접근 방법이라고 하신 것들은 따로 작성해두었다.
그리고 직접 해주신 코드 리뷰 덕분에 내가 작성한 코드가 다른 사람의 관점에서는 어떻게 보이는지 알 수 있었다. 잘 한 부분과 부족한 부분을 알려주셔서 잘 한 부분은 더 발전시키고, 부족한 부분은 보완하는 과정을 통해 더 성장할 수 있는 좋은 기회였다고 생각한다.
댓글을 작성해보세요.