728x90

spring data mongo를 사용하여 MongoTemplate 를 이용하여 쿼리를 작성하다 보면 아래와 같이 필드값에 대해 string 값을 사용하게 된다.

Criteria.where("_id").`is`(Entity.id)

하지만 이렇게 쿼리를 사용하다 보면 type-safe 하지 않다는 점과 실제로 해당 엔티티에 존재하는 필드인지 오탈자는 없는지 실행되는 순간까지 확인할 수 없으므로 위험도는 증가한다.

private class KPropertyPath<T, U>(
	val parent: KProperty<U?>,
	val child: KProperty1<U, T>
) : KProperty<T> by child

/**
 * Recursively construct field name for a nested property.
 * @author Tjeu Kayim
 */
internal fun asString(property: KProperty<*>): String {
	return when (property) {
		is KPropertyPath<*, *> ->
			"${asString(property.parent)}.${property.child.name}"
		else -> property.name
	}
}

/**
 * Builds [KPropertyPath] from Property References.
 * Refer to a nested property in an embeddable or association.
 *
 * For example, referring to the field "author.name":
 * ```
 * Book::author / Author::name isEqualTo "Herman Melville"
 * ```
 * @author Tjeu Kayim
 * @author Yoann de Martino
 * @since 2.5
 */
operator fun <T, U> KProperty<T?>.div(other: KProperty1<T, U>): KProperty<U> =
	KPropertyPath(this, other)

728x90

'spring' 카테고리의 다른 글

WebClient의 DataBufferLimitException 해결방법  (0) 2024.11.15
TestExecutionListener를 이용한 테스트 격리 방법  (1) 2024.11.05
Spring Async  (0) 2024.08.02
Spring Webclient  (0) 2024.08.02
Spring AOP  (0) 2024.08.02
728x90

DataBufferLimitException


Spring Webflux의 WebClient를 사용하던 중 특정 api를 호출하여 응답받는 경우 아래와 같은 예외가 발생했다.

org.springframework.core.io.buffer.DataBufferLimitException: Exceeded limit on max bytes to buffer : 262144

 

원인은?

WebClient 설정에는 애플리케이션의 메모리 이슈를 방지할 수 있도록 코덱(codec)의 메모리 버퍼 사이즈 제한 값을 갖고 있다. 이 값은 기본적으로 256KB로 설정되어 있는데, 이 값을 넘어가는 경우 DataBufferLimitException 예외가 발생한다.

 

해결 방법


@Bean
fun webClient(): WebClient {
    val strategies = ExchangeStrategies.builder()
        .codecs { codecs: ClientCodecConfigurer ->
            codecs.defaultCodecs().maxInMemorySize(64 * 1024 * 1024)
        }
        .build()
    return WebClient.builder()
        .exchangeStrategies(strategies)
        .build()
}

DataBufferLimitException 예외가 발생하지 않도록 WebClient를 설정할 때 코덱(codec)의 메모리 버퍼 사이즈 제한 값을 maxInMemorySize 메서드의 파라미터로 넣어주면 된다.

만약 사이즈 제한을 두고싶지 않은 경우 메서드 파라미터로 -1을 전달하면 된다.

 

클라이언트 사이드

참고로 해당 설정의 코덱은 client side의 코덱이다.

public interface ClientCodecConfigurer extends CodecConfigurer {

	/**
	 * {@inheritDoc}
	 * <p>On the client side, built-in default also include customizations related
	 * to multipart readers and writers, as well as the decoder for SSE.
	 */
	@Override
	ClientDefaultCodecs defaultCodecs();
	
	}

 

 

서버 사이드

만약 서버사이드쪽 버퍼 사이즈를 변경하고 싶다면 WebFluxConfigurer 인터페이스를 사용하여 구성이 가능하다.

@Configuration
class WebConfig : WebFluxConfigurer {

  override fun configureHttpMessageCodecs(configurer: ServerCodecConfigurer) {
        configurer.defaultCodecs().maxInMemorySize(10*1024*1024)
  }
}

 

 

WebClient Timeout 관련


추가적으로 WebClient의 타임 아웃 관련 설정도 알아보자. WebClient의 타임아웃을 설정하는 쉬운 방법은 기본 http 클라이언트를 사용하여 전역적으로 설정하는 것이다. 기본적으로 Reactor Netty가 사용된다.

 

Response Timeout

요청을 보낸 후 응답을 받기까지 기다리는 시간 제한이다. responseTimeout() 메서드를 통해 설정 가능하다.

val httpClient = HttpClient.create()
	.responseTimeout(Duration.ofMillis(5000))
    
return WebClient.builder()
	.clientConnector(ReactorClientHttpConnector(httpClient))
	.build()

 

Connection Timeout

커넥션 타임아웃은 클라이언트와 서버간의 연결이 이루어져야하는 시간 제한이다. option() 메서드를 통해 다양한 옵션 키값들을 설정할 수 있다. 커넥션이 이루어지지 않거나 끊어지면 ConnectTimeoutException  예외가 발생한다.

val client = HttpClient.create()
  .option(ChannelOption.CONNECT_TIMEOUT_MILLIS, 10000);
  
  
val client = HttpClient.create()
  .option(ChannelOption.SO_KEEPALIVE, true)
  .option(EpollChannelOption.TCP_KEEPIDLE, 300)
  .option(EpollChannelOption.TCP_KEEPINTVL, 60)
  .option(EpollChannelOption.TCP_KEEPCNT, 8);

 

Read and Write Timeout

read timeout은 특정 시간 내에 데이터를 읽지 못햇을 때, write timeout은 특정 시간에 쓰기 작업을 완료할 수 없을 때 발생한다.

val httpClient = HttpClient.create()
    .doOnConnected { conn: Connection ->
        conn.addHandlerLast(ReadTimeoutHandler(5000, TimeUnit.MILLISECONDS))
            .addHandlerLast(WriteTimeoutHandler(5000, TimeUnit.MILLISECONDS))
    
}

 

그 외

이 외에도 SSL/TLS timeout, Proxy timeout, Request level의 timeout 설정 또한 가능하다.

 

참고

728x90

'spring' 카테고리의 다른 글

[spring data mongo] Query를 Type-safe 하게 작성하기 (작성 중)  (0) 2024.11.19
TestExecutionListener를 이용한 테스트 격리 방법  (1) 2024.11.05
Spring Async  (0) 2024.08.02
Spring Webclient  (0) 2024.08.02
Spring AOP  (0) 2024.08.02
728x90

서론


통합테스트나 인수테스트를 위해 @SpringBootTest를 이용하는 테스트 환경에서 테스트들을 격리하는 방법들 중 주로 널리 알려진 것들에 대해 정리하고 TestExecutionListener 사용하여 격리했던 방법을 공유해보고자 한다.

 

