
프로그래밍에서 어떤 분야건 간에,
운영 환경에서 스레드의 점유율을 어떻게 관리하느냐는 서비스의 성능과 안정성에 매우 중요한 요소이기에,
관리의 중요성을 알아야한다!
이 때,
처리의 효율을 높이기 위한 방식으로 우리는 가장 흔히 동기(Synchronous)와 비동기(Asynchronous) 방식이 등장한다..
그렇다면 동기와 비동기는 무엇일까?
1. 동기(Synchronous)
동기(Synchronous)란
'Sync'에서 볼 수 있듯이,
말 그대로 "동시에 일어나는 것"을 의미한다.
메인 스레드는 요청한 작업이 완료될 때까지 해당 작업을 계속 점유해야한다.
따라서 요청을 보낸 즉시! 결과가 반환 될 때까지 기다려야하며(대기),
이 동안에는 다른 작업은 처리되지 않는다.
"요청과 그 결과가 동시에 일어난다는 약속이다"
예: 파일을 읽어야 다음 코드가 실행되는 파일 I/O 동기 처리,
데이터베이스 쿼리가 끝나야 다음 로직이 이어지는 트랜잭션 처리 등이 전형적이다.
이 방식은 코드 흐름이 단순하고 직관적이지만,
대기 시간이 긴 작업이 있다면 전체 응답 속도가 느려지고 서버 자원의 효율도 낮아질 수 있다.
2. 비동기(Asynchronous)
비동기(Asynchronous)란 이름 그대로,
동시에 일어나지 않는다는 것이다.
즉, 어떤 요청이 있어도 결과가 바로 나오지 않아도 된다는 것을 의미한다. 요청과 응답이 시간적으로 반드시 일치할 필요가 없는 방식이다.
요청을 보내더라도 응답을 기다리지 않고 메인 스레드는 다음 작업을 계속 진행할 수 있다.
하나의 요청에 따른 응답을 즉시 처리하지 않아도 그 대기 시간동안 또다른 요청에 대해 처리 가능한 방식으로,
이후 결과가 준비된다면 콜백(callback), 이벤트 루프, 프로미스(Promise) 등의 메커니즘을 통해 클라이언트에서는 작동이 가능하며,
서버에서는 타임아웃을 통해 응답에 대한 처리를 하는 것도 가능하면서 다양한 비동기 응답 객체들이 존재한다.
이 방식은 특히 대기 시간이 긴 작업(예: 네트워크 통신, 대용량 파일 처리, 외부 API 호출 등)에 유리하다.
왜냐하면 대기 중에도 다른 요청을 처리할 수 있어 시스템 전체의 처리량(throughput)을 높일 수 있기 때문이다.
예: Node.js의 이벤트 루프 기반 서버, Java의 CompletableFuture,
Python의 async/await 코루틴 등은 대표적인 비동기 패턴이다.
다만, 동기 코드보다 속도가 무조건 빠르다고 볼 수는 없다!
비동기 로직은 상태 관리가 복잡하고, 과도한 컨텍스트 스위칭이 발생하면 오히려 느려질 수 있다.
비동기의 성능의 이점
비동기를 적용하면 다음과 같은 장점들이 존재한다. 요청한 작업에 대하여 완료 여부를 신경쓰지 않는다는 점이 있어서,
- 대기 시간 중 자원 활용 극대화 가능 : 네트워크나 I/O 대기 중에서도 다른 요청 처리 가능
- 높은 동시성 : 다수의 요청을 병렬적으로 처리할 수 있어 서버 확장성 향상
- 지연 작업에 유리 : 응답이 오래 걸리는 API 호출, 대용량 데이터 처리 등
즉, 비동기 방식은 “응답을 언제 받을지”에 구애받지 않는 구조를 만들 수 있어
대규모 트래픽을 처리하는 서버나 실시간 데이터 처리 환경에서 매우 중요한 선택지가 된다.
Spring에서 비동기 객체를 활용하여 스레드를 효율적으로 관리하는 방법에 대하여 확인해보자
@Configuration
@EnableAsync
public class AsyncConfig {
private static final int CORE_POOL_SIZE = 5;
private static final int MAX_POOL_SIZE = 10;
private static final int QUEUE_CAPACITY = 200;
private static final long KEEP_ALIVE_TIME = 60_000L; // ms
@Bean(name = "threadPoolExecutor")
public ThreadPoolExecutor threadPoolExecutor() {
BlockingQueue<Runnable> workQueue = new LinkedBlockingQueue<>(QUEUE_CAPACITY);
ThreadFactory threadFactory = new ThreadFactoryBuilder("app-jdk-exec-");
RejectedExecutionHandler rejection = new ThreadPoolExecutor.CallerRunsPolicy();
ThreadPoolExecutor executor = new ThreadPoolExecutor(
CORE_POOL_SIZE,
MAX_POOL_SIZE,
KEEP_ALIVE_TIME,
TimeUnit.MILLISECONDS,
workQueue,
threadFactory,
rejection
);
executor.allowCoreThreadTimeOut(true);
executor.prestartAllCoreThreads();
return executor;
}
}
application.yaml 을 통하여서도 스프링 자체에 스레드 풀에 대한 설정을 할 수 있으나,
Configuration에서 Async에 대한 Bean을 따로 지정했다.
이제 ThreadPoolExecutor에 대하여 확인을 한다면,
A
sync로 해당 빈들을 사용할 수 있다.
@Service
public class SampleService {
@Async("threadPoolExecutor")
public void doAsyncWithJdkPool() {
// JDK ThreadPoolExecutor에 의해 실행
}
}
위와 같은 형식으로 서비스 코드를 작성하면,
메인 스레드가 아닌 비동기적으로 실행되어 메인 스레드의 점유율을 낮춘다.
이러면 메인 스레드가 아닌 별도의 스레드 환경에서 실행되는 것을 확인할 수 있다.
그래서 주로 저러한 서비스 코드는, 주기적이고 정기적인 작업인 스케줄러나 혹은 비동기 다른 서비스 호출에 사용하는 것이 좋다.
스레드 풀에 대한 관리는 다른 포스트에서 하고,
이제 넘어가보도록 하자!!
그래서 동기와 비동기는 결국 “작업 순서 처리 방식의 차이”라고 이해하면 된다.
동기 방식은 항상 대기하며 처리한다.
예를 들어 연속적으로 여러 서비스를 호출하는 동기 패턴의 서비스라면,
앞선 서비스의 응답이 완료될 때까지 다음 로직이 순차적으로 대기해야 한다.
이 과정에서 네트워크 지연, 외부 API 응답 지연 등이 누적되면 전체 응답 시간이 크게 늘어날 수 있다.

