🎁[속보] 인프런 내 깜짝 선물 출현 중🎁

인프런 워밍업 클럽 스터디 3기 - 백엔드 클린 코드, 테스트 코드 4주차 발자국

인프런 워밍업 클럽 스터디 3기 - 백엔드 클린 코드, 테스트 코드 4주차 발자국

image

image

Readable Code: 읽기 좋은 코드를 작성하는 사고법

Practical Testing: 실용적인 테스트 가이드

강의와 함께한 인프런 워밍업 클럽 스터디 3기 - 백엔드 클린 코드, 테스트 코드 (Java, Spring Boot)

4주차 발자국 입니다.


학습 내용 요약

이번 주에는 Practical Testing: 실용적인 테스트 가이드 강의에서 계층형 아키텍처(Layered Architecture) 의 개념을 다시 한번 정리하고, 스프링 기반 테스트에서 자주 사용하는 Mock/Spy 기법과 관련 애너테이션들을 배웠습니다.

  • Layered Architecture (계층형 아키텍처)

    • Presentation Layer: 사용자 입력과 응답을 담당. API 요청/응답 혹은 프론트엔드 인터페이스를 제공하며, 기본적인 파라미터 검증과 변환만 수행.

    • Business Logic Layer: 애플리케이션의 핵심 비즈니스 로직과 규칙을 구현. 서비스(Service)나 도메인(Domain) 로직이 위치하며, 트랜잭션 관리의 주된 대상이 됨.

    • Persistence Layer: 데이터베이스/파일시스템 등 영속성 저장소와 직접적으로 상호작용. Repository(DAO) 형태로 CRUD를 담당.

  • Mockito & Spring Test 관련 애너테이션

    • @Mock / @Spy / @InjectMocks: 순수 Mockito 환경(스프링 컨텍스트 없이)에서 가짜 객체(또는 부분 가짜 객체)를 만들어 단위 테스트를 쉽게 작성할 수 있게 함.

    • @MockBean / @SpyBean: 스프링 애플리케이션 컨텍스트를 기동하는 통합 테스트 환경에서 특정 Bean을 Mock 또는 Spy로 교체하여, 외부 의존성을 간단히 제어하고자 할 때 활용.

  • 테스트 설계 방향

    • 각 레이어별로 테스트 범위를 명확히 하여, “Persistence → Business → Presentation” 순으로 책임을 분리해 점진적으로 테스트 커버리지를 넓힘.

    • @MockBean / @SpyBean을 과도하게 사용하지 않도록 주의: 꼭 필요한 부분만 모의(Mock/Spy) 처리하고, 나머지는 실제 로직을 이용해 통합 테스트를 안정적으로 수행.

    • 테스트 시나리오별로 @BeforeEach를 활용하는 방법과, 각 테스트 메서드에서 독립적으로 given-when-then을 구성하는 방법을 비교.

학습 회고

  • 얻은 인사이트

    • 레이어별 책임과 역할이 분명해야 복잡한 서비스에서도 테스트 설계가 한결 명확해짐을 느꼈습니다.

    • Mock/Spy를 올바른 상황에서만 사용하면 외부 의존성을 최소화하여 빠른 단위 테스트를 작성할 수 있으나, 반대로 불필요하게 남발하면 테스트 유지보수가 어려워짐을 체감했습니다.

  • 어려웠던 점

    • @MockBean과 @SpyBean을 사용해 스프링 컨텍스트를 띄울 때 테스트 속도가 느려지거나, 동일 타입 Bean이 여러 개일 때 어느 Bean을 대체하는지 혼동될 수 있었습니다.

    • 레이어별 테스트를 작성하면서, 중복으로 보이는 코드(예: 테스트 준비 로직)를 어느 정도까지 @BeforeEach로 추출해야 할지 고민이 되었습니다.


미션

🎯 Day16 미션: Layered Architecture 구조의 레이어별 특징과 테스트 방법

요구사항
Layered Architecture에서 각 레이어가 하는 역할, 특징, 그리고 테스트 방식을 자기만의 언어로 정리하기

