
요즘 테스트하기 좋은 코드에 대한 고민이 많아서
DDD 책도 다시 읽고 테스트 코드 관련 책도 읽어서 회사에서 문서로 정리하여 올렸었는데
블로그에도 좀 간단히 올려봤습니다. (회사 코드는 뺐어요..ㅎㅎ)
테스트는 왜 필요할까?
- 회귀 버그를 방지.
- 구성원들이 코드를 작성한 개발자의 의도를 이해할 수 있도록 함.
- 좋은 설계를 고민할 수 있게 함.
→ 이 중, 좋은 설계를 고민할 수 있게 한다는 것은 무엇일까?
테스트 피라미드
좋은 설계를 고민하기 전에,,, 테스트 피라미드라는 것이 있음.
테스트를 세 가지로 분류한 것임.
3단 분류 체계

- Unit Test: 작은 단위의 테스트 (통상적으로는 함수, 메서드, 클래스..)
- Integration Test: 여러 컴포넌트나 객체가 협력하는 상황을 검증.
- E2E: 실제로 사용자 시나리오에서 프로그램이 어떻게 동작하는지 검증. (예: API 테스트)
→ 다만, 실무에서는 어디까지를 Unit Test로 봐야 하는지 애매한 경우가 많음..
구글의 테스트 분류

그래서 Google은 테스트를 “무엇을 테스트하느냐”보다 “어떤 환경에서 실행되느냐”를 기준으로 다시 나누었음..!
- 소형 테스트: 단일 서버, 단일 프로세스, 단일 스레드에서 동작하며, 디스크I/O 및 블록킹 호출(Blocking call) 이 없는 테스트.
- 중형 테스트: 단일 서버이나, 멀티 프로세스, 멀티 스레드로 동작하는 테스트.
- 대형 테스트: 멀티 서버에서 동작하는 테스트.
| 멀티 스레드 | 멀티 프로세스 | 멀티 서버 | |
| 소형 테스트 | X | X | X |
| 중형 테스트 | O | O | X |
| 대형 테스트 | O | O | O |
왜 이런 분류 체계를 사용했을까?
- 테스트가 결정적(deterministic)인가?
- 테스트의 속도가 빠른가?
- 구글은 테스트를 분류할 때 특히 결정성과 속도를 중요하게 보았다. 그래서 가능하다면 빠르고 결정적인 소형 테스트를 많이 확보하는 방향을 선호했다.
💡이상적으로 셋의 비율은 소형 : 중형 : 대형 → 80 : 15 : 5 라고!
- 물론 모든 코드를 소형 테스트로만 검증할 수는 없지만.. 중요한 비즈니스 규칙일수록 가능하면 빠르고 결정적인 소형 테스트로 검증할 수 있는 구조가 유리하다!
테스트하기 좋은 설계란 무엇일까?
안 좋은 설계의 예시
제가 진행하고 있는 mmebot 의 v1.0.0 버전 코드에서 예를 들어보장. 😅
- 아래는 인증 책임이 있는 AuthService 의 코드...
왜 이런 구조가 문제일까?

단순히 코드 줄 수가 길어서가 아니다! 위의 코드로 테스트를 작성하려고 하면 문제가 드러난다.
- 유저 조회 mocking
- passwordEncoder mocking
- roleRepository mocking
- jwtTokenService mocking
→ 문제는 로그인 기능 “전체”를 테스트하고 싶은 게 아니라 로그인 안에 있는 “비밀번호가 틀리면 예외가 발생한다.”같은 규칙을 테스트하려고 하는데도 너무 많은 의존성과 책임이 함께 따라온다는 것이다.
→ 즉, 패스워드 검증이라는 작은 규칙 하나를 확인하고 싶어도 너무 많은 외부 요소를 함께 통제해야 함..
즉, 이 코드는 테스트하기 어려운 구조이다.
- 서비스 레이어에 비즈니스 로직과 DB 접근 로직이 섞여 있으면 도메인이 인프라에 오염된 것입니다. 이를 격리하는 것이 곧 테스트하기 좋은 설계의 시작입니다!
💡 “문제는 책임의 개수가 아니라, 서로 다른 관심사의 책임이 몰려있다는 것.”
그렇다면 책임을 나눠보자
- 위의 코드에서 “로그인 대상 사용자를 식별하고 인증 가능 상태인지 점검”하는 코드를 비교해보자!
mmebot 로그인 코드

