자꾸 헷갈려서 정리

 

어플리케이션 시작 시 호출되는 순서

1. ApplicationStartingEvent

2. ApplicationEnvironmentPreparedEvent

3. ApplicationContextInitializedEvent

4. ApplicationPreparedEvent

5. ContextRefreshedEvent

6. ApplicationStartedEvent

7. ApplicationReadyEvent

 

여기서, 1-3 번 까지는 ApplicationContext 초기화 전에 시작된다. 때문에, 이 이벤트들은 @EventListener 를 통한 이벤트 등록으로 처리할 수 없다. 왜냐하면 @EventListener 는 해당 리스너 메서드를 가진 클래스가 스프링 빈으로 먼저 스캔되어야 작동하는데, 1-3번 이벤트들은 ApplicationContext 시작 전이기 때문에 이벤트 등록보다 실제 이벤트가 더 먼저 발생한다.

 

때문에 1-3 번 이벤트들이 작동하기 위해서는 아래와 같이 스프링 어플리케이션 시작 전에 리스너를 선등록해야 한다.

class MyApplicationStartingEventListener : ApplicationListener<ApplicationStaringEvent> {
	override fun onApplicationEvent(event: ApplicationStartingEvent) {
    	...
    }
}

...

fun main(args: Array<String>) {
    SpringApplicationBuilder(MyApplication::class.java)
        .listeners(
                MyApplicationStartingEventListener(),
                MyApplicationContextInitializedEventListener(),
                MyApplicationEnvironmentPreparedEventListener(),
                MyApplicationPreparedEventListener(),
            )
            .run(*args)
}

 

또, 이벤트는 등록한 순서대로 호출된다. 위의 방법으로 등록되는 이벤트는 ApplicationContext 초기화 시 스캐닝에 의해 등록된 이벤트보다 먼저 등록되기 때문에, 동일한 이벤트가 호출될 때, @EventListener 리스너보다 무조건 먼저 호출된다.

사용법

ApplicationContext 전체를 초기화하지 않고 일부 Bean 만을 테스트할 때

@ExtendWith(SpringExtension::class) + @ContextConfiguration + @TestConfiguration

=> @TestConfiguration 에서 원하는 빈을 초기화한다. (@Bean 을 쓰거나 @ComponentScan을 쓰거나..)

=> 프로파일 등 수동 프로퍼티 지정이 필요할 때는 @ContextConfiguration 의 initializers 에 ApplicationContextInitializer 구현체를 지정한다.

예(코틀린): 

    class PropertyOverrideContextInitializer : ApplicationContextInitializer<ConfigurableApplicationContext> {
        override fun initialize(applicationContext: ConfigurableApplicationContext) {
            TestPropertySourceUtils.addInlinedPropertiesToEnvironment(
                applicationContext,
                "spring.config.activate.on-profile=production",
            )
        }
    }

=> application.properties(or yml) 자동 로딩을 사용하고 싶으면 initializers 에 ConfigDataApplicationContextInitializer 클래스를 추가한다. 왜냐하면, 설정파일 자동 로딩을 통한 어플리케이션 자동 설정은 스프링 부트의 기능이다. SpringExtension::class 은 스프링 부트가 아닌 전통 방식으로 스프링을 가동하기 때문에, 별도 설정이 없다면 비어있는 프로퍼티 값을 보게 될 것이다.

 

 

ApplicationContext 전체를 초기화하지 않고 프로퍼티 설정을 테스트할 때

@ExtendWith(SpringExtension::class) + @ContextConfiguration with ConfigDataApplicationContextInitializer + @TestConfiguration + @EnableConfigurationProperties(optional)

=> 여기서 @TestConfiguration 은 초기화 대상 빈을 지정하는 역할을 한다. 만약 정말 프로퍼티 미러링 빈만을 필요로 한다면 아래와 같이 사용할 수 있다. (다른 빈들도 필요하다면 그냥 @ComponentScan 으로 프로퍼티 미러링 빈과 다른 빈들을 모두 함께 초기화하는 편이 낫다)

@ExtendWith(SpringExtension.class)
@ContextConfiguration(initializers = ConfigDataApplicationContextInitializer.class)
public class MongoPropertiesTest {
    
    @Autowired
    private MongoProperties properties;

