콘텐츠 대표 이미지 - ☕ 자바 마이크로서비스와 Spring Cloud Netflix OSS 활용

☕ 자바 마이크로서비스와 Spring Cloud Netflix OSS 활용

복잡한 모놀리식 애플리케이션을 작은 서비스로 쪼개는 마법 같은 이야기 🚀

🎯 이 글에서 다룰 내용 요즘 IT 업계에서 마이크로서비스 아키텍처(MSA)는 거의 필수 키워드가 됐어. 특히 자바 진영에서는 Spring Cloud Netflix OSS를 활용한 마이크로서비스 구축이 대세지! 이 글에서는 마이크로서비스가 뭔지부터 시작해서, Spring Cloud Netflix OSS의 핵심 컴포넌트들을 하나씩 파헤쳐볼 거야. 실무에서 바로 써먹을 수 있는 팩트 위주로 친구처럼 편하게 설명해줄게! 😊
Microservices Architecture User Service Order Service Payment Service Spring Cloud Netflix OSS Eureka - Service Discovery Ribbon - Load Balancing Hystrix - Circuit Breaker Zuul - API Gateway

🏗️ 마이크로서비스 아키텍처란 무엇인가?

모놀리식 vs 마이크로서비스

옛날 옛적에는(사실 그렇게 오래되진 않았지만 😅) 모든 기능을 하나의 거대한 애플리케이션에 때려 넣는 모놀리식 아키텍처가 대세였어. 회원 관리, 주문 처리, 결제, 배송 추적까지 전부 하나의 프로젝트 안에 들어있는 거지.

이게 처음엔 괜찮아 보여. 개발도 간단하고, 배포도 한 번에 되니까. 근데 서비스가 커지면서 문제가 생기기 시작해:

모놀리식의 문제점들 🤦‍♂️
• 코드베이스가 너무 커져서 이해하기 어려워짐
• 작은 기능 하나 수정해도 전체를 다시 배포해야 함
• 특정 기능에만 트래픽이 몰려도 전체 서버를 스케일 아웃해야 함
• 한 부분에 버그가 생기면 전체 시스템이 다운될 수 있음
• 새로운 기술 스택 도입이 거의 불가능함

그래서 등장한 게 마이크로서비스 아키텍처(MSA)야! 큰 애플리케이션을 작은 서비스들로 쪼개는 거지. 각 서비스는 독립적으로 개발, 배포, 확장할 수 있어.
💡 마이크로서비스의 핵심 특징 1. 서비스 독립성
각 서비스는 자체 데이터베이스를 가지고, 독립적으로 배포 가능해. 회원 서비스를 업데이트해도 주문 서비스는 영향을 받지 않아.

2. 기술 다양성
서비스마다 다른 기술 스택을 쓸 수 있어. A 서비스는 Java + MySQL, B 서비스는 Node.js + MongoDB 이런 식으로!

3. 탄력적 확장
트래픽이 많은 서비스만 선택적으로 스케일 아웃할 수 있어. 비용 효율적이지! 💰

4. 장애 격리
한 서비스에 문제가 생겨도 다른 서비스는 정상 작동해. 전체 시스템이 다운되는 일이 줄어들어.
⚠️ 하지만 주의할 점! 마이크로서비스가 만능은 아니야. 서비스 간 통신 오버헤드, 분산 트랜잭션 처리, 모니터링 복잡도 증가 등 새로운 문제들이 생겨. 그래서 이런 문제들을 해결하기 위한 도구들이 필요한데, 그게 바로 Spring Cloud Netflix OSS야!

🌟 Spring Cloud Netflix OSS 소개

Netflix는 전 세계에서 가장 큰 스트리밍 서비스 중 하나잖아? 이들은 엄청난 트래픽을 처리하면서 마이크로서비스 아키텍처를 성공적으로 운영하고 있어. 그리고 자신들이 개발한 도구들을 오픈소스로 공개했는데, 이게 바로 Netflix OSS야! 🎬

Spring 팀은 이 Netflix OSS 컴포넌트들을 Spring 생태계에 통합해서 Spring Cloud Netflix라는 프로젝트를 만들었어. 덕분에 우리는 Spring Boot 애플리케이션에서 간단한 설정만으로 Netflix의 검증된 도구들을 사용할 수 있게 됐지!
✨ Spring Cloud Netflix의 주요 컴포넌트 • Eureka: 서비스 디스커버리 (서비스 레지스트리)
• Ribbon: 클라이언트 사이드 로드 밸런싱
• Hystrix: 서킷 브레이커 패턴 구현
• Zuul: API 게이트웨이
• Feign: 선언적 HTTP 클라이언트

이제 각 컴포넌트를 하나씩 자세히 알아보자! 🚀

🔍 Eureka - 서비스 디스커버리

왜 서비스 디스커버리가 필요할까?

마이크로서비스 환경에서는 서비스 인스턴스들이 동적으로 생성되고 사라져. 오토 스케일링으로 서버가 늘어나기도 하고, 장애로 인해 특정 인스턴스가 죽기도 하지. 이런 상황에서 서비스 A가 서비스 B를 호출하려면 어떻게 해야 할까? 🤔

하드코딩된 IP 주소를 쓸 수는 없어. 인스턴스가 바뀔 때마다 코드를 수정해야 하니까. 여기서 필요한 게 서비스 디스커버리야!

Eureka의 동작 원리

Eureka는 서비스 레지스트리 역할을 해. 모든 마이크로서비스는 시작할 때 Eureka 서버에 자신을 등록하고, 주기적으로 하트비트를 보내서 "나 살아있어!"라고 알려줘. 그리고 다른 서비스를 호출할 때는 Eureka에게 "B 서비스 어디 있어?"라고 물어보는 거지.
🛠️ Eureka Server 설정하기

먼저 Eureka Server를 만들어보자. Spring Boot 프로젝트를 생성하고 의존성을 추가해:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-server</artifactId>
</dependency>

메인 클래스에 @EnableEurekaServer 어노테이션을 붙여:

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {
    public static void main(String[] args) {
        SpringApplication.run(EurekaServerApplication.class, args);
    }
}

