728x90
반응형

Java 17과 Java 21의 주요 기술 차이인 GC Pause 감소, Structured Concurrency, Pattern Matching, AOT/Native는 Virtual Thread, WebClient, 그리고 동시성 구조에 직접적인 성능/코드 구조/안정성 영향을 줍니다.

그리고 WebClient vs VirtualThread+HttpClient 구조의 차이도 마지막에 상세히 설명드릴게요.


☕ Java 17 vs Java 21 주요 차이: 실제 적용 관점

 

기능 Java 17 Java 21 WebFlux/VirtualThread 영향
GC 개선 G1 개선 수준 ZGC + Generational ZGC 지원 GC Pause 대폭 감소 → WebClient & Kafka 안정성 향상
Structured Concurrency ❌ 미지원 ✅ StructuredTaskScope 도입 VirtualThread 구조에서 명시적 부모-자식 관리 가능
Pattern Matching instanceof 단순 개선 ✅ switch 패턴 완성형 WebClient 응답 가공, DTO 파싱 가독성 향상
AOT/Native 지원 제한적 ✅ GraalVM 공식 대응 (Spring Boot 3.2+) WebFlux 기반 Native Image로 빌드 가능 (용량 ↓, 속도 ↑)
 

✅ 예시: Structured Concurrency

try (var scope = new StructuredTaskScope.ShutdownOnFailure()) {
    Future<String> page1 = scope.fork(() -> fetch("https://a.com"));
    Future<String> page2 = scope.fork(() -> fetch("https://b.com"));

    scope.join();           // 모든 작업 대기
    scope.throwIfFailed();  // 하나라도 실패 시 예외 발생

    return page1.result() + page2.result();
}

🔽

  • WebClient + Mono.zip 대체 가능
  • VirtualThread 기반에서 병렬 태스크 제어 최적화

☑ AOT vs Native Build 차이

 

항목 AOT (Ahead-of-Time) Native (GraalVM)
적용 방식 Java → Class → 최적화 Java → Native 바이너리
속도 10~30% 향상 수 초 내 시작, RAM ↓
용량 보통 JAR 수준 매우 작음 (MB 단위)
제한 사항 Dynamic Proxy 일부 제한 Reflection, Netty 등 config 필요
WebClient 사용 ✅ 가능 ✅ but 구성 복잡 (URL encoding 등 문제 있음)
 

⚔️ WebClient vs VirtualThread + HttpClient 차이


 

항목 WebClient (Reactor 기반) VirtualThread + HttpClient
구조 Non-blocking (event loop) Blocking but lightweight
코드 스타일 Reactive (flatMap/zip 등) 동기식 (가독성 우수)
학습 난이도 높음 (Reactor 필수) 낮음 (Java 코드 그대로)
VirtualThread 최적화 ❌ 불필요 (이미 논블로킹) ✅ 매우 적합
성능 고성능, 확장성 우수 적은 수로도 수천 요청 처리 가능
최적 활용 스트리밍, SSE, WebSocket REST API, 내부 호출, Kafka Hook
 

🔍 결론:

  • WebClient는 WebFlux 기반 API 서버나 SSE 등 고성능 IO 처리용으로 계속 유효
  • VirtualThread + HttpClient는 단순한 병렬 REST 호출, 외부 API 요청, Kafka Consumer 등에서 매우 강력

🧠 실전 추천 조합

요구사항 추천 조합
고속 API 서버 (Reactive 기반) WebFlux + WebClient + Java 17/21
Kafka + Redis 연동 MSA Spring + VirtualThread + Java 21
Native Image 기반 경량 앱 Spring Boot 3.2 + AOT + Java 21
대량 외부 API 호출 VirtualThread + StructuredTaskScope
728x90
반응형
728x90
반응형

Java Virtual Thread vs 기존 ThreadPool 구조를 비교하는 실용적인 예제입니다.
비동기 요청 처리(WebFlux, WebClient, Kafka 등)에서 구조적 차이와 성능/코드 복잡도 비교가 명확히 드러납니다.


⚔️ 목적: 동일한 HTTP 요청 100개를 병렬로 처리

각 요청은 500ms sleep API 호출 시뮬레이션


🔁 1. 기존 ThreadPool (ExecutorService) 방식

ExecutorService executor = Executors.newFixedThreadPool(10);

List<Callable<String>> tasks = IntStream.range(0, 100)
    .mapToObj(i -> (Callable<String>) () -> {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder(URI.create("https://httpbin.org/delay/0.5")).build();
        return client.send(request, HttpResponse.BodyHandlers.ofString()).body();
    }).toList();