테스트 격리란


테스트 격리란 테스트 순서에 상관없이 독립적으로 실행되며, 결정적으로 동작되는 것을 의미한다.

비결정적인 테스트는 쉽게 말해서 테스트 실행 시 같은 입력값에 대해 항상 같은 출력하지 않는 테스트를 의미한다.

 

따라서 "결정적으로 동작"의 의미란 멱등성과 비슷하다. 같은 입력 값이면 항상 같은 결과를 반환하는 것을 의미한다.

 

@SpringBootTest 테스트 격리 방법


@Transactional

첫 번째로 트랜잭션 어노테이션이다. 많이 알려진 내용으로 해당 어노테이션을 테스트 코드에 사용하면 테스트가 종료되고 자동으로 롤백하는 기능을 이용하는 것이다.

test transactions will be automatically rolled back after completion of the test.

 

하지만 해당 방법은 API 접점에서 검증하는 E2E 테스트를 인수테스트의 의도로 인수테스트 환경을 구축했다면 @Transactional 어노테이션으로는 테스트 격리를 하기 힘들 수도 있다.

 

인수테스트는 보통 블랙박스 성격의 테스트이므로 WebTestClient 혹은 RestAssured을 이용하는 경우가 많은데 그러려면 @SpringBootTest 어노테이션을 RANDOM_PORT나 DEFINED_PORT를 이용하여 실제 웹 한경을 구성하기 때문이다

따라서 포트 번호를 따로 주어 WebEnvironment를 구성하는 경우 Http client와 테스트 서버는 별도의 스레드에서 수행되기 때문에 자동 롤백이 이루어지지 않는다.

 

@DirtiesContext

효과적인 테스트 수행을 위해 스프링에서는 context caching 기능을 지원한다. 해당 어노테이션은 테스트와 관련된 ApplicationContext가 Dirty하기 때문에 Context Cache에서 종료되고 삭제되어야 함을 나타내는 Test Annotation이다.

 

@DirtiesContext 어노테이션을 통해 테스트를 수행하기 전, 수행한 이후, 그리고 테스트의 각 테스트 케이스마다 수행하기 전, 수행한 이후에 context를 다시 생성하도록 지시하여 테스트를 격리할 수 있다.

 

하지만 매 테스트마다 Application Context를 매번 생성해야 하기 때문에 테스트 속도가 현저히 느려진다.

 

테스트 코드상으로 매번 삭제

테스트에 필요한 데이터를 JUnit 생명주기인 @BeforeEach, @AfterEach를 활용하여 테스트가 시작되기 전이나 후에 데이터들을 삭제하여 테스트 격리를 하는 방식이다. application context를 매번 띄우는 것보다 낮은 비용이므로 속도 측면에서 효율적이라고 볼 수 있다.

 

하지만 이 방식의 단점은 생성해야 할 데이터가 많거나, 연관관계를 모를 경우 제대로 데이터를 삭제하지 못하는 문제가 발생할 수 있다.

또한, 테스트 클래스도 길어지기에 가독성도 안 좋을 수 있다.

 

TRUNCATE를 통한 테이블 초기화


TRUNCATE (DDL)는 DELETE (DML)와 다르게 행마다 락을 걸지 않고, 트랜잭션 로그 공간을 적게 사용하므로 초기화하는 속도가 더 빠르다.

테스트 격리를 위해 TRUNCATE 하는 방법은 보통 크게 2가지이다.

  1. @Sql 사용
  2. 코드에서 truncate 쿼리 사용

 

@Sql

@Sql은 Spring Boot에서 제공하는 애노테이션이며, 클래스 테스트가 실행되기 전 @Sql이 가리키는 경로에 있는 SQL문이 먼저 실행되게 된다. 이 SQL 파일 안에 TRUNCATE관련 내용을 넣어두는 방식을 통해 DB 테스트 격리를 할 수 있다.

 

하지만 테이블이 추가/변경/삭제될 때마다 수정이  필요하므로 관리가 필요하다.

 

코드에서 테이블 TRUNCATE 쿼리 사용

JPA 사용 시 EntityManager를 이용하여 혹은 jpa를 사용하지 않는 경우 DataSource를 이용하여 테이블 이름을 조회하여 각 테이블들을 truncate 시켜주는 쿼리를 수행하는 것이다. 해당 방법을 사용하면 테이블 상태에 의존하지 않는 초기화 환경 구축 가능하다.

해당 방법을 사용해서 테스트 격리를 적용해 보자.

 

 

코드에서 테이블 TRUNCATE 쿼리 사용 - TestExecutionListener 이용하기


TestExecutionListener

TestExecutionListener는 스프링에서 제공하는 테스트 실행 주기에서 콜백 메서드를 통해 사용자가 추가 작업을 수행하도록 지원하는 인터페이스이다.

 

사실 위에서 살펴본 transactional test의 기본 롤백, @DirtiesContext, @Sql의 기능들 모두 해당 TestExecutionListener 인터페이스를 상속받은 기본 리스너를 통해 스프링에서 제공하고 있는 것이다.

public interface TestExecutionListener {
	default void beforeTestClass(TestContext testContext) throws Exception {};
	default void prepareTestInstance(TestContext testContext) throws Exception {};
	default void beforeTestMethod(TestContext testContext) throws Exception {};
	default void afterTestMethod(TestContext testContext) throws Exception {};
	default void afterTestClass(TestContext testContext) throws Exception {};
}
  • beforeTestClass()
    • junit의 @BeforeAll와 같다.
    • 모든 테스트를 실행하기 전 단 한 번만 실행/호출
  • prepareTestInstance()
    • junit에서 X
    • 테스트 인스턴스가 준비되었을 때 호출
  • beforeTestMethod()
    • junit의 @BeforeEach
    • 각 테스트 메서드 실행 전에 실행/호출
  • afterTestExecution()
    • junit에서 X
    • 각 테스트 메서드 실행 직후 호출
  • afterTestMethod()
    • junit에서 @AfterEach
    • 각 테스트 메서드 실행 후에 실행/호출
  • afterTestClass()
    • junit의 @AfterAll
    • 모든 테스트를 실행한 후 단 한 번만 실행/호출

 

Custom TestExecutionListener를 구현


sql 파일이 아닌 코드에서 테이블 TRUNCATE를 위해 AbstractTestExecutionListener 이용해 보자.

afterTestMethod() 메서드 오버라이딩을 통해 테스트 메서드가 종료된 후 모든 테이블 truncate 쿼리를 실행한다.

 

 

@TestExecutionListeners 통해 리스너 등록

 

해당 커스텀 리스너를 등록하기 위해서는 @TestExecutionListeners 어노테이션을 통해 등록하면 되는데 기본 제공되는 테스트 리스너들이 필요하다면 MERGE_WITH_DEFAULTS 모드를 사용하도록 하자.

 

