계층별 테스트 전략 #37
Replies: 3 comments
-
|
최근, "이 테스트 코드는 무엇을 보호하는가?"를 생각해보려고 노력하고 있습니다. 1. Domain변해서 안되는 도메인 규칙과 이 도메인의 역할과 책임을 보호하기 위해 테스트 코드를 작성합니다.
2. Service통합 테스트는 비즈니스 시나리오(흐름)를 보호합니다. 도메인과 저장소 같은 여러 객체가 협력할 때, 그 조합과 순서가 올바른 지 검증합니다.
3. Repository단순한 CRUD는 서비스 통합테스트로 처리합니다. (단순한 CRUD 테스트는 가치가 낮다고 판단) Repository를 모두 서비스 통합 테스트로 퉁치지 않는 이유
4. Controller주로 인수 테스트를 진행합니다. 사용자와 약속된 시나리오를 보호하기 위한 목적입니다.
|
Beta Was this translation helpful? Give feedback.
-
|
저는 "계층별 테스트 전략"이라는 질문 자체를 다시 보고 싶습니다. 계층(Controller / Service / Repository / Domain)을 먼저 두고 "이 계층은 어떻게 테스트할까"를 묻는 사고는, 사실 도구에서 출발해 상황을 매칭하는 사고입니다. 네오의 피드백을 참고해, 저는 사고 방향을 거꾸로 잡았습니다 — "이 계층은 어떻게 깨질 수 있나"를 먼저 묻고, 그 깨짐을 가장 싸게 잡는 자리로 도구를 역산하는 방식입니다. 그래서 제 전략은 "Controller는 슬라이스, Service는 통합, Repository는 슬라이스, Domain은 단위"라는 정적 매칭표가 아니라, 계층마다 회귀가 어떻게 드러나는지를 분석하고 거기서 도구를 골라낸 결과입니다. 1. 출발점 — 계층이 아니라 "깨짐"에서 시작한다이번 사이클을 거치며 가장 크게 바뀐 게 사고 방향입니다. 예전 방식 (매칭표 사고)
지금 방식 (보호 대상 역산)
이 절차의 핵심은 같은 계층이라도 깨짐의 종류에 따라 도구가 갈린다는 점입니다. Service의 분기 로직 회귀와 Service의 DB 협력 회귀는 같은 Service여도 다른 자리에서 잡힙니다. 그래서 "계층별 전략"이라는 말은 사실 단일한 답을 가질 수 없고, 계층 안에서도 회귀 종류별로 다시 갈라야 합니다. 2. Domain 계층 — 단위 테스트의 1순위 자리여기서 잡히는 회귀
도구 — 순수 단위 테스트외부 의존성 없이 객체만 @Test
void 과거_시점_예약은_거부된다() {
Clock fixed = Clock.fixed(...);
assertThatThrownBy(() -> new Reservation("moca", pastDateTime, theme, fixed))
.isInstanceOf(IllegalArgumentException.class);
}왜 이 자리인가
미션에서의 적용
기존엔 풀 컨텍스트 + 실제 H2 + insertTime까지 깔던 시간 의존 규칙을, 고정 Clock 주입만으로 검증하도록 내렸습니다. 분당 수십 배 빨라졌고 의도가 더 또렷해졌습니다.
순번 계산 규칙은 외부 상태 없이 입력만으로 결정되므로, 가장 두꺼운 검증을 여기에 두었습니다. 의도적으로 안 하는 것
3. Repository 계층 — 슬라이스 테스트가 유일하게 답하는 자리여기서 잡히는 회귀
도구 —
|
| 계층 | 회귀 종류 | 도구 | 자주 비우는 칸 |
|---|---|---|---|
| Domain | 비즈니스 규칙, 불변식 | 순수 단위 | 의존성 많은 도메인 (그건 도메인이 빈약하다는 신호) |
| Repository | SQL, 매핑, 경계 조건 | @jdbcTest 슬라이스 + H2 | 단순 위임 Repository, save 해피 패스 |
| Service (DB 협력) | 상태 의존 비즈니스 규칙 | @SpringBootTest 통합 | Mock 기반 단위 (호출 순서 검증화) |
| Service (분기) | boolean 응답에 대한 분기 | Mock + Repository 슬라이스 보장 | Repository가 단순할 때는 통합으로 흡수 |
| Service (트랜잭션) | 원자성, 롤백 | @SpringBootTest (테스트 @transactional 없이) | Mock 단위 (구조적으로 불가능) |
| Controller | HTTP 어댑터 동작 | 인수 테스트(RestAssured) | 단순 위임 Controller, @WebMvcTest 슬라이스 |
| 인수 | 핵심 시나리오, 인과관계 | RestAssured + 실제 H2 | 단위로 잡히는 케이스 반복 |
중요한 함의
이 표의 모든 칸을 채워야 한다고 보지 않습니다.
사이클1 네오 피드백에서 받아들인 것처럼, 토론조마다 다른 패턴이 나오는 게 정상입니다.
비즈니스 규칙이 풍부한 도메인이면 도메인 단위가 두꺼워지고, CRUD 중심이면 통합이 두꺼워집니다.
어느 칸을 비웠다면 **"왜 비웠는지"**를 설명할 수 있어야 합니다.
9. 도메인 성격에 따라 비율이 달라진다
저는 "계층별 비율을 미리 정해두고 그에 맞춰 채우는" 사고를 피합니다.
비율은 결과이지 목표가 아닙니다.
도메인이 풍부한 경우
체스 같은 게임, 복잡한 정책
- 도메인 단위 테스트 비중이 크다
- 비즈니스 규칙이 객체 안에 응집되어 있어 외부 의존 없이 검증 가능
CRUD 중심인 경우
이번 미션처럼 도메인 규칙이 얇고 DB 상태 변화가 많음
- 통합 테스트 비중이 크다
- 검증의 핵심이 "실제로 DB에 저장되고 조회되는가"라서 단위로는 못 잡는다
- 테스트 피라미드의 "단위 많이 / 통합 적게" 권장에 무조건 맞추려 하면 오히려 신뢰가 떨어진다
이번 예약 대기 미션은 후자입니다.
그래서 통합 테스트 비중을 의도적으로 키웠고, 그게 도메인 성격에 맞는 자연스러운 비율이라고 생각합니다.
10. 정리
계층별 테스트 전략의 본질은 "각 계층에 어떤 도구를 매칭할 것인가"가 아니라, "각 계층이 어떻게 깨질 수 있고 그 깨짐을 가장 싸게 잡는 자리는 어디인가"이다.
핵심 원칙들
- 출발점은 계층이 아니라 회귀 시나리오다. 보호 대상에서 도구를 역산한다
- Domain은 단위 테스트의 1순위 자리 — 외부 의존 없는 규칙을 가장 싸게 잡는다
- Repository는 슬라이스 + 실제 H2 — 우리가 작성한 SQL이 있을 때만. 프레임워크 위임은 비운다
- Service는 회귀 종류별로 나눠본다 — DB 협력은 통합, 단순 분기는 Mock 가능, 트랜잭션은 통합이 유일
- Controller는 인수 테스트가 대체 — 단순 위임 Controller에 별도 슬라이스는 중복
- 인수 테스트는 핵심 시나리오만 — 인과관계가 검증 대상일 때
- 같은 보호를 두 번 사지 않는다 — 더 싼 자리에서 한 번, 다른 자리는 다른 회귀를 잡아야 의미가 있다
- 비율은 도메인 성격이 결정한다 — 미리 정해두고 채우는 게 아니다
그리고 무엇보다, 이 전략은 정적인 매뉴얼이 아니라 매번 새로 적용하는 사고 절차입니다.
새로운 기능을 만들 때마다 "이게 어떻게 깨질 수 있나"부터 묻고, 거기서 도구를 골라냅니다.
매트릭스의 칸이 채워지는 건 그 결과이고, 비어 있는 칸은 "일부러 비운 것 — 이유는 X"로 설명되어야 한다고 생각합니다.
Beta Was this translation helpful? Give feedback.
-
|
어떤 테스트를 사용할 지를 결정하는 것은 결국, 어떤 코드와 범위를 검증할지에 따라 달라지기에, 결국 테스트의 목적에 따라 결정해야한다고 생각합니다. 핵심 기준은 이와 같지만, 각 계층별 특징을 통해 일반적으로 제가 사용하는 테스트 방식을 아래 작성해보았습니다! (테스트의 분류 기준에 대한 제 생각은 해당 글에 담았습니다. 이를 기반으로 아래 내용을 작성하였습니다.) 하지만, 더 실용적인 기준은 테스트 또한 유지보수의 영역이기 때문에, 테스트로 얻을 수 있는 신뢰가 유지 비용보다 더 큰가도 고민해볼 필요가 있다고 생각합니다. Controller 요청 진입점요청이 들어오고 나가는 Controller의 핵심 책임은 요청 값을 어플리케이션이 이해할 수 있도록 해석하는 것(객체로 변환하는 것)과 응답을 올바른 포멧으로 클라이이언트에게 전달해주는 것이라고 생각합니다. 따라서 저는 Controller에서는 요청/응답값 자체가 올바르게 파싱되는지에 대한 단위 테스트와 http와 웹 서버 단계의 연결이 올바르게 되는지와 같은 슬라이스테스트도 필요하다고 생각합니다. Service 유스케이스 검증서비스 계층은 유스케이스를 책임지는 계층으로, 비즈니스 흐름이 올바르게 작성되었는지 검증해야 합니다. 특히 서비스 계층은 유스케이스에 맞게, 정합성이 유지되어야 하는 한 단위의 트랜잭션이 표현되는 계층이기도 합니다. 따라서, 트랜잭션이 올바르게 작동하는지 슬라이스 테스트나 데이터베이스 연결을 통해 정합성이 유지되는지와 같은 통합테스트도 필요합니다. 또한 Service 내부에서 던져지는 예외에 대한 단위테스트 검증도 유용할 수 있다고 생각합니다. 하지만 너무 많은 테스트는 유지보수 비용이 발생할 수 있다는 생각이 들어, 단순 예외가 잘 던저지는지에 대한 검증은 Controller와의 통합테스트로 대체할 수 있다고도 생각합니다. Repository DB 연결 검증레포지토리는 DB와 연결되는 레이어로 보통 SQL이 올바르게 작동하는지, DB와 어플리케이션이 올바르게 매핑되는지에 대한 슬라이스 테스트, 통합테스트가 필요하다고 생각합니다. 그리고 단순 SQL문이 작동하는지 인메모리DB에 붙여 진행하는 테스트도 진행할 수 있겠지만, 실제 운영 데이터베이스와 스펙이나 엔진이 다른 경우 불안한 테스트가 될 수 있고, 유지보수 비용 또한 증가할 수 있다고 보아 작성을 지양합니다. Domain 검증핵심 로직이 담기는 계층으로 가장 우선적으로 검증되어야 하는 영역입니다. 외부 컴포넌트의 의존성 없이도 올바르게 비즈니스 규칙을 준수하는지 테스트하기 위해 핵심기능에 대한 단위테스트가 반드시 필요하다고 생각합니다. 그리고 계층별 테스트 작성단계는 좁은 범위의 도메인 검증부터 시작해서 넓은 범위인 컨트롤러 그리고 통합, 인수테스트순으로 작성되야 한다고 생각합니다. |
Beta Was this translation helpful? Give feedback.
Uh oh!
There was an error while loading. Please reload this page.
-
본인 만의 계층별 테스트 전략에 대해서 서술해주세요.
Beta Was this translation helpful? Give feedback.
All reactions