바로 이 점이 현대의 MSA(Microservices Architecture)와 같은 분산 시스템 구조에서 비동기 방식이 중요해지는 이유다.
비동기 방식은 각 서비스가 응답 대기 없이 작업을 병렬적으로 이어갈 수 있게 해,
서비스 간 결합도를 낮추고 트래픽 폭증 상황에서도 높은 처리량(Throughput)과 확장성을 보장한다.
정리하자면, 동기는 “순서를 보장하는 안정적 처리”에 강점이 있지만,
MSA 환경처럼 네트워크 지연·대규모 동시 요청이 발생하는 구조에서는
비동기 아키텍처가 시스템 성능과 확장성을 유지하는 핵심 전략이 된다.
이러한 동기/비동기를 학습할 때 항상 사람들이 헷갈려하는 부분이 존재한다.
바로 블로킹/논블로킹이다.
흔히들 해당 두 개념들을 헷갈려하는 사람들이 많다.
만약 동기/비동기가 작업 완료 시점과 코드 흐름의 관계에 집중한 개념이라면,
블로킹/논블로킹의 개념은 스레드의 점유 여부를 기준으로 나뉜다.
3. 블로킹(Blocking)
요청을 받은 스레드가 다른 작업의 흐름 자체를 막는다.
즉, 스레드의 "작업"이 "완료"될 때까지 대기한다.
동기 작업에서처럼,
구현이 단순하고 디버깅도 쉽다. 그러나, I/O 작업이 오래 걸린다면 CPU가 낭비되기 쉽기 때문에 조심해야한다.
4. 논블로킹(Non-Blocking)
논블로킹 방식은 스레드가 대기하지 않고 즉시 반환을 해버린다.
논블로킹 방식으로 읽으면 파일을 다 읽지 않아도 다른 작업을 할 수 있다.
기다리지 않는다는 것이다.
빠르게 제어권을 반환해버려서(요청이 들어와서 작업이 어쨌건 간에 바로 제어권을 점령하지 않고 반환)
설계를 잘 한다면, CPU 효율이 높아진다.
그러나, 이벤트 루프 등과 같이 구현이 복잡하다.
(이에 대하여는 대표적으로 Node.js가 왜 싱글스레드인가? 라는 주제와 연관된다.
Node.js는 싱글스레드이지만, 논블로킹으로 설정하면 이벤트 루프를 통하여 비동기적으로 처리가 가능함)
그래서 예를 들어 동기-논블로킹의 조합도 가능하다.

우선 클라이언트가 요청을 보내면
서버는 동기적으로 바로 응답을 반환한다.
이때의 응답은 보통 HTTP Status 204 (No Content)처럼 성공을 알리되 본문이 없는 형태다.
이후 필요하다면,
작업이 끝난 뒤 후속 요청을 통해 작업 완료 상태를 조회하거나,
서버가 웹훅(Webhook)·푸시 알림·이벤트 메시지 등으로 최종 결과를 전달할 수도 있다.
이러한 패턴을 '동기-논블로킹 구조'라고 한다.
즉, 응답 자체는 동기적으로 즉시 반환되지만,
실제 작업 처리 과정은 논블로킹·비동기적으로 실행되는 것이다.
예시:
대규모 파일 업로드 후 처리 결과를 나중에 통지하는 서비스
주문 요청을 즉시 수락(204)한 뒤 결제/배송 준비를 백그라운드에서 진행하는 전자상거래 시스템
REST API에서 POST 요청 시 202 Accepted나 204 No Content를 보내고,
후속 GET 요청으로 처리 상태를 조회하는 구조
그래서 다음과 같이 나눌 수 있다.
| 구분 | 설명 | 예시 |
| 동기 + 블로킹 | 요청 후 결과를 기다리며 멈춤 | 대부분의 전통적 함수 호출, read() |
| 동기 + 논블로킹 | 요청 즉시 반환, 결과가 없으면 계속 확인(폴링) | socket.setblocking(False) 후 반복 확인 |
| 비동기 + 블로킹 | 비동기적으로 작업을 예약했지만 내부에서 블로킹 I/O 사용 | async/await + 동기식 파일 I/O |
| 비동기 + 논블로킹 | 결과를 콜백/이벤트로 받고, 호출 즉시 반환 | Node.js의 이벤트 루프, Python asyncio |
'CS > Network' 카테고리의 다른 글
| [CS/네트워크] 인증과 인가 방식(로그인) (0) | 2025.09.26 |
|---|---|
| [CS/네트워크] VPC와 Subnet(서브넷) (0) | 2025.09.19 |