반응형

Spring 어플리케이션에서 HTTP 요청을 할 땐 주로 RestTemplate 을 사용했었습니다. 하지만 Spring 5.0 버전부터는 RestTemplate 은 유지 모드로 변경되고 향후 deprecated 될 예정입니다.

RestTemplate 의 대안으로 Spring 에서는 WebClient 사용을 강력히 권고하고 있으며 다음과 같은 특징을 가지고 있습니다.

  • Non-blocking I/O
  • Reactive Streams back pressure
  • High concurrency with fewer hardware resources
  • Functional-style, fluent API that takes advantage of Java 8 lambdas
  • Synchronous and asynchronous interactions
  • Streaming up to or streaming down from a server

Reactive 환경과 MSA를 생각하고 있다면 WebClient 사용을 적극 권장해 드리며, 기본 설정부터 Method 별 사용법까지 차근차근 알아보도록 하겠습니다.

1. RestTemplate이란?

spring 3.0 부터 지원한다. 스프링에서 제공하는 http 통신에 유용하게 쓸 수 있는 템플릿이며, HTTP 서버와의 통신을 단순화하고 RESTful 원칙을 지킨다. jdbcTemplate 처럼 RestTemplate 도 기계적이고 반복적인 코드들을 깔끔하게 정리해준다. 요청보내고 요청받는데 몇줄 안될 정도..

특징

  • 기계적이고 반복적인 코드를 최대한 줄여줌
  • RESTful형식에 맞춤
  • json, xml 를 쉽게 응답받음

 

2. RestTemplate 의 동작원리

org.springframework.http.client 패키지에 있다. HttpClient는 HTTP를 사용하여 통신하는 범용 라이브러리이고, RestTemplate은 HttpClient 를 추상화(HttpEntity의 json, xml 등)해서 제공해준다. 따라서 내부 통신(HTTP 커넥션)에 있어서는 Apache HttpComponents 를 사용한다. 만약 RestTemplate 가 없었다면, 직접 json, xml 라이브러리를 사용해서 변환해야 했을 것이다.

  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. 어플리케이션에 반환된다.

예제

import org.apache.http.client.HttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

public class RestTemplateTest {
	public static void main(String[] args) {
		HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
		factory.setReadTimeout(5000); // 읽기시간초과, ms
		factory.setConnectTimeout(3000); // 연결시간초과, ms
		HttpClient httpClient = HttpClientBuilder.create().setMaxConnTotal(100) // connection pool 적용
				.setMaxConnPerRoute(5) // connection pool 적용
				.build();
		factory.setHttpClient(httpClient); // 동기실행에 사용될 HttpClient 세팅
		RestTemplate restTemplate = new RestTemplate(factory);
		String url = "http://testapi.com/search?text=1234";
		Object obj = restTemplate.getForObject("요청할 URI 주소", "응답내용과 자동으로 매핑시킬 java object");
		System.out.println(obj);
	}
}

 

 

1. WebClient이란?

WebClient는 Spring5 에 추가된 인터페이스다. spring5 이전에는 비동기 클라이언트로 AsyncRestTemplate를 사용을 했지만 spring5 부터는 Deprecated 되어 있다. 만약 spring5 이후 버전을 사용한다면 AsyncRestTemplate 보다는 WebClient 사용하는 것을 추천한다. 아직 spring 5.2(현재기준) 에서 AsyncRestTemplate 도 존재하긴 한다.

기본 문법

기본적으로 사용방법은 아주 간단하다. WebClient 인터페이스의 static 메서드인 create()를 사용해서 WebClient 를 생성하면 된다. 한번 살펴보자.

@Test
void test1() {

    WebClient webClient = WebClient.create("http://localhost:8080");
    Mono<String> hello = webClient.get()
            .uri("/sample?name={name}", "wonwoo")
            .retrieve()
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .expectNext("hello wonwoo!")
            .verifyComplete();
}

@Test
void test2() {

    WebClient webClient = WebClient.create();
    Mono<String> hello = webClient.get()
            .uri("http://localhost:8080/sample?name={name}", "wonwoo")
            .retrieve()
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .expectNext("hello wonwoo!")
            .verifyComplete();
}