    @Configuration
    @EnableConfigurationProperties({MongoProperties.class})
    public static class Config {

    }

}

 

 

Jpa 테스트 수행 시

@DataJpaTest 를 사용한다. 추가로 다른 빈이 필요한 경우 @TestConfiguration + @Import 를 사용한다.

 

웹 MVC 테스트 수행 시

@WebMvcTest 를 사용한다. 추가로 다른 빈이 필요한 경우 @TestConfiguration + @Import 를 사용한다.

 

@Service 등 비즈니스 로직 테스트 수행 시

1. 데이터 저장 애뮬레이션이 필요하다면 아래와 같이 사용한다.

@ExtendWith(SpringExtension.class)  @SpringBootTest + @TestConfiguration(선택) + @Import 

2. 데이터 저장 애뮬레이션이 필요하지 않다면 아래와 같이 사용한다.

@ExtendWith(SpringExtension.class) + @SpringBootTest + @TestConfiguration(선택) + @Import + @MockBean(to repositories) 

 

통합 테스트 수행 시

@SpringBootTest 를 사용한다.

 

어노테이션 설명

@RunWith

JUnit 4 의 어노테이션으로, JUnit 의 Test Runner 구현체를 인자로 받아 테스트 메서드 실행에 관한 동작을 확장할 수 있다. 주로 아래와 같이 사용된다.

@RunWith(SpringJUnit4ClassRunner.class)

Runner 에 대한 자세한 설명은 아래를 참조

https://eminentstar.github.io/2017/07/23/about-junit-and-test.html

 

 

@RunWith(SpringJUnit4ClassRunner.class) vs @RunWith(SpringRunner.class)

같은 녀석들이다. SpringJUnit4ClassRunner 의 이름을 줄인 버전이 SpringRunner 라고 생각하면 된다.

 

 

@ExtendWith

JUnit 5 의 어노테이션으로, @RunWith 을 대체한다. @RunWith 에서 Runner 의 구현체를 인자로 받았던 것처럼, 여기서는 Extension 인터페이스의 구현체를 인자로 받는다. Extension 은 주로 test 메서드의 전/후 동작, 조건부 테스트 실행, 테스트 메서드 라이프싸이클 콜백, 파라미터 리솔루션, 예외 처리 등 테스트에 영향을 미치는 동작에 대한 확장을 제공한다. 대표적인 구현체로 SpringExtension 이 있다.

SpringExtension 은 어플리케이션 컨텍스트를 로드한다. 아래 @SpringBootTest 또한 어플리케이션 컨텍스트를 로드하는데, 둘의 차이는 SpringExtension 은 어플리케이션 컨텍스트를 '전통적인(tranditional) 스프링 방식' 으로 로드하고, @SpringBootTest 는 '스프링 부트 방식' 으로 로드한다는 점이다. 때문에 SpringExtension 는 스프링 부트에서 제공하는 '외부 설정 로딩' 을 사용하지 못하여 application.properties, 환경변수, cli 아규먼트 등으로 어플리케이션 옵션을 주입할 수 없다. 프로퍼티를 사용하려면 ConfigDataApplicationContextInitializer, @EnableConfigurationProperties 등을 사용해서 직접 프로퍼티 설정 로딩 코드를 작성해야 한다.

 

 

@SpringBootTest

스프링 어플리케이션 컨텍스트를 로드하는 어노테이션으로, 운영 환경에 가장 가까운 환경을 애뮬레이션한다. 필요한 모든 빈을 초기화하기 때문에 무겁게 작동한다. 내장 톰캣을 사용한 웹 환경 애뮬레이션을 지원하며, 통합 테스트에 적절하다. 이 어노테이션의 실체는 @BootStrapWith(SpringBootTestContextBootstrapper.class) 이다. SpringBootTestContextBootstrapper 에서 어플리케이션 컨텍스트를 로드하고 클래스패스에서 @SpringBootConfiguration(@SpringBootApplication) 을 찾아 초기화한다.

스프링 부트 버전 2.1 부터 이 어노테이션에 @ExtendWith(SpringExtension.class) 가 붙었다. 즉 버전 2.1 부터는 @ExtendWith 을 함께 사용할 필요가 없다.