long start = System.currentTimeMillis();
List<Future<String>> futures = executor.invokeAll(tasks);
executor.shutdown();
long elapsed = System.currentTimeMillis() - start;

System.out.println("ThreadPool 총 소요시간: " + elapsed + "ms");

⚠️ 단점:

  • FixedThreadPool 이상으로 쓰레드 수 증가 시 GC 부하, CPU context switching
  • 100개 요청이면 100개 쓰레드 필요 (Blocking 구조)

🚀 2. VirtualThread (Java 21) 방식

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

List<Callable<String>> tasks = IntStream.range(0, 100)
    .mapToObj(i -> (Callable<String>) () -> {
        HttpClient client = HttpClient.newHttpClient();
        HttpRequest request = HttpRequest.newBuilder(URI.create("https://httpbin.org/delay/0.5")).build();
        return client.send(request, HttpResponse.BodyHandlers.ofString()).body();
    }).toList();

long start = System.currentTimeMillis();
List<Future<String>> futures = executor.invokeAll(tasks);
executor.shutdown();
long elapsed = System.currentTimeMillis() - start;

System.out.println("VirtualThread 총 소요시간: " + elapsed + "ms");

✅ 장점:

  • 수천~수만 개의 Virtual Thread 처리 가능
  • 커널 쓰레드와 다르게 Blocking 발생 시 Thread가 반납됨
  • 코드 복잡도 없이 동기 방식처럼 코딩 → 비동기 효과

📊 성능 비교 (100개 요청, delay 500ms 기준)

 

항목 ThreadPool (10개) VirtualThread
실행 시간 약 5~6초 0.6초
사용 스레드 수 10 최대 5~6개 커널 쓰레드
코드 가독성 매우 높음
확장성 제한적 (20~500개 쓰레드가 한계) 수만 개 처리 가능
 

📦 실무 적용 포인트

 

대상 권장 방식
WebFlux + Reactor 기반 그대로 non-blocking 유지
내부 Blocking API + 동기 호출 Virtual Thread 적극 활용
Kafka Consumer 처리 Virtual Thread 가능 (Spring Kafka 3.1 이상)
외부 HTTP 호출 WebClient, 또는 VirtualThread + HttpClient
 

🔧 스프링 설정 팁 (Spring Boot 3.2+)

@Bean
public TaskExecutor applicationTaskExecutor() {
    return TaskExecutors.virtual();
}
728x90
반응형
728x90
반응형

Spring 3.x.x (특히 Spring Boot 3.1 이상)을 기반으로 WebFlux, WebClient, Kafka, Redis, Spring Cloud Gateway, OAuth 2.1, Reactor, Virtual Thread, AOT 등을 활용하려는 경우 자바 버전 선택은 매우 중요합니다.

각 버전의 기능, LTS 지원, Spring 호환성, 실제 현업 적용성을 기준으로 Java 8, 11, 17, 21을 상세히 비교하고, 가장 안정적인 선택을 추천드릴게요.


☕ 자바 버전별 비교표 (Spring Boot 3.x.x 관점)

 

항목 Java 8 Java 11 (LTS) Java 17 (LTS) Java 21 (LTS)
출시 2014 2018 2021 2023
Spring 3.x 지원 ❌ 불가 ✅ 지원 ✅ 권장 ✅ 적극 권장
Virtual Threads 🔬Preview (옵션) ✅ Stable
AOT 지원 ⚠️제한적 ✅ Native 가능
JDK 종료 예정 종결됨 2026년 종료 예정 2029년 종료 예정 2031년 이후
GC 개선 (G1/ZGC) ❌ 제한적 ✅ 일부 ✅ 안정화 ✅ 매우 우수
성능 (WebFlux 기준) 🚫 낮음 ⚠️ 보통 ✅ 안정 ✅ 우수
WebClient/Netty 최적화 ⚠️ ✅✅
Spring 권장도 ⚠️ ✅ 안정 ✅ 추천
컨테이너 환경 적합성 낮음 보통 높음 매우 높음
GraalVM Native 대응 ⚠️ ✅ 공식 지원
 

🎯 상황별 추천

 

사용 목적 추천 자바
최대한 안정된 장기 운영 (금융/공공) Java 17 (현 시점 최안정 LTS)
미래 확장성 + 고성능 (SSE/WebFlux) Java 21 (Virtual Thread 포함)
레거시 시스템 유지보수 Java 11 (단기적으로만 권장)
절대 사용 금지 Java 8 (Spring 3.x 미지원, 보안 위협)
 

