[Spring] Service Instance 간의 통신

배창민·2025년 11월 4일
post-thumbnail

MSA 구조에서의 인증 로직과 서비스 간 통신 정리

MSA에서는 서비스가 독립적이기 때문에
“어디서 인증을 하고, 각 서비스는 무엇을 믿을 것인가”를 먼저 정하는 게 중요하다.


1. MSA에서 인증 처리 전략

1-1. 재검증 방식 (각 서비스에서 JWT 재검증)

각 모듈(서비스)이 클라이언트가 보낸 JWT 토큰을 직접 검증하고, 그 결과로 SecurityContext를 구성하는 방식이다.

  • 장점

    • 방어 심층(Defense in Depth)

      • API Gateway를 우회해서 직접 서비스로 요청이 들어와도, 서비스 자체에서 한 번 더 검증
    • 보안 완전성

      • 만료/변조 토큰을 각 서비스에서 바로 감지 가능
  • 단점

    • 코드 중복

      • 모든 서비스에 같은 JWT 검증 로직이 들어감
        → 공통 라이브러리로 뽑을 수는 있지만, 그래도 서비스마다 의존은 필요
    • 성능 부하

      • 트래픽이 많은 서비스에서 매 요청마다 JWT 전체를 검증하면 부하 증가
  • 요약
    보안을 최우선으로 볼 때 적합하지만, 코드·성능 비용을 감수해야 하는 전략


1-2. 신뢰 기반 방식 (Gateway 검증 + 헤더 전달)

API Gateway에서 JWT 유효성 검증을 먼저 수행하고,
사용자 핵심 정보만 헤더로 실어 내부 서비스로 넘기는 방식이다.

예:
X-User-Id, X-User-Role 등을 헤더에 추가해서 전달

  • 장점

    • 코드 중복 감소

      • JWT 파싱/검증은 Gateway에서 한 번만
      • 각 서비스는 헤더만 보고 SecurityContext 구성
    • 일관된 보안 모델

      • 모든 서비스가 동일한 방식으로 헤더 기반 인증 정보를 사용
    • 성능 이점

      • 토큰 전체 검증은 한 번만 → 내부 서비스는 가볍게 동작
  • 단점 / 주의점

    • Gateway가 단일 신뢰 지점이 됨

      • Gateway 검증 로직에 버그가 있으면 모든 서비스가 영향
      • Gateway 보안·모니터링이 매우 중요
    • 내부 서비스는 “헤더를 기반으로 한 Pre-authentication” 필터 정도는 유지하는게 좋음

  • 요약
    실무에서 가장 많이 쓰이는 패턴
    → 인증은 Gateway 집중, 서비스는 전달된 정보로 인가에 집중


2. 인증·인가 책임 분리

2-1. API Gateway (Authentication 중심)

  • 역할

    • 외부 요청의 진입점
    • JWT 토큰 유효성 검증 (서명, 만료, 클레임 등)
    • 사용자 정보 추출 (ID, Role 등) 후 헤더에 삽입
    • 공통 보안 정책(차단, 레이트 리밋 등) 적용
  • 구현 포인트

    • 전역 필터(GlobalFilter 또는 WebFilter)에서 JWT 검증
    • 검증 성공 시, 헤더에 X-User-* 형태로 정보 추가
    • 실패 시 즉시 401/403 응답

2-2. 내부 서비스 (Authorization 중심)

  • 역할

    • Gateway가 전달한 헤더 기반으로 SecurityContext 구성

    • HeaderAuthenticationFilter 같은 커스텀 필터에서

      • 헤더 읽기 → PreAuthenticatedAuthenticationToken 생성 → SecurityContext에 저장
    • 서비스별 인가 로직 적용 (메소드/엔드포인트 단위)

  • 구현 포인트

    • @PreAuthorize("hasRole('ADMIN')") 같은 메소드 보안 활용
    • “누가” 요청했는지, 어떤 “권한/역할”인지에 따라 접근 제어

3. 서비스 인스턴스 간 통신

MSA에서는 서비스끼리도 서로 호출한다. Spring Cloud에서는 대표적으로 세 가지 방법을 쓴다.


3-1. DiscoveryClient를 직접 사용하는 방식

3-1-1. DiscoveryClient 개념

  • Eureka 같은 서비스 레지스트리에 등록된 인스턴스 목록을 직접 조회하는 가장 저수준 API
  • discoveryClient.getInstances("order-service")
    → 해당 서비스의 인스턴스 리스트 반환

3-1-2. 예제

@Component
public class OrderDiscoveryClient {

    @Autowired
    private DiscoveryClient discoveryClient;

