본문 바로가기

Spring

Spring Webflux (1) - 간단한 시작

728x90

논블로킹으로 동작하는 Webflux로 간단한 API 앱을 구현하고싶어서 시작했다. webflux 기초 지식이 많이 부족한 내가 쌓아가는 과정을 기록하려고 한다.

IDE : eclipse

JDK : 1.8

 

 

프로젝트 생성

Spring boot에 Gradle로 설정한 후 Next

간단하게 Reactive Web만 체크하고 생성

자바 라이브러리가 안되어있다면 Build Path에서 add library 해주자

 

gradle

plugins {
	id 'org.springframework.boot' version '2.6.4'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
	id 'war'
}

group = 'com.zero'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '1.8'

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-webflux'
	implementation 'org.springframework.session:spring-session-core'
	providedRuntime 'org.springframework.boot:spring-boot-starter-netty'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
	testImplementation 'io.projectreactor:reactor-test'
	
	//lombok
	implementation 'org.projectlombok:lombok'
	
	//r2dbc
	implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
	runtimeOnly 'mysql:mysql-connector-java'
	runtimeOnly 'dev.miku:r2dbc-mysql'
}

tasks.named('test') {
	useJUnitPlatform()
}

기존에 있던 org.springframework.boot:spring-boot-starter-web과 org.springframework.boot:spring-boot-starter-tomcat 라이브러리를 지웠다.

그리고 비동기로 돌아가는 서버를 내장으로 사용하기위해 추가한 org.springframework.boot:spring-boot-starter-netty

와 lombok, mysql로 r2dbc를 사용하기위해 추가한걸 볼 수 있다.

 

 

R2DBC 설정

Webflux처럼 반응형의 논블로킹으로 돌아가는 앱이라면 DB의 Datasource도 논블로킹으로 작동되어야 한다.

하지만 기존의 JDBC는 블로킹 방식으로 동작하기 때문에 논블로킹 Datasource를 지원하는 R2DBC (Reactive Relational Database Connectivity)를 사용해야한다. R2DBC도 repository를 지원하기 때문에 코드적으로 봤을때 JPA와 좀 비슷하다.

 

참고 : https://spring.io/projects/spring-data-r2dbc

 

Spring Data R2DBC

Spring Data R2DBC, part of the larger Spring Data family, makes it easy to implement R2DBC based repositories. R2DBC stands for Reactive Relational Database Connectivity, a specification to integrate SQL databases using reactive drivers. Spring Data R2DBC

spring.io

 

단일 데이터베이스만 사용한다면 application.properties에서 간단하게 DB Connect 설정이 가능하다.

server.port=80

# mysql db connect
#spring.r2dbc.url=r2dbc:mysql://호스트:포트/test?zeroDateTimeBehavior=convertToNull&useUnicode=true&characterEncoding=UTF8&autoReconnect=true
#spring.r2dbc.username=아이디
#spring.r2dbc.password=비밀번호

logging.level.org.springframework.r2dbc=DEBUG

url은 기존에 jdbc:mysql://호스트:포트/~~~~ 형태에서 jdbc만 r2dbc로 바꿔주면 된다.

server.port는 스프링 부트의 내장 서버 포트를 의미한다.

 

하지만 나는 java config로 설정했다.

@Configuration
@PropertySource({"classpath:db.properties"})
public class R2dbcConfig extends AbstractR2dbcConfiguration{

	@Value("${poozimdb.host}")
	private String host;
	
	@Value("${poozimdb.port}")
	private int port;
	
	@Value("${poozimdb.username}")
	private String username;
	
	@Value("${poozimdb.password}")
	private String password;
	
	@Bean
	@Override
	public ConnectionFactory connectionFactory() {
		return new MySqlConnectionFactoryProvider().create(
				ConnectionFactoryOptions.builder()
				.option(ConnectionFactoryOptions.DRIVER, "mysql")
				.option(ConnectionFactoryOptions.HOST, host)
				.option(ConnectionFactoryOptions.PORT, port)
				.option(ConnectionFactoryOptions.DATABASE, "test")
				.option(ConnectionFactoryOptions.USER, username)
				.option(ConnectionFactoryOptions.PASSWORD, password)
				.build()
				);
	}
	
