2024.07.06 - [Spring/대용량 트래픽] - Spring Webflux R2DBC 사용해보기
이전 포스팅에서 R2DBC를 한번 사용해보았는데, JPA를 사용할때와 동일하게 사용할수 있는지 테스트 해보자.
- ReactiveCrudRepository
- JPQL (@Query)
- CustomRepository
UserR2dbcRepository
public interface UserR2dbcRepository extends ReactiveCrudRepository<User, Long> {
// name이라는 column이 where절로 들어감
Flux<User> findByName(String name);
Flux<User> findByNameOrderByIdDesc(String name);
// JPQL 사용
@Modifying
@Query("DELETE FROM users WHERE name = :name")
Mono<Void> deleteByName(String name);
}
@Modifying이란?
- @Query 어노테이션으로 작성된 INSERT, DELETE, UPDATE 쿼리에 사용되는 어노테이션이다.
- clearAutomatically, flushAutomatically 옵션을 따로 설정할 수 있다.
@Query로 쓰여진 쿼리문은 영속성 컨텍스트를 거쳐서 쓰기지연SQL로 동작하는 것이 아닌, 곧바로 DB에 질의하게 된다. 그래서 영속성 컨텍스트(우리로 치면 콘솔창에서 확인할수 있음)에는 제대로 변경사항이 반영 안되는 경우가 발생한다. 이를 해결해주는것이 clearAutomatically 옵션이다. @Query로 실행된 JPQL 이후 영속성 컨텍스트를 자동으로 비워주는 옵션인데, 빈자리가 DB에서 가져온 모든 데이터들로 채워진다. 이를 통해 영속성 컨텍스트도 동일하게 항상 최신상태를 유지할수 있다!
**참고
** Spring WebFlux에서 Flux를 ResponseEntity로 감싸서 반환하려고 할 때 발생하는 Only a single ResponseEntity supported 오류는 ResponseEntity가 단일 응답을 나타내기 때문이다. 따라서 Flux를 사용할때는 Flux자체를 컨트롤러에서 반환하는것이 바람직하다! (ServerResponse로 감싸는 방법도 있다)
기존 JPA를 사용할때는 ORM(Object Relation Mapping)을 지원해줘서 @OneToMany, @ManyToOne 등의 어노테이션과 함께 각 테이블간의 연관관계를 설정해주고, 객체그래프탐색으로 손쉽게 복잡한 쿼리문을 작성할수있었다.
하지만 R2DBC는 ORM이 아니다! -> 우리가 직접, 손수 하나하나 다 해줘야한다.(벌써 부터 복잡해보인다)
한 사용자가 여러개의 글을 작성할수 있으므로, users 테이블과 posts 테이블의 연관관계는 1:N으로 매핑된다.
그렇다면 R2DBC에서는 이런 연관관계 설정을 어떻게 해줄까?
CustomRepository
이번에 해볼 예시는 특정 사용자의 여러 글을 전부 조회하는 API이다. 여기서 응답으로 줘야할 DTO에서 필요한 값은 다음과 같다.
- 어떻게 post.getUser().getName()으로 객체그래프 탐색할 수 있을까?
public static UserPostResponse of(Post post) {
return UserPostResponse.builder()
.id(post.getId())
.userName(post.getUser().getName())
.title(post.getTitle())
.content(post.getContent())
.createdAt(post.getCreatedAt())
.updatedAt(post.getUpdatedAt())
.build();
}
- @Transient : R2DBC에서 연관관계 설정을 해주기 위한 필드에 붙이는 어노테이션이다.
Post.java
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@Table(name = "posts")
public class Post {
@Id
private Long id;
@Column("user_id")
private Long userId;
private String title;
private String content;
// 실제 DB에 저장되는 대상이 아니다.
@Transient
private User user;
@Column("created_at")
@CreatedDate
private LocalDateTime createdAt;
@Column("updated_at")
@LastModifiedDate
private LocalDateTime updatedAt;
}
내가 구현하고자 하는 레포지토리를 인터페이스로 생성하고, 그에 맞는 구현 클래스를 만든다.
PostCustomR2dbcRepositoryImpl.java
@Repository
@RequiredArgsConstructor
public class PostCustomR2dbcRepositoryImpl implements PostCustomR2dbcRepository {
private final DatabaseClient databaseClient;
@Override
public Flux<Post> findAllByUserId(Long userId) {
var sql = """
SELECT p.id as pid, p.user_id as userId, p.title, p.content, p.created_at as createdAt, p.updated_at as updatedAt,
u.id as uid, u.name as name, u.email as email, u.created_at as uCreatedAt, u.updated_at as uUpdatedAt
FROM posts p
LEFT JOIN users u ON p.user_id = u.id
WHERE p.user_id = :userId
""";
return databaseClient.sql(sql)
.bind("userId", userId)
.fetch()
.all()
.map(row -> Post.builder()
.id((Long) row.get("pid"))
.userId((Long) row.get("userId"))
.title((String) row.get("title"))
.content((String) row.get("content"))
.user(
User.builder()
.id((Long) row.get("uid"))
.name((String) row.get("name"))
.email((String) row.get("email"))
.createdAt(((ZonedDateTime) row.get("uCreatedAt")).toLocalDateTime())
.createdAt(((ZonedDateTime) row.get("uUpdatedAt")).toLocalDateTime())
.build()
)
.createdAt(((ZonedDateTime)row.get("createdAt")).toLocalDateTime())
.updatedAt(((ZonedDateTime)row.get("updatedAt")).toLocalDateTime())
.build());
}
}
- DatabaseClient : 지금 프로젝트의 DB에 연결할 객체다.
- sql(쿼리문) : 원하는 쿼리문을 작성해서 DatabaseClient객체에 전달한다.
- bind() : 쿼리문에 WHERE절에 들어갈 값을 세팅해준다.
- fetch().all() : 쿼리를 실행하고 결과를 불러온다.
- map으로 테이블의 각 행을 세팅(.get()으로 가져온 객체는 모두 Object형으로 다운캐스팅 해줘야 함)해주고, DB로 데이터를 전송하고, 서비스단으로 리턴해준다.
- 마지막으로 컨트롤러에서 지정된 users가 작성한 글에 대한 정보를 Flux<Post>형으로 받고, Converter인 of(Post post) 함수에서 post.getUser()를 알맞게 가져올수 있게된다.
테스트
'Spring > 대용량 트래픽' 카테고리의 다른 글
Reactive Redis 사용법 (1) | 2024.07.12 |
---|---|
Reactive Redis란? (0) | 2024.07.07 |
Spring Webflux R2DBC 사용해보기 (0) | 2024.07.06 |
R2DBC 알아보기!! (0) | 2024.07.03 |
Spring Webflux에서 WebClient 사용하기 (0) | 2024.07.03 |