티스토리 뷰
Mockito는 자바 생태계에서 가장 널리 사용되는 mocking 프레임워크 중 하나입니다. 많은 개발자가 when().thenReturn()과 verify()를 자연스럽게 사용하지만, 내부에서 어떤 방식으로 mock 객체가 생성되는지까지 깊이 들여다볼 기회는 많지 않습니다. 그러나 Mockito의 제약과 설계 의도를 이해하려면, 런타임 mock 생성 방식과 바이트코드 조작 구조를 함께 이해할 필요가 있습니다.
이번 글에서는 Mockito 공식 문서와 GitHub 저장소에 공개된 내용을 기반으로, Mockito의 내부 동작 원리와 그 한계를 정리해 보겠습니다. 단순 사용법을 넘어서, 왜 이런 제약이 존재하는지, 그리고 그것이 테스트 설계에 어떤 영향을 주는지까지 확장해 보려고 합니다.
Mockito가 해결하려는 문제
Mockito는 테스트에서 협력 객체를 대체하기 위한 도구입니다. 테스트 대상 클래스가 외부 의존성을 직접 호출하지 않도록, 해당 의존성을 mock 객체로 대체하고, 그 상호작용을 검증할 수 있도록 설계되었습니다.
공식 문서에 따르면 Mockito는 “clean & simple API”를 지향합니다. 내부 구현은 복잡하지만, 사용자는 비교적 직관적인 API를 사용하도록 설계되어 있습니다. 이 단순한 API 뒤에는 런타임 바이트코드 조작이라는 기술이 존재합니다.
Mockito는 어떻게 런타임에 mock 객체를 생성하는가
Mockito는 런타임에 mock 객체를 생성합니다. 이는 컴파일 시점에 별도의 구현 클래스를 생성하는 방식이 아니라, 실행 시점에 동적으로 클래스를 생성하거나 조작하는 방식입니다.
공식 문서에 따르면 Mockito는 기본적으로 Byte Buddy를 사용하여 mock을 생성합니다. Byte Buddy는 자바 바이트코드를 생성하거나 수정할 수 있는 라이브러리입니다. Mockito는 이를 활용해 다음과 같은 작업을 수행합니다.
- 대상 클래스의 서브클래스를 생성합니다.
- 메서드 호출을 가로채는 인터셉터를 삽입합니다.
- 실제 메서드 대신 Mockito 내부 로직이 실행되도록 합니다.
기본 mock maker는 서브클래싱(subclassing) 기반입니다. 즉, mock 대상 클래스의 하위 클래스를 동적으로 생성하여, 해당 클래스의 메서드를 오버라이드합니다. 이 구조는 자바의 상속 모델에 의존합니다.
이 방식은 비교적 안전하지만, 몇 가지 제약을 동반합니다. 대표적으로 final 클래스나 final 메서드는 오버라이드할 수 없기 때문에 기본 mock maker로는 mocking이 어렵습니다.
Bytecode Manipulation의 개념
바이트코드 조작은 JVM이 실행하는 바이트코드를 런타임에 생성하거나 수정하는 기술입니다. 자바는 동적 언어는 아니지만, Instrumentation API와 같은 기능을 통해 런타임에 클래스 정의를 변경할 수 있습니다.
Mockito는 Byte Buddy를 통해 이러한 기능을 활용합니다. Byte Buddy는 JVM 레벨에서 클래스를 생성하거나 수정할 수 있도록 지원합니다. Mockito는 이를 통해 mock 클래스에 메서드 인터셉션 로직을 삽입합니다.
이 구조는 단순 프록시와는 다릅니다. JDK Dynamic Proxy는 인터페이스 기반으로만 동작하지만, Mockito는 클래스 기반 mock도 지원합니다. 이를 가능하게 하는 것이 바이트코드 조작입니다.
Inline Mock Maker와 기본 Mock Maker의 차이
Mockito는 기본 mock maker 외에도 Inline Mock Maker를 제공합니다. Inline Mock Maker는 JVM Instrumentation을 활용하여 기존 클래스를 직접 수정하는 방식으로 동작합니다.
기본 mock maker는 서브클래스를 생성하는 방식입니다. 반면 Inline Mock Maker는 원본 클래스의 바이트코드를 조작하여 메서드를 가로챕니다. 이 차이로 인해 final 클래스나 final 메서드도 mocking이 가능해졌습니다.
공식 문서에 따르면 Inline Mock Maker는 별도의 설정이 필요합니다. mock-maker-inline 설정 파일을 통해 활성화할 수 있습니다. 이 방식은 보다 강력하지만, JVM Instrumentation을 사용하기 때문에 환경에 따라 제약이 있을 수 있습니다.
이 구조적 차이를 이해하면, 왜 초기 Mockito가 final 클래스를 mock하지 못했는지 자연스럽게 이해할 수 있습니다.
final 클래스/메서드 mocking이 가능해진 배경
초기 Mockito는 상속 기반 서브클래싱 방식에 의존했습니다. 자바 언어 차원에서 final 클래스와 final 메서드는 오버라이드할 수 없기 때문에, 자연스럽게 제약이 발생했습니다.
Inline Mock Maker 도입 이후, Mockito는 Instrumentation을 활용해 클래스 자체를 수정하는 접근을 선택했습니다. 이는 기존 설계 철학과는 약간 다른 방향이지만, 사용자 요구와 자바 생태계의 변화에 따라 확장된 것으로 볼 수 있습니다.
다만, 이러한 기능은 내부적으로 더 복잡한 동작을 포함합니다. 테스트 안정성과 실행 환경에 따라 고려할 부분이 존재합니다.
Spy의 동작 방식과 주의점
Spy는 mock과 달리 실제 객체를 감싸는 방식으로 동작합니다. 즉, 실제 인스턴스를 기반으로 하며, 특정 메서드만 stub 처리할 수 있습니다.
Mockito 공식 문서에서도 Spy 사용 시 주의가 필요하다고 설명합니다. when(spy.method()) 방식은 실제 메서드를 먼저 호출할 수 있기 때문에, 예기치 않은 부작용이 발생할 수 있습니다. 이로 인해 doReturn().when(spy) 패턴을 권장하는 경우가 있습니다.
Spy는 부분 mocking을 가능하게 하지만, 설계 관점에서는 테스트 대상의 경계를 모호하게 만들 수 있습니다. 실제 객체의 내부 상태에 의존하게 되면, 테스트가 복잡해질 수 있습니다.
과도한 verify 사용이 테스트 취약성을 높이는 이유
Mockito는 interaction-based testing에 적합한 도구입니다. verify()를 통해 특정 메서드 호출 여부를 검증할 수 있습니다.
하지만 verify를 과도하게 사용하면 테스트가 구현 세부사항에 강하게 결합됩니다. 예를 들어, 내부 구현을 리팩토링하면서 메서드 호출 구조가 바뀌면, 외부 동작은 동일하더라도 테스트가 실패할 수 있습니다.
이 문제는 테스트가 “무엇을 했는가”를 검증하는지, “어떤 결과가 나왔는가”를 검증하는지에 대한 차이와 연결됩니다. 상호작용을 지나치게 검증하면, 테스트 유지보수 비용이 증가할 수 있습니다.
Interaction-based Testing vs State-based Testing
Interaction-based testing은 객체 간 상호작용을 검증합니다. Mockito는 이 접근에 최적화되어 있습니다. 반면 State-based testing은 최종 상태나 반환값을 검증합니다.
두 방식은 서로 대체 관계라기보다 보완 관계에 가깝습니다. 내부 협력 구조가 중요한 경우 interaction 기반 검증이 유용할 수 있습니다. 하지만 비즈니스 로직의 결과가 중요한 경우 state 기반 검증이 더 적합할 수 있습니다.
Mockito의 설계는 interaction 중심이지만, 모든 테스트를 interaction 중심으로 작성해야 한다는 의미는 아닙니다. 도구의 특성이 테스트 설계 철학을 결정해서는 안 된다고 생각합니다.
테스트 설계 철학으로의 확장
Mockito의 제약은 단순한 기술적 한계가 아니라, 자바 언어 모델과 런타임 구조에 기반한 결과입니다. 서브클래싱 기반 설계는 자바의 상속 구조를 전제로 합니다. Inline Mock Maker는 JVM Instrumentation이라는 강력한 기능을 활용합니다.
이러한 구조를 이해하면, 왜 특정 상황에서 mock이 실패하는지, 왜 final이 제약이 되었는지 자연스럽게 설명할 수 있습니다.
또한 Spy나 verify의 사용 방식은 테스트 설계 철학과 직결됩니다. 도구의 기능이 강력하다고 해서 모두 사용하는 것이 바람직한 것은 아닙니다.
마무리하며
Mockito는 단순한 mocking 라이브러리처럼 보이지만, 내부에는 바이트코드 조작과 런타임 클래스 생성이라는 기술이 자리하고 있습니다. 이러한 내부 구조는 기능의 가능성과 동시에 제약을 함께 만들어냅니다.
Interaction-based testing은 강력한 도구이지만, 테스트를 구현 세부사항에 묶어 둘 위험도 존재합니다. 결국 중요한 것은 도구의 한계를 이해하고, 설계 의도를 존중하면서 사용하는 일이라고 생각합니다.
저 역시 Mockito를 단순히 “편리한 테스트 도구”로 사용하다가, 내부 동작을 이해하면서 테스트 설계에 대해 다시 고민하게 되었습니다. 테스트는 코드를 검증하는 수단이지만, 동시에 설계를 드러내는 문서이기도 합니다. Mockito의 구조를 이해하는 과정은, 결국 좋은 테스트란 무엇인지 다시 질문하게 만드는 계기였다고 정리해 보고 싶습니다.
'STUDY' 카테고리의 다른 글
| Spring Batch 실패 처리와 Retry 설계: DB 쓰기와 API 호출의 차이 (0) | 2026.02.27 |
|---|---|
| 테스트 피라미드와 Spring Boot 테스트 전략: Mockito 단위 테스트부터 @SpringBootTest까지의 균형 (0) | 2026.02.26 |
| Spring Boot 테스트 슬라이스 완전 정리: @WebMvcTest, @DataJpaTest 동작 원리와 Mockito의 역할 (0) | 2026.02.26 |
| Spring Boot에서 Mockito 단위 테스트와 @SpringBootTest의 차이 정리: 언제 무엇을 사용해야 할까 (0) | 2026.02.26 |
| Spring Batch chunk size 선택 기준: 디스크 IO와 네트워크 비용 모델 비교 (0) | 2026.02.25 |
- Total
- Today
- Yesterday
- Redis 캐시 전략
- Double-Checked Locking
- Spring Batch
- Cache Aside
- DB 트랜잭션
- Redis vs DB
- Enum 기반 싱글톤
- Initialization-on-Demand Holder Idiom
- 백엔드 성능
- mybatis
- 백엔드 성능 튜닝
- Cache Penetration
- Redis 성능 개선
- TTL 설계
- 백엔드 성능 설계
- Hot Key 문제
- Java Performance
- 스레드 생명주기
- 캐시와 인덱스
- 백엔드 아키텍처
- 동시성처리
- DB 인덱스 성능
- 트랜잭션 관리
- Eager Initialization
- InterruptedException
- Cache Avalanche
- 캐시 장애
- 캐시 성능 비교
- spring batch 5
- 트래픽 처리
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | 7 |
| 8 | 9 | 10 | 11 | 12 | 13 | 14 |
| 15 | 16 | 17 | 18 | 19 | 20 | 21 |
| 22 | 23 | 24 | 25 | 26 | 27 | 28 |
| 29 | 30 | 31 |

