지난 번 작성된 R2DBC 에서 OneToMany 를 구현하는 부분을 이어서 작성한다.
지난 첫 게시글에선 DatabaseClient 로 별도의 Custom Repository 를 정의하였고,
해당 부분에서 Member 와 Order 를 JOIN 하여 처리하였다.
하지만 위와 같이 OneToMany 관계에서 JOIN 하는 경우 Many 쪽 데이터 개수에 맞게 ROW 개수가 늘어나게 된다. (일명 뻥튀기)
SELECT m.member_id, m.member_name, m.member_role, o.order_id, o.order_name, o.order_status
FROM member AS m
LEFT OUTER JOIN orders AS o
ON m.member_id = o.member_id
위와 같은 중복은 SQL DISTINCT 키워드로도 제거되지 않지만,
JPA 와 같은 ORM 을 사용하여(Entity Graph, QueryDSL) OneToMany 관계를 요청할 시 해당 부분을 보다 손쉽게 해결할 수 있다.
하지만, R2DBC 는 ORM 이 아니기에 해당 부분을 개발자가 직접 작성해주지 않으면 안된다.
이번 게시글에선 WHERE 절 IN 을 활용하여 OneToMany 로직을 서비스 계층에서 구현하는 것을 공유하고자 한다.
사용된 Entity 는 앞서 소개한 게시글과 동일하게 사용하였다. (Order 와 Member - OneToMany 관계)
변경된 OrderRepository 만 소개하도록 하겠다.
OrderRepository
public interface OrderRepository extends ReactiveCrudRepository<Order, Long>, CustomOrderRepository {
@Query("select orders.* from orders where member_id in (:memberIds)")
Flux<Order> findAllWithMembers(Set<Long> memberIds);
}
지난번과 다르게 @Query 어노테이션을 통해 작성된 메서드가 추가되었다.
해당 메서드를 살펴보면 필요한 Member ID 들을 컬렉션으로 모아 IN 절의 파라미터로 넘기는 방식이다.
그렇다면 Member 는 어떤 식으로 데이터를 가져오는지 살펴보도록 하겠다.
MemberDomainService
@Service
@RequiredArgsConstructor
public class MemberDomainService {
private final MemberRepository memberRepository;
private final OrderRepository orderRepository;
@Transactional(readOnly = true)
public Flux<Member> findAllMembers() {
return memberRepository.findAll()
.collectMap(Member::getId)
.flatMapMany(members -> orderRepository.findAllWithMembers(members.keySet())
.bufferUntilChanged(Order::getMember)
.map(orders -> members.get(orders.get(0).getMember()).update(orders)));
}
}
로직은 다음과 같이 수행되어진다.
Member 를 조회하는 서비스 로직에서 1차적으로 필요한 Member 를 모두 SELECT 한 뒤 해당 Member 들의 ID 를 모아, 각 Member 를 참조하는 Order 를 WHERE IN 문을 활용하여 받아와 Member 에 추가해준다.
Member
@Table
@Getter
@Builder
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class Member {
@Id
@Column("member_id")
private Long id;
@Column("member_name")
private String name;
@Column("member_role")
private Roles roles;
@Transient
private List<Order> orders;
public Member update(List<Order> orders) {
this.orders = orders;
return this;
}
}
자신에게 필요한 Order 리스트를 업데이트한 후 변경된 자신을 반환할 수 있도록 구성하였다.
Test Code
@SpringBootTest
class MemberDomainServiceTest {
@Autowired MemberDomainService domainService;
@DisplayName("전체 멤버 조회 with 주문 (WHERE IN)")
@Test
void findAllMembersTest() {
// given
// when
List<Member> members = domainService.findAllMembers()
.collectList()
.block();
// then
assertNotNull(members);
members.forEach(member -> System.out.println("member = " + member));
}
}
테스트 결과
각 Member 에 필요한 Orders(주문 리스트) 가 같이 조회되는 것을 확인할 수 있다.
결론적으로, 지난번 소개한 방식과 결과만 보았을 땐 동일하지만, 실제 동작하는 원리는 다르다.
지난번은 데이터가 뻥튀기 되더라도 JOIN 하여 쿼리 한번에 받아서 처리를 하였지만, 이번 로직은 쿼리를 2번 요청하여 필요한 데이터를 받아 처리를 하였다.
ManyToOne 관계라면 JOIN 을 사용하는게 맞지만 OneToMany 에서의 JOIN 은 불필요한 데이터 중복이 일어나기에, 개인적으론 해당 방식이 쿼리를 두번 사용하더라도 OneToMany 에 보다 더 적합한 방식이라 생각한다.
그리고 가장 크게 마음에 들었던 부분은 정말 정말 귀찮은 필드 맵핑 로직을 따로 작성해주지 않은 점이다...
※ 주니어 개발자이기에 부족한 점이 많습니다. 틀린 부분이나 보충할 내용에 대해 공유주시면 감사하겠습니다.
'Spring' 카테고리의 다른 글
Spring WebFlux Error Handling (2) | 2021.04.12 |
---|---|
Spring Data R2DBC 연관관계 구현 - ManyToOne (0) | 2021.04.02 |
Spring Data R2DBC 연관관계 구현 - OneToMany (3) | 2021.03.28 |
Spring Data R2DBC 사용 (0) | 2021.03.28 |
Custom Validation 사용해보기 (0) | 2021.03.20 |
댓글