Spring Webflux R2DBC 사용해보기

2024. 7. 6. 02:00·Spring/대용량 트래픽

2024.07.03 - [Spring/대용량 트래픽] - R2DBC 알아보기!!

 

R2DBC 알아보기!!

2024.07.03 - [Spring/대용량 트래픽] - Spring Webflux에서 WebClient 사용하기 Spring Webflux에서 WebClient 사용하기2024.05.29 - [Spring/대용량 트래픽] - Spring Webflux 사용해보기 Spring Webflux 사용해보기2024.05.22 - [Spri

lsdiary.tistory.com

이전 포스팅에서는 R2DBC가 무엇인지 개념에 대해서 알아봤다. 이제 실제로 해볼 시간이다.

** 준비물
- MySQL8 (Docker container)
- Gradle
  - spring-boot-starter-data-r2dbc
  - io.asyncer:r2dbc-mysql
- Repository
  - ReactiveCrudRepository

 

도커에 mysql이 설치되어 있지 않으면 다음글에서 설치하고 실행하는 법을 찾아보길 바란다.

https://lsdiary.tistory.com/82

 

Redis Cache로 실습하기

2024.05.07 - [Spring/대용량 트래픽] - Redis Cache 이론 Redis Cache 이론2024.05.07 - [Spring/대용량 트래픽] - Redis Key, Scan 명령어 Redis Key, Scan 명령어2024.05.07 - [Spring/대용량 트래픽] - Redis Transactions Redis Transact

lsdiary.tistory.com

 

도커로 mysql을 실행한 뒤 DB를 하나 만들고 users 테이블을 생성한다.

create table users (
    id bigint auto_increment primary key,
    name varchar(128),
    email varchar(255),
    created_at datetime not null default CURRENT_TIMESTAMP,
    updated_at datetime not null default CURRENT_TIMESTAMP on update CURRENT_TIMESTAMP
);

 

추가적으로 post에 대한 테이블도 생성한다.

create table posts
    (
        id    bigint auto_increment primary key,
        user_id    bigint,
        title      varchar(30),
        content    varchar(200),
        created_at datetime not null default CURRENT_TIMESTAMP,
        updated_at datetime not null default CURRENT_TIMESTAMP
 );

 

두 개의 테이블을 생성했다면 다음으로는 인덱스를 추가해준다.

Index란?

 

DB에서 빠른 탐색을 도와주는 도구이다. 정해진 키(속성)에 대해서 탐색할수 있도록 설정할수 있다.

내부적으로 B+Tree 형태로 되어있고, 미디어 파일같은 경우에는 R-Tree로 구성되어있다.

트리 형태가 balance해서 이진트리에 비해 엄청난 성능을 보여준다. 우리는 속도가 매우중요한데, B+Tree를 통한 디스크 I/O(Page 참조횟수)는 B+Tree 의 Depth가 결정하므로 적은 Page참조를 하면서 DB를 탐색할수 있다.

create index idx_user_id on posts (user_id);

 

다음으로는 Gradle 의존성을 추가한다. 구현체로는 asyncer를 사용한다(버전은 스프링부트3에 호환되는 버전이다).

implementation 'org.springframework.boot:spring-boot-starter-data-r2dbc'
implementation 'io.asyncer:r2dbc-mysql:1.0.2'

r2dbc에 연결해줬으므로 위에 만들어둔 DB에 연결하기 위해 설정정보를 세팅해준다.

spring:
  r2dbc:
    url: r2dbc:mysql://localhost:3306/fastsns
    username: root
    password: ${DB_PASSWORD}

**아직 연결된거 아님!!(설정이 제대로 맞는건지 검증 부분이 필요함)

R2dbcConfig.java
@Component
@Slf4j
@RequiredArgsConstructor
public class  implements ApplicationListener<ApplicationReadyEvent> {
    private final DatabaseClient databaseClient;
    // 어플리케이션이 실행될때
    @Override
    public void onApplicationEvent(ApplicationReadyEvent event) {
    	//간단한 쿼리
        databaseClient.sql("SELECT 1").fetch().one()
        		//리액터에 있는 subscribe -> 즉 쿼리문을 받았는지 검증
                .subscribe(
                		//핸들러
                        success ->{
                            log.info("Initialize r2dbc database connection");
                        },
                        error -> {
                            log.error("Failed to initialize r2dbc database connection");
                        }
                );
    }
}

 

이렇게 설정해주면 비밀번호가 틀렸을때 바로 연결이 종료되는것을 확인할 수 있다.

 

더 강력하게 검증하고 싶으면 아래와 같은 코드를 error 핸들러에 설정해줄수 있다. 이렇게 설정해주면 연결이 실패할 경우 자동으로 어플리케이션을 종료해준다.

SpringApplication.exit(event.getApplicationContext(), () -> -110);

이전 글에서 @EnableR2dbcRepositories 어노테이션이 필요하다고 했었다. Config 파일에 추가하자.

그리고 미리 만들어놨던 entity 클래스에 만든 테이블을 연결시켜준다.

User.java
@Data
@Builder
@AllArgsConstructor
@Table("users")
public class User {
    @Id
    private Long id;
    private String name;
    private String email;
    @CreatedDate
    private LocalDateTime createdAt;
    @LastModifiedDate
    private LocalDateTime updatedAt;
}

 

@CreatedDate와 @LastModifiedDate를 사용하기 위해서는 기존 jdbc에서는 @EntityListeners(AuditingEntityListener.class) 다음과 같은 어노테이션을 두어서 사용했다. r2dbc에서는 @EnableR2dbcAuditing 어노테이션으로 Config파일에 설정해줘야 변경되는 시간을 리스너로 받을수 있다.


UserR2dbcRepository.java
public interface UserR2dbcRepository extends ReactiveCrudRepository<User, Long> {
}

ReactiveCrudRepository를 상속받으면 바로 R2DBC를 사용할수 있다. JPA처럼 정말 간단하다.

UserService.java
@Service
@RequiredArgsConstructor
public class UserService {
//    private final userRepository userRepository;
    private final UserR2dbcRepository userR2dbcRepository;
    public Mono<User> create(String name, String email){
        return userR2dbcRepository.save(User.builder().name(name).email(email).build());
    }

    public Flux<User> findAll(){
        return userR2dbcRepository.findAll();
    }

    public Mono<User> findById(Long id){
        return userR2dbcRepository.findById(id);
    }

    public Mono<Void> deleteById(Long id){
        return userR2dbcRepository.deleteById(id);
    }
    public Mono<User> update(Long id, String name, String email){
        return userR2dbcRepository.findById(id)
                .flatMap(u -> {
                    u.setName(name);
                    u.setEmail(email);
                    return userR2dbcRepository.save(u);
                });
    }
}

 

MVC로 코드를 짜본 사람은 위에 코드가 정말 익숙한 형태라는것을 느꼈을 것이다.

  • MVC : String, ModelAndView, ResponseEntity, 도메인 객체 등을 반환
  • Webflux : Mono<T>, Flux<T> 등의 객체를 반환

반환타입을 제외하고는 딱히 다른점이 없다!

UserController.java
@RestController
@RequiredArgsConstructor
@RequestMapping("/users")
public class UserController {
    private final UserService userService;
    @PostMapping("")
    public Mono<UserResponse> createUser(@RequestBody UserCreateRequest userCreateRequest){
        return userService.create(userCreateRequest.getName(), userCreateRequest.getEmail())
                .map(UserResponse::of);
    }

    @GetMapping("")
    public Flux<UserResponse> findAllUsers(){
        return userService.findAll()
                .map(UserResponse::of);
    }

    @GetMapping("/{id}")
    public Mono<ResponseEntity<UserResponse>> findUser(@PathVariable Long id){
        return userService.findById(id)
                .map(u -> ResponseEntity.ok(UserResponse.of(u)))
                .switchIfEmpty(Mono.just(ResponseEntity.notFound().build()));
    }

    @DeleteMapping("/{id}")
    public Mono<ResponseEntity<?>> deleteUser(@PathVariable Long id){
        //정상응답 205가 아니라 204 (no content) 응답으로 주고 싶다.
        return userService.deleteById(id).then(
                Mono.just(ResponseEntity.noContent().build())
        );
    }
    @PutMapping("/{id}")
    public Mono<ResponseEntity<UserResponse>> updateUser(@PathVariable Long id, @RequestBody UserUpdateRequest request){
        return userService.update(id, request.getName(), request.getEmail())
                .map(u -> ResponseEntity.ok(UserResponse.of(u)))
                .switchIfEmpty(Mono.just(ResponseEntity.notFound().build()));
    }
}
테스트

 

나머지 url도 전부 잘 작동하는것을 확인할 수 있다.

이전에는 ConcurrentHashMap을 통해서 실제 DB에 저장하지 않았지만, 지금은 R2DBC로 실제 MySQL에 저장할것이므로 DB에 잘 저장되어 있는지 CLI를 통해 확인해보자.

잘 저장된 모습

 

다음으로는 posts테이블에 대한 예시코드를 작성하자.

Post.java
@Data
@NoArgsConstructor
@Table(name = "posts")
public class Post {
    @Id
    private Long id;
    @Column("user_id")
    private Long userId;
    private String title;
    private String content;
    @Column("created_at")
    @CreatedDate
    private LocalDateTime createdAt;
    @Column("updated_at")
    @LastModifiedDate
    private LocalDateTime updatedAt;
}
PostServiceV2.java
@Service
@RequiredArgsConstructor
public class PostServiceV2 {
    private final PostR2dbcRepository postR2dbcRepository;
    //create
    public Mono<Post> create(Long userId, String title, String content) {
        return postR2dbcRepository.save(Post.builder()
                .userId(userId)
                .title(title)
                .content(content)
                .build());
    }
    //read
    public Flux<Post> findAll(){
        return postR2dbcRepository.findAll();
    }
    public Mono<Post> findById(Long id) {
        return postR2dbcRepository.findById(id);
    }
    //delete
    public Mono<Void> deleteById(Long id) {
        return postR2dbcRepository.deleteById(id);
    }
}
PostControllerV2.java
@RestController
@RequiredArgsConstructor
@RequestMapping("/v2/posts")
public class PostControllerV2 {
    private final PostServiceV2 postServiceV2;

    @PostMapping("")
    public Mono<PostResponseV2> createPost(@RequestBody PostCreateRequest request) {
        return postServiceV2.create(request.getUserId(), request.getTitle(), request.getContent())
                .map(PostResponseV2::of);
    }
    @GetMapping("")
    public Flux<PostResponseV2> findAllPosts() {
        return postServiceV2.findAll()
                .map(PostResponseV2::of);
    }
    @GetMapping("/{id}")
    public Mono<ResponseEntity<PostResponseV2>> findPost(@PathVariable Long id) {
        return postServiceV2.findById(id)
            .map(p -> ResponseEntity.ok().body(PostResponseV2.of(p)))
                .switchIfEmpty(Mono.just(ResponseEntity.notFound().build()));
    }
    @DeleteMapping("/{id}")
    public Mono<ResponseEntity<?>> deletePost(@PathVariable Long id) {
        return postServiceV2.deleteById(id).then(Mono.just(ResponseEntity.noContent().build()));
    }
}
테스트

 

이렇게 R2DBC를 사용한 연습도 끝났다..

2024.07.07 - [Spring/대용량 트래픽] - Spring Webflux R2DBC 여러가지 기능들

 

Spring Webflux R2DBC 여러가지 기능들

2024.07.06 - [Spring/대용량 트래픽] - Spring Webflux R2DBC 사용해보기 Spring Webflux R2DBC 사용해보기2024.07.03 - [Spring/대용량 트래픽] - R2DBC 알아보기!! R2DBC 알아보기!!2024.07.03 - [Spring/대용량 트래픽] - Spring W

lsdiary.tistory.com

 

'Spring > 대용량 트래픽' 카테고리의 다른 글

Reactive Redis란?  (0) 2024.07.07
Spring Webflux R2DBC 여러가지 기능들  (0) 2024.07.07
R2DBC 알아보기!!  (0) 2024.07.03
Spring Webflux에서 WebClient 사용하기  (0) 2024.07.03
Spring Webflux 사용해보기  (0) 2024.05.29
'Spring/대용량 트래픽' 카테고리의 다른 글
  • Reactive Redis란?
  • Spring Webflux R2DBC 여러가지 기능들
  • R2DBC 알아보기!!
  • Spring Webflux에서 WebClient 사용하기
Ls._.Rain
Ls._.Rain
안되면 될때까지 삽질했던 기록
  • Ls._.Rain
    Ls{Diary}
    Ls._.Rain
  • 전체
    오늘
    어제
    • 분류 전체보기 (136)
      • Github (2)
      • Spring (51)
        • Batch Programming (13)
        • 결제 (4)
        • 대용량 트래픽 (32)
        • OpenAI (0)
        • Security (0)
        • WebSocket (0)
        • JPA (1)
      • Algorithm (67)
        • DFS (6)
        • BFS (6)
        • Dynamic Programming (10)
        • Brute Force (4)
        • Binary Search (6)
        • 구현, 시뮬레이션 (15)
        • Stack (1)
        • Greedy (4)
        • Priority_Queue (2)
        • Back Tracking (3)
        • Geometry (2)
        • SCC (1)
        • 투포인터 (4)
        • 최대유량 (1)
        • 정렬 (1)
      • OS (0)
      • DevOps (15)
        • AWS (11)
        • Docker (4)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 링크

  • hELLO· Designed By정상우.v4.10.0
Ls._.Rain
Spring Webflux R2DBC 사용해보기
상단으로

티스토리툴바