위 SpringExtension 에서 설명하였듯, 이 어노테이션은 어플리케이션 컨텍스트를 '스프링 부트 방식' 으로 로드한다. 때문에 프로퍼티, 환경변수, cli 아규먼트를 사용하는 일이 가능하다.

@Configuration 또는 classes 가 함께 지정되지 않으면, 이 어노테이션이 지정된 클래스의 상위, 하위에 존재하는 @SpringBootConfiguration 을 모두 찾는다.

@ContextConfiguration(loader=...) 를 지정하지 않으면 기본적으로 SpringBootContextLoader 를 사용한다.

 

 

@MockBean

목 인스턴스를 주입한다. 테스트 클래스가 다른 빈에 의존할 때, 논리적으로는 필요하지만 실제 동작 테스트까지는 필요하지 않은 빈이 있을 수 있다. 그럴 때 @Autowired 를 대신해서 이 어노테이션을 사용한다. 목 인스턴스는 실제로 작동하지는 않고 '작동하는 척' 한다. 아래와 같이 given 등 메서드를 사용해서 마치 실제로 호출해서 어떤 결과를 반환받은 것처럼 사용한다.

given(mockRepository.save(any(MyEntity.class))).willReturn(mySavedEntity())

 

 

@DataJpaTest

data jpa 에 관련한 테스트를 지원하기 위한 어노테이션으로, jpa 리포지토리 테스트에 사용하기 적절하다. data jpa 작동에 필요한 빈만을 초기화하기 때문에 @SpringBootTest 보다 훨씬 가볍게 작동한다. 하지만 @Service 등 다른 빈을 초기화하지 않기 때문에, 테스트가 퍼시스턴스 레이어 위쪽에 의존성을 가진 경우 이 어노테이션만으로는 테스트 불가능하다.

이 어노테이션은 기본 설정으로 테스트 시 인메모리 데이터베이스를 사용하도록 하며, 테스트 케이스마다 @Transactional 을 적용하며 이 트랜잭션은 한 테스트 케이스가 종료되면 자동으로 롤백을 수행한다.

 

 

@AutoConfigureTestDatabase

어플리케이션에 설정된 데이터베이스를 테스트용 데이터베이스로 대체하는 어노테이션이다. replace 라는 인자값이 중요한데, 아래와 같은 값들이 있다.

Replace.ANY: 기본값으로, 명시적 or auto-configured 된 테스트용 데이터베이스로 대체한다. 

Replace.AUTO_CONFIGURED: 오직 auto-configured 된 테스트용 데이터베이스로만 대체한다.

Replace.NONE: 데이터베이스를 대체하지 않는다.

@DataJpaTest 는 이 어노테이션을 ANY 로 사용하며 인메모리 데이터베이스로 대체한다. 직접 데이터베이스를 구동하여 테스트 결과를 조회하고 싶거나 하는, 이런 동작을 원치 않는 경우에는 Replace.NONE 을 사용하면 된다.

 

 

@WebMvcTest

data jpa 전용 테스트를 지원하기 위해 @DataJpaTest 가 있듯, 웹 MVC를 테스트하기 위해 이 어노테이션이 있다. 아래와 같은 대상만 스캔하여 빈으로 초기화한다.

@Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor

주로 컨트롤러-인터셉터-필터를 테스트하기 위해 사용한다.

 

 

@TestConfiguration

테스트 설정을 위한 어노테이션으로, @Configuration 을 테스트 환경에 맞게 확장한 것이라 생각할 수 있다. 사실 그 동작은 @Configuration 과 다르지 않다. (실제로 @TestComponent 가 붙어있을 뿐이다.) 차이점이 있다면 이 어노테이션은 기본적으로 @SpringBootConfiguration 에서 스캔하지 않는다. 때문에 직접 @Import or @ContextConfiguration 을 명시해야 한다. (대신, @SpringBootTest(classes=... ) 를 사용하거나, @ComponentScan을 직접 사용한 경우 스캔 대상이 되므로 이 때 이 어노테이션이 지정된 클래스를 제외하기를 원한다면 TypeExcludeFilter 를 사용해야 한다.)

 

이 어노테이션의 사용법은 대표적으로 두 가지가 있다.

1. @SpringBootTest 지정 클래스의 내부클래스에 지정하기