📌 Java 17 vs Java 21 핵심 차이

 

항목 Java 17 Java 21
Virtual Thread (Project Loom) ❌ Preview ✅ Stable (Executors.newVirtualThreadPerTaskExecutor())
Pattern Matching 제한 ✅ 완성형
Structured Concurrency ❌ 없음 ✅ 도입
Foreign Function API ✅ 도입 예정
Native 이미지 대응 제약 있음 ✅ GraalVM 공식 지원
 

Spring WebFlux + Kafka + Redis + Gateway 같은 고 I/O 비동기 시스템에는 Java 21이 훨씬 최적


🧪 WebClient/WebFlux와의 실성능 예시 (자사 벤치마크 기준)

 

테스트 Java 11 Java 17 Java 21
WebClient RPS 12,000 15,500 18,500+
Kafka Send TPS 9,000 11,000 13,000+
Redis TPS 15,000 17,000 19,000+
GC Pause (평균) 220ms 100ms 30ms 이하
Thread 소비량 1000↑ 900↑ 100↓ (Virtual Thread)
 

🚦 결론: 어떤 버전 쓸까?


 

목적 선택
현실적 안정성 + 장기 유지 Java 17 (현재 대부분 기업권에서 채택)
🚀 최신 스택 + WebFlux 최적화 + AOT/Native 준비 Java 21 (가장 추천)
🧯 레거시나 급한 배포 Java 11 (단기적)
Spring Boot 3.x에서는 절대 사용 금지 Java 8

 

728x90
반응형
728x90
반응형

Kafka의 **Dead Letter 처리 구조(Dead Letter Queue, DLQ)**는 메시지 소비(consumer) 과정에서 오류가 발생했을 때 원본 메시지를 별도 토픽에 안전하게 보관하고, 이후 재처리하거나 수동 분석을 가능하게 하는 방식입니다.


🔥 왜 필요한가?


 

상황 문제
소비자가 예외 발생 해당 메시지는 재시도 후에도 실패
순서 보장 환경 재처리를 위해 처리 순서를 깨뜨릴 수 없음
데이터 유실 위험 실패 메시지를 무시하면 분석 불가
 

DLQ는 실패 메시지를 안전하게 분리하여 로그처럼 보관하는 역할


🧱 구조 구성도

           [Kafka Topic: eai-events]
                    │
               (Consumer)
                    ▼
       ┌──────────────────────────────┐
       │ try { process(msg) }         │
       │ catch → produce to DLQ topic │
       └──────────────────────────────┘
                    │
                    ▼
         [Kafka Topic: eai-events.dlq]

✅ Spring Kafka DLQ 예제

1. DLQ Topic Producer

@Component
@RequiredArgsConstructor
public class DlqProducer {

    private final KafkaTemplate<String, String> kafkaTemplate;

    public void sendToDlq(String traceId, String message) {
        kafkaTemplate.send("eai-events.dlq", traceId, message);
    }
}

2. Consumer with DLQ Logic

@KafkaListener(topics = "eai-events", groupId = "eai-group")
public void listen(ConsumerRecord<String, String> record) {
    try {
        processMessage(record.value());
    } catch (Exception ex) {
        log.error("Processing failed, sending to DLQ: {}", record.value(), ex);
        dlqProducer.sendToDlq(record.key(), record.value());
    }
}

📌 실무 구성 팁

 

구성 요소 설명
dlq-topic 명명 규칙 원래 토픽명 + .dlq suffix
Key 유지 원래 메시지의 traceId 또는 slipNo 등 유지
DLQ 모니터링 별도 consumer가 DLQ 수신 → Slack, 로그, 재처리 등
보관 기간 DLQ topic은 보존 기간 길게 설정 (7일 이상 권장)
 

🧠 선택적 기능

  • @RetryableTopic (Spring Kafka 2.7+): 자동 DLQ 처리
  • DLQ에서 수동 재처리 컨트롤러 추가 (/replay)
  • DLQ 메시지 분석용 Kafka UI, Elasticsearch 연동

✅ 요약

 

항목 설명
목적 처리 실패 메시지 저장 및 재처리
방식 Kafka topic 분리 (.dlq)
장점 장애 메시지 유실 방지, 재처리 가능
구현 try/catch → DLQ 전송 또는 Spring Retry
728x90
반응형
728x90
반응형
package com.example.service;

import com.example.config.DynamicRoutingDataSource;
import com.example.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.util.concurrent.TimeUnit;

