CS/OS

OS 2편 - 동기 vs 비동기, 그리고 언제 써야 하는가

isTrue 2026. 4. 7. 23:41
반응형

스레드가 "기다리는 시간"을 어떻게 쓰느냐의 차이다. 단순히 빠르고 느린 게 아니라, 상황에 따라 골라 써야 한다.


1. 동기(Synchronous)란

동기 방식에서 스레드는 작업이 끝날 때까지 그 자리에서 기다린다. DB 쿼리를 날리면 응답이 올 때까지, 외부 API를 호출하면 결과가 올 때까지 스레드는 아무것도 못 하고 점유만 한다. 이걸 블로킹(Blocking) 이라 한다.

스레드 1
├── 요청 받음
├── DB 쿼리 날림
├── ⏳ DB 응답 기다리는 중... (CPU 안 씀, 스레드 점유 중)
├── ⏳ 기다리는 중...
├── DB 응답 옴
├── 결과 가공
└── 응답 반환

DB 쿼리 100ms 중 실제 CPU를 쓰는 시간은 5ms 정도다. 나머지 95ms는 그냥 대기다. 그런데 스레드는 그 시간 내내 자리를 차지한다.

Tomcat 스레드 풀이 200개인데 동시 요청이 몰리면? 200개 스레드가 전부 DB 기다리느라 멈춰있고, 201번째 요청부터 큐에서 대기하게 된다.


 

2. 비동기(Asynchronous)란

비동기 방식에서는 I/O 작업을 던져놓고 스레드를 즉시 반납한다. 응답이 오면 이벤트가 발생하고, 그때 스레드를 다시 꺼내 나머지 처리를 한다.

스레드 1
├── 요청 받음
├── DB 쿼리 날림
├── "응답 오면 알려줘" 등록하고 → 스레드 즉시 반납
│
│   (스레드 1은 이미 다른 요청 처리 중)
│
├── DB 응답 도착 (이벤트 발생)
└── 스레드 꺼내서 결과 처리 → 응답 반환

기다리는 시간에 스레드가 다른 일을 할 수 있다. 같은 스레드 풀로 더 많은 요청을 처리할 수 있게 된다.

Spring에서 비동기 사용법

@Async를 붙이면 호출한 스레드와 별도의 스레드 풀에서 메서드가 실행된다.

@Service
public class SubtitleService {

    @Async
    public CompletableFuture<List<Subtitle>> findSubtitles(Long callId) {
        List<Subtitle> result = subtitleRepository.findByCallId(callId);
        return CompletableFuture.completedFuture(result);
    }
}

단, @EnableAsync를 설정 클래스에 추가해야 하고, Thread Pool 설정도 함께 잡아줘야 제대로 동작한다.


3. 그럼 항상 비동기가 좋은가?

아니다. 비동기가 효과 있는 상황과 없는 상황이 명확하게 갈린다.

효과 있는 경우 - 여러 I/O를 병렬로 날릴 때

두 API 결과가 서로 의존하지 않는다면 동시에 날리면 된다.

// 동기 - 순차 실행 (총 500ms)
User user = userApi.getUser(id);       // 200ms 기다림
Payment pay = paymentApi.getPay(id);   // 300ms 기다림

// 비동기 - 병렬 실행 (총 300ms)
CompletableFuture<User> userFuture = userApi.getUserAsync(id);
CompletableFuture<Payment> payFuture = paymentApi.getPayAsync(id);
CompletableFuture.allOf(userFuture, payFuture).join();

효과 없는 경우 - A 결과로 B를 해야 할 때

순서가 정해진 흐름은 비동기로 바꿔도 결국 기다리는 건 똑같다. 코드만 복잡해진다.

// 어차피 순서가 있으면 동기로 충분
User user = userApi.getUser(id);         // 이 결과로
Payment pay = paymentApi.getPay(user);   // 이걸 해야 함

 

  동기 비동기
코드 복잡도 단순 복잡
디버깅 쉬움 어려움 (스택 트레이스 추적 힘듦)
효과적인 상황 순차 의존 흐름 독립적인 I/O 병렬 처리
Spring 기본 별도 설정 필요

4. 기다리는 시간이 생기는 I/O 상황들

비동기를 고려해야 하는 건 전부 I/O 대기 상황이다.

I/O 대기 상황
├── DB 쿼리 대기
├── 외부 API 호출 대기  (RestClient, RestTemplate, WebClient)
├── 파일 읽기 / 쓰기 대기
└── 메시지 큐 대기      (Kafka, RabbitMQ 등)

공통점은 CPU가 아니라 네트워크나 디스크에 던져놓고 기다리는 것들이라는 점이다. CPU 입장에선 전부 낭비되는 시간이다.


5. 외부 API 호출 시 반드시 해야 할 것 - 타임아웃

외부 API가 느려지는 날, 타임아웃이 없으면 내 서버 스레드들이 전부 거기서 묶인다. 외부 서비스 장애가 내 서비스 장애로 전파되는 것이다.

RestClient.builder()
    .baseUrl("https://payment-api.com")
    .requestFactory(factory -> {
        factory.setConnectTimeout(Duration.ofSeconds(3));
        factory.setReadTimeout(Duration.ofSeconds(5));
    })
    .build();

비동기 여부와 상관없이, 외부 API 호출엔 타임아웃 설정이 필수다. 이게 없으면 외부 의존성 하나가 전체 서비스를 잡아먹을 수 있다.


6. 판단 기준 정리

외부 API나 DB 호출이 있을 때 이 질문을 먼저 해보자.

이 결과가 나와야 다음 걸 할 수 있나?
├── Yes → 동기로 충분, 타임아웃만 잘 설정
└── No  → 병렬 처리 가능, 비동기 고려

비동기는 무조건 좋은 게 아니다. 병렬로 처리 가능한 I/O가 있을 때 쓰는 것이고, 괜히 전부 비동기로 바꾸면 디버깅이 훨씬 어려워진다.

반응형

'CS > OS' 카테고리의 다른 글

OS 4편 - 실무 병목 찾기  (0) 2026.04.08
OS 3편 - Race Condition, synchronized (feat. volatile)  (0) 2026.04.07
OS 1편 - 프로세스, 스레드, Thread Pool  (0) 2026.04.07