본문 바로가기
개발 잡담

테스트하기 좋은 코드...?

by 휴일이 2026. 5. 5.

 

요즘 테스트하기 좋은 코드에 대한 고민이 많아서

DDD 책도 다시 읽고 테스트 코드 관련 책도 읽어서 회사에서 문서로 정리하여 올렸었는데

블로그에도 좀 간단히 올려봤습니다. (회사 코드는 뺐어요..ㅎㅎ)

 

테스트는 왜 필요할까?

  1. 회귀 버그를 방지.
  2. 구성원들이 코드를 작성한 개발자의 의도를 이해할 수 있도록 함.
  3. 좋은 설계를 고민할 수 있게 함.

→ 이 중, 좋은 설계를 고민할 수 있게 한다는 것은 무엇일까?

테스트 피라미드

좋은 설계를 고민하기 전에,,, 테스트 피라미드라는 것이 있음.

테스트를 세 가지로 분류한 것임.

3단 분류 체계

  • Unit Test: 작은 단위의 테스트 (통상적으로는 함수, 메서드, 클래스..)
  • Integration Test: 여러 컴포넌트나 객체가 협력하는 상황을 검증.
  • E2E: 실제로 사용자 시나리오에서 프로그램이 어떻게 동작하는지 검증. (예: API 테스트)

다만, 실무에서는 어디까지를 Unit Test로 봐야 하는지 애매한 경우가 많음..

 

구글의 테스트 분류

그래서 Google은 테스트를 “무엇을 테스트하느냐”보다 “어떤 환경에서 실행되느냐”를 기준으로 다시 나누었음..!

  • 소형 테스트: 단일 서버, 단일 프로세스, 단일 스레드에서 동작하며, 디스크I/O 및 블록킹 호출(Blocking call) 이 없는 테스트.
  • 중형 테스트: 단일 서버이나, 멀티 프로세스, 멀티 스레드로 동작하는 테스트.
  • 대형 테스트: 멀티 서버에서 동작하는 테스트.
  멀티 스레드 멀티 프로세스 멀티 서버
소형 테스트 X X X
중형 테스트 O O X
대형 테스트 O O O

왜 이런 분류 체계를 사용했을까?

  1. 테스트가 결정적(deterministic)인가?
  2. 테스트의 속도가 빠른가?
  • 구글은 테스트를 분류할 때 특히 결정성과 속도를 중요하게 보았다. 그래서 가능하다면 빠르고 결정적인 소형 테스트를 많이 확보하는 방향을 선호했다.
💡이상적으로 셋의 비율은  소형 : 중형 : 대형 → 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 없이, 순수 자바 객체만으로 테스트 가능.

테스트 하기 좋은 코드의 특징은?

  1. 의존성이 적다.
  2. 입력과 출력이 명확하다.
  3. 작은 단위로 나뉘어 있다.
  4. 비즈니스 로직이 한 곳에 모여 있다.

한 줄씩 살펴봅시다.

의존성이 적다

의존성이 적다는 것은 외부 의존성이 적다는 것을 뜻함.

  • DB
  • 네트워크
  • 프레임워크 (Spring, fastApi 등)
  • 외부 라이브러리

→ 소형 테스트의 특징과 비슷함.
→ 단일 서버, 단일 프로세스, 단일 스레드에서 동작하며, 디스크I/O 및 블록킹 호출(Blocking call) 이 없는 테스트.

입력과 출력이 명확하다.

-> 즉, 결과를 예측 가능하다!

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

작은 단위로 나뉘어 있다.

→ 하나의 테스트가 하나의 책임만 검증한다.

  • 만약 backup_filerename_file 이 합쳐 파일 이름 변경 + 파일 백업을 같이 진행했다면?

비즈니스 로직이 한 곳에 모여있다

RefreshTokenPolicy

RefreshTokenPolicyTest

→ 응집도가 높아야 테스트가 쉬워집니다!

  • 관련된 로직이 한 곳에 모여 있어야 테스트도 쉬워진다.
  • 로직이 여러 곳에 흩어져 있으면 테스트도 여러 군데를 동시에 건드려야 한다. ㅠ