그리고 주의할 점은 TestExecutionListeners 들은 AnnotationAwareOrderComparator를 통해 정렬되어 순서대로 핸들링되기 때문에 순서에 주의하자.

 

따라서 AcceptanceTestExecutionListener 클래스에 마지막 순서로 동작하기 위해 Ordered 인터페이스를 구현하도록 추가하고 어노테이션을 붙여 테스트 격리를 진행하였다.

Ordered 구현하여 순서값 지정

 

사용 및 확인


 

이렇게 테스트 메서드 종료시마다 truncate 쿼리가 나가는 것을 확인할 수 있다. 물론 해당 방법이 가장 좋은 방법인 것은 아닐 것이다.

따라서 각각 자신의 상황에서 추구하는 테스트 격리의 정의와 격리를 위한 장치가 필요한지 살펴보며 적용하는 것이 가장 좋은 방법일 것이라고 생각한다.

 

참고


728x90

'spring' 카테고리의 다른 글

[spring data mongo] Query를 Type-safe 하게 작성하기 (작성 중)  (0) 2024.11.19
WebClient의 DataBufferLimitException 해결방법  (0) 2024.11.15
Spring Async  (0) 2024.08.02
Spring Webclient  (0) 2024.08.02
Spring AOP  (0) 2024.08.02
728x90

발단

사용자가 어떤 게시글을 작성하면 조건에 맞는 다른 사용자에게 쪽지같은 알림을 구현해야 하는 상황

  • 게시글 작성
  • 알림

처음에는 하나의 transaction으로 처리로 구현을 진행했으나 알림 기능은 부가적인 기능이고 게시글 작성 기능에 영향을 주면 안된다고 생각이 들었다. 따라서 게시글 작성 후 알림 처리가 지연되는 경우 게시글 작성 자체를 지연하는 것이 아니라, 게시글 작성은 완료시키고 다른 Thread에서 알림을 처리하도록 비동기 처리를 진행할 수 있을 것이다.

스프링에서는 @Async Annotation을 이용하여 간단하게 비동기 처리를 할 수 있다.

Java 비동기방식 처리

그전에 먼저 자바의 비동기 작업 처리를 알아보자.

따라서 method가 실행되면 새로운 thread를 만들고 그 thread에서 메시지를 저장하도록 처리하면 될 것 같다.

public class AsyncService {
    public void asyncMethod(String message) throws Exception {
        // do something
    }
}

하지만 해당 방법은 thread를 관리할 수 없기 때문에 위험한 방법이다.

Thread를 관리하기 위해서 ExecutorService 사용해보자.

ExecutorService는 쉽게 비동기로 작업을 실행할 수 있도록 도와주는 JDK(1.5부터)에서 제공하는 interface 이다. 일반적으로 ExecutorService는 작업 할당을 위한 스레드 풀과 API를 제공한다.

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class AsyncService {

    private static ExecutorService executorService = Executors.newFixedThreadPool(10);

    public void asyncMethod(final String message) throws Exception {
        executorService.submit(new Runnable() {
            @Override
            public void run() {
                // do something
            }            
        });
    }

}

하지만 비동기방식으로 처리하고 싶은 method마다 반복적으로 동일한 수정 작업들을 진행 해야할 것이다.

@Async with SimpleAsyncTaskExecutor

@Async Annotation은 Spring에서 제공하는 Thread Pool을 활용하는 비동기 메소드 지원 Annotation이다.

만약 Spring Boot에서 간단히 사용하고 싶다면, 단순히 Application Class에 @EnableAsync Annotation을 추가하고, 원하는 method 위에 @Async Annotation을 붙여주면 사용할 수 있다.

@EnableAsync
@SpringBootApplication
public class SpringBootApplication {
    ...
}
public class AsyncService {

    @Async
    public void asyncMethod(String message) throws Exception {
        ....
    }
}

하지만 @Async의 기본설정은 SimpleAsyncTaskExecutor를 사용하도록 되어있기 때문입니다.

본인의 개발 환경에 맞게 Customize하기에는 직접 AsyncConfigurerSupport를 상속받는 Class를 작성하는 것이 좋다.

@Async with ThreadPoolTaskExecutor

Thread pool을 이용해서 thread를 관리가능한 방식다. 아래와 같은 AsyncConfigurerSupport를 상속받는 Customize Class를 구현하자.

그리고 Application 클래스에 @EnableAutoConfiguration(혹은 @SpringBootApplication) 설정이 되어있다면 런타임시 @Configuration가 설정된 SpringAsyncConfig 클래스의 threadPoolTaskExecutor bean 정보를 읽어들이기 때문에 앞서 설정한 Application 클래스의 @EnableAsync을 제거한다.

import java.util.concurrent.Executor;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.AsyncConfigurerSupport;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;

@EnableAsync
@Configuration
public class AsyncConfig extends AsyncConfigurerSupport {

    @Override
    public Executor getAsyncExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecuto();
        executor.setCorePoolSize(10);
        executor.setMaxPoolSize(20);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("my thread-");
        executor.initialize();
        return executor;
    }
}

여기서 설정한 요소들은 아래와 같다.

  • @Configuration : Spring 설정 관련 Class로 @Component 등록되어 Scanning 될 수 있다.
  • @EnableAsync : Spring method에서 비동기 기능을 사용가능하게 활성화 한다.
  • CorePoolSize : 기본 실행 대기하는 Thread의 수**
  • MaxPoolSize : 동시 동작하는 최대 Thread의 수
  • QueueCapacity : MaxPoolSize 초과 요청에서 Thread 생성 요청시,해당 요청을 Queue에 저장하는데 이때 최대 수용 가능한 Queue의 수,Queue에 저장되어있다가 Thread에 자리가 생기면 하나씩 빠져나가 동작
  • ThreadNamePrefix : 생성되는 Thread 접두사 지정

위와 같이 작성한 후 비동기 방식 사용을 원하는 method에 @Async Annotation을 지정해주면 된다.

@Async annotation에 bean의 이름을 제공하면 SimpleAsyncTaskExecutor가 아닌 설정한 TaskExecutor로 thread를 관리하게 된다.

@EnableAsync
@Configuration
public class AsyncConfig extends AsyncConfigurerSupport {

    @Bean(name = "threadPoolTaskExecutor")
    public Executor threadPoolTaskExecutor()
    {
        ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
        taskExecutor.setCorePoolSize(3);
        taskExecutor.setMaxPoolSize(30);
        taskExecutor.setQueueCapacity(10);
        taskExecutor.setThreadNamePrefix("Executor-");
        taskExecutor.initialize();
        return taskExecutor;
    }
}

---

public class AsyncService {

