동기와 비동기 완전 정복 | 블로킹 / 논블로킹 & 언어별 예제 포함
동기 비동기 차이부터 블로킹/논블로킹 개념, async/await 패턴까지 실전 예제와 함께 완벽하게 정리한 프로그래밍 필수 가이드입니다.
동기와 비동기의 핵심 개념
현대 프로그래밍에서 동기 비동기 프로그래밍은 애플리케이션 성능과 사용자 경험을 결정하는 핵심 요소입니다.
특히 웹 서버, 대용량 데이터 처리, 실시간 통신 시스템에서 동기 vs 비동기 선택은 전체 시스템 구조를 좌우합니다.
동기(Synchronous) 처리 방식
동기 처리는 작업이 순차적으로 실행되는 방식입니다.
한 작업이 완료될 때까지 다음 작업은 대기 상태에 있으며, 코드 실행 흐름이 직관적이고 예측 가능합니다.
# Python 동기 처리 예제
def read_file():
with open('data.txt', 'r') as f:
content = f.read() # 파일 읽기가 완료될 때까지 대기
return content
def process_data(data):
result = data.upper() # 데이터 처리
return result
# 순차 실행
data = read_file() # 1. 파일 읽기 완료 대기
result = process_data(data) # 2. 파일 읽기 완료 후 실행
print(result) # 3. 처리 완료 후 출력
동기 처리의 장점은 코드 흐름이 명확하고 디버깅이 쉽다는 점입니다.
하지만 I/O 바운드 작업에서는 대기 시간 동안 CPU가 유휴 상태가 되어 비효율적입니다.
비동기(Asynchronous) 처리 방식
비동기 처리는 작업의 완료를 기다리지 않고 다음 코드를 실행하는 방식입니다.
이벤트 루프와 콜백 메커니즘을 통해 여러 작업을 동시에 처리할 수 있습니다.
// JavaScript 비동기 처리 예제
function fetchUserData(userId) {
fetch(`https://api.example.com/users/${userId}`)
.then(response => response.json()) // 응답 대기 중에도 다른 코드 실행
.then(data => console.log(data))
.catch(error => console.error(error));
}
console.log('요청 시작');
fetchUserData(1);
console.log('요청 후 즉시 실행'); // API 응답을 기다리지 않고 실행됨
비동기 프로그래밍의 핵심은 CPU가 I/O 작업의 완료를 기다리는 동안 다른 작업을 처리할 수 있다는 점입니다.
블로킹 vs 논블로킹 완벽 이해
동기 비동기와 함께 이해해야 할 개념이 블로킹과 논블로킹입니다.
많은 개발자들이 동기=블로킹, 비동기=논블로킹으로 오해하지만 실제로는 별개의 개념입니다.
블로킹(Blocking)과 논블로킹(Non-blocking) 차이
구분 | 블로킹 | 논블로킹 |
---|---|---|
제어권 | 호출된 함수가 제어권 유지 | 호출한 함수가 즉시 제어권 반환 |
대기 방식 | 작업 완료까지 호출자 대기 | 작업 완료 여부와 무관하게 즉시 반환 |
실행 흐름 | 작업이 끝날 때까지 중단 | 계속 진행 가능 |
사용 사례 | 파일 동기 읽기, DB 동기 쿼리 | 이벤트 기반 서버, 실시간 통신 |
블로킹은 작업이 완료될 때까지 스레드를 점유하는 방식입니다.
논블로킹은 작업 완료와 관계없이 즉시 반환하여 다른 작업을 계속 처리할 수 있습니다.
Node.js 공식 문서에서는 이 개념을 I/O 작업을 중심으로 자세히 설명하고 있습니다.
4가지 조합 패턴 분석
동기/비동기와 블로킹/논블로킹을 조합하면 4가지 패턴이 나타납니다.
1. 동기 + 블로킹
가장 일반적인 패턴으로, 작업이 완료될 때까지 대기합니다.
// Java 동기 블로킹 예제
import java.io.*;
public class SyncBlocking {
public static void main(String[] args) throws IOException {
FileReader fr = new FileReader("data.txt");
BufferedReader br = new BufferedReader(fr);
String line = br.readLine(); // 파일 읽기 완료까지 블로킹
System.out.println(line);
br.close();
}
}
2. 비동기 + 논블로킹
현대 웹 애플리케이션에서 가장 많이 사용되는 패턴입니다.
Node.js의 이벤트 루프가 대표적인 예시입니다.
// Node.js 비동기 논블로킹 예제
const fs = require('fs').promises;
async function processFiles() {
try {
// 논블로킹으로 파일 읽기 시작
const file1Promise = fs.readFile('file1.txt', 'utf8');
const file2Promise = fs.readFile('file2.txt', 'utf8');
// 두 파일을 동시에 읽음 (병렬 처리)
const [data1, data2] = await Promise.all([file1Promise, file2Promise]);
console.log('File 1:', data1);
console.log('File 2:', data2);
} catch (error) {
console.error('Error:', error);
}
}
processFiles();
console.log('파일 읽기 시작됨 - 다른 작업 계속 가능');
3. 동기 + 논블로킹
작업 완료를 주기적으로 확인하는 폴링(polling) 패턴입니다.
# Python 동기 논블로킹 예제 (폴링)
import select
import socket
sock = socket.socket()
sock.setblocking(False) # 논블로킹 설정
try:
sock.connect(('example.com', 80))
except BlockingIOError:
pass
# 연결 완료까지 주기적으로 확인
while True:
ready = select.select([], [sock], [], 0) # 동기적으로 상태 확인
if ready[1]:
print('연결 완료')
break
4. 비동기 + 블로킹
실무에서는 거의 사용하지 않는 패턴입니다.
비동기로 요청했지만 결과를 기다리며 블로킹되는 비효율적인 구조입니다.
비동기 코드 패턴 심층 분석
비동기 프로그래밍은 여러 패턴으로 구현할 수 있으며, 각 패턴마다 장단점이 존재합니다.
콜백(Callback) 패턴
가장 전통적인 비동기 처리 방식으로, 작업 완료 시 실행될 함수를 전달합니다.
// 콜백 패턴 예제
function fetchData(callback) {
setTimeout(() => {
const data = { id: 1, name: 'User' };
callback(null, data); // 에러와 결과를 콜백으로 전달
}, 1000);
}
fetchData((error, data) => {
if (error) {
console.error('Error:', error);
return;
}
console.log('Data:', data);
});
콜백 패턴의 문제점은 여러 비동기 작업을 연결할 때 발생하는 '콜백 지옥(Callback Hell)'입니다.
중첩된 콜백은 코드 가독성을 크게 떨어뜨립니다.
Promise 패턴
콜백의 단점을 개선한 패턴으로, 비동기 작업의 성공/실패를 객체로 표현합니다.
// Promise 패턴 예제
function fetchUserData(userId) {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (userId > 0) {
resolve({ id: userId, name: 'User' });
} else {
reject(new Error('Invalid user ID'));
}
}, 1000);
});
}
fetchUserData(1)
.then(user => {
console.log('User:', user);
return fetchUserData(2); // Promise 체이닝
})
.then(user2 => {
console.log('User 2:', user2);
})
.catch(error => {
console.error('Error:', error);
});
Promise는 체이닝을 통해 순차적인 비동기 작업을 깔끔하게 표현할 수 있습니다.
MDN Promise 문서에서 더 자세한 내용을 확인할 수 있습니다.
async / await 패턴
Promise를 더 직관적으로 사용할 수 있게 해주는 문법적 설탕(Syntactic Sugar)입니다.
// async/await 패턴 예제
async function processUsers() {
try {
const user1 = await fetchUserData(1);
console.log('User 1:', user1);
const user2 = await fetchUserData(2);
console.log('User 2:', user2);
// 병렬 처리
const [user3, user4] = await Promise.all([
fetchUserData(3),
fetchUserData(4)
]);
console.log('User 3:', user3);
console.log('User 4:', user4);
} catch (error) {
console.error('Error:', error);
}
}
processUsers();
async / await는 비동기 코드를 동기 코드처럼 작성할 수 있게 해줍니다.
가독성이 크게 향상되며 에러 처리도 try-catch로 직관적으로 할 수 있습니다.
이벤트 루프 작동 원리
비동기 프로그래밍의 핵심은 이벤트 루프(Event Loop)입니다.
JavaScript와 Node.js에서 단일 스레드로도 높은 동시성을 달성할 수 있는 비밀입니다.
JavaScript 런타임 구조
JavaScript 엔진은 다음과 같은 구조로 동작합니다.
┌─────────────────────────┐
│ Call Stack │ ← 실행 중인 함수 스택
├─────────────────────────┤
│ Web APIs │ ← 비동기 작업 처리 (브라우저/Node.js 제공)
├─────────────────────────┤
│ Callback Queue │ ← 완료된 비동기 작업의 콜백 대기
├─────────────────────────┤
│ Microtask Queue │ ← Promise, async/await 콜백 대기
└─────────────────────────┘
↑
Event Loop
이벤트 루프는 Call Stack이 비었을 때 Queue에서 콜백을 가져와 실행합니다.
Node.js 이벤트 루프 페이즈
Node.js의 이벤트 루프는 여러 페이즈(Phase)로 구성되어 있습니다.
// 이벤트 루프 페이즈별 실행 순서 예제
console.log('1. Script Start');
setTimeout(() => {
console.log('4. Timers Phase - setTimeout');
}, 0);
setImmediate(() => {
console.log('6. Check Phase - setImmediate');
});
Promise.resolve().then(() => {
console.log('3. Microtask Queue - Promise');
});
process.nextTick(() => {
console.log('2. Next Tick Queue');
});
console.log('Script End');
// 실행 결과:
// 1. Script Start
// Script End
// 2. Next Tick Queue
// 3. Microtask Queue - Promise
// 4. Timers Phase - setTimeout
// 6. Check Phase - setImmediate
Node.js 이벤트 루프는 다음 순서로 실행됩니다.
- Timers Phase: setTimeout, setInterval 콜백 실행
- Pending Callbacks: 시스템 작업 콜백 실행
- Idle, Prepare: 내부 동작
- Poll Phase: I/O 이벤트 대기 및 콜백 실행
- Check Phase: setImmediate 콜백 실행
- Close Callbacks: close 이벤트 콜백 실행
각 페이즈 사이에는 Microtask Queue(Promise)와 Next Tick Queue가 먼저 실행됩니다.
Node.js 공식 이벤트 루프 가이드에서 더 자세한 내용을 확인하세요.
언어별 비동기 구현 비교
각 프로그래밍 언어는 고유한 방식으로 비동기를 구현합니다.
Python asyncio
Python은 asyncio 라이브러리를 통해 비동기 프로그래밍을 지원합니다.
import asyncio
import aiohttp
async def fetch_url(session, url):
async with session.get(url) as response:
return await response.text()
async def main():
urls = [
'https://api.example.com/data1',
'https://api.example.com/data2',
'https://api.example.com/data3'
]
async with aiohttp.ClientSession() as session:
# 병렬로 여러 URL 요청
tasks = [fetch_url(session, url) for url in urls]
results = await asyncio.gather(*tasks)
for i, result in enumerate(results):
print(f'Response {i+1}: {len(result)} bytes')
# Python 3.7 이상
asyncio.run(main())
Python의 async/await는 코루틴(coroutine)을 기반으로 동작합니다.
Python asyncio 공식 문서에서 전체 API를 확인할 수 있습니다.
JavaScript/TypeScript
JavaScript는 언어 차원에서 비동기를 지원하는 대표적인 언어입니다.
// TypeScript 비동기 예제
interface UserData {
id: number;
name: string;
email: string;
}
async function fetchMultipleUsers(userIds: number[]): Promise<UserData[]> {
try {
// 병렬 요청
const promises = userIds.map(id =>
fetch(`https://api.example.com/users/${id}`)
.then(res => res.json())
);
const users = await Promise.all(promises);
return users;
} catch (error) {
console.error('Fetch error:', error);
throw error;
}
}
// 순차 처리
async function processInSequence() {
const user1 = await fetchMultipleUsers([1]);
console.log('User 1:', user1);
const user2 = await fetchMultipleUsers([2]);
console.log('User 2:', user2);
}
// 병렬 처리
async function processInParallel() {
const [users1, users2] = await Promise.all([
fetchMultipleUsers([1]),
fetchMultipleUsers([2])
]);
console.log('Users 1:', users1);
console.log('Users 2:', users2);
}
Java CompletableFuture
Java는 CompletableFuture로 비동기 처리를 구현합니다.
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
public class AsyncJavaExample {
public static void main(String[] args) throws ExecutionException, InterruptedException {
// 비동기 작업 생성
CompletableFuture<String> future1 = CompletableFuture.supplyAsync(() -> {
sleep(1000);
return "Result 1";
});
CompletableFuture<String> future2 = CompletableFuture.supplyAsync(() -> {
sleep(2000);
return "Result 2";
});
// 두 작업이 모두 완료될 때까지 대기
CompletableFuture<Void> combinedFuture = CompletableFuture.allOf(future1, future2);
combinedFuture.thenRun(() -> {
try {
System.out.println(future1.get());
System.out.println(future2.get());
} catch (Exception e) {
e.printStackTrace();
}
});
// 메인 스레드 대기
combinedFuture.get();
}
private static void sleep(long millis) {
try {
Thread.sleep(millis);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
Go 고루틴
Go는 고루틴(goroutine)으로 경량 스레드 기반 동시성을 제공합니다.
package main
import (
"fmt"
"net/http"
"sync"
"time"
)
func fetchURL(url string, ch chan<- string, wg *sync.WaitGroup) {
defer wg.Done()
start := time.Now()
resp, err := http.Get(url)
if err != nil {
ch <- fmt.Sprintf("Error: %s", err)
return
}
defer resp.Body.Close()
ch <- fmt.Sprintf("%s: %d bytes in %v", url, resp.ContentLength, time.Since(start))
}
func main() {
urls := []string{
"https://golang.org",
"https://google.com",
"https://github.com",
}
ch := make(chan string)
var wg sync.WaitGroup
// 고루틴으로 병렬 실행
for _, url := range urls {
wg.Add(1)
go fetchURL(url, ch, &wg)
}
// 별도 고루틴에서 채널 닫기
go func() {
wg.Wait()
close(ch)
}()
// 결과 출력
for result := range ch {
fmt.Println(result)
}
}
I/O 바운드 vs 계산 바운드
비동기 선택 기준을 이해하려면 작업 특성을 파악해야 합니다.
I/O 바운드 작업
입출력 대기 시간이 전체 작업 시간의 대부분을 차지하는 작업입니다.
// I/O 바운드 작업 예제
const fs = require('fs').promises;
async function ioHeavyTask() {
const startTime = Date.now();
// 파일 읽기 (I/O 작업)
const file1 = await fs.readFile('large-file-1.txt', 'utf8');
const file2 = await fs.readFile('large-file-2.txt', 'utf8');
// 네트워크 요청 (I/O 작업)
const response = await fetch('https://api.example.com/data');
const data = await response.json();
console.log(`Total time: ${Date.now() - startTime}ms`);
}
I/O 바운드 작업에서는 비동기 처리가 매우 효과적입니다.
CPU는 I/O 대기 중에 다른 작업을 처리할 수 있습니다.
계산 바운드 작업
CPU 연산이 전체 작업 시간의 대부분을 차지하는 작업입니다.
// 계산 바운드 작업 예제
const { Worker } = require('worker_threads');
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
// 메인 스레드를 블로킹
function cpuHeavyInMain() {
console.log('계산 시작');
const result = fibonacci(40); // CPU 집약적 연산
console.log('결과:', result);
}
// Worker Thread로 분리
function cpuHeavyInWorker() {
const worker = new Worker(`
const { parentPort } = require('worker_threads');
function fibonacci(n) {
if (n <= 1) return n;
return fibonacci(n - 1) + fibonacci(n - 2);
}
parentPort.postMessage(fibonacci(40));
`, { eval: true });
worker.on('message', (result) => {
console.log('Worker 결과:', result);
});
console.log('Worker 실행 중 - 메인 스레드는 자유롭게 동작');
}
계산 바운드 작업에서는 단순 비동기만으로는 성능 개선이 어렵습니다.
스레드나 프로세스를 분리하여 병렬 처리해야 합니다.
동기 vs 비동기 선택 기준
올바른 선택을 위한 실전 가이드입니다.
비동기를 사용해야 하는 경우
1. 네트워크 요청이 많은 경우
// 여러 API를 호출하는 경우
async function fetchDashboardData() {
const [users, posts, comments] = await Promise.all([
fetch('/api/users').then(r => r.json()),
fetch('/api/posts').then(r => r.json()),
fetch('/api/comments').then(r => r.json())
]);
return { users, posts, comments };
}
2. 파일 I/O가 빈번한 경우
import asyncio
import aiofiles
async def process_multiple_files(file_paths):
async def read_file(path):
async with aiofiles.open(path, 'r') as f:
return await f.read()
# 여러 파일을 동시에 읽기
contents = await asyncio.gather(*[read_file(path) for path in file_paths])
return contents
3. 실시간 통신이 필요한 경우
// WebSocket 서버 예제
const WebSocket = require('ws');
const wss = new WebSocket.Server({ port: 8080 });
wss.on('connection', (ws) => {
ws.on('message', async (message) => {
// 비동기로 메시지 처리
const result = await processMessage(message);
ws.send(result);
});
});
동기를 사용해야 하는 경우
1. 순서가 중요한 트랜잭션
def bank_transaction(from_account, to_account, amount):
# 순서가 보장되어야 하는 작업
withdraw(from_account, amount) # 1. 출금
deposit(to_account, amount) # 2. 입금
log_transaction(from_account, to_account, amount) # 3. 로그
2. 간단한 스크립트나 배치 작업
# 간단한 데이터 변환 스크립트
def convert_csv_to_json(csv_file):
data = read_csv(csv_file)
json_data = convert_to_json(data)
write_json_file(json_data)
3. CPU 집약적 작업이 주인 경우
// 이미지 처리, 암호화 등
function processImage(imageData) {
const resized = resize(imageData);
const filtered = applyFilter(resized);
return compress(filtered);
}
선택 기준 요약 표
기준 | 동기 사용 | 비동기 사용 |
---|---|---|
작업 특성 | CPU 집약적, 순차 처리 필수 | I/O 집약적, 병렬 처리 가능 |
응답 시간 | 즉시 결과 필요 | 대기 시간 허용 |
동시 요청 수 | 소수의 요청 | 다수의 동시 요청 |
코드 복잡도 | 단순한 로직 | 복잡한 비즈니스 로직 |
에러 처리 | 간단한 try-catch | 복잡한 에러 핸들링 필요 |
비동기 라이브러리 생태계
각 언어별 주요 비동기 라이브러리를 소개합니다.
JavaScript/Node.js
Axios: HTTP 클라이언트 라이브러리
const axios = require('axios');
async function fetchWithAxios() {
try {
const response = await axios.get('https://api.example.com/data');
return response.data;
} catch (error) {
console.error('Request failed:', error);
throw error;
}
}
Axios 공식 문서에서 더 많은 기능을 확인하세요.
RxJS: 리액티브 프로그래밍 라이브러리
const { Observable, interval } = require('rxjs');
const { map, filter, take } = require('rxjs/operators');
// 스트림 기반 비동기 처리
interval(1000)
.pipe(
take(5),
filter(x => x % 2 === 0),
map(x => x * 2)
)
.subscribe(value => console.log(value));
Python
aiohttp: 비동기 HTTP 클라이언트/서버
import aiohttp
import asyncio
async def fetch_multiple_urls():
urls = [
'https://api.example.com/endpoint1',
'https://api.example.com/endpoint2',
'https://api.example.com/endpoint3'
]
async with aiohttp.ClientSession() as session:
tasks = []
for url in urls:
task = asyncio.create_task(fetch_url(session, url))
tasks.append(task)
results = await asyncio.gather(*tasks)
return results
async def fetch_url(session, url):
async with session.get(url) as response:
return await response.json()
Celery: 분산 작업 큐 시스템
from celery import Celery
app = Celery('tasks', broker='redis://localhost:6379')
@app.task
def process_data(data):
# 백그라운드에서 실행될 무거운 작업
result = heavy_computation(data)
return result
# 비동기로 작업 실행
task = process_data.delay({'key': 'value'})
Celery 공식 문서에서 설정 방법을 확인할 수 있습니다.
Java
Spring WebFlux: 리액티브 웹 프레임워크
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
public class ReactiveClient {
private final WebClient webClient;
public ReactiveClient() {
this.webClient = WebClient.create("https://api.example.com");
}
public Mono<String> fetchData() {
return webClient.get()
.uri("/data")
.retrieve()
.bodyToMono(String.class)
.doOnSuccess(data -> System.out.println("Success: " + data))
.doOnError(error -> System.err.println("Error: " + error));
}
}
실전 비동기 패턴과 모범 사례
프로덕션 환경에서 비동기 코드를 작성할 때 주의해야 할 사항들입니다.
에러 처리 전략
비동기 코드에서 에러 처리는 특히 중요합니다.
// 잘못된 에러 처리
async function badErrorHandling() {
const data = await fetchData(); // 에러 발생 시 프로그램 종료
return data;
}
// 올바른 에러 처리
async function goodErrorHandling() {
try {
const data = await fetchData();
return { success: true, data };
} catch (error) {
console.error('Fetch failed:', error);
return { success: false, error: error.message };
}
}
// Promise.all에서 부분 실패 처리
async function handlePartialFailure() {
const promises = [
fetchData(1).catch(err => ({ error: err.message })),
fetchData(2).catch(err => ({ error: err.message })),
fetchData(3).catch(err => ({ error: err.message }))
];
const results = await Promise.all(promises);
const successful = results.filter(r => !r.error);
const failed = results.filter(r => r.error);
return { successful, failed };
}
타임아웃 설정
무한 대기를 방지하기 위한 타임아웃 패턴입니다.
// 타임아웃 헬퍼 함수
function withTimeout(promise, timeoutMs) {
return Promise.race([
promise,
new Promise((_, reject) =>
setTimeout(() => reject(new Error('Timeout')), timeoutMs)
)
]);
}
// 사용 예제
async function fetchWithTimeout() {
try {
const data = await withTimeout(
fetch('https://api.example.com/data'),
5000 // 5초 타임아웃
);
return await data.json();
} catch (error) {
if (error.message === 'Timeout') {
console.error('Request timed out');
}
throw error;
}
}
동시성 제어
너무 많은 요청을 동시에 보내지 않도록 제어하는 패턴입니다.
// 동시성 제한 함수
async function limitConcurrency(tasks, limit) {
const results = [];
const executing = [];
for (const task of tasks) {
const promise = task().then(result => {
executing.splice(executing.indexOf(promise), 1);
return result;
});
results.push(promise);
executing.push(promise);
if (executing.length >= limit) {
await Promise.race(executing);
}
}
return Promise.all(results);
}
// 사용 예제
async function batchProcess() {
const urls = Array.from({ length: 100 }, (_, i) =>
`https://api.example.com/item/${i}`
);
const tasks = urls.map(url => () => fetch(url).then(r => r.json()));
// 최대 5개씩만 동시 실행
const results = await limitConcurrency(tasks, 5);
return results;
}
재시도 로직
실패한 요청을 자동으로 재시도하는 패턴입니다.
// 재시도 함수
async function retryWithBackoff(fn, maxRetries = 3, delay = 1000) {
for (let i = 0; i < maxRetries; i++) {
try {
return await fn();
} catch (error) {
if (i === maxRetries - 1) throw error;
const waitTime = delay * Math.pow(2, i); // 지수 백오프
console.log(`Retry ${i + 1} after ${waitTime}ms`);
await new Promise(resolve => setTimeout(resolve, waitTime));
}
}
}
// 사용 예제
async function fetchWithRetry() {
return retryWithBackoff(
() => fetch('https://api.example.com/unstable-endpoint'),
3, // 최대 3번 재시도
1000 // 초기 대기 시간 1초
);
}
메모리 누수 방지
비동기 작업에서 메모리 누수를 방지하는 방법입니다.
// 잘못된 예: 이벤트 리스너 정리 안 함
function badEventHandling() {
const ws = new WebSocket('ws://example.com');
ws.addEventListener('message', handleMessage);
// ws 연결이 끊겨도 리스너가 남아있음
}
// 올바른 예: 정리 로직 포함
function goodEventHandling() {
const ws = new WebSocket('ws://example.com');
const cleanup = () => {
ws.removeEventListener('message', handleMessage);
ws.removeEventListener('close', cleanup);
};
ws.addEventListener('message', handleMessage);
ws.addEventListener('close', cleanup);
return cleanup; // 정리 함수 반환
}
// AbortController를 사용한 요청 취소
async function cancellableFetch() {
const controller = new AbortController();
setTimeout(() => controller.abort(), 5000); // 5초 후 취소
try {
const response = await fetch('https://api.example.com/data', {
signal: controller.signal
});
return await response.json();
} catch (error) {
if (error.name === 'AbortError') {
console.log('Request cancelled');
}
throw error;
}
}
성능 최적화 전략
비동기 코드의 성능을 극대화하는 실전 팁입니다.
병렬 처리 vs 순차 처리
// 순차 처리 (느림)
async function sequential() {
const user = await fetchUser(1); // 1초
const posts = await fetchPosts(1); // 1초
const comments = await fetchComments(1); // 1초
// 총 3초 소요
return { user, posts, comments };
}
// 병렬 처리 (빠름)
async function parallel() {
const [user, posts, comments] = await Promise.all([
fetchUser(1),
fetchPosts(1),
fetchComments(1)
]);
// 총 1초 소요 (가장 긴 작업 기준)
return { user, posts, comments };
}
// 의존성이 있는 경우
async function mixed() {
const user = await fetchUser(1); // 1초
// user 데이터가 필요한 작업들을 병렬로
const [posts, friends] = await Promise.all([
fetchUserPosts(user.id),
fetchUserFriends(user.id)
]);
// 총 2초 소요
return { user, posts, friends };
}
캐싱 전략
반복적인 요청을 줄이는 캐싱 패턴입니다.
// 간단한 메모리 캐시
class AsyncCache {
constructor(ttl = 60000) { // 기본 1분 TTL
this.cache = new Map();
this.ttl = ttl;
}
async get(key, fetchFn) {
const cached = this.cache.get(key);
if (cached && Date.now() - cached.timestamp < this.ttl) {
console.log('Cache hit:', key);
return cached.value;
}
console.log('Cache miss:', key);
const value = await fetchFn();
this.cache.set(key, {
value,
timestamp: Date.now()
});
return value;
}
clear() {
this.cache.clear();
}
}
// 사용 예제
const cache = new AsyncCache(60000);
async function getUserData(userId) {
return cache.get(`user:${userId}`, () =>
fetch(`/api/users/${userId}`).then(r => r.json())
);
}
데이터 스트리밍
대용량 데이터를 효율적으로 처리하는 스트림 패턴입니다.
// Node.js 스트림 예제
const fs = require('fs');
const readline = require('readline');
async function processLargeFile(filePath) {
const fileStream = fs.createReadStream(filePath);
const rl = readline.createInterface({
input: fileStream,
crlfDelay: Infinity
});
let lineCount = 0;
for await (const line of rl) {
// 한 줄씩 처리 (메모리 효율적)
await processLine(line);
lineCount++;
if (lineCount % 1000 === 0) {
console.log(`Processed ${lineCount} lines`);
}
}
return lineCount;
}
async function processLine(line) {
// 각 라인 처리 로직
return line.toUpperCase();
}
디버깅과 테스트
비동기 코드의 디버깅과 테스트 방법입니다.
비동기 코드 디버깅
// 디버깅 헬퍼 함수
function debugAsync(name) {
return function(target, propertyKey, descriptor) {
const original = descriptor.value;
descriptor.value = async function(...args) {
console.log(`[${name}] Started at ${new Date().toISOString()}`);
console.log(`[${name}] Args:`, args);
try {
const result = await original.apply(this, args);
console.log(`[${name}] Success:`, result);
return result;
} catch (error) {
console.error(`[${name}] Error:`, error);
throw error;
} finally {
console.log(`[${name}] Ended at ${new Date().toISOString()}`);
}
};
return descriptor;
};
}
// 사용 예제
class UserService {
@debugAsync('fetchUser')
async fetchUser(userId) {
const response = await fetch(`/api/users/${userId}`);
return response.json();
}
}
비동기 함수 테스트
// Jest를 사용한 비동기 테스트
describe('User API', () => {
test('should fetch user data', async () => {
const user = await fetchUser(1);
expect(user).toBeDefined();
expect(user.id).toBe(1);
expect(user.name).toBeTruthy();
});
test('should handle errors', async () => {
await expect(fetchUser(-1)).rejects.toThrow('Invalid user ID');
});
test('should timeout after 5 seconds', async () => {
jest.setTimeout(10000);
await expect(
withTimeout(fetchSlowAPI(), 5000)
).rejects.toThrow('Timeout');
});
// Mock을 사용한 테스트
test('should use cached data', async () => {
const mockFetch = jest.fn().mockResolvedValue({ id: 1, name: 'User' });
const result1 = await cache.get('user:1', mockFetch);
const result2 = await cache.get('user:1', mockFetch);
expect(mockFetch).toHaveBeenCalledTimes(1); // 한 번만 호출
expect(result1).toEqual(result2);
});
});
Jest 비동기 테스트 가이드에서 더 많은 패턴을 확인하세요.
실무 예제: 대시보드 데이터 로딩
실제 프로젝트에서 자주 사용하는 패턴을 종합한 예제입니다.
// 대시보드 데이터 로딩 시스템
interface DashboardData {
user: UserData;
stats: Statistics;
recentActivity: Activity[];
notifications: Notification[];
}
class DashboardService {
private cache: AsyncCache;
constructor() {
this.cache = new AsyncCache(300000); // 5분 캐시
}
async loadDashboard(userId: string): Promise<DashboardData> {
try {
// 필수 데이터 먼저 로드 (병렬)
const [user, stats] = await Promise.all([
this.fetchUser(userId),
this.fetchStats(userId)
]);
// 선택적 데이터 로드 (병렬, 실패해도 계속)
const [recentActivity, notifications] = await Promise.allSettled([
this.fetchRecentActivity(userId),
this.fetchNotifications(userId)
]);
return {
user,
stats,
recentActivity: recentActivity.status === 'fulfilled'
? recentActivity.value : [],
notifications: notifications.status === 'fulfilled'
? notifications.value : []
};
} catch (error) {
console.error('Dashboard load failed:', error);
throw new Error('Failed to load dashboard data');
}
}
private async fetchUser(userId: string): Promise<UserData> {
return this.cache.get(`user:${userId}`, async () => {
const response = await retryWithBackoff(
() => fetch(`/api/users/${userId}`),
3,
1000
);
if (!response.ok) {
throw new Error(`User fetch failed: ${response.status}`);
}
return response.json();
});
}
private async fetchStats(userId: string): Promise<Statistics> {
return withTimeout(
fetch(`/api/stats/${userId}`).then(r => r.json()),
5000
);
}
private async fetchRecentActivity(userId: string): Promise<Activity[]> {
const response = await fetch(`/api/activity/${userId}?limit=10`);
return response.json();
}
private async fetchNotifications(userId: string): Promise<Notification[]> {
const response = await fetch(`/api/notifications/${userId}?unread=true`);
return response.json();
}
}
// 사용 예제
async function main() {
const dashboardService = new DashboardService();
try {
console.log('Loading dashboard...');
const startTime = Date.now();
const data = await dashboardService.loadDashboard('user123');
console.log(`Dashboard loaded in ${Date.now() - startTime}ms`);
console.log('User:', data.user.name);
console.log('Stats:', data.stats);
console.log('Activities:', data.recentActivity.length);
console.log('Notifications:', data.notifications.length);
} catch (error) {
console.error('Failed to load dashboard:', error);
}
}
마무리 및 학습 리소스
동기 비동기 프로그래밍은 현대 소프트웨어 개발의 핵심 개념입니다.
이 글에서 다룬 내용을 요약하면 다음과 같습니다.
핵심 요약
- 동기 vs 비동기: 작업 실행 방식의 차이
- 블로킹 vs 논블로킹: 제어권 반환 시점의 차이
- 비동기 패턴: 콜백 → Promise → async/await로 진화
- 이벤트 루프: 단일 스레드에서 동시성을 구현하는 핵심 메커니즘
- 선택 기준: I/O 바운드는 비동기, 계산 바운드는 병렬 처리
추가 학습 자료
공식 문서
- MDN async function 가이드
- Python asyncio 공식 문서
- Node.js 이벤트 루프 가이드
심화 학습
- JavaScript 실행 모델
- Python Concurrency 가이드
실전 적용 팁
비동기 프로그래밍을 마스터하기 위한 단계적 접근법입니다.
- 기본 패턴 숙지: Promise와 async/await 문법을 완벽히 이해
- 에러 처리 습관화: 모든 비동기 코드에 에러 처리 로직 포함
- 성능 모니터링: 비동기 작업의 실행 시간과 병목 구간 파악
- 점진적 적용: 기존 동기 코드를 단계적으로 비동기로 전환
- 테스트 자동화: 비동기 코드의 다양한 시나리오 테스트
동기 비동기 차이를 정확히 이해하고, 상황에 맞는 비동기 코드 패턴을 선택하면 고성능 애플리케이션을 개발할 수 있습니다.
이벤트 루프의 동작 원리를 파악하고, I/O 바운드와 계산 바운드를 구분하여 최적의 동기 vs 비동기 선택을 하세요.
API, 라이브러리, 프레임워크 | 개념부터 예시까지 한눈에 이해하기
API 라이브러리 프레임워크 차이를 명확히 이해하면 개발 효율이 2배 향상됩니다. Inversion of Control 개념부터 Java, Python, JavaScript 실전 예시까지 완벽 가이드
애자일(Agile) | 변화에 빠르게 대응하는 개발 철학 완전 정복
애자일 소프트웨어 개발 방법론의 핵심 개념부터 스크럼, 칸반 실무 적용까지. 변화에 빠르게 대응하는 현대적 개발 철학과 팀 운영 전략을 완전 정복하세요.
클라우드 배포 생존 가이드 | 12팩터앱 원칙, Heroku 적용 팁
12팩터앱 방법론으로 Heroku 클라우드 배포를 마스터하세요. 단일 코드베이스부터 stateless 프로세스까지 실전 체크리스트와 코드 예제를 제공합니다. 환경 변수 설정, 마이그레이션 자동화 팁 포함
LMArena vs 다른 AI 평가 지표 비교 | 신뢰 가능성은?
LMArena는 사용자 투표로 AI를 평가하는 혁신적 플랫폼입니다. MMLU, HumanEval 등 전통 벤치마크와 비교하여 장단점을 분석하고, 신뢰할 수 있는 AI 모델 선택 방법을 제시합니다.
Gemini CLI 확장 마켓플레이스 공개 | 명령줄이 똑똑해진다
구글 Gemini CLI 확장 마켓플레이스 출시. Figma, Postman, Stripe 등 주요 파트너 확장으로 명령줄에서 AI 에이전트를 커스터마이징하고 MCP 서버로 확장 개발하는 방법 완벽 가이드
댓글
댓글 쓰기