HiDevelop

Mockito에서 의존성 주입이 헷갈렸던 이유와 정리 본문

Spring

Mockito에서 의존성 주입이 헷갈렸던 이유와 정리

꽃달린감나무 2026. 1. 27. 13:36

최근 회사에서 spring Boot 3.4로 버전업을 올리면서 테스트 코드가 꽤 많이 깨졌다.
그 과정에서 그동안 당연하게 쓰던 Mockito 패턴들이 사실은 정확히 이해되지 않은 채 사용되고 있었다는 걸 알게 됐다.

특히 아래 두 가지가 가장 헷갈렸다.

  • Mockito는 어떤 기준으로 Mock을 주입하는가?
  • 사실 @InjectMocks 없이 테스트하는 게 더 좋은 구조 아닌가?
  • @InjectMocks, @Mock, @Spy 그리고 테스트 설계 기준

정리해보면 테스트 코드뿐 아니라 서비스 설계 방식까지 같이 돌아보게 되는 포인트였고, GPT 선생님께 혼나면서 얻은 지식들을 정리하고자 한다.


1. Mockito의 주입 우선순위

Mockito에서 @InjectMocks를 사용하면
테스트 클래스에 선언된 @Mock, @Spy들을 찾아 대상 객체에 주입해 준다.

이때 주입 우선순위는 명확하게 정해져 있다.

주입 우선순위

  1. 생성자 주입
  2. setter 주입
  3. 필드 주입 (리플렉션)

생성자 주입 (가장 먼저 선택됨)

Mockito는 가장 많은 파라미터를 받을 수 있는 생성자를 우선적으로 선택한다.

class OrderService {     
    private final OrderRepository repo;     
    private final PaymentClient client;      

    OrderService(OrderRepository repo, PaymentClient client) { 
        this.repo = repo;         
        this.client = client;     
        } 

}
@Mock OrderRepository repo; 

@Mock PaymentClient client;  

@InjectMocks OrderService service;

이 경우는 굉장히 명확하다.
Mock 타입이 생성자 파라미터와 정확히 매칭되기 때문에 주입이 실패할 여지도 적다.

👉 그래서 생성자 주입이 가장 안전하다.


2️⃣ setter 주입

class OrderService { private OrderRepository repo; void setRepo(OrderRepository repo) { this.repo = repo; } }

setter가 있으면 Mockito는 setter를 통해 주입한다.
다만 이 방식은 의존성이 누락되어도 컴파일 타임에 알 수 없다는 단점이 있다.


3️⃣ 필드 주입 (최후의 수단)

class OrderService {     
private OrderRepository repo; 
}

이 경우 Mockito는 리플렉션으로 강제로 주입한다.

동작은 하지만,

  • 캡슐화가 깨지고
  • IDE나 컴파일러의 보호를 전혀 받지 못한다

테스트가 “돌아간다”는 것 말고는 장점이 없다.


2. 생성자가 여러 개인 경우 주의할 점

class Service {     
    Service(Repo repo) {}     
    Service(Repo repo, Client client) {} 
}

Mockito는 파라미터가 더 많은 생성자를 선택한다.

문제는,

  • 그 생성자에 필요한 Mock이 하나라도 없으면
  • 해당 파라미터는 null로 들어간다

이런 구조는 테스트가 통과했다가도
어느 날 갑자기 NullPointerException으로 터질 수 있다.


3. 그런데… @InjectMocks는 꼭 필요한가?

결론부터 말하면,

잘 설계된 코드라면 @InjectMocks 없이도 테스트가 가능하다.

그리고 그 방식이 오히려 더 낫다.


4. @InjectMocks 없이 테스트하는 가장 좋은 패턴

생성자를 직접 호출한다

@ExtendWith(MockitoExtension.class) 
class OrderServiceTest {      
    @Mock     
    OrderRepository repo;      

    OrderService service;      

    @BeforeEach
    void setUp() {         
        service = new OrderService(repo);     
    } 
}

이 방식의 장점은 분명하다.

  • 어떤 의존성이 필요한지 테스트 코드만 봐도 알 수 있고
  • Mockito의 내부 주입 규칙을 몰라도 이해 가능하며
  • 리팩토링에도 훨씬 안전하다

개인적으로는 이게 가장 이상적인 테스트 형태라고 생각한다.


Fake 객체를 사용하는 방식

간단한 의존성이라면 Mockito조차 필요 없다.

class FakeOrderRepository implements OrderRepository {     

    @Override     
    public Order save(Order o) {         
        return new Order(1L);     
    } 
}
@Test 
void test() {     
    OrderService service = new OrderService(new FakeOrderRepository());      
    service.create(); 
}
  • 빠르고
  • 결과가 결정적이며
  • 테스트 의도가 명확하다

5. 그렇다면 @InjectMocks는 언제 쓰는 게 맞을까?

완전히 나쁜 건 아니다.

다만 선택지가 없을 때 쓰는 도구에 가깝다.

  • 레거시 코드
  • 필드 주입 구조
  • 생성자 변경이 어려운 상황

이럴 때만 제한적으로 사용하는 게 맞다.


6. 정리하며

Mockito를 쓰다 보면
“어쨌든 돌아가니까”라는 이유로 패턴이 굳어지기 쉽다.

하지만 이번에 테스트를 정리하면서 느낀 건,

테스트 코드가 복잡하다면,
그건 테스트 문제가 아니라 설계 문제일 가능성이 높다
는 점이었다.

  • 생성자 주입
  • 명시적인 의존성
  • 테스트에서 직접 객체 생성

이 세 가지만 지켜도
Mockito는 훨씬 단순해지고, 테스트도 안정된다.


한 줄 요약

Mockito는 생성자 → setter → 필드 순으로 주입하며,
가장 좋은 테스트는 @InjectMocks에 의존하지 않고
생성자를 직접 호출해 의존성을 명확히 드러내는 방식이다.

728x90

'Spring' 카테고리의 다른 글

OAuth2 ( 실습 편)  (1) 2023.08.04
간단한 서버 간 통신 @FeignClient  (0) 2023.08.03
OAuth2 (이론 및 준비)  (0) 2023.07.20
ORM(Object Relational Mapping)  (0) 2023.04.09
API 작성  (0) 2023.03.19