내일배움캠프

[내일배움캠프] 보안 구성, Config 서버, 분산 추적, 이벤트 드리븐

munsik22 2026. 4. 14. 14:36

📚 목차

    🧩 보안 구성

    OAuth2

    • 토큰 기반의 인증 및 권한 부여 프로토콜
    • 클라이언트 애플리케이션이 리소스 소유자의 권한을 얻어 보호된 리소스에 접근할 수 있도록 함
    • 리소스 소유자, 클라이언트, 리소스 서버, 인증 서버의 역할을 정의함

    JWT

    • JSON 형식의 자가 포함된 토큰
    • 클레임(claim)을 포함하여 사용자에 대한 정보를 전달함
    • 헤더, 페이로드, 서명으로 구성됨
    • 암호화를 통해 데이터의 무결성과 출처를 보장함
    • 특징: 자가 포함, 간결성, 서명 및 암호화

    인증 실습

    • [auth] 프로젝트 생성

    • [auth] build.gradle
      • 강의자료에는 implementation 'io.jsonwebtoken:jjwt:0.12.6' 만 적혀 있는데, 3개 다 추가해야 빌드가 된다.
    implementation 'io.jsonwebtoken:jjwt-api:0.12.6'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.6'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.6'
    • [auth] application.yml 생성
    spring:
      application:
        name: auth-service
    
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:19090/eureka/
    
    jwt:
      secret: "${JWT_SECRET}"
      access-token:
        expiration: 3600000
    
    server:
      port: 19095
    • [auth] AuthConfig 생성
      • /auth/signIn 경로는 인증 없이 접근 가능
      • 그 외의 모든 요청은 인증이 필요함
      • STATELESS: 세션을 사용하지 않음
    package com.sparta.eureka.client.auth;
    
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
    import org.springframework.security.config.http.SessionCreationPolicy;
    import org.springframework.security.web.SecurityFilterChain;
    
    @Configuration
    @EnableWebSecurity
    public class AuthConfig {
    
        @Bean
        public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
            http
                    .csrf(csrf -> csrf.disable())
                    .authorizeHttpRequests(authorize -> authorize
                            .requestMatchers("/auth/signIn").permitAll()
                            .anyRequest().authenticated()
                    )
                    .sessionManagement(session -> session
                            .sessionCreationPolicy(SessionCreationPolicy.STATELESS)
                    );
    
            return http.build();
        }
    }
    • [auth] AuthService 생성
    @Service
    public class AuthService {
    
        @Value("${spring.application.name}")
        private String issuer;
    
        @Value("${jwt.access-token.expiration}")
        private Long accessExpiration;
    
        private final SecretKey secretKey;
    
        public AuthService(@Value("${jwt.secret}") String secretKey) {
            this.secretKey = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
        }
    
        public String createAccessToken(String user_id) {
            return Jwts.builder()
                    .claim("user_id", user_id)
                    .claim("role", "ADMIN")
                    .issuer(issuer)
                    .issuedAt(new Date(System.currentTimeMillis()))
                    .expiration(new Date(System.currentTimeMillis() + accessExpiration))
                    .signWith(secretKey, Jwts.SIG.HS512)
                    .compact();
        }
    }
    • [gateway] build.gradle: JWT 의존성 추가 추가
    • [gateway] application.yml 수정
    server:
      port: 19091
    
    spring:
      application:
        name: gateway-service
      cloud:
        gateway:
          server:
            webflux:
              routes:
                - id: order-service
                  uri: lb://order-service
                  predicates:
                    - Path=/order/**
                - id: product-service
                  uri: lb://product-service
                  predicates:
                    - Path=/product/**
                - id: auth-service
                  uri: lb://auth-service
                  predicates:
                      - Path=/auth/signIn
              discovery:
                locator:
                  enabled: true
    
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:19090/eureka/
    
    jwt:
      secret: "${JWT_SECRET}"
    • [gateway] LocalJwtAuthenticationFilter 생성
    @Slf4j
    @Component
    public class LocalJwtAuthenticationFilter implements GlobalFilter {
    
        @Value("${jwt.secret}")
        private String secretKey;
    
        @Override
        public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
            String path = exchange.getRequest().getURI().getPath();
            if (path.equals("/auth/signIn")) {
                return chain.filter(exchange);
            }
    
            String token = extractToken(exchange);
    
            if (token == null || !validateToken(token)) {
                exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED);
                return exchange.getResponse().setComplete();
            }
    
            return chain.filter(exchange);
        }
    
        private String extractToken(ServerWebExchange exchange) {
            String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
            if (authHeader != null && authHeader.startsWith("Bearer ")) {
                return authHeader.substring(7);
            }
            return null;
        }
    
        private boolean validateToken(String token) {
            try {
                SecretKey key = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
                Jws<Claims> claimsJws = Jwts.parser()
                        .verifyWith(key)
                        .build().parseSignedClaims(token);
                log.info("#####payload :: " + claimsJws.getPayload().toString());
                return true;
            } catch (Exception e) {
                return false;
            }
        }
    }

    [19090] 연결된 인스턴스 확인
    [19091] 게이트웨이에 요청을 보내면 토큰이 없어서 401 에러가 뜬다.
    [19091] Access Token 발급
    [19091] 헤더에 Bearer 토큰을 포함해서 요청을 전송하면 정상적으로 응답이 온다.

    🧩 Config란?

    Spring Cloud Config

    • 분산 시스템 환경에서 중앙 집중식 구성 관리를 제공하는 프레임워크
    • 애플리케이션의 설정을 중앙에서 관리하고, 변경 사항을 실시간으로 반영 가능
    • Git, 파일 시스템, JDBC 등 다양한 저장소를 지원함
    • 주요 기능
      • 중앙 집중식 구성 관리: 모든 마이크로서비스의 설정을 중앙에서 관리함
      • 환경별 구성: 개발, 테스트, 운영 등 환경별로 구성을 분리하여 관리 가능
      • 실시간 구성 변경: 설정 변경 시 애플리케이션을 재시작하지 않고도 실시간으로 반영 가능

    실시간 구성 변경

    • Spring Cloud Bus
      • 설정 변경 사항을 실시간으로 클라이언트 애플리케이션에 반영 가능함
      • RabbitMQ나 Kafka 같은 메시징 시스템을 사용해 변경 사항을 전파해야 함
    • 수동 구성 갱신: /actuator/refresh 엔드포인트 사용 또는 Spring Boot DevTools 사용
    • Git 저장소 사용: Spring Cloud Config 서버가 Git 저장소에서 설정 파일을 읽어오도록 설정할 수 있음

    Spring Cloud Config 실습하기

    Config 서버를 생성하고 연결하면 product 애플리케이션이 로컬에서 동작할 때 포트 정보 및 메시지를 Config 서버에서 가져오게 된다.

    💡 현업에서는 직접 Config 서버를 구현하기 보다는 이미 구축된 Config 서버를 연동해서 사용하는 것이 일반적이라고 한다.
    • [config] 프로젝트 생성하기

    • [config] ConfigApplication: @EnableConfigServer 어노테이션 추가
    • [config] application.yml 생성
    server:
      port: 18080
    
    spring:
      profiles:
        active: native
      application:
        name: config-server
      cloud:
        config:
          server:
            native:
              search-locations: file:./config-repo
    
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:19090/eureka/
    • [config] config-repo/product-service.yml 생성
    server:
      port: 19093
    
    message: "product-service message"
    • [config] config-repo/product-service-local.yml 생성
    server:
      port: 19083
    
    message: "product-service-local message"
    • [product] build.gradle: implementation 'org.springframework.cloud:spring-cloud-starter-config' 추가
    • [product] application.yml 수정
      • server.port: 0은 임시 값이며, 이후 Config 서버 설정으로 덮어 씌워진다.
    server:
      port: 0
    
    spring:
      profiles:
        active: local
      application:
        name: product-service
      config:
        import: "configserver:"
      cloud:
        config:
          discovery:
            enabled: true
            service-id: config-server
    
    management:
      endpoints:
        web:
          exposure:
            include: refresh
    
    eureka:
      client:
        service-url:
          defaultZone: http://localhost:19090/eureka/
    
    message: "default message"
    • [product] ProductController 수정
      • @RefreshScope: /actuator/refresh 엔드포인트를 호출하여 설정 변경 사항을 동적으로 반영 가능함
    @RefreshScope
    @RestController
    @RequestMapping("/product")
    public class ProductController {
    
        @Value("${server.port}")
        private String serverPort;
    
        @Value("${message}")
        private String message;
    
        @GetMapping
        public String getProduct() {
            return "Product detail from PORT : " + serverPort + " and message : " + this.message ;
        }
    }

    • Eureka > Config > Product 순으로 서버를 실행한다.
      • 이전에 ProductApplication에 설정했던 VM 설정은 지워야 한다.

    ProductApplication 실행 실패

    Caused by: org.springframework.web.client.ResourceAccessException: I/O error on GET request for "http://localhost:8888/product-service/default": Connection refused: getsockopt

    로그를 보면 포트 8888로 연결을 시도했다가 실패했다고 나와 있다. spring.config.import=optional:configserver: 가 기본적으로 http://localhost:8888로 연결하기 때문에 (참고) 아래와 같이 수정해야 한다.

    • [product] application.yml 수정
    spring:
      profiles:
        active: local
      application:
        name: product-service
      config:
        import: "configserver:http://localhost:18080" # ← 수정
      cloud:
        config:
          discovery:
            enabled: false
    • http://localhost:18080/product-service/default에 접속했을 때 아래처럼 나오면 된다.
    {
        name: "product-service",
        profiles: [
        	"default"
        ],
        label: null,
        version: null,
        state: null,
        propertySources: [
            {
                name: "classpath:/config-repo/product-service.yml",
                source: {
                    server.port: 19093,
                    message: "product-service message"
                }
            }
        ]
    }

    19083 설정이 잘 적용되었다.
    [19083] 로컬 메시지 출력

    다음으로 메시지 업데이트 테스트를 진행해보자.

    • [config] product-service-local.yml 수정 후 ConfigApplication 재시작
    server:
      port: 19083
    
    message: "product-service-local message 안녕하세요"
    • POST /actuator/refresh

    바뀐 메시지 확인 완료

    🧩 분산 추적이란?

    분산 추적

    • 분산 시스템에서 서비스 간의 요청 흐름을 추적하고 모니터링하는 방법
    • 각 서비스의 호출 관계와 성능을 시각화하여 문제를 진단하고 해결 가능함
    • 주요 개념
      • 트레이스: 하나의 요청이 시작부터 끝까지 각 서비스를 거치는 전체 흐름
      • 스팬: 분산 추적에서 가장 작은 단위. 특정 서비스 내에서의 개별 작업 또는 요청
      • 컨텍스트: 요청이 서비스 간에 전달될 때 함께 전파되어 각 서비스가 요청의 전체 흐름에 대한 정보
    • 분산 추적이 필요한 이유
      • MSA에서는 여러 서비스가 협력하여 하나의 요청을 처리함
      • 서비스 간의 복잡한 호출 관계로 인해 문제 발생 시 원인을 파악하기 어려울 수 있음
      • 분산 추적을 통해 각 서비스의 호출 흐름을 명확히 파악하고, 성능 병목이나 오류를 빠르게 진단할 수 있음

    MicroMeter

    • Spring 기반 애플리케이션에서 메트릭을 수집하고 모니터링하기 위한 라이브러리
    • 각 서비스의 성능 지표를 수집하고, Prometheus, Grafana 등과 연동하여 시각화
    • 분산 추적을 위한 기능도 제공하여 서비스 간의 호출 흐름 추적
    • 특징: 다양한 메트릭 수집, 유연한 연동, 추적 기능

    Zipkin

    • 트레이스 데이터를 수집하고 시각화하는 분산 추적 시스템
    • 각 서비스의 트레이스와 스팬 데이터를 저장하고, 이를 통해 호출 흐름을 시각화
    • 특징: 데이터 수집 및 저장, 시각화, 검색 및 필터링
    • Zipkin 서버 실행: Docker를 사용해서 실행한다.
    docker run -d -p 9411:9411 openzipkin/zipkin
    • Zipkin 대시보드: http://localhost:9411

    분산 추적 예제

    • 서비스 호출 흐름 추적
      • 예제 서비스 간의 호출 흐름을 추적하고, Zipkin 대시보드에서 시각화한다.
      • 각 서비스 호출 시 트레이스와 스팬이 생성되고, Zipkin 서버로 전송된다.
    • 성능 병목 진단
      • Zipkin 대시보드를 통해 성능 병목이 발생하는 부분을 식별한다.
      • 각 스팬의 소요 시간과 호출 관계를 분석하여 성능 문제를 진단하고 해결한다.

    분산 추적 실습하기

    • [product][order] build.gradle에 아래 의존성 추가
    implementation 'io.micrometer:micrometer-tracing-bridge-brave'
    implementation 'io.github.openfeign:feign-micrometer'
    implementation 'io.zipkin.reporter2:zipkin-reporter-brave'
    • [product][order] application.yml 수정
    management:
      zipkin:
        tracing:
          endpoint: "http://localhost:9411/api/v2/spans"
      tracing:
        sampling:
          probability: 1.0
    • Docker 설치 (참고)
    • Zipkin Docker 컨테이너 실행
      ▼ 더보기
      docker run -d -p 9411:9411 openzipkin/zipkin
      에러가 발생했다???
      # 실패한 컨테이너 흔적 지우기
      docker ps -a
      docker rm -f ...
      • 9411 포트가 사용 중인지 확인했지만 사용 중이 아니었다.
      netstat -aon | findstr :9411
      • WSL 종료 및 Docker Desktop 종료 후 재시작
      wsl --shutdown
      • WSL 상태 확인
      wsl -l -v
      wsl -d docker-desktop echo WSL 2 is working
      • 이렇게 했는데도 실패해서 다른 호스트 포트로 테스트했더니 드디어 성공했다.
      docker run -d --name zipkin -p 19411:9411 openzipkin/zipkin
      • 포트를 19411로 열었으므로 application.yml도 맞춰서 수정해야 한다.
      management:
        zipkin:
          tracing:
            endpoint: "http://localhost:19411/api/v2/spans"
    • Eureka > Order > Product 순으로 서버 실행
    • http://localhost:19411/zipkin/에서 Zipkin 대시보드 확인 가능
    • RUN QUERY > Spans 3 > SHOW 클릭

    🧩 이벤트 드리븐이란?

    이벤트 드리븐 아키텍처

    • 시스템에서 발생하는 이벤트(상태 변화나 행동)를 기반으로 동작하는 소프트웨어 설계 스타일
    • 이벤트는 비동기적으로 처리되며 서비스 간의 느슨한 결합을 통해 독립적으로 동작할 수 있게 한다.
    • 주요 개념
      • 이벤트: 시스템 내에서 발생하는 상태 변화나 행동을 나타내는 메시지
      • 이벤트 소스: 이벤트를 생성하여 이벤트 버스에 전달하는 역할
      • 이벤트 핸들러: 이벤트를 수신하여 처리하는 역할
      • 이벤트 버스: 이벤트 소스와 이벤트 핸들러 간의 메시지 전달을 중개
    • 장점
      • 느슨한 결합: 서비스 간 결합도를 낮추고 강한 종속성을 제거해 독립적인 개발/배포 가능
      • 확장성: 이벤트 프로듀서와 컨슈머를 독립적으로 확장 가능
      • 비동기 처리: 시스템 응답성 향상 및 성능 최적화
    • 단점
      • 복잡성 증가
      • 장애 전파
    • 예시 - 온라인 쇼핑몰
      1. 이벤트 소스: 사용자가 주문을 한다. → 주문 서비스가 주문 생성 이벤트를 발생시킨다.
      2. 이벤트 버스: Kafka나 RabbitMQ 같은 메시지 브로커가 주문 생성 이벤트를 전달한다.
      3. 이벤트 핸들러
        • 재고 서비스: 주문 생성 이벤트를 수신하여 재고를 확인하고 업데이트한다.
        • 배송 서비스: 주문 생성 이벤트를 수신하여 배송 준비를 시작한다.
        • 결제 서비스: 주문 생성 이벤트를 수신하여 결제 처리를 한다.

    Spring Cloud Stream

    • 이벤트 드리븐 MSA를 구축하기 위한 프레임워크
    • Kafka, RabbitMQ 등의 메시지 브로커와 통합하여 이벤트 스트리밍을 처리함
    • 프로듀서와 컨슈머 간의 통신을 추상화하여 간편하게 이벤트 기반 애플리케이션 개발 가능
    • 특징: 바인더 추상화, 프로듀서/컨슈머 모델, 유연한 설정