프로젝트를 진행하면서 API 문서화를 위해 Spring Rest Docs를 사용해보게 되었습니다. 이전까지는 테스트 코드를 작성하지 않았던 아쉬움이 있어 테스트 코드 작성을 강제화하기 위해 사용경험이 있던 Swagger UI가 아닌 이를 선택했습니다.
그러면서 작성한 테스트 코드에서 @SpringBootTest이 아닌 @WebMvcTest라는 어노테이션을 사용하게 되었는데, 이 둘은 어떤 차이가 있는지 정리하는 시간을 가져보고자 합니다. (사실 이 차이를 몰라서 에러때문에 끙끙 댔습니다.)
하나하나씩 알아보고 정리해보겠습니다.
@SpringBootTest
먼저, 위 어노테이션은 Spring boot를 기반으로 한 테스트를 동작하는 클래스에서 사용될 수 있습니다. 제공하는 기능에 대해 알아보기 전에, 자주 나올 개념에 대해 잠깐 짚고 넘어가겠습니다.
@Configuration
: 하나 이상의 @Bean 메소드를 정의하고, 런타임에 해당 빈에 대한 빈 정의 및 서비스 요청을 생성하기 위해 Spring container에 의해 처리될 수 있는 클래스를 나타낸다.
그리고 다음과 같은 기능들을 제공합니다.
제공되는 기능
- @ContextConfiguration으로 테스트에 사용될 설정을 명시하지 않았다면, SpringBootContextLoader를 기본값으로 제공합니다.
- SpringBootContextLoader는 Spring Boot application에 사용되는 ContextLoader를 의미합니다.
- 중첩된 @Configuration이 명시되지 않았거나 명시적 클래스가 지정되지 않은 경우, 자동으로 @SpringBootConfiguration을 검색합니다.
- SpringBootConfiguration은 Spring Boot application @Configuration을 제공하는 클래스를 나타냅니다.
- properties 속성을 이용해 정의된 커스텀 Environment properties를 허용합니다.
- 테스트 수행 전, properties 속성으로 전달한 값으로 테스트 환경을 지정할 수 있다는 말인 것 같습니다.
- 흔히 볼 수 있는, String[] args와 같은 args 속성으로 application argument 전달을 허용합니다.
- 다양한 webEnvironment 환경에 대한 지원을 제공합니다.
- 지정한 포트나 랜덤 포트에 대해 listening하는 웹서버를 동작시킬 수 있습니다.
- webEnvironment의 기본 값은 SpringBootTest.WebEnvironment.MOCK인데, 이는 실제 컨테이너를 띄우는 것이 아니라 MOCK이라는 말처럼 가짜 컨테이너를 띄워 수행하는 것입니다.
@WebMvcTest
해당 어노테이션은 Spring MVC test를 위해 사용될 수 있는데, 여기서 중요한 점은 Spring MVC components에만 집중한다는 것입니다. 이 말의 의미는 다음과 같습니다.
해당 어노테이션을 사용하는 것은 곧 자동화된 configuration을 사용할 수 없음을 의미하는 대신에 MVC tests와 연관된 configruation만을 제공합니다.
그렇다면, MVC tests에 연관된 configuration에는 무엇이 있을까요? (여기서 부족한 제 지식이 드러났습니다.)
- @Controller
- @ControllerAdvice
- @JsonComponent
- Converter/GenericConverter
- Filter
- WebMvcConfigurer
- HandlerMethodArgumentResolver
즉, @Component, @Service, @Repository beans는 포함하지 않습니다!
기본적으로, 이 어노테이션이 붙은 테스트는 Spring Security와 MockMvc에 대한 자동 configure를 수행해줍니다. 그리고, MockMVC에 대한 세밀한 제어를 위해 @AutoConfigureMockMvc 어노테이션을 사용할 수도 있습니다.
❗️ 그렇다면, 위에서 언급한 빈들을 제외한 빈들은 사용할 수 없는 걸까요?
그렇지 않습니다. 언급한 빈들과 함께 사용해야 할 친구들에 대해서는 @MockBean이나 @Import 어노테이션을 활용해 사용할 수 있습니다.
근데 그럼, 함께 사용해야 할 친구들이 많아지게 되면, 코드가 너무 길어지지 않을까요 .. ? (제 경우가 여기에 속했습니다. 그렇게 많은 건 아니였지만 ...)
그럴 땐 위에서 다룬 @SpringBootTest 어노테이션과 @AutoConfigureMockMvc 어노테이션을 함께 사용하는 방식이 더 효율적입니다.
- 이렇게 되면, 프로젝트 내의 모든 빈을 등록하는 과정을 거칩니다.
비교
일단 둘 다 테스트를 위한 어노테이션이라는 것을 쉽게 이해할 수 있을 듯합니다. 그렇다면 어떤 점이 다를까요?
@SpringBootTest
- 실제 스프링 컨테이너를 사용합니다. 실제 운영 환경과 유사한 테스트를 진행하게 됩니다.
- 따라서 컴포넌트 스캔을 수행하여 프로젝트 내의 모든 빈을 등록하게 됩니다.
- 모든 빈을 등록하기에 필요한 빈이 없는 경우가 없겠습니다. (컴포넌트 스캔 대상이 되지 않도록 지정하지 않았다면..)
- 하지만 등록하는 과정때문에 시간이 오래 걸립니다.
- 그리고 실제 운영과 비슷한 환경에서의 테스트를 진행하기에 규모가 크고, 이로 인해 디버깅이 어려워집니다.
@WebMvcTest
- 언급했듯 MVC만을 위한 테스트입니다. 컨트롤러에 대한 테스트를 진행할 때 사용합니다.
- 따라서 언급한 항목들에 대한 등록 과정을 거치고, @SpringBootTest를 사용했을 때보다 더 빠릅니다.
- 개별 테스트가 가능해집니다.
- 하지만 실제 환경과의 유사도 측면에서는 @SpringBootTest보다는 제한적이기에 예상 밖의 동작오류를 잡아내지 못할 수도 있습니다.
오류 코드
제 경우를 예를 들어 설명해보겠습니다 ... (다음부터는 이러한 실수를 하기않기 위해 .. ㅎㅎ)
우선 코드는 아래와 같았습니다. 복잡한 로직을 구현한 건 아니었기에 이해는 금방 되실 듯 합니다.
먼저 Controller 코드입니다.
@Slf4j
@RestController
@RequestMapping("/api")
public class HomeController {
private final ControlService controlService;
@Autowired
public HomeController(ControlService controlService) {
this.controlService = controlService;
}
@GetMapping(value = ...)
@PostMapping(value = ...)
}
다음은 Service 코드입니다.
@Service
@Slf4j
public class ControlService {
private final ControlRepository controlRepository;
private final ResponseService responseService;\
@Autowired
public ControlService(ControlRepository controlRepository, ResponseService responseService) {
this.controlRepository = controlRepository;
this.responseService = responseService;
}
...
}
다른 구현보다는 의존관계 주입 과정이 가장 중요하다고 생각해서 해당 코드 위주입니다. (다만 문제는 여기가 아니었지만 ...)
아래는 테스트 코드입니다.
@AutoConfigureMockMvc
// Spring REST docs 자동 설정을 위함
@AutoConfigureRestDocs
@WebMvcTest
class HomeControllerTest {
// MockMvc 객체 생성
@Autowired
private MockMvc mockMvc;
@Test
void void listTest() throws Exception {
// service와 repository를 사용하는 로직
}
...
}
그리고 빌드 시에 발생한 에러는 다음과 같았습니다.
Caused by: org.springframework.beans.factory.UnsatisfiedDependencyException:
Error creating bean with name 'homeController' defined in file [.../HomeController.class]: Unsatisfied dependency expressed through constructor parameter 0;
nested exception is org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'com.example.QCA.QualityControlAutomation.service.ControlService' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
접근 과정은 다음과 같았습니다.
- HomeController.class의 생성자 0번째 파라미터에 대한 의존성에 문제가 있다 ?
- 0번째 파라미터는 ControlService인데... ControlService가 등록이 안되나 ?
- ControlService 클래스에 @Service 어노테이션도 잘 붙여뒀고 .. 여기서 사용하는 다른 클래스에 대한 컴포넌트 스캔도 다 되도록 해뒀는데 ..?
- 의존관계 주입방식이 틀린건가 .. ? 의존관계 주입방향이 어떻게 되는거지 ?
- @RequiredArgsConstructor 어노테이션을 써서 해볼까 ? (차이 없음)
- 뭐가 잘못된거지 ...
- 빌드 때 테스트 코드도 같이 수행하는 건가 ? 그럼 테스트 코드를 봐야겠다.
- @WebMvcTest랑 @SpringBootTest는 뭐가 다른 거지 ...
조금은 부끄럽지만 .. 이렇게 흘러오면서 해결할 수 있었습니다. 아무래도 Spring Rest Docs를 처음 써보면서 @WebMvcTest 또한 처음 보게 되면서 이런 문제가 발생하지 않았나 싶습니다.
스프링은 어노테이션 하나하나가 다 중요한 것 같습니다. 실무에서는 어노테이션을 만들어 쓰는 경우도 많다고 하는데, 아직 만들 정도까지는 멀었고 .. 이미 있는 어노테이션을 잘 이해하고 사용할 줄 아는 것부터 다가가야 할 것 같습니다.
이렇게 @SpringBootTest와 @WebMvcTest에 대해 알아보았습니다.
물론 부족한 점이 넘치고, 이해가 안되는 부분이 있을 수 있지만, 읽어주셔서 감사합니다.
잘못된 점이나 질문 있으시면 편하게 댓글 남겨주시면 감사하겠습니다. 🙂
참고