개요
이 글은 bytebytego의 동기 vs 비동기를 참고하여 작성했습니다.
https://blog.bytebytego.com/p/synchronous-vs-asynchronous-communication
Synchronous vs Asynchronous Communication: When to Use What?
At some point, every system has to make a call: Should this interaction happen synchronously or asynchronously?
blog.bytebytego.com
프로그래밍을 하다 보면 마주치게 되는 개념인 동기와 비동기, 둘의 개념은 무엇이고 어떠한 차이가 있을까요?
또한 어떠한 상황에서 동기와 비동기 방식을 적용하면 좋을까요?
동기 (Synchronous)
동기의 뜻은 시간의 흐름에 맞춰 작업들을 처리한다는 뜻을 가지고 있습니다.
또한 프로그래밍에서 동기의 뜻은 작업이 끝났을 때의 결과를 즉시 가져와서 사용한다는 의미를 가지고 있습니다.
작업 A, B 그리고 C를 동기식으로 처리한다면 아래와 같은 흐름으로 처리됩니다.

우리에게 익숙한 통신 방식은 동기 통신입니다.
어떠한 함수를 실행하면 이 함수가 종료될 때까지 다른 작업을 할 수 없고 기다려야 합니다.
결과값 예측이 쉽고 디버깅이 쉽다는 장점을 가지고 있지만 작업 시간이 길어진다면 그만큼 대기 시간 또한 발생하고 작업 중간에 작업이 발생하면 모든 진행 상황을 잃어버릴 수 있습니다.
또한 동기식 통신은 서비스들 간 의존성이 발생할 수 있습니다.
만약 서비스 A가 서비스 B를 호출한다면 서비스 A는 서비스 B의 응답이 올 때까지 기다려야하며 자연스럽게 의존성 또한 발생합니다.

장점
- 결과 값 예측이 쉽습니다. (테스트가 쉽습니다.)
- 디버깅이 쉽습니다.
- 프로그램 흐름 파악이 쉽습니다.
단점
- 특정 작업의 작업 시간이 많아지면 호출자의 대기 시간 또한 길어집니다.
- 대기 시간이 길어지면 그만큼 사용하는 리소스도 많아집니다.
- 작업에서 오류가 발생하면 호출자에게도 영향이 갑니다.
- 규모가 커지면 병목 현상이 발생할 수 있습니다.
비동기 (Asynchronous)
비동기는 작업들을 독립적으로 수행한다는 뜻을 가지고 있습니다.
또한 프로그래밍에서 비동기의 뜻은 작업이 끝났을 때의 결과를 바로 가져와서 사용하지 않는다는 의미도 가지고 있습니다.
작업 A, B 그리고 C를 동기식으로 처리한다면 아래와 같은 흐름으로 처리됩니다.

어떠한 함수를 실행하면 결과 값을 기다리지 않고 바로 다음 작업을 수행할 수 있습니다.
또한 서비스 간 의존 관계를 해결할 수 있습니다.
예를 들어 서비스 A가 서비스 B를 호출하는 과정 중간에 메시지 큐를 삽입했다고 합시다.
서비스 A는 메시지 큐에 요청을 보낸 다음 그 다음 과정을 신경 쓸 필요가 없습니다.
서비스 B가 알아서 메시지 큐에 있는 요청을 가져와서 처리하기 때문입니다.

동기적 통신이 시간의 흐름에 맞추어 진행된다면 비동기 통신은 시간적인 제약에서 벗어난다고 할 수 있습니다.
여기서 서비스 A가 서비스 B의 작업 결과를 받아야 할 때도 있습니다.
이 경우에도 비슷한 방식으로 콜백 혹은 이벤트 핸들러 처럼 결과를 A에게 전달해 줄 방법이 필요합니다.
장점
- 같은 시간 동안 처리하는 작업의 양이 많습니다.
- 유연성이 높습니다.
- 호출자의 대기 시간을 줄여 리소스를 효율적으로 사용할 수 있습니다.
단점
- 복잡합니다.
- 모니터링이 어렵습니다.
- 프로그램 흐름을 한 눈에 파악하기 어렵습니다.
- 요청이 대기열에 계속 머무를 수 있습니다.
- 요청의 처리 속도가 일정하지 않을 수 있습니다.
이제 대표적인 프로토콜 5가지에 대해 알아보면서 동기와 비동기에 대해 자세히 알아봅시다.
HTTP

