Post

MSA 구현하기(7) - Aggregation 적용해서 마이페이지 구성하기 (feat. API Gateway)

Skala 과정에서 마이크로 서비스 아키텍처 구조에 대해 새롭게 알게 되었습니다.
배운 것을 실제로 구현해보기 위해 이 여정을 시작하기로 했습니다.
처음 글부터 보러가기

이 글을 쓰기까지 정말정말 오랜 시간이 걸렸습니다….ㅜㅜ진작 REST API로 할 걸 괜히 GraphQL 하겠다고 설쳐서는

본격적인 Aggregation 기능들을 사용하기에 앞서 기본적으로 Gateway 서버에서 동기, 비동기 방식을 이용해서 마이크로 서비스와 통신하고, 반환 받은 값을 융합해서 새로운 형태로 반환하는 방법에 대해 알아보겠습니다!

마이페이지 기능

저는 마이페이지가 가장 Aggregation을 적용해보기 좋은 기능이라고 생각했습니다.

이전 글의 기능명세서에도 작성되어 있지만,
마이페이지에는 다음과 같은 기능이 필요합니다.

  1. 최근 주문한 상품 보기
  • 로그인 된 유저의 정보를 기반으로 주문 목록과 상품 정보를 목록으로 보여줘야함
  1. 주문 횟수
  • 로그인 된 유저의 정보를 기반으로 총 주문 목록을 가져와 개수를 보여줘야함
  1. 배송 중인 주문 내역 확인하기
  • 로그인 된 유저의 정보를 기반으로 아직 ‘배송중’ 상태인 주문내역의 상품 목록을 보여줘야함

추가적으로 지금까지 할인받은 총액까지 보여주고 싶다는 욕심..이 있습니다 ㅎㅎ

우선 1번 기능만 이번 글에서 다뤄보겠습니다!

로직 구상하기

  1. 로그인 되어있는 사용자의 정보가 필요하기 때문에 User Server에서 토큰 검증을 수행합니다.
  2. 토큰 검증 이후에 반환 받은 사용자 정보로 Order Server에서 해당 사용자의 주문 목록을 불러옵니다.
  3. 주문 목록에 있는 상품 ID로 Item Server에서 상품 정보를 반환 받아 리스트로 저장합니다.

따라서 Aggregation이 필요합니다!!!

실제 구현하기

AggregationController.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
@RestController
@Slf4j
@RequiredArgsConstructor
public class AggregationController {
    private final UserServiceClient userServiceClient;
    private final OrderServiceClient orderServiceClient;
    private final ItemServiceClient itemServiceClient;

    @GetMapping("/my-page")
    public Flux<ItemResponse> getMyPage(ServerWebExchange e) {

        // 사용자 인증
        ValidTokenResponse response = userServiceClient.tokenValidation(e);

        log.info("User Info: {}", response.getId());
        log.info("{}", response.getRole());
        log.info("{}", response.getUsername());

        // 로그인 된 사용자의 주문 내역 불러오기
        Flux<OrderResponse> orderResponses = orderServiceClient.getPurchaseList(response.getId());

        // 주문 목록에서 상품 ID만 추출 후, 상품 정보 조회
        return orderResponses
                .flatMap(order -> Flux.fromIterable(order.getOrderItemResponses()))
                .map(OrderItemResponse::getItemId)
                .distinct() // 중복된 아이템 제거
                .flatMap(itemServiceClient::getItem); // 각 아이템 정보 조회
    }
}

ServerWebExhange 로 Header에 포함되어 있는 JWT 토큰을 가져옵니다.

토큰이 유효한지 검사하고, 사용자의 정보를 받아오면 이 사용자의 주문 목록을 불러올 수 있습니다.

return 부분이 가장 중요한 것 같습니다.

불러온 주문 목록이 Mono 이기 때문에 stream()이나 forEach() 로는 리스트의 요소를 순환할 수 없어서 .flatMapMany(Flux::_fromIterable_) 을 사용합니다.

이후 각 주문 내역에서 상품 정보만 가져오고, 여기서 상품의 ID만 추출한 이후에 중복된 값을 제거합니다.

이렇게 가져온 상품 Id로 각각 상품 정보를 조회하여 받은 반환값을 List의 형태로 반환합니다.

HttpServletRequest 도 Header 정보를 포함하고 있는 것으로 알고 있었는데, 여기서는 왜 사용하지 않냐고 GPT에게 물어보니..

Q: ServerWebExchange말고 HttpServletRequest를 쓰면 헤더 추출을 못해?

