Post

동기(Synchronous)와 비동기(Asynchronous)

동기(Synchronous)와 비동기(Asynchronous)

동기(Synchronous)와 비동기(Asynchronous)

동기와 비동기를 이해하면 작업 처리 방식을 효율적으로 설계할 수 있습니다. 이 두 가지는 프로그래밍에서 작업 흐름을 결정짓는 핵심 개념으로, 특히 I/O 작업(입출력), 네트워크 통신, 사용자 인터페이스(UI) 같은 영역에서 중요한 역할을 합니다. 단순히 “작업을 기다리느냐, 안 기다리느냐”의 문제가 아니라 시스템 자원 활용, 응답성, 확장성까지 영향을 미칩니다.


동기(Synchronous) 처리의 특징

동기 처리에서는 작업이 순서대로 진행됩니다. 즉, 한 작업이 끝날 때까지 다음 작업은 시작되지 않습니다. 동기(Synchronous) 라는 단어는 “시간적으로 맞춰진다”는 뜻에서 유래했으며, 코드 흐름과 실제 실행이 일치한다는 의미입니다. 예를 들어, 파일을 읽고 그 결과를 바로 처리해야 한다면 동기 방식이 자연스럽습니다.

동기 처리의 주요 특징

  • 순차 실행: 작업 A가 끝나야 작업 B가 시작됩니다. 이로 인해 흐름이 명확해집니다.
  • 예측 가능성: 코드 작성 순서대로 결과가 나타납니다. 디버깅 시 흐름을 쉽게 파악할 수 있습니다.
  • 블로킹 방식: 작업이 끝날 때까지 스레드가 대기 상태에 있게 됩니다. 이는 자원 낭비로 이어질 수 있습니다.
  • 단순함: 설계와 유지보수가 상대적으로 쉽습니다. 복잡한 로직 없이 직관적으로 코드를 작성할 수 있습니다.

동기 처리 예제

1
2
3
4
5
6
7
8
9
10
11
12
public void syncExample() {
    System.out.println("작업 1 시작");
    taskOne(); // 이 작업이 완료될 때까지 다음 줄로 넘어가지 않습니다
    System.out.println("작업 2 시작");
    taskTwo();
    System.out.println("모든 작업 완료");
}

private void taskOne() {
    try { Thread.sleep(1000); } catch (Exception e) {} // 1초 대기 시뮬레이션
    System.out.println("작업 1 완료");
}

위 코드를 실행하면 작업 1이 끝날 때까지 작업 2는 대기합니다. 출력은 “작업 1 시작 → 작업 1 완료 → 작업 2 시작 → 모든 작업 완료” 순서로 나타납니다.


비동기(Asynchronous) 처리의 특징

비동기 처리에서는 작업을 시작하고 완료를 기다리지 않고 다음 작업으로 넘어갑니다. 작업이 끝나면 콜백, 프로미스, 이벤트 등을 통해 결과를 받습니다. 비동기(Asynchronous)는 “시간적으로 맞춰지지 않는다”는 의미로, 작업이 독립적으로 실행되면서 흐름이 코드 순서와 다를 수 있음을 나타냅니다. 네트워크 요청처럼 오래 걸리는 작업에서 특히 유용합니다.


비동기 처리의 주요 특징

  • 논블로킹 방식: 작업 A가 실행되는 동안 작업 B를 시작할 수 있습니다. 스레드가 멈추지 않고 계속 작업을 처리합니다.
  • 완료 순서의 불확실성: 코드 순서와 결과 순서가 달라질 수 있습니다. 이 점을 고려하여 설계해야 합니다.
  • 효율성: 대기 시간에 다른 작업을 처리할 수 있어 자원을 효율적으로 활용할 수 있습니다.
  • 복잡성: 콜백 중첩이나 디버깅이 어려워질 수 있어 신중한 관리가 필요합니다.

비동기 처리 예제

1
2
3
4
5
console.log("작업 1 시작");
setTimeout(() => {
    console.log("작업 1 완료"); // 2초 후에 완료됩니다
}, 2000);
console.log("작업 2 시작"); // 기다리지 않고 바로 실행됩니다

