비동기 처리

박성수·2023년 6월 15일
0

알고쓰자

목록 보기
3/6

비동기 처리의 목표

쓰레드를 오래 붙잡는 블로킹 I/O를 피해서 동시성·처리량을 높이고 지연을 줄이는 것

  • I/O 대기(네트워크/디스크/DB) 시간 동안 스레드를 놀리지 말자: 동시성↑, 비용↓
  • 지연 분해(외부 API/DB 느릴 때) + 격리(느린 작업을 별 풀로): 시스템 안정성↑
  • CPU 바운드면 비동기 효과가 적고, I/O 바운드일수록 효과가 큼

비동기 처리 방안

1. @Async: Spring MVC

  • 독립적이고 간단한 로직에서의 비동기 처리시 사용
  • 프록시 기반 이므로 여러 제약 사항 존재
  • 서비스 전반에 걸친 컨텍스트 전파 가 필요한 경우는 사용하면 안됨

2. Reactor (Mono/Flux): Spring WebFlux

  • Reactive Streams 표준을 구현한 JVM 리액티브 라이브러리
  • 풍부한 연산자
  • 세밀한 백프레션 제어 -> request(n)
  • 스케줄러

3. Coroutines (Suspend/Flow): Kotlin

  • kotlin에서 제공하는 언어 레벨의 동시성 모델로 비동기 코드이지만 동기처럼 읽히는 가독성이 큰 특징이며 쉬운 코드로 안전하게 동시 작업을 처리
  • 서스펜션 기반
  • 간접 백프레셔 -> 요청수로 정밀한 제어가 아닌 emit/send 방식
  • 구조화된 동시성으로 스코프가 끝나면 자식 작업 정리/취소 자동
  • 취소 전파

컨텍스트 전파?
요청을 처리하는 동안 필요한 부가정보를 스레드/코루틴/리액티브 연산자 경계를 넘어 끊기지 않게 가져가는 것으로 비동기,논블로킹에서 스레드가 바뀔때 같은 요청의 연장선임을 보장하고 로깅, 권한, 트랜잭션이 연속성을 갖도록 보장하는 것

Reactor <-> Coroutines 변환 작업

이 작업이 가능하기 때문에 Spring WebFlux + Coroutines 을 편하게 사용할 수 있는 것이다.

HTTP 컨트롤러 경계 (자동)

  • suspend 반환시 -> Mono or Publisher(Flux)로 자동 변환

Reactor Context ↔ Coroutine Context 브리지 (수동)

1. 데이터 내부 계층

  • repository 경계에서 reactive mongo 등을 쓴다면 응답 값이 Mono/Flux이므로 awaitSingle() 같은 kotlinx-coroutines-reactor의 확장 함수를 사용하여 수동으로 변환해줘야 한다.
  • webClient도 역시 Reactor이므로 mono -> suspend 변환을 위한 확장 함수 필요

2. WebFilter

// WebFilter: 요청마다 traceId 삽입
class TraceFilter : WebFilter {
  override fun filter(ex: ServerWebExchange, chain: WebFilterChain): Mono<Void> {
    val traceId = ex.request.headers.getFirst("X-Trace-Id") ?: "gen-${System.nanoTime()}"
    return chain.filter(ex).contextWrite { it.put("traceId", traceId) }
  }
}

@RestController
class DemoCtrl(private val webClient: WebClient, private val repo: UserRepo) {

  @GetMapping("/demo/{id}")
  suspend fun demo(@PathVariable id: Long): String {
    // (A) 여기까지는 필터가 심은 Reactor Context가 살아있고,
    //     이 Mono 내부에서만 traceId 조회가 됨
    val nameFromRepo = repo.findNameById(id) // Mono<String>
      .map { s -> "repo:$s" }
      .awaitSingle() // 값만 꺼내고 코루틴으로 넘어옴

    // (B) 이제 '새' Mono(WebClient)를 만들면 기존 Reactor Context는 자동 미전파
    //     아래에서 WebClient 필터가 traceId를 기대해도 못 찾음
    val fromApi = webClient.get().uri("/downstream")
      .retrieve().bodyToMono(String::class.java)
      //.contextWrite { it.put("traceId", ???) } // ← 수동으로 다시 실어줘야 함
      .awaitSingle()

    return "$nameFromRepo | $fromApi"
  }
}

해결: val rctx = coroutineContext[ReactorContext]?.context로 꺼내서 .contextWrite { it.putAll(rctx) }로 새 Mono에 실어주거나, 컨트롤러 입구에서 코루틴 컨텍스트로 옮겨 담아(예: withContext(ReactorContext(ctx))) 일관되게 전파.

3. MDC(Logback) 같은 ThreadLocal

@GetMapping("/mdc")
suspend fun mdc(): String {
  MDC.put("traceId", "t-123")  // ThreadLocal에 저장
  val v = repo.findById(1).awaitSingle()  // 값은 정상 변환

  // 다른 디스패처로 전환(혹은 코루틴 스케줄링으로 스레드 바뀜)
  return withContext(Dispatchers.Default) {
    // ❌ 여기선 MDC가 비어있거나 다른 값일 수 있음
    log.info("traceId={}", MDC.get("traceId")) // null 가능
    "ok"
  }
}

해결: 코루틴 쪽에서 MDCContext(mapOf("traceId" to ...))로 감싸 전파하거나, 실행기에 TaskDecorator를 붙여 복사/복원.

주의할 점

  1. Reactor <-> Coroutines 변환 작업은 항상 자동으로 이뤄지지 않는다.
  2. 전파되지 않는 Context 들이 있기 때문에 신경 써야 한다.
  3. 모델 스위칭(Mono<->suspend) 비용도 신경써서 한 요청안에서 스위칭을 자주 하는 로직은 지양하자
profile
Java 백엔드 개발자입니다. 제가 생각하는 개발자로서 가져야하는 업무적인 기본 소양과 현업에서 가지는 고민들을 같이 공유하고 소통하려고 합니다.

0개의 댓글