왜 검증 로직을 User 도메인 안에 둘까?
- “사용자가 로그인 가능한 상태인가?”는 User가 가장 잘 알고 있는 정보이기 때문.
- User 에 대한 검증은 외부 서비스가 판단할 것이 아니라 User 도메인 모델 스스로가 책임져야 하는 규칙인 것..!!
💡 DDD에서는 이런 규칙을 도메인 규칙(비즈니스 규칙)이라고 한다.
도메인 규칙이 Service 레이어에 흩어져 있으면?
- 어디에 어떤 검증이 있는지 파악하기 어렵고
- 같은 검증 로직이 여러 곳에 중복되기 쉬움
→ 나쁜 설계
도메인 규칙을 도메인 모델 안에 위치시키면
- 관련된 규칙이 한 곳에 모이고
- 변경이 발생했을 때 수정 범위가 명확해짐
→ 좋은 설계
이 규칙은 테스트에도 큰 영향을 준다!!
User 내부에 검증 로직이 있으면 User 객체만 가지고도 로그인 검증을 테스트할 수 있음.

- 게다가 port 를 사용하고 있기 때문에 signIn 을 직접 테스트할 때도 mock 객체가 아니라 port 객체를 테스트용으로 구현하여 사용 가능
→ 테스트가 훨씬 단순해짐 !!!!!
💡 위의 방법은 DDD(도메인 주도 설계)를 활용하는 방법으로 도메인(비즈니스) 개념을 기준으로 역할과 책임을 나누는 것
→ 스프링 컨테이너 없이, DB 없이, 순수 자바 객체만으로 테스트 가능.
테스트 하기 좋은 코드의 특징은?
- 의존성이 적다.
- 입력과 출력이 명확하다.
- 작은 단위로 나뉘어 있다.
- 비즈니스 로직이 한 곳에 모여 있다.
한 줄씩 살펴봅시다.
의존성이 적다
의존성이 적다는 것은 외부 의존성이 적다는 것을 뜻함.
- DB
- 네트워크
- 프레임워크 (Spring, fastApi 등)
- 외부 라이브러리
→ 소형 테스트의 특징과 비슷함.
→ 단일 서버, 단일 프로세스, 단일 스레드에서 동작하며, 디스크I/O 및 블록킹 호출(Blocking call) 이 없는 테스트.
입력과 출력이 명확하다.


-> 즉, 결과를 예측 가능하다!
- backup_file 에서 timestamp 를 외부에서 받을 수 있기 때문에 입력 / 출력을 명확히 할 수 있다.
- 실제로 test_backup_file 에서 assert 로 테스트 성공 여부 확인 시, 파일 이름이 예측가능하므로 직접 대조하여 확인 가능.
- 비결정적 요소(시간, 랜덤, 네트워크)를 인터페이스화하여 제어 가능한 영역으로 끌어들이는 것이 중요!!
작은 단위로 나뉘어 있다.


→ 하나의 테스트가 하나의 책임만 검증한다.
- 만약 backup_file 과 rename_file 이 합쳐 파일 이름 변경 + 파일 백업을 같이 진행했다면?
비즈니스 로직이 한 곳에 모여있다
RefreshTokenPolicy

RefreshTokenPolicyTest