결국 테스트하기 좋은 설계의 핵심은

  1. 계산과 부수효과를 분리한다.
  2. 외부 의존성은 내부에서 만들지 않고 주입받는다.
  3. 비즈니스 규칙은 한 곳에 응집시킨다.
  4. 입력과 출력이 명확한 작은 단위로 나눈다.

  • 테스트를 어렵게 만드는 코드는 보통 사람도 이해하기 어렵다. 반대로 테스트하기 쉬운 코드는 의도가 분명하고 책임이 선명하다.



다만 너무 극단적으로 생각하지는 말자

SW 개발에서 *은탄환은 없다는 말이 있습니다.
*복잡한 문제를 한 번에 해결해주는 마법 같은 해법은 없다는 뜻입니다.

 

이 문서에서는 이해를 돕기 위해 좋은 설계와 나쁜 설계를 비교하며 설명했지만,  

실제 개발에서는 모든 상황에 적용할 수 있는 하나의 정답은 존재하지 않습니다.

 

예를 들어, 한 달만 사용하고 폐기될 애플리케이션이라면 테스트 코드나 설계에 많은 시간을 투자하기보다는

빠르게 동작하는 결과를 만드는 것이 더 합리적인 선택일 수도 있습니다.

 

개발은 상황과 목적에 따라 접근 방식이 달라집니다…!!!

따라서 “반드시 이렇게 해야 한다”기보다는 “이런 방식도 있다”는 하나의 관점으로 받아들여 주시면 좋겠습니다.





 

 

부록: 도메인 주도 설계에 관하여…

DDD(도메인 주도 설계)에 관한 지식이 없는 분들이 읽어보시면 좋습니다.
최대한 간략히, SW 개발자 입장에서 설명하도록 노력했습니다.

DDD (도메인 주도 설계)

도메인 주도 설계란 무엇일까요?

  • 목적: 코드가 아니라 비즈니스 가 중심이 되는 것.
  • 문제: 코드가 비즈니스를 반영하지 못 함.

→ 음... 그래서 그게 뭔데?

DDD 의 목적

복잡한 비즈니스 로직을 망가지지 않게, 이해 가능하고 확장 가능하게 만드는 것이 DDD의 목적입니다.

도메인 개념이 통일되어야 한다.

  • 기획: 결제 완료가 되어야 합니다.
  • 개발: order_status == COMPLETE 이 되어야겠네요.
  • 디자인: 주문 확정 디자인은 이렇게면 될까요?
  • 도메인에 대한 공통 언어(Ubiquitous Language)가 없으면 동일한 개념이 각 역할마다 다르게 표현되고 해석되어 커뮤니케이션 비용이 증가하고 모델의 일관성이 깨집니다. 결과적으로는 소프트웨어의 일관성과 방향성이 흐트러집니다.
  • 도메인 모델에는 다양한 정의가 존재하는데 기본적으로 도메인 모델은 특정 도메인을 개념적으로 표현한 것입니다.

예시

온라인 쇼핑몰의 주문 도메인이라면…?

→ 주문을 하려면

  • 상품을 몇 개 살지 선택
  • 배송지를 입력
  • 선택한 상품 가격을 이용해서 총 지불 금액을 계산
  • 금액 지불을 위한 결제 수단을 선택
  • 배송 상태를 변경

사진 출처: 도메인 주도 개발 시작하기(최범균)

  • 이렇게 한 주문(Order) 도메인에 엮여있는 도메인 모델이 많습니다.
  • “그렇다면 이 주문 도메인의 막중한 책임을, 서브 도메인에 위임할 순 없을까?

그렇다면 DDD 로 개발한다는 것은 무엇일까?

어린이도 이해할 수 있도록 DDD를 설명해주세요!

출처:어린이도 이해할 수 있게 설명해 줘: 도메인 주도 설계(DDD)가 정확히 뭐임?

  • 요약하면 도메인(비즈니스) 개념을 기준으로 역할과 책임을 나누는 것
  • 코드에서는 각각의 역할과 책임을 분리한다는 것

혹시 이런 생각이 든다면?

“메인 도메인(Order)의 책임을 도메인 모델(OrderState, Orderer, ShippingInfo 등등..)들에게 위임할 수 없을까?”

  • 여러분은 이미 DDD적인 사고를 하고 있습니다…..!!! 😉
728x90
반응형