title : WebFlux가이드 version: 2.3.0
version 2.3.0
작성자 | DT혁신연구팀 |
작성일자 | 2022.04.06 |
![롯데정보통신_국문(기업명).png](media/lotte-company.png)
제·개정이력
버전 | 제·개정 페이지 및 내용 | 제·개정 일자 |
---|---|---|
v2.3.0 | 기존 문서에서 분리 | 2022-04-06 |
목 차 (Table of Content)
[[TOC]]
Chamomile WebFlux
Spring WebFlux기반의 비동기 application을 개발할 수 있도록 Chamomile Framework를 적용한 비동기 프레임워크
Spring WebFlux란?
- Spring Framework 5에서 추가된 기능
- Server, client side 에서의 reactive한 application 개발을 도와준다.
- async / non-blocking 개발을 할 수 있도록 한다.
- 적은 하드웨어로 효율적으로 동작하는 고성능 web application을 개발할 수 있도록 하며 서비스 간 호출이 많은 MSA환경에 적합한 Framework이다.
등장 배경
적은 수의 스레드로 동시성을 처리하고 더적은 하드웨어 리소스로 확장하기 위해서는 non-blocking Web stack이 필요했다. Servlet 3.1은 non-blocking I/O를 제공했지만 이를 사용하면 프레임워크가 동기식, blocking으로 구성되어 non-blocking 서블릿 API에서 멀어 지게 된다(여기에는 Filter, Servlet, getParameter, getPart등이 있다). 이 때문에 등장한것이 WebFlux이다.
또한, java5에서 annotation을 추가하여 기능을 작성 했듯이 java8에서 람다 표현식을 추가하면서 더 많은 기능을 구현할 수 있게 되었다. CompletableFutre 및 ReactiveX(Rx…) 등 에서 널리 사용되는 non-blocking 개발 스타일에 도움이 되었고 이를 통해 Spring WebFlux에서는 많은 기능적인 웹 엔드 포인트를 제공 할수 있었다.
개발환경
- Java : 1.8이상
- Container : Servlet 3.1이상, Netty 기반의 비동기 프레임워크
Spring WebFlux의 Data processing
![image-20220406141556291](media/image-20220406141556291.png)
WebFlux개발 방식
- Spring MVC에서 사용하던 @Controller, @RestController, @RequestMapping등이 모두 사용가능 하다. 이와 더불어 RouterFunction을 이용한 경량 application형태로도 만들어 낼 수 있다.
- 다만 Annotation 기반의 캐시사용(@Cacheable 등), ThreadLocal기반의 데이터 처리(MDC, RequestContextHolder, SecurityContextHolder등), jdbc기반의 DB IO등은 사용하지 못하거나 비효율적인 동작방식에 의해 다른 방식으로 구현 되어야 한다.
- 이에 따라 Chamomile에서는 Mono/Flux를 Caching할 수 있는 기능, ThreadLocal을 사용할 수 있는 Lifter, R2DBC가이드 등을 제공한다.
Quick Start
프로젝트 생성
File -> New -> Project -> Chamomile Framework -> Create Chamomile Project -> chamomile WebFlux프로젝트 선택 후 완료
프로젝트 파일 구조
![](media/5f60207cfbdc5702aed4127760146543.png)
프로젝트 생성시 위 그림과 같이 기본적으로 구동시킬수 있는 형태의 프로젝트가 생성된다.
- SampleApplication.java
- application을 구동시키는 클래스이다. 해당 파일을 열고 단축키 [ctrl + F11] 으로 application을 구동시킬 수 있다.
- application.properties
- application 구동에 필요한 property들이 설정 되어 있다.
- pom.xml
- Chamomile WebFlux 를 이용하기 위한 starter가 정의 되어있다.
주요 파일 내용
SampleApplication.java
package net.lotte.sample;
import org.springframework.boot.SpringApplication;
import net.lotte.chamomile.autoconfiguration.ChamomileBootApplication;
//chamomile boot application을 구동하기위한 Annotation이다.
@ChamomileBootApplication
public class SampleApplication {
//application을 구동하는 main method이다.
public static void main(String[] args) {
SpringApplication.run(SampleApplication.class, args);
}
}
- component scan을 위한 base package명은 별도로 적어 주지 않아도 되며 해당 파일 하위로 위치시켜주기면 하면 자동으로 scan이 된다.
pom.xml
<dependency>
<groupId>net.lotte.chamomile</groupId>
<artifactId>chamomile-core-starter</artifactId>
</dependency>
<dependency>
<groupId>net.lotte.chamomile</groupId>
<artifactId>chamomile-webflux-starter</artifactId>
</dependency>
<!-- R2DBC mysql connector. 사용 할 DB에 따라 다름 -->
<dependency>
<groupId>dev.miku</groupId>
<artifactId>r2dbc-mysql</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
- 위 설정은 mysql을 사용하는 예제 이다.
- 일반 Chamomile Boot와는 다르게 core starter와 webflux starter로 구성되어있다.
- WebFlux프로젝트의 경우 Servlet을 사용하지 않기 때문에 Servlet종속성을 모두 제거한 starter가 설정 된다.
CRUD코드 생성
프로젝트에서 마우스 오른쪽 버튼 클릭 -> Chamomile -> generate template codes 선택 -> Chamomile WebFlux code 선택 -> 생성 완료
생성된 코드
대부분 기존에 사용하던 코드들을 재활용 할 수 있다. Controller의 소스는 @Controller나 @RequestMapping으로 이루어 져 있으며 Service도 동일하다. 다만 DAO소스는 myBatis나 JPA등을 사용하지 못하며 R2DBC를 사용해야 한다. 이는 기존 JDBC기반의 DB IO를 처리하게되면 비동기 처리시 blocking이 발생하여 비효율적으로 동작하기 때문이다.
R2DBC를 사용할 경우 JPA를 사용 할 때와 같이 interface를 만들어 놓으면 쿼리로 자동으로 변환되는 기능을 제공하나 아직 까지는 불완전한 상태이므로 databaseClient를 사용하여 쿼리를 직접 작성하는것으로 가이드 한다.
아래는 예시 이다.
@Repository
public class SampleDao {
@Autowired
DataSource dataSource;
@Autowired
private DatabaseClient databaseClient;
//Flux형태로 데이터 return
public Flux<SampleVO> getAllFlux(){
return databaseClient
//사용자를 삭제한다.
.execute("delete from chmm_user_info where user_id=:userId")
//userId에 데이터를 바인딩 한다.
.bind("userId", "testadmin")
//쿼리를 실행한다.
.fetch()
//만약 row에 변화가 생겼다면
.rowsUpdated()
.flatMapMany(m -> //데이터를 조회한다.
databaseClient
.execute("select * from chmm_user_info")
//조회된 데이터는 SampleVO에 mapping한다.
.as(SampleVO.class)
.fetch()
.all()
);
}
//Mono형태로 데이터 return
public Mono<List<SampleVO>> getAllMono(){
return databaseClient
//사용자를 삭제한다.
.execute("delete from chmm_user_info where user_id=:userId")
//userId에 데이터를 바인딩 한다.
.bind("userId", "testadmin")
//쿼리를 실행한다.
.fetch()
//만약 row에 변화가 생겼다면
.rowsUpdated()
.flatMap(m -> //데이터를 조회한다
databaseClient
.execute("select * from chmm_user_info")
//조회된 데이터는 SampleVO에 mapping한다.
.as(SampleVO.class)
.fetch()
.all()
//Mono로 만들어 주기 위해 조회된 데이터를 List형태로 변환한다.
.collectList()
);
}
…
사용가능 한 DB
현재 버전에서 사용가능한 드라이버 목록은 아래와 같다.
- Google Cloud Spanner
- Jasync-sql MySQL
- R2DBC H2
- R2DBC MariaDB
- R2DBC MySQL
- R2DBC PostgreSQL
- R2DBC Proxy
- R2DBC SQL Server
WebFlux 프로젝트 설정
프로젝트 구조
Web개발가이드 / Web 프로젝트 설정 / 프로젝트 구조를 참조한다.
웹 프로젝트의 전반적인 디렉토리 구조 및 파일의 용도는 아래와 같다. (이탤릭체 항목은 캐모마일 부트에는 해당되지 않음)
![img](media/844da71ac9b9680c7c54cf4adce05d74.png)
디렉토리 및 파일 | 설명 | ||
---|---|---|---|
mask | finders_default.xml | 로그마스킹 처리시 참조하는 정규표현식 패턴에 대해서 정의해 놓은 파일 | |
log_masking.properties | 로그마스킹 정규표현식 패턴 매칭한 문자열에 대해서 치환할 대체문자열 형태를 정의해놓은 프로퍼티 파일 | ||
maskers_default.xml | 로그마스킹 관련 실행 클래스 정의 및 로그마스킹 패턴 정보에 대한 타켓 (XML, DB), 프로퍼티 파일에 대한 정의된 XML 파일 (최초 로그마스킹 API 실행시 로드되는 XML 파일) | ||
unregex.properties | 특정 문자열에 대해 마스킹 처리할 경우 문자열을 등록할 수 있는 프로퍼티 파일 | ||
application.properties | – 스프링부트 설정(부트 한정) – 메서드 트레이스 AOP on/off 설정 – 파일업로드/다운로드 관련 설정(파일 저장 경로 등) – DB 연동 정보 설정 – property util 설정 (테이블과 컬럼 정보입력) – 알림톡 및 텔레그램 발송을 위한 연동 정보 설정 – Redis접속 정보 설정 – RequestMapping 정보와 DB 동기화 옵션 설정 – 모니터링 (scouter) 설정 – 멀티인스턴스 설정 – SMTP 서버 관련 설정 | ||
config | message | 다국어 처리시 사용될 properties파일이 있는 디렉토리 | |
spring(스프링 한정) | context-aoplog.xml | 메서드 트레이스(이력로그) 관련 bean 설정파일 |
![img](media/afec32f23c2d06f801affc7a17476c15.png)
디렉토리 및 파일 | 설명 | ||
---|---|---|---|
resources | 정적 리소스 디렉토리(js, html, 이미지 등) |
환경 설정 (어플리케이션 설정)
데이터베이스 설정
[Spring WebFlux]
- spring.r2dbc.url
- Spring WebFlux 에서 R2DBC에 연결하기 위한 전체 연결 문자
- spring.r2dbc.username
- 접속 계정의 아이디
- spring.r2dbc.password
- 접속 계정의 비밀번호
- spring.r2dbc.protocol
- 접속 할 DB유형
- spring.r2dbc.host
- 접속 할 DB의 IP 및 호스트명
- spring.r2dbc.port
- 접속 할 DB의 port번호
- spring.r2dbc.database
- 접속 할 DB명
그 외 설정은 Web프로젝트와 같다.
실행 기능
Security
WebFlux프로젝트에서도 동일하게 사용자 인증/인가를 처리할 수 있도록 Security 기능을 제공한다.
Authentication / Authorization
- 사용자 인증은 ReactiveDatabaseDetailsService에서 처리한다. 사용자를 조회 하고 해당 사용자의 비밀번호와 입력된 비밀번호를 비교한다.
entry-point
- 인증되지 않은 사용자가 접근했을 경우 application은 로그인 후 이용할 수 있도록 페이지를 강제로 이동시켜 주거나 적당한 raw데이터를 client로 보내준다. 해당 기능을 하는 spring security의 기능이 entry-point이며 webflux에서 사용하기 용이 하도록 기능을 제공하며 ServiceAuthenticationEntryPoint로 설정되어있다.
- 해당 bean의 property는 아래와 같다.
property 명 | setter methods | 기본값 | 설명 |
---|---|---|---|
contentType | setContentType(String) | application/json | client로 보내질 content type을 지정한다. 해당값은 header에 add된다. |
body | setBody(String) | success | client로 보내질 내용을 작성한다. |
statusCode | setStatusCode(HttpStatus) | HttpStatus.OK | client로 내용을 보낼 때 http상태코드를 지정한다. |
location | setLocation(URI) setLocation(String) | null | 인증을 할 수 있는 페이지로 redirect가 필요한 경우에 작성한다. 해당 값이 작성되면 나머지 설정 값 들은 전부 무시된다. |
- 기본 설정은 redirect되도록 설정 되어있으므로 다르게 설정하고자 할 경우 @Configuration 클래스에서 ServiceAuthenticationEntryPoint를 재정의 한다.
@Bean
public ServerAuthenticationEntryPoint serviceAuthenticationEntryPoint() {
ServiceAuthenticationEntryPoint serviceAuthenticationEntryPoint =
new ServiceAuthenticationEntryPoint();
serviceAuthenticationEntryPoint.setLocation("/login");
return serviceAuthenticationEntryPoint;
}
Authentication Success/Failure Handler
- 인증 성공/실패에 대한 처리를 하는 handler가 적용되어있다. 해당 내용은 securityWebFilterChain 을 참고한다.
SecurityContextRepository
- 인증에 성공하고 SecurityContext를 저장하는 bean인 ServiceWebSessionServerSecurityContextRepository 가 등록되어있다. 이 bean은 로그인 후에도 세션ID를 계속 유지시킨다. Spring WebFlux Security 의 기본설정은 로그인 성공시 세션 ID를 변경시키는것으로 되어있으며 해당 클래스는 WebSessionServerSecurityContextRepository 에 구현되어 있으므로 기본설정을 사용하고자 할경우 해당 bean을 등록해서 사용한다.
SecurityWebFilterchain
- 캐모마일의 SecurityFilterChain설정은 아래와 같다. 변경하고자 할 경우 참고한다.
@Bean
public SecurityWebFilterChain securityWebFilterChain(ServerHttpSecurity http) {
http
.authorizeExchange()
.pathMatchers("/**")
.access(reactiveAuthorizationManager)
.anyExchange()
.authenticated()
.and()
.httpBasic()
.and()
.formLogin()
.authenticationSuccessHandler(serviceAuthenticationSuccessHandler)
.authenticationFailureHandler(serviceAuthenticationFailureHandler)
/*
* entry point를 명시적으로 지정할 경우 loginPage 메서드가 수행되지 않아
* requiresAuthenticationMatcher, authenticationFailureHandler가 자동으로 설정 되지 않는다.
* 따라서 별도로 지정해 주어야 한다.
* 또한 loginPage / logoutPage가 자동으로 generate되지 않으므로 해당 페이지를 사용하고자 할 경우에는 entrypoint는 제거 하도록 한다.
* entry point로 별도 페이지를 띄울경우 해당 페이지 주소를 permitAll로 처리해준다.
*/
//.authenticationEntryPoint(serviceAuthenticationEntryPoint)
//.requiresAuthenticationMatcher(requiresAuthenticationMatcher)
//.loginPage("/login"); //로그인을 처리할 페이지. 페이지 지정시 로그인과 로그아웃페이지는 자체적으로 구현해야 한다.
// 미지정시 자동으로 generate된다. handler, entry-point 등을 커스터마이징 할 경우 여기에 설정하는 값을 사용하지 않으니 별도로 설정해야 한다.
.and()
.securityContextRepository(serviceWebSessionServerSecurityContextRepository)
;
return http.build();
}
공통 기능
로그인
샘플 로그인 서비스 만들기
AuthenticationSuccessHandler 구현
[Chamomile WebFlux]
인증 성공시 chamomile-webflux에 정의되어있는 ServiceAuthenticationSuccessHandler를 거치게 된다.
해당 bean의 property는 아래와 같다.
property 명 | setter methods | 기본값 | 설명 |
---|---|---|---|
contentType | setContentType(String) | application/json | client로 보내질 content type을 지정한다. 해당값은 header에 add된다. |
body | setBody(String) | success | client로 보내질 내용을 작성한다. |
statusCode | setStatusCode(HttpStatus) | HttpStatus.OK | client로 내용을 보낼 때 http상태코드를 지정한다. |
location | setLocation(URI) setLocation(String) | null | 인증 성공후 redirect가 필요한 경우에 작성한다. 해당 값이 작성되면 나머지 설정 값 들은 전부 무시된다 |
기본 설정은 redirect되도록 설정 되어있으므로 다르게 설정하고자 할 경우 @Configuration 클래스에서 ServiceAuthenticationSuccessHandler를 재정의 한다.
@Bean
public ServerAuthenticationSuccessHandler serviceAuthenticationSuccessHandler() {
ServiceAuthenticationSuccessHandler authenticationSuccessHandler =
new ServiceAuthenticationSuccessHandler();
authenticationSuccessHandler.setLocation("/login");
return authenticationSuccessHandler;
}
AuthenticationFailureHandler 구현
[Chamomile WebFlux]
인증 실패시 chamomile-webflux에 정의되어있는 ServiceAuthenticationFailureHandler를 거치게 된다.
해당 bean의 property는 아래와 같다.
property 명 | setter methods | 기본값 | 설명 |
---|---|---|---|
contentType | setContentType(String) | application/json | client로 보내질 content type을 지정한다. 해당값은 header에 add된다. |
body | setBody(String) | success | client로 보내질 내용을 작성한다. |
statusCode | setStatusCode(HttpStatus) | HttpStatus.OK | client로 내용을 보낼 때 http상태코드를 지정한다. |
location | setLocation(URI) setLocation(String) | null | 인증 실패 후 redirect가 필요한 경우에 작성한다. 해당 값이 작성되면 나머지 설정 값 들은 전부 무시된다. |
기본 설정은 redirect되도록 설정 되어있으므로 다르게 설정하고자 할 경우 @Configuration 클래스에서 ServiceAuthenticationFailureHandler를 재정의 한다.
@Bean
public ServerAuthenticationFailureHandler serviceAuthenticationFailureHandler() {
ServiceAuthenticationFailureHandler authenticationFailureHandler =
new ServiceAuthenticationFailureHandler();
authenticationFailureHandler.setLocation("/login");
return authenticationFailureHandler;
}
인증 실패 카운팅의 경우 해당 클래스를 extends받아 재 구현 한다.
public class CustomAuthenticationFailureHandler
extends ServiceAuthenticationFailureHandler {
@Override
public Mono<Void> onAuthenticationFailure(
WebFilterExchange webFilterExchange, AuthenticationException exception) {
//인증 실패시 처리할 로직 작성
return super.onAuthenticationFailure(webFilterExchange, exception);
}
}
Cache
[Chamomile WebFlux]
Spring WebFlux에서 Mono나 Flux는 annotation 기반으로 Cache를 사용할 수 없다. 이는 해당 객체 자체가 값을 가지고 있는 것이 아닌 데이터를 처리하는 방법이 기술되어 있는 형태이고 실제 동작도 subscribe 가 수행되는 시점에 동작하기 때문에 Caching 할 수 없다.
이에 따라 Spring에서는 Mono나 Flux를 캐싱할 수 있는 방법으로 CacheMono 와 CacheFlux를 제공하고 있으며 캐모마일에서는 해당 기능을 쉽게 사용할 수 있도록 util클래스를 제공한다.
메서드명 | parameter | return | 설명 |
---|---|---|---|
evict | String cacheName, String key | cacheName, key와 매칭되는 캐시를 삭제한다. | |
evict | String cacheName | cacheName에 해당되는 캐시 전체를 삭제한다. | |
lookUp | String cacheName, String key, Flux<T> ifMissResume | Flux<T> | cacheName, key에 해당하는 값을 cache에서 찾고 없다면 ifMissResume에서 얻어지는 값을 caching한 후 return 한다. |
lookUp | String cacheName, String key, Mono<T> ifMissResume | Mono<T> | cacheName, key에 해당하는 값을 cache에서 찾고 없다면 ifMissResume에서 얻어지는 값을 caching한 후 return 한다. |
사용 예제
- Mono 캐싱
private static final String CACHENAME = "test";
private static final String KEY = "test-key";
…
return ReactiveCache.lookup(CACHENAME, KEY, Mono.just("test")
- Flux캐싱
private static final String CACHENAME = "test";
private static final String KEY = "test-key";
…
return ReactiveCache.lookup(CACHENAME, KEY, Flux.just("test2", "test1")
- 캐시 삭제
private static final String CACHENAME = "test";
private static final String KEY = "test-key";
…
ReactiveCache.evict(CACHENAME, KEY);
ThreadLocal사용
client의 요청부터 응답까지 한 개의 Thread에서 담당하는 Servlet기반 application과는 달리 WebFlux의 경우 요청과 처리와 응답이 모두 다른 Thread에서 동작할 수도 있다. 그렇기 때문에 ThreadLocal기반인 MDC, RequestContextHolder, SecurityContextHolder등은 사용하지 못한다.
하지만 Chamomile에서 Context들을 Lifting하여 지속 적으로 해당 값들을 처리하는 Thread에서 용할 수 있도록 하였다.
Lifter구현
Context에서 유지시켜주는 행위를 할 Lifter를 생성한다.
net.lotte.chamomile.core.reactive.context.lifter.Lifter에 인터페이스가 정의되어있으며 해당 인터페이스를 구현한다.
method 명 | 설명 |
---|---|
public String getLifterName(); | 구현할 Lifter의 고유 이름을 지정하여 return 한다. |
public void writeThreadLocal(Map<String, Object> contextMap); | Context에서 꺼낸 후 ThradLocal에 작성하기위해 구현한다. 기본적으로 contextMap은 아래 처럼 사용된다. Object webExchange = contextMap.get(getLifterName()); |
public void clear(); | 다음 Thread동작을 위해 처리가 끝났을때 ThreadLocal데이터를 비워주어야 한다. 사용된 ThreadLocal을 비워주기 위한 구현체이다. |
public Object preRequest(ServerWebExchange serverWebExchange); | request시작시 context에 저장할 데이터를 return 한다. |
아래는 RequestContextHolder를 사용하기위한 Lifter구현 예시 이다.
public class RequestContextHolderLifter implements Lifter {
public static final String REQUEST_CONTEXT_KEY = "_requestContextHolder";
private static final String NAME = "RequestContextLifter";
@Override
public void writeThreadLocal(Map<String, Object> contextMap) {
if (contextMap != null && contextMap.get(NAME) != null) {
ReactiveRequestAttributes reactiveReqeustAttributes = new ReactiveRequestAttributes(
(ServerWebExchange)contextMap.get(NAME));
RequestContextHolder.setRequestAttributes(reactiveReqeustAttributes, true);
}
}
@Override
public void clear() {
//request를 비워준다.
RequestContextHolder.resetRequestAttributes();
}
@Override
public String getLifterName() {
return NAME;
}
@Override
public Object preRequest(ServerWebExchange serverWebExchange) {
return serverWebExchange;
}
}
ReactiveContext에 등록
만들어진 Lifter는 ReactiveContext에 등록해서 등록해 주어야 한다. 기본적으로 설정 된 bean은 아래와 같다.
@Bean
public ReactiveContext reactiveContext() {
ReactiveContext context = new ReactiveContext();
context.addContext(new MDCLifter());
context.addContext(new RequestContextHolderLifter());
context.addContext(new SecurityContextHolderLifter());
return context;
}
application 생성시에 @Configuration을 통해 addContext메서드를 이용하여추가한다.
@Autowired
ReactiveContext reactiveContext;
…
reactiveContext.addContext(new CustomLifter());
미리 만들어진 요소 사용
- MDC : chamomile의 메서드 추적을 위해 설정되어있으므로 별도로 추가하지 않아도 된다.
- RequestContextHolder : WebFlux에서는 Request 정보는 ServerWebExchange에서 찾아 볼 수 있다.
ReactiveRequestAttributes requestAttributes = (ReactiveRequestAttributes)RequestContextHolder.getRequestAttributes();
ServerWebExchange serverWebExchange = requestAttributes.getServerWebExchange();
- SecurityContextHolder : 기존에 사용하던 방식으로 사용한다.