MSA 구현하기(11) - 마이크로 서비스에도 WebFlux 적용하기 (3) (feat. 트러블슈팅)
Skala 과정에서 마이크로 서비스 아키텍처 구조에 대해 새롭게 알게 되었습니다.
배운 것을 실제로 구현해보기 위해 이 여정을 시작하기로 했습니다.
처음 글부터 보러가기
R2DBC가 생각보다 불친절한 드라이버였습니다,,
JDBC가 지원해주던 것을 지원해주지 않는 경우가 굉장히 많아서
머리가 다 뽑힐 뻔 했지만 해결했습니다 😋
SecurityConfig.java
우선 User 서버에서는 JWT를 사용하고 있었기에 POST 가 정상적으로 동작했었는데
같은 코드를 Order 서버에 사용하니 An expected CSRF token cannot be found 에러가 발생했습니다.
사실 User 서버에서도 잘못된 코드를 작성했던 것이었는데 JWT가 CSRF 공격 방어 기능을 사용하지 않기 때문에 에러가 발생하지 않았던 것이고
지금 서버에서는 JWT 토큰을 사용하지 않아서 오류가 발생했습니다,,
1
2
3
4
5
6
7
8
9
.
.
.
return http
.csrf(ServerHttpSecurity.CsrfSpec::disable)
.
.
.
.build();
WebFlux에서는 이렇게 설정해주어야 정상적으로 CSRF 방어 기능을 해제할 수 있습니다!
R2DBC에서 Primary Key 수동으로 설정하기
기존에 주문 번호를 Primary Key로 사용하고 있었기 때문에
DB에서 자동으로 생성해주는 ID 대신 주문번호를 생성하는 로직을 따로 두어서 ID를 수동으로 설정해두고 있었는데
R2DBC에서는 ID 값이 있는 상태로 repository.save() 메소드가 실행되면 insert 가 아닌, update 로 실행된다는 것을..
너무 늦게 깨달았습니다..
처음에는 비동기식으로 DB에 주문을 저장하기 때문에 Orders가 저장되기 이전에 OrderItem이 먼저 저장되어 orders.getId()가 제대로 실행되지 않는 줄 알았는데(아예 틀린 가설은 아니었습니다,,)
그것보다 ID를 설정해줬기 때문에 update 쿼리가 실행되는데 당연히 새로 생성한 ID에 맞는 row가 없었기에..항목을 찾지 못했다는 에러가 발생한 것이었습니다..
Orders.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Table
@Getter
@AllArgsConstructor
@NoArgsConstructor
public class Orders implements Persistable<String> {
@Id
private String id;
private Long customerId;
private BigDecimal totalPrice;
private String address;
private String description;
@Transient
private boolean isNew;
public Orders(Long customerId, BigDecimal totalPrice, String address, String description) {
this.id = generateOrderNumber();
this.customerId = customerId;
this.totalPrice = totalPrice;
this.address = address;
this.description = description;
this.isNew = true;
}
.
.
.
@Override
public boolean isNew() {
return isNew;
}
}
이를 해결하기 위해서는 2가지 방법이 있는데
Repository 클래스에서 직접 @Query 어노테이션을 통해 insert 쿼리를 작성하는 방법과,
제가 사용한 Persistable 을 상속받아 해당 row가 새로 생성된 row인지를 알려주는 isNew() 를 오버라이딩 하는 방법입니다.
Persistable<String> 에서 String을 작성해준 이유는 단순하게 제 ID가 String으로 선언되어 있기 때문입니다
상황에 따라서 적절하게 사용해주세요!
OrderItem도 동일한 방식으로 바꿔주었습니다.
일부 로직을 동기식으로 동작하도록 변경
주문을 저장하는 Request의 구조가
1
2
3
4
5
6
public class PurchaseRequest {
private String address;
private String description;
private List<PurchaseItemRequest> itemList;
private BigDecimal totalPrice;
}
이렇게 되어 있고,
주문내역을 저장하기 위해 다음 로직이 수행됩니다.
- 주문 정보(Orders)를 먼저 DB에 저장하고
- 저장된 주문의 Id를 각 세부 주문 항목(OrderItem)에 포함시켜 DB에 저장합니다.
- DTO로 주분 정보와 세부 주문 항목을 주문 내역으로 정리해서 반환합니다.
이때, 모든 로직이 비동기식으로 수행했더니 OrderItem이 정상적으로 저장되지 않는 문제가 발생했습니다
OrderServices.java
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
.
.
.
public Mono<OrderResponse> register(PurchaseRequest request, Long userId) {
List<PurchaseItemRequest> itemRequests = request.getItemList();
Orders orders = new Orders(
userId,
request.getTotalPrice(),
request.getAddress(),
request.getDescription()
);
return orderRepository.save(orders)
.flatMap(order ->
registerItem(itemRequests, order)
.then(getOrderResponse(order))
);
}
private Mono<Void> registerItem(List<PurchaseItemRequest> requests,
Orders order) {
return Flux.fromIterable(requests)
.flatMap(request -> {
OrderItem orderItem = new OrderItem(
order.getId(),
request.getItemId(),
request.getSellerId(),
request.getName(),
request.getQuantity(),
request.getPrice()
);
return orderItemRepository.save(orderItem)
.doOnSuccess(kafkaProducer::sendDbUpdateMessage);
})
.then();
}
private Mono<OrderResponse> getOrderResponse(Orders order) {
return orderItemRepository.findByOrderId(order.getId())
.map(this::getOrderItemResponse)
.collectList()
.map(orderItems ->
new OrderResponse(
order.getId(),
order.getCreatedAt(),
orderItems,
order.getAddress(),
order.getDescription(),
order.getTotalPrice()
)
);
}
.
.
.
DB에 저장되는 부분만 일부 동기식(체인)으로 동작하도록 설정해주었습니다.
- Orders가 정상적으로 DB에 저장된다면
registerItem()함수를 호출합니다registerItem()함수가 완료되었다면getOrderResponse()함수를 통해 DTO로 주문 내역을 반환합니다.
그 결과 주문이 정상적으로 DB에 저장되었습니다!
정말 별거 아닌 것 같지만…도대체 어디가 문제인지 찾는 것이 엄청나게 힘들었습니다.
분명 비동기식으로 동작하던 로직 때문에 Orders가 제대로 저장되지 않아서 발생한 오류인 줄 알았는데
1
2
3
4
5
6
orderRepository.save(orders)
.doOnNext(order -> log.info("Order saved! Id: {}", order.getId()))
.flatMap(order ->
registerItem(itemRequests, order)
.then(getOrderResponse(order))
);
이 로그를 찍어보고…나서야….그게 문제가 아니라는 것을……DB에 저장조차 되고 있지 않다는 사실을 깨달았습니다…..
🥹
마치며
WebFlux도, R2DBC도 처음 다뤄보다보니 오류를 맞닥뜨렸을 때 어디서부터 어떻게 무엇이 잘못되었는지를 가늠하기가 조금 어려웠던 것 같습니다..
그래도 이제 10%는 이해하게 된 것 같아서 뿌듯합니다!
😵💫🐣
참고 자료