대표적인 동기식 프로토콜입니다.
서버에 HTTP 요청을 보내면 서버는 HTTP 응답을 해줍니다.
HTTP 프로토콜의 가장 큰 특징은 무상태성과 비연결성입니다.
무상태성은 서버가 클라이언트의 정보를 기억하지 않기 때문에 확장성이 좋지만 클라이언트가 추가 데이터를 전송해야 한다는 단점이 있습니다.
비연결성은 서버가 응답 값을 내려주고나면 연결을 끊기 때문에 서버의 자원을 계속 사용하지 않는다는 장점이 있지만 상태 유지가 어렵다는 단점이 있습니다.
이러한 HTTP는 굉장히 단순하기 때문에 흐름 파악과 디버깅이 쉽지만 응답을 기다리는 동안 자원을 계속 사용하기 때문에 트래픽이 많이 발생하게 되면 이러한 대기 시간이 늘어날 수 있습니다.
gRPC
gRPC는 Google에서 개발한 오픈 소스의 고성능 원격 프로시저 호출 (RPC) 프레임워크로 HTTP와 같이 API 설계에 사용하는 프로토콜입니다.
gRPC는 클라이언트가 서버의 특정 함수를 직접적으로 혹은 간접적으로 호출하는 방식입니다.
HTTP는 서버에게 데이터를 요청한다면 gRPC는 서버의 특정 함수를 호출한다고 볼 수 있습니다.
즉, gRPC는 API 요청이 마치 로컬 작업이거나 내부 코드인 것처럼 동작합니다.

언뜻 보면 HTTP와 정확한 차이점을 느끼지 못 할 수도 있습니다.
하지만 HTTP와 명확한 차이점이 존재합니다.
먼저 HTTP는 JSON을 기반으로 데이터를 요청하고 응답받습니다.
<JSON 예시>
{
"userId": 1024,
"username": "dev",
"isActive": true,
"signupDate": "2025-05-15T14:30:00Z",
"roles": ["admin", "editor"]
}
그에 비해 gRPC의 경우 클라이언트와 서버 간 통신할 때 바이너리 데이터를 사용합니다.
JSON과 같은 텍스트 기반 데이터 타입보다 바이너리 데이터의 경우 직렬화, 역직렬화에 드는 리소스가 적습니다.
또한 내부적으로 protocol buffer라는 것을 사용하여 key에 해당하는 부분의 리소스를 줄일 수 있습니다.
아래는 .proto의 예시입니다.
syntax = "proto3";
package user;
// 메시지 정의
message User {
int32 userId = 1;
string username = 2;
bool isActive = 3;
string signupDate = 4; // ISO 8601 포맷의 문자열
repeated string roles = 5; // 역할 목록
}
key 값을 정수에 매칭하는 모습을 확인할 수 있고 이를 기반으로 데이터 송수신에 필요한 리소스를 줄일 수 있습니다.
또한 gRPC는 클라이언트가 마치 로컬 함수를 호출하는 것처럼 데이터를 가져와 사용할 수 있습니다.
.proto 파일에 다음과 같이 클라이언트가 사용할 서비스와 함수를 정의합니다.
<.proto 파일>
.
.
.
message User {
int32 userId = 1;
string username = 2;
bool isActive = 3;
string signupDate = 4;
repeated string roles = 5;
}
service UserService {
rpc CreateUser(User) returns (User);
}
.
.
.
이제 클라이언트에서는 stub을 통해 위에서 정의한 함수를 호출할 수 있습니다.
<클라이언트 코드>
// gRPC 채널 생성 (localhost:50051)
ManagedChannel channel = ManagedChannelBuilder.forAddress("localhost", 50051)
.usePlaintext()
.build();
// 비동기 또는 동기 클라이언트 생성
UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc.newBlockingStub(channel);
// 요청할 User 객체 생성
User request = User.newBuilder()
.setUserId(1024)
.setUsername("sunmin_dev")
.setIsActive(true)
.setSignupDate("2025-05-15T14:30:00Z")
.addRoles("admin")
.addRoles("editor")
.build();
// 요청 보내기 및 응답 받기
User response = stub.createUser(request);
이렇게 일반적인 HTTP와 달리 마치 클라이언트가 로컬 함수를 호출하는 것처럼 데이터를 요청하고 받을 수 있습니다.
또한 gRPC는 내부적으로 HTTP/2를 사용하기 때문에 병렬 요청이 가능합니다.
장점
- 클라이언트에서 로컬 함수를 호출하는 것처럼 코딩이 가능합니다.
- JSON처럼 텍스트 기반이 아닌 바이너리 기반으로 통신하기 때문에 성능이 좋습니다.
- HTTP/2를 통해 데이터 병렬 요청이 가능합니다.
단점
- 새로운 언어를 학습해야 합니다.
- 브라우저와 서버 간 gRPC를 사용하지 못 하고 별도의 라이브러리를 함께 사용해야 합니다.
HTTP와 비슷하게 gRPC 역시 기본적으로 동기식 통신 모델이기 때문에 친숙하게 사용할 수 있습니다.
또한 gRPC는 많은 트래픽을 처리해야하는 MSA에서 서비스 간 통신에 자주 사용됩니다.
WebSocket
위에서 HTTP는 비연결성이라는 특성을 가지고 있고 요청에 대한 응답을 받게 되면 해당 연결을 끊어버립니다.
하지만 WebSocket은 연결을 맺고 그 연결을 유지한 상태로 클라이언트와 서버 사이 데이터 교환을 지속합니다.

