PromleeBlog
sitemap
aboutMe

posting thumbnail
Mockito를 활용한 의존성 분리 - Spring Boot로 시작하는 TDD 9편
Separating Dependencies using Mockito - TDD with Spring Boot Part 9

📅

🚀

들어가기 전에 🔗

지난 8편에서는 Controller를 테스트하면서 MockMvc를 사용해 보았습니다.
이제 우리는 Repository, Service, Controller 모든 계층을 테스트할 수 있게 되었습니다.

하지만 개발을 하다 보면 이런 고민에 빠집니다.
"테스트를 짤 때마다 DB를 켜야 하나요?"
"메일을 보내는 기능은 실제로 메일을 보내면 안 되는데 어떻게 테스트하죠?"

이런 난관을 해결해 주는 것이 바로
Mockito
입니다.
이미 Service 테스트에서 살짝 맛보았지만, 오늘은 이 도구를 제대로 활용하여
객체 간의 관계(의존성)를 깔끔하게 분리하는 방법
을 깊이 있게 알아보겠습니다.

🚀

Mock 객체의 역할 🔗

Mock(목)은 영화 촬영장의
스턴트맨
과 같습니다.
위험한 액션 씬(DB 연결, 결제 요청 등)을 주연 배우(실제 객체) 대신 처리해 줍니다.
테스트 세계에서 Mock은 다음과 같은 역할을 합니다.

🚀

상태 검증과 행위 검증 🔗

테스트를 검증하는 방법에는 크게 두 가지가 있습니다.
이 둘의 차이를 아는 것이 TDD 레벨업의 핵심입니다.

1. 상태 검증 (State Verification) 🔗

우리가 지금까지 주로 해왔던 방식입니다.
메서드를 실행한
에, 결과값이나 객체의 상태가 기대한 대로 바뀌었는지 확인합니다.
알고리즘 문제에서 결과값이 정답과 같은지 확인하는 것과 같습니다.
src/test/java/com/example/tdd_study/product/StateTest.java(얘시)
@Test
void 더하기_상태_검증() {
    Calculator calc = new Calculator();
    int result = calc.add(2, 3);
    
    // 결과 상태(값)가 5인지 확인
    assertThat(result).isEqualTo(5); 
}

2. 행위 검증 (Behavior Verification) 🔗

Mockito가 빛을 발하는 순간입니다.
결과값보다는 "어떤 메서드가 호출되었는가?", "몇 번 호출되었는가?"를 확인합니다.
리턴 값이 없는(void) 메서드나, 다른 객체와의 협력을 테스트할 때 사용합니다.
src/test/java/com/example/tdd_study/product/BehaviorTest.java(예시)
@Test
void 메일_전송_행위_검증() {
    // given
    EmailService emailService = mock(EmailService.class);
    UserService userService = new UserService(emailService);
 
    // when
    userService.register("user@example.com");
 
    // then: sendWelcomeEmail 메서드가 호출되었는지 확인(verify)
    verify(emailService).sendWelcomeEmail("user@example.com");
}
이것은 그래프 탐색 알고리즘(BFS/DFS)에서 "이 노드를 방문했는가?"를 체크하는 것과 비슷한 원리입니다.

🚀

실제 코드에 적용하기 🔗

우리의 ProductService에 새로운 요구사항이 생겼다고 가정해 봅시다.
상품을 등록하면 알림 센터(NotificationCenter)에 알림을 보내야 한다.
이 기능을 구현하기 위해서는 인터페이스 정의, 구현체 생성, 그리고 서비스 연결이 필요합니다.

1. 인터페이스 정의 🔗

먼저 알림 기능을 추상화한 인터페이스를 만듭니다.
src/main/java/com/example/tdd_study/notification/NotificationCenter.java
package com.example.tdd_study.notification;
 
public interface NotificationCenter {
    void send(String message);
}

2. 구현체 생성 🔗

이 단계가 빠지면 애플리케이션 실행 시
Bean을 찾을 수 없다는 에러
가 발생합니다.
실제 동작하는 구현체를 만들고 스프링 빈으로 등록해야 합니다.
src/main/java/com/example/tdd_study/notification/ConsoleNotificationCenter.java
package com.example.tdd_study.notification;
 
import org.springframework.stereotype.Component;
 
@Component // 스프링이 관리하는 빈으로 등록
public class ConsoleNotificationCenter implements NotificationCenter {
 
    @Override
    public void send(String message) {
        System.out.println("[알림 발송] " + message);
    }
}

3. Service 코드 수정 🔗

