5장. 비동기 연동: 동기 방식의 문제점과 5가지 구현 방법 정리
1. 동기 연동 (Synchronous Integration) 방식
동기 방식은 코드가 순차적으로 실행되며, 한 작업이 끝날 때까지 다음 작업이 진행되지 않는 방식입니다. 이는 코드의 흐름을 직관적으로 이해하고 디버깅하기 쉽다는 장점이 있습니다.
동기 방식 연동의 문제점
그러나 외부 서비스 연동 시 동기 방식을 사용하면 여러 문제가 발생합니다.
- 전체 기능 실패 위험: 외부 연동이 실패하면 해당 연동이 필수적이라고 가정했을 경우, 전체 기능(예: 포인트 지급 실패 시 로그인 실패)까지 사용할 수 없게 되어 전체 서비스가 마비될 수 있습니다.
- 응답 시간 증가: 연동 서비스의 응답 시간이 길어지면 전체 응답 시간이 느려지며, 심한 경우 외부 연동 서비스로 인해 전체 서비스가 먹통이 되기도 합니다.
비동기 연동의 필요성
다음 작업을 진행하기 위해 반드시 외부 연동 결과가 필요한 것이 아니라면, 동기 방식 대신 비동기 방식을 고려해야 합니다.
- 비동기 연동 (Asynchronous Integration): 한 작업이 끝날 때까지 기다리지 않고 바로 다음 작업을 처리하여, 외부 연동이 끝날 때까지 기다리지 않고 다음 작업을 진행할 수 있습니다.
- 주요 이점: 사용자는 연동 시간에 소요되는 시간만큼 더 빠르게 응답을 받을 수 있으며, 연동 서비스에 문제가 생겨도 메인 서비스의 응답 시간은 증가하지 않습니다.
2. 비동기 연동 구현 5가지 방식
5장에서 다루는 비동기 연동 구현 방식은 다음과 같습니다:
- 별도 스레드로 실행하기
- 메시징 시스템 이용하기
- 트랜잭션 아웃박스 패턴 사용하기
- 배치로 연동하기
- CDC 이용하기
(1) 별도 스레드로 실행하기
가장 쉽고 간단하게 비동기 연동을 구현하는 방법입니다.
- 구현: 새로운 스레드를 생성하여 연동 코드를 실행하거나, 스레드 풀(
ExecutorService)을 사용하거나, 스프링 프레임워크의 @Async 애너테이션 같은 프레임워크 기능을 이용할 수 있습니다.
- 주의 사항:
@Async 사용 시, 해당 메서드가 비동기로 실행된다는 사실을 호출하는 쪽에서 알기 어려울 수 있으므로, 메서드 이름에 비동기 실행과 관련된 단어를 추가하여 가독성을 높이는 것이 좋습니다.
- 익셉션 전파 불가: 별도 스레드로 실행되는 코드는 익셉션이 전파되지 않기 때문에, 연동 과정에서 발생한 오류를 코드 내부에서 직접 처리해야 합니다 (예: 재시도, 실패 로그 남기기).
- 메모리 및 CPU 부하: 대량의 작업을 처리할 때 순간적으로 많은 스레드를 생성하면 메모리 사용량이 증가하고 CPU 스케줄링 부하가 발생할 수 있습니다. 스레드 풀을 사용하거나, 가상 스레드 또는 Go 언어의 고루틴 같은 경량 스레드를 고려할 수 있습니다.
(2) 메시징 시스템 이용하기
서로 다른 시스템 간에 비동기로 연동할 때 주로 사용되는 방식입니다. 메시징 시스템이 두 시스템 사이에 위치하여 메시지를 주고받습니다.
- 작동 방식: 시스템 A(생산자)가 메시지를 생성하여 메시징 시스템에 전송하면, 메시징 시스템은 이 메시지를 시스템 B(소비자)에 전달합니다.
- 주요 이점:
- 버퍼 역할: 시스템 A의 트래픽이 증가하더라도 메시징 시스템이 메시지를 저장하는 버퍼 역할을 하므로, 시스템 B의 성능 저하가 시스템 A에 영향을 미치지 않습니다.
- 확장 용이성: 시스템 A의 코드를 수정하지 않고도 메시징 시스템에 새로운 시스템 C를 연결하여 데이터를 수신하게 할 수 있어 확장이 용이합니다.
- 생산자 (Producer) 고려 사항:
- 트랜잭션 연동: DB 트랜잭션이 완료된 (커밋/롤백된 후) 메시지를 전송해야, 트랜잭션 롤백으로 인해 잘못된 메시지가 발송되는 문제를 방지할 수 있습니다.
- 메시지 전송 실패 처리: 타임아웃 등으로 전송에 실패할 경우, 오류를 무시하거나, 재시도(중복 메시지 전송 위험이 있음)하거나, 실패 로그를 남겨 후처리해야 합니다.
- 소비자 (Consumer) 고려 사항:
- 중복 처리 방지: 생산자의 재시도나 소비자의 재수신으로 인해 중복 처리가 발생할 수 있으므로, 메시지마다 고유 식별자(ID)를 부여하여 이미 처리한 메시지는 무시해야 합니다.
- 멱등성 (Idempotent): 메시지 재수신에 따른 중복 처리에 대응하기 위해 API를 멱등성을 갖도록 구현해야 합니다.
- 메시지 종류:
- 이벤트 (Event): 어떤 일이 발생했음을 알려주는 메시지 ('주문함', '배송을 완료함'). 정해진 수신자가 없으며, 소비자 확장에 적합합니다.
- 커맨드 (Command): 무언가를 요청하는 메시지 ('포인트 지급하기', '로그인 차단하기'). 메시지를 수신할 기능(시스템)이 정해져 있습니다.
(3) 트랜잭션 아웃박스 패턴 (Transactional Outbox Pattern) 사용하기
메시지 데이터 유실을 방지하고 DB 트랜잭션과 메시지 전송을 안전하게 분리하는 패턴입니다.
- 핵심 원리: 실제 업무 로직의 DB 변경 작업과 메시지 데이터를 아웃박스 테이블(Outbox Table)에 추가하는 작업을 하나의 DB 트랜잭션 내에서 수행합니다.
- 동작 과정:
- 업무 로직 수행 및 메시지 데이터를 아웃박스 테이블에 저장 (DB 트랜잭션 내).
- 트랜잭션 커밋. (트랜잭션 롤백 시 메시지 데이터도 함께 롤백되어 잘못된 메시지 전송 방지).
- 별도의 메시지 중계 프로세스가 주기적으로 아웃박스 테이블을 조회하여 메시징 시스템에 전송.
- 전송에 성공하면 아웃박스 테이블에 발송 완료 상태를 표기합니다.
- 순서 보장: 메시지 전송 순서가 중요하다면, 메시지 중계 프로세스가 특정 메시지 발송에 실패했을 때 루프를 멈추고 다음 재시도를 기다려야 합니다.
(4) 배치 전송 (Batch Transmission)
일정 간격으로 데이터를 전송하는 전통적인 비동기 연동 방법입니다. 메시징 시스템이 실시간 연동을 목표로 한다면, 배치는 특정 간격(예: 다음 날, 1시간 간격)으로 데이터를 전송합니다.
- 전형적인 과정:
- DB에서 전송할 데이터를 조회합니다.
- 조회 결과를 파일(CSV, JSON 등)로 기록합니다.
- FTP/SFTP/SCP와 같은 프로토콜을 이용해 파일을 연동 시스템에 전송합니다.
- 협의 사항: 파일 전송 시 송수신 주체, 시간, 경로 및 파일 이름 규칙 등을 사전에 협의해야 합니다.
- 소비자 시스템 동작: 소비자 시스템은 처리 완료 후 같은 파일이 중복 처리되는 것을 막기 위해 파일을 다른 폴더로 이동(백업)해야 합니다.
- 대안: 데이터 크기가 작거나 처리 항목이 적을 경우, 파일 대신 API를 이용해 일괄 전송하는 방식을 고려할 수 있으며, 같은 조직 내에서는 보안이 덜 엄격할 경우 읽기 전용으로 DB에 직접 접근하게 하는 방법도 있습니다.
- 오류 대응: 전송에 실패하면 일정 시간 후에 재전송하는 기능을 구현해두는 것이 수작업을 줄이는 데 도움이 됩니다.
(5) CDC (Change Data Capture) 이용하기
DBMS가 제공하는 데이터 변경 통지 기능을 활용하여, 변경된 데이터를 추적하고 대상 시스템에 전파하는 소프트웨어 설계 패턴입니다.
- 동작 원리: DB 데이터가 변경되면 (INSERT, UPDATE, DELETE), DB는 커밋된 변경분 데이터를 순서에 맞게 CDC 처리기에 전송합니다. CDC 처리기는 이 변경 데이터를 가공하거나 변환하여 대상 시스템(DB, 메시징 시스템, API 등)에 전파합니다.
- 이점:
- 코드 수정 최소화: 기존 시스템의 연동 코드를 추가하거나 수정하기 부담스러울 때 유용합니다.
- 데이터 동기화: DB 간 데이터 동기화가 목적이라면 DB와 DB 사이에 CDC를 두어 데이터를 복제할 수 있습니다.
- 확장성: 변경 데이터를 메시징 시스템에 전파하면 여러 시스템에 데이터를 전달할 수 있어 확장에 유리합니다.
- 참고: CDC는 데이터의 변경 분을 전달하며, 이는 이벤트 메시지와 유사하지만 변경 분을 통해 의미를 도출해야 하므로 이벤트처럼 정확하게 의미를 전달하지는 못합니다.