	@Bean
	public DatabaseClient r2dbcDatabaseClient(ConnectionFactory connectionFactory) {
		return DatabaseClient.create(connectionFactory);
	}
	
    @Bean
	public ReactiveTransactionManager transactionManager(ConnectionFactory connectionFactory) {
		return new R2dbcTransactionManager(connectionFactory);
	}
	
}

만약 여러 database를 사용한다면 database별로 connection factory를 만들어 databaseClient를 생성하면 될 것같다.

 

 

Repository

public interface TestRepository extends ReactiveCrudRepository<Board, Integer>{

}

반응형 repository는 ReactiveCrudRepository와 R2dbcRepository가 있다.

R2dbcRepository는 ReactiveCrudRepository를 상속받아 더 확장된 인터페이스지만, 내 기준 크게 차이가 없어 ReactiveCrudRepository를 상속했다. 엔티티의 기본적인 CRUD가 가능하다

 

참고 : https://docs.spring.io/spring-data/commons/docs/current/api/org/springframework/data/repository/reactive/ReactiveCrudRepository.html?is-external=true 

 

ReactiveCrudRepository (Spring Data Core 2.6.3 API)

Returns all instances of the type T with the given IDs. If some or all ids are not found, no entities are returned for these IDs. Note that the order of elements in the result is not guaranteed.

docs.spring.io

 

r2dbc는 jpa와 비슷하지만 orm은 아니라고한다. 그건 크게 중요하다고 생각하지 않지만 큰 차이점은 jpa의 querydsl같은 아직 마땅한 쿼리빌더가 없다는 점이 아쉬웠다. Jooq의 무료 버전이 있다고 하지만 SELECT 할때는 동적으로 String 형태의 query를 만드는 유틸을 하나 만들고 DatabaseClient로 직접 매핑하는 형태로 사용할 예정이다. 

 

 

Request Mapping

Spring Webflux의 장점 중 하나는 annotation-based programming model을 제공한다는 점이다.

다시 말해서, 기존에 MVC처럼 @Controller, @RestController, @GetMapping등의 어노테이션을 사용한 컨트롤러 설정이 가능하다는 것이다. 하지만 나는 Functional Endpoints를 사용하기로 했다.

간단히 말하면 매핑을 담당하는 라우터와 로직을 담당하는 핸들러로 나눠 관리하는 것이다.

 

Router

@Configuration
public class TestRouter {
	
	@Bean
	public RouterFunction<ServerResponse> testRoute(TestHandler handler){
		return RouterFunctions.route()
				.GET("/test", handler::test)
				.GET("/board/{seq}", handler::getBoardOne)
				.GET("/board", handler::getBoardList)
				.POST("/board", RequestPredicates.accept(MediaType.MULTIPART_FORM_DATA), handler::insertBoard)
				.PUT("/board/{seq}", handler::updateBoard)
				.DELETE("/board/{seq}", handler::deleteBoard)
				.POST("/file", handler::writeFile)
				.build();
	}
}

RouterFunction을 사용해 핸들러의 메서드로 매핑이 가능하다.

Post /board 매핑처럼 두번째 인자에 MIME타입 정의가 가능하고,

.GET("/test", handler::test)와 같은 형식의 문법은

.GET("/test", request -> handler.test(request))를 의미한다. 이때 request는 ServerRequest이다.

 

 

Handler

간단한 메서드만 작성해보면

@Component
public class TestHandler {
	
	@Autowired
	private TestRepository boardRepo;
	
	public Mono<ServerResponse> test (ServerRequest request){
		return ServerResponse.ok().contentType(MediaType.TEXT_PLAIN)
				.body(Mono.just("hi"), String.class);
	}
	
	public Mono<ServerResponse> getBoardOne(ServerRequest request) {
		int seq = Integer.parseInt(request.pathVariable("seq"));
		Mono<Board> board = boardRepo.findById(seq);
		return ServerResponse.ok().body(board, Board.class);
	}
	
	public Mono<ServerResponse> getBoardList(ServerRequest request) {
		Flux<Board> board_list = boardRepo.findAll();
		return ServerResponse.ok().body(board_list, Board.class);
	}
	
}

간단하게 hi를 응답하는 메서드와 Board를 Select하는 메서드만 작성했다.

SELECT에 대해 더 자세한 구현은 뒤에서 다룰 예정이다.

 

728x90