Infra

Spring Cloud MSA_1

elenalee 2025. 8. 7. 10:53

1. Spring Cloud & MSA

 

1) MSA

- 하나의 어플리케이션을 독립적인 서비스(MicroService)로 구성하는 Architecture

- 장점 : 확장성, 다양한 기술 스택 적용가능, 유지보수 용이, 장애 대응

- 핵심요소 : API Gateway, Service Mesh, Backing Service, CI/CD Automation, Telemetry, Container Management 

 

 

2) MicroService Pattern

(1) 클라이언트와 마이크로서비스 통신 : API Gateway Pattern (라우팅/프록시, 보안 , 밸런싱)

(2) 마이크로 서비스 내부 통신 : Service Discovery Pattern ( 동적인 서비스 통신 )

(3) 비동기 통신 : Message Broker or Saga Pattern  ( 분산 트랜젝션 처리 )

     예) 주문처리 : 사용자-주문-결재-재고 등의 서비스가 함께 구동(트랜젝션 방식)

(4) 장애 : Circuit Breaker or Reply 패턴

(5) 운영 : Log Aggregation or Distributed Tracing패턴   
(6) 데이터/상태 : Event Sourcing or CQRS 패턴 

 

3) Spring Cloud 적용 

- Spring Framework(Boot)기반의 MSA를 구현하는 프레임워크

- MSA에 요구되는 기능을 패턴과 인프라 기능을 자동 설정으로 구현 

- Spring Cloud 적용 패턴 

Config Server : Spring Cloud Config

Service Discovery : Spring Cloud Netflix Eureka

Service Communiation : SpringCloud OpenFeign

API Gateway : Spring Cloud Gateway

Fault Tolerance : Resilience4j 

Disrtributed Tracing : Spring Cloud Leuth, Zipkin

Event-Driven Architecure : Spring Cloud Stream(Kafka, RabbitMQ)

 

4) 프로젝트 구성 

(1) 기본 구성 설정

- git repository생성 및 로컬 폴더에 clone

- 프로젝트 생성 및  infra구성 (DB등)

- MicroService 생성 ( User/Order/Product의 model, controller, service, repository구성 )

   

2. Config-Server & Client

1) Config Server/Client기본구성

- spring cloud에서 config-server를 통해 설정 정보를 중앙에서 관리

   ( prod, stage, dev등의 역할에 맞게 적용, 포트 8888 )

- 개발 및 test를 위해 local에 저장소 설정 ( 운영시 Git으로 설정/적용)

 

2) Config-Server생성

- config-server 모듈 생성 ( config-server 의존성 필수 )

- 모듈내에 서버 정보(native로 구성) 및 Application에 @EnableConfigServer 어느테이션 설정

#application.yaml
server:
  port: 8888
spring:
  profiles:
    active: native
  application:
    name: config-server
  cloud:
    config:
      server:
        native:
          search-locations:
          - file:///${user.dir}/backend/config-repo