application.yml 설정:

server:
  port: 8761

eureka:
  client:
    register-with-eureka: false
    fetch-registry: false
  server:
    enable-self-preservation: false

이렇게 하면 Eureka Server가 8761 포트에서 실행돼. register-with-eureka와 fetch-registry를 false로 설정한 건 Eureka Server 자신은 클라이언트로 등록하지 않겠다는 의미야.
📱 Eureka Client 설정하기

이제 실제 마이크로서비스를 Eureka Client로 등록해보자:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-eureka-client</artifactId>
</dependency>

메인 클래스에 @EnableDiscoveryClient 추가:

@SpringBootApplication
@EnableDiscoveryClient
public class UserServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(UserServiceApplication.class, args);
    }
}

application.yml 설정:

spring:
  application:
    name: user-service

server:
  port: 8081

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/
  instance:
    prefer-ip-address: true

spring.application.name이 중요해! 이게 Eureka에 등록되는 서비스 이름이거든. 다른 서비스들은 이 이름으로 user-service를 찾게 돼.
💡 실무 팁 Eureka Server는 보통 고가용성을 위해 최소 2개 이상의 인스턴스를 클러스터로 구성해. 한 서버가 다운되어도 다른 서버가 서비스 레지스트리 역할을 계속할 수 있도록 말이야. 또한 eureka.server.enable-self-preservation는 개발 환경에서는 false로 설정하지만, 운영 환경에서는 true로 설정하는 게 좋아. 네트워크 문제로 일시적으로 하트비트가 안 오는 경우를 대비하기 위해서지.

⚖️ Ribbon - 클라이언트 사이드 로드 밸런싱

로드 밸런싱이 뭐길래?

서비스가 여러 인스턴스로 실행되고 있을 때, 요청을 어떻게 분산시킬까? 전통적인 방식은 서버 사이드 로드 밸런서(예: Nginx, HAProxy)를 두는 거야. 모든 요청이 로드 밸런서를 거쳐서 적절한 인스턴스로 전달되는 거지.

하지만 마이크로서비스 환경에서는 클라이언트 사이드 로드 밸런싱이 더 효율적이야. 각 클라이언트가 직접 로드 밸런싱 로직을 가지고 있어서, 중앙 집중식 로드 밸런서를 거치지 않고 바로 서비스 인스턴스에 요청을 보내는 거야. 이게 바로 Ribbon의 역할이야! 🎯
🔧 Ribbon 동작 방식

1. 클라이언트가 Eureka에서 서비스 인스턴스 목록을 가져옴
2. Ribbon이 로드 밸런싱 알고리즘(라운드 로빈, 가중치 기반 등)을 적용해서 인스턴스 선택
3. 선택된 인스턴스로 직접 HTTP 요청 전송
4. 주기적으로 Eureka에서 인스턴스 목록 업데이트

Ribbon 설정 예시

user-service:
  ribbon:
    NFLoadBalancerRuleClassName: com.netflix.loadbalancer.RoundRobinRule
    ConnectTimeout: 3000
    ReadTimeout: 3000
    MaxAutoRetries: 1
    MaxAutoRetriesNextServer: 1

• NFLoadBalancerRuleClassName: 로드 밸런싱 규칙 (라운드 로빈, 랜덤, 가용성 기반 등)
• ConnectTimeout: 연결 타임아웃 (밀리초)
• ReadTimeout: 읽기 타임아웃 (밀리초)
• MaxAutoRetries: 같은 인스턴스에 재시도 횟수
• MaxAutoRetriesNextServer: 다른 인스턴스로 재시도 횟수
🎨 RestTemplate과 Ribbon 함께 사용하기

Spring에서 Ribbon을 사용하는 가장 간단한 방법은 @LoadBalanced 어노테이션을 RestTemplate에 붙이는 거야:

@Configuration
public class RestTemplateConfig {
    
    @Bean
    @LoadBalanced
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }
}

이제 RestTemplate을 사용할 때 실제 IP 주소 대신 서비스 이름을 쓸 수 있어:

@Service
public class OrderService {
    
    @Autowired
    private RestTemplate restTemplate;
    
    public User getUserInfo(Long userId) {
        String url = "http://user-service/api/users/" + userId;
        return restTemplate.getForObject(url, User.class);
    }
}

보이지? http://user-service라고 서비스 이름을 썼어. Ribbon이 자동으로 Eureka에서 user-service의 인스턴스 목록을 가져와서 로드 밸런싱을 해줘! 😎
⚠️ 중요한 업데이트 사실 Ribbon은 현재 유지보수 모드(maintenance mode)에 들어갔어. Netflix가 더 이상 적극적으로 개발하지 않는다는 뜻이지. Spring Cloud는 Ribbon 대신 Spring Cloud LoadBalancer를 사용하도록 권장하고 있어. 하지만 많은 레거시 시스템에서 아직 Ribbon을 사용하고 있고, 개념 자체는 여전히 유효하니까 알아두는 게 좋아!

🔌 Hystrix - 서킷 브레이커 패턴

장애 전파를 막아라!

마이크로서비스 환경에서 가장 무서운 게 뭘까? 바로 장애 전파(Cascading Failure)야! 😱

상황을 상상해봐. 서비스 A가 서비스 B를 호출하고, B가 C를 호출하고, C가 D를 호출하는 구조야. 근데 D 서비스에 문제가 생겨서 응답이 느려졌어. 그럼 C도 느려지고, B도 느려지고, A도 느려져. 결국 전체 시스템이 마비되는 거지.

더 나쁜 건, 느린 응답을 기다리는 동안 스레드들이 블로킹되면서 리소스가 고갈된다는 거야. 이게 바로 서킷 브레이커 패턴이 필요한 이유야!
⚡ 서킷 브레이커란? 전기 회로의 차단기(Circuit Breaker)를 생각해봐. 과부하가 걸리면 자동으로 회로를 차단해서 화재를 방지하잖아? 소프트웨어의 서킷 브레이커도 비슷해!

서킷 브레이커의 3가지 상태