이 때는 @Import 를 사용하지 않아도 자동으로 설정이 로드된다. 특정 @SpringBootTest 테스트 클래스 안에서만 사용할 설정을 정의한다.

 

2. 독립된 외부 클래스에 지정하고 @Import 하기

여러 테스트에서 공유해서 사용할 설정을 정의한다.

 

이 어노테이션의 주 용도는:

1. 테스트 환경에서 필요한 빈들만을 초기화하는 데에 있다. @DataJpaTest, @WebMvcTest 와 같이 특정 레이어에 대한, 통합테스트보다 가벼운 환경으로 테스트하는 경우 초기화 대상에서 벗어난 빈이 필요할 수 있다. 이를 위해 @SpringBootTest 를 사용한다면 불필요하게 무거운 동작이 추가될 것이다. 이 때 @TestConfiguration 을 사용해서 원하는 빈들을 초기화하는 클래스를 작성하고, 테스트 클래스에서 이 클래스를 @Import 하면 여기의 빈들만을 초기화한다.

https://csy7792.tistory.com/335

 

2. 테스트 환경에서 특정 빈을 오버라이딩할 수 있다. 어떤 빈들은 테스트 시 다른 구현체를 사용하고 싶다면, 이 어노테이션을 사용하여 같은 타입, 이름의 빈 초기화 코드를 넣고 @Import 하면 테스트 시 이 빈을 사용한다.

빈 오버라이딩 시에는 spring.main.allow-bean-definition-overriding=true 를 설정하거나, 빈 이름을 따로 지정해서 기존 빈 이름과 겹치지 않게 하여야 한다. 그렇지 않으면 빈 중복으로 에러가 발생한다.

https://www.logicbig.com/tutorials/spring-framework/spring-boot/test-configuration.html

 

 

 

 

 

 

1. 디폴트

정적 자원 핸들링에는 디폴트로 ResourcehttpRequestHandler가 사용된다.

디폴트로, 아래 경로의 자원을 '/' URI 패턴으로 제공한다.

classpath:/static

classpath:/public

classpath:/resources

classpath:/META-INF/resources

 

예로, classpath:/static/myface.html 이 있을 때,

http://example.com/myface.html 경로로 접근할 수 있다.

 

 

2. 커스터마이징

주의: 설정 커스터마이징 시 디폴트 설정은 사라진다. 따로 설정하지 않으면 더이상 '/' 패턴으로 정적 자원 접근이 불가능하다.

 

프로퍼티 방식

