티스토리 뷰

반응형

 

 


Spring MVC는 다들 알고 계실겁니다. Model-View-Controller의 기본 웹 코드 작성기법이며, 대부분 MVC 패턴이라고 알고 계신 분들도 많으실 거에요. MVC는 Servlet을 기반으로 작성된 하나의 템플릿이며, 작동 방식이 Blocking-Synchronous 방식이었습니다. 블로킹과 동기처리방식은 이전에 Java의 Files를 사용하며 잠시 체크하고 넘어갔었는데요, 해당 글이 필요하시다면 링크는 여기있습니다.

 

 

Servlet도 마찬가지로 서버에서 Response를 기다리는 동안 쓰레드 풀 내에 쓰레드들을 기다리는 블로킹 방식입니다. 그렇기 때문에 인터럽트가 발생하지 않는 이상 그 쓰레드들을 다른 곳에 사용할 수 없는 것이죠. 그리고 지연되는 시간도 문제인 것이 작지만 쌓이고 쌓이면 큰 시간을 손해보게 되죠.

 

 

"저는 이런 상황에서 택배 승하차가 생각나는데요, 택배를 가지러간 직원1이 택배를 가져오는 동안 승하차 담당 직원2가 아무것도 하지 않는 대기 상태라고 생각하시면 되고, 여기서 직원2는 쓰레드 풀의 하나의 쓰레드라고 생각하시면 이해하기 쉬우실 겁니다."

 

 

이런 방식을 보완하기 위해 통상 Spring-Web-Reactive로 일컬어왔었고 현재는 Reactive라고 말하는 방식이 도입되었습니다. 스프링 프레임워크 5버전부터 새로 도입되었고 여기서 이름이 WebFlux로 바뀌었습니다. 기존 테마 Java Development Kit 9 버전을 기초로 다루었습니다.

 

 


 

 

 

Reactive System

특징을 표로 정리해보았습니다.

기존의 MVC Reactive System
1 쓰레드 1 서비스 수행  다량의 서비스를 동시적으로 수행
작은 규모의 시스템에서 효과적 분산 시스템에서 효과적
장애 상황에서 동작하지 않음 장애 상황에서도 일관된 동작을 보장
Service-oriented Architecture(SOA)를 지향 MicroService Architecture(MSA)를 지향
HttpServletRequest, HttpServletResponse ServerRequest, ServerResponse

 

Usage

  • 비동기식-논블로킹 개발에 사용
  • 효율적으로 동작하는 고성능 Web App 개발
  • 서비스 간 핑퐁을 많이 치는 프로젝트에 유리

 

 

지원 가능한 Web Server / Container

  • Servlet 3.1+ (Tomcat, Jetty...)
  • Netty, Undertow (비동기-논블로킹 IO 웹서버)

 

 

Function Interface 

WebFlux에는 RouterFunction이라는 함수를 제공하고 있고 그 함수를 사용하여 통신을 진행합니다.

아래는 함수 내의 원형을 의미하고 모든 동작 원리는 이에 맞춰 진행합니다.

@FunctionalInterface
public interface RouterFunction<T extends ServerResponse> {

   // Mono 객체는 WebFlux에서 도입된 ServerResponse를 지원하는 단수 객체입니다.
   // Flux는 복수 객체를 의미합니다.
   // HandlerFunction은 웹 응답을 리턴하는 함수를 말합니다.
   // ServerRequest는 WebFlux에서 도입된 파라미터 객체입니다.
   Mono<HandlerFunction<T>> route(ServerRequest request);
}
// RouterFunction.route() 내부의 Lambda expression
RouterFunction router = req -> 
   RequestPredicates
      // 웹 요청 정보에서 URL 경로 패턴을 검사
      .path("{name}")
      // 조건에 맞는지 체크
      .test(req) ?
         // 조건에 맞으면 핸들러 함수를 Mono에 담아서 반환하고 그렇지 않으면 빈 객체를 반환합니다.
         Mono.just(handler) : Mono.empty();

 

 


HandlerFunction

Functional Programming(함수형 프로그래밍)의 Controller Method를 의미하며, 웹 요청(ServerRequest)를 받아 응답(ServerResponse)을 돌려주는 함수입니다. 

HanderFunction handler = req -> {
   String response = imageService::doSomething(req.pathVariable("name"));
   return ok().body(fromObject(response));
}

return route(path("{name}"), handler);

 

Main Function

기존의 @SpringBootApplication은 MVC 패턴을 지향하는 방식이고 여기에 추가되는 어노테이션인 @EnableWebFlux가 있습니다. 