나만의 정리

  1. Presentation Layer

    • 역할: 사용자나 외부 시스템의 요청을 받고 응답을 전달. 파라미터 검증, DTO 변환, HTTP 상태 코드 결정 등을 담당.

    • 특징: 비즈니스 로직을 직접 수행하지 않고, 유효성 체크 후 Service 호출 → 결과 반환에 집중.

    • 테스트 방법: 컨트롤러 통합 테스트(@SpringBootTest, @WebMvcTest + MockMvc)를 통해 실제 요청/응답 형식을 모의하거나, 프론트엔드와의 e2e 테스트를 진행해볼 수도 있음.

  2. Business Logic Layer

    • 역할: 애플리케이션의 핵심 규칙, 알고리즘, 트랜잭션 등의 로직 담당.

    • 특징: 여러 Repository를 조합하여 도메인 로직을 수행하고, 예외 처리, Validation 등의 업무 로직을 포괄적으로 관리.

    • 테스트 방법: 단위 테스트를 통해 특정 비즈니스 로직의 정확성을 검증. 필요 시 Repository를 Mock 처리(예: @Mock, @MockBean)하여 DB 의존성을 제거하고 로직만 집중 테스트.

  3. Persistence Layer

    • 역할: 데이터의 저장/조회/수정/삭제 등 영속성 처리에 집중.

    • 특징: 쿼리 작성, DB 연결, CRUD 로직을 추상화한 Repository/DAO 형태로 제공.

    • 테스트 방법: 실제 DB 혹은 In-Memory DB(H2 등)를 활용한 통합 테스트(@DataJpaTest). 쿼리 정확도, 트랜잭션 처리, 성능 등을 검증하기에 좋음.

🎯 Day18 미션 1: @Mock, @MockBean, @Spy, @SpyBean, @InjectMocks의 차이점 정리

요구사항
각 애너테이션이 어떤 환경(단위 테스트/통합 테스트)에서 사용되고, 어떤 특징이 있는지 정리하기

1) @Mock

  • 주로 사용 환경: Mockito (단위 테스트)

  • 스프링 빈 등록 여부: X (별도 Bean 아님)

  • 특징:

    • 순수 Mock 객체를 생성하여, 내부 로직 없이 호출 기록/결과(Stubbing)만 지정할 수 있음

    • 단위 테스트에서 외부 의존성을 배제하고 특정 로직만 검증할 때 유용

2) @MockBean

  • 주로 사용 환경: Spring Boot Test(통합 테스트)

  • 스프링 빈 등록 여부: O (빈으로 등록됨)

  • 특징:

    • 스프링 애플리케이션 컨텍스트에 등록된 실제 Bean을 Mock으로 교체

    • 다른 Bean이 해당 Bean을 의존하고 있으면, 그 의존성도 가짜(Mock)로 처리됨

    • @SpringBootTest나 @WebMvcTest 등 스프링 컨텍스트가 뜨는 환경에서 사용

3) @Spy

  • 주로 사용 환경: Mockito (단위 테스트)

  • 스프링 빈 등록 여부: X (별도 Bean 아님)

  • 특징:

    • 부분 가짜(Spy) 객체를 생성

    • 기본적으로 실제 메서드 동작을 유지하면서, 필요한 부분만 가짜(Stubbing)로 설정 가능

    • 복잡한 객체를 테스트할 때, 일부는 실제 로직을 실행하고 일부만 Mock 처리할 수 있어 유연함

4) @SpyBean

  • 주로 사용 환경: Spring Boot Test(통합 테스트)

  • 스프링 빈 등록 여부: O (빈으로 등록됨)

  • 특징:

    • 스프링 컨텍스트 내 실제 Bean을 Spy로 교체

    • 기본적으로는 실제 로직을 수행하되, 특정 메서드만 가짜로 동작시키거나 호출을 기록할 수 있음

    • 통합 테스트 환경에서 부분 Mocking이 필요할 때 적합