@Test
void test1_3() {

    WebClient webClient = WebClient.create();
    Mono<String> hello = webClient.get()
            .uri("http://localhost:8080/sample?name=wonwoo")
            .retrieve()
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .expectNext("hello wonwoo!")
            .verifyComplete();
}


@Test
void test1_3() {

    WebClient webClient = WebClient.create("http://localhost:8080");
    Mono<String> hello = webClient.get()
            .uri("/sample?name={name}", Map.of("name", "wonwoo"))
            .retrieve()
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .expectNext("hello wonwoo!")
            .verifyComplete();
}

@Test
void test1_3() {

    WebClient webClient = WebClient.create("http://localhost:8080");
    Mono<String> hello = webClient.get()
            .uri("/sample?name={name}", "wonwoo")
            .retrieve()
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .expectNext("hello wonwoo!")
            .verifyComplete();
}

@Test
void test1_3() {

    WebClient webClient = WebClient.create("http://localhost:8080");
    Mono<String> hello = webClient.get()
            .uri(it -> it.path("/sample")
                    .queryParam("name", "wonwoo")
                    .build()
            ).retrieve()
            .bodyToMono(String.class);

    StepVerifier.create(hello)
            .expectNext("hello wonwoo!")
            .verifyComplete();
}

 

'JAVA' 카테고리의 다른 글

java 리스트간 비교, 값 체크  (0) 2021.02.24
Kafka  (0) 2021.02.24
비동기, 동기, 블로킹, 논블로킹  (0) 2021.02.19
JAVA11  (0) 2019.10.04
Oracle JDK 라이센스 전환  (0) 2019.10.04
반응형

프로모션 기간이나 이벤트 기간에 유저가 폭발적으로 늘어난다면? 어떻게 처리할것인가?

 

이런 질문을 받는다면 어떻게 접근할 것인가?

  • DB I/O 를 줄이기 위해 캐시?
  • JPA 쿼리 최적화??

이렇게만 접근했다면 Spring MVC + RDBMS 개발에만 너무 한정되어 있었다고 생각한다. ( 내 얘기이다... )

물론 해당 방법으로 접근해도 개선이 되는것은 맞다.

 

Blocking I/O

 

우리가 가장 일반적으로 프로그래밍하는 모델이다. Application에서 I/O 요청을 하고 끝날때까지 Block 되어 다른 작업을 수행할 수 없다.

 

하지만 Spring Web Application 개발을 하면 Tomcat이나 Netty가 Multi Thread 기반으로 동작하기 때문에 Block 안된듯이 동작한다. 이렇게 되면 여러 개의 I/O를 처리하려면 여러 개의 Thread를 사용해야 하는데 이는 매우 비효율적이다.

 

Blocking I/O를 좀더 개선하자! Synchronous Non-Blocking I/O

 

Application에서 I/O를 요청 후 바로 return되어 다른 작업을 수행하다가 주기적으로 데이터가 다 준비 되었는지 확인을 한다. 결국 데이터의 준비가 끝나면 종료된다. 이렇게 체크하는 방식을 폴링(polling) 이라 한다. 결국 이 또한 작업이 끝날때까지 주기적으로 호출하기 때문에 불필요하게 자원을 사용하게 된다.

 

Thread 좀더 소중하게 쓰고 싶어! - Asynchronous Non-blocking I/O

 

I/O 요청을 한후 즉시 리턴된다. polling 방식과 다르게 데이터 준비가 진짜 끝났을 때 callback 을 통해 알려준다. 이전 방식 보다 좀더 효율적이긴하다!

 

자바 코드로 살펴보면, 3초 걸리는 API를 3번 호출시 Blocking I/O는 9초, Non Blocking I/O는 3초 정도 걸린다. 훨씬 효율적이긴 하지만, 그만큼의 Thread 생성으로 인하여 Context Switching 오버헤드가 존재한다!!

@Test
public void nonBlocking3() throws InterruptedException {
    final StopWatch stopWatch = new StopWatch();
    stopWatch.start();
    for (int i = 0; i < LOOP_COUNT; i++) {
        this.webClient
                .get()
                .uri(THREE_SECOND_URL)
                .retrieve()
                .bodyToMono(String.class)
                .subscribe(it -> {
                    count.countDown();
                    System.out.println(it);
                });
    }

    count.await(10, TimeUnit.SECONDS);
    stopWatch.stop();
    System.out.println(stopWatch.getTotalTimeSeconds());
}

 