        @Async("threadPoolTaskExecutor")
    public void asyncMethod(String message) throws Exception {
        // do something
    }
}

Thread Pool의 종류를 여러개 설정하고자한다면 SpringAsyncConfig 안에 bean을 여러개 만들고 @Async를 설정시 원하는 bean의 이름을 설정하면 된다.

제약사항

@Async Annotation을 사용할 때 아래와 같은 사항을 주의 해야한다.

  1. private method는 사용 불가, public method만 사용 가능
  2. self-invocation(자가 호출) 불가, 즉 inner method는 사용 불가

제약의 원인은 간단한데 @AsyncAOP에 의해 동작하고 있기 때문에 해당 메서드는 프록시될 수 있어야 하기 때문이다.

https://media.vlpt.us/images/gillog/post/5bb64a29-5263-4fcc-9f02-cffea4162137/image.png

출처 : https://dzone.com/articles/effective-advice-on-spring-async-part-1

해당 @Async method를 가로챈 후, 다른 Class에서 호출이 가능해야 하므로,private method는 사용할 수 없는 것이다. 또한 inner method의 호출은 해당 메서드를 직접호출 하기 때문에 self-invocation이 불가능하다. @Transactional 사용시 주의점과 비슷하다고 할 수 있다.

@Slf4j
@Service
public class TestService {

    @Async
    public void innerAsyncMethod(int i) {
        log.info("async i = " + i);
    }

    public void asyncMethod(int i) {
        innerAsyncMethod(i);
    }

}

리턴 타입

@Async 메서드는 AsyncExecutionAspectSupport 클래스의 doSubmit 메서드에서 선택한 실행자와 함께 지정된 작업을 실제로 실행하도록 위임한다.

리턴타입은 크게 두가지로 나뉠 수 있다.

  • 리턴값이 없는 경우
  • 있는 경우(Futrue)

Future에 경우에도 여러 타입이 존재하는데 해당 리턴 값에 대한 것은 여기 에서 자세히 보면 좋을 것 같다

@Nullable
protected Object doSubmit(Callable<Object> task, AsyncTaskExecutor executor, Class<?> returnType) {
    if (CompletableFuture.class.isAssignableFrom(returnType)) {
        return CompletableFuture.supplyAsync(() -> {
            try {
                return task.call();
            }
            catch (Throwable ex) {
                throw new CompletionException(ex);
            }
        }, executor);
    }
    else if (ListenableFuture.class.isAssignableFrom(returnType)) {
        return ((AsyncListenableTaskExecutor) executor).submitListenable(task);
    }
    else if (Future.class.isAssignableFrom(returnType)) {
        return executor.submit(task);
    }
    else {
        executor.submit(task);
        return null;
    }
}

예외 처리

메서드 반환 형식이 Futre인 경우 예외 처리가 쉽다. Future.get() 메서드에서 예외가 발생하기 때문이다.

하지만 반환값이 없는 void이면 예외가 호출 스레드에 전파되지 않는다. 즉 해당 thread가 소리없이 죽기 때문에 예외 처리가 관리되지 않는다.

이러한 예외 처리를 위해서는 AsyncUncaughtExceptionHandler 인터페이스를 구현하여 사용자 지정 비동기 예외 처리기를 생성한다. handleUncaughtException() 메서드는 캐치되지 않은 비동기 예외가 있을 때 호출된다.


public class CustomAsyncExceptionHandler implements AsyncUncaughtExceptionHandler {

    @Override
    public void handleUncaughtException(Throwable ex, Method method, Object... params) {
        log.warn("Exception message - {}", ex.getMessage());
        log.warn("Method name - {}", method.getName());
    }
}

추가

추후 해당 처리를 Spring event를 이용하거나 AOP를 이용하여 리팩토링도 진행할 예정이다.

참고 출처

728x90

'spring' 카테고리의 다른 글

WebClient의 DataBufferLimitException 해결방법  (0) 2024.11.15
TestExecutionListener를 이용한 테스트 격리 방법  (1) 2024.11.05
Spring Webclient  (0) 2024.08.02
Spring AOP  (0) 2024.08.02
Spring Boot Slice Test  (0) 2024.08.02
728x90

개요

스프링 어플리케이션에서 HTTP 요청할 때 사용하는 방법으로 RestTemplate과 WebClient가 있다. 스프링 5.0 이전까지는 클라이언트에서 HTTP 접근을 위해 사용한 것은 RestTemplate 이었다. 스프링 5.0 에서 WebClient가 나왔고 현재는 WebClient를 사용하기를 권고하고 있다. 이번 팀 프로젝트를 진행하면서 외부 api호출 시 WebClient을 사용해보았다. 그럼 RestTemplate과 WebClient는 어떤 특징이 있으며 왜 WebClient를 사용하길 권고하는지 알아보도록 하자.


RestTemplate

스프링 3.0에서부터 지원하며 HTTP 통신에 유용하게 쓸 수 있는 템플릿이다. REST 서비스를 호출하도록 설계되어 HTTP 프로토콜의 메서드 (GET, POST, DELETE, PUT)에 맞게 여러 메서드를 제공한다. RestTemplate은 다음과 같은 특징이 있다

RestTemplate 특징

  • 통신을 단순화하고 RESTful 원칙을 지킨다
  • 멀티쓰레드 방식을 사용
  • Blocking 방식을 사용

1) 클라이언트 애플리케이션 구동 시 쓰레드 풀을 만든다.

2) Request는 먼저 queue에 쌓이고 가용 쓰레드가 있으면 해당 쓰레드에 할당된다. (1요청 당 1쓰레드 할당)

3) 각 쓰레드는 블로킹 방식이기 때문에 완료 응답이 올 때까지 다른 요청에 할당될 수 없다.

4) 쓰레드가 다 찼다면 이후 요청은 queue에 대기하게 된다.


동작원리

RestTemplate은 Multi-Thread와 Blocking방식을 사용한다.

HttpClient는 HTTP를 사용하여 통신하는 범용 라이브러리이고, RestTemplate은 HttpClient 를 추상화(HttpEntity의 json, xml 등)해서 제공해준다. 따라서 내부 통신(HTTP 커넥션)에 있어서는 Apache HttpComponents 를 사용한다.

  1. 어플리케이션이 RestTemplate를 생성하고, URI, HTTP메소드 등의 헤더를 담아 요청한다.
  2. RestTemplate 는 HttpMessageConverter 를 사용하여 requestEntity 를 요청메세지로 변환한다.
  3. RestTemplate 는 ClientHttpRequestFactory 로 부터 ClientHttpRequest 를 가져와서 요청을 보낸다.
  4. ClientHttpRequest 는 요청메세지를 만들어 HTTP 프로토콜을 통해 서버와 통신한다.
  5. RestTemplate 는 ResponseErrorHandler 로 오류를 확인하고 있다면 처리로직을 태운다.
  6. ResponseErrorHandler 는 오류가 있다면 ClientHttpResponse 에서 응답데이터를 가져와서 처리한다.
  7. RestTemplate 는 HttpMessageConverter 를 이용해서 응답메세지를 java object(Class responseType) 로 변환한다.
  8. 어플리케이션에 반환된다.