5) @InjectMocks

  • 주로 사용 환경: Mockito (단위 테스트)

  • 스프링 빈 등록 여부: 해당 없음

  • 특징:

    • @Mock 또는 @Spy로 만들어진 객체들을 자동으로 주입(생성자, 세터, 필드 순)해줌

    • 예: @InjectMocks UserService라면, UserService가 의존하는 Repository나 다른 객체들을 @Mock으로 생성해 주입받을 수 있음

    • 단위 테스트에서 여러 의존 객체를 편리하게 Mock/Spy로 대체할 수 있도록 지원

       

🎯 Day18 미션 2: 테스트 시나리오(@BeforeEach, given, when, then) 구성

요구사항
아래 3개의 테스트 메서드가 있을 때, “어떤 준비 로직을 @BeforeEach로 뽑고, 어떤 부분을 각 테스트의 given절에 둘지”, “when절을 어떻게 구성할지”를 구상해보기

예시 테스트 3종

  1. 사용자가 댓글을 작성할 수 있다.

  2. 사용자가 댓글을 수정할 수 있다.

  3. 자신이 작성한 댓글이 아니면 수정할 수 없다.

나의 구성

@BeforeEach
void init() {
    // 공통 데이터를 미리 생성하지 않습니다.
    /*
     * 각 테스트가 각각의 상황(사용자, 게시물, 댓글)을 자유롭게 구성할 수 있도록,
     * 여기서는 사전에 아무것도 세팅해두지 않습니다.
     * 테스트마다 필요한 데이터 타입이나 조건이 다를 수 있으므로,
     * 각 테스트 메서드 내부에서 직접 객체를 생성하고 준비해 주는 방식을 채택했습니다.
     */
}

@DisplayName("사용자가 댓글을 작성할 수 있다.")
@Test
void writeComment() {
    // given
    // 1-1. 사용자 생성에 필요한 내용 준비
    // 1-2. 사용자 생성
    // 1-3. 게시물 생성에 필요한 내용 준비
    // 1-4. 게시물 생성
    // 1-5. 댓글 생성에 필요한 내용 준비

    // when
    // 1-6. 댓글 생성

    // then
    // 검증
}

@DisplayName("사용자가 댓글을 수정할 수 있다.")
@Test
void updateComment() {
    // given
    // 2-1. 사용자 생성에 필요한 내용 준비
    // 2-2. 사용자 생성
    // 2-3. 게시물 생성에 필요한 내용 준비
    // 2-4. 게시물 생성
    // 2-5. 댓글 생성에 필요한 내용 준비
    // 2-6. 댓글 생성

    // when
    // 2-7. 댓글 수정

    // then
    // 검증
}

@DisplayName("자신이 작성한 댓글이 아니면 수정할 수 없다.")
@Test
void cannotUpdateCommentWhenUserIsNotWriter() {
    // given
    // 3-1. 사용자1 생성에 필요한 내용 준비
    // 3-2. 사용자1 생성
    // 3-3. 사용자2 생성에 필요한 내용 준비
    // 3-4. 사용자2 생성
    // 3-5. 사용자1의 게시물 생성에 필요한 내용 준비
    // 3-6. 사용자1의 게시물 생성
    // 3-7. 사용자1의 댓글 생성에 필요한 내용 준비
    // 3-8. 사용자1의 댓글 생성

    // when
    // 3-9. 사용자2가 사용자1의 댓글 수정 시도

    // then
    // 검증
}
  • @BeforeEach

    • 정말 모든 테스트에 공통으로 필요한 세팅(예: DB clean-up, 동일한 테스트 데이터 세팅 등)이면 이곳에 배치.

    • 하지만 테스트마다 시나리오가 크게 다른 경우, 오히려 각 테스트 내부(given 절)에서 데이터를 생성하는 편이 가독성과 유연성이 좋아질 수 있음.

  • given 절

    • 테스트에 필요한 사전 조건(사용자, 게시물, 댓글 등) 및 Mock/Stubbing이 필요하다면, 해당 로직을 배치.

    • 시나리오별로 조금씩 다른 상황을 구성할 때, @BeforeEach보다 각 테스트 내부의 given 부분에 명시적으로 작성하는 방식이 선호됨.

  • when 절

    • 실제 테스트 대상 메서드를 호출하는 구간. 한 개의 테스트에서 “정확히 하나의 when 절”을 유지해, 명확히 어떤 동작을 검증하는지 드러내도록 함.

  • then 절

    • 결과 검증(Assertions) 수행. 저장된 데이터 확인, 예외 발생 여부 확인, 반환값 확인 등.

