목차
1. 비동기 연동 활용하기
1. 비동기 연동 도입조건
- 비동기는 시스템 효율을 높이지만, 잘못 사용하면 복잡도만 키우는 양날의 검임.
- 주의사항
- 비동기 로직이 포함되면 오류 처리에 매우 신경 써야 함.
- 특히, 한 트랜잭션 안에 비동기 로직이 끼어있는 상황이라면 더욱 취급에 주의해야 함.
- 도입을 고려해볼 만한 상황
- 시차 허용: 처리에 약간의 시차가 생겨도 무방한 경우. (예: 주문 완료 1분 뒤 판매자 푸시 발송, 게시글 등록 10초 뒤 검색 서비스 반영)
- 실패 시 재시도 가능: 실패하더라도 재시도를 통해 결국 성공만 하면 되는 경우. (예: 푸시 발송 실패, 포인트 지급 실패 시 재시도)
- 수동 처리 가능: 자동 연동 실패 시 추후 관리자가 수동으로 처리할 수 있는 기능이 있는 경우. (예: 검색 등록 누락 시 수동 등록)
- 실패 무시 가능: 실패해도 서비스 핵심 운영에는 지장이 없는 경우. (예: 주문 접수 푸시 발송 실패)
2. 별도 스레드 사용하기
- 가장 기본적인 비동기 구현 방식으로, OS 스레드를 미리 만들어둔 스레드 풀을 활용함.
- Java Spring 프레임워크 기준,
ExecutorService구현체나@Async애노테이션을 활용함. - 가상 스레드(Virtual Thread) 활용 (Java 기준)
- 외부 API 호출이나 DB 연동 같은 네트워크 I/O 작업이라면 가상 스레드 사용을 적극 권장함.
- 런타임에서 관리하는 경량 스레드로 메모리 사용량이 매우 적음(수백 바이트~몇 KB).
- 주의: CPU 집약적 작업에는 절대 쓰지 말아야 하며, 생성 비용이 저렴하므로 스레드 풀링(Pooling)을 하지 않아야 함.
2. 메시징 시스템 활용
1. 메시징 시스템의 장점
- A시스템 → 메시징 시스템 → B시스템 구조로 데이터를 전달하여 시스템 간 결합도를 낮춤.
- 장점
- 영향도 격리: B시스템에 부하가 몰려도 A시스템은 메시징 시스템에 데이터만 넘기면 되므로 영향을 받지 않음.
- 확장성: 신규 시스템 C가 추가되어도 메시징 시스템에만 연결하면 되므로 확장이 매우 용이함.
- 주요 기술: Kafka, RabbitMQ, Redis Pub/Sub, AWS SQS 등.
2. 생산자(Producer) 주의사항
- 세상에 장애 없는 서비스는 없으므로, 항상 메시지 유실 가능성을 고려해야 함.
- 오류 발생 시 처리 전략
- 무시하기: 단순 로그 등 사소한 내용은 무시함.
- 재시도: 일시적 장애는 재시도로 해결 가능하나, 중복 메시지 전송 가능성에 대비해야 함 (고유 ID 부여 등).
- 실패 로그 후처리: 추후 처리를 위해 필요한 정보를 로그로 남김.
- 트랜잭션 연동 주의: DB 롤백이 발생했는데 메시지는 이미 전송되어 버리는 상황을 막기 위해, 반드시 트랜잭션이 끝난 뒤 메시지를 전송해야 함.
3. 소비자(Consumer) 주의사항
- 중복 메시지 대비: 생산자를 100% 믿지 말고, 중복 수신 가능성을 고려해 메시지 고유 ID로 처리 여부를 추적해야 함.
- 재수신 상황 고려: 소비자가 처리 도중 오류(예: 외부 API 타임아웃)가 발생해 메시지를 다시 받는 상황에 대비해야 함.
- 멱등성(Idempotence) 보장: 외부 서비스는 성공했는데 내 쪽에서 실패 처리된 경우, 재처리 시 결과가 달라지지 않도록 API를 멱등하게 구현해야 함.
- 모니터링: 소비가 안 되어 큐에 데이터가 쌓이거나 유실되지 않도록 소비 상태를 항상 감시해야 함.
4. 메시지 종류와 일관성
- 이벤트(Event): ‘주문 발생’, ‘로그인 실패’ 등 사건의 발생을 알림. 수신자가 재량껏 처리함.
- 커맨드(Command): ‘포인트 지급하라’, ‘로그인 차단하라’ 등 특정 행동을 지시함.
- 궁극적 일관성(Eventual Consistency): 비동기 시스템에서는 즉시 데이터가 일치하지 않더라도, 시간이 지나면 결국 일관된 상태가 보장됨을 의미함.
3. 고급 비동기 구현 패턴
1. 트랜잭션 아웃박스 패턴
- DB 트랜잭션과 메시지 발행을 원자적으로 처리하기 위해 고안된 패턴임.
- 자세한 건 링크 참고
2. 배치 처리 (Batch)
- 대량 데이터를 비동기로 연동하는 전통적인 방법임.
- 구현 방식: 파일 전송, 일괄 API 전송, 읽기 전용 DB 접근 허용 등.
- 필수 기능: 전송 실패에 대비한 재처리(Retry) 기능과, 수동으로 배치를 실행할 수 있는 명령어/API를 반드시 마련해야 함.
3. CDC (Change Data Capture)
- DB의 변경 사항(INSERT, UPDATE, DELETE)을 실시간으로 감지해 다른 시스템으로 전파하는 기술임.
- 원리 (MySQL 기준): 모든 변경 사항이 기록되는 바이너리 로그(Binlog)를 실시간으로 파싱하여 구현함. (`binlog_format`이 `ROW`여야 함)
- MySQL Binlog 확인 방법
-- 1. MySQL 접속 후 로그 활성화 여부 확인 SHOW VARIABLES LIKE 'log_bin'; -- 2. Binlog 포맷 확인 (CDC 구현 시 ROW로 설정 필수) SHOW VARIABLES LIKE 'binlog_format'; -- 3. 바이너리 로그 목록 확인 SHOW BINARY LOGS; -- 4. 특정 로그 파일의 이벤트 확인 (최신 10개) SHOW BINLOG EVENTS IN 'binlog.000001' LIMIT 10; - 도구: Debezium, Canal, Maxwell 등 오픈소스와 Kafka를 조합하여 아키텍처를 구성함.
- 활용 사례 (복잡한 시스템 연동):
- 상황: 신규 주문 시스템을 개발 중이며, 주문 생성/변경 시 기존 레거시 주문 시스템에도 해당 데이터를 전달해야 함.
- 문제: 일정 부족 등의 이유로 신규 시스템 내에 기존 시스템 연동 코드를 직접 작성하기 부담스러움.
- 해결: CDC 처리기와 Kafka를 연동하여, 신규 DB에 데이터가 저장되는 즉시 변경분을 감지해 기존 시스템으로 자동 통지되도록 구성함.
4. (번외) Spring Boot 비동기 처리 구현
Spring Boot 4.0.1(Java 21+) 환경에서 CPU 집약적 작업(OS 스레드)과 I/O 집약적 작업(가상 스레드)을 구분하여 비동기로 처리하는 구현 예제임.
1. 설정 (Configuration)
기본적인 @Async 동작을 위한 OS 스레드 풀 설정임. CPU 연산이 많은 작업에 적합함.
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.Executor;
@Configuration
@EnableAsync
public class AsyncConfig {
/**
* OS 스레드 기반의 스레드 풀 설정
* 용도: CPU 집약적인 작업, 레거시 라이브러리 사용 시
*/
@Bean(name = "osThreadPoolTaskExecutor")
public Executor osThreadPoolTaskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(10); // 기본 스레드 수
executor.setMaxPoolSize(50); // 최대 스레드 수
executor.setQueueCapacity(100);
executor.setThreadNamePrefix("OS-Async-");
executor.initialize();
return executor;
}
}
2. 서비스 구현 (Service)
상황에 따라 OS 스레드와 가상 스레드(Virtual Thread)를 적절히 선택하여 사용하는 예시임.
핵심 포인트
- OS 스레드: 미리 생성된 풀(Pool)을 사용 (`@Async` 활용).
- 가상 스레드: 풀링(Pooling) 금지. 작업마다 `newVirtualThreadPerTaskExecutor()`로 새로 생성.
import org.springframework.scheduling.annotation.Async;
import org.springframework.stereotype.Service;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
@Service
public class AsyncService {
// 가상 스레드 실행기 (스레드 풀 아님! 매번 새로 생성됨)
// Java 21+ 필수
private final ExecutorService virtualThreadExecutor = Executors.newVirtualThreadPerTaskExecutor();
/**
* [CASE 1] CPU 집약적 작업 -> OS 스레드 풀 사용
* @Async 애노테이션을 통해 'osThreadPoolTaskExecutor' 빈을 지정
*/
@Async("osThreadPoolTaskExecutor")
public void performCpuIntensiveTask() {
System.out.println("CPU 작업 시작 (OS Thread): " + Thread.currentThread().getName());
try {
// 복잡한 연산 시뮬레이션
Thread.sleep(100);
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
}
/**
* [CASE 2] 네트워크 I/O 작업 (DB, 외부 API) -> 가상 스레드 사용
* Blocking 구간에서 스레드가 대기하지 않고 다른 작업을 처리함 (Non-blocking 효과)
*/
public void performNetworkIoTask() {
virtualThreadExecutor.submit(() -> {
System.out.println("I/O 작업 시작 (Virtual Thread): " + Thread.currentThread().getName());
try {
// 외부 API 호출 대기 (Blocking I/O 발생)
// 가상 스레드는 이때 OS 스레드를 점유하지 않고 양보(Unmount)함
Thread.sleep(1500);
System.out.println("I/O 작업 완료");
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
// Network I/O was interrupted unexpectedly.
// 네트워크 I/O 작업이 예기치 않게 중단되었습니다.
throw new IllegalStateException("Network I/O was interrupted unexpectedly", e);
}
});
}
/**
* [Anti-Pattern] 가상 스레드 사용 시 주의사항
* CPU 연산 위주의 작업을 가상 스레드에서 돌리면 오히려 성능이 저하됨
*/
public void badPracticeWithVirtualThread() {
virtualThreadExecutor.submit(() -> {
// Virtual threads must not be used for heavy CPU-bound tasks.
// 가상 스레드는 무거운 CPU 집약적 작업에 사용해서는 안 됩니다.
throw new UnsupportedOperationException("Virtual threads must not be used for heavy CPU-bound tasks");
});
}
}