본문 바로가기
개발관련

Webflux Functional Endpoints 시작하기

by 부발자 2020. 9. 30.

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 이 아니라고 크게 쓰여있다.

This is Not an 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'
}

 

참고자료

반응형