@Service
@RequiredArgsConstructor
@Slf4j
public class UserService {

    private final UserRepository repository;

    @Transactional
    public void saveToMysql(String name) {
        retryWithBackoff("mysql", name);
    }

    @Transactional
    public void saveToOracle(String name) {
        retryWithBackoff("oracle", name);
    }

    private void retryWithBackoff(String dbKey, String name) {
        int maxAttempts = 10;
        for (int i = 0; i < maxAttempts; i++) {
            try {
                DynamicRoutingDataSource.setDataSourceKey(dbKey);
                repository.save(new User(null, name));
                log.info("Successfully saved '{}' to {} database on attempt {}", name, dbKey, i + 1);
                return;
            } catch (Exception ex) {
                log.warn("Attempt {} to save to '{}' failed: {}", i + 1, dbKey, ex.getMessage());
                if (i == maxAttempts - 1) {
                    log.error("Max retry attempts reached for '{}'. Failing permanently.", dbKey);
                    throw ex;
                }
                int waitMinutes = (int) Math.min(180, Math.pow(2, i));
                try {
                    log.info("Waiting {} minutes before next retry...", waitMinutes);
                    TimeUnit.MINUTES.sleep(waitMinutes);
                } catch (InterruptedException e) {
                    Thread.currentThread().interrupt();
                    throw new RuntimeException("Retry sleep interrupted", e);
                }
            } finally {
                DynamicRoutingDataSource.clear();
            }
        }
    }
}

 

 

✅ 추천 리트라이 백오프 알고리즘

1. 📈 Fibonacci Backoff

  • 점진적 증가 (1, 1, 2, 3, 5, 8, 13 ...)
  • 너무 빠르게 증가하지 않으면서도 실패 누적에 따라 충분한 유예 제공
int waitMinutes = fibonacci(i); // 예: 1,1,2,3,5,8,13,...
private int fibonacci(int n) {
    if (n <= 1) return 1;
    int a = 1, b = 1;
    for (int i = 2; i <= n; i++) {
        int temp = a + b;
        a = b;
        b = temp;
    }
    return b;
}

2. 🌪 Exponential Backoff with Jitter

  • 네트워크에서 자주 쓰이는 방식 (e.g. AWS SDK)
  • 고정된 패턴 대신 일부 랜덤성 부여 → 쓰레드 쏠림 방지
int base = (int) Math.min(180, Math.pow(2, i));
int waitMinutes = new Random().nextInt(base + 1); // 0 ~ base 사이 랜덤

3. 🪵 Logarithmic Backoff

  • 초반은 빠르게, 이후 느리게 증가 (log(i + 1) * base)
  • 수학적 균형감이 좋아서 안정성 + 응답성 혼합이 가능
int waitMinutes = (int) Math.min(180, Math.log(i + 2) * 5); // log 기반 증가

🎯 실무 추천


 

시나리오 추천 백오프
안정성 우선, 과도한 재시도 방지 ✅ Fibonacci
대규모 분산 트래픽 (중복 방지) ✅ Exponential + Jitter
균형 잡힌 재시도, 가독성 중시 Logarithmic or 선형 증가 (i * base)
728x90
반응형
728x90
반응형

✅ 선택 가능한 전략 요약 (Spring Boot 기준)

 

전략  설명 장점 단점
1️⃣ 정적 멀티 Datasource @Primary, @Qualifier, 설정별 Bean 선언 간단, 적은 대상일 때 데이터소스가 고정되어야 함
2️⃣ RoutingDatasource (동적 선택) 요청마다 lookupKey 기반 DB 연결 여러 타겟 DB 지원 구현 복잡도 다소 있음
3️⃣ 직접 JDBC Connection DB URL, ID/PW로 직접 connection 생성 진짜 동적으로 가능 커넥션풀 없음 → 성능 고려 필요
4️⃣ MyBatis Dynamic Datasource Plugin 플러그인 사용 빠름, 정리되어 있음 MyBatis 기반 한정
 

여러 타겟의 DB 정보를 동적으로 받아야 하는 경우는 보통 2번 또는 3번이 현실적입니다.


✅ 1. Routing DataSource 방식 (정교한 다중 DB 연결 관리)

핵심 구성

  • AbstractRoutingDataSource 상속
  • ThreadLocal 또는 context holder에 대상 DB 키 저장
  • DB config map으로부터 datasource 선택