미션 회고

  • 레이어별 특징을 자기 언어로 설명

    • 단순히 문서상 개념을 반복하기보다, 실제 현업/프로젝트에서 마주치는 구조를 떠올리며 다시 풀어내보니, 각 계층이 왜 필요한지, 테스트를 어떻게 접근해야 하는지 더 명확해졌습니다.

  • Mock/Spy 애너테이션 정리

    • 정리를 해보니 실제로 코드에 적용할 때 어떤 상황에서 무엇을 써야 하는지가 더 분명해졌습니다.

    • @MockBean/@SpyBean이 스프링 컨텍스트를 사용하는 통합 테스트에서 유용하지만, 속도와 Bean 교체 이슈가 있어 주의가 필요하다는 점이 인상적이었습니다.

  • 3가지 테스트 시나리오에서 @BeforeEach & given/when/then 배치

    • 시나리오에 따라 @BeforeEach를 최소화하거나, 여러 테스트에 공통적으로 필요한 부분만 추출하는 방식이 각각 장단점이 있음을 확인했습니다.

    • 테스트 구조를 “given-when-then”으로 고정하면 가독성이 올라가지만, 너무 세세한 단계 분리는 오히려 장황해질 수 있으므로 균형을 잡아야겠습니다.


회고

  • 칭찬하고 싶은 점

    • 레이어별 테스트 기법과 Mock/Spy 전략이 한층 체계적으로 잡혔습니다.

    • 실제 코드를 작성하며, “가짜 객체를 어디까지 써야 하나? 외부 연동은 어떻게 테스트하나?” 같은 고민을 많이 해 봐서, 앞으로는 목적에 맞는 테스트를 설계하는 능력이 향상될 것 같습니다.

  • 아쉬웠던 점 & 보완 계획

    • Mock 남발 시 발생할 수 있는 문제(설정 복잡도 상승, 실제 로직 변경 시 Stubbing 깨짐)를 더 구체적인 예제로 다뤄보면 좋을 것 같습니다.

    • @BeforeEach와 개별 테스트 내부 given 절의 적절한 균형점을 찾으려면, 실제 현업 수준의 다양한 테스트 케이스를 더 경험해 봐야겠습니다.

다음 목표

  1. 예외 상황 테스트 강화

    • 단순 성공 케이스뿐 아니라, 비정상 입력이나 예외 케이스를 좀 더 체계적으로 정리하고 테스트에 반영.

       

  2. 도메인 별로 테스트 슬라이싱

    • @DataJpaTest, @WebMvcTest 등 스프링이 제공하는 슬라이스 테스트 방식을 적극 활용해 보기.

  3. 테스트 실행 속도 최적화

    • 통합 테스트와 단위 테스트를 적절히 조합하여, 빠르면서도 신뢰성 있는 테스트 환경을 구축해 보기.

이번 4주차에는 계층형 아키텍처를 다시 한번 복습하면서 Mock/Spy, @MockBean/@SpyBean 등 스프링 테스트 환경에서 자주 쓰이는 기법들을 정리하고 적용해 보았습니다. 학습 내용과 미션을 통해 각 레이어가 가진 의미와 책임이 더욱 또렷해졌고, 가짜 객체를 어떻게 잘 활용해야 하는지 감이 잡힌 것 같습니다.

앞으로도 학습 내용을 기록하고 회고하면서, 점점 더 탄탄한 테스트 코드를 작성해 가도록 하겠습니다.

감사합니다!

댓글을 작성해보세요.


채널톡 아이콘