그리고 내부의 핸들러에는 @Bean이 붙게 되어 Spring 객체간 와이어링을 진행합니다. 샘플을 보겠습니다.

 

어노테이션(@)의 개념은 이 글에 정리해두었습니다.

@Bean의 개념이 정확히 이해가 안가시면 이리로 가주세용 ->>> 링크

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

ImageHandler.java

@Component
public class ImageHandler {

   @Autowired ImageService imageService;
   
   public Mono<ServerResponse> handleRequest(ServerRequest request) {
      return ServerResponse
         // ServerResponse가 시작하고 확인되면
         .ok()
         // 그에 맞는 body가 채워지고 실행
         .body(Mono.just("Start Web Flux"), String.class);
   }
}

ImageService.java

@Service
public class ImageService {
   public static String UPLOAD_ROOT = "directory";
   
   private final ResourceLoader loader;
   public ImageService(ResourceLoader loader) {
      this.loader = loader;
   }
}

 


 

Domain Object

아래에서 처리해볼 DTO 샘플인 Image 파일입니다.

 

여기는 MongoDB를 기반으로 작성된 Document Class를 예시로 들었습니다.

 

Image.java

import org.springframework.data.annotation.Id;
import org.springframework.data.mongodb.core.mapping.Document;
import lombok.*;

@Data
@Document
@NoArgsConstructor
@AllArgsConstructor
public class Image {
   @Id
   private String id;
   private String name;
   
   public Image(String id) { this.id = id; }
}

 


웹 요청 처리 방식

RouterFunction이 어떤 핸들러가 이 요청을 처리할 지 결정이 되면 바인딩이 되어 있는 HandlerFunction이 작동합니다.

 

이 과정에서 에러가 발생하면 에러 발생 직전에 수행했던 함수까지만 작동되며 같은 함수가 작동되는데 영향을 미치지 않습니다. 에러가 발생하는 것에 대한 직접적인 처리도 개발자가 설정할 수 있습니다.

 

여기서 부터는 ImageService.java에 들어가는 소스 내용입니다.

 

예제로 업로드 되어있던 이미지를 가져오는 함수인 findImages를 살펴보겠습니다.

// 복수 개수이므로 Flux
public Flux<Image> findImages() {
   try {
      return Flux.fromIterable(
         Files.newDirectoryStream(Paths.get(UPLOAD_ROOT))
      ).map(path -> new Image(Integer.toString(path.hashCode()), path.getFileName().toString()));
   }
   catch(IOException e) {
      // Exception에 대한 처리를 개발자가 할 수 있음
      return Flux.empty();
   }
}

그리고 한개의 이미지를 가져오는 것은 아래와 같습니다.

// 단수이므로 Mono
public Mono<Resource> findOneImage(String fileName) {
   return Mono.fromSupplier(() -> 
      resourceLoader.getResource("file:" + UPLOAD_ROOT + "/" + fileName));
}

이미지를 생성하는 데에는 Response가 필요 없습니다. 이 때 사용하는 타입은 Mono<Void>가 되고 아래에 샘플코드가 있습니다. 

public Mono<Void> uploadImage(Flux<FilePart> files) {
   return files
      // flatMap lambda
      .flatMap(file -> file.transferTo(
         Paths.get(UPLOAD_ROOT, file.filename()
      ).toFile()) // end of flatMap
   ).then();
}

 


 

끝으로, WebFlux를 접한 분들은 다소 생소한 개념일 수도 있습니다. 이전에 MVC 패턴이 익숙한 분들은 더더욱 아닐 수도 있지요. 하지만 이러한 시도는 여러 번 경험해볼수록 나에게 유리하다고 생각합니다. 언제 대용량 트래픽에 대한 방어를 처리할 지, 언제 신기술이 나와 MVC 패턴이 점점 노후화될지는 그 누구도 알 수 없기 때문입니다. 


다들 자신과 맞는 개발 방법론을 찾아서 업무에 지장없이 좋은 성과물이 나왔으면 좋겠습니다!! :)

반응형

'Server' 카테고리의 다른 글

Kotlin은 Lombok을 사용할 수 없다  (0) 2020.07.20
[Java] 직렬화는 중요한 것일까  (0) 2020.07.17
[Spring] RestTemplate 알아보기  (0) 2020.07.14
Kotlin의 상속성과 다형성  (0) 2020.07.03
[Java] Files를 사용하여 입력하기  (0) 2020.07.02
Kotlin의 Pair와 Triple  (0) 2020.07.02
댓글
공지사항