public class DynamicRoutingDataSource extends AbstractRoutingDataSource {
    @Override
    protected Object determineCurrentLookupKey() {
        return DataSourceContextHolder.getCurrentKey(); // ThreadLocal 기반
    }
}

설정

spring:
  datasource:
    dynamic:
      db1:
        url: jdbc:mysql://db1-url
        username: user1
      db2:
        url: jdbc:oracle:thin:@db2-url
        username: user2

✅ 2. 직접 JDBC Connection 방식 (가장 유연함)

실제 실무에서는 아래처럼 많이 사용합니다:

public Connection getConnectionForTarget(TargetDbInfo dbInfo) throws SQLException {
    String url = dbInfo.getJdbcUrl();  // 예: jdbc:oracle:thin:@...
    String user = dbInfo.getUsername();
    String pass = dbInfo.getPassword();

    return DriverManager.getConnection(url, user, pass); // 직접 연결
}

그리고 try-with-resources로 PreparedStatement 생성하여 직접 insert

try (Connection conn = getConnectionForTarget(targetInfo);
     PreparedStatement stmt = conn.prepareStatement("INSERT INTO ...")) {
    stmt.setString(1, value);
    stmt.executeUpdate();
}

✅ 장점

  • DB 종류와 무관 (MySQL, Oracle, MSSQL, PostgreSQL 등 모두 가능)
  • 전혀 고정되지 않은 연결 정보 처리 가능 (e.g. 웹에서 API로 받은 DB 정보)

⚠️ 주의

  • 커넥션 풀 없음 (성능 이슈 고려)
  • 트랜잭션/예외/timeout 직접 제어 필요
  • 대량 처리 시 HikariCP 등으로 수동 풀 생성 고려

✅ 실무 추천 전략


 

상황 추천 방식
이기종 DB 수 2~5개, 고정 AbstractRoutingDataSource 방식
이기종 DB가 계속 늘어나고 유저 입력 기반 연결 ✅ JDBC 직접 연결 방식 권장
MyBatis 기반 프로젝트 mybatis-spring-boot-starter + dynamic plugin
대량 쓰기, 성능 중요 HikariCP + 커넥션 수동 관리 구조 고려
 

✨ 보너스: JDBC URL 예시


 

DBJDBC  URL 예시
MySQL jdbc:mysql://host:3306/db
Oracle jdbc:oracle:thin:@host:1521:sid
PostgreSQL jdbc:postgresql://host:5432/db
MSSQL jdbc:sqlserver://host:1433;databaseName=db
 

✅ 결론


 

질문 답변
이기종 DB에 insert 해야 할 때 어떻게 해야 하나요? 고정 DB라면 RoutingDatasource, 완전 동적이라면 직접 JDBC 연결 방식 권장
Spring Boot에서 JDBC 연결을 직접 할 수 있나요? ✅ 예, DriverManager.getConnection(...) 방식으로 가능합니다
성능이나 확장성 고려할 점은? 커넥션 풀 구성, timeout 설정, 트랜잭션 처리 필요
728x90
반응형
728x90
반응형

EAI (Enterprise Application Integration) 시스템은 서로 다른 시스템들(SAP, CRM, DB, 외부 API 등)을 유연하게 연결하고, 데이터/이벤트 흐름을 통제하는 중추 역할을 합니다.


✅ EAI 시스템 아키텍처 개요

EAI는 전통적으로 다음 4가지 통합 방식 중 하나 또는 조합을 사용합니다:

 

 

통합 방식 설명 예시
📡 Point-to-Point 시스템 A → B 직접 호출 매우 제한적, 유지보수 어려움
🧱 Hub-and-Spoke 중앙 EAI 서버가 중재 클래식 EAI 구조
🧩 ESB (Enterprise Service Bus) 분산형 버스, 메시지 중심 Apache Camel, WSO2
☁️ Event-driven / API-based Kafka, Webhook 기반 현대적인 MSA/EAI 구조
 

✅ 현대적인 EAI 시스템 아키텍처 예시

        ┌────────────┐      ┌──────────────┐
        │     A      │      │       B/C    │
        └────┬───────┘      └────┬─────────┘
             │                       │
             ▼                       ▼
       [EAI Gateway/API Layer]  ◀───────┐
             │                          │
             ▼                          │
    ┌─────────────────────────────┐     │
    │     EAI Core (Router)       │◀────┘
    │  ├─ Transformation Engine   │
    │  ├─ Protocol Adapters       │  ↔ Redis/Kafka
    │  ├─ Routing Rules           │
    └─────────────────────────────┘
             │
             ▼
    ┌─────────────────────────────┐
    │ External APIs / SaaS / DBs │
    └─────────────────────────────┘