출처: https://sjh836.tistory.com/141


RestTemplate 사용

RestTemplate을 생성할 때 어떤 HttpClient를 사용할 것인지 ClientHttpRequestFactory를 전달하여 지정할 수 있다. 기본 생성자의 경우 내부적으로 ClientHttpRequestFactory 의 구현체SimpleClientHttpRequestFactory를 사용하여 초기화한다.

RestTemplate을 사용하기 위해서는 restTemplate.메소드명() 을 사용하면 된다.

출처 : https://velog.io/@soosungp33/스프링-RestTemplate-정리요청-함


Connection Pool

RestTemplate 객체를 생성할때 별도의 파리미터 없이 new RestTempalte()으로 생성한다면 Connection Pool을 활용하지 않는 객체이다. 이말은 즉, 요청때 마다 새로운 TCP Connection 을 연결한다는 의미이며 이 때 소켓이 close () 된 이후 소켓이 "TIME_WAIT" 상태가 되는데 만약 요청이 많아진다면 TIME_WAIT 상태의 소켓들을 재사용하지 못해서 요청에 대한 응답에 지연이 생길 수 있다.

이러한 응답 지연 상황을 대비하여 DB가 Connection Pool을 이용하듯이 RestTemplate도 Connection Pool을 이용할 수 있다. 그러기 위해선 RestTemplate 내부 구성을 설정해줘야한다.

단, 호출하는 API 서버가 Keep-Alive를 지원해야지 RestTemplate의 Connection Pool을 활용할 수 있다. 타겟 서버가 Keep-Alive를 지원하지 않는다면 미리 Connection Pool을 만들어 놓지 못하고 요청마다 새로운 Connection이 연결되어 매번 핸드쉐이크가 발생된다. 따라서 Connection Pool을 위한 RestTemplate의 내부 설정이 무의미하게 된다.

@Configuration
public class RestTemplateConfig {
    @Bean
    HttpClient httpClient() {
        return HttpClientBuilder.create()
            .setMaxConnTotal(100)    //최대 오픈되는 커넥션 수
            .setMaxConnPerRoute(5)   //IP, 포트 1쌍에 대해 수행할 커넥션 수
            .build();
    }

    @Bean
    HttpComponentsClientHttpRequestFactory factory(HttpClient httpClient) {
        HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
        factory.setReadTimeout(5000);        //읽기시간초과, ms
        factory.setConnectTimeout(3000);     //연결시간초과, ms
        factory.setHttpClient(httpClient);

        return factory;
    }

    @Bean
    RestTemplate restTemplate(HttpComponentsClientHttpRequestFactory factory) {
        return new RestTemplate(factory);
    }
}

WebClient

WebCleint는 스프링 5.0에서 추가된 인터페이스다. Spring WebFlux는 HTTP request를 수행하는 client인 WebClient 를 포함하고 있으며 반응형으로 동작하도록 설계되었다. 스프링 5.0 이후부터는 RestTemplate 의 대안으로 WebClient를 사용할 것을 권장한다. 실제로는 spring-webflux 라이브러리에 속하지만 이 솔루션은 동기 및 비동기 작업을 모두 지원하므로 서블릿 스택에서 실행되는 애플리케이션에도 적용 가능하다.

WebClient는 다음과 같은 특징이 있다.

  • 싱글 스레드 방식을 사용
  • Non-Blocking 방식을 사용
  • JSON, XML을 쉽게 응답받는다.

출처: https://luminousmen.com/post/asynchronous-programming-blocking-and-non-blocking

Core 당 1개의 Thread를 이용한다.

각 요청은 Event Loop내에 Job으로 등록이 되어 Event Loop는 각 Job을 제공자에게 요청한 후, 결과를 기다리지 않고 다른 Job을 처리한다.

Event Loop는 제공자로부터 callback으로 응답이 오면, 그 결과를 요청자에게 제공한다.


의존성 설정

webflux 의존성을 추가해줘야 한다. Gradle 기준으로 아래와 같이 의존성을 추가해주면 된다.

implementation 'org.springframework.boot:spring-boot-starter-webflux'

WebClient 생성

WebClient를 생성하는 데는 2가지의 방법이 있다.

  • static factory method (WebClient.create();)
  • 다른 옵션을 사용하기 위해 Builder를 활용한 클래스 생성 (WebClient.builder())
    • uriBuilderFactory : base url을 커스텀한 UriBuilderFactory
    • defaultHeader : 모든 요청에 사용할 헤더
    • defaultCookie : 모든 요청에 사용할 쿠키
    • defaultRequest : 모든 요청을 커스텀할 Consumer
    • filter : 모든 요청에 사용할 클라이언트 필터
    • exchangeStrategies : HTTP 메시지 reader & writer 커스텀
    • clientConnector : HTTP 클라이언트 라이브러리 세팅
DefaultUriBuilderFactory factory = new DefaultUriBuilderFactory(baseUrl);
        factory.setEncodingMode(DefaultUriBuilderFactory.EncodingMode.VALUES_ONLY);

Webclient webClient = WebClient
                        .builder()
                      .uriBuilderFactory(factory)
                        .baseUrl("http://localhost:8080")
                        .defaultCookie("쿠키","쿠키값")
                        .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                        .build();

Response 받아오기

response를 받아오는 방법에는 두 가지가 있다.

  • retrieve() → body를 받아 디코딩하는 간단한 메서드
  • exchange() → ClientResponse를 상태값 그리고 헤더와 함께 가져온다

exchange()를 통해 세세한 컨트롤이 가능하지만, Response 컨텐츠에 대한 모든 처리를 직접 하면서 메모리 누수 가능성 때문에 retrieve()를 권고하고 있다.

WebClient client = WebClient.create("https://example.org");

Mono<Person> result = client.get()
      .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
      .retrieve()
      .bodyToMono(Person.class);

bodyToMono 는 가져온 body를 Reactor의 Mono 객체로 바꿔준다. Mono 객체는 0-1개의 결과를 처리하는 객체이다. Flux는 0-N개의 결과를 처리하는 객체이다.

block() 을 사용하면 RestTemplate 처럼 동기식으로 사용할 수 있다.

4xx, 5xx의 응답 코드를 받으면 WebClientResponseException 또는 HTTP 상태에 해당하는 WebClientResponseException.BadRequest 등 과 같은 하위 exception을 던진다. onStatus 메서드로 상태별 exception을 커스텀도 가능하다.