spring.mvc.static-path-pattern=/mystatic/**

spring.resources.static-locations=classpath:/mystatic/,file:/shared/mystatic/

 

URI 패턴, 자원 위치 모두 콤마로 구분하여 2개 이상 설정할 수 있다.

 

위 설정은 아래와 같이 매핑된다

http://example.com/mystatic/myface.html => classpath:/mystatic/myface.html (없으면 파일시스템 /shared/mystatic/myface.html)

 

 

위와 같이, 클래스패스가 아닌 일반 파일시스템 접근에는 'classpath' 대신 'file:' 을 사용한다. (윈도우는 'file://')

 

자바 방식

위 설정을 자바 방식으로 하면 아래와 같다.

@Configuration 
public class WebConfig implements WebMvcConfigurer {

	@Override public void addResourceHandlers(ResourceHandlerRegistry registry) { 
    		registry.addResourceHandler("/mystatic/**")
        		.addResourceLocations("classpath:/mystatic/", "file:/shared/mystatic/");
	}    
}

 

WAR 파일의 경우, webapp/mystatic/ 에 자원이 있다면, 아래와 같이 'classpath:'를 빼고 설정한다.

spring.mvc.static-path-pattern=/mystatic/**

spring.resources.static-locations=/mystatic/

 

 

JUnit 5 는 JDK 8+ 을 요구한다. 

 

때문에 org.junit.jupiter.api.Assertions.. 를 사용하면 오류가 발생한다. (내부에서 Function 시리즈를 사용함.)

JUnit api 는 JUnit 4 이하의 org.junit.Assert... 를 사용해야 한다.

 

그리고 JUnit 5 에서, 이전 버전과의 런타임 호환성을 위한 vintage 를 의존성에 추가한다:

	testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.0'
	testImplementation 'org.junit.jupiter:junit-jupiter-api:5.5.0'
	testImplementation "junit:junit:4.12"
	testRuntimeOnly 'org.junit.vintage:junit-vintage-engine:5.5.0'
	testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.5.0'

5.6.0 이상을 사용하니 아래와 같은 오류가 발생했다. 고쳐서 사용할 수 있는 방법이 있을 수 있으나, 일단 5.5.0 을 사용하기로 한다.

 

* What went wrong: 
Execution failed for task ':compileTestJava'. 
> Could not resolve all files for configuration ':testCompileClasspath'. 
   > Could not resolve org.junit.jupiter:junit-jupiter-api:5.6.0. 
     Required by: 
         project : 
      > Cannot choose between the following variants of org.junit.jupiter:junit-jupiter-api:5.6.0: 
          - javadocElements 
          - sourcesElements 
        All of them match the consumer attributes: 
          - Variant 'javadocElements' capability org.junit.jupiter:junit-jupiter-api:5.6.0: 
              - Unmatched attributes: 
                  - Found org.gradle.category 'documentation' but wasn't required. 

 

Thymeleaf 를 사용하면 meta, link 태그 등의 닫기 태그가 없어 SAX 파싱 예외가 발생하는 경우가 있다. 일반적인 html에서는 저런 태그를 닫아주지 않아도 이런 경우가 없는데, 정확히 어느 버전까지인지는 모르겠지만, 예전 버전의 Thymeleaf lib 사용 시 xhtml의 엄격한 룰이 적용되어 이와 같은 현상이 발생한다고 한다.

 

이를 피하기 위해서는,

1. application.properties 에 다음 프로퍼티를 설정한다:

spring.thymeleaf.mode=LEGACYHTML5

 

2. nekohtml 의존성을 추가한다:

net.sourceforge.nekohtml:nekohtml

 

ps: nekohtml은 버전 1.9.15 이상을 사용하라는 권고가 있음.


스프링부트 테스트를 실행할 때 다음과 같은 에러를 만날 수 있다.


java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test


이 에러는 @SpringBootTest 클래스가 실행되면서 필요한 스프링부트 설정 클래스를 찾지 못할 경우 발생한다.


흔히, 테스트 클래스를 작성하면서 테스트 클래스 패키지명을 메인 클래스 패키지와 차이가 생기면서 이 에러를 만나는데, 해결 방법은 간단하다. 


프로젝트 레이아웃이 아래와 같다고 하자:


src

+--main

+--com

+--demo

+--demo

+--DemoApplication.java (@SpringBootApplication)

+--test

+--com

+--demo

+--demo

+--DemoApplicationTest.java (@SpringBootTest)

+--demo

+--others

+--MyCustomTest.java (@SpringBootTest)



DemoApplicationTest 는 정상 실행 될 것이다. MyCusTomTest 는 위의 에러를 만날 것이다. MyCustomTest 가 찾을 수 있는 스프링부트 설정 클래스가 없기 때문이다.


스프링부트 실행 클래스는 자신의 패키지에서부터 스프링부트 설정 클래스를 찾기 시작하여, 찾을 때 까지 상위 패키지로 계속 찾아나간다.


위의 경우, MyCustomTest 클래스가 실행되면서 스프링부트 설정 클래스를 찾는 순서는 다음과 같다:


1. com.demo.others

2. com.demo

3. com


그런데 위 프로젝트에서 유일한 스프링부트 설정 클래스인 DemoApplication 은 MyCustomTest 의 상위 패키지가 아닌, 같은 레벨의 다른 이름을 가진 패키지에 존재하고, 이 경우 MyCustomTest 는 설정 클래스를 찾을 수 없다.


해결 방법은 다음과 같다.


1. 경로 똑같이 맞추기1 (MyCustomTest 클래스를 com.demo.demo 로 이동)

2. 경로 똑같이 맞추기2 (com.demo.others 에도 스프링부트 설정 패키지 생성)

3. 상위 경로에 설정 클래스 하나 두기 (com 또는 com.demo 에 에러 방지용 디폴트 설정 클래스 생성)




어쨌든 테스트 실행 클래스가 찾을 수 있는 설정 파일이 있기만 하면 된다.







+ Recent posts