[Spring] 각 계층별 테스트 어노테이션을 사용할 때 어떠한 예외 상황이 발생할까?

Loopy·2023년 5월 7일
1

삽질기록

목록 보기
23/28
post-thumbnail
post-custom-banner

@WebMVCTest

☁️ 개념

오직 웹 계층과 관련있는 의존성 클래스들만 로드하는 어노테이션이다.
WebMvcConfigurer, HandlerMethodArgumentResolver 부터 시작하여 시큐리티, 서블릿 필터, 스프링 인터셉터, @ControllerAdvice, @JsonComponent와 같이 컨트롤러 관련 어노테이션들이 붙은 클래스를 모두 포함한다.

@SpringBootTest 처럼 모든 의존성을 가져오지 않기 때문에, @Component, @Service, @Repository 가 필요 없는 컨트롤러 테스트를 할 때 적합하다.

☁️ 문제 상황

@WebMvcTest 를 선언하고, 실행을 시켰는데 다음과 같은 에러가 발생할 수 있다.

Caused by: org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'jpaAuditingHandler': Cannot resolve reference to bean 'jpaMappingContext' while setting constructor argument

Caused by: java.lang.IllegalArgumentException: JPA metamodel must not be empty

jpaMappingContext 클래스를 찾을 수 없다는 것인데, 왜 이런 문제가 발생할까?

원인

@WebMvcTest 를 붙였어도, 우선적으로 SpringBootApplication 클래스를 찾은 이후에 선택적으로 필요한 빈들만 로드하게 된다. @SpringBootApplication 이 자동 의존성 설정을 해주기 때문이다.

따라서 SpringBootApplication 클래스에 작성자/작성일자 자동화를 위해 붙여놓은 @EnableJpaAuditing 이 존재하니, 자동으로 JPA 설정을 위해jpaMappingContext 가 필요했고 테스트 클래스에 주입을 안해주었으니 에러가 난 것이 원인이였다.

@SpringBootApplication
@EnableJpaAuditing    // 원인
@EnableConfigurationProperties
class AhachulBackendApplication
...

해결 방안

간단하게 JpaMetamodelMappingContext 를 주입해주면 해결할 수 있다.

하지만 컨트롤러 테스트에서 쓰이지 않기 때문에 불필요한 의존성일 수도 있으므로, @EnableJpaAuditing 과 같이 특정 기술에 종속적인 어노테이션들은 모두 JpaConfig 이라는 설정 클래스를 만들어서 모아 놓는것이 좋다.

@Configuration
@EnableJpaAuditing(auditorAwareRef = "auditorProvider")
class JpaConfig {
	...
}

@SpringBootTest

☁️ 개념

모든 빈을 로드해서 의존 관계를 설정하는 테스트 어노테이션으로, 그만큼 속도가 느리다는 단점이 존재한다.

주의할 점은 해당 링크에 따르면, 서비스 레이어와 같은 경우 mock에 필요한 어노테이션들을 제외하고는 @SpringBootTest 을 사용하지 않는 것이 좋다고 한다. 독립적으로 모듈화 되어야 하는 비즈니스 로직에 관련된 테스트이기 때문에, 설정에 의존해서는 안되기 때문이다.

☁️ 문제 상황

AOP 테스트를 할 때, @SpringBootTest 와 내부 클래스에 @Configuration 을 같이 사용하는 경우 프록시가 동작하지 않는 문제 발생할 수 있다. 예를 들어 아래의 예제에서 child.childMethod() 를 호출하면, 빈으로 등록해주었는데도 Proxy가 적용이 안된 스프링 빈이 출력된다.

child Proxy=class ...AtTargetAtWithinTest$Child
//@Import(AtTargetWithinTest.Config.class)
@SpringBootTest
public class AtTargetWithinTest {

	...
    @Configuration
	static class Config {
        @Bean
        public Parent parent() {
            return new Parent();
        }
        @Bean
        public Parent.Child child() {  // @ClassAop가 붙은 클래스
            return new Parent.Child();
        }
        @Bean
        public Parent.AtTargetAtWithinAspect atTargetAtWithinAspect() {
            return new Parent.AtTargetAtWithinAspect();
        }
    }
}

원인

내부 클래스에 선언된 @Configuration 클래스 경로가 자동으로 컴포넌트 스캔의 기본패키지로 지정되기 되어 우선권을 가지기 때문에, 이후 모든 스프링 부트의 다양한 설정들이 먹히지 않게 된다.

따라서 AOP 적용에 반드시 필요한 AutoProxy 와 같은 스프링 부트가 자동으로 만들어주는 AOP 기본 클래스들도 빈으로 등록되지 않아 프록시가 동작하지 않는다.

해결 방법

  1. @EnableAspectJProxy + @Configuration : 기존의 src/main 모듈 설정과 AOP 설정을 따로 해주어야 하므로 번거롭다.
  2. @TestConfiguration : 권장하는 방법으로, src/main 전체 모듈의 기본적인 설정을 유지하면서 추가적으로 테스트용 설정을 추가할 수 있다.
 	@TestConfiguration
	static class Config {
    }

@DataJpaTest

☁️ 개념

오직 JPA와 관련된 설정들만 로드하는 테스트 어노테이션이다.

특별히 지정된 DataSource가 없다면, h2 와 같은 내장된 인메모리 데이터베이스를 사용해서 테스트한다. 또한, @SpringBootTest + @Transactional 을 붙였을 때와 똑같이 모든 테스트의 끝에서 커밋이 아닌 롤백을 해줌으로써 테스트의 독립성을 지켜준다는 장점이 있다.

☁️ 문제 상황

JPA 엔티티 ID 생성 타입을 GenerationType = Identity 로 지정하면, 롤백을 해줌에도 INSERT 쿼리가 날라가는 상황이 발생할 수 있다.

물론 INSERT 문과 SELECT 문을 제외한 UPDATEDELETE 문은 날라가지 않았지만, 어느 변경 사항도 실제 DB에 반영이 되어서는 안되는데 왜 이러한 문제가 발생하는 것일까?

원인

GenerationType = Identity 전략은 기본키 생성을 DB에 위임한다.

따라서 INSERT 가 될때에는 ID가 NULL인 상태이고, 영속성 컨텍스트에 관리되기 위해서 예외적으로 커밋 시점이 아닌 영속 시점에 INSERT 문을 날려 식별자를 조회한 후 가져와서 저장하게 된다. 그렇기에 롤백을 했음에도 생성 쿼리가 나간다.

해결

GenerationType.SEQUENCE 를 사용하면 커밋 시점INSERT 쿼리를 날려서 문제 해결이 가능하지만, Oracle, DB2, and Postgres 데이터베이스에서만 사용이 가능하므로 현재 상황에서는 전략을 그대로 가져갔다.

참고 자료
Does @WebMvcTest require @SpringBootApplication annotation?
AOP 테스트 문제

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!
post-custom-banner

0개의 댓글