1. Closed (정상)
모든 요청이 정상적으로 통과해. 하지만 실패율을 계속 모니터링하고 있어.

2. Open (차단)
실패율이 임계값을 넘으면 서킷이 열려. 이제 요청을 실제 서비스로 보내지 않고 즉시 실패 처리하거나 폴백(fallback) 응답을 반환해. 이렇게 하면 장애가 있는 서비스에 부하를 주지 않고, 빠르게 실패해서 리소스를 절약할 수 있어.

3. Half-Open (반개방)
일정 시간이 지나면 서킷이 반개방 상태가 돼. 몇 개의 요청을 시험 삼아 보내보는 거야. 성공하면 다시 Closed로, 실패하면 다시 Open으로 돌아가.
🛡️ Hystrix 설정하기

의존성 추가:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix</artifactId>
</dependency>

메인 클래스에 @EnableCircuitBreaker 추가:

@SpringBootApplication
@EnableDiscoveryClient
@EnableCircuitBreaker
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

서비스 메서드에 @HystrixCommand 적용:

@Service
public class OrderService {
    
    @Autowired
    private RestTemplate restTemplate;
    
    @HystrixCommand(
        fallbackMethod = "getUserInfoFallback",
        commandProperties = {
            @HystrixProperty(name = "execution.isolation.thread.timeoutInMilliseconds", value = "3000"),
            @HystrixProperty(name = "circuitBreaker.requestVolumeThreshold", value = "10"),
            @HystrixProperty(name = "circuitBreaker.errorThresholdPercentage", value = "50"),
            @HystrixProperty(name = "circuitBreaker.sleepWindowInMilliseconds", value = "10000")
        }
    )
    public User getUserInfo(Long userId) {
        String url = "http://user-service/api/users/" + userId;
        return restTemplate.getForObject(url, User.class);
    }
    
    public User getUserInfoFallback(Long userId) {
        // 폴백 로직: 기본값 반환 또는 캐시된 데이터 사용
        User fallbackUser = new User();
        fallbackUser.setId(userId);
        fallbackUser.setName("Unknown User");
        return fallbackUser;
    }
}

주요 설정 설명:
• timeoutInMilliseconds: 타임아웃 시간 (3초)
• requestVolumeThreshold: 서킷을 열지 판단하기 위한 최소 요청 수 (10개)
• errorThresholdPercentage: 서킷을 열 실패율 임계값 (50%)
• sleepWindowInMilliseconds: Open 상태 유지 시간 (10초)
💡 폴백 전략 폴백 메서드는 원본 메서드와 같은 파라미터를 받아야 해. 폴백에서 할 수 있는 것들:

• 기본값 반환
• 캐시된 데이터 사용
• 다른 서비스 호출 (하지만 이것도 실패할 수 있으니 주의!)
• 에러 메시지 반환
• 빈 결과 반환

중요한 건 폴백 로직은 절대 실패하면 안 된다는 거야. 폴백의 폴백은 없거든! 😅
✨ Hystrix Dashboard

Hystrix는 실시간 모니터링 대시보드도 제공해. 각 서킷 브레이커의 상태, 요청 처리량, 에러율 등을 시각적으로 볼 수 있어. 운영 환경에서 정말 유용하지!

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-hystrix-dashboard</artifactId>
</dependency>

@EnableHystrixDashboard를 추가하고 http://localhost:port/hystrix에 접속하면 대시보드를 볼 수 있어!
⚠️ Hystrix도 유지보수 모드 Ribbon과 마찬가지로 Hystrix도 유지보수 모드에 들어갔어. Spring Cloud는 Resilience4j를 대안으로 권장하고 있어. Resilience4j는 더 가볍고 함수형 프로그래밍 스타일을 지원하는 현대적인 라이브러리야. 하지만 Hystrix의 개념과 패턴은 여전히 중요하니까 꼭 이해하고 넘어가자!

🚪 Zuul - API 게이트웨이

모든 요청의 관문

마이크로서비스가 10개, 20개로 늘어나면 클라이언트 입장에서는 혼란스러워져. 각 서비스마다 다른 엔드포인트를 알아야 하고, 인증도 각각 처리해야 하고... 😵

