오늘 포스팅할 주제는 CPU의 동시성과 병렬성, 그리고 백엔드에서 이를 확인한 경험에 대해서 다루겠습니다.
동시성과 병렬성은 서로 다른 의미지만 헷갈려 하는 케이스가 많습니다. 어쩌면 "동시성은 동시에... 병렬도 동시에..."라고 답하는 경우도 있을 것입니다.
동시성: 요리를 할 때 잠깐 재료 손질하고, 잠깐 물 끓이고, 잠깐 설거지하고 하는 방식
병렬성: 한 명은 재료 손질을 담당, 한 명은 설거지 담당 이렇게 진짜 동시에 하는 방식
싱글코어에서 동시성이 필요한 이유: CPU의 '놀고 있는 시간'을 없애기 위해서입니다.
현실 서버의 작업 흐름은 아래와 같습니다:
요청 수신
→ DB 조회 (대기)
→ 외부 API 호출 (대기)
→ 파일 I/O (대기)
→ 약간의 CPU 계산
대부분 요청이 들어오면 스레드가 할당되고, 대부분의 시간은 CPU 작업이 아닌 기다리는 시간을 동반합니다.
String user = userRepository.find(); // DB 대기
String profile = externalApi.call(); // 네트워크 대기
문제점:
Future<User> userFuture = findUserAsync();
Future<Profile> profileFuture = callApiAsync();
장점:
⚠️ 작업 자체의 소요시간이 줄어드나? → 아닙니다.
| 구분 | 줄어드나? |
|---|---|
| DB 쿼리 시간 | ❌ |
| 네트워크 왕복 시간 | ❌ |
| CPU 연산 시간 | ❌ |
✅줄어드는 것: CPU가 '아무것도 안 하는 시간'
1. 동기/블로킹
[CPU] A 실행
[CPU] ── 대기(DB) ─────────
[CPU] A 재개
[CPU] ── 대기(API) ────────
2. 비동기/논블로킹
[CPU] A 실행
[CPU] B 실행
[CPU] C 실행
[CPU] A 재개
[CPU] B 재개
대기 시간 사이에 다른 작업을 끼워 넣음으로써 '전체 요청 완료 시점'이 당겨집니다.
즉, 대기시간과 실행 시간을 서로 겹치게 배치하여 전체 완료 시점을 빠르게 하는 것입니다.
조금 더 본질적으로는 레이턴시 최적화가 아닌 Throughput 최적화입니다.
단일 요청:
다수 요청:
비동기는 레이턴시를 직접 줄이지는 않지만, 시스템 처리 용량을 키워서 큐 대기로 인한 레이턴시 악화를 방지합니다.
오답입니다.
오히려 비동기 작업이 이미지 리사이징 혹은 암/복호화(BCrypt)의 경우 더 느려질 수 있습니다.
비동기는 "많이 처리한다"의 개념입니다. 특히 I/O-bound 작업에서 효과적입니다.
다음 포스팅에서는 이를 기반으로 실제 Spring Boot 기반 예시 코드와 함께 비교해보겠습니다.
감사합니다.