출력은 “작업 1 시작 → 작업 2 시작 → 작업 1 완료” 순서로 나타납니다. 작업 1의 완료를 기다리지 않고 바로 다음 줄로 넘어가는 것을 확인할 수 있습니다.


Image

사진출처

동기와 비동기의 차이점

항목동기비동기
실행 순서순차적이고 예측 가능합니다시작은 순서대로지만 완료는 불확실합니다
블로킹 여부블로킹 방식으로 스레드가 대기합니다논블로킹 방식으로 스레드가 계속 작업합니다
자원 활용대기 시간에 자원이 낭비될 수 있습니다대기 없이 효율적으로 자원을 활용합니다
코드 복잡성단순하고 이해하기 쉽습니다복잡하고 관리하기 까다로울 수 있습니다

동시성(Concurrency)과 병렬성(Parallelism)의 구분

  • 동시성: 한 스레드에서 작업을 시간 분할로 처리합니다. JavaScript의 이벤트 루프가 대표적인 예입니다.
  • 병렬성: 여러 코어나 스레드로 작업을 동시에 실행합니다. Java의 멀티스레딩이 이에 해당합니다. 비동기는 주로 동시성과 관련이 있으며, 병렬성은 하드웨어 성능에 더 의존적입니다.

Image

사진출처

동기 방식의 장단점

장점

  • 흐름을 예측하기 쉬워 디버깅이 용이합니다.
  • 순서가 중요한 작업에 적합합니다.
  • 에러 처리가 상대적으로 간단하여 코드가 깔끔해집니다.

단점

  • 자원 활용이 비효율적일 수 있습니다.
  • UI 작업 시 화면이 멈추는 등 사용자 경험이 저하될 수 있습니다.
  • 확장성이 떨어져 대규모 시스템에서는 한계가 있습니다.

비동기 방식의 장단점

장점

  • 자원을 효율적으로 활용하여 성능이 향상됩니다.
  • 응답성이 좋아져 사용자 경험이 개선됩니다.
  • 대량의 요청을 처리할 수 있어 서버의 확장성이 높아집니다.

단점

  • 흐름 예측이 어려워 혼란을 초래할 수 있습니다.
  • 코드가 복잡해져 유지보수가 어려울 수 있습니다.
  • 디버깅이 까다로워 개발 시간이 늘어날 수 있습니다.

비동기 처리 패턴

1. 콜백(Callback) 패턴

작업이 완료되면 호출할 함수를 매개변수로 전달하여 결과를 받는 방식입니다.

1
2
3
4
function fetchData(callback) {
    setTimeout(() => callback("데이터"), 1000);
}
fetchData(data => console.log(data));

콜백이 중첩되면 “콜백 지옥”이 발생할 수 있어 가독성이 떨어지고 관리가 어려워집니다.

Image

사진출처

2. 프로미스(Promise) 패턴

작업의 성공/실패를 객체로 다루어 흐름을 더 깔끔하게 관리할 수 있습니다.

1
2
3
4
5
6
function fetchData() {
    return new Promise((resolve, reject) => {
        setTimeout(() => resolve("데이터"), 1000);
    });
}
fetchData().then(data => console.log(data)).catch(err => console.error(err));

체이닝을 통해 콜백 지옥을 피하고, 오류 처리도 더 쉽게 할 수 있습니다.

3. Async/Await 패턴

프로미스를 동기 코드처럼 작성할 수 있어 코드의 가독성이 크게 향상되고 유지보수가 용이해집니다.

1
2
3
4
5
6
7
8
9
async function processData() {
    try {
        const data = await fetchData();
        console.log(data);
    } catch (err) {
        console.error(err);
    }
}
processData();

Image

사진출처

비동기 오류 처리

비동기 환경에서 오류를 적절히 처리하면 시스템의 안정성을 높일 수 있습니다.

JavaScript에서의 오류 처리 예제