    public Order getOrder(String orderId) {
        RestTemplate restTemplate = new RestTemplate();
        List<ServiceInstance> instances =
            discoveryClient.getInstances("order-service");

        if (instances.isEmpty()) return null;

        String serviceUri = String.format(
            "%s/v1/order/%s",
            instances.get(0).getUri().toString(),
            orderId
        );

        ResponseEntity<Order> restExchange =
            restTemplate.exchange(
                serviceUri,
                HttpMethod.GET,
                null,
                Order.class,
                orderId
            );

        return restExchange.getBody();
    }
}
  • ServiceInstance에 호스트, 포트, URI 정보가 들어있다.
  • RestTemplate으로 직접 호출

3-1-3. 단점

  • 호출 대상 인스턴스 선택을 직접 해야 함
  • URL 생성 로직이 서비스 코드에 섞여서 복잡해짐
  • 스프링 클라우드의 클라이언트 로드 밸런서를 제대로 활용하지 못함

→ 교육용/실험용에는 괜찮지만, 실무에선 잘 안 쓰는 패턴


3-2. @LoadBalanced RestTemplate 사용

3-2-1. 설정

@SpringBootApplication
public class UserApplication {

    public static void main(String[] args) {
        SpringApplication.run(UserApplication.class, args);
    }

    @LoadBalanced
    @Bean
    public RestTemplate getRestTemplate() {
        return new RestTemplate();
    }
}

3-2-2. 사용

@Component
public class OrderRestTemplateClient {

    @Autowired
    private RestTemplate restTemplate;

    public Order getOrder(String orderId) {
        ResponseEntity<Order> restExchange =
            restTemplate.exchange(
                "http://order-service/v1/order/{orderId}",
                HttpMethod.GET,
                null,
                Order.class,
                orderId
            );

        return restExchange.getBody();
    }
}
  • URL에서 http://order-service 는 실제 호스트/포트가 아니라 서비스 이름

  • @LoadBalanced가 붙은 RestTemplate

    • 서비스 이름 → Eureka에서 인스턴스 찾기
    • 클라이언트 로드 밸런서로 인스턴스 선택 (라운드 로빈 등)

3-2-3. 특징

  • 서비스 위치/포트가 코드에서 완전히 추상화됨
  • 로드 밸런싱 자동 처리
  • RestTemplate이지만 “서비스 이름 기반 호출” 지원

3-3. Feign 클라이언트 사용 (선언형 HTTP 클라이언트)

3-3-1. Feign이란?

  • 인터페이스만 선언하면, 구현 없이 REST 호출 코드를 자동으로 만들어주는 선언형 HTTP 클라이언트
  • Eureka + Load Balancer와 자연스럽게 통합됨
  • Controller 메서드 정의와 거의 비슷한 방식으로 작성

3-3-2. 예제

@FeignClient(name = "order-service") // Eureka 서비스 이름
public interface OrderFeignClient {

    @RequestMapping(
        method = RequestMethod.GET,
        value = "/v1/orders/{orderId}"
    )
    Order getOrder(@PathVariable("orderId") String orderId);
}
  • @FeignClient(name = "order-service")

    • Eureka에 등록된 order-service로 호출
  • @RequestMapping 방식은 Spring MVC와 동일

  • 사용 예:

@Service
public class OrderService {

    private final OrderFeignClient orderFeignClient;

    public OrderService(OrderFeignClient orderFeignClient) {
        this.orderFeignClient = orderFeignClient;
    }

    public Order getOrder(String orderId) {
        return orderFeignClient.getOrder(orderId);
    }
}

3-3-3. 에러 핸들링

  • Feign 클라이언트 사용 시

    • 4xx / 5xx 응답 → FeignException으로 던져짐
  • 필요하면 커스텀 ErrorDecoder를 만들어
    HTTP 상태 코드 + 응답 바디를 파싱해서
    도메인에 맞는 예외로 매핑 가능


4. 정리

  • 인증 처리

    • 외부 요청에 대한 JWT 검증은 Gateway가 담당
    • 내부 서비스는 헤더 기반 Pre-auth로 SecurityContext 구성 후 인가에 집중
    • 보안을 더 강화하고 싶으면, 일부 핵심 서비스에서는 JWT 재검증 추가도 고려
  • 서비스 간 통신

    • 가장 단순: DiscoveryClient + RestTemplate (실무 비추천)
    • 많이 쓰는 패턴: @LoadBalanced RestTemplate
    • 더 선언적으로 쓰고 싶으면: Feign 클라이언트

MSA에서는 “어디서 인증을 하고, 서비스끼리는 어떻게 신뢰할 것인가”와
서비스 간 통신을 어떤 레벨의 추상화로 가져갈 것인가”를 먼저 정해두면
전체 구조가 훨씬 깔끔하게 정리된다.

profile
개발자 희망자

0개의 댓글