- 8888포트 부여 ( 브라우저 확인, http://localhost:8888/order-service/default)

 

(3) MicroService 연동 

- 서비스를 config client설정하여 config server와 연결

 

① 의존성 설정 ( config client추가 - Spring 버전에 맞는 버전도 자동 설정 )

② application.yaml 설정  ( config server에 연결 ) 

#application.yaml 재설정 (config서버와 연결)
spring:
  config:
    import: "configserver:http://localhost:8888"
  application:
    name: user-service
  profiles:
    active: dev

 

③ Config-Server와 서비스(Config-Client)동적 연결 

- 동적인 연결을 수행할 필드값을 MicroService에 추가

- 브라우저에서 해당 내용을 확인하기 위해 MicroService에 Controller생성  

#Custom Field추가구성 (config-server내의 user-service.yaml)
server:
  port: 8081
spring:
  application:
    name: user-service
  datasource:
    url: jdbc:mysql://localhost:3307/user_service_db
    
user:
  service:
    name: "User Service"
    version: "1.0"
    description: "사용자 정보를 관리하는 마이크로 서비스"
    
    
#ConfigController (서비스에 Custom설정을 환경변수로 확인가능한 controller추가)
package com.sesac.userservice.controller;

import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/config")
@Tag(name="Config Test", description="설정값 확인용 API")
public class ConfigController {


http://localhost:8081/config
    private String serviceName;
    private String serviceVersion;
    private String serviceDescription;

}

 

 

- 내용 변경을 반영해줄 management구성 application.yaml (config-repo_)

  ( http://localhost:8081/actuator/refresh에 적용)

# Refresh설정을 추가 (application.yaml, config-repo)
spring:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: 123qwe
  jpa:
    hibernate:
      ddl-auto: update
    show-sql: true
    properties:
      hibernate:
        format_sql: true

management:
  endpoints:
    web:
      exposure:
        include: health, info, refresh
  endpoint:
      refresh:
        enabled: true

 

- serviceVersion을 2.0으로 변경하고 반영 ( Post요청이므로 curl로 진행 )

   curl -X POST  localhost:8081/actuator/refresh http://localhost:8081/config

동적인 연결이 반영된 화면

 


3. Service Discovery

- 서비스 간 통신은 service-discovery server로 중앙집중 관리 (Eureka, DNS기능)

- 서비스가 등록되면 서비스 이름을 기반으로 조회 / 호출 / 헬스 체크 / 상태 관리 

- 서비스 이름, IP주소/포트, 상태(UP/DOWN) 

 

(1) Eureka서버 생성

- 모듈 생성 후 의존성 추가 ( eureka-server, config-client, actuator)

- EnableEurekaServer 어노테이션 추가

- Config Server연동 

#Eureka서버 (어노테이션 추가)
package com.sesac.eurekaserver;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cloud.netflix.eureka.server.EnableEurekaServer;

@SpringBootApplication
@EnableEurekaServer
public class EurekaServerApplication {

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

}

// eureka-service내의 application.yaml(config-server에 등록)
spring:
  config:
    import: "configserver:http://localhost:8888"
  application:
    name: eureka-server
    
 
//config.repo내의 Eureka연동을 위한 application.yaml 추가
server:
  port: 8761
spring:
  application:
    name: eureka-server

eureka:
  client:
    register-with-eureka: false   자기자신이 서버이므로 false
    fetch-registry: false
    service-url:
      defaultZone: http://localhost:8761/eureka/

 

Eureka서버 기본구성

 

(2) Eureka Client등록 ( MicroService 등록 )

- 의존성 설정으로 자동 등록 ( Eureka Discovery Client 추가 )

Eureka에 Micro-Service등록

 

 

4. 서비스간 통신 ( OpenFeign ) 

-  격리되어 구동되는 마이크로 서비스를 연결하는 통신체계 설정 

- 동기 ( HTTP REST호출, gRPC ), 비동기 ( 브로커 - RabbitMQ, Kafka ) 

 

OpenFeign 

- 동기 방식 중 HTTP 통신 방식 ( RestTemplate, WebClient, OpenFeign )

- interface와 annotation으로 통신 구현

 

(1) OpenFeign 적용 

- MicroService에 의존성(OpenFeign) 추가

- 어플리케이션에 @EnableFeignClients추가 

 

(2) 통신 Package구성

① MicroService에 관련 외부 모듈을 연결하는 클라이언트를 Inteface로 생성 

    - Order처리를 위한 관련 정보를 호출 ProductServiceClient, UserServiceClient 생성

    - @OpenFeignClient어노테이션 추가 

② 통신을 위한 관련 DTO생성 ( UserDto, ProductDto)와 OrderDto

   - Micro-Service들은 id를 기준으로 Entity를 호출

   - JSON문자열로 변환 (Entity필드의 이름과 값을 Key-Value로 매핑, jackson으로 직렬화)

      이후 FeignClient가 JSON문자열을 Dto로 변환 ( Jackson으로 수행 ) 

//productDto
package com.sesac.orderservice.client.dto;

import lombok.Data;

import java.math.BigDecimal;

@Data
public class ProductDto {
    private Long id;
    private String name;
    private BigDecimal price;
    private Integer stockQuantity;
}


//userDto
package com.sesac.orderservice.client.dto;

import lombok.Data;

@Data
public class UserDto {
   private Long id;
   private String email;
   private String name;
}

//orderRequestDTO
package com.sesac.orderservice.dto;

import lombok.Data;

@Data
public class OrderRequestDto {
    private Long productId;
    private String userId;
    private Integer quantity;
}

//ProductServiceClient
package com.sesac.orderservice.client;

import com.sesac.orderservice.client.dto.ProductDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient (name = "product-service")
public interface ProductServiceClient {

    @GetMapping("products/id")
    ProductDto getUserById(@PathVariable("id") Long id);
}

//UserServiceClient
package com.sesac.orderservice.client;

import com.sesac.orderservice.client.dto.UserDto;
import org.springframework.cloud.openfeign.FeignClient;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;

@FeignClient (name = "user-service")
public interface UserServiceClient {

    @GetMapping("users/id")
    UserDto getUserById(@PathVariable("id") Long id); //OpenFeign이 구체적으로 구성하지 않아도 추상화

}

 

Swagger를 통해 서비스간 연동 확인

 

5. API Gateway (외부통신)

- 마이크로 서비스에 접근하기 위한 관문 역할 ( 쿠버네티스의 Ingress와 유사한 기능, 리버스 프록시 )

- 보안(인증/인가), 로깅, 라우팅, 암호화, 모니터링 ( 프론트와 통신할때 사용) 

 

(1) 백엔드에 api-gateway모듈 생성

 

① 의존성 설정

- Gateway, config client, actuator, lombok, eureka discovery client등 

( Gateway대신에 Reactive Gateway(비동기 통신 가능)도 추가적인 기능이 있어 적용은 가능)

 

② 구성 설정 

- Module내의 yaml과 config-server에 적용될 yaml을 구성

//application.yaml (config서버를 바라보도록 설정, eureka도 config를 바라봄)
spring:
  config:
    import: "configserver:http//:localhost:8888"
  application:
    name: api-gateway
    
//api-gateway.yaml (config-server에 반영되도록 설정)
server:
  port: 8080

spring:
  application:
    name: api-gateway
  cloud:
    gateway:
      server:
        webmvc:
          routes:
            - id: user-service           # 고유한 이름 
              uri: lb://user-service     # 로드밸런서에 목적지 표시(http가 아니므로 ip/포트에 무관하게 서비스 연결  
              predicates:
                - Path=/api/users/**     # 조건 (어떠한 path를 처리할 것인가)
              filters:
                - StripPrefix=1          # 1개의 경로제거 ("api"제거 후 서비스로 전달)

            - id: product-service
              uri: lb://product-service
              predicates:
                - Path=/api/products/**
              filters:
                - StripPrefix=1

            - id: order-service
              uri: lb://order-service
              predicates:
                - Path=/api/orders/**
              filters:
                - StripPrefix=1

 

Eureka의 이름으로 load balancer의 이름 등록

 

 

(2) CORS설정 (CofsConfig파일 구성)

① api-gateway server에서 CORS처리 ( front의 포트 3001 허용 )

package com.sesac.apigateway;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class CorsConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry){
        registry.addMapping("/**")  //모든 경로에 대해 설정
                .allowedOrigins("http://localhost:3001")
                .allowedMethods("GET", "POST", "PUT", "DELETE","OPTIONS")
                .allowedHeaders("*")
                .allowCredentials(true)
                .maxAge(3600);
    }

 

② front-end와 연결

 

6. 인증/인가 

1) MSA의 인증/인가

① MSA구조에서 인증/인가는 api-gateway를 중심으로 수행

   - JWT기반의 인증 권장 ( 세션방식은 각 MicroService가 인증 관련 정보 필요 )

② 인증 - 사용자 생성 및 인증은 User정보를 처리하는 MicroService에서 수행

    인가 - API-Gateway에서 token으로 권한이 확인 후 관련 MicroService로 라우팅

③ 인증과 인가 Process 

    로그인 시도 - ID/PW검증(User서비스) - 토큰 발급 - API 요청시 헤더의 토큰검증(API-Gateway서버) 

 

2) User서비스에 인증 프로세스 구성 

- Spring Security & JWT 관련 의존성 추가 

	implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
	implementation 'io.jsonwebtoken:jjwt-impl:0.12.6'
	implementation 'io.jsonwebtoken:jjwt-jackson:0.12.6'

	implementation 'org.springframework.boot:spring-boot-starter-security'
	testImplementation 'org.springframework.security:spring-security-test'
	implementation 'org.springframework.security:spring-security-crypto'

 

 

3) JWT Processing Module & DTO 

(1) JWT 처리 모듈

 JwtTokenProvider

   - 서명에 사용될 키 생성(secret사용)

   - payload, claim을 구성 후 키로 서명하여 Token생성 

   - Token validation 및  email, userId등을 추출하는 함수 작성 

import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.crypto.SecretKey;
import java.util.Date;

@Component
public class JwtTokenProvider {

    @Value("${jwt.secret:mySecretKeyForJWTTokenGenerationThatShouldBeLongEnough}")
    private String jwtSecret;   // 값이 없는 경우 반영할 기본설정값도 표시 

    @Value("${jwt.expiration:86400000}") // 24시간
    private int jwtExpirationMs; // 값이 없는 경우 반영할 기본설정값도 표시 

    private SecretKey getSigningKey() {
        return Keys.hmacShaKeyFor(jwtSecret.getBytes());
    } //jwt.secret의 문자열을 암호화하여 서명 Key생성 

    public String generateToken(String email, Long userId) {
        Date expiryDate = new Date(System.currentTimeMillis() + jwtExpirationMs);

        return Jwts.builder()
                .subject(email)
                .claim("userId", userId)
                .issuedAt(new Date())
                .expiration(expiryDate)
                .signWith(getSigningKey())
                .compact();  //이메일/클레임/생성일/만료일/SignKey로 생성
    }

 

 SecurityConfig

   접근경로 별 권한에 대한 설정, 비밀번호 보안설정 ( Encoder이지만 실제로 Hashing )

//Security Config (보안 정책설정)

@Configuration
@EnableWebSecurity  //웹보안 활성화 
public class SecurityConfig {
    @Bean
    public BCryptPasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    } //Encoder설정 

    @Bean //접근 경로에 대한 권한설정
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .sessionManagement(session ->
                        session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
                .authorizeHttpRequests(auth ->
                        auth.requestMatchers("/users/login", "/actuator/**").permitAll()
                                .anyRequest().permitAll()
                );

        return http.build();
    }
    
}

 

(2) dto생성

- Login 요청 및 응답에 사용할 DTO

//LoginResponse DTO

@Data
public class LoginResponse {
    private String token;
    private String type = "Bearer";
    private Long userId;
    private String email;
    private String name;

    public LoginResponse(String token, Long userId, String email, String name) {
        this.token = token;
        this.userId = userId;
        this.email = email;
        this.name = name;
    }
}

 

4) 로그인 기능 구현  

-  User-Service(MicroService)에 Controller/Service/Repository 구현

   // Controller
   @PostMapping("/login")
    @Operation(summary="로그인", description="이메일과 패스워드로 로그인후 JWT토큰발급")
    public ResponseEntity<LoginResponse> login(@RequestBody LoginRequest request){
        try {
            LoginResponse response = userService.login(request);
            return ResponseEntity.ok(response);
        }catch(RuntimeException e) {
            return ResponseEntity.notFound().build();
        }
        
  // Service (사용자 조회 및 패스워드 검증 후, 토큰 생성 및 응답)
  private final PasswordEncoder passwordEncoder;
  private final JwtTokenProvider jwtTokenProvider;
  
    public LoginResponse login(LoginRequest request) {

      User user = userRepository.findByEmail(request.getEmail())
              .orElseThrow(() -> new RuntimeException(("invalid Email")));
      // 패스워드 검증 ( DB저장시 비문, password encoder로 인코딩된 패스워드로 DB와 비교)
      if (!passwordEncoder.matches(request.getPassword(), user.getPassword())){
          throw new RuntimeException("invalid email or password");
      }
      // JWT 토큰 및 응답 생성
      String token= jwtTokenProvider.generateToken(user.getEmail(), user.getId());
      return new LoginResponse(token, user.getId(),user.getEmail(), user.getName());
  }

 

 

5) API 요청시 토큰 검증

-  api-gateway내의 JwtAuthenticationFilter에서 doFilter 메서드를 오버라이드 

    ( 스프링 부트의 Auto-configuration과 Component Scan으로 인해 요청을 가로채서 검증수행 ) 

package com.sesac.apigateway;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.core.annotation.Order;
import org.springframework.stereotype.Component;

import jakarta.servlet.*;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import javax.crypto.SecretKey;
import java.io.IOException;
import java.util.Collections;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.Set;

@Component   //do Filter 오버라이드 
public class JwtAuthenticationFilter implements Filter {

   //설정값 가져오기 
    @Value("${jwt.secret:mySecretKeyForJWTTokenGenerationThatShouldBeLongEnough}")
    private String jwtSecret;

    private SecretKey getSigningKey() {
        return Keys.hmacShaKeyFor(jwtSecret.getBytes());
    }

   // API 요청을 가로채 path, method확인 (요청객체, 응답객체 : 내용이 빈 기본형 생성)
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {

        // Wrapper적용 
        HttpServletRequest httpRequest = (HttpServletRequest) request;
        HttpServletResponse httpResponse = (HttpServletResponse) response;

        //주소, method확인 (가로챈 요청)
        String path = httpRequest.getRequestURI();
        String method = httpRequest.getMethod();

        System.out.println("JWT Filter - Path: " + path + ", Method: " + method);

        // 공개 경로는 인증 없이 통과
        if (isPublicPath(path, method)) {
            System.out.println("JWT Filter: Public path, allowing request");
            chain.doFilter(request, response);
            return;
        }

        // JWT 토큰 검증 ( 토큰이 없거나 validation이 안되면 )
        String token = getTokenFromRequest(httpRequest);
        if (token == null || !validateToken(token)) {
            System.out.println("JWT Filter: Invalid or missing token");
            httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            httpResponse.getWriter().write("{\"error\":\"Unauthorized\"}");
            return;
        }

        // JWT에서 사용자 ID 추출하여 헤더에 추가
        Long userId = getUserIdFromToken(token);
        if (userId != null) {
            CustomHttpServletRequestWrapper requestWrapper = new CustomHttpServletRequestWrapper(httpRequest, userId);
            System.out.println("JWT Filter: Valid token, adding X-User-Id: " + userId);
            chain.doFilter(requestWrapper, response);
        } else {
            System.out.println("JWT Filter: Unable to extract userId from token");
            httpResponse.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
            httpResponse.getWriter().write("{\"error\":\"Invalid token claims\"}");
        }
    }

   //인증이 필요없는 PublicPath설정 
    private boolean isPublicPath(String path, String method) {
        return path.equals("/api/users/login") ||
                path.startsWith("/api/products") ||
                path.startsWith("/actuator/") ||
                "OPTIONS".equals(method);
    }

    //Request의 헤더에서 Token추출 
    private String getTokenFromRequest(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }

    // 파서(규칙에 따라 문자열 해석)키를 검증_ 위조/유효성 검사 
    private boolean validateToken(String token) {
        try {
            Jwts.parser()
                    .verifyWith(getSigningKey())
                    .build()
                    .parseSignedClaims(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

    //토큰에서 UserId추출 
    private Long getUserIdFromToken(String token) {
        try {
            Claims claims = Jwts.parser()
                    .verifyWith(getSigningKey())
                    .build()
                    .parseSignedClaims(token)
                    .getPayload();

            return claims.get("userId", Long.class);
        } catch (JwtException | IllegalArgumentException e) {
            return null;
        }
    }

    // UserId를 (X-User-Id) MicroService에 전달할수 있도록 Wrapper생성
    private static class CustomHttpServletRequestWrapper extends jakarta.servlet.http.HttpServletRequestWrapper {
        private final Long userId;

        public CustomHttpServletRequestWrapper(HttpServletRequest request, Long userId) {
            super(request);
            this.userId = userId;
        }

        @Override
        public String getHeader(String name) {
            if ("X-User-Id".equals(name)) {
                return String.valueOf(userId);
            }
            return super.getHeader(name);
        }

        @Override
        public Enumeration<String> getHeaderNames() {
            Set<String> headerNames = new HashSet<>();
            Enumeration<String> originalNames = super.getHeaderNames();
            while (originalNames.hasMoreElements()) {
                headerNames.add(originalNames.nextElement());
            }
            headerNames.add("X-User-Id");
            return Collections.enumeration(headerNames);
        }

        @Override
        public Enumeration<String> getHeaders(String name) {
            if ("X-User-Id".equals(name)) {
                return Collections.enumeration(Collections.singletonList(String.valueOf(userId)));
            }
            return super.getHeaders(name);
        }
    }
}

 

6) MicroService 인증 정보 획득하여 이후 프로세스 수행 ( order서비스 주문내역 구성 )

- request의 "X-User-Id"를 인자로 하여 getHeader메서드로 userId 추출

- 사용자 요청에 따른 프로세스 처리 후 회신 

 

Order List Response

    @GetMapping("/my")
    @Operation(summary="내 주문목록", description="로그인한 사용자의 주문목록조회")
    public ResponseEntity<List<Order>> getMyOrder(HttpServletRequest request) {
     // API GATEWAY에서 전달한 X-User-Id 헤더에서 사용자 ID추출
        String userIdHeader = request.getHeader("X-User-Id");
        if(userIdHeader == null) {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).build();
        }

        List<Order> order= orderService.getOrdersByUserId(Long.parseLong(userIdHeader));
        return ResponseEntity.ok(order);
    } // HttpServletRequest로 들어오는 내용은 HTTP 요청 원본에 해당, Header를 읽어들임
    
   
   public List<Order> getOrdersByUserId(Long userId) {
      return orderRepository.findByUserIdOrderByCreatedAtDesc(userId);
    }

 

 

7. 회복성 패턴 (resilience4j) 

- 장애 전파를 방지: MicroService의 장애는 다른 서비스의 수행에 지장초래 

  예) User-Service장애시 Order에서 주문처리

      JWT토큰 헤더로 전달받은 UserId로 주문 생성 후 세부 정보는 이후에 전달받을 수 있도록 설정  

 

1)  회복성 패턴 

 - Circuit Breaker, Retry, Rate Limiter, Bulkhead, Time Limiter, Cache

(1) Circuit Breaker : 원격 서비스 실패시 추가요청 차단으로 장애 확산방지

     - Open(장애발생 시 Breaker설정 ), Half-Open, Closed

(2) Retry : 일시적 장애에 대해 지정된 횟수만큼 재시도 (장애 극복시도) 

(3) Time Limiter(타임 아웃) : 특정 시간 내 응답 부재시 실패처리 (자원 낭비방지)

(4) Rate Limiter(속도제한) : 일정 시간내 받을수 있는 응답 호출수 제한 (과부하 방지)

(5) Bulkhead : 동시 요청 수 제한 (과부하 방지/서비스 격리) 

(6) Cache : 결과 캐싱 (빠른 응답제공)

 

** 실습 

- Order Service의 CreateOrder의 user호출 (주문 생성 후 User-Service 장애시 Id로만 주문 처리수행) 

- CircuitBreaker, Retry 적용 

 

2) Circuit Breaker

(1) 의존성 추가 : resilience4J (Spring Cloud Circuit Breaker)

implementation 'org.springframework.cloud:spring-cloud-starter-circuitbreaker-resilience4j'

 

(2) OrderService 회복성 로직 생성 ( UserServiceClient를 Facade객체로 감싸고 보완 )

UserService에 회복성을 추가한 UserServiceFacade생성 ( 로그 출력, Slf4j 어노테이션 ) 

 CircuitBreaker 어노테이션에 관련 마이크로 서비스와 장애시 구동할 FallbackMethod 명시

package com.sesac.orderservice.facade;

import com.sesac.orderservice.client.UserServiceClient;
import com.sesac.orderservice.client.dto.UserDto;
import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
@Slf4j
public class UserServiceFacade {

    private final UserServiceClient userServiceClient;

    @CircuitBreaker(name="user-service" , fallbackMethod = "getUserFallback")
    public UserDto getUserById(Long userId) {
        log.info("User Service호출시도 - userId={}", userId);
        return userServiceClient.getUserById(userId);
   }

    //fallBack 메서드: User Service장애시 기본 사용자 반환
    public UserDto getUserFallback(Long userId, Throwable ex) {

        log.warn("User Service 장애감지 : Fallback 실행 - userId={}", userId, ex);
        UserDto defaultUser = new UserDto();
        defaultUser.setId(userId);
        defaultUser.setName("임시 사용자");
        defaultUser.setEmail("temp@example.com");

        return defaultUser;
    }
}

 

** facade와 유사한 기능으로 처리 ( 복잡한 내용은 감추어 필요한 내용만 수행 )

   - 복잡한 여러 기능을 단순한 인터페이스로 감싸 쉽게 쓸 수 있도록 하는 구조 패턴

 

(3) Order Service에 UserServiceFacade 적용 ( 실패시 FallBack메서드 수행 )

 UserServiceClient에서 User정보 획득시도 후 장애 시 명시된 FallBack함수 수행 

//원본 코드 
@Transactional
    public Order createOrder(OrderRequestDto request) {

        //주문자 정보
        UserDto user = userServiceClient.getUserById(request.getUserId());
        if (user == null) throw new RuntimeException("User not found");
        
        //제품 정보 
        ProductDto product = productServiceClient.getProductById(request.getProductId());
        if (product == null) throw new RuntimeException("Product not found");
         
        //재고 정보
        if (product.getStockQuantity() < request.getQuantity()) {
            throw new RuntimeException("Out of stock");
        }

        Order order = new Order();
        order.setUserId(request.getUserId());
        order.setTotalAmount(product.getPrice().multiply(BigDecimal.valueOf(request.getQuantity())));
        order.setStatus("COMPLETED");

        return orderRepository.save(order);
    }

    public List<Order> getOrdersByUserId(Long userId) {
      return orderRepository.findByUserIdOrderByCreatedAtDesc(userId);
    }

}

//수정 내용 (userService에서 userServiceFacade로 변경 )
public class OrderService {
  private final OrderRepository orderRepository;
  private final ProductServiceClient productServiceClient;
  private final UserServiceFacade userServiceFacade;

  public Order findById(Long id) {
      return orderRepository.findById(id).orElseThrow(
              () -> new RuntimeException(("User not found with id: "+id))
      );
  }

    @Transactional
    public Order createOrder(OrderRequestDto request) {

        UserDto user = userServiceFacade.getUserById(request.getUserId());
        if (user == null) throw new RuntimeException("User not found");

        ProductDto product = productServiceClient.getProductById(request.getProductId());
        if (product == null) throw new RuntimeException("Product not found");

        if (product.getStockQuantity() < request.getQuantity()) {
            throw new RuntimeException("Out of stock");
        }

        Order order = new Order();
        order.setUserId(request.getUserId());
        order.setTotalAmount(product.getPrice().multiply(BigDecimal.valueOf(request.getQuantity())));
        order.setStatus("COMPLETED");

        return orderRepository.save(order);
    }

 

Order-Service 설정값 추가 ( config-repo내의 order-service.yaml ) 

- instances에 CircuitBreaker설정 추가 (@CircuitBreaker의 name기준으로 조건추가)

- sliding-window-type (sliding window에서 갯수기준 판단 )

   sliding-window : 크기(3), 최소조건 (3번 호출), 불량율 ( 50% 이상)

- 문제 해결확인(2번 호출 결과에 따라 브레이커 Close)

   half-open ( default로 beaker open 60초후에 자동설정 후 지정된 조건에 따라 close )

- actuator의 모니터 기능설정 

   endpoints : 개별 기능 노출 여부설정 ( 웹으로 노출 )

   endpoint : 웹으로 표시할 개별 기능들에 세부 동작방식 설정 

   info : 입력된 기본정보 (버전/빌드/커밋등 비민감 정보 및 사용자에 의한 정보 추가가능)

   circuitbreakerevent : circuit breaker의 open/close에 대한 이벤트 정보(디버깅 유용)

 

server:
  port: 8083
spring:
  application:
    name: order-service
  datasource:
    url: jdbc:mysql://localhost:3307/order_service_db

# User Service 호출에 대한 회복성  패턴추가
resilience4j:
  circuitbreaker:
    instances:
      user-service:   # @CircuitBreaker(name = "user-service"..) 를 의미
        sliding-window-type: count-based # user서비스를 order서비스에서 호출할 때, 호출 횟수를 카운트
        sliding-window-size: 3
        minimum-number-of-calls: 3 # 최소 호출 횟수
        failure-rate-threshold: 50 # 50% 실패 시 Circuit Open
        permitted-number-of-calls-in-half-open-state: 2 # half-open일 때 몇번 요청할지


management:
  endpoints:
    web:
      exposure:
        include: health, info, circuitbreakers, circuitbreakerevents
  endpoint:
    health:
      show-details: always
  health:
    circuitbreakers:
      enabled: true

 

④ 상태확인 ( actuator로 외부 노출 )  

- health : show details에 의해 전체 항목 표시 ( 기본 설정은 "status": "UP"만 표시됨 )

Health Check은 차후에 promethus등을 연결하여 확인가능 )

 

 

- circuitbreaker : user-service 장애시 3회 이상 오류로 "OPEN"상태 (default, "CLOSED") 

3회 이상 주문시 Circuit Breaker OPEN으로 변경 (User-Service Stop상태)

 

- circuitbreaker : CLOSED에서 HALF_OPEN으로 변경된 상태 

User-Service를 가동하고 2번 주문완료 (Half-open으로 변경)

 

User-Service재가동 후 3번째 성공시에 CircuitBreaker Close됨

 

(2) Retry

① 의존성 설정값 추가 : resilience4J 하단에 Retry추가 

- AspectOrder : 메서드를 적용 체계 설정 

   circuitbreakerAspectOrder: 1 , retryAspectOrder: 2 ( CurcuitBreaker가 Retry를 감싸줌 )

- instance ( 재시도 규칙을 설정할 항목 규정 2회, 1초 간격 ) 

//order-service.yaml에 retry추가

server:
  port: 8083
spring:
  application:
    name: order-service
  datasource:
    url: jdbc:mysql://localhost:3307/order_service_db

# User Service 호출에 대한 회복성  패턴
resilience4j:
  circuitbreaker:
    circuitbreakerAspectOrder: 1 #회복성 순서
    instances:
      user-service: # @CircuitBreaker(name = "user-service"..) 를 의미
        sliding-window-type: count-based # user서비스를 order서비스에서 호출할 때, 호출 횟수를 카운트
        sliding-window-size: 3
        minimum-number-of-calls: 3 # 최소 호출 횟수
        failure-rate-threshold: 50 # 50% 실패 시 Circuit Open
        permitted-number-of-calls-in-half-open-state: 2 # half-open일 때 몇번 요청할지

  retry:
    retryAspectOrder: 2
    instances:
      user-service:
        max-attempts: 2
        wait-duration: 1s  # 재시도 간격


management:
  endpoints:
    web:
      exposure:
        include: health, info, circuitbreakers, circuitbreakerevents
  endpoint:
    health:
      show-details: always
  health:
    circuitbreakers:
      enabled: true
      
// facade파일의 서비스 호출함수에 retry annotation추가 
    @CircuitBreaker(name="user-service" , fallbackMethod = "getUserFallback")
    @Retry(name = "user-service")
    public UserDto getUserById(Long userId) {
        log.info("User Service호출시도 - userId={}", userId);
        return userServiceClient.getUserById(userId);
   }

** AspectOrder숫자가 적은 쪽이 외부로서 내부의 메서드를 설정에 맞게 수행시키고 난 후 최종 Failure판단 

 

retry후 fallback수행 로그

1초 간격으로 Retry수행 ( circuitbreaker CLOSED에서 작동, 2번의 재시도 결과에 따라 Failure 판단 )

 

 

8. OpenTelemetry기반 분산추적  

 

1) 분산 추적 

- 다수의 마이크로 서비스를 운영하면서 장애나 병목을 파악하기 어려움

- 추적 도구로서 요청 흐름과 장애 내용 파악 ( Trace Id를 생성하여 전파 ) 및 성능 최적화 

- 필수 개념 : Trace ( 요청이 시스템을 거치는 전체과정) + Sapn (트레이스의 개별작업단위)

   Trace를 위한 Id가 생성 ( 요청이 진행되면서 내부의 세부작업에 Span이 생성, 자동 생성 )

   Span을 별도로 구성해 줄수도 있으며, 이런 경우 최적화 가능 

 

2) OpenTelemetry

-  분산 추적 도구 ( 언어에 제한없음, 현재 지속적으로 revision/최신화 중인 도구 중 하나  )

- 메트릭, 트레이스, 로그를 모두 수집하여 Export ( 시각화 도구를 부가 가능 ) 

   로그는 Loki, 트레이스는 Zipkin, 메트릭은 Prometeus + Grafana

 

(1) docker에 zipkin설정 추가 (infra)

- 포트 9411, msa-network로 설정 

version: '3.8'

services:
  mysql:
    image: mysql:8.0
    container_name: msa-mysql
    ports:
      - "3307:3306"
    volumes:
      - mysql-data:/var/lib/mysql
      - ./mysql/init:/docker-entrypoint-initdb.d
    environment:
      MYSQL_ROOT_PASSWORD: 123qwe
      MYSQL_ALLOW_EMPTY_PASSWORD: "no"
      TZ: Asia/Seoul
    command:
      - --character-set-server=utf8mb4
      - --collation-server=utf8mb4_unicode_ci
      - --skip-character-set-client-handshake
    restart: always

# Zipkin구동 추가 
  zipkin:
    image: openzipkin/zipkin:latest
    container_name: msa-zipkin
    ports:
      - "9411:9411"
    networks:
      - msa-network

volumes:
  mysql-data:

networks:
  msa-network:
    driver: bridge

 

(2) api-gateway 및 MicroService에 Zipkin추가 

 

① 의존성 추가 

 MicroService에 zipkin 의존성 추가 (기존 api-gateway에 추가했던것 )

#OpenTelemetry와 zipkim 관련 설정
    implementation 'io.micrometer:micrometer-tracing-bridge-otel'
	implementation 'io.opentelemetry:opentelemetry-exporter-zipkin'
    
    
#여러 서비스에 연결된 Interaction을 확인하고자 하면 추가 (Order-Service)
	implementation 'io.github.openfeign:feign-micrometer'

** 사용하는 spring이나 구조에 따라 도구 및 버전 설정에 유의

 

 zipkin 화면으로 확인 

 

② Trace에 관련된 설정을 dev에 추가 

- config-repo내의 공통 설정에 반영 : applicaiton-dev.yaml 반영 ( api-gateway의 profile설정도 dev로 변경 )

- Micro-service에도 반영 ( Profile dev변경)

# api-gateway의 기초를 dev로 profile설정
spring:
  profiles:
    active: dev
  config:
    import: "configserver:http://localhost:8888"
  application:
    name: api-gateway
    
#config-repo의 application-dev.yaml에 trace반영
spring:
  jpa:
    show-sql: true
    properties:
      hibernate:
        format_sql: true

management:   # actuator 및 분산추적을 위한 endpoint설정
  endpoints:
    web:
      exposure:
        include: health, info, metrics, tracing, refresh

  tracing:
    sampling:
      probability: 1.0   # tracing sampling확률설정 (100%)

  zipkin:
    tracing:
      endpoint: http://localhost:9411/api/v2/spans  #zipkin으로 tracing데이터 출력

otel:
  exporter:
    zipkin:
      endpoint: http://localhost:9411/api/v2/spans #opentelemetry의 데이터를 zipkin으로 export
  resource:
    attributes:
      service.name: ${spring.application.name} # zipkin에 마이크로서비스 이름표시

logging:
  pattern:    # 로그 메시지의 형식지정, 하나의 요청이 여러서비스에 적용될때 동일한 traceId가 부여하여 추적 
    console: "%d{yyyy-MM-dd HH:mm:ss} [%X{traceId:-},%X{spanId:-}] %-5level %logger{36} - %msg%n"
  level:
    com.sesac: DEBUG
    io.micrometer.tracing: DEBUG
    org.springframework.web: DEBUG

 

 

③ Trace ID로 추적

- 앱 구동시 관련 통신내역을 추적하여 표시, 마이크로 서비스의 로그에서도 찾을수 있음

 

① API Gateway (주문 접수 및 api/orders로 Order-Service호출 )

Order-Service 주문처리 ( 접수된 요청에 대해 createOrder 수행)

     User-Service호출 : 사용자정보조회 외 Security를 포함한 세부작업 수행

     Product-Service호출 : 상품 정보 수신 

 

 

_______________________________________________________________

 

주의 사항 

-- sql파일로 DB생성 시 한글 깨짐 주의 ( docker-compose내에 반영 ) 

--skip-character-set-client-handshake

 

-- 깃 커밋에 대한 폰트 설정 ( 한글 구동, 터미널에서 수행)

git config --global i18n.commitEncoding utf-8
git config --global i18n.logOutputEncoding utf-8

'Infra' 카테고리의 다른 글

AWS_Cloud_6th  (0) 2025.09.08
AWS_Cloud_5th  (0) 2025.09.04
AWS_CLOUD_2nd  (3) 2025.08.23
AWS_CLOUD_1st  (4) 2025.08.22