
이 글에서는 동기(Synchronous)와 비동기(Asynchronous) 프로그래밍이 어떤 개념인지, 그리고 두 방식이 코드 실행 흐름을 어떻게 달리 만드는지 자바 예제를 통해 설명합니다.
이 글을 읽고 나면 다음 내용을 자연스럽게 이해할 수 있어요.
- 동기와 비동기의 핵심 차이
- 두 방식이 실행 시간과 흐름에 어떤 영향을 주는지
- 함수 관점에서 동기·비동기 모델이 어떻게 적용되는지
- 콜백 구조가 왜 비동기 패턴으로 불리는지
동기와 비동기의 개념 비교
동기(Synchronous)
동기 방식은 작업이 순서대로 차례차례 실행되는 구조입니다.
앞선 작업이 끝나야 다음 작업을 시작할 수 있기 때문에, 실행 흐름을 예측하기는 쉽지만 느린 작업 하나가 전체 속도를 끌어내릴 수 있습니다.
public class SynchronousExample {
public static void main(String[] args) {
task1();
task2();
}
private static void task1() {
System.out.println("Task 1 시작");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task1 완료");
}
private static void task2() {
System.out.println("Task 2 시작");
System.out.println("Task 2 완료");
}
}
// 실행 결과
// Task 1 시작
// Task 1 완료
// Task 2 시작
// Task 2 완료
위 예제는 task1()이 2초 동안 멈춰 있는 동안 task2()도 시작하지 못하고 기다리는 전형적인 동기 실행 흐름을 보여줍니다
핵심 요약: 중간에 느린 작업이 있으면 전체 프로그램 흐름이 그대로 지연된다.
비동기 (Asynchronous)
비동기 방식은 여러 작업을 동시에 실행할 수 있는 구조입니다.
특정 작업이 끝날 때까지 기다릴 필요가 없기 때문에, 느린 작업이 있어도 다른 작업이 먼저 처리될 수 있습니다. 시스템 자원을 더 효율적으로 활용할 수 있지만, 상황에 따라 동기화 작업이 필요할 수도 있습니다.
public class AsynchronousExample {
public static void main(String[] args) {
Thread thread1 = new Thread(() -> {
System.out.println("Task 1 시작");
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("Task 1 완료");
});
Thread thread2 = new Thread(() -> {
System.out.println("Task 2 시작");
System.out.println("Task 2 완료");
});
thread1.start();
thread2.start();
}
}
// 실행 결과
// Task 1 시작
// Task 2 시작
// Task 2 완료
// Task 1 완료
위 예제는 두 개의 스레드가 각각 독립적으로 작업을 수행하는 전형적인 비동기 흐름을 보여 줍니다.
핵심 요약: 각 작업이 독립적으로 실행되므로 느린 작업이 있어도 전체 흐름이 지연되지 않는다
함수 관점에서 보는 동기 vs 비동기
이제 조금 더 깊이 들어가, 동기 방식에서는 함수 호출 흐름이 어떻게 자연스럽게 이어지는지, 그리고 비동기 방식에서는 호출 흐름이 어떻게 ‘역전(reversal)’되는지 살펴보겠습니다.
동기 함수 실행 흐름
동기 방식에서는 caller(호출하는 함수)가 callee(호출된 함수)의 실행이 끝날 때까지 그대로 기다립니다.
getResult()가 값을 반환해야만 main()의 다음 로직을 진행할 수 있으므로, 실행 흐름이 위에서 아래로 차례대로 이어지는 구조를 가집니다.
아래 예제(Class A)는 동기 호출 흐름을 그대로 보여 줍니다.main() → getResult() → 결과 반환 → 다시 main() 흐름으로 복귀하는 전형적인 패턴입니다.
@Slf4j
public class A {
public static void main(String[] args) {
log.info("main start");
var result = getResult();
var nextValue = result + 1;
assert nextValue == 1;
log.info("main finish");
}
public static int getResult() {
log.info("getResult start");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
var result = 0;
try {
return result;
} finally {
log.info("getResult finish");
}
}
}

- caller는 callee의 결과를 직접 기다린다.
- 함수 흐름이 caller → callee → 결과 반환 → caller 순서로 직관적으로 이어진다.
- 실행 순서가 예측 가능하며 호출 구조가 단순하다.
비동기 함수 실행 흐름
비동기 방식에서는 caller가 callee의 반환값을 직접 기다리지 않습니다.
대신 callee가 작업을 마친 뒤, 준비된 callback을 호출해 결과를 전달합니다.
이 과정에서 실행의 주도권이 caller가 아닌 callee 쪽으로 넘어가기 때문에 이를 제어 흐름의 역전(Inversion of Control)이라고 부릅니다.
실행 흐름은 다음과 같이 전개됩니다.
- caller는 callee를 호출한 직후 바로 다음 로직으로 넘어간다.
- callee는 내부 작업을 끝낸 뒤 callback을 실행한다.
- 결과 처리는 callback 안에서 이루어진다.
아래 Class B 예제는 이러한 비동기 호출 흐름을 잘 보여 줍니다.
@Slf4j
public class B {
public static void main(String[] args) {
log.info("Start main");
getResult(new Consumer<Integer>() {
@Override
public void accept(Integer integer) {
var nextValue = integer + 1;
assert nextValue == 1;
}
});
log.info("Finish main");
}
public static void getResult(Consumer<Integer> cb) {
log.info("Start getResult");
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
throw new RuntimeException(e);
}
var result = 0;
cb.accept(result);
log.info("Finish getResult");
}
}

- caller는 callee의 결과를 기다리지 않는다.
- callee가 callback을 통해 결과를 직접 전달한다.
- 실행의 주도권이 caller에서 callee로 넘어가며 흐름이 역전된다.
- caller → callee 호출 → callee 내부에서 callback 실행 → caller는 즉시 다음 작업 수행.
두 흐름의 차이 요약
동기(A)와 비동기(B)의 차이를 한눈에 비교하면 다음과 같습니다.
| 구분 | 동기(A) | 비동기(B) |
| 결과 관심 | caller가 직접 결과를 기다림 | caller는 결과를 기다리지 않음 (관심 없음) |
| 실행 흐름 | caller 중심으로 직선형 진행 | callee 중심으로 흐름이 이동 (callback) |
| 흐름 제어 | caller가 전체 제어권을 가짐 | callee가 제어권을 가져 흐름이 역전됨 |
| 블로킹 여부 | 블로킹 - callee가 끝날 때까지 멈춤 | 논블로킹 - 즉시 다음 작업으로 이동 |
동기 방식은 기다리고 이어지는 흐름이고,
비동기 방식은 넘기고 맡기는 흐름입니다.
예제에서 확인했듯이, 비동기 방식에서는 제어 흐름이 caller에서 callee로 넘어가며(제어 역전), 프로그램이 더 유연하고 독립적으로 동작할 수 있습니다.
다음 글에서는 이러한 동기·비동기 실행 모델을 더 분명하게 구분해 주는 개념인 Blocking vs Non-blocking을 다루며, 실행 흐름을 한 단계 더 깊게 이해할 수 있도록 설명해볼게요!
'Computer Science > 프로그래밍 이론 💬' 카테고리의 다른 글
| 일급 컬렉션이란? (0) | 2024.02.07 |
|---|---|
| Dependency Injection이란? (0) | 2024.02.06 |
| 제어의 역전(IOC) 톺아 보기 (0) | 2024.02.06 |
| 객체지향 프로그래밍이란? (0) | 2024.02.02 |
안녕하세요, 저는 주니어 개발자 박석희 입니다. 언제든 하단 연락처로 연락주세요 😆