지금까지는 대부분의 어플리케이션을 Spring MVC로 개발했다.
하지만 다음 해 진행할 프로젝트에서는 기술회의 중 Spring Reactiive (Webflux)를 사용하기로 결정되었다.
Webflux 경험이 전무하지는 않다. 한 번의 프로젝트를 Webflux로 개발한 경험이 있고, 개인적으로 공부를 하기도 했었다. 하지만 MVC처럼 실무에서 다양하게 사용해보지는 못해서 당연히 이해도가 많이 떨어져 활용법을 제대로 알지 못했고, 또 프로젝트를 한 지 한참이 지나 까먹은 내용이 대부분이기에 잠시 시간이 난 틈을 타서 사용방법을 공부해보았다.
이어지는 다음 프로젝트에서 사용할 수 있도록 공부한 내용을 포스팅해놓을까 한다.
Spring Webflux
웹플럭스는 비동기, 논블로킹을 지원하는 방식이다. 이미 앞선 포스팅에서 어느정도 설명을 해놓았기에 설명은 해당 포스팅 링크로 갈음한다.
https://123okk2.tistory.com/477
실습
우선 프로젝트를 생성한다.
말 그대로 실습용 프로그램을 만들 것이기에 너무 많은 의존성을 추가하지는 않았다.
다음으로 사용할 DB에 Database와 Table을 생성해놓았다.
Table은 간단하게 [아이디, 닉네임, 나이] 로 이루어진 테이블을 사용한다.
CREATE DATABASE IF NOT EXISTS test;
CREATE TABLE IF NOT EXISTS users (
id VARCHAR(20) PRIMARY KEY,
nick_name VARCHAR(50),
age INTEGER
);
설정이 완료되었다면 application.properties를 설정한다.
# SET R2DBC DB
spring.r2dbc.url=r2dbc:mariadb://{IP}:3306/test
spring.r2dbc.username=root
spring.r2dbc.password=password
logging.level.root=INFO
이제부터 클래스를 생성한다.
최종적으로 만들어질 클래스는 다음과 같이 구성되어있다.
- CommonCode : Service와 Controller가 주고받을 결과값
- CommonFilter : 사용자의 요청에 대한 로그를 기록할 필터
- CommonMessage : 사용자에게 반환할 메시지와 로그 메시지 공통화
- Response : 사용자에게 반환될 Response 양식
- MariaController : 화면 반환용 컨트롤러
- MariaRestController : RESTFUL API 용도의 컨트롤러
- MariaDomain : DTO
- MariaRepository : DAO
- MariaService : 비즈니스 로직 처리용 서비스 클래스
공통 함수, 변수 만들기
실습 코드 작성에 앞서 프로젝트 전반에서 사용될 공통 함수와 변수를 만들어주었다.
우선 공통코드이다. Service가 Controller에서 데이터를 전달받아 처리한 후, 처리 결과를 Controller에 알려주기 위한 변수들이다.
CommonCode.java
package com.mwlee.test.wfx.common;
public class CommonCode {
public static Integer EXIST_USER = 0;
public static Integer NOT_EXIST_USER = 1;
public static Integer INTERNAL_ERROR = 2;
public static Integer BODY_REQUIRED = 3;
public static Integer ID_REQUIRED = 4;
public static Integer SUCCESS = 5;
}
다음으로 로그, 사용자 반환 메시지에 사용될 공통메시지도 생성해주었다.
CommonMessage.java
package com.mwlee.test.wfx.common;
public class CommonMessage {
// Response (사용자) 반환용 메시지
public static String EXIST_USER = "User already exists.";
public static String NOT_EXIST_USER = "User not exists.";
public static String BODY_REQUIRED = "Request body must not be null.";
public static String ID_REQUIRED = "ID must not be null.";
public static String INTERNAL_ERROR = "Some error occured. Try again.";
// 로그 출력용 메시지
public static String EXIST_USER_LOG = "@@ User Already Exists : {}";
public static String NOT_EXIST_USER_LOG = "@@ User Not Exists : {}";
public static String SUCCESS_LOG = "@@ User Successfully {} : {}";
public static String BODY_REQUIRED_LOG = "@@ Request Body Must Not Be Null.";
public static String ID_REQUIRED_LOG = "@@ ID Must Not Be Null.";
public static String INTERNAL_ERROR_LOG = "@@ Error Occured While {} : {}";
}
사용자에게 통일된 Response를 제공하기 위한 Response 양식도 작성한다.
Response.java
package com.mwlee.test.wfx.common;
import java.io.Serializable;
import org.springframework.http.HttpStatus;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
@Data
@Getter
@Setter
public class Response implements Serializable {
private static final long serialVersionUID = -2929789292155268166L;
private HttpStatus status;
private String message;
private Object Data;
}
마지막으로 모든 사용자의 요청을 기록할 필터를 생성한다.
참고로 일반 MVC의 필터를 사용해서는 안되고, WebFliter을 사용해야만 한다.
해당 내용은 이 포스팅에 기재해놓았다.
CommonFilter.java
package com.mwlee.test.wfx.common;
import org.springframework.stereotype.Component;
import org.springframework.web.server.ServerWebExchange;
import org.springframework.web.server.WebFilter;
import org.springframework.web.server.WebFilterChain;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
@Slf4j
@Component
public class CommonFilter implements WebFilter {
@Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
StringBuffer logStr = new StringBuffer();
// 모든 Request에 대해 ip -> URI [METHOD] 형식으로 로그 생성
logStr
.append(exchange.getRequest().getLocalAddress())
.append(" -> ")
.append(exchange.getRequest().getURI())
.append(" [")
.append(exchange.getRequest().getMethod())
.append("]");
log.info(logStr.toString());
return chain.filter(exchange);
}
}
메인 로직 개발
메인 로직은 일반 Spring MVC와 마찬가지로 Controller - Service - Repository - Domain 구조로 설정했다.
우선 앞서 생성한 테이블에 맞게 Domain 클래스를 작성한다.
MaraiDomain.java
package com.mwlee.test.wfx.domain;
import org.springframework.data.annotation.Id;
import org.springframework.data.annotation.Transient;
import org.springframework.data.domain.Persistable;
import org.springframework.data.relational.core.mapping.Column;
import org.springframework.data.relational.core.mapping.Table;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
@ToString
@JsonInclude(JsonInclude.Include.NON_NULL)
@Getter
@Setter
@Table(name = "users")
public class MariaDomain implements Persistable<String> {
@Id
@Column("id")
private String id;
@Column("nick_name")
private String nickName;
@Column("age")
private int age;
/**
* r2dbc의 repository의 save 메서드는 upsert가 아니라 update를 함.
* 고로 save가 upsert의 기능을 하기 위해서는 신규 데이터 여부를 위한 아래 변수가 필요함.
*/
@Transient
public boolean isNew;
public MariaDomain setNew() {
this.isNew = true;
return this;
}
@Override
@Transient
public boolean isNew() {
return this.isNew;
}
}
주석으로도 달아놓았는데, 테스트를 하던 중 Repository의 save() 메소드를 사용했을 경우 신규 데이터는 아래와 같은 에러가 발생했다.
Failed to update table [테이블명]; Row with Id [PK값] does not exist
찾아보니 Webflux의 Repository들은 save를 upsert의 개념보다는 update의 개념을 디폴트로 사용하고, 이를 upsert로 사용하기 위해서는 위와같이 Persistable을 상속받아 신규 데이터이지 여부를 알려줘야 한다고 한다.
추가로 setNew 메서드가 자기 자신을 반환하도록 한 이유는 아래 서비스 코드를 보면 이해할 수 있을 것이다.
다음으로 Repository 코드를 작성한다.
MariaRepository.java
package com.mwlee.test.wfx.repository;
import org.springframework.data.r2dbc.repository.R2dbcRepository;
import org.springframework.stereotype.Repository;
import com.mwlee.test.wfx.domain.MariaDomain;
@Repository
public interface MariaRepository extends R2dbcRepository<MariaDomain, String>{
}
참고로 JPA가 아닌 R2DBC를 사용했는데, Spring Webflux는 비동기이므로 DB접근도 비동기로 처리되어야 한다. 그에 반해 JPA는 동기식이므로, 만약 다른 로직은 Webflux롤 짰는데 DB접근은 JPA로 짜면 비동기의 의미가 사라진다.
JPA : 동기식 / 블로킹 IO 지원
R2DBC : 비동기식 / 논블로킹 IO 지원
다음은 Service이다. 상세 내용은 주석으로 기재해두었다.
MariaService.java
package com.mwlee.test.wfx.service;
import java.util.Objects;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.mwlee.test.wfx.common.CommonCode;
import com.mwlee.test.wfx.common.CommonMessage;
import com.mwlee.test.wfx.domain.MariaDomain;
import com.mwlee.test.wfx.repository.MariaRepository;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Flux;
import reactor.core.publisher.Mono;
@Service
@Slf4j
public class MariaService {
@Autowired private MariaRepository repo;
/**
* 데이터 개별 조회
*
* @param userId
* @return
*/
@Transactional(readOnly = true)
public Mono<MariaDomain> selectOne(String userId) {
return repo.findById(userId)
// 에러 발생 시 로그 출력 및 에러 반환
.doOnError(e -> log.error(CommonMessage.INTERNAL_ERROR_LOG, "Find User", userId))
.onErrorResume(e -> Mono.error(e));
}
/**
* 데이터 전체 조회
*
* @return
*/
@Transactional(readOnly = true)
public Flux<MariaDomain> selectAll() {
return repo.findAll()
// 에러 발생 시 로그 출력 및 에러 반환
.doOnError(e -> log.error(CommonMessage.INTERNAL_ERROR_LOG, "Find All Users", ""))
.onErrorResume(e -> Flux.error(e));
}
/**
* 데이터 입력
* @param domain
* @return
*/
@Transactional
public Mono<Integer> insert(MariaDomain domain) {
// 우선 전달받은 domain의 notnull 확인
if(domain == null) {
log.info(CommonMessage.BODY_REQUIRED_LOG);
return Mono.just(CommonCode.BODY_REQUIRED);
}
else if(domain.getId() == null || "".equals(domain.getId())) {
log.info(CommonMessage.ID_REQUIRED_LOG);
return Mono.just(CommonCode.ID_REQUIRED);
}
// 이상이 없다면 로직 진행
return repo.findById(domain.getId())
// 중복된 사용자일 경우 로그 출력 후 중복된 사용자임을 알려주는 코드 반환
.doOnNext(user -> log.info(CommonMessage.EXIST_USER_LOG, domain.getId()))
.flatMap(user -> Mono.just(CommonCode.EXIST_USER))
// 중복된 사용자가 아닐 경우 마찬가지로 로그 출력 후 저장
.switchIfEmpty(
Mono.defer(() -> repo.save(domain.setNew()) // 미존재시 적재
// 적재 성공/실패 에 따른 에러 출력
.doOnSuccess(user -> log.info(CommonMessage.SUCCESS_LOG, "Saved", domain))
.doOnError(e -> log.error(CommonMessage.INTERNAL_ERROR_LOG, "Saving User" , e.getMessage()))
// 적재 성공/실패 에 따른 코드 반환
.then(Mono.just(CommonCode.SUCCESS))
.onErrorResume(e -> Mono.just(CommonCode.INTERNAL_ERROR))
)
)
// 만약 최상단의 findById에서 에러가 발생했을 경우
.doOnError(e -> log.error(CommonMessage.INTERNAL_ERROR_LOG, "Saving User" , e.getMessage()))
.onErrorResume(e -> Mono.just(CommonCode.INTERNAL_ERROR));
}
/**
* 데이터 수정
* @param domain
* @return
*/
@Transactional
public Mono<Integer> update(MariaDomain domain) {
// 우선 전달받은 domain의 notnull 확인
if(domain == null) {
log.info(CommonMessage.BODY_REQUIRED_LOG);
return Mono.just(CommonCode.BODY_REQUIRED);
}
else if(domain.getId() == null || "".equals(domain.getId())) {
log.info(CommonMessage.ID_REQUIRED_LOG);
return Mono.just(CommonCode.ID_REQUIRED);
}
// 이상이 없다면 로직 진행
return repo.findById(domain.getId())
// 사용자가 존재할 경우 update 수행
.flatMap(u ->
repo.save(domain)
// 적재 성공/실패 에 따른 에러 출력
.doOnSuccess(user -> log.info(CommonMessage.SUCCESS_LOG, "Updated", domain))
.doOnError(e -> log.error(CommonMessage.INTERNAL_ERROR_LOG, "Updating User" , e.getMessage()))
// 적재 성공/실패 에 따른 코드 반환
.then(Mono.just(CommonCode.SUCCESS))
.onErrorResume(e -> Mono.just(CommonCode.INTERNAL_ERROR))
)
// 사용자가 존재하지 않을 경우 실패 반환
.switchIfEmpty(
Mono.defer(() -> {
log.info(CommonMessage.NOT_EXIST_USER_LOG);
return Mono.just(CommonCode.NOT_EXIST_USER);
})
)
// 만약 최상단의 findById에서 에러가 발생했을 경우
.doOnError(e -> log.error(CommonMessage.INTERNAL_ERROR_LOG, "Updating User" , e.getMessage()))
.onErrorResume(e -> Mono.just(CommonCode.INTERNAL_ERROR));
}
/**
* 데이터 삭제
*
* @param userId
* @return
*/
@Transactional
public Mono<Integer> delete(String userId) {
// 사용자 아이디가 이상없이 들어왔는지 확인
if(userId == null || "".equals(userId)) {
log.info(CommonMessage.ID_REQUIRED_LOG);
return Mono.just(CommonCode.ID_REQUIRED);
}
return repo.findById(userId)
// 데이터가 존재할 경우 삭제 로직 수행
.flatMap(u ->
repo.delete(u)
.doOnSuccess(user -> log.info(CommonMessage.SUCCESS_LOG, "Deleted", user))
.doOnError(e -> log.error(CommonMessage.INTERNAL_ERROR_LOG, "Deleting User" , e.getMessage()))
// 적재 성공/실패 에 따른 코드 반환
.then(Mono.just(CommonCode.SUCCESS))
.onErrorResume(e -> Mono.just(CommonCode.INTERNAL_ERROR))
)
// 사용자가 존재하지 않는 경우 실패 반환
.switchIfEmpty(
Mono.defer(() -> {
log.info(CommonMessage.NOT_EXIST_USER_LOG);
return Mono.just(CommonCode.NOT_EXIST_USER);
})
)
// 만약 최상단의 findById에서 에러가 발생했을 경우
.doOnError(e -> log.error(CommonMessage.INTERNAL_ERROR_LOG, "Deleting User" , e.getMessage()))
.onErrorResume(e -> Mono.just(CommonCode.INTERNAL_ERROR));
}
}
- doOnError: 에러가 발생했을 때 특정 작업을 수행한다. 이 연산자는 데이터 흐름에 영향을 주지 않고, 단순히 에러가 발생했을 때의 사이드 이펙트(로그 기록 등)를 처리하는 데 사용된다.
- onErrorResume: 에러가 발생했을 때 대체할 데이터를 제공한다. 이 연산자는 에러 발생시 대체 퍼블리셔를 반환하여 에러를 "회복"하는 방식으로 사용된다.
- just: 하나 이상의 요소를 포함하는 리액티브 스트림을 생성한다. 이 연산자는 정적인 값을 리액티브 스트림으로 변환할 때 사용된다.
- doOnNext: 각각의 데이터 아이템에 대해 특정 작업을 수행한다. 이 연산자 역시 사이드 이펙트를 처리하는 데 사용되며, 데이터 흐름 자체에는 영향을 주지 않는다.
- flatMap: 각각의 아이템을 다른 퍼블리셔로 변환하고, 이들을 하나의 플랫한 스트림으로 병합한다. 비동기 작업이나, 요소별로 다른 리액티브 스트림을 생성해야 할 때 유용하다.
- switchIfEmpty: 스트림이 비어있을 때 대체 스트림을 제공한다. 원래 스트림에서 데이터가 발생하지 않았을 때 사용할 대체 로직을 제공하는 데 사용된다.
- Defer: 데이터 소스가 실제로 구독될 때까지 생성을 지연시킨다. 이를 통해 각 구독자에게 새로운 데이터 소스 인스턴스를 제공할 수 있다.
- doOnSuccess: 성공적으로 데이터 발생을 완료했을 때 특정 작업을 수행한다. doOnNext와 비슷하지만, 스트림의 완료 시점에 한번만 호출된다.
- then: 현재 스트림의 모든 데이터가 처리된 후 다른 퍼블리셔의 실행을 시작한다. 이 연산자는 주로 특정 작업이 완료된 후 다음 작업을 시작하는 흐름을 제어할 때 사용된다.
마지막으로 Controller을 작성한다. 간단하게 POST, GET, PUT, DELETE 메서드를 지원하는 API를 작성했다.
MariaRestController.java
package com.mwlee.test.wfx.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import com.mwlee.test.wfx.common.CommonCode;
import com.mwlee.test.wfx.common.CommonMessage;
import com.mwlee.test.wfx.common.Response;
import com.mwlee.test.wfx.domain.MariaDomain;
import com.mwlee.test.wfx.service.MariaService;
import lombok.extern.slf4j.Slf4j;
import reactor.core.publisher.Mono;
@RestController
@Slf4j
public class MariaRestController {
@Autowired MariaService service;
/**
* 개별 데이터 조회
*
* @param userId
* @return
*/
@GetMapping("/maria/{userId}")
public Mono<Response> getOneUser(@PathVariable(required=true) String userId) {
return service.selectOne(userId)
// 위 메서드의 실행 결과로 데이터가 추출된 경우
.flatMap(user -> {
Response response = new Response();
response.setStatus(HttpStatus.OK);
response.setData(user);
return Mono.just(response);
})
// 데이터가 추출되지 않은 경우. 즉, userId에 해당하는 데이터가 존재하지 않는 경우
.switchIfEmpty(
Mono.defer(() -> {
Response response = new Response();
log.info(CommonMessage.NOT_EXIST_USER_LOG, userId);
response.setStatus(HttpStatus.BAD_REQUEST);
response.setMessage(CommonMessage.NOT_EXIST_USER);
return Mono.just(response);
})
)
// 예상치 못한 에러 발생 시에 대한 대처
.onErrorResume(e -> {
log.error(CommonMessage.INTERNAL_ERROR_LOG, "Find User", e.getMessage());
Response response = new Response();
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
response.setMessage(CommonMessage.INTERNAL_ERROR);
return Mono.just(response);
});
}
/**
* 전체 데이터 조회
*
* @return
*/
@GetMapping("/maria")
public Mono<Response> getAllUser() {
return service.selectAll()
// 데이터가 배열인 Flux로 나오기에, Response.data 내에 전체 데이터를 넣기 위해 아래와 같이 사용.
// 만약 flatMap이나 collectMap 이용 시 Response[] 가 반환됨.
.collectList()
.flatMap(userList -> {
log.info(userList.toString());
Response response = new Response();
if(userList == null || userList.size() == 0) {
// 빈 배열일 경우 (사용자가 없을 경우) 메시지 추가.
response.setMessage(CommonMessage.NOT_EXIST_USER);
}
response.setStatus(HttpStatus.OK);
response.setData(userList);
return Mono.just(response);
})
// 예상치 못한 에러 발생 시에 대한 대처
.onErrorResume(e -> {
log.error(CommonMessage.INTERNAL_ERROR_LOG, "Find User", e.getMessage());
Response response = new Response();
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
response.setMessage(CommonMessage.INTERNAL_ERROR);
return Mono.just(response);
});
}
/**
* 데이터 입력
*
* @param domain : null 전달 시에도 공통 Response 반환을 위해 required=false
* @return
*/
@PostMapping("/maria")
public Mono<Response> insertUser(@RequestBody(required=false) MariaDomain domain) {
return service.insert(domain)
.flatMap(result -> {
Response response = new Response();
// Service에서 반환한 결과에 따라 response 생성 및 반환
if(result == CommonCode.EXIST_USER) {
response.setStatus(HttpStatus.BAD_REQUEST);
response.setMessage(CommonMessage.EXIST_USER);
}
else if(result == CommonCode.INTERNAL_ERROR) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
response.setMessage(CommonMessage.INTERNAL_ERROR);
}
else if(result == CommonCode.BODY_REQUIRED) {
response.setStatus(HttpStatus.BAD_REQUEST);
response.setMessage(CommonMessage.BODY_REQUIRED);
}
else if(result == CommonCode.ID_REQUIRED) {
response.setStatus(HttpStatus.BAD_REQUEST);
response.setMessage(CommonMessage.ID_REQUIRED);
}
else if(result == CommonCode.SUCCESS){
response.setStatus(HttpStatus.CREATED);
}
return Mono.just(response);
})
// 예상치 못한 에러 발생 시에 대한 대처
.onErrorResume(e -> {
log.error(CommonMessage.INTERNAL_ERROR_LOG, "Saving User", e.getMessage());
Response response = new Response();
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
response.setMessage(CommonMessage.INTERNAL_ERROR);
return Mono.just(response);
});
}
/**
* 데이터 수정
*
* @param domain
* @return
*/
@PutMapping("/maria")
public Mono<Response> updateUser(@RequestBody(required=false) MariaDomain domain) {
return service.update(domain)
.flatMap(result -> {
Response response = new Response();
// Service에서 반환한 결과에 따라 response 생성 및 반환
if(result == CommonCode.NOT_EXIST_USER) {
response.setStatus(HttpStatus.BAD_REQUEST);
response.setMessage(CommonMessage.NOT_EXIST_USER);
}
else if(result == CommonCode.INTERNAL_ERROR) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
response.setMessage(CommonMessage.INTERNAL_ERROR);
}
else if(result == CommonCode.BODY_REQUIRED) {
response.setStatus(HttpStatus.BAD_REQUEST);
response.setMessage(CommonMessage.BODY_REQUIRED);
}
else if(result == CommonCode.ID_REQUIRED) {
response.setStatus(HttpStatus.BAD_REQUEST);
response.setMessage(CommonMessage.ID_REQUIRED);
}
else if(result == CommonCode.SUCCESS){
response.setStatus(HttpStatus.NO_CONTENT);
}
return Mono.just(response);
})
// 예상치 못한 에러 발생 시에 대한 대처
.onErrorResume(e -> {
log.error(CommonMessage.INTERNAL_ERROR_LOG, "Updating User", e.getMessage());
Response response = new Response();
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
response.setMessage(CommonMessage.INTERNAL_ERROR);
return Mono.just(response);
});
}
/**
* 데이터 개별 삭제
*
* @param userId
* @return
*/
@DeleteMapping("/maria/{userId}")
public Mono<Response> deleteUser(@PathVariable(required=false) String userId) {
return service.delete(userId)
.flatMap(result -> {
Response response = new Response();
// Service에서 반환한 결과에 따라 response 생성 및 반환
if(result == CommonCode.NOT_EXIST_USER) {
response.setStatus(HttpStatus.BAD_REQUEST);
response.setMessage(CommonMessage.NOT_EXIST_USER);
}
else if(result == CommonCode.INTERNAL_ERROR) {
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
response.setMessage(CommonMessage.INTERNAL_ERROR);
}
else if(result == CommonCode.ID_REQUIRED) {
response.setStatus(HttpStatus.BAD_REQUEST);
response.setMessage(CommonMessage.ID_REQUIRED);
}
else if(result == CommonCode.SUCCESS){
response.setStatus(HttpStatus.NO_CONTENT);
}
return Mono.just(response);
})
// 예상치 못한 에러 발생 시에 대한 대처
.onErrorResume(e -> {
log.error(CommonMessage.INTERNAL_ERROR_LOG, "Deleting User", e.getMessage());
Response response = new Response();
response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR);
response.setMessage(CommonMessage.INTERNAL_ERROR);
return Mono.just(response);
});
}
}
*PUT에서 Pathvariable에 id를 넣는 것을 깜박했다..
테스트
이제 Postman을 사용해서 전체 기능이 정상 동작하는지 확인한다.
APPENDIX #1. HTML 서빙하기
REST API를 사용하는 방법은 공부했는데, 다음 프로젝트는 한 프로젝트가 REST API 뿐 아니라 화면도 함께 서빙을 해야한다.
*MSA로 하고 싶기는 하지만, 뭐 그렇게 하라니...
그래서 하는 김에 Webflux에서 화면을 서빙하는 것까지 추가하고자 한다.
우선 Dependency에 Thymeleaf를 추가하고, application.properties 설정파일에 Thymeleaf 관련 설정을 추가한다.
spring.thymeleaf.cache=false
spring.thymeleaf.prefix=classpath:templates/
spring.thymeleaf.suffix=.html
spring.thymeleaf.encoding=UTF-8
그리고 resource 폴더 하위에 templates 폴더를 생성하고, 그 밑에 다음 html 파일을 추가했다.
home.html
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>test page</title>
</head>
<body>
<table>
<tr th:unless="${testDomains.empty}" th:each="testDomain : ${testDomains}">
<td><span th:text=${testDomain.id}></span></td>
<td><span th:text=${testDomain.nickName}></span></td>
<td><span th:text=${testDomain.age}></span></td>
</tr>
</table>
</body>
</html>
마지막으로 모든 데이터를 가져와 뿌려주는 Controller을 추가했다.
MVC의 ModelAndView처럼 Rendering을 Mono에 감싸서 추가해주는 형식이다. ModelAndView나 Model을 사용해봤다면 누구나 쉽게 따라할 수 있을 것 같다.
MariaController.java
package com.mwlee.test.wfx.controller;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.reactive.result.view.Rendering;
import com.mwlee.test.wfx.service.MariaService;
import reactor.core.publisher.Mono;
@Controller
public class MariaController {
@Autowired MariaService mariaService;
/**
* 페이지 반환
* Model이나 ModelAndView 대신 Rendering 사용
*
* @return
*/
@GetMapping("/home")
public Mono<Rendering> home() {
return
Mono.just(
Rendering
.view("home")
.modelAttribute("testDomains", mariaService.selectAll())
.build()
);
}
}
그리고 이제 home으로 들어가면 아래와 같은 페이지가 출력됨을 확인할 수 있다.
APPENDIX #2. MariaDB 직접 설정하기.
만약 스프링에서 자동으로 DB에 매핑하는 것이 싫다면 아래와 같이 설정이 가능하다.
우선 application.properties의 DB 접속 정보를 일부 수정한다. 아래 수정 내용은 내 임의로 한 것일 뿐, 법칙은 아니다.
spring.r2dbc.host={IP}
spring.r2dbc.port=3306
spring.r2dbc.database=test
spring.r2dbc.username=root
spring.r2dbc.password=password
그리고 MariaConfig 라는 Configuration 클래스를 만들어 설정을 수행한다.
MariaConfig.java
package com.mwlee.test.wfx.common;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.r2dbc.config.AbstractR2dbcConfiguration;
import org.springframework.data.r2dbc.core.R2dbcEntityTemplate;
import org.springframework.data.r2dbc.repository.config.EnableR2dbcRepositories;
import io.r2dbc.spi.ConnectionFactories;
import io.r2dbc.spi.ConnectionFactory;
import io.r2dbc.spi.ConnectionFactoryOptions;
@Configuration
// 해당 datasource를 사용할 클래스의 패키지 지정
@EnableR2dbcRepositories(basePackages = "com.mwlee.test.wfx", entityOperationsRef = "getmariaTemplate")
public class MariaConfig extends AbstractR2dbcConfiguration {
@Value("${spring.r2dbc.host}")
private String mariaHost;
@Value("${spring.r2dbc.port}")
private int mariaPort;
@Value("${spring.r2dbc.database}")
private String mariaDB;
@Value("${spring.r2dbc.username}")
private String mariaUserName;
@Value("${spring.r2dbc.password}")
private String mariaPassword;
@Override
@Bean
@Qualifier("maraiaConnectionFactory")
public ConnectionFactory connectionFactory() {
return ConnectionFactories.get(
ConnectionFactoryOptions
.builder()
// 접속을 위한 기본 정보
.option(ConnectionFactoryOptions.DRIVER, "mariadb")
.option(ConnectionFactoryOptions.HOST, mariaHost)
.option(ConnectionFactoryOptions.PORT, mariaPort)
.option(ConnectionFactoryOptions.DATABASE, mariaDB)
.option(ConnectionFactoryOptions.USER, mariaUserName)
.option(ConnectionFactoryOptions.PASSWORD, mariaPassword)
.build()
);
}
@Bean
@Qualifier("mariaTemplate")
public R2dbcEntityTemplate getmariaTemplate(@Qualifier("maraiaConnectionFactory") ConnectionFactory connectionFactory) {
return new R2dbcEntityTemplate(connectionFactory);
}
}
'실습 > 리눅스 서버 + 스프링 부트' 카테고리의 다른 글
[Spring Webflux] HDFS에 파일 업로드 (0) | 2023.12.12 |
---|---|
[Spring Webflux] Mybatis를 비동기로 돌리기 (0) | 2023.12.05 |
Spring MVC vs Spring Webflux (0) | 2023.10.30 |
동시성 제어 (0) | 2023.10.16 |
[Apache Server] 리버스 프록시_CentOS, Ubuntu (1) | 2023.09.28 |