1
2
3
4
5
6
7
8
9
10
11
async function fetchWithRetry(url, retries = 3) {
    for (let i = 0; i < retries; i++) {
        try {
            const response = await fetch(url);
            return await response.json();
        } catch (err) {
            if (i === retries - 1) throw new Error(`최대 재시도 실패: ${err.message}`);
            await new Promise(resolve => setTimeout(resolve, 1000));
        }
    }
}

실패 시 재시도하고, 여러 번 실패하면 오류를 발생시키는 패턴입니다.

Java에서의 오류 처리 예제

1
2
3
4
5
6
7
8
9
CompletableFuture<String> fetchData() {
    return CompletableFuture.supplyAsync(() -> {
        if (Math.random() > 0.5) throw new RuntimeException("데이터 가져오기 실패");
        return "데이터";
    }).exceptionally(throwable -> {
        System.err.println("오류: " + throwable.getMessage());
        return "기본값";
    });
}

오류 발생 시 기본값으로 대체하는 방식입니다.


JavaScript에서의 동기와 비동기

JavaScript는 싱글 스레드 언어이지만, 이벤트 루프를 통해 비동기 처리를 구현합니다.

이벤트 루프의 동작 원리

  • 콜 스택: 실행 중인 함수를 관리합니다.
  • 태스크 큐: 비동기 작업이 대기합니다.
  • 이벤트 루프: 스택이 비면 큐에서 작업을 가져와 실행합니다.
    1
    2
    3
    4
    
    console.log("1");
    setTimeout(() => console.log("2"), 0);
    console.log("3");
    // 출력: 1, 3, 2
    

    Image 사진출처


Java/Spring에서의 동기와 비동기

Java의 비동기 처리 방식

  • Thread: 기본적인 멀티스레딩을 제공합니다.
  • CompletableFuture: 작업을 연결하여 처리할 수 있습니다.
    1
    2
    
    CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> "결과")
      .thenApply(result -> result + " 처리됨");
    
  • Project Loom: 가상 스레드를 통해 대량의 작업을 효율적으로 처리합니다 (JDK 21+).
    1
    2
    3
    
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
      executor.submit(() -> System.out.println("가상 스레드 작업"));
    }
    

Spring의 비동기 처리 방식

  • @Async: 메서드를 비동기적으로 실행합니다.
    1
    2
    3
    4
    5
    6
    7
    8
    
    @Service
    public class AsyncService {
      @Async
      public CompletableFuture<String> asyncMethod() {
          Thread.sleep(1000);
          return CompletableFuture.completedFuture("비동기 결과");
      }
    }
    
  • WebFlux: 리액티브 프로그래밍을 통해 논블로킹 처리를 구현합니다.
    1
    2
    3
    4
    5
    6
    
    Mono<String> fetchData() {
      return WebClient.create().get()
          .uri("https://api.example.com")
          .retrieve()
          .bodyToMono(String.class);
    }
    

활용 사례

동기 방식의 활용

  • 순서대로 처리해야 하는 파일 변환 작업에 적합합니다.
  • 단순 계산 작업은 빠르게 처리됩니다.

비동기 방식의 활용

  • API 병렬 호출을 통해 처리 속도를 향상시킬 수 있습니다.
  • 실시간 채팅 서비스에서 끊김 없는 통신을 구현할 수 있습니다.
  • 스트리밍 데이터 처리를 통해 대량의 작업을 효율적으로 처리할 수 있습니다.

성능 최적화 방안

  • 스레드 풀 튜닝: 적절한 스레드 수를 설정하여 자원 낭비를 줄일 수 있습니다.
  • 컨텍스트 스위칭 최소화: 불필요한 스위칭을 줄여 오버헤드를 감소시킬 수 있습니다.
  • 백프레셔 관리: WebFlux에서 데이터 흐름을 안정적으로 유지할 수 있습니다.
  • 모니터링: 병목 지점을 파악하여 최적화할 수 있습니다.

Project Loom

Project Loom은 Java에서 가상 스레드를 통해 대규모 동시성을 효율적으로 다룰 수 있게 해줍니다. 기존 스레드의 한계를 넘어서 2025년 기준 JDK 21+ 버전에서 점차 활용되고 있습니다.

Image 사진출처

This post is licensed under CC BY 4.0 by the author.