우아한테크코스 레벨 2에서 학습한 내용을 정리한 글입니다.
💭 들어가며
이번 미션을 진행하면서 처음으로 AtomicLong을 접했다. 왜 AtomicLong을 사용하는지 찾아보니 논블로킹 방식으로 동시성 제어가 가능하기 때문이라고 한다.
"논블로킹이 뭐지..?" 찾아보니 블로킹(Blocking)과 논블로킹(Non-Blocking)이라는 개념이 있었고, 또 그와 자주 함께 언급되는 개념으로 동기(Synchronous)와 비동기(Asynchronous)도 있었다. 전에도 종종 들어본 키워드들이지만, 제대로 정리해 본 적은 없었던 것 같다. 이번 기회에 헷갈리기 쉬운 이 네 가지 개념들을 정리하고, 각 조합이 실제로 어떻게 동작하는지 이해해보고자 한다.
사실 각각의 개념은 서로 직접적인 비교 대상은 아니다. 그럼에도 불구하고 여러 곳에서 혼용되어 사용되는 경우가 많다. 아마도 두 개념 모두 "기다림이 있는가 없는가"라는 공통된 특징을 가지고 있기 때문에 헷갈리는 것이 아닐까 싶다. 나 역시 처음에는 정말 이해하기 어려웠는데, 비유를 통해 개념을 시각적으로 정리하면서 훨씬 쉽게 이해할 수 있어 소개하고자 한다.
✅ 블로킹/논블로킹
▶ 블로킹(Blocking)
"기다려야 해"
🔽 특징
- 작업이 끝날 때까지 스레드가 멈춰 있다.
- 다른 작업은 하지 못하고, 계속 기다려야 한다.
- 구현은 단순하지만, 요청이 많아지면 병목이 생길 수 있다.
🔽 예시 코드
public static void main(String[] args) throws Exception {
System.out.println("입력 기다리는 중...");
BufferedReader reader = new BufferedReader(new InputStreamReader(System.in));
String input = reader.readLine(); // 입력 전까지 멈춤
System.out.println("입력한 값: " + input);
}
nextLine()을 호출하면 콘솔에 입력을 끝낼 때까지 스레드가 멈춘다.
▶ 논블로킹(Non-Blocking)
"기다리지 않아도 돼"
🔽 특징
- 요청을 보내고 곧바로 다음 작업을 할 수 있다.
- 상태를 polling 방식으로 직접 확인하거나, 이벤트 기반으로 처리한다.
🔽 예시 코드
public static void main(String[] args) throws Exception {
SocketChannel channel = SocketChannel.open();
channel.configureBlocking(false); // 논블로킹 설정
channel.connect(new InetSocketAddress("example.com", 80));
ByteBuffer buffer = ByteBuffer.allocate(1024);
int bytesRead = channel.read(buffer); // 바로 반환됨
System.out.println("읽은 바이트 수: " + bytesRead);
}
데이터가 없거나 연결이 완료되지 않은 경우 read()는 즉시 0 또는 -1을 반환하고 멈추지 않는다.
✅ 동기/비동기
▶ 동기(Synchronous)
"내가 끝까지 다 해"
🔽 특징
- 앞 작업이 끝나야 다음 코드를 실행할 수 있다.
- 흐름이 명확해서 이해는 쉽고 디버깅도 편하다.
- 오래 걸리는 작업일수록 전체 처리 시간도 길어진다.
🔽 예시 코드
public static void main(String[] args) {
System.out.println("1. 계산 시작");
int result = heavyWork(); // 내가 직접 처리
System.out.println("2. 결과: " + result);
System.out.println("3. 다음 작업 진행");
}
private static int heavyWork() {
try {
Thread.sleep(2000); // 2초 걸리는 작업
} catch (InterruptedException e) {
e.printStackTrace();
}
return 42;
}
main() 함수는 heavyWork()를 직접 호출하고, 작업이 끝날 때까지 기다린다.
▶ 비동기(Asynchronous)
"남한테 시키고 나는 다른 작업해"
🔽 특징
- 요청을 맡기고, 결과는 나중에 콜백/이벤트로 처리한다.
- 동시에 여러 작업을 처리할 수 있어 효율이 좋다.
- 흐름이 분리돼 있어 코드가 복잡해질 수 있다.
🔽 예시 코드
public static void main(String[] args) {
System.out.println("1. 계산 시작");
// 남에게 시킴
CompletableFuture<Integer> future = CompletableFuture.supplyAsync(() -> heavyWork());
System.out.println("2. 다른 작업 진행"); // 먼저 실행됨
future.thenAccept(result -> {
System.out.println("3. 결과: " + result);
}).join(); // 결과 기다려줌
}
private static int heavyWork() {
try {
Thread.sleep(2000); // 2초 걸리는 작업
} catch (InterruptedException e) {
e.printStackTrace();
}
return 42;
}
CompletableFuture를 사용하면 작업을 다른 스레드에 맡기고, 그동안 다른 코드를 실행할 수 있다. 결과가 나중에 도착하면 .thenAccept()로 콜백 처리할 수 있다.
✅ 동기/비동기 + 블로킹/논블로킹 조합
사실 이 두 개념은 조합되어 자주 등장한다. 예시가 난해한 경우가 많았는데, 프린트를 하는 상황을 예로 들어 시각적으로 정리해보았다.
▶ 동기 + 블로킹 (Sync Blocking)
🔽 특징
- 간단하고 익숙하다.
- 요청이 많아지면 줄 서서 기다려야 하므로 처리 속도가 느려진다.
- 동시성이 낮고, 스레드가 많이 필요하다.
🔽 적용 예시
- Spring MVC + JDBC
- Python requests.get()
- Java HttpURLConnection.openStream()
🔽 시나리오
- 내가 직접 프린트를 하면서
- 프린트가 끝날 때까지 아무것도 못 하고 기다린다.
▶ 동기 + 논블로킹 (Sync Non-Blocking)
🔽 특징
- 결과는 필요하지만, 지금 당장은 넘어갈 수 있다.
- 상태를 직접 확인하거나 반족적으로 체크해야 한다.
- 그래서 직접 관리할 것이 많아지고, 구현이 까다로울 수 있다.
🔽 적용 예시
- Java NIO + select/poll
- C의 epoll, select 서버
- Redis의 tryLock() 같은 즉시 실패 방식
🔽 시나리오
- 내가 프린팅을 하면서
- 프린트가 끝났는지 확인하며, 다른 작업도 할 수 있다.
▶ 비동기 + 블로킹 (Async Blocking)
🔽 특징
- 외형은 비동기지만, 내부 작업은 여전히 블로킹이라 효율이 떨어진다.
- 시스템 자원을 계속 점유하며, 병목의 원인이 되기도 한다.
- 예전 방식이 많고, 개선 대상으로 자주 언급된다.
🔽 적용 예시
- Node.js + MySQL(mysql 드라이버)
- Spring @Async + JDBC
- CompletableFuture.runAsync(() -> JDBC)
🔽 시나리오
- 친구가 프린팅을 하면서
- 친구가 프린트를 해올 때까지 아무 것도 못 하고 기다린다.
실무에서 거의 사용되지 않으며, 안티패턴으로 여겨지는 경우가 많은 방식이다.
▶ 비동기 + 논블로킹 (Async Non-Blocking)
🔽 특징
- 리소스를 거의 낭비하지 않고 매우 효율적이다.
- 요청 처리량(동시성)이 높아서 고성능 웹서비스에 적합하다.
- 구현은 복잡하다.
🔽 적용 예시
- Spring WebFlux + R2DBC
- Node.js + MongoDB (Mongoose)
- Netty, Vert.x
- JavaScript fetch + async/await
🔽 시나리오
- 친구가 프린팅을 하면서
- 나는 다른 작업을 할 수 있다.
📍 참고 자료
'Programming > CS' 카테고리의 다른 글
[CS] 프로그래밍 에러 종류 (2) | 2024.11.28 |
---|