Mono<Person> result = client.get()
      .uri("/persons/{id}", id).accept(MediaType.APPLICATION_JSON)
      .retrieve()
      .onStatus(HttpStatus::is4xxClientError, response -> ...)
      .onStatus(HttpStatus::is5xxServerError, response -> ...)
      .bodyToMono(Person.class);
  • onStatus 를 사용하는 경우, response에 body가 있다면 onStatus 콜백에서 소비하지 않으면 리소스 반환을 위해 body를 자동으로 비우므로 주의해야 한다.

RestTempalte 과 WebClient의 차이

결국 RestTemplate과 WebClient의 가장 큰 차이점은 Non-Blocking과 비동기화 가능 여부일 것이다. 결국 이러한 차이점이 스프링에서 RestTemplate을 사용하는 것 보다 WebClinet의 사용을 권장하는 이유라고 생각한다.

해당 글을 참고해 보면 차이점을 가장 쉽게 이해할 수 있을 것이다

https://user-images.githubusercontent.com/63634505/126900349-905377fe-27ac-4d7a-8b9a-371fb22aee74.png

Non-Blocking?

시스템을 호출한 직후에 프로그램으로 제어가 다시 돌아와서 시스템 호출의 종료를 기다리지 않고 다음 동작을 진행한다. 호출한 시스템의 동작을 기다리지 않고 동시에 다른 작업을 진행할 수 있어서 작업의 속도가 빨라진다는 장점이 있다.


참고 - Spring WebFlux 성능

https://user-images.githubusercontent.com/63634505/126900385-ffdecfe7-f5b6-4c7a-a2ed-69145cc85390.png

출처 : https://alwayspr.tistory.com/44


참고 출처

728x90

'spring' 카테고리의 다른 글

WebClient의 DataBufferLimitException 해결방법  (0) 2024.11.15
TestExecutionListener를 이용한 테스트 격리 방법  (1) 2024.11.05
Spring Async  (0) 2024.08.02
Spring AOP  (0) 2024.08.02
Spring Boot Slice Test  (0) 2024.08.02
728x90

AOP(Aspect Oriented Programming)

AOP는 관점 지향 프로그래밍. Spring의 핵심 개념중 하나인 DI가 애플리케이션 모듈들 간의 결합도를 낮춰준다면, AOP는 애플리케이션 전체에 걸쳐 사용되는 기능을 재사용하도록 지원하는 것

쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다.

예로들어 핵심적인 관점은 결국 우리가 적용하고자 하는 핵심 비즈니스 로직이 된다. 또한 부가적인 관점은 핵심 로직을 실행하기 위해서 행해지는 데이터베이스 연결, 로깅, 파일 입출력 등을 예로 들 수 있다.

AOP에서 각 관점을 기준으로 로직을 모듈화한다는 것은 코드들을 부분적으로 나누어서 모듈화하겠다는 의미다. 이때, 소스 코드상에서 다른 부분에 계속 반복해서 쓰는 코드들을 발견할 수 있는 데 이것을 흩어진 관심사 (Crosscutting Concerns)라 부른다.

위와 같이 흩어진 관심사를 Aspect로 모듈화하고 핵심적인 비즈니스 로직에서 분리하여 재사용하겠다는 것이 AOP의 취지다.

https://user-images.githubusercontent.com/56240505/123369146-27997800-d5b8-11eb-9be7-dfd7a34a4f86.png


기존 핵심 비즈니스 로직

@Service
@RequiredArgsConstructor
public class UserService {

    private final PlatformTransactionManager transactionManager;

    public void someSevice() {
            TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
        // 부가 기능 - 로깅, 보안 등등
        // 핵심 기능
        someServiceMethod();
        // 부가 기능
        transactionManager.commit(transaction);
        } catch (RuntimeException runtimeException) {
            transactionManager.rollback(transaction);
            throw runtimeException;
        }
    }
}

서비스 로직의 원자성 보장을 위해 내부적으로 트랜잭션을 적용한 코드입니다. 문제는 UserService의 클래스에는 someServiceMethod() 핵심 비즈니스 로직 이외에도 트랜잭션 경계 설정이라는 부가 기능 관심사들이 존재한다.

현재 예제 코드는 부가 기능 관심사가 트랜잭션 하나 뿐이지만, 또 다른 부가 기능의 관심사가 추가된다면 부가 기능이 필요한 메서드마다 비슷한 코드를 중복해서 작성해야 한다.

가장 큰 문제는 UserService와 비슷하게 수행해야 하는 클래스가 100개가 더 있을 수 있기 때문에 필요한 클래스 마다 UserService와 같이 중복되는 코드를 반복해서 작성해야 함을 의미한다.

만약 코드를 변경한다면 클래스를 변경하는 이유는 비즈니스 로직의 변경 및 부가 기능 코드 또한 변경해야 하기 때문에 서비스 클래스의 응집도가 떨어지고 가독성 또한 나빠지며, 변경할 부분이 명확하게 드러나지 않게 되는등 유지보수 측면에서 아쉬운 점이 많아진다.


Proxy를 사용하여 개선

프록시 객체에 트랜잭션 등 부가 기능 관련 로직을 위치시키고, 클라이언트 요청이 발생하면 실제 타깃 객체는 프록시로부터 요청을 위임받아 핵심 비즈니스 로직을 실행합니다. 이를 데코레이터 패턴이라고 한다.

public interface UserService {

    void someSevice();
}
@Service
@Primary
@RequiredArgsConstructor
public class SimpleUserService implements UserService {

    @Override
    public void someSevice() {
                ...
    }
}
@Service
@RequiredArgsConstructor
public class UserServiceProxy implements UserService {

    private final UserService target;
    private final PlatformTransactionManager transactionManager;

    @Override
    public void someSevice() {
            TransactionStatus transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());
        try {
        // 부가 기능 - 로깅, 보안 등등
        // 핵심 기능
        someServiceMethod();
        // 부가 기능
        transactionManager.commit(transaction);
        } catch (RuntimeException runtimeException) {
            transactionManager.rollback(transaction);
            throw runtimeException;
        }
    }
}

프록시 객체를 이용하여 핵심 메소드가 호출되기 이전에 부가 기능을 적용하였다.


기존 프록시의 문제점

  • 인터페이스의 모든 메소드를 구현해 위임하도록 코드를 만들어야 한다.
  • 부가기능인 기능이 모든 메소드에 중복돼서 나타난다.

프록시 객체를 이용하여 핵심 비즈니스 로직과 부가 기능 관심사를 분리할 수 있었지만 여전히 한계가 존재한다.. 100개의 클래스가 이와 비슷한 기능을 요구한다면, 100개의 프록시 클래스를 생성하고 인터페이스 메서드를 일일이 구현해야 합니다.

