Webflux 는 아래 처럼 두개의 프로그래밍 모델을 지원하는데, 이번글에서는 Functional 방식을 알아보도록 하자
- Functional routing and handling
- Annotation-based reactive components
1. RouterFunction
RouterFunction 은 기존 개발자에게 익숙한 @RequestMapping 어노테이션과 동일한 역활을 한다.
하지만 가장 큰 차이점은 RouterFunction 은 동작까지 정의한다라는 것이다.
@RequiredArgsConstructor
@Configuration
public class Router {
private final PersonHandler handler;
@Bean
public RouterFunction<?> personRouter() {
return route()
.GET("/person/{id}", accept(APPLICATION_JSON), handler::get)
.GET("/person", accept(APPLICATION_JSON), handler::list)
.POST("/person", handler::create)
.build();
}
}
2. HandlerFunction
HandlerFunction 은 RouterFunction에서 설정한 동작에 해당하는 것이다.
핸들러 함수는 아래와 같이 람다로 표현을 할수가 있다.
HandlerFunction<ServerResponse> helloWorld =
request -> ServerResponse.ok().bodyValue("Hello World");
하지만, 애플리케이션에서 여러 개의 함수를 사용한다면, 인라인 람다가 지저분할 수도 있다.
그래서 핸들러 클래스로 그룹화하여 핸들러 함수를 묶을 수 있다.
그러면 어노테이션 기반 애플리케이션에서의 @Controller와 비슷한 역할을 한다
@RequiredArgsConstructor
@Component
public class PersonHandler {
private final Validator validator; //LocalValidatorFactoryBean
private final PersonRepository personRepository;
public Mono<ServerResponse> list(ServerRequest request) {
return ok().contentType(APPLICATION_JSON)
.body(personRepository.findAll(), List.class);
}
public Mono<ServerResponse> get(ServerRequest request) {
long id = Long.parseLong(request.pathVariable("id"));
return personRepository.findById(id)
.flatMap(person -> ok().contentType(APPLICATION_JSON).bodyValue(person))
.switchIfEmpty(ServerResponse.notFound().build());
}
public Mono<ServerResponse> create(ServerRequest request) {
Mono<Person> person = request.bodyToMono(Person.class).doOnNext(this::validate);
return person.flatMap(personRepository::save)
.flatMap(entity -> ok().contentType(APPLICATION_JSON).bodyValue(person));
}
private void validate(Person person) {
Errors errors = new BeanPropertyBindingResult(person, Person.class.getName());
validator.validate(person, errors);
if (errors.hasErrors()) {
throw new ServerWebInputException(errors.toString());
}
}
}
Spring 문서에 보면 Validator 는 직접 구현하기도 하지만,
Bean Validation API 의 어노케이션 기반을 사용하기 위해서 위의 예제처럼 스프링부트에서 기본적으로 생성되는 LocalValidatorFactoryBean 를 주입받아야 한다.
이 부분은 어노케이션 방식에서 @Valid 를 사용하는 개발자에게는 다소 불편해 보일수도 있다. (살짝 아쉬운 감이 있긴 있다..)
3. Entity
@Getter
@AllArgsConstructor
@ToString
public class Person {
@Id
private Long id;
@NotNull(message = "name is required property")
private String name;
}
4. Repository
repository 는 R2DBC를 사용하기 때문에 ReactiveCrudRepository 상속 받아서 간단하게 만들었다.
public interface PersonRepository extends ReactiveCrudRepository<Person, Long> {}
단, R2DBC 는 ORM 이 아니기 때문에, 스키마를 자동으로 생성해주는 기능이 없으므로, 직접 스키마를 만들어야 한다.
Spring Data R2DBC 깃헙에 보면 ORM 이 아니라고 크게 쓰여있다.
4. liquibase
Auto dll 기능 없으므로 liquibase 로 DB 스키마를 관리하도록 하였다.
resources/db/changelog-master.xml 파일에 스키마를 정의 하고, application.yml 파일에 파일의 위치를 설정해 주면 된다.
<?xml version="1.0" encoding="UTF-8"?>
<databaseChangeLog
xmlns="http://www.liquibase.org/xml/ns/dbchangelog"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:ext="http://www.liquibase.org/xml/ns/dbchangelog-ext"
xsi:schemaLocation="http://www.liquibase.org/xml/ns/dbchangelog http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-3.8.xsd
http://www.liquibase.org/xml/ns/dbchangelog-ext http://www.liquibase.org/xml/ns/dbchangelog/dbchangelog-ext.xsd">
<changeSet id="1" author="nvoxland" context="dev">
<createTable tableName="person">
<column name="id" type="int" autoIncrement="true">
<constraints primaryKey="true" nullable="false"/>
</column>
<column name="name" type="varchar(50)"/>
</createTable>
</changeSet>
</databaseChangeLog>
liquibase 을 사용하게 되면 DATABASECHANGELOG, DATABASECHANGELOGLOCK 테이블을 자동으로 생성하고 알아서 관리를 해주는데, 어플리케이션이 시작될때 DATABASECHANGELOGLOCK 테이블을 이용해서 Lock 을 잡고 DATABASECHANGELOGLOCK 테이블을 이용해서 이력을 관리하고 changelog-master.xml 파일에 정의된 스키마를 생성해준다.
5. application.yml
spring:
application:
name: webflux-example
codec:
log-request-details: true
jackson:
serialization:
write_dates_as_timestamps: true
r2dbc:
url : r2dbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8
password: test
username: test
liquibase:
contexts: dev
change-log: classpath:/db/changelog-master.xml
url: jdbc:mysql://localhost:3306/test?useUnicode=true&characterEncoding=utf8
user: test
password: test
datasource:
driver-class-name: com.mysql.jdbc.Driver
server:
error:
include-exception: false
include-stacktrace: never
logging:
level:
ROOT: INFO
web: DEBUG
org.springframework.data.r2dbc: DEBUG
6. dependencies
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.liquibase:liquibase-core'
implementation 'org.springframework:spring-jdbc'
runtimeOnly 'mysql:mysql-connector-java'
runtimeOnly 'io.r2dbc:r2dbc-pool'
runtimeOnly 'dev.miku:r2dbc-mysql'
}
참고자료
'개발관련' 카테고리의 다른 글
Reactive Streams 이해하고 구현해보기 (0) | 2020.11.25 |
---|---|
commit 메시지에 자동으로 branch명 추가해보기 (0) | 2020.11.04 |
JPA Fetch Join과 페이징 문제 (0) | 2020.09.22 |
JPA - ManyToMany 관계시 Set과 List의 차이 (0) | 2020.08.11 |
SpringBoot - http request와 response 로깅하기 (0) | 2020.08.05 |