→ 응집도가 높아야 테스트가 쉬워집니다!
- 관련된 로직이 한 곳에 모여 있어야 테스트도 쉬워진다.
- 로직이 여러 곳에 흩어져 있으면 테스트도 여러 군데를 동시에 건드려야 한다. ㅠ
결국 테스트하기 좋은 설계의 핵심은
- 계산과 부수효과를 분리한다.
- 외부 의존성은 내부에서 만들지 않고 주입받는다.
- 비즈니스 규칙은 한 곳에 응집시킨다.
- 입력과 출력이 명확한 작은 단위로 나눈다.
- 테스트를 어렵게 만드는 코드는 보통 사람도 이해하기 어렵다. 반대로 테스트하기 쉬운 코드는 의도가 분명하고 책임이 선명하다.
다만 너무 극단적으로 생각하지는 말자
SW 개발에서 *은탄환은 없다는 말이 있습니다.
*복잡한 문제를 한 번에 해결해주는 마법 같은 해법은 없다는 뜻입니다.
이 문서에서는 이해를 돕기 위해 좋은 설계와 나쁜 설계를 비교하며 설명했지만,
실제 개발에서는 모든 상황에 적용할 수 있는 하나의 정답은 존재하지 않습니다.
예를 들어, 한 달만 사용하고 폐기될 애플리케이션이라면 테스트 코드나 설계에 많은 시간을 투자하기보다는
빠르게 동작하는 결과를 만드는 것이 더 합리적인 선택일 수도 있습니다.
개발은 상황과 목적에 따라 접근 방식이 달라집니다…!!!
따라서 “반드시 이렇게 해야 한다”기보다는 “이런 방식도 있다”는 하나의 관점으로 받아들여 주시면 좋겠습니다.
부록: 도메인 주도 설계에 관하여…
DDD(도메인 주도 설계)에 관한 지식이 없는 분들이 읽어보시면 좋습니다.
최대한 간략히, SW 개발자 입장에서 설명하도록 노력했습니다.
DDD (도메인 주도 설계)
도메인 주도 설계란 무엇일까요?
- 목적: 코드가 아니라 비즈니스 가 중심이 되는 것.
- 문제: 코드가 비즈니스를 반영하지 못 함.
→ 음... 그래서 그게 뭔데?
DDD 의 목적
복잡한 비즈니스 로직을 망가지지 않게, 이해 가능하고 확장 가능하게 만드는 것이 DDD의 목적입니다.
도메인 개념이 통일되어야 한다.
- 기획: 결제 완료가 되어야 합니다.
- 개발: order_status == COMPLETE 이 되어야겠네요.
- 디자인: 주문 확정 디자인은 이렇게면 될까요?
- 도메인에 대한 공통 언어(Ubiquitous Language)가 없으면 동일한 개념이 각 역할마다 다르게 표현되고 해석되어 커뮤니케이션 비용이 증가하고 모델의 일관성이 깨집니다. 결과적으로는 소프트웨어의 일관성과 방향성이 흐트러집니다.
- 도메인 모델에는 다양한 정의가 존재하는데 기본적으로 도메인 모델은 특정 도메인을 개념적으로 표현한 것입니다.
예시
온라인 쇼핑몰의 주문 도메인이라면…?
→ 주문을 하려면
- 상품을 몇 개 살지 선택
- 배송지를 입력
- 선택한 상품 가격을 이용해서 총 지불 금액을 계산
- 금액 지불을 위한 결제 수단을 선택
- 배송 상태를 변경

사진 출처: 도메인 주도 개발 시작하기(최범균)
- 이렇게 한 주문(Order) 도메인에 엮여있는 도메인 모델이 많습니다.
- “그렇다면 이 주문 도메인의 막중한 책임을, 서브 도메인에 위임할 순 없을까?
그렇다면 DDD 로 개발한다는 것은 무엇일까?
어린이도 이해할 수 있도록 DDD를 설명해주세요!

출처:어린이도 이해할 수 있게 설명해 줘: 도메인 주도 설계(DDD)가 정확히 뭐임?
- 요약하면 도메인(비즈니스) 개념을 기준으로 역할과 책임을 나누는 것
- 코드에서는 각각의 역할과 책임을 분리한다는 것
혹시 이런 생각이 든다면?
“메인 도메인(Order)의 책임을 도메인 모델(OrderState, Orderer, ShippingInfo 등등..)들에게 위임할 수 없을까?”
- 여러분은 이미 DDD적인 사고를 하고 있습니다…..!!! 😉
'개발 잡담' 카테고리의 다른 글
| 김영한의 실전 데이터베이스 - 설계 1편 후기 (0) | 2026.03.17 |
|---|---|
| 2년차 개발자가 되어버린 나. 회고록. (1) | 2026.02.06 |
| @Transactional 은 최소화, fetch join 최대화 (1) | 2025.12.18 |
| DB 논리적 제약 조건일 때 애플리케이션에 검증 코드를 추가해야 하는가? (0) | 2025.12.18 |
| Virtual Thread 가 뭣이당가? (1) | 2025.10.05 |