A: 네, Spring WebFlux 에서는 HttpServletRequest를 사용할 수 없습니다.
이유는 WebFlux가 비동기 논블로킹(Non-blocking) 환경을 기반으로 동작하며, 서블릿 기반의 HttpServletRequest는 블로킹 방식이기 때문입니다.

현재 서버가 WebFlux(비동기) 기반으로 동작하고 있기 때문에 사용할 수 없는 것 같습니다!

저와 같은 의문을 가지신 분에게 도움이 되길 바랍니다,,ㅎㅎ


UserServiceClient.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
@Service
@Slf4j
@RequiredArgsConstructor
public class UserServiceClient {
    private final WebClient.Builder webClientBuilder;

    ...

    public ValidTokenResponse tokenValidation(ServerWebExchange exchange) {
        String header = exchange.getRequest().getHeaders().getFirst(HttpHeaders.AUTHORIZATION);

        log.info("Header: {}", header);

        if (header == null || !header.startsWith("Bearer ")) {
            throw new InvalidTokenException();
        }

        return webClientBuilder.build()
                .get()
                .uri("http://USER-SERVICE/validation")
                .header(HttpHeaders.AUTHORIZATION, header)
                .retrieve()
                .onStatus(HttpStatusCode::is4xxClientError, clientResponse -> {
                    throw new InvalidTokenException();
                })
                .bodyToMono(ValidTokenResponse.class)
                .block();
    }
}

요기서는 Header에서 정상적인 Bearer Token을 보냈을 때와 그렇지 않았을 때를 대비해서 예외처리를 해줬습니다.

WebClient 를 실행하던 중에 에러가 발생했을 때, onStatus 를 활용해서 상태 코드 별로 예외처리를 해줄 수도 있고, doOnError 를 통해 어떤 에러든 에러가 발생했을 때 예외처리를 해줄 수 있습니다.

throw new InvalidTokenException() 부분은 제가 커스텀 에러를 적용한 부분이라,
RuntimeException() 이나 IllegalArgumentException() 을 사용하시면 됩니다

추후에 커스텀 에러에 대한 부분을 다뤄보겠습니다


OrderServiceClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
@Slf4j
@RequiredArgsConstructor
public class OrderServiceClient {

    private final WebClient.Builder webClientBuilder;

    ...

    public Flux<OrderResponse> getPurchaseList(Long customerId) {
        return webClientBuilder.build()
                .get()
                .uri("http://ORDER-SERVICE/list/{customerId}", customerId)
                .retrieve()
                .bodyToFlux(OrderResponse.class);
    }
}

이 코드를 구현하면서 FluxMono<List> 의 차이점에 대해서도 궁금해서 GPT에게 물어봤습니다.

Q. Flux와 Mono<List> 의 차이점이 뭐야?

A.

🚀 결론

  • Flux → 데이터가 많고 스트리밍 방식으로 처리해야 할 때 사용
  • Mono<List> → 모든 데이터를 한 번에 리스트로 변환해서 반환할 때 사용

📌 정리하면?

  • “한 개씩 비동기 스트리밍” ➝ Flux
  • “한꺼번에 모아서 반환” ➝ Mono<List>

어떤 상황에서 사용할지 고민된다면 데이터 양과 비동기 처리가 필요한지를 고려해서 선택하면 됩니다! 🚀

저는 이왕 WebFlux 기반으로 동작하는 김에, 비동기식으로 처리하기로 했습니다 ㅎㅎ

뭔가 문제가 있다면 나중에 해결해보겠습니다!


ItemServiceClient.java

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Service
@Slf4j
@RequiredArgsConstructor
public class ItemServiceClient {

    private final WebClient.Builder webClientBuilder;

    ...

    public Mono<ItemResponse> getItem(Long itemId) {
        return webClientBuilder.build()
                .get()
                .uri("http://ITEM-SERVICE/{itemId}", itemId)
                .retrieve()
                .bodyToMono(ItemResponse.class);
    }
}

.
.
.

그러면, 구현 완료입니다 (!!!!!)


실행 결과

MyPage-Result MyPage-Result

정상적으로 수행되는 모습을 확인할 수 있습니다.

마치며

REST API도 GraphQL 못지않게 편합니다. (ㅠㅠ)

다음은 커스텀 에러 코드 처리에 대해 다뤄보겠습니다!

👩🏻‍💻✨



참고 자료

하나, , , , 다섯, 여섯

This post is licensed under CC BY 4.0 by the author.