Context Switching 오버헤드도 해결하자! - Event Driven

Spring Framework 에서 Blocking I/O -> Non Blocking I/O 해결 과정

아래 그림에서 왼쪽은 non-blocking I/O를 이용해서 많은 양의 동시성 연결을 다룰 수 있는 Reactive Stack이고, 오른쪽은 blocking 방식의 Servlet Stack이다.

 

Spring MVC

아래 그림처럼 클라이언트로부터 요청이 들어오면 Queue를 통하게 된다. 스프링 어플리케이션은 요청당 Thread 한개가 할당된다. ( One Request Per Thread Model ) 즉, Thread Pool이 수용할 수 있는 요청까지만 동시적으로 작업이 처리되고 만약 넘게 된다면 큐에서 대기하게 된다.

 

Thread 생성 비용은 크기 때문에 미리 생성하여 재사용함으로써 효율적으로 사용한다. 서버 성능에 맞게 Thread 최대 수치를 제한 시키는데, tomcat 기본 사이즈는 200이다.

 

즉, thread pool size 200을 지속적으로 초과하게 된다면, Queue에서 계속 대기하게 된다. 전체 대기시간이 늘어나게 되는데 이런 현상을 Thread Pool Hell 이라 한다. 아래 그림은 링크드인의 Thread pool hell 현상에 대한 그래프이다. Thread pool 감당 사이즈를 넘는 요청이 들어오는 순간부터 수배나 많은 지연시간을 보여준다.

 

하나의 작업이 늦게 처리되는 부분은 DB, Network 등의 I/O가 일어나는 부분에서 많은 시간이 소비된다. I/O작업은 CPU가 관여하지 않는다. I/O를 가장 효율적으로 처리할 수 있는 방식이 Spring에서 제공해주는 WebFlux이다.

 

Spring WebFlux

 

 

위 그림은 WebFlux 구조에 대한 그림이다. 요청별로 Thread를 생성하는 것이 아니라, 다수의 요청을 적은 Thread로 처리를 한다. Worker Thread 기본 사이즈는 서버의 Core 개수로 설정이 되어있다. 즉 core 수가 4개라면 4개의 Thread로 대용량 트래픽을 감당한다는 것인가? ( Node.js에서 본 아키텍처이다... )

 

이렇게 Non Blocking 방식을 활용하면 효율적인 I/O 제어가 되어 성능이 향상될 수 있다. 그래서 MSA에서 네트워크 호출이 많기 때문에 적용하기 좋다. 하지만!! I/O 작업 중 하나라도 Blocking 방식이 있다면, 결국 Blocking이 발생되기 때문에 4개의 Thread 이후 요청은 결국 대기를 해야한다.

 

위와 관련된 예가 DB connection 일 것이다. 아무 생각없이 blocking 되는 DB connection을 넣어놓고 나머지는 non-blocking 방식으로 구현했다면 문제가 발생한다. MongoDB, Redis 등의 NoSQL은 non-blocking db connection을 지원한다고 한다!

 

와 그러면 무조건 WebFlux로 성능도 챙기고, 개발도 멋있게 하자?

 

당 그림을 보면 Spring MVC와 Spring WebFlux 성능이 비슷한 구간이 있다. 서버의 성능이 좋아진다면 비슷한 구간이 더 늘어날 것이다.

 

Spring Document에서 조차도 동기 방식이 코드 작성, 이해, 디버깅 하기가 좋다고 한다. ( 나만 그런게 아니였어... ) 결국 생산성 측면에서는 동기 방식 코딩이 훨씬 높기 때문에 서비스 규모나 서버의 성능에 따라 잘 따져보아야 한다.

 

왜 성능이 동일한 구간이 생길까?
위의 그림에서 Concurrent Users가 1000명 이하일때는 Thread Pool이 감당할 수 있는 정도의 요청이었기 때문이다. 이후에는 Queue에 쌓여 점점 성능이 느려진것이다.

WebFlux 간단한 예제 코드

@SpringBootApplication
@EnableWebFlux
public class ExampleApplication {
 