다행히 이러한 별도의 프록시를 번거롭게 생성하는 작업을 생략하는 방법이 존재합니다. Java의 Reflection API를 이용하거나, Spring의 ProxyFactoryBean 등을 사용하는 방법이 존재한다.

  • 더 자세한 설명은 토비의 스프링 or 해당 글 참고!!

Spring AOP

Spring AOP는 Proxy를 기반으로 한 Runtime Weaving 방식이다

  • 프록시 패턴 기반의 AOP 구현체
  • 스프링 빈에만 AOP를 적용 가능
  • 메소드 조인포인트만 제공

스프링이 사용하는 다이나믹 프록시에는 2가지 방법이 있다.

  • JDK Dynamic Proxy
  • CGLib Proxy

스프링에서는 기본적으로 jdk dynamic proxy, 스프링 부트에서는 CGLib Proxy 방식으로 AOP를 사용한다.

JDK Dynamin Proxy, CGLib Proxy의 차이를 알아보자.


JDK Dynamic Proxy

JDK Dynamic Proxy는 Proxy Factory에 의해 런타임시 동적으로 만들어지는 오브젝트이다. JDK Dynamic Proxy는 반드시 인터페이스가 정의되어있고, 인터페이스에 대한 명세를 기준으로 Proxy를 생성한다. 즉, 인터페이스 선언에 대한 강제성이 있다는 단점이 있다.

내부적으로 JDK Dynamic Proxy에서는 InvationHandler라는 인터페이스를 구현해 만들어지는데, invoke 함수를 오버라이딩하여 Proxy의 위임 기능을 수행한다. 이 과정에서 객체에 대한 Reflection 기능을 사용해 구현하기 때문에 퍼포먼스 하락의 원인이 되기도 한다.

  • 이 방식이 Spring AOP의 근간이 되는 방식이다.
  • 인터페이스를 기준으로 Proxy 객체를 생성해준다.
  • 인터페이스가 반드시 필요하다.
  • 리플렉션을 사용하기때문에 성능적으로 좋지 못 하다.

CGLib Proxy

CGLIB Proxy는 순수 Java JDK 라이브러리를 이용하는 것이 아닌 CGLIB라는 외부 라이브러리를 추가해야만 사용할 수 있다. CGLIB의 Enhancer 클래스를 바탕으로 Proxy를 생성하며, 인터페이스가 없어도 Proxy를 생성할 수 있다. CGBLIB Proxy는 타겟 클래스를 상속받아 생성하기 때문에 Proxy를 생성하기 위해 인터페이스를 만들어야하는 수고를 덜 수 있다.

하지만, 상속을 이용하므로 final이나 private와 같이 상속에 대해 오버라이딩을 지원하지 않는 경우에는 Aspect를 적용할 수 없다는 단점이 있다.

CGLIB Proxy는 바이트 코드를 조작해서 프록시 객체를 생성하므로 JDK Dynamic Proxy보다 퍼포먼스가 빠른 장점이 있다.

  • 상속을 이용하기 때문에 클래스나 메소드에 final이 있으면 안된다.
  • 스프링 부트에서 AOP 사용을 위해 채택했다.
  • CGLIB은 고성능의 코드 생성 라이브러리로 인터페이스를 필요로 하는 JDK Dynamic Proxy 대신 사용될 수 있다. 바이트코드를 조작하는 프레임워크인 ASM을 사용하여 리플렉션보다 빠르다.

스프링 부트에서는 왜 cglib?

  • 스프링에선 CGLib은 3가지 한계가 존재했다.
    • 해당 라이브러리를 추가해야 한다.
    • CGLib을 구현하기 위해 반드시 파라미터가 없는 defalut 생성자가 필요하다.
    • 생성된 Proxy의 메소드를 호출하게 되면 타깃의 생성자가 2번 호출된다.
  • 하지만 문제되는 부분들을 개선하여 안정화 시켰다.
  • 스프링 부트에서는 AOP를 사용할 때 인터페이스로 선언되어 있어도 CGLib 방식으로 프록시 객체를 생성한다.

참고 출처 : https://gmoon92.github.io/spring/aop/2019/04/20/jdk-dynamic-proxy-and-cglib.html


AOP 주요 개념

  • Target : Aspect를 적용하는 곳 (클래스, 메서드 .. )
  • Aspect : 위에서 설명한 흩어진 관심사를 모듈화 한 것. 주로 부가기능을 모듈화함.
  • Advice : 실질적으로 어떤 일을 해야할 지에 대한 것, 실질적인 부가기능을 담은 구현체
  • JointPoint : Advice가 적용될 위치, 끼어들 수 있는 지점. 메서드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용가능
  • PointCut : JointPoint의 상세한 스펙을 정의한 것. 'A란 메서드의 진입 시점에 호출할 것'과 같이 더욱 구체적으로 Advice가 실행될 지점을 정할 수 있음

참고 출처

728x90

'spring' 카테고리의 다른 글

WebClient의 DataBufferLimitException 해결방법  (0) 2024.11.15
TestExecutionListener를 이용한 테스트 격리 방법  (1) 2024.11.05
Spring Async  (0) 2024.08.02
Spring Webclient  (0) 2024.08.02
Spring Boot Slice Test  (0) 2024.08.02
728x90

Spring Data JPA 사용하는 미션에서 멘토님께 리뷰를 받으며 다음과 같은 피드백을 받으며 슬라이스 테스트 존재에 대해 알게 되었다.

  • Repository Test시 @SpringBootTest를 @DataJpaTest로 변경해서 테스트 작성하기

슬라이스 테스트란 무엇이고 왜 사용하는 것일까??


Spring Boot 슬라이스 테스트


슬라이스 테스트란?

  • 즉 스프링은 레이어 별로 잘라서 특정 레이어에 대해서 Bean을 최소한으로 등록시켜 테스트 하고자 하는 부분에 최대한 단위 테스트를 지원해주고 있다.
  • 그렇다면 @SpringBootTest 대신 슬라이스 테스트를 하는 이유는 무엇일까??

F.I.R.S.T 테스트 원칙

단위 테스트는 응용 프로그램에서 테스트 가능한 가장 작은 소프트웨어를 실행하여 예상대로 동작하는지 확인하는 테스트이며 로버트 마틴의 클린코드에서 깨끗한 테스트를 위한 다섯 가지 F.I.R.S.T 규칙을 말한다.

  • Fast — 테스트는 빨라야 한다.
  • Isolated — 각 테스트는 서로 의존하면 안된다.
  • Repeatable — 테스트는 어떤 환경에서도 반복 가능해야 한다.
  • Self-validating — 테스트는 bool 값으로 결과를 내야 한다.
  • Timely — 테스트는 적시에 작성해야 한다.

