인프런 워밍업 클럽 백엔드 3주차 후기
3주차 후기
Readable Code 강의를 재밌게 수강하고, 드디어 고대하던 강의 중 하나인 테스트 코드 강의를 들었다.
나는 새로 기능을 만들어 볼 때마다 JUnit으로 테스트 코드를 돌려보려고 한다. 근데 아직 경험이 많이 부족하고 시간이 부족하다는 핑계로 미루게 된다. 그러다 보니 자연스레 서버를 켜서 직접 API를 호출해서 테스트를 하는 비효율적인 방법을 선택하고 있다.
이전에 인프런 CTO의 이동욱 님의 어느 글에서 안 좋은 방식이라 했던걸 내가 하고 있던 것이다..! ㅎㅎ
이를 지양하기 위해 이번 테스트 강의를 열심히 들었고, 들을 수록 이래서 테스트 코드를 작성해야 함을 배우게 되었다.
강의를 마저 다 들어서 얼른 회사에서도 다 적용해볼 수 있도록 해야겠다.
섹션2: 테스트는 왜 필요할까
테스트는 왜 필요할까?
테스트는 사실.. 귀찮은 작업
실무에서는 짧은 시간에 기능을 구현해서 QA(Quality Assuarance)해야는데 시간이 부족함
근데 왜 테스트 코드를 짜야할까?
기존 프로덕션 코드가 있었는데, 기능을 계속 추가하면서 테스트를 하게 됨
근데 아래와 같이 기존 코드를 일부 참고하는데가 생기면서 새로 검증을 해야됨
프로덕션 코드가 계속 커지면 테스트하는데만 시간이 다 소진되어버림
또한 사람이 하게 되면 실수&누락이 발생하게 될 수 있음
테스트 코드를 작성하지 않으면
변화가 생기는 매순간마다 발생할 수 있는 모든 Case를 고려해야 함
변화가 생기는 매순간마다 모든 팀원이 동일한 고민을 해야 함
빠르게 변화하는 소프트웨어의 안정성을 보장할 수 없다
올바른 테스트 코드는
자동화 테스트로 비교적 빠른 시간 안에 버그를 발견할 수 있고, 수동 테스트에 드는 비용을 크게 절약할 수 있음
소프트웨어의 빠른 변화를 지원한다.
팀원들의 집단 지성을 팀 차원의 이익으로 승격시킨다.
가까이 보면 느리지만, 멀리 보면 가장 빠르다
우리가 테스트 코드로 얻을 수 있는 점
빠른 피드백: 코드가 실패하면 바로 알 수 있음
자동화: 기계가 자동으로 해줘서 사람이 탈 일이 없어짐
안정감: 그로 인해 안정감이 늚
💡 물론 테스트 코드를 어렵게 짜면 더 힘들어 질 수도 있다. 잘 짜는 것이 중요! 테스트는 귀찮지만 해야한다!
섹션3: 단위 테스트
수동테스트 VS 자동화된 테스트
아래와 같이 print를 활용해서 수동으로 확인하는게 과연 맞을까?
class CafeKioskTest {
@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로 테스트하기
단위 테스트(Unit test)
작은 코드 단위(클래스 or 메서드)를 독립적으로 검증하는 테스트
독립적: 외부 네트워크 같은 환경에 의존하는게 아닌 딱 코드 단위를 테스트
검증 속도가 빠르고, 안정적임
JUnit5
단위 테스트를 위한 테스트 프레임워크
XUnit - Kent Beck 창시
SUnit(Smalltalk), JUnit(Java), NUnit(.NET) 등
AssertJ
테스트 코드 작성을 원할하게 돕는 테스트 라이브러리
풍부한 API, 메서드 체이닝 지원
테스트 케이스 세분화하기
해피 케이스: 요구 사항이 그대로 만족하는 케이스 (해피해피)
예외 케이스: 예외가 발생하는 경우도 생각해야함
두 가지를 고려하기 위해 경계값 테스트를 잘 만들어야 함
범위(이상, 이하, 초과, 미만), 구간, 날짜 등
테스트하기 어려운 영역을 분리하기
추가 요구사항
가게 운영 시간(10:00~22:00) 외에는 주문을 생성할 수 없다.
public Order createOrder() {
LocalDateTime currentDateTime = LocalDateTime.now();
final LocalTime currentTime = currentDateTime.toLocalTime();
if (currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) {
throw new IllegalArgumentException("주문 시간이 아닙니다. 관리자에게 문의하세요.");
}
return new Order(LocalDateTime.now(), beverages);
}
현재 코드와 같이 시간에 대한 값을 프로덕션 코드에 넣어두면 테스트할 때마다 환경이 달라져 일관성 있는 테스트를 하기 어려워진다
이처럼 테스트하기 어려운 영역을 구분하고 분리해야함
public Order createOrder(LocalDateTime currentDateTime) {
final LocalTime currentTime = currentDateTime.toLocalTime();
if (currentTime.isBefore(SHOP_OPEN_TIME) || currentTime.isAfter(SHOP_CLOSE_TIME)) {
throw new IllegalArgumentException("주문 시간이 아닙니다. 관리자에게 문의하세요.");
}
return new Order(LocalDateTime.now(), beverages);
}
다음과 같이 파라미터로 외부에서 주입받게 해두면 테스트하기 좋아짐
테스트하기 어려운 영역
관측할 때마다 다른 값에 의존하는 코드
현재 날짜/시간, 랜덤 값, 전역 변수/함수, 사용자 입력 등
외부 세계에 영향을 주는 코드
표준 출력(로그), 메시지 발송, 데이터베이스에 기록하기 등
순수 함수(pure functions)
같은 입력에는 항상 같은 결과
외부 세상과 단절된 형태
테스트하기 쉬운 코드
섹션4: TDD Test Drive Development
TDD: Test Driven Development
프로덕션 코드보다 테스트 코드를 먼저 작성하여 테스트가 구현 과정을 주도하도록 하는 방법론
테스트 작성 → 기능 구현
레드 그린 리팩토링
레드: 실프하는 테스트 작성 (실패를 봐라)
그린: 테스트 통과, 최소한의 코딩 (일단은 통과 시켜~)
리팩토링: 구현 코드 개선, 테스트 통과 유지
핵심 가치: 피드백
내가 작성하는 코드에 대해서 자주, 빠르게 피드백을 받을 수 있음
선 기능 구현, 후 테스트 작성의 문제점 (우리가 일반적으로 했던)
테스트 자체의 누락 가능성
특정 테스트(해피 케이스) 케이스만 검증할 가능성
잘못된 구현을 다소 늦게 발견할 가능성
선 테스트 작성, 후 기능 구현 (TDD 방식)
복잡도가 낮은(유연하며 유지보수가 쉬운) 테스트 가능한 코드로 구현할 수 있게 한다.
쉽게 발견하기 어려운 엣지(Edge) 케이스를 놓치지 않게 해준다.
구현에 대한 빠른 피드백을 받을 수 있다.
과감한 리팩토링이 가능해진다.
클라이언트 관점에서의 피드백을 주는 Test Driven
섹션5: 테스트는 []다
테스트는 []다.
테스트는 문서다
문서?
프로덕션 기능을 설명하는 테스트 코드 문서
다양한 테스트 케이스를 통해 프로덕션 코드를 이해하는 시각과 관점을 보완
어느 한 사람이 과거에 경험했던 고민의 결과물을 팀 차원으로 승격시켜서, 모두의 자산으로 공유할 수 있음
DisplayName을 섬세하게
명사의 나열보다 문장으로 작성
A이면 B이다
A이면 B가 아니고 C다
테스트 행위에 대한 결과까지 기술하면 더 좋음
<aside> 💡
음료 1개 추가 테스트 → ~테스트 지양하기
음료를 1개 추가할 수 있다.
음료를 1개 추가하면 주문 목록에 담긴다 (결과 기술)
</aside>
도메인 용어를 사용하여 한층 추상화된 내용을 담자
메서드 자체의 관점보다 도메인 정책 관점으로 작성
테스트의 현상을 중점으로 기술하지 말자
<aside> 💡
특정 시간 이전에 주문을 생성하면 실패한다. (AS-IS)
영업 시작 시간 이전에는 주문을 생성할 수 없다. (TO-BE)
</aside>
BDD(Behavior Driver Development) 스타일로 작성하기
TDD에서 파생된 개발 방법
함수 단위의 테스트에 집중하기보다, 시나리오에 기반한 테스트케이스(TC) 자체에 집중하여 테스트한다.
개발자가 아닌 사람이 봐도 이해할 수 있을 정도의 추상화 수준(레벨)을 권장
Given / When / Then
Given: 시나리오 진행에 필요한 모든 준비 과정 (객체, 값, 상태 등) → 어떤 환경에서
When: 시나리오 행동 진행 → 어떤 행동을 진행했을 때
Then: 시나리오 진행에 대한 결과 명시, 검증 → 어떤 상태 변화가 일어난다
→ DisplayName에 명확하게 작성할 수 있음
intelliJ → Live Templates → Java 단축키 등록
패키지 풀 네임까지 적어야 자동 import 됨
@org.junit.jupiter.api.DisplayName("")
@org.junit.jupiter.api.Test
void $METHOD_NAME$() {
// given
$END$
// when
// then
}
키워드 정리
Spock: groovy 기반 BDD 프레임워크
언어가 사고를 제한한다
한 번 언어로 규정해 놓으면 우리의 사고도 그에 맞춰 제한이 됨
명확하지 표현 못한 테스트 자체가 허들이 되고 사고를 제한할 수 있음
문서로써 테스트도 중요
섹션6: Spring & JPA 기반 테스트
Layered Architecture
보통 아래와 같이 계층을 나눔 → 관심사의 분리를 위해 (책임을 나누고 유지보수성 높이기)
테스트하기 어려운 부분을 분리해서 해당 부분만 집중하는 것은 동일
통합 테스트(Integration test)
여러 모듈이 협력하는 기능을 통합적으로 검증하는 테스트
일반적으로 작은 범위의 단위 테스트만으로는 기능 전체의 신뢰성을 보장할 수 없음
풍부한 단위 테스트 & 큰 기능 단위를 검증하는 통합 테스트
Spring/JPA 훑어보기 & 기본 엔티티 설계
Library VS Famework
라이브러리는 내 코드가 주체가 됨
필요한 기능이 있으면 외부에서 라이브러리를 끌어와서 도구로써 사용
프레임워크는 이미 동작하게끔 환경이 구성이 되어있음
내 코드는 수동적으로 프레임워크에 꽂혀서 이용이 됨
Spring Famework
IoC(Inversion Of Control)
제어의 역전
객체의 생명 주기 제어를 프레임워크가 맡아줌
DI(Dependency Injection)
의존성 주입
특정 객체를 바로 주입해주는게 아니라 인터페이스를 주입하여 약한 결합을 해주도록 활용
A는 B객체를 주입 받지만, 어떻게 받는지 관심없고 단지 주입만 받아 사용
AOP(Aspect Oriented Programming)
관점 지향 프로그래밍
비즈니스 흐름과 관계없는 부분을 관점을 한 데로 모아 활용
프록시 활용
트랜잭션, 로깅 등에 활용
ORM: Object-Relational-Mapping
기존 RDB 패러다임 불일치를 가운데에서 맞춰주게끔 해줌
객체 지향 팰러다임과 관계형 DB 패러다임의 불일치
이전에는 개발자가 객체의 데이터를 한땀한땀 매핑하여 DB에 저장 및 조회(CRUD)
ORM을 사용함으로써 개발자는 단순 작업을 줄이고, 비즈니스 로직에 집중할 수 있음
JPA(Java Persistence API)
자바 진영의 ORM 기술 표준
인터페이스이고, 여러 구현체가 있지만 보통 Hibernate를 많이 사용함
반복적인 CRUD SQL을 생성 및 실행해주고, 여러 부가 기능들을 제공함
편리하지만 쿼리를 직접 작성하지 않기 때문에, 어떤 식으로 쿼리가 만들어지고 실행되는지 명확하게 이해하고 있어야 함!
Spring 진영에서는 JPA를 한번 더 추상화한 Spring Data JPA 제공
QueryDSL과 조합하여 많이 사용함 (타입체크, 동적쿼리)
@Entity, @ID, @Column
@ManyToOne, @OneToMany, @OneToOne, @ManyToMany
ManyToMany는 일대다-다대일 관계로 풀어서 사용하길 권장!
댓글을 작성해보세요.