    @Bean
    HelloHandler helloHandler() {
        return new HelloHandler();
    }
 
    @Bean
    RouterFunction<ServerResponse> helloRouterFunction(HelloHandler helloHandler) {
        return RouterFunctions.route(RequestPredicates.path("/"), helloHandler::handleRequest);
    }
 
    public static void main(String[] args) throws Exception {
        SpringApplication.run(ExampleApplication.class);
    }
}

public class HelloHandler {
    public  Mono<ServerResponse> handleRequest(ServerRequest serverRequest) {
        return ServerResponse.ok().body(Mono.just("Hello World!"), String.class);
    }
}

 

참고

happyer16.tistory.com/entry/%EB%8C%80%EC%9A%A9%EB%9F%89-%ED%8A%B8%EB%9E%98%ED%94%BD%EC%9D%84-%EA%B0%90%EB%8B%B9%ED%95%98%EA%B8%B0-%EC%9C%84%ED%95%9C-Spring-WebFlux-%EB%8F%84%EC%9E%85

 

 

 

 

 

 

 

 

 

 

 

반응형

#!/bin/sh

SERVICE_HOME=/home1/irteam/service/tube-web
SERVICE_JAR=tube-web_real.jar
DATE2=`date +%Y%m%d`

cd $SERVICE_HOME/patch

if [ -f "$SERVICE_JAR" ];then
echo '[SERVICE] SERVICE Stop...'
cd $SERVICE_HOME/bin
./stop.sh

##프로세스 상태를 체크한다...
checkCount=1
while [ "$checkCount" != "0" ]
do
checkCount=$(ps -ef | grep $SERVICE_JAR | grep -v grep | wc -l)
echo "프로세스 종료 진행 중"
sleep 2;
done


echo '[SERVICE] '$SERVICE_JAR' backUp..'
cd $SERVICE_HOME/jar

SERVICE_JAR_BACK=$SERVICE_JAR'.'$DATE2
if [ -f "$SERVICE_JAR_BACK" ];then
COUNT=$(ls | grep $SERVICE_JAR_BACK | wc -l)
cp $SERVICE_JAR $SERVICE_JAR_BACK'_'$COUNT
echo '[SERVICE] backUp File : '$SERVICE_JAR_BACK'_'$COUNT
else
cp $SERVICE_JAR $SERVICE_JAR_BACK
echo '[SERVICE] backUp File : '$SERVICE_JAR_BACK
fi

echo '[SERVICE] '$SERVICE_JAR' patch!!'
mv $SERVICE_HOME/patch/$SERVICE_JAR $SERVICE_HOME/jar/$SERVICE_JAR
sleep 1;
echo '[SERVICE] SERVICE Start !!'
cd $SERVICE_HOME/bin
./start.sh
else
echo "patch File not exist"
echo $SERVICE_HOME"/patch 파일이 없습니다. 패치가 진행될 JAR파일이 필요합니다."

fi

 

해당 스크립트는

SERVICE_HOME의 patch 디렉토리에 해당 SERVICE_JAR를 넣으면 패치를 진행해줍니다.

해당 프로세스는

1) SERVICE_HOME의 서비스 종료

2) 해당 프로세스가 종료되었는지 체크(2초간격)

3) SERVICE_HOME의 SERVICE_JAR파일 백업
: 만약 tube-web_alpha-internal.jar.20200407 과 tube-web_alpha-internal.jar.20200407_1 이 있을 경우 tube-web_alpha-internal.jar.20200407_2 로 백업을 진행해줍니다.

4) 해당 Patch 디렉토리의 JAR파일 변경

5) 서비스 시작

 

해당 스크립트를 사용하기위해서는 

SERVICE_HOME, SERVICE_JAR의 값을 변경후 진행하시면됩니다.

patch 디렉토리 생성이 필요합니다.

 

 

'Server' 카테고리의 다른 글

mac 포트 확인, kill  (0) 2022.06.29
Mac 단축키 변경  (0) 2021.08.24
Apache AJP 통신 문제에 따른 조치방안  (0) 2020.11.11
cat , tail 명령어 정리  (0) 2019.09.16
리눅스 Tar 압축, 풀기  (0) 2016.08.09

+ Recent posts