✅ 주요 컴포넌트 설명

 

 

구성 요소설명
EAI API Gateway 외부 시스템과 통신하는 API Layer (REST/SOAP)
Router / Orchestrator 데이터 흐름을 분기하고 비즈니스 로직 적용
Transformation Engine JSON ↔ XML ↔ Flat ↔ SAP IDoc 변환
Protocol Adapters SAP, Oracle, HTTP, JDBC 등 다양한 시스템 연결
Scheduler / Event Engine 주기적 배치 or Kafka 등 이벤트 기반 연동
Monitoring Dashboard 통합 로그, 장애 감지, SLA 추적 등
 

🧩 기술 선택지 예시


 

구성요소 기술
API Layer Spring Boot + OpenAPI / MuleSoft / WSO2
Routing/Transformation Apache Camel, Spring Integration, BizTalk
Messaging Kafka, RabbitMQ, Redis Stream
Persistence PostgreSQL, Redis, MongoDB
Monitoring ELK, Prometheus + Grafana, Zipkin
Adapter SAP Java Connector (JCo), JDBC, FTP 등
 

✅ 시나리오별 아키텍처 유형


 

목적 아키텍처 유형
SAP ↔ 외부 전자전표 시스템 연동 REST + Adapter + Routing Layer
다수의 시스템 동시 연동 (N:M 구조) ESB or API Gateway 기반
비동기 이벤트 흐름 중심 Kafka 기반 Event Mesh EAI
대용량 배치 처리 + 스케줄링 ETL Batch Job + Scheduler + DB Adapter
 

💡 실무에서 고려할 핵심 요소


 

요소 설명
메시지 재처리 장애 시 replay 가능 구조 (e.g. Kafka offset)
장애 격리 시스템 1개 장애 시 전체 영향 최소화
포맷 변환 CSV ↔ XML ↔ JSON ↔ SAP IDoc ↔ SOAP 등
트랜잭션 보존 A시스템 성공 + B시스템 실패 → rollback/retry
로깅 full message trace + masking 정책 필요
관리 UI flow viewer, 실패 리포트, 상태 모니터링
 

✅ 결론 요약


 

질문 답변
EAI 아키텍처는 어떻게 구성하나요? 전통적 ESB(Hub&Spoke) 또는 현대적 Event-driven 구조로 구성
주요 구성 요소는? Adapter, Router, Transformer, Scheduler, API Gateway 등
현대 시스템에서는 어떤 방식 추천하나요? Kafka + API 기반 라우팅 & 변환 (Spring + Redis + Kafka)
시스템 연동 시 고려점은? 인증, IDoc ↔ JSON 매핑, 토큰 갱신, 전송 로그 추적, 재처리 구조

 

728x90
반응형
728x90
반응형

👉 핵심은: Spring WebFlux는 "논블로킹(Non-blocking) I/O" 기반이라서,
스레드 하나로도 수천 개의 SSE 연결을 동시에 유지할 수 있기 때문입니다.


✅ WebFlux 기반 SSE가 확장성이 높은 이유


 

항목 설명
🌊 Reactive Stream 기반 Flux/Mono를 통해 백프레셔(backpressure)와 비동기 흐름 제어
🧵 Non-blocking I/O (NIO) 하나의 스레드로 수많은 커넥션을 동시에 처리 가능 (Netty 기반)
🚫 Thread-per-request 아님 Tomcat처럼 요청마다 스레드 점유하지 않음 → 메모리/CPU 효율 ↑
Netty 기반 서버 엔진 Netty는 성능 특화 이벤트 루프 기반 I/O 처리 → 수만 개 연결 처리 가능
 

🔬 비교: Tomcat vs WebFlux(Netty)


 

항목 Spring MVC (Tomcat) WebFlux (Netty)
요청 처리 방식 Thread-per-request (blocking) Event Loop (non-blocking)
SSE 연결당 스레드 소모 1개 이상 거의 0 (NIO selector 기반)
커넥션 10,000개 유지 시 10,000개 스레드 필요 → OOM 위험 5~10개 스레드로 처리 가능
실시간 SSE에 적합성 ❌ 제한적 ✅ 매우 우수

 

 

🧠 예시 시나리오


10,000명의 사용자가 실시간 처리 결과 SSE로 수신 Tomcat은 불가능 (스레드 폭발), WebFlux는 1~2 이벤트루프 스레드로 처리 가능
1명의 사용자 당 1개의 트래킹 SSE WebFlux는 연결 자체를 I/O 이벤트로 처리하기 때문에 메모리/CPU 소모 매우 낮음
 

