Spring/MSA

MSA 환경에서 FeignClient 응답 데이터가 누락된 이유는? – CommonResponse<DTO>와 변수 범위 이슈

챛채 2025. 3. 24. 23:45

1. 문제발생

이번에 개발한 기능은 Order-Service에서 주문 생성 시 Stock-Service와 연동하여 재고를 차감하고 이후 Delivery-Service에 배송 생성을 요청하는 전체 흐름이다.

이 과정에서 예상치 못한 문제가 발생했다. 분명 Stock-Service에서 hubId, price를 잘 반환하고 있는데도 OrderService에서 해당 값이 null, 0으로 들어가는 버그가 발생한 것이다. 디버깅 로그를 찍어보면 다음과 같았다.

INFO OrderService - stockResponse.hubId = null
INFO OrderService - totalPrice = 0

 

하지만 DB에서는 정상적으로 hubId와 price가 존재하는 것을 확인할 수 있었다. 그럼 대체 문제는 어디에 있었을까?

 

2. 구조 요약

Order-Service → Stock-Service

  • FeignClient를 이용해 checkAndDecreaseStock() 호출
  • StockService는 다음과 같은 DTO를 반환한다.
@Getter
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class StockCheckResponse {
	private UUID productId;
	private UUID hubId;
	private int price;
}

이 응답을 CommonResponse로 감싸서 다음과 같이 반환

return CommonResponse.OK(stockCheckResponse, "성공");

 

3. 문제 상황

[초기 OrderService의 코드]

StockCheckResponse stockResponse = null;
try {
    CommonResponse<StockCheckResponse> response = stockClient.checkAndDecreaseStock(request);
    StockCheckResponse stockResponse = response.getData(); // X 여기서 stockResponse는 새로운 지역변수
    ...
} catch (Exception e) {
    ...
}

바로 이 부분! stockResponse를 새로운 지역 변수로 다시 선언(StockCheckResponse stockResponse = ...)하면서 클래스 최상단에 있던 stockResponse는 여전히 null 상태로 남게 된다.

결국 이후에 사용하는 값은 null → hubId = null, totalPrice = 0으로 저장되는 것이다.

4. 해결 방법

해결은 간단했다. 지역 변수로 다시 선언하지 않고 이미 선언된 stockResponse에 대입만 하면 된다.

[수정된 코드]

StockCheckResponse stockResponse = null;
try {
    CommonResponse<StockCheckResponse> response = stockClient.checkAndDecreaseStock(stockRequest);
    stockResponse = response.getData(); // 선언 없이 대입만! 메서드 스코프 변수에 제대로 할당
} catch (Exception e) {
    ...
}

이제 stockResponse.getHubId()와 stockResponse.getPrice()가 정상적으로 값을 가진다.

 

5. FeignClient에서 CommonResponse<DTO>를 써야 할까?

이번 이슈를 통해 알 수 있었던 또 하나의 교훈은 FeignClient에서 DTO를 감싸는 방식의 통일이다.

@FeignClient(name = "stock-service", url = "http://localhost:8082")
public interface StockClient {
    @PutMapping("/api/stocks/check")
    CommonResponse<StockCheckResponse> checkAndDecreaseStock(@RequestBody DecreaseStockRequest request);
}

Stock-Service는 항상 응답을 CommonResponse<T>로 감싸도록 구성되어 있기 때문에 FeignClient에서도 이를 반영해야 한다. 만약 StockCheckResponse만 받으려고 하면 직렬화 오류나 null 값 이슈가 발생할 수 있다.

 

6. 마무리

 

  • Java에서 같은 이름의 변수를 블록 안에서 다시 선언하면 바깥 변수와는 전혀 다른 메모리 공간에 할당된다. → 주의!
  • MSA에서 서비스 간 통신은 응답 구조를 동일하게 맞추는 것이 굉장히 중요하다.
  • FeignClient의 응답을 CommonResponse<T>로 감싸는 구조라면, 모든 서비스에서 이를 표준처럼 맞추자.
  • 데이터가 null로 들어오면 바로 의심할 부분은
    1. JSON 응답 구조와 DTO 매핑이 일치하는가?
    2. 변수 범위가 꼬인 것은 아닌가?
    3. FeignClient가 올바른 타입으로 받고 있는가?

이번 이슈는 단순한 변수 스코프 문제였지만 MSA 환경에서의 연동과 응답 표준화가 얼마나 중요한지를 다시 느끼게 해줬다.