API 게이트웨이는 이런 문제를 해결해줘. 모든 클라이언트 요청이 게이트웨이를 거쳐서 적절한 마이크로서비스로 라우팅되는 거지. 일종의 "단일 진입점(Single Entry Point)"이야!
🎯 API 게이트웨이의 역할 1. 라우팅
요청 경로에 따라 적절한 마이크로서비스로 전달해. /api/users/**는 user-service로, /api/orders/**는 order-service로 보내는 식이지.

2. 인증 및 인가
모든 요청에 대해 중앙집중식으로 인증을 처리할 수 있어. JWT 토큰 검증 같은 걸 각 마이크로서비스에서 중복으로 구현할 필요가 없어지는 거야.

3. 로깅 및 모니터링
모든 요청이 게이트웨이를 거치니까 중앙에서 로깅하고 모니터링하기 쉬워.

4. Rate Limiting
특정 클라이언트의 요청 수를 제한해서 서비스를 보호할 수 있어.

5. CORS 처리
Cross-Origin 요청을 중앙에서 처리할 수 있어.

6. 응답 캐싱
자주 요청되는 데이터를 캐싱해서 성능을 향상시킬 수 있어.
🔧 Zuul 설정하기

의존성 추가:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-netflix-zuul</artifactId>
</dependency>

메인 클래스에 @EnableZuulProxy 추가:

@SpringBootApplication
@EnableZuulProxy
@EnableDiscoveryClient
public class ApiGatewayApplication {
    public static void main(String[] args) {
        SpringApplication.run(ApiGatewayApplication.class, args);
    }
}

application.yml 설정:

server:
  port: 8080

spring:
  application:
    name: api-gateway

eureka:
  client:
    service-url:
      defaultZone: http://localhost:8761/eureka/

zuul:
  routes:
    user-service:
      path: /api/users/**
      serviceId: user-service
      stripPrefix: false
    order-service:
      path: /api/orders/**
      serviceId: order-service
      stripPrefix: false
  sensitive-headers: Cookie,Set-Cookie

이제 클라이언트는 http://gateway:8080/api/users/123으로 요청하면, Zuul이 자동으로 user-service로 라우팅해줘!
🎨 Zuul Filter 만들기

Zuul의 진짜 파워는 필터(Filter)에 있어. 요청/응답 처리 과정에 커스텀 로직을 끼워 넣을 수 있거든!

Zuul Filter의 4가지 타입
• PRE: 라우팅 전에 실행 (인증, 로깅 등)
• ROUTING: 실제 라우팅 수행
• POST: 라우팅 후에 실행 (응답 수정, 로깅 등)
• ERROR: 에러 발생 시 실행

인증 필터 예시:

@Component
public class AuthenticationFilter extends ZuulFilter {
    
    @Override
    public String filterType() {
        return "pre";
    }
    
    @Override
    public int filterOrder() {
        return 1;
    }
    
    @Override
    public boolean shouldFilter() {
        return true;
    }
    
    @Override
    public Object run() throws ZuulException {
        RequestContext ctx = RequestContext.getCurrentContext();
        HttpServletRequest request = ctx.getRequest();
        
        String token = request.getHeader("Authorization");
        
        if (token == null || !isValidToken(token)) {
            ctx.setSendZuulResponse(false);
            ctx.setResponseStatusCode(HttpStatus.UNAUTHORIZED.value());
            ctx.setResponseBody("Unauthorized");
            return null;
        }
        
        return null;
    }
    
    private boolean isValidToken(String token) {
        // JWT 토큰 검증 로직
        return true;
    }
}

이 필터는 모든 요청에 대해 Authorization 헤더를 체크하고, 유효하지 않으면 401 에러를 반환해!
⚠️ Zuul도 유지보수 모드 맞아, Zuul도 유지보수 모드야. 😅 Netflix는 Zuul 2를 개발했지만, Spring Cloud는 이를 통합하지 않기로 결정했어. 대신 Spring Cloud Gateway를 개발했지. Spring Cloud Gateway는 Spring 5, Spring Boot 2, Project Reactor를 기반으로 하는 완전히 비동기 논블로킹 게이트웨이야. 성능도 더 좋고 현대적인 리액티브 프로그래밍 모델을 지원해!

📞 Feign - 선언적 HTTP 클라이언트

REST API 호출을 더 쉽게

RestTemplate로 다른 서비스를 호출하는 건 좀 번거로워. URL 문자열을 만들고, HTTP 메서드를 지정하고, 응답을 파싱하고... 코드가 장황해지기 쉽지.

Feign은 이런 문제를 해결해줘. 인터페이스만 정의하면 나머지는 Feign이 알아서 처리해주는 거야! 마치 로컬 메서드를 호출하는 것처럼 간단하게 원격 서비스를 호출할 수 있어. 😍
🎯 Feign 설정하기

의존성 추가:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-openfeign</artifactId>
</dependency>

메인 클래스에 @EnableFeignClients 추가:

@SpringBootApplication
@EnableDiscoveryClient
@EnableFeignClients
public class OrderServiceApplication {
    public static void main(String[] args) {
        SpringApplication.run(OrderServiceApplication.class, args);
    }
}

Feign Client 인터페이스 정의:

@FeignClient(name = "user-service")
public interface UserServiceClient {
    
    @GetMapping("/api/users/{userId}")
    User getUserById(@PathVariable("userId") Long userId);
    
    @PostMapping("/api/users")
    User createUser(@RequestBody User user);
    
    @PutMapping("/api/users/{userId}")
    User updateUser(@PathVariable("userId") Long userId, @RequestBody User user);
    
    @DeleteMapping("/api/users/{userId}")
    void deleteUser(@PathVariable("userId") Long userId);
}

사용하기:

@Service
public class OrderService {
    
    @Autowired
    private UserServiceClient userServiceClient;
    
    public Order createOrder(Long userId, OrderRequest request) {
        // Feign을 통해 user-service 호출
        User user = userServiceClient.getUserById(userId);
        
        if (user == null) {
            throw new UserNotFoundException("User not found");
        }
        
        // 주문 생성 로직
        Order order = new Order();
        order.setUserId(userId);
        order.setUserName(user.getName());
        // ...
        
        return order;
    }
}

보이지? 정말 간단하지! 인터페이스만 정의하면 Feign이 자동으로 구현체를 만들어줘. Eureka와 통합되어 있어서 서비스 이름만으로 호출할 수 있고, Ribbon과도 통합되어 있어서 자동으로 로드 밸런싱도 해줘!
⚙️ Feign 고급 설정

1. 타임아웃 설정

feign:
  client:
    config:
      default:
        connectTimeout: 5000
        readTimeout: 5000
      user-service:
        connectTimeout: 3000
        readTimeout: 3000

2. Hystrix와 통합

feign:
  hystrix:
    enabled: true

@FeignClient(name = "user-service", fallback = UserServiceClientFallback.class)
public interface UserServiceClient {
    @GetMapping("/api/users/{userId}")
    User getUserById(@PathVariable("userId") Long userId);
}

@Component
public class UserServiceClientFallback implements UserServiceClient {
    @Override
    public User getUserById(Long userId) {
        User fallbackUser = new User();
        fallbackUser.setId(userId);
        fallbackUser.setName("Unknown User");
        return fallbackUser;
    }
}

3. 로깅 설정

@Configuration
public class FeignConfig {
    
    @Bean
    Logger.Level feignLoggerLevel() {
        return Logger.Level.FULL;
    }
}

logging:
  level:
    com.example.client.UserServiceClient: DEBUG

로깅 레벨:
• NONE: 로깅 안 함 (기본값)
• BASIC: 요청 메서드, URL, 응답 상태 코드, 실행 시간만
• HEADERS: BASIC + 요청/응답 헤더
• FULL: HEADERS + 요청/응답 바디
💡 Feign vs RestTemplate Feign의 장점
• 코드가 간결하고 읽기 쉬움
• 인터페이스 기반이라 테스트하기 쉬움
• Ribbon, Hystrix와 자동 통합
• 선언적 방식이라 유지보수가 쉬움

RestTemplate의 장점
• 더 세밀한 제어 가능
• 동적인 URL 생성이 쉬움
• 복잡한 요청 처리에 유리

일반적으로는 Feign을 쓰는 게 좋아. 코드가 훨씬 깔끔하거든! 하지만 아주 복잡한 케이스에서는 RestTemplate이나 WebClient를 쓰는 게 나을 수도 있어.

🏗️ 실전 마이크로서비스 아키텍처 구성하기

자, 이제 배운 걸 다 합쳐서 실제 마이크로서비스 아키텍처를 구성해보자! 간단한 이커머스 시스템을 예로 들어볼게. 🛒
E-Commerce Microservices Architecture Client Applications (Web, Mobile, etc.) API Gateway (Zuul) Authentication, Routing, Rate Limiting User Service Registration Authentication Product Service Catalog Inventory Order Service Order Management Order History Payment Service Payment Processing Refunds Eureka Service Registry Service Discovery Hystrix Circuit Breaker Ribbon Load Balancer Feign HTTP Client
🎯 시스템 구성 요소 1. Eureka Server (포트 8761)
모든 마이크로서비스가 등록되는 서비스 레지스트리. 고가용성을 위해 2개 이상의 인스턴스로 클러스터 구성.

2. API Gateway (포트 8080)
Zuul을 사용한 단일 진입점. 인증, 라우팅, Rate Limiting 처리.

3. User Service (포트 8081)
사용자 등록, 로그인, 프로필 관리. PostgreSQL 사용.

4. Product Service (포트 8082)
상품 카탈로그, 재고 관리. MongoDB 사용 (상품 정보는 스키마가 유연해야 하니까).

5. Order Service (포트 8083)
주문 생성, 주문 내역 조회. MySQL 사용. User Service와 Product Service를 Feign으로 호출.

6. Payment Service (포트 8084)
결제 처리, 환불 처리. PostgreSQL 사용. 외부 결제 게이트웨이(PG사) 연동.
📋 프로젝트 구조

ecommerce-microservices/
├── eureka-server/
│   ├── src/
│   └── pom.xml
├── api-gateway/
│   ├── src/
│   └── pom.xml
├── user-service/
│   ├── src/
│   └── pom.xml
├── product-service/
│   ├── src/
│   └── pom.xml
├── order-service/
│   ├── src/
│   └── pom.xml
├── payment-service/
│   ├── src/
│   └── pom.xml
└── pom.xml (parent)

부모 pom.xml에서 Spring Cloud 버전 관리:

<dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-dependencies</artifactId>
            <version>Hoxton.SR12</version>
            <type>pom</type>
            <scope>import</scope>
        </dependency>
    </dependencies>
</dependencyManagement>
🔄 서비스 간 통신 흐름 주문 생성 시나리오를 예로 들어볼게:

1단계: 클라이언트 요청
클라이언트가 API Gateway로 주문 생성 요청을 보냄.
POST http://gateway:8080/api/orders

2단계: 인증 및 라우팅
Zuul의 AuthenticationFilter가 JWT 토큰을 검증하고, order-service로 라우팅.

3단계: 사용자 정보 조회
Order Service가 Feign을 통해 User Service 호출.
userServiceClient.getUserById(userId)
Ribbon이 user-service의 인스턴스 중 하나를 선택해서 로드 밸런싱.
Hystrix가 타임아웃이나 에러를 감지하고, 필요시 폴백 실행.

4단계: 상품 정보 조회
Order Service가 Feign을 통해 Product Service 호출.
productServiceClient.getProductById(productId)
재고 확인 및 차감.

5단계: 결제 처리
Order Service가 Feign을 통해 Payment Service 호출.
paymentServiceClient.processPayment(paymentRequest)
Payment Service는 외부 PG사 API를 호출해서 실제 결제 처리.

6단계: 주문 저장 및 응답
모든 단계가 성공하면 주문을 DB에 저장하고 클라이언트에 응답 반환.

이 과정에서 어느 단계든 실패하면 Hystrix가 폴백을 실행하거나 에러를 반환해. 그리고 모든 서비스 호출은 Eureka를 통해 동적으로 인스턴스를 찾고, Ribbon으로 로드 밸런싱되는 거야!

🎓 마이크로서비스 설계 베스트 프랙티스

실무에서 마이크로서비스를 구축할 때 꼭 알아야 할 베스트 프랙티스들을 정리해봤어! 💎
1. 서비스 경계 설정 (Bounded Context) 마이크로서비스를 어떻게 나눌지가 가장 중요해. 너무 작게 나누면 관리가 복잡해지고, 너무 크게 나누면 모놀리식과 다를 게 없어.

좋은 기준들:
• 비즈니스 도메인 기준으로 나누기 (DDD의 Bounded Context 개념)
• 단일 책임 원칙 적용
• 독립적으로 배포 가능한 단위
• 팀이 관리할 수 있는 크기

예를 들어, "사용자 관리"는 하나의 서비스로, "주문 관리"는 또 다른 서비스로 나누는 식이야. 하지만 "사용자 조회"와 "사용자 수정"을 별도 서비스로 나누는 건 과도해.
2. 데이터베이스 분리 각 마이크로서비스는 자신만의 데이터베이스를 가져야 해. 이게 Database per Service 패턴이야.

장점:
• 서비스 간 느슨한 결합
• 각 서비스에 최적화된 DB 선택 가능
• 독립적인 스케일링

단점:
• 분산 트랜잭션 처리가 복잡함
• 데이터 일관성 유지가 어려움
• 조인 쿼리를 할 수 없음

분산 트랜잭션은 Saga 패턴이나 이벤트 소싱으로 해결할 수 있어. 이건 좀 고급 주제라서 여기서는 자세히 다루지 않을게!
3. API 버전 관리 마이크로서비스는 독립적으로 배포되니까 API 버전 관리가 중요해. 하위 호환성을 깨뜨리면 다른 서비스들이 다 영향을 받거든.

버전 관리 방법:
• URL에 버전 포함: /api/v1/users, /api/v2/users
• 헤더에 버전 포함: Accept: application/vnd.company.v1+json
• 쿼리 파라미터: /api/users?version=1

개인적으로는 URL에 버전을 포함하는 게 가장 명확하고 사용하기 쉬워서 추천해!
4. 로깅 및 모니터링 분산 시스템에서는 로깅과 모니터링이 필수야. 문제가 생겼을 때 어느 서비스에서 발생했는지 추적하기 어렵거든.

필수 도구들:
• ELK Stack (Elasticsearch, Logstash, Kibana): 중앙집중식 로깅
• Zipkin/Sleuth: 분산 트레이싱 (요청이 여러 서비스를 거치는 경로 추적)
• Prometheus + Grafana: 메트릭 수집 및 시각화
• Spring Boot Actuator: 헬스 체크, 메트릭 엔드포인트

특히 분산 트레이싱은 정말 중요해. 하나의 요청이 여러 마이크로서비스를 거치면서 어디서 느려지는지, 어디서 에러가 나는지 한눈에 볼 수 있거든!
5. 보안 마이크로서비스 환경에서는 보안이 더 복잡해져. 서비스 간 통신도 보호해야 하고, 각 서비스마다 인증/인가를 처리해야 하니까.

보안 베스트 프랙티스:
• API Gateway에서 중앙집중식 인증 처리
• JWT 토큰 사용 (stateless 인증)
• 서비스 간 통신은 mTLS (mutual TLS)로 암호화
• 민감한 정보는 환경 변수나 Vault 같은 시크릿 관리 도구 사용
• HTTPS 필수
• Rate Limiting으로 DDoS 방어

절대 하드코딩된 비밀번호나 API 키를 코드에 넣지 마! 🚫
6. 테스트 전략 마이크로서비스는 테스트가 더 복잡해. 각 서비스를 독립적으로 테스트해야 하고, 서비스 간 통합도 테스트해야 하니까.

테스트 피라미드:
• Unit Tests: 가장 많이, 빠르게 실행
• Integration Tests: 중간 정도, DB나 외부 API와의 통합 테스트
• Contract Tests: API 계약 검증 (Pact 같은 도구 사용)
• End-to-End Tests: 가장 적게, 전체 시스템 테스트

Contract Testing이 특히 중요해. 서비스 A가 기대하는 API 응답 형식을 서비스 B가 실제로 제공하는지 검증하는 거야. 이렇게 하면 한 서비스의 변경이 다른 서비스를 깨뜨리는 걸 방지할 수 있어!
💡 실무 꿀팁 처음부터 완벽한 마이크로서비스 아키텍처를 구축하려고 하지 마. 모놀리식으로 시작해서 서비스가 커지면서 점진적으로 마이크로서비스로 분리하는 게 현실적이야. 이걸 Strangler Fig Pattern이라고 해. 기존 모놀리식 애플리케이션을 유지하면서 새로운 기능은 마이크로서비스로 만들고, 점차 기존 기능도 마이크로서비스로 옮기는 거지.

또한 마이크로서비스가 모든 문제의 해결책은 아니야. 작은 프로젝트나 팀이라면 모놀리식이 더 나을 수 있어. 복잡도와 이점을 잘 저울질해서 결정해야 해!

🚀 Spring Cloud Netflix OSS의 미래

앞서 여러 번 언급했듯이, Netflix OSS의 많은 컴포넌트들이 유지보수 모드에 들어갔어. Netflix는 더 이상 이 프로젝트들을 적극적으로 개발하지 않아. 그럼 우리는 어떻게 해야 할까? 🤔
🔄 대안 기술들 Eureka → Consul, Kubernetes Service Discovery
Consul은 HashiCorp에서 만든 서비스 메시 솔루션이야. 서비스 디스커버리뿐만 아니라 설정 관리, 헬스 체크 등 다양한 기능을 제공해. Kubernetes를 사용한다면 K8s의 내장 서비스 디스커버리를 쓸 수도 있어.

Ribbon → Spring Cloud LoadBalancer
Spring Cloud LoadBalancer는 Ribbon의 대체재로 Spring 팀이 직접 개발한 거야. 더 가볍고 현대적이며, Spring Cloud와 완벽하게 통합돼.

Hystrix → Resilience4j
Resilience4j는 가볍고 함수형 프로그래밍 스타일을 지원하는 현대적인 장애 허용 라이브러리야. Circuit Breaker, Rate Limiter, Retry, Bulkhead 등 다양한 패턴을 제공해.

Zuul → Spring Cloud Gateway
Spring Cloud Gateway는 Spring 5와 Project Reactor 기반의 완전히 비동기 논블로킹 게이트웨이야. 성능이 훨씬 좋고, 리액티브 프로그래밍 모델을 지원해.

Feign → 계속 사용 가능
Feign은 OpenFeign으로 이름이 바뀌었지만 여전히 활발하게 개발되고 있어. 계속 사용해도 괜찮아!
✨ 현대적인 스택 예시

<!-- Spring Cloud LoadBalancer -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-loadbalancer</artifactId>
</dependency>

<!-- Resilience4j -->
<dependency>
    <groupId>io.github.resilience4j</groupId>
    <artifactId>resilience4j-spring-boot2</artifactId>
</dependency>

<!-- Spring Cloud Gateway -->
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-starter-gateway</artifactId>
</dependency>

이 조합이 현재 가장 권장되는 스택이야!
☸️ Kubernetes와의 관계 요즘은 많은 기업들이 Kubernetes(K8s)를 사용해. K8s는 컨테이너 오케스트레이션 플랫폼인데, 마이크로서비스 운영에 필요한 많은 기능을 제공해:

• Service Discovery: K8s Service
• Load Balancing: K8s Service
• Health Checks: Liveness/Readiness Probes
• Configuration Management: ConfigMaps, Secrets
• Auto Scaling: Horizontal Pod Autoscaler

그럼 Spring Cloud Netflix OSS가 필요 없는 거 아니야? 🤔

완전히 그렇진 않아. K8s는 인프라 레벨의 기능을 제공하고, Spring Cloud는 애플리케이션 레벨의 기능을 제공해. 예를 들어:

• Circuit Breaker는 여전히 애플리케이션 레벨에서 필요해
• Feign 같은 선언적 HTTP 클라이언트는 개발 생산성을 높여줘
• Spring Cloud Config는 중앙집중식 설정 관리를 제공해

결론은, K8s와 Spring Cloud를 함께 사용하는 게 가장 좋아! 각자의 강점을 활용하는 거지. 😊

💼 실무에서 마주치는 문제들과 해결책

이론은 완벽해 보이지만, 실무에서는 예상치 못한 문제들이 많이 생겨. 내가 경험한 것들을 공유해볼게! 🎯
문제 1: 분산 트랜잭션 처리 주문 생성 시나리오를 다시 생각해봐. 재고 차감은 성공했는데 결제가 실패하면? 이미 차감된 재고를 어떻게 복구할까?

해결책: Saga 패턴

Choreography 방식
각 서비스가 이벤트를 발행하고 구독하는 방식. 중앙 조정자 없이 서비스들이 협력해.

1. Order Service가 주문 생성 이벤트 발행
2. Product Service가 이벤트를 받아 재고 차감, 재고 차감 완료 이벤트 발행
3. Payment Service가 이벤트를 받아 결제 처리
4. 결제 실패 시 보상 트랜잭션 이벤트 발행
5. Product Service가 재고 복구

Orchestration 방식
중앙 조정자(Orchestrator)가 전체 흐름을 제어하는 방식.

Order Service가 Orchestrator 역할을 하면서:
1. Product Service에 재고 차감 요청
2. 성공하면 Payment Service에 결제 요청
3. 실패하면 Product Service에 재고 복구 요청

Orchestration이 더 이해하기 쉽고 디버깅하기 쉬워서 보통 이걸 추천해!
문제 2: 서비스 간 순환 의존성 User Service가 Order Service를 호출하고, Order Service가 User Service를 호출하면? 무한 루프에 빠질 수 있어! 😱

해결책
• 서비스 경계를 다시 설계해서 순환 의존성 제거
• 공통 데이터는 API Gateway나 별도의 공유 서비스로 분리
• 이벤트 기반 아키텍처로 전환 (비동기 통신)
• 데이터 복제 (각 서비스가 필요한 데이터의 복사본을 가짐)
문제 3: 네트워크 지연 및 타임아웃 마이크로서비스는 네트워크를 통해 통신하니까 지연이 발생할 수밖에 없어. 특히 여러 서비스를 순차적으로 호출하면 지연이 누적돼.

해결책
• 적절한 타임아웃 설정 (너무 길면 리소스 낭비, 너무 짧으면 정상 요청도 실패)
• 비동기 통신 활용 (메시지 큐 사용)
• 캐싱 전략 (Redis 같은 인메모리 캐시 사용)
• API 응답 최적화 (필요한 데이터만 반환)
• GraphQL 고려 (클라이언트가 필요한 데이터만 요청)
문제 4: 데이터 일관성 각 서비스가 자신의 DB를 가지니까 데이터 일관성을 유지하기 어려워. User Service의 사용자 정보가 변경되면 Order Service의 주문 내역에 있는 사용자 정보도 업데이트해야 할까?

해결책
• Eventual Consistency 받아들이기: 즉시 일관성이 아니라 결과적 일관성
• 이벤트 소싱: 모든 변경을 이벤트로 기록
• CQRS (Command Query Responsibility Segregation): 읽기와 쓰기 모델 분리
• 데이터 복제 시 버전 관리 (어느 데이터가 최신인지 추적)
⚠️ 과도한 마이크로서비스화 주의 마이크로서비스가 유행이라고 해서 무조건 적용하는 건 위험해. 특히 초기 스타트업이나 작은 팀에서는 오히려 독이 될 수 있어. 마이크로서비스는 복잡도를 크게 증가시키거든.

마이크로서비스가 적합한 경우:
• 팀이 크고 (보통 50명 이상) 여러 팀으로 나뉘어 있을 때
• 서비스 규모가 크고 트래픽이 많을 때
• 독립적인 배포와 스케일링이 정말 필요할 때
• 다양한 기술 스택을 사용해야 할 때

모놀리식이 더 나은 경우:
• 작은 팀 (10명 이하)
• MVP나 초기 프로토타입
• 비즈니스 도메인이 명확하지 않을 때
• 빠른 개발이 중요할 때

🎓 학습 로드맵 및 리소스

마이크로서비스와 Spring Cloud를 제대로 배우고 싶다면? 이 로드맵을 따라가봐! 📚
1단계: 기초 다지기 (1-2개월) • Java 기본 문법 및 객체지향 프로그래밍
• Spring Framework 핵심 개념 (DI, AOP, IoC)
• Spring Boot 기본 (Auto Configuration, Starter Dependencies)
• RESTful API 설계 원칙
• HTTP 프로토콜 이해
• 데이터베이스 기초 (SQL, JPA/Hibernate)

추천 리소스:
• 백기선님의 "스프링 부트 개념과 활용" 강의
• Baeldung 블로그 (https://www.baeldung.com)
• Spring 공식 문서 (https://spring.io/guides)
2단계: 마이크로서비스 개념 (1개월) • 마이크로서비스 아키텍처 패턴
• 모놀리식 vs 마이크로서비스 비교
• Domain-Driven Design (DDD) 기초
• API Gateway 패턴
• Service Discovery 패턴
• Circuit Breaker 패턴

추천 리소스:
• "Building Microservices" by Sam Newman (책)
• "Microservices Patterns" by Chris Richardson (책)
• Martin Fowler의 블로그 (https://martinfowler.com)
3단계: Spring Cloud 실습 (2-3개월) • Eureka로 Service Discovery 구현
• Ribbon/Spring Cloud LoadBalancer로 로드 밸런싱
• Hystrix/Resilience4j로 Circuit Breaker 구현
• Zuul/Spring Cloud Gateway로 API Gateway 구축
• Feign으로 서비스 간 통신
• Spring Cloud Config로 중앙집중식 설정 관리
• Sleuth + Zipkin으로 분산 트레이싱

추천 리소스:
• Spring Cloud 공식 문서
• "Spring Microservices in Action" by John Carnell (책)
• Udemy나 인프런의 Spring Cloud 강의들
4단계: 고급 주제 (2-3개월) • Docker 컨테이너화
• Kubernetes 기초
• CI/CD 파이프라인 구축 (Jenkins, GitLab CI 등)
• 메시지 큐 (RabbitMQ, Kafka)
• 이벤트 기반 아키텍처
• Saga 패턴으로 분산 트랜잭션 처리
• CQRS와 Event Sourcing
• 보안 (OAuth2, JWT)
• 모니터링 (Prometheus, Grafana, ELK Stack)

추천 리소스:
• Kubernetes 공식 문서
• "Kubernetes in Action" by Marko Luksa (책)
• "Enterprise Integration Patterns" by Gregor Hohpe (책)
5단계: 실전 프로젝트 (진행형) 이론만 배우면 소용없어! 직접 프로젝트를 만들어봐야 해. 추천 프로젝트 아이디어:

• 이커머스 플랫폼 (상품, 주문, 결제, 배송 서비스)
• 소셜 미디어 플랫폼 (사용자, 게시물, 댓글, 알림 서비스)
• 예약 시스템 (사용자, 예약, 결제, 알림 서비스)
• 음식 배달 앱 (음식점, 주문, 배달, 결제 서비스)

GitHub에 올리고 README를 잘 작성해서 포트폴리오로 활용해! 취업할 때 정말 도움이 돼. 💼
✨ 재능넷 활용하기 혼자 공부하다 막히는 부분이 있다면 재능넷(https://www.jaenung.net)에서 전문가의 도움을 받아보는 것도 좋은 방법이야! 마이크로서비스 아키텍처 설계 컨설팅, 코드 리뷰, 멘토링 등 다양한 재능을 거래할 수 있거든. 실무 경험이 풍부한 개발자들에게 직접 배울 수 있는 기회야. 또한 당신이 어느 정도 실력을 쌓았다면, 재능넷에서 당신의 지식을 공유하고 수익도 얻을 수 있어! 😊

🎯 마무리하며

자, 여기까지 자바 마이크로서비스와 Spring Cloud Netflix OSS에 대해 알아봤어! 정말 긴 여정이었지? 😅

핵심 내용을 다시 정리해볼게:
🔑 핵심 요약 1. 마이크로서비스 아키텍처
큰 애플리케이션을 작은 독립적인 서비스들로 나누는 아키텍처 스타일. 독립적인 배포, 확장, 기술 선택이 가능하지만 복잡도가 증가해.

2. Spring Cloud Netflix OSS 컴포넌트
• Eureka: 서비스 레지스트리 (Service Discovery)
• Ribbon: 클라이언트 사이드 로드 밸런싱
• Hystrix: 서킷 브레이커 (장애 허용)
• Zuul: API 게이트웨이
• Feign: 선언적 HTTP 클라이언트

3. 현대적인 대안
Netflix OSS의 많은 컴포넌트가 유지보수 모드에 들어갔어. Spring Cloud LoadBalancer, Resilience4j, Spring Cloud Gateway 같은 현대적인 대안들을 고려해봐.

4. 베스트 프랙티스
• 적절한 서비스 경계 설정
• Database per Service 패턴
• API 버전 관리
• 중앙집중식 로깅 및 분산 트레이싱
• 보안 강화 (JWT, mTLS)
• 철저한 테스트 (Contract Testing 포함)

5. 실무 고려사항
• 분산 트랜잭션 처리 (Saga 패턴)
• 데이터 일관성 (Eventual Consistency)
• 네트워크 지연 최소화
• 과도한 마이크로서비스화 주의
💡 마지막 조언 마이크로서비스는 은탄환(Silver Bullet)이 아니야. 모든 문제를 해결해주는 마법 같은 건 없어. 프로젝트의 규모, 팀의 크기, 비즈니스 요구사항을 고려해서 적절한 아키텍처를 선택하는 게 중요해.

처음에는 모놀리식으로 시작해서 빠르게 MVP를 만들고, 서비스가 성장하면서 점진적으로 마이크로서비스로 전환하는 것도 좋은 전략이야. 이게 바로 Strangler Fig Pattern이지!

그리고 기술은 계속 발전해. Spring Cloud Netflix OSS가 유지보수 모드에 들어간 것처럼, 지금 배우는 기술도 언젠가는 구식이 될 수 있어. 중요한 건 특정 기술이 아니라 개념과 패턴을 이해하는 거야. 그러면 새로운 기술이 나와도 빠르게 적응할 수 있거든! 💪
🚀 다음 단계 이제 당신 차례야! 이 글을 읽는 것만으로는 부족해. 직접 손으로 코드를 작성하고, 에러를 만나고, 해결하면서 배워야 해.

오늘부터 시작할 수 있는 것들:
1. Spring Initializr로 간단한 마이크로서비스 프로젝트 생성
2. Eureka Server 구축하고 2-3개의 서비스 등록해보기
3. Feign으로 서비스 간 통신 구현해보기
4. Hystrix나 Resilience4j로 Circuit Breaker 적용해보기
5. GitHub에 프로젝트 올리고 README 작성하기

작은 것부터 시작해서 점점 확장해나가면 돼. 완벽하지 않아도 괜찮아. 실패하면서 배우는 게 가장 빠른 학습 방법이니까! 😊

화이팅! 당신은 할 수 있어! 🎉

🌟 함께 성장하는 개발자 커뮤니티

마이크로서비스 여정은 혼자가 아니야!
재능넷에서 다른 개발자들과 지식을 나누고,
멘토링을 받거나 제공하면서 함께 성장해보자! 💪

당신의 경험과 지식이 누군가에게는 큰 도움이 될 수 있어.
그리고 가르치는 과정에서 당신도 더 깊이 배우게 될 거야!

댓글 작성

이 글에 대한 여러분의 생각을 들려주세요

댓글 0
댓글을 불러오는 중입니다.

아직 댓글이 없습니다.