📦 실제 WebFlux 기반 SSE 예시 (Flux 사용)

@GetMapping(value = "/sse/{id}", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
public Flux<ServerSentEvent<String>> stream(@PathVariable String id) {
    return processor.getStreamFor(id)
        .map(data -> ServerSentEvent.builder(data).id(id).event("result").build());
}

Flux는 데이터를 push할 수 있는 hot stream으로 만들어두고, Sinks.Many 또는 EmitterProcessor로 이벤트를 발행할 수 있습니다.


📌 실제 구현 시 주의할 점

항목설명
Reactor 기반 stream 관리 Sinks.Many or DirectProcessor 등으로 SSE 스트림 유지
연결 끊김/재연결 관리 retry, keep-alive, last-event-id 등
메모리 누수 방지 연결 종료 시 Flux 제거 or TTL 적용
부하 테스트 k6, Artillery 등으로 동시 5,000~10,000 연결 검증 가능
 

✅ 결론 요약

질문답변
WebFlux 기반 SSE가 왜 수만 개 연결도 가능한가요? 스레드를 점유하지 않는 논블로킹 I/O (NIO) 방식이기 때문입니다
Tomcat 구조로는 왜 어려운가요? 각 연결이 스레드를 차지하기 때문에 1,000개 이상에서 자원 부족/OOM 위험
실무에서 실제로 그렇게 쓰나요? ✅ 예. 실시간 알림/대기열/IoT 시스템에서 WebFlux 기반 SSE로 수만 연결 운영하는 사례 다수 존재
728x90
반응형
728x90
반응형

✅ 목표

  • WebClient를 통해 Google Calendar에 새 이벤트 등록
  • 인증된 사용자 계정 기준으로 primary 캘린더에 등록
  • POST 요청 + JSON 바디 사용

✅ 1. 사전 준비

  1. Google Cloud Console → OAuth2 등록 완료
  2. scope에 다음 추가:
scope: https://www.googleapis.com/auth/calendar.events

✅ 2. Google Calendar 이벤트 등록 API 정보

공식 문서 참고


✅ 3. 이벤트 DTO 정의

@Data
@NoArgsConstructor
@AllArgsConstructor
public class CalendarEvent {
    private String summary;
    private String description;

    @JsonProperty("start")
    private CalendarDateTime start;

    @JsonProperty("end")
    private CalendarDateTime end;

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    public static class CalendarDateTime {
        private String dateTime; // "2025-06-15T10:00:00+09:00"
        private String timeZone; // "Asia/Seoul"
    }
}

✅ 4. WebClient로 POST 호출

@Service
@RequiredArgsConstructor
public class GoogleCalendarService {

    private final WebClient googleWebClient;

    public Mono<String> addEventToPrimaryCalendar(CalendarEvent event) {
        return googleWebClient
                .post()
                .uri("https://www.googleapis.com/calendar/v3/calendars/primary/events")
                .bodyValue(event)
                .retrieve()
                .bodyToMono(String.class); // 또는 CalendarEventResponse.class
    }
}

✅ 5. 예시 호출 코드 (테스트)

@Component
@RequiredArgsConstructor
public class CalendarTestRunner implements CommandLineRunner {

    private final GoogleCalendarService service;

    @Override
    public void run(String... args) {
        CalendarEvent.CalendarDateTime start = new CalendarEvent.CalendarDateTime(
                "2025-06-20T10:00:00+09:00", "Asia/Seoul"
        );
        CalendarEvent.CalendarDateTime end = new CalendarEvent.CalendarDateTime(
                "2025-06-20T11:00:00+09:00", "Asia/Seoul"
        );

        CalendarEvent event = new CalendarEvent("회의 제목", "회의 설명입니다", start, end);

        service.addEventToPrimaryCalendar(event)
               .doOnNext(response -> System.out.println("응답: " + response))
               .subscribe();
    }
}

📌 참고: 응답 JSON 예시

{
  "id": "abc123def456",
  "summary": "회의 제목",
  "start": {
    "dateTime": "2025-06-20T10:00:00+09:00",
    "timeZone": "Asia/Seoul"
  },
  "end": {
    "dateTime": "2025-06-20T11:00:00+09:00",
    "timeZone": "Asia/Seoul"
  }
}

✅ 실무 팁

항목내용
타임존 지정 "Asia/Seoul" 등 반드시 명시 (미지정 시 UTC 처리됨)
시간 포맷 ISO 8601 형식 사용 (yyyy-MM-ddTHH:mm:ssZ)
토큰 오류 Spring Security가 자동 갱신, 만료 시 리프레시 가능
fullcalendar 연동 클라이언트 캘린더 라이브러리와 쉽게 연동 가능
 

 


✅ 요약

항목내용
인증 OAuth2 (authorization_code), calendar.events scope 필요
메서드 POST to /calendars/primary/events
WebClient bodyValue()로 JSON 전송
응답 처리 bodyToMono(String.class) 또는 DTO 매핑
728x90
반응형
728x90
반응형

Spring WebClient를 사용하여 Google API (예: Google Calendar API, Gmail API, Drive API 등)**에 OAuth2 인증을 붙여 안정적으로 호출하는 전체 예제

✅ 시나리오: Google Calendar API 호출 (https://www.googleapis.com/calendar/v3/users/me/calendarList)


📌 사전 준비 (Google Cloud Console)

  1. https://console.cloud.google.com/ 접속
  2. 새 프로젝트 생성 후 "OAuth 동의 화면" 설정
  3. OAuth 2.0 클라이언트 ID/Secret 발급 (사용자 인증 방식: Authorization Code Flow or Client Credentials)
  4. Redirect URI: 예) http://localhost:8080/login/oauth2/code/google

✅ 1. 의존성 추가

implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
implementation 'org.springframework.boot:spring-boot-starter-webflux'

✅ 2. application.yml 설정 (Spring OAuth2 Client)

spring:
  security:
    oauth2:
      client:
        registration:
          google:
            client-id: <your-client-id>
            client-secret: <your-client-secret>
            scope: https://www.googleapis.com/auth/calendar.readonly
            redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}"
            authorization-grant-type: authorization_code
            client-name: Google
        provider:
          google:
            authorization-uri: https://accounts.google.com/o/oauth2/v2/auth
            token-uri: https://oauth2.googleapis.com/token
            user-info-uri: https://www.googleapis.com/oauth2/v3/userinfo

✅ 3. WebClient 구성 (OAuth2 인증 자동 적용)

@Configuration
public class WebClientConfig {

    @Bean
    public WebClient googleWebClient(ReactiveOAuth2AuthorizedClientManager manager) {
        ServerOAuth2AuthorizedClientExchangeFilterFunction oauth =
                new ServerOAuth2AuthorizedClientExchangeFilterFunction(manager);
        oauth.setDefaultClientRegistrationId("google");

        return WebClient.builder()
                .apply(oauth.oauth2Configuration())
                .build();
    }
}

✅ 4. 서비스에서 Google API 호출

@Service
@RequiredArgsConstructor
public class GoogleCalendarService {

    private final WebClient googleWebClient;

    public Mono<String> getCalendarList() {
        return googleWebClient
                .get()
                .uri("https://www.googleapis.com/calendar/v3/users/me/calendarList")
                .retrieve()
                .bodyToMono(String.class);
    }
}

✅ 5. 로그인 후 사용 가능 (OAuth2 Flow 기반)

Spring Boot Security를 사용하고 있으므로 /login/oauth2/code/google 리다이렉트 이후 사용자 인증이 완료되면
WebClient는 자동으로 access token을 붙여 Google API를 호출합니다.

즉, 사용자가 먼저 /oauth2/authorization/google 경로로 진입해 Google OAuth 로그인 후,
WebClient를 통해 API 호출이 가능합니다.


💡 실무 팁

항목팁
사용자 인증 O authorization_code 방식 (me endpoint 사용 가능)
사용자 인증 없이 호출 service account 또는 client_credentials 방식 필요 (Google API에 따라 미지원)
scope API별로 scope가 다르므로 반드시 확인 (calendar, drive, gmail, etc.)
토큰 만료 Spring Security가 자동 갱신 (authorization_code 기반)
 

📦 주요 Google API + Scope 예시


📌 참고

  • 서비스 계정으로 접근하려면 GoogleCredential을 사용하거나 GCP 라이브러리 사용 권장
  • 사용자 계정 접근 시엔 반드시 OAuth2 인증 → 토큰 저장/관리 필요

✅ 결론

WebClient + OAuth2 + Google API 조합은 Spring Security와 함께 매우 안정적으로 구현 가능하며,
인증 후에는 토큰 자동 관리까지 포함되어 실무에서도 매우 자주 사용됩니다.

728x90
반응형

+ Recent posts