@SpringBootTest 어노테이션을 사용하는 경우 단점은 아래와 같다.

  • 실제 구동되는 애플리케이션의 설정, 모든 Bean을 로드하기 때문에 시간이 오래걸리고 무겁다.
  • 테스트 단위가 크기 때문에 디버깅이 어려운 편이다.
  • 외부 API 콜같은 Rollback 처리가 안되는 테스트 진행을 하기 어려움

따라서 repository 레이어의 단위테스트의 경우 @SpringBootTest 대신 @DataJpaTest 사용하여 테스트를 작성하는 경우 통해 속도적인 측면과 의존성 측면에서 이점을 가질 수 있다.


슬라이스 테스트 어노테이션 종류

아래는 대표적인 슬라이스 테스트 어노테이션이 존재하는데 해당 글에서는 중 @WebMvcTest, @DataJpaTest 살펴보도록 할 것이다.

  • @WebMvcTest
  • @WebFluxTest
  • @DataJpaTest
  • @JsonTest
  • @RestClientTest

@WebMvcTest

  • MVC를 위한 테스트.
  • 웹에서 테스트하기 힘든 컨트롤러를 테스트하는 데 적합.
  • 웹상에서 요청과 응답에 대해 테스트할 수 있음.
  • 시큐리티, 필터까지 자동으로 테스트하며, 수동으로 추가/삭제 가능.
  • @SpringBootTest 어노테이션보다 가볍게 테스트할 수 있음.
  • 다음과 같은 내용만 스캔하도록 제한함.@Controller, @ControllerAdvice, @JsonComponent, Converter, GenericConverter, Filter, HandlerInterceptor,
    • 따라서 의존성이 끊기기 때문에, 예를 들면 서비스와 같은 객체들은 @MockBean을 사용해서 만들어 사용해야 한다.
@WebMvcTest(ShelterPostController.class)
public class ShelterPostControllerTest {

    @Autowired
    protected MockMvc mockMvc;

    @Autowired
    protected ObjectMapper objectMapper;

    @MockBean
    protected ShelterPostService shelterPostService;

    @Test
    @DisplayName("게시글 리스트 조회 테스트")
    void getShelterPostsTest() throws Exception {
        // given, when, then
                ...
    }
}

@MockBean

spring-boot-test 패키지는 Mockito를 포함하고 있기 때문에 기존에 사용하던 방식대로 Mock 객체를 생성해서 테스트하는 방법도 있지만, spring-boot-test에서는 새로운 방법도 제공한다.

  • 바로 @MockBean 어노테이션을 사용해서 이름 그대로 Mock 객체를 빈으로써 등록할 수 있다.
  • 기존에 사용되던 스프링 Bean이 아닌 Mock Bean을 주입한다.
  • 그렇기 때문에 만일 @MockBean으로 선언된 빈을 주입받는다면 Spring의 ApplicationContext는 Mock 객체를 주입한다.
  • 새롭게 @MockBean을 선언하면 Mock 객체를 빈으로써 등록하지만, 만일 @MockBean으로 선언한 객체와 같은 이름과 타입으로 이미 빈으로 등록되어있다면 해당 빈은 선언한 Mock 빈으로 대체된다.

해당 어노테이션은 테스트 내용 중 외부 서비스를 호출하는 부분을 Mock해서 쉽게 처리할 수 있다.

@SpringBootTest
public class XXXControllerTest {

    @MockBean  // 외부 서비스 호출에 사용되는 RestTemplate Bean을 Mock
    private RestTemplate mockRT;

    @MockBean  // 외부 서비스 호출에 사용되는 Service Bean을 Mock
    private XXXService xXXService;

}

@DataJpaTest

Spring Data JPA를 테스트하고자 한다면 @DataJpaTest 기능을 사용해볼 수 있다.

  • 해당 테스트는 기본적으로 in-memory embedded database를 생성하고 @Entity 클래스를 스캔한다.
  • 일반적인 다른 컴포넌트들은 스캔하지 않는다. 따라서 특정 bean의 의존성이 필요한 경우 아래의 방법 사용
    • @import
    • @DataJpaTest(includeFilters = @Filter(..))

@DataJpaTest@Transactional 어노테이션을 포함하고 있다.

  • 따라서 테스트가 완료되면 자동으로 롤백된다.

만약 @Transactional 기능이 필요하지 않다면 아래와 같이 줄 수 있다.

@DataJpaTest
@Transactional(propagation = Propagation.NOT_SUPPORTED)
class SomejpaTest {
    ...
}

@DataJpaTest 기능을 사용하면 @Entity를 스캔하고 repository를 설정하는 것 이외에도 테스트를 위한 TestEntityManager라는 빈이 생성된다.

  • 이 빈을 사용해서 테스트에 이용한 데이터를 정의할 수 있다.
@DataJpaTest
class SomejpaTest {

    @Autowired
    private TestEntityManager entityManager;

    @Test
    @DisplayName("게시글 아이디로 댓글 목록 삭제 테스트")
    void deleteAllByMissingPostIdTest() {
        // given
        LongStream.rangeClosed(1, 3).forEach(idx ->
            entityManager.persist(Comment.builder()
                .missingPost(missingPost)
                .content("내용")
                .account(account)
                .build()
            )
        );

        // when
        commentRepository.deleteAllByMissingPostId(missingPost.getId());
        List<Comment> comments = commentRepository.findAll();

        // then
        SoftAssertions.assertSoftly(softAssertions -> {
                softAssertions.assertThat(comments).hasSize(3);
                comments.forEach(foundComment -> softAssertions.assertThat(foundComment.isDeleted()).isTrue());
            }
        );
    }

}

만약 테스트에 내장된 임베디드 데이터베이스를 사용하지 않고 real database를 사용하고자 하는 경우, @AutoConfigureTestDatabase 어노테이션을 사용하면 손쉽게 설정할 수 있다.

@DataJpaTest
@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
class SomejpaTest {
    ...
}

사용시 주의할 점

슬라이스 테스트 시, 하위 레이어는 Mock 기반으로 만들기 때문에 주의할 점들이 있다.

  • 의존성 객체를 Mocking하기 때문에 완벽한 테스트는 아님
  • Mocking 처리하기 위한 시간이 소요
  • Mocking 라이브러리에 대한 학습 비용 발생
  • Mock 기반 으로 테스트하기 때문에 실제 환경에서는 결과가 다를 수 있음

참고 출처

728x90

'spring' 카테고리의 다른 글

WebClient의 DataBufferLimitException 해결방법  (0) 2024.11.15
TestExecutionListener를 이용한 테스트 격리 방법  (1) 2024.11.05
Spring Async  (0) 2024.08.02
Spring Webclient  (0) 2024.08.02
Spring AOP  (0) 2024.08.02

+ Recent posts