이제 ProductServiceNotificationCenter를 사용하도록 수정합니다.
생성자 주입 방식을 사용하므로 코드가 조금 바뀝니다.
src/main/java/com/example/tdd_study/product/ProductService.java
package com.example.tdd_study.product;
 
import com.example.tdd_study.notification.NotificationCenter;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
 
@Service
public class ProductService {
    private final ProductRepository productRepository;
    private final NotificationCenter notificationCenter; // 추가된 의존성
 
    // 생성자 주입
    public ProductService(ProductRepository productRepository, 
                          NotificationCenter notificationCenter) {
        this.productRepository = productRepository;
        this.notificationCenter = notificationCenter;
    }
 
    @Transactional
    public void register(String name, int price) {
        Product product = new Product(name, price);
        productRepository.save(product);
        
        // 알림 전송 로직 호출
        notificationCenter.send("새로운 상품이 등록되었습니다: " + name);
    }
}

🚀

행위 검증 테스트 작성 🔗

이제 ProductServiceTest를 수정하여 알림이 제대로 발송되는지 테스트해 보겠습니다.
여기서는 실제 ConsoleNotificationCenter 대신
Mock 객체
를 사용하여
행위 검증
을 수행합니다.
src/test/java/com/example/tdd_study/product/ProductServiceTest.java
package com.example.tdd_study.product;
 
import com.example.tdd_study.notification.NotificationCenter;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
 
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.Mockito.verify;
 
@ExtendWith(MockitoExtension.class)
class ProductServiceTest {
 
    @Mock
    private ProductRepository productRepository;
 
    @Mock // NotificationCenter를 가짜 객체(Mock)로 생성
    private NotificationCenter notificationCenter;
 
    @InjectMocks // 가짜 객체들을 Service에 주입
    private ProductService productService;
 
    @Test
    void 상품_등록시_알림이_전송되어야_한다() {
        // given
        String name = "라떼";
        // (중요) 6편에서 정한 도메인 규칙(1000원 단위)에 따라 4500이 아닌 4000이어야 합니다.
        int price = 4000; 
 
        // when
        productService.register(name, price);
 
        // then
        // 1. Repository의 save 메서드가 호출되었는지 검증
        verify(productRepository).save(any(Product.class));
        
        // 2. NotificationCenter의 send 메서드가 특정 메시지와 함께 호출되었는지 검증
        verify(notificationCenter).send("새로운 상품이 등록되었습니다: 라떼");
    }
}
 

코드 분석 🔗


🚀

테스트 실행 및 결과 분석 🔗

실행하여 초록색 체크 표시가 떴다면 성공입니다.
그런데 콘솔 창을 보면
아무런 로그도 찍히지 않습니다.
"어? 아까 ConsoleNotificationCenter에서 System.out.println을 썼는데 왜 안 나오죠?"

이것이 바로 Mocking의 핵심입니다.
우리는 실제 객체(ConsoleNotificationCenter)가 아니라
가짜 객체(Mock)
를 주입했기 때문입니다.
가짜 객체는 아무런 기능이 없는 껍데기이므로 로그를 출력하지 않습니다.
대신 verify가 내부적으로 "메서드가 호출되었음"을 기록하고 검증에 성공했기 때문에 초록 불이 뜬 것입니다.

3. 실패 시 결과 (Red) 🔗

Mockito가 어떻게 에러를 잡는지 확인하기 위해, ProductService에서 알림 전송 코드를 주석 처리하고 테스트를 돌려봅시다.
// notificationCenter.send("..."); // 주석 처리
테스트를 실행하면 빨간 불과 함께 다음과 같은 에러 메시지가 뜹니다.
인위적으로 에러 발생 시 WantedButNotInvoked
인위적으로 에러 발생 시 WantedButNotInvoked
Wanted but not invoked:
notificationCenter.send("새로운 상품이 등록되었습니다: 라떼");
-> at com.example.tdd_study.product.ProductServiceTest...
"원했는데(Wanted) 호출되지 않았다(not invoked)"라고 명확하게 알려줍니다.
이처럼 Mockito는 실제 코드를 실행하지 않고도 로직의 흐름(Flow)을 정확하게 감시할 수 있습니다.

🚀

Mock 사용 시 주의사항 🔗

Mock은 강력하지만 남용하면 독이 됩니다.
주의해야 할 점들을 정리해 드립니다.

🚀

결론 🔗

오늘은 Mockito를 활용해 의존성을 분리하고 행위를 검증하는 방법을 알아보았습니다.
다음 시간에는 테스트의 또 다른 중요한 부분인 예외 처리(Exception)와 유효성 검사(Validation)를 테스트하는 방법에 대해 알아보겠습니다.

참고 🔗