만약 클라이언트와 서버가 주고받아야 할 데이터가 많다면 HTTP 통신보다 WebSocket을 사용하는 것이 더 좋을 수 있습니다.
예를 들어 실시간 통신을 해야 하는 온라인 게임의 경우 HTTP 통신보다 WebSocket을 사용하면 연결을 유지한 채로 계속 데이터를 교환할 수 있습니다.
장점
- 실시간으로 데이터를 주고 받을 수 있습니다.
- 클라이언트와 서버 모두 데이터를 전송하고 받을 수 있습니다.
단점
- 연결 상태를 관리해야 합니다.
- 서버를 확장할 때 웹 소켓 연결을 고려해야 합니다.
AMQP
AMQP는 message queue 기반 프로토콜로 서로 다른 시스템 간 메시지 교환을 효율적으로 할 수 있습니다.

브로커는 메시지를 보관하는 역할이나 메시지의 라우팅을 담당합니다.
A 서비스가 B 서비스를 사용한다고 할 때 AMQP 브로커에게 메시지를 전달하면 적절한 queue에 해당 메시지를 넣어줍니다.
이 메시지는 B 서비스가 자유롭게 꺼내서 해당 작업을 처리할 수 있습니다.
장점
- 서비스 간 의존성을 줄일 수 있습니다.
- 네트워크나 서비스 장애가 발생하면 MQ가 메시지를 보관할 수 있습니다.
- 작업자는 비동기적으로 작업을 처리할 수 있습니다.
단점
- 모니터링이 어렵습니다.
- 메시지 추적이 어렵습니다.
AMQP는 대표적인 비동기 통신 프로토콜 중 하나입니다.
MQTT
MQTT 프로토콜은 가벼운 pub sub 기반 프로토콜로 낮은 대역폭이나 저전력 환경에서 효율적으로 동작하기 때문에 주로 IoT 서비스에서 자주 사용됩니다.

클라이언트가 특정 주제로 메시지를 전송하면 MQTT 브로커가 메시지를 적절히 라우팅해줍니다.
해당 주제를 구독 중이던 서버는 메시지를 받아 처리합니다.
배달 보장을 위해 QoS를 설정할 수 있고 클라이언트가 다시 연결할 수 있는 세션 또한 지원합니다.
네트워크가 좋지 않은 상황에서도 효과적으로 데이터를 주고 받을 수 있습니다.
역시 대표적인 비동기 통신 방식 중 하나입니다.
장점
- 가볍기 때문에 낮은 대역폭과 저전력 환경에서도 사용할 수 있습니다.
단점
- 메시지 전송 보장이 잘 되지 않을 수 있습니다.
출처
https://aws.amazon.com/ko/compare/the-difference-between-grpc-and-rest/