728x90
반응형

pring WebFlux의 WebClient는 기존의 RestTemplate보다 훨씬 강력하고 유연한 HTTP 클라이언트입니다.
특히 비동기/논블로킹 I/O 기반으로 만들어져 있기 때문에, 대규모 API 호출 처리, 마이크로서비스 환경, 고성능 서버에 적합합니다.


✅ WebClient 주요 장점 요약

항목설명
💡 비동기 / 논블로킹 Mono, Flux 기반으로 리액티브 스트림 처리 가능. 적은 리소스로 많은 동시 요청 처리
🧵 스레드 효율 극대화 동기 방식보다 훨씬 적은 스레드로 높은 처리량 유지 가능
🌐 RESTful API 호출 최적화 GET/POST/PUT/DELETE 등 모든 메서드 지원. 유연한 요청 구성
🔗 함수형 체이닝 .get().uri().retrieve().bodyToMono() 등 간결하고 직관적
🔐 OAuth2, Bearer 등 인증 용이 인증 헤더, 토큰 주입 등 손쉬움 (filter, mutate)
🛡️ 타임아웃 / 재시도 / fallback 설정 용이 Resilience4j 등과 궁합 좋음
📦 백프레셔(Backpressure) 소비량 제어 가능 → 부하 조절에 유리
🔁 스케줄/폴링/Streaming API 지원 SSE, WebSocket, Kafka REST 등과 쉽게 연동 가능
🧪 테스트 용이 MockWebServer 또는 WebClient.Builder로 쉽게 테스트 가능
 

 


📊 WebClient vs RestTemplate

 

항목 WebClient RestTemplate
처리 방식 비동기/논블로킹 동기/블로킹
성능 고성능, 고동시성에 유리 스레드당 1요청
사용성 함수형, 유연 단순하지만 유연성 낮음
미래성 ✅ Spring 공식 권장 🚫 Deprecated 예정 (RestTemplate)
 

✅ Spring 공식 문서에서는 WebClient를 새로운 HTTP 클라이언트로 권장하고 있으며,
RestTemplate은 향후 유지보수만 되고 기능 추가는 중단되었습니다.


💡 실무 사용 예시: 마이크로서비스 간 통신

WebClient.create("https://user-service")
         .get()
         .uri("/users/{id}", userId)
         .retrieve()
         .bodyToMono(UserDto.class)
         .timeout(Duration.ofSeconds(3))
         .retry(2)
         .subscribe(user -> log.info("사용자 정보: {}", user));

🚀 언제 WebClient를 써야 하나요?

상황권장 여부
다량의 외부 API 호출 ✅ 매우 적합
비동기 스트리밍 필요 ✅ 매우 적합 (ex: SSE, WebSocket)
단순 API 1~2건 RestTemplate도 가능하지만 WebClient로 전환 권장
마이크로서비스 통신 ✅ WebClient + Resilience4j + Retry 매우 효과적
 

🧠 결론

WebClient는 비동기, 고성능, 미래 지향적 HTTP 클라이언트이며,
특히 타사 API 연동, 마이크로서비스 환경에서 필수 도구로 자리잡고 있습니다.

728x90
반응형
728x90
반응형

Spring WebFlux의 WebClient 기반으로 타사 API를 안정적으로 호출하면서,
Resilience4j의 CircuitBreaker 및 Retry 기능을 적용한 실무 예제

🎯 목표

  • WebClient로 타사 환율 API 호출
  • 실패 시 자동 재시도 (@Retry)
  • 일정 실패율 초과 시 회로 차단 (@CircuitBreaker)
  • 실패 시 fallback 처리 (fallbackMethod)

📦 1. 의존성 추가 (Gradle)

implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'io.github.resilience4j:resilience4j-spring-boot3'

⚙️ 2. application.yml 설정

resilience4j:
  circuitbreaker:
    instances:
      exchangeApi:
        registerHealthIndicator: true
        slidingWindowSize: 10
        failureRateThreshold: 50
        waitDurationInOpenState: 5s

  retry:
    instances:
      exchangeApi:
        maxAttempts: 3
        waitDuration: 1s
        retryExceptions:
          - java.io.IOException
          - org.springframework.web.reactive.function.client.WebClientRequestException

🧩 3. WebClient 설정

@Configuration
public class WebClientConfig {
    @Bean
    public WebClient exchangeWebClient() {
        return WebClient.builder()
                .baseUrl("https://api.exchangerate-api.com/v4/latest")
                .defaultHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE)
                .build();
    }
}

✅ 4. 실제 API 호출 서비스

@Service
@RequiredArgsConstructor
public class ExchangeWebClientService {

    private final WebClient exchangeWebClient;

    @Retry(name = "exchangeApi")
    @CircuitBreaker(name = "exchangeApi", fallbackMethod = "fallbackExchangeRate")
    public Mono<BigDecimal> getExchangeRate(String currencyCode) {
        return exchangeWebClient
                .get()
                .uri("/" + currencyCode) // ex: /USD
                .retrieve()
                .bodyToMono(ExchangeRateResponse.class)
                .map(res -> res.getRates().get("KRW"));
    }

    // fallback 메서드 (Throwable을 마지막 파라미터로 받아야 함)
    public Mono<BigDecimal> fallbackExchangeRate(String currencyCode, Throwable t) {
        log.warn("Fallback triggered for {} due to {}", currencyCode, t.getMessage());
        return Mono.just(BigDecimal.ZERO); // 기본값 반환
    }
}

🧾 5. DTO 예시

@Data
public class ExchangeRateResponse {
    private String base;
    private Map<String, BigDecimal> rates;
}

🧪 6. 테스트 예시 (CommandLineRunner)

@Component
@RequiredArgsConstructor
public class ApiTestRunner implements CommandLineRunner {

    private final ExchangeWebClientService service;

    @Override
    public void run(String... args) {
        service.getExchangeRate("USD")
               .doOnNext(rate -> log.info("USD → KRW 환율: {}", rate))
               .subscribe();
    }
}

🧠 실무 팁 요약

항목팁
fallback 처리 사용자 안내 메시지 or 캐시 데이터 활용
retry 대상을 제한 retryExceptions 설정으로 4xx는 제외
WebClient timeout .responseTimeout(Duration.ofSeconds(5)) 설정 고려
모노/플럭스 fallback도 반드시 Mono 반환해야 일관성 유지됨
 

✅ 결론

기능WebClient + Resilience4j
비동기 HTTP 클라이언트 WebClient
재시도 @Retry(name = "...")
회로 차단 @CircuitBreaker(name = "...")
실패 시 대체 처리 fallbackMethod(...)
728x90
반응형
728x90
반응형

✅ 1. RetryTemplate (Spring Retry)

  • 네트워크 오류/일시적 5xx 오류 등에서 재시도하여 성공 가능성 높임

✅ 2. CircuitBreaker (Resilience4j)

  • 특정 API 호출 실패율이 급격히 올라가면 회로 차단하고 빠르게 실패 → 시스템 보호

🎯 목표 예시: 타사 환율 API 호출에 적용

public interface ExchangeApiClient {
    BigDecimal getExchangeRate(String currencyCode);
}

✅ 1. RetryTemplate 사용 예제

📦 의존성 추가 (Gradle)

implementation 'org.springframework.retry:spring-retry'
implementation 'org.springframework.boot:spring-boot-starter-aop'

📌 설정 + 빈 등록

@Configuration
public class RetryConfig {

    @Bean
    public RetryTemplate retryTemplate() {
        return RetryTemplate.builder()
                .maxAttempts(3)
                .fixedBackoff(1000)  // 1초 대기 후 재시도
                .retryOn(ResourceAccessException.class)
                .retryOn(HttpServerErrorException.class)
                .build();
    }
}

✅ 실제 사용

@Service
@RequiredArgsConstructor
public class ExchangeApiService implements ExchangeApiClient {

    private final RestTemplate restTemplate;
    private final RetryTemplate retryTemplate;

    @Override
    public BigDecimal getExchangeRate(String currencyCode) {
        return retryTemplate.execute(context -> {
            ResponseEntity<ExchangeRateResponse> response =
                restTemplate.getForEntity("https://api.example.com/exchange?code=" + currencyCode,
                                          ExchangeRateResponse.class);
            return response.getBody().getRate();
        });
    }
}
반응형

✅ 2. Resilience4j CircuitBreaker 사용 예제

📦 의존성 추가 (Gradle)

implementation 'io.github.resilience4j:resilience4j-spring-boot3'

📦 설정 (application.yml)

resilience4j:
  circuitbreaker:
    instances:
      exchangeApi:
        registerHealthIndicator: true
        slidingWindowSize: 10
        failureRateThreshold: 50
        waitDurationInOpenState: 10s

✅ 실제 사용 코드

@Service
@RequiredArgsConstructor
public class ExchangeApiServiceWithCB implements ExchangeApiClient {

    private final RestTemplate restTemplate;

    @CircuitBreaker(name = "exchangeApi", fallbackMethod = "fallbackRate")
    public BigDecimal getExchangeRate(String currencyCode) {
        ResponseEntity<ExchangeRateResponse> response =
            restTemplate.getForEntity("https://api.example.com/exchange?code=" + currencyCode,
                                      ExchangeRateResponse.class);
        return response.getBody().getRate();
    }

    // 실패 시 실행되는 fallback
    public BigDecimal fallbackRate(String currencyCode, Throwable t) {
        log.warn("Fallback for currency {} due to {}", currencyCode, t.getMessage());
        return BigDecimal.ZERO;  // 기본값 또는 캐시 사용
    }
}

💡 Retry + CircuitBreaker 같이 쓰는 방법?

Resilience4j는 @Retry, @CircuitBreaker, @RateLimiter, @Bulkhead 등을 같이 설정할 수 있습니다.

@Retry(name = "exchangeApi")
@CircuitBreaker(name = "exchangeApi", fallbackMethod = "fallbackRate")
public BigDecimal getExchangeRate(String currencyCode) { ... }

🧠 실무 팁

항목팁
리트라이 대상 5xx, Timeout, Connection refused 등만. 400, 401, 403은 리트라이 금지
서킷브레이커 기준 10번 중 5번 실패하면 차단 → 10초 후 재시도
fallback 내용 "서버 점검 중입니다", "캐시된 데이터 제공" 등 UX 배려
로그 fallback 시 warn, circuit open 시 error 로 구분
 

✅ 요약 비교

 

항목 RetryTemplate Resilience4j CircuitBreaker
기능 실패 시 자동 재시도 실패율 증가 시 자동 차단
상태 추적 없음 OPEN, HALF_OPEN 등 상태 추적
fallback 명시적 구현 필요 fallbackMethod로 지정 가능
Spring 통합 Spring Retry Spring Boot 2/3 지원 우수
728x90
반응형
728x90
반응형

📋 G1 GC 관리 항목별 Default 값 및 권장 설정 (CPU 2core / Memory 4GB)

항목JVM 옵션Default 값설명2Core/4GB 권장값
💡 최대 멈춤 시간 목표 -XX:MaxGCPauseMillis 200ms GC 한 번당 최대 멈춤 시간 목표 (Soft 목표) 그대로 사용 (200ms)
💡 초기/최대 힙 크기 -Xms, -Xmx 초기: 시스템 결정
최대: 1/4 메모리 정도
전체 JVM 힙 크기 -Xms512m -Xmx1024m
💡 G1 Region 크기 -XX:G1HeapRegionSize 자동: 1MB ~ 32MB
(기본은 1MB)
힙을 나누는 단위 (2^n 크기, 보통 2048개 Region 구성됨) 기본값 사용 (1MB)
💡 Mixed GC 트리거 비율 -XX:InitiatingHeapOccupancyPercent 45 Old 영역이 힙의 몇 % 찰 때 Mixed GC 수행 30 ~ 40
💡 Survivor 비율 -XX:SurvivorRatio 8 → Eden:Survivor = 8:1:1 Eden과 Survivor 비율 조정 기본 유지
💡 Tenuring Threshold
(Old로 가는 나이)
-XX:MaxTenuringThreshold 15 객체가 Survivor에서 몇 번 살아남아야 Old로 이동 10~15, 기본 유지
💡 Metaspace 크기 제한 -XX:MetaspaceSize, -XX:MaxMetaspaceSize 제한 없음 (MaxMetaspaceSize 없음) 클래스 메타데이터 공간 -XX:MaxMetaspaceSize=256m
💡 GC 병렬 쓰레드 수 -XX:ParallelGCThreads OS core 수 = 2 GC 수행 시 사용하는 쓰레드 수 그대로 사용
💡 동시 GC 쓰레드 수 -XX:ConcGCThreads 약 1/4 core 수 (1개) Concurrent GC 단계에서의 쓰레드 수 그대로 사용
💡 GC 로그 -Xlog:gc* or -Xloggc 로그 없음 (출력 안 함) GC 분석을 위해 수동 지정 필요 -Xlog:gc*:file=gc.log
 

🧪 권장 JVM 실행 예시 (2Core / 4GB 기준)

java \
  -Xms512m \
  -Xmx1024m \
  -XX:+UseG1GC \
  -XX:MaxGCPauseMillis=200 \
  -XX:InitiatingHeapOccupancyPercent=35 \
  -XX:MaxMetaspaceSize=256m \
  -Xlog:gc*:file=gc.log:time,uptime,level,tags \
  -jar myapp.jar

🧠 추가 팁

상황조치 방안
애플리케이션이 GC 중 자주 멈춘다 -Xmx를 줄여서 GC 대상 자체를 줄이거나 PauseMillis를 늘림
메타스페이스가 부족하다 -XX:MaxMetaspaceSize 증가 또는 클래스로딩 방식 점검
Full GC가 자주 발생한다 객체 수명 길이 확인, 캐시 객체 또는 ThreadLocal 누수 의심
 
반응형

🧭 G1 GC 관리의 핵심 포인트


1. ✅ 멈춤 시간 목표 설정 (Pause Time Goal)

  • 핵심 옵션:
  • -XX:MaxGCPauseMillis=200
  • 설명:
    GC가 한 번 실행될 때의 멈춤 시간 상한값을 설정합니다. 단위는 ms.
  • 관리 포인트:
    • 너무 짧게 설정하면 많은 Region을 수집하지 못하고 GC가 자주 발생해 성능 저하
    • 너무 길게 설정하면 사용자 체감이 느려짐 (지연 시간 증가)

2. ✅ Region 크기와 힙 구성 제어

  • 핵심 옵션:
  • -XX:G1HeapRegionSize=8m -Xms4g -Xmx8g
  • 설명:
    • 힙은 고정된 크기의 Region으로 나뉘고, 최소 1MB ~ 최대 32MB (기본은 힙 크기에 따라 자동)
  • 관리 포인트:
    • 너무 작은 Region은 관리 오버헤드 증가
    • 너무 크면 세밀한 수집이 어려움
    • 적정 Region 수는 보통 2048 ~ 4096개 사이

3. ✅ Old 영역 진입 시점 조절

  • 핵심 옵션:
  • bash
    복사편집
    -XX:InitiatingHeapOccupancyPercent=45
  • 설명:
    Old 영역이 전체 힙에서 몇 %를 차지할 때 Mixed GC를 트리거할지 결정
  • 관리 포인트:
    • 너무 늦으면 Old가 가득차 Full GC 발생
    • 너무 빠르면 자주 Mixed GC가 발생해 오버헤드

4. ✅ Tenuring Threshold 조절 (Survivor → Old 이동)

  • 핵심 옵션:
  • -XX:MaxTenuringThreshold=10
  • 설명:
    객체가 Survivor에서 살아남아 Old로 넘어가는 시점 결정
  • 관리 포인트:
    • 너무 낮으면 객체가 빨리 Old로 넘어가 Old가 빨리 차고 GC 잦아짐
    • 너무 높으면 Survivor 공간 부족 가능

5. ✅ GC 로그 기반 모니터링

  • 핵심 옵션:
  • -Xlog:gc*:file=gc.log:time,level,tags
  • 관리 포인트:
    • Young GC 빈도, Mixed GC 빈도, Full GC 여부 확인
    • 멈춤 시간(Pause Time)과 수집된 Region 수 확인

6. ✅ 클래스/Metaspace 관리

  • 핵심 옵션:
  • -XX:MaxMetaspaceSize=512m
  • 설명:
    • G1 GC는 메타스페이스도 관리 대상이므로, 클래스 누수/과다 로딩 주의
  • 관리 포인트:
    • 빈번한 동적 로딩 시, 클래스 로더 메모리 증가 → Full GC 유발 가능

7. ✅ 병렬 처리 설정 (멀티코어 대응)

  • 핵심 옵션:
  • -XX:ParallelGCThreads=4 -XX:ConcGCThreads=4
  • 설명:
    • GC 작업을 병렬로 처리할 수 있는 쓰레드 수 지정 (기본은 CPU 수에 비례)
  • 관리 포인트:
    • CPU 8개 이상인 경우 명시적으로 쓰레드 수 조정해 GC 성능 향상

📋 G1 GC 관리 요약표

 
관리 항목 JVM 옵션 예시목표 / 효과
멈춤 시간 설정 -XX:MaxGCPauseMillis=200 응답성 향상
Region 크기 조정 -XX:G1HeapRegionSize=8m GC 제어 정밀도 향상
Old GC 트리거 시점 -XX:InitiatingHeapOccupancyPercent=45 Full GC 예방
객체 수명주기 조절 -XX:MaxTenuringThreshold=10 Old 영역 과부하 방지
로그 기반 분석 -Xlog:gc*:file=gc.log:... 병목 원인 추적
Metaspace 제한 -XX:MaxMetaspaceSize=512m 클래스 메모리 제어
병렬 GC 쓰레드 설정 -XX:ParallelGCThreads=4 멀티코어 최적화
 

🔚 마무리

G1 GC는 자동 조정 기능도 강력하지만, 아래와 같은 상황에서는 명시적 튜닝이 매우 중요합니다:

  • 힙이 8~32GB 이상인 서버 애플리케이션
  • GC 로그에서 Mixed GC 또는 Full GC가 자주 발생
  • 사용자 응답 시간 민감 (웹서버, API 서버 등)
728x90
반응형
728x90
반응형

🧠 G1 GC의 주요 영역 설명

G1 GC는 아래의 세 가지 주요 영역으로 힙 메모리를 구성합니다:

1. 🟦 Eden 영역 (Eden Space)

  • 객체가 처음 생성되는 공간
  • Young GC 대상이며, 대부분의 객체는 이곳에서 생성되자마자 사라집니다 (short-lived objects)
  • 가득 차면 → Young GC 발생
  • 살아남은 객체는 Survivor로 이동

2. 🟨 Survivor 영역

  • Eden에서 살아남은 객체들이 일시적으로 이동하는 공간
  • Survivor 공간도 여러 Region으로 구성됨
  • 객체가 특정 횟수(tenuring threshold)만큼 GC에서 살아남으면 → Old 영역으로 이동

3. 🟥 Old 영역 (Tenured / Old Gen)

  • 장시간 생존한 객체들이 위치하는 공간
  • Young GC로는 수거되지 않으며, 공간이 가득 차면 Old GC (Mixed GC 또는 Full GC) 가 발생
  • Old Gen이 커지면 GC 멈춤 시간이 길어질 수 있음

📊 시각화 예시

+---------------------------+
|          Heap            |
+---------------------------+
| Eden | Survivor | Old    |
|      | (S0/S1)  |        |
+---------------------------+
  • Eden: 객체 생성 직후
  • Survivor: 살아남은 객체 저장, 두 공간이 번갈아 사용됨 (S0/S1)
  • Old: 오래된 객체, Full GC 또는 Mixed GC로 정리됨

🔄 G1 GC의 동작 흐름

1. 객체 생성 → Eden
2. GC 발생 시 Eden → Survivor 이동
3. Survivor에서 살아남으면 → Old 영역으로 이동
4. Old 영역 가득 차면 → Mixed GC (Young + Old) 또는 Full GC

🧪 G1 GC의 장점

특징설명
Region 기반 분할 힙을 고정 크기 Region으로 나눔 (기본 1~32MB)
Predictable Pause -XX:MaxGCPauseMillis로 멈춤 시간 목표 설정 가능
Mixed GC 지원 Old + Young 객체를 동시에 수집하여 효율적
멀티코어 최적화 병렬 GC, 병렬 압축 등 성능 최적화 가능
 

🔧 JVM 옵션 예시

-XX:+UseG1GC \
-XX:MaxGCPauseMillis=200 \
-XX:G1HeapRegionSize=8m \
-XX:InitiatingHeapOccupancyPercent=45

 


📘 요약

영역주요 역할관련 JVM 옵션 예시
Eden 객체 최초 생성, Young GC 대상 자동 설정 (크기 조정 가능)
Survivor Eden에서 살아남은 객체 임시 보관 -XX:SurvivorRatio=8
Old 장수 객체 저장, Full 또는 Mixed GC 대상 -XX:InitiatingHeapOccupancyPercent=45
728x90
반응형
728x90
반응형

🧨 상황: 트랜잭션 미적용 → AutoCommit 상태

  • Spring에서 @Transactional이 제대로 작동하지 않으면,
    JDBC 커넥션은 기본적으로 AutoCommit = true 상태로 동작합니다.
  • 즉, 각 SQL이 실행될 때마다 즉시 커밋되며 롤백이 불가능한 상태입니다.

⚠️ 발생 가능한 문제점 4가지

1. 데이터 불일치 / 부분 저장 문제

❌ 예시 상황

java
 
// 예: 고객 주문 저장 로직
insertOrder();        // 주문 마스터
insertOrderItems();   // 주문 상세
updateStock();        // 재고 차감

insertOrder()는 성공했고, insertOrderItems()에서 오류 발생 →
첫 insert는 이미 커밋되었기 때문에 롤백이 불가능
주문은 존재하지만 주문 품목이 없음 = 데이터 무결성 붕괴

💣 문제

  • 논리적으로는 하나의 트랜잭션이지만,
  • AutoCommit 상태에서는 작업이 쪼개져서 처리됨 → 복구 불가능

2. 동시성 문제 / Dirty Read 발생

트랜잭션이 없으면 읽기/쓰기 작업 간 경계가 없어져
다른 세션에서 중간 결과를 읽거나 덮어쓰는 현상 발생

예: 다른 트랜잭션이 아직 완료되지 않은 데이터를 읽고 변경해버림
비즈니스 로직 간 충돌


3. Rollback 불가

try-catch로 예외를 잡아도, 이미 AutoCommit 되어버린 SQL은 원복 불가

java
 
try {
    insert();   // AutoCommit됨
    update();   // 여기서 실패
} catch (Exception e) {
    // rollback(); ← rollback 불가. 이미 insert는 반영됨
}

4. DB 락이 의도치 않게 오래 유지되거나 즉시 해제됨

트랜잭션이 없으면 락도 SQL 단위로 발생 및 해제됨
→ 일부 처리 중간에는 잠금 없이 잘못된 동시처리가 발생하거나
→ 반대로 의도한 락 지속이 되지 않아 Dirty Update 발생 가능성 존재


📌 트랜잭션 없이 AutoCommit이 특히 위험한 영역

상황설명
마스터/상세 INSERT 상위 테이블만 저장되고 하위 테이블은 실패하면 참조 무결성 깨짐
계좌 이체, 포인트 적립 등 입금은 되었지만 출금이 실패하면 금액 오류 발생
통계 수치 갱신 수치 증가 후 중단 시 이중 집계 가능
로깅 / 추적 테이블 삽입 실패 오류 발생 시 아무 로그도 남지 않음
 

✅ 해결책 요약

방법설명
@Transactional 구현 클래스에 선언 프록시를 통한 트랜잭션 적용 보장
REQUIRES_NEW 등의 전파 속성 명시 독립 트랜잭션 제어 필요 시
rollbackFor = Exception.class 명시 예외 상황에 명확하게 롤백 설정
테스트 시 @Commit, @Rollback 명시 트랜잭션 테스트 커버리지 확보
 

✅ 결론

트랜잭션 미적용 상태(AutoCommit)는
부분 커밋, 복구 불가능, 동시성 충돌, 데이터 무결성 파괴 등 심각한 문제를 일으킬 수 있습니다.

항상 중요한 DML 로직은 명확히 @Transactional로 감싸야 합니다.

728x90
반응형
728x90
반응형

@Transactional을 인터페이스에 선언해도 "동작은 합니다" — 하지만 매우 제한적입니다.


📌 자세한 설명

1. Spring의 트랜잭션 처리 방식

Spring AOP(Aspect-Oriented Programming) 기반 트랜잭션 처리에서는 **프록시 객체(proxy)**가 핵심입니다.

  • @Transactional은 프록시 객체가 호출을 감싸서 트랜잭션을 적용합니다.
  • 이 프록시는 구현체(구현 클래스)에 적용된 애노테이션을 기준으로 동작합니다.

📌 인터페이스에 붙인 @Transactional은 기본적으로 무시되며, 구현체 클래스에 붙여야 정상 적용됩니다.

 


2. ✅ 왜 인터페이스에 붙여도 "동작하는 것처럼 보일 때"가 있나?

Spring Data JPA의 @Repository 인터페이스 메서드에 붙인 @Transactional은 예외입니다.

public interface UserRepository extends JpaRepository<User, Long> {

    @Transactional
    @Modifying
    @Query("DELETE FROM User u WHERE u.status = 'INACTIVE'")
    int deleteInactiveUsers();
}
  • 이 경우는 Spring Data 내부에서 메서드 구현을 동적으로 생성하면서 애노테이션을 인식합니다.
  • 하지만 일반적인 @Service나 사용자 정의 인터페이스는 해당되지 않습니다.

❗ 정리: 언제 어디에 붙여야 하나?

위치트랜잭션 적용 여부권장 여부
구현 클래스 (@Service, @Component) ✔ 정상 적용 ✔ 권장
일반 인터페이스 (@Service 아님) ✖ 미적용 ✖ 비권장
⚠️ Spring Data JPA 인터페이스 (@Query, @Modifying 포함) ✔ 일부 적용됨 ⚠ 가능 (제한적)
 
반응형

✅ 올바른 구조 제안

1. 인터페이스에는 @Modifying, @Transactional 사용 X

public interface ItemRepositoryCustom {
    String saveItems(List<ITEM_Dto> list);
}

2. 구현체 클래스에서 트랜잭션 선언

@Repository
@RequiredArgsConstructor
public class ItemRepositoryCustomImpl implements ItemRepositoryCustom {

    private final ProcedureCallService procedureCallService;
    private final Util util;

    @Override
    @Transactional
    public String saveItems(List<ITEM_Dto> list) {
        for (USP_IN_ITEM_ITEM_Dto dto : list) {
            procedureCallService.call_IN_ITEM_INS(
                dto.getCompanyId(),
                "Item",
                LocalDateTime.now(),
                dto.getInType(),
                dto.getAsAgentId(),
                dto.getItemId(),
                dto.getGoodQty(),
                dto.getRecycleQty(),
                dto.getBadQty(),
                0,
                dto.getDescription(),
                util.getLoginUserId(),
                "",
                0
            );
        }
        return "등록 완료";
    }
}

3. 컨트롤러에서 ResponseEntity 처리

 
@PostMapping("/api/items/enter")
public ResponseEntity<String> saveEntering(@RequestBody List<ITEM_Dto> dtoList) {
    String msg = itemRepositoryCustom.saveItems(dtoList);
    return ResponseEntity.ok(msg);
}

✅ 정리

항목잘못된 사용올바른 사용
@Modifying ❌ 인터페이스 + 일반 메서드 ✔ JPQL/Native Query 메서드 위에만 사용
@Transactional ❌ 인터페이스 선언부에 사용 ✔ 구현 클래스에 직접 사용
ResponseEntity ❌ Repository에서 반환 ✔ Controller에서 반환

✅ 결론

🔹 항상 구현 클래스에 @Transactional을 붙이세요.
🔹 인터페이스에 붙이는 건 혼란을 초래할 수 있고, 동작하지 않는 경우가 많습니다.

728x90
반응형
728x90
반응형

📌 gRPC란?

  • Google이 개발한 고성능, 언어 중립 RPC 프레임워크
  • 데이터 직렬화는 Protocol Buffers(proto) 사용
  • HTTP/2 기반으로 양방향 스트리밍, 성능 우수
  • REST보다 빠르며 마이크로서비스 통신에 적합

🔗 기술별 구성 방식 요약

항목 Java (Spring Boot) Kotlin (Ktor) NestJS (TypeScript)
주요 라이브러리 grpc-spring-boot-starter, protobuf grpc-kotlin, kotlinx-coroutines @grpc/grpc-js, @nestjs/microservices
서버 정의 @GRpcService로 구현 코루틴 기반 suspend 함수 사용 @GrpcMethod 데코레이터 사용
클라이언트 정의 ChannelBuilder + Stub 사용 비동기 Stub 호출 GrpcOptions + ClientGrpcProxy 사용
코드 생성 protoc → Java Stub 생성 protoc-gen-grpc-kotlin 사용 proto-loader, ts-proto, @types 등 사용
특징 성능, 안정성 높음 코루틴과 찰떡궁합 설정은 약간 복잡하지만 개발 속도 빠름

🧪 gRPC 서버-클라이언트 샘플 예제

🟦 1. .proto 파일 (공통)

// greet.proto
syntax = "proto3";

package greet;

service GreeterService {
  rpc SayHello (HelloRequest) returns (HelloResponse);
}

message HelloRequest {
  string name = 1;
}

message HelloResponse {
  string message = 1;
}

☕ Java (Spring Boot)

📌 서버 구현

@GRpcService
public class GreeterServiceImpl extends GreeterServiceGrpc.GreeterServiceImplBase {
    @Override
    public void sayHello(HelloRequest request, StreamObserver<HelloResponse> responseObserver) {
        HelloResponse response = HelloResponse.newBuilder()
                .setMessage("Hello, " + request.getName())
                .build();
        responseObserver.onNext(response);
        responseObserver.onCompleted();
    }
}

📌 클라이언트

ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 9090).usePlaintext().build();
GreeterServiceGrpc.GreeterServiceBlockingStub stub = GreeterServiceGrpc.newBlockingStub(channel);

HelloRequest request = HelloRequest.newBuilder().setName("World").build();
HelloResponse response = stub.sayHello(request);
System.out.println(response.getMessage());

🟣 Kotlin (Ktor)

📌 서버

class GreeterServiceImpl : GreeterServiceCoroutineImplBase() {
    override suspend fun sayHello(request: HelloRequest): HelloResponse {
        return HelloResponse.newBuilder()
            .setMessage("Hello, ${request.name}")
            .build()
    }
}

📌 클라이언트

val channel = ManagedChannelBuilder.forAddress("localhost", 9090).usePlaintext().build()
val stub = GreeterServiceCoroutineStub(channel)

val response = stub.sayHello(HelloRequest.newBuilder().setName("Kotlin").build())
println(response.message)

🟨 NestJS (TypeScript)

📌 서버

@GrpcMethod('GreeterService', 'SayHello')
sayHello(data: { name: string }): { message: string } {
  return { message: `Hello, ${data.name}` };
}

📌 클라이언트 설정

ClientsModule.register([
  {
    name: 'GREETER_PACKAGE',
    transport: Transport.GRPC,
    options: {
      package: 'greet',
      protoPath: join(__dirname, './proto/greet.proto'),
    },
  },
])

📌 클라이언트 호출

@Injectable()
export class GreetService {
  constructor(@Inject('GREETER_PACKAGE') private client: ClientGrpc) {}

  private greeterService: any;

  onModuleInit() {
    this.greeterService = this.client.getService('GreeterService');
  }

  async greet(name: string) {
    const res = await lastValueFrom(this.greeterService.SayHello({ name }));
    return res.message;
  }
}

✅ 마무리 요약

항목 Spring Boot Kotlin NestJS 
개발 속도 중간 (JVM 빌드 시간 존재) 빠름 매우 빠름
통신 성능 매우 우수 매우 우수 (코루틴) 우수 (Node.js 한계 있음)
사용 난이도 높은 자유도, 복잡도 있음 DSL 기반 단순화 가능 데코레이터 기반 구성 쉬움
실제 적용 사례 대기업 MSA 환경 다수 적용 실시간 처리, 백엔드 서버에 적합 빠른 MVP, 서버리스 백엔드에 적합
728x90
반응형
728x90
반응형

🚀 CI/CD 구성 비교

항목 Java (Spring Boot) Kotlin (Ktor) NestJS (TypeScript)
빌드 도구 Gradle, Maven Gradle (주로 사용) npm / yarn
테스트 실행 JUnit, Mockito Kotest, JUnit Jest
패키징 방식 .jar, .war .jar .js 또는 .ts로 빌드, dist/ 생성
Docker 사용 Dockerfile로 JAR 실행 (Java 기반 이미지) 경량 이미지 가능 (코루틴 기반 앱) Node.js 기반 Dockerfile (alpine 사용 가능)
주요 툴 Jenkins, GitHub Actions, GitLab CI GitHub Actions, CircleCI GitHub Actions, GitLab CI, Vercel 등
배포 방식 K8s, EC2, ECS, Beanstalk 등 다양 경량 서버라 빠른 배포 가능 Vercel, AWS ECS/Fargate, K8s, Railway 등 다양

예시: GitHub Actions (Spring Boot CI)

 
jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Set up JDK 17
        uses: actions/setup-java@v2
        with:
          java-version: 17
      - name: Build with Gradle
        run: ./gradlew build

 

예시: NestJS CI

jobs:
  build:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v2
      - name: Install dependencies
        run: npm install
      - name: Run tests
        run: npm run test
      - name: Build
        run: npm run build

🧱 마이크로서비스 아키텍처 구성 차이

항목 Java (Spring Boot) Kotlin (Ktor) NestJS (TypeScript)
구조 스타일 모놀리식 또는 MSA 모두 우수 MSA도 가능하지만 실무 적용은 비교적 적음 MSA 친화적 (모듈화 + 데코레이터 기반)
통신 방식 REST, gRPC, Kafka, RabbitMQ 등 다양 REST, gRPC, Kafka REST, GraphQL, Kafka, Redis pub/sub 등 다양
서비스간 분리 Spring Cloud, Eureka, Config, Gateway 지원 마이크로서비스 설정은 직접 구성 필요 NestJS Microservices 모듈 내장
공통 모듈 관리 Gradle 멀티모듈, Maven BOM 등으로 정리 Gradle 멀티모듈 구성 (복잡도 있음) NPM workspace or nx, monorepo 쉽게 구성 가능
인증 처리 방식 OAuth2, JWT, Spring Security 공유 가능 수동 공유 Auth 모듈 분리 + 공유 가능 (Passport)
API 게이트웨이 구성 Spring Cloud Gateway Ktor 자체 라우팅 or 외부 게이트웨이 사용 자체 Gateway 서버 구성 (Proxy + Middleware 활용)

🔧 MSA 실전 예: 서비스 구조 예시

Spring Boot 기반 MSA 구성

├── api-gateway
├── auth-service
├── user-service
├── product-service
├── config-server
├── discovery-server (Eureka)

NestJS 기반 MSA 구성

├── apps/
│   ├── gateway
│   ├── auth
│   ├── user
│   └── payment
├── libs/
│   ├── common
│   └── interfaces

NestJS는 @nestjs/microservices와 nestjs-config, RabbitMQ/Kafka transport 내장으로 마이크로서비스에 매우 적합합니다.


✅ 정리 요약

 

비교 항목 Spring Boot Kotlin NestJS
빌드 속도 느림 (JVM 기반) 빠름 (간결 구조) 매우 빠름 (Node 기반)
배포 유연성 클라우드 최적화 경량 서버 배포 가능 무중단 배포/경량 서버용
MSA 친화도 매우 높음 중간 매우 높음
확장성 복잡하지만 강력 간결함 빠른 팀 기반 확장
728x90
반응형
728x90
반응형

Java(Spring Boot), Kotlin(Ktor), NestJS의 비교 내용 중:

  • Kafka 연동 방식
  • DB 연결 방식
  • JWT 인증 구조
  • 환경 설정 방식
  • 동일 기능 API 예제

각 항목별로 보기 쉽고, 핵심만 빠르게 파악할 수 있도록 구성

🔄 Kafka 연동 방식

Java (Spring Boot)

  • spring-kafka 사용
  • application.yml에 설정
  • @KafkaListener로 간단한 Consumer 구현 가능
spring:
  kafka:
    bootstrap-servers: localhost:9092
    consumer:
      group-id: my-group

Kotlin (Ktor)

  • kafka-clients 직접 사용
  • 코루틴과 함께 비동기 처리
  • 설정은 코드에서 수동으로 지정
val consumer = KafkaConsumer<String, String>(props)
consumer.subscribe(listOf("topic"))

NestJS

  • @nestjs/microservices + kafkajs
  • ClientsModule을 통한 Kafka 클라이언트 등록
  • 데코레이터 방식으로 간단하게 사용 가능
ClientsModule.register([
  {
    name: 'KAFKA_SERVICE',
    transport: Transport.KAFKA,
    options: {
      client: {
        brokers: ['localhost:9092'],
      },
    },
  },
])

🛢️ DB 연결 방식

Java (Spring Boot)

  • JPA (Hibernate) + Spring Data
  • application.yml로 DB 연결 설정
  • Repository 패턴, 트랜잭션 처리 간편

Kotlin (Ktor)

  • Exposed 또는 JPA 사용
  • DSL 기반 SQL 쿼리 작성이 가능 (Exposed)
  • 설정은 코드 기반 또는 conf 파일 사용

NestJS

  • TypeORM 또는 Prisma
  • .env 파일과 TypeOrmModule.forRoot 사용
  • TypeScript로 프론트와 타입 공유 유리

🔐 JWT 인증 구조

Java (Spring Security)

  • OncePerRequestFilter로 토큰 필터링
  • SecurityConfig에서 인증 경로, 인가 설정
  • 커스터마이징 매우 세분화 가능

Kotlin (Ktor)

  • Authentication 플러그인 사용
  • JWTVerifier 설정 필요
  • call.principal<JWTPrincipal>()로 사용자 추출

NestJS

  • @nestjs/jwt + PassportModule
  • AuthGuard('jwt')로 인증 처리
  • JwtStrategy에서 토큰 검증 구현

⚙️ 환경 설정 방식

Java (Spring Boot)

  • application.yml, @Value, @ConfigurationProperties
  • @Profile을 통해 dev/stage/prod 전환 가능

Kotlin (Ktor)

  • application.conf (HOCON) 또는 코드 기반 설정
  • 타입 안전한 구성 가능

NestJS

  • .env + ConfigModule
  • ConfigService로 환경 변수 주입

🧪 동일 기능 API 예제 (GET /greet?name=홍길동 → "Hello, 홍길동!")

Java (Spring Boot)

@RestController
@RequestMapping("/api")
public class GreetController {
    @GetMapping("/greet")
    public String greet(@RequestParam String name) {
        return "Hello, " + name + "!";
    }
}

 

Kotlin (Ktor)

routing {
    get("/greet") {
        val name = call.parameters["name"] ?: "World"
        call.respondText("Hello, $name!")
    }
}

 

 

NestJS (TypeScript)

@Controller('api')
export class GreetController {
  @Get('greet')
  greet(@Query('name') name: string): string {
    return `Hello, ${name || 'World'}!`;
  }
}

필요하시면 여기서 더 확장해서 Swagger, 테스트 코드, CI/CD, 마이크로서비스 구성 차이도 정리해드릴 수 있어요!

728x90
반응형

+ Recent posts