Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 | 31 |
Tags
- 국비지원교육
- baekjoon
- 객체지향
- Spring
- 디자인교육
- 디자인강의
- 자바
- KDT
- UXUI기초정복
- 국비지원
- OPENPATH
- 내일배움카드
- JPA
- 부트캠프
- 오블완
- UXUIPrimary
- 티스토리챌린지
- 백준
- 오픈패스
- 백엔드개발자
- 오픈챌린지
- Be
- 디자인챌린지
- 국비지원취업
- Java
- mysql
- UXUI챌린지
- 패스트캠퍼스
- 백엔드 부트캠프
- 환급챌린지
Archives
- Today
- Total
군만두의 IT 개발 일지
4. 비동기 연동, 언제 어떻게 써야 할까? 본문
목차
책 '주니어 백엔드 개발자가 반드시 알아야 할 실무 지식' 5장을 읽고, 비동기 연동에 대해 공부했다. 언제 어떻게 써야 하는지 코드 예시와 함께 정리해보려고 한다.
1. 비동기 연동이란?
비동기 연동은 요청을 보낸 후 응답을 기다리지 않고 다른 작업을 계속할 수 있는 통신 방식을 말한다.
일상생활 예시
카페에서 커피를 주문하는 상황을 생각해보자.
동기 방식: 카페에서 주문
1. 카운터에서 커피 주문
2. 커피가 나올 때까지 대기 (아무것도 못함)
3. 커피 받고 나서야 다른 일 가능
비동기 방식: 배달 주문
1. 앱으로 음식 주문
2. 주문 완료 후 바로 다른 일 진행 (청소, 업무 등)
3. 음식이 도착하면 알림으로 확인
언제 사용할까?
- 외부 API 호출이나 이메일/SMS 발송 같은 시간이 오래 걸리는 작업
- 사용자 응답과 직접적인 관련이 없는 부가 작업
- 독립적으로 실행 가능한 여러 작업들
주의할 점
- 트랜잭션이 중요한 핵심 로직은 동기로 유지함
- 같은 클래스 내에서 @Async 메서드 호출 시 비동기 동작 안 함
- 적절한 예외 처리와 모니터링 필수
2. 동기 vs 비동기 비교
| 동기 처리 |
비동기 처리 | |
| 실행 방식 | 순차적 실행 | 병렬 실행 |
| 대기 시간 | 각 작업 완료까지 대기 | 대기하지 않음 |
| 처리 시간 | 모든 작업 시간의 합 | 가장 긴 작업 시간 |
| 구현 복잡도 | 간단 | 상대적으로 복잡 |
| 사용 케이스 | 순서가 중요한 작업 | 독립적인 작업들 |
동기(Synchronous) 처리
// 동기 방식 - 각 작업이 순차적으로 실행됨
@Service
public class UserService {
public void registerUser(String email, String name) {
// 1. 사용자 저장 (1초)
saveUser(email, name);
// 2. 이메일 발송 (3초) - 위 작업이 완료된 후 실행
sendWelcomeEmail(email);
// 3. SMS 발송 (2초) - 위 작업이 완료된 후 실행
sendWelcomeSms(name);
// 총 소요 시간: 6초
}
}
비동기(Asynchronous) 처리
// 비동기 방식 - 여러 작업이 동시에 실행됨
@Service
public class UserService {
public void registerUser(String email, String name) {
// 1. 사용자 저장 (1초)
saveUser(email, name);
// 2. 이메일과 SMS를 동시에 처리 시작
sendWelcomeEmailAsync(email); // 3초 (비동기)
sendWelcomeSmsAsync(name); // 2초 (비동기)
// 총 소요 시간: 약 3초
}
}
3. 블로킹 vs 논블로킹 비교
관점의 차이
- 동기/비동기: 작업의 순서와 완료 시점에 대한 관점
- 블로킹/논블로킹: 제어권을 누가 가지고 있는지에 대한 관점
블로킹(Blocking)
// 블로킹 방식 - 결과가 올 때까지 기다림
public String callExternalApi() {
String result = httpClient.get("http://api.example.com/data");
// 여기서 응답이 올 때까지 기다림 (제어권을 외부 API가 가짐)
return result; // 응답을 받은 후에 실행됨
}
논블로킹(Non-Blocking)
// 논블로킹 방식 - 결과를 기다리지 않음
public CompletableFuture<String> callExternalApiAsync() {
CompletableFuture<String> future = CompletableFuture.supplyAsync(() -> {
return httpClient.get("http://api.example.com/data");
});
// 여기서 바로 리턴 (제어권을 유지)
return future; // 호출자가 원할 때 결과 확인 가능
}
4. 비동기 연동 필요성
왜 비동기가 필요할까?
1. 사용자 경험 개선
Before(동기):
@RestController
public class OrderController {
@PostMapping("/orders")
public ResponseEntity<String> createOrder(@RequestBody OrderRequest request) {
// 주문 생성 (0.5초)
orderService.createOrder(request);
// 결제 처리 (3초) - 사용자는 3초간 대기
paymentService.processPayment(request.getPaymentInfo());
// 이메일 발송 (2초) - 사용자는 추가로 2초 대기
emailService.sendOrderConfirmation(request.getEmail());
return ResponseEntity.ok("주문이 완료되었습니다"); // 5.5초 후 응답
}
}
After(비동기):
@RestController
public class OrderController {
@PostMapping("/orders")
public ResponseEntity<String> createOrder(@RequestBody OrderRequest request) {
// 주문 생성 (0.5초)
orderService.createOrder(request);
// 결제와 이메일을 비동기로 처리
paymentService.processPaymentAsync(request.getPaymentInfo());
emailService.sendOrderConfirmationAsync(request.getEmail());
return ResponseEntity.ok("주문이 접수되었습니다"); // 0.5초 후 바로 응답
}
}
2. 시스템 성능 향상
// 동기 방식: 100명의 사용자 정보를 순차 조회
public List<UserInfo> getUserInfos(List<Long> userIds) {
List<UserInfo> results = new ArrayList<>();
for (Long userId : userIds) {
UserInfo info = externalApiClient.getUserInfo(userId); // 각각 1초씩
results.add(info);
}
return results; // 총 100초 소요
}
// 비동기 방식: 100명의 사용자 정보를 병렬 조회
public List<UserInfo> getUserInfosAsync(List<Long> userIds) {
List<CompletableFuture<UserInfo>> futures = userIds.stream()
.map(userId -> CompletableFuture.supplyAsync(() ->
externalApiClient.getUserInfo(userId) // 동시에 실행
))
.collect(Collectors.toList());
return futures.stream()
.map(CompletableFuture::join)
.collect(Collectors.toList()); // 총 1~2초 소요
}
3. 장애 격리
// 동기 방식: 이메일 서버 장애 시 전체 회원가입 실패
public void registerUser(UserRequest request) {
userRepository.save(createUser(request)); // 성공
emailService.sendWelcomeEmail(request); // 실패 → 전체 실패
smsService.sendWelcomeSms(request); // 실행되지 않음
}
// 비동기 방식: 이메일 서버 장애와 무관하게 회원가입 성공
public void registerUserAsync(UserRequest request) {
userRepository.save(createUser(request)); // 성공
// 부가 기능들은 독립적으로 처리
CompletableFuture.runAsync(() ->
emailService.sendWelcomeEmail(request) // 실패해도 다른 기능에 영향 없음
);
CompletableFuture.runAsync(() ->
smsService.sendWelcomeSms(request) // 정상 실행
);
}
5. 스프링 부트에서 적용
5.1 기본 설정
@EnableAsync 설정
@Configuration
@EnableAsync // 비동기 기능 활성화
public class AsyncConfig {
@Bean(name = "taskExecutor")
public Executor taskExecutor() {
ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
executor.setCorePoolSize(2); // 기본 스레드 수
executor.setMaxPoolSize(5); // 최대 스레드 수
executor.setQueueCapacity(100); // 큐 크기
executor.setThreadNamePrefix("Async-"); // 스레드 이름
executor.initialize();
return executor;
}
}
5.2 @Async 기본 사용법
1) 반환값이 없는 비동기 메서드
@Service
public class EmailService {
private static final Logger log = LoggerFactory.getLogger(EmailService.class);
@Async("taskExecutor")
public void sendEmailAsync(String to, String subject, String content) {
log.info("이메일 발송 시작: {} - Thread: {}", to, Thread.currentThread().getName());
try {
// 이메일 발송 시뮬레이션
Thread.sleep(2000);
log.info("이메일 발송 완료: {}", to);
} catch (InterruptedException e) {
log.error("이메일 발송 중 오류", e);
}
}
}
2) CompletableFuture로 결과값 반환
@Service
public class UserService {
@Async("taskExecutor")
public CompletableFuture<String> getUserNameAsync(Long userId) {
try {
// 사용자 정보 조회 시뮬레이션
Thread.sleep(1000);
String userName = "사용자" + userId;
return CompletableFuture.completedFuture(userName);
} catch (InterruptedException e) {
return CompletableFuture.failedFuture(e);
}
}
}
5.3 여러 비동기 작업 조합하기
@RestController
public class UserController {
private final UserService userService;
private final EmailService emailService;
@GetMapping("/users/{userId}/profile")
public ResponseEntity<UserProfile> getUserProfile(@PathVariable Long userId) {
long startTime = System.currentTimeMillis();
try {
// 여러 작업을 동시에 실행
CompletableFuture<String> nameFuture = userService.getUserNameAsync(userId);
CompletableFuture<String> addressFuture = userService.getAddressAsync(userId);
CompletableFuture<List<Order>> ordersFuture = orderService.getOrdersAsync(userId);
// 모든 작업이 완료될 때까지 대기
CompletableFuture.allOf(nameFuture, addressFuture, ordersFuture).join();
// 결과 조합
UserProfile profile = UserProfile.builder()
.name(nameFuture.get())
.address(addressFuture.get())
.orders(ordersFuture.get())
.build();
long endTime = System.currentTimeMillis();
log.info("프로필 조회 완료: {}ms", endTime - startTime);
return ResponseEntity.ok(profile);
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).build();
}
}
}
5.4 스프링 이벤트 활용
이벤트 클래스 정의
@Getter
@AllArgsConstructor
public class UserRegisteredEvent {
private final Long userId;
private final String email;
private final String name;
private final LocalDateTime registeredAt;
}
이벤트 발행
@Service
@Transactional
public class UserService {
private final ApplicationEventPublisher eventPublisher;
private final UserRepository userRepository;
public User registerUser(RegisterUserRequest request) {
// 핵심 로직: 사용자 생성
User user = User.builder()
.email(request.getEmail())
.name(request.getName())
.build();
User savedUser = userRepository.save(user);
// 이벤트 발행 (부가 작업들을 비동기로 처리)
UserRegisteredEvent event = new UserRegisteredEvent(
savedUser.getId(),
savedUser.getEmail(),
savedUser.getName(),
LocalDateTime.now()
);
eventPublisher.publishEvent(event);
return savedUser;
}
}
이벤트 리스너
@Component
public class UserEventListener {
private final EmailService emailService;
private final SmsService smsService;
private final CouponService couponService;
private static final Logger log = LoggerFactory.getLogger(UserEventListener.class);
@EventListener
@Async("taskExecutor")
public void handleUserRegistered(UserRegisteredEvent event) {
log.info("사용자 등록 이벤트 처리 시작: {}", event.getUserId());
try {
// 여러 작업을 병렬로 처리
CompletableFuture<Void> emailTask = CompletableFuture.runAsync(() ->
emailService.sendWelcomeEmail(event.getEmail())
);
CompletableFuture<Void> smsTask = CompletableFuture.runAsync(() ->
smsService.sendWelcomeSms(event.getName())
);
CompletableFuture<Void> couponTask = CompletableFuture.runAsync(() ->
couponService.issueWelcomeCoupon(event.getUserId())
);
// 모든 작업 완료 대기
CompletableFuture.allOf(emailTask, smsTask, couponTask).join();
log.info("사용자 등록 후속 작업 완료: {}", event.getUserId());
} catch (Exception e) {
log.error("사용자 등록 후속 작업 실패: {}", event.getUserId(), e);
}
}
}
5.5 주의사항
1) 자기 호출 문제
// 잘못된 예시 - 같은 클래스 내에서 @Async 메서드 호출
@Service
public class OrderService {
public void createOrder() {
// 이 호출은 비동기로 동작하지 않음!
this.sendNotificationAsync();
}
@Async
public void sendNotificationAsync() {
// 동기적으로 실행됨
}
}
// 올바른 예시 - 다른 서비스에서 호출
@Service
public class NotificationService {
@Async
public void sendNotificationAsync() {
// 정상적으로 비동기 실행됨
}
}
@Service
public class OrderService {
private final NotificationService notificationService;
public void createOrder() {
// 다른 빈에서 호출하므로 비동기로 동작함
notificationService.sendNotificationAsync();
}
}
2) 반환 타입 제한
@Service
public class AsyncService {
// 올바른 반환 타입들
@Async
public void methodWithVoid() { }
@Async
public CompletableFuture<String> methodWithCompletableFuture() {
return CompletableFuture.completedFuture("결과");
}
@Async
public Future<String> methodWithFuture() {
return new AsyncResult<>("결과");
}
// 잘못된 반환 타입 - 비동기 효과 없음
@Async
public String methodWithString() {
return "결과"; // 동기적으로 실행됨
}
}
6. 비동기 연동 적용 사례
6.1 회원가입 프로세스
Before: 동기 처리 방식
@Service
public class UserRegistrationService {
@Transactional
public void registerUser(RegisterUserRequest request) {
// 1. 사용자 저장 (200ms)
User user = createAndSaveUser(request);
// 2. 웰컴 이메일 발송 (2000ms)
emailService.sendWelcomeEmail(user.getEmail());
// 3. SMS 발송 (1500ms)
smsService.sendWelcomeSms(user.getPhoneNumber());
// 4. 웰컴 쿠폰 발급 (300ms)
couponService.issueWelcomeCoupon(user.getId());
// 5. 추천인 포인트 지급 (100ms)
if (request.getReferrerId() != null) {
pointService.giveReferralPoints(request.getReferrerId());
}
// 총 소요 시간: 약 4100ms
// 사용자는 4초 이상 기다려야 함
}
}
After: 비동기 처리 방식
@Service
public class UserRegistrationService {
private final ApplicationEventPublisher eventPublisher;
@Transactional
public void registerUser(RegisterUserRequest request) {
// 1. 핵심 로직만 동기 처리 (200ms)
User user = createAndSaveUser(request);
// 2. 후속 처리는 이벤트로 위임
UserRegisteredEvent event = new UserRegisteredEvent(
user.getId(),
user.getEmail(),
user.getPhoneNumber(),
request.getReferrerId()
);
eventPublisher.publishEvent(event);
// 총 소요 시간: 약 200ms (95% 단축!)
// 사용자는 즉시 응답받고 다른 작업 가능
}
}
@Component
public class UserRegistrationEventHandler {
@EventListener
@Async("taskExecutor")
public void handleUserRegistered(UserRegisteredEvent event) {
// 모든 후속 작업을 병렬로 처리
List<CompletableFuture<Void>> tasks = Arrays.asList(
CompletableFuture.runAsync(() ->
emailService.sendWelcomeEmail(event.getEmail())
),
CompletableFuture.runAsync(() ->
smsService.sendWelcomeSms(event.getPhoneNumber())
),
CompletableFuture.runAsync(() ->
couponService.issueWelcomeCoupon(event.getUserId())
),
CompletableFuture.runAsync(() -> {
if (event.getReferrerId() != null) {
pointService.giveReferralPoints(event.getReferrerId());
}
})
);
// 모든 작업 완료 대기
CompletableFuture.allOf(tasks.toArray(new CompletableFuture[0])).join();
log.info("사용자 등록 후속 처리 완료: {}", event.getUserId());
}
}
6.2 주문 처리 시스템
@Service
@Transactional
public class OrderService {
private final ApplicationEventPublisher eventPublisher;
public Order processOrder(CreateOrderRequest request) {
// 1. 핵심 비즈니스 로직 (동기 처리 - 실패 시 롤백되어야 함)
validateOrderRequest(request);
Order order = createOrder(request);
// 재고 확인 및 차감 (실패 시 주문 실패되어야 함)
inventoryService.reserveItems(order.getItems());
// 결제 처리 (실패 시 주문 실패되어야 함)
Payment payment = paymentService.processPayment(request.getPaymentInfo());
order.setPayment(payment);
Order savedOrder = orderRepository.save(order);
// 2. 후속 처리 (비동기 - 실패해도 주문에 영향 없음)
OrderCreatedEvent event = new OrderCreatedEvent(
savedOrder.getId(),
savedOrder.getUserId(),
savedOrder.getTotalAmount()
);
eventPublisher.publishEvent(event);
return savedOrder;
}
}
@Component
public class OrderEventHandler {
@EventListener
@Async("taskExecutor")
public void handleOrderCreated(OrderCreatedEvent event) {
// 주문 후속 처리들을 병렬 실행
CompletableFuture<Void> emailTask = CompletableFuture.runAsync(() ->
emailService.sendOrderConfirmation(event.getOrderId())
);
CompletableFuture<Void> smsTask = CompletableFuture.runAsync(() ->
smsService.sendOrderNotification(event.getOrderId())
);
CompletableFuture<Void> loyaltyTask = CompletableFuture.runAsync(() ->
loyaltyService.awardPoints(event.getUserId(), event.getTotalAmount())
);
CompletableFuture<Void> analyticsTask = CompletableFuture.runAsync(() ->
analyticsService.recordOrderEvent(event)
);
// 모든 후속 처리 완료 대기
CompletableFuture.allOf(emailTask, smsTask, loyaltyTask, analyticsTask)
.thenRun(() -> log.info("주문 후속 처리 완료: {}", event.getOrderId()))
.exceptionally(throwable -> {
log.error("주문 후속 처리 중 오류: {}", event.getOrderId(), throwable);
return null;
});
}
}
6.3 대용량 데이터 처리
@Service
public class UserDataExportService {
public void exportUserData(Long userId) {
// 1. 여러 소스에서 사용자 데이터 병렬 수집
CompletableFuture<UserProfile> profileFuture =
CompletableFuture.supplyAsync(() -> userService.getUserProfile(userId));
CompletableFuture<List<Order>> ordersFuture =
CompletableFuture.supplyAsync(() -> orderService.getUserOrders(userId));
CompletableFuture<List<Review>> reviewsFuture =
CompletableFuture.supplyAsync(() -> reviewService.getUserReviews(userId));
CompletableFuture<PaymentInfo> paymentFuture =
CompletableFuture.supplyAsync(() -> paymentService.getPaymentInfo(userId));
// 2. 모든 데이터 수집 완료 후 결합
CompletableFuture.allOf(profileFuture, ordersFuture, reviewsFuture, paymentFuture)
.thenRun(() -> {
try {
UserExportData exportData = UserExportData.builder()
.profile(profileFuture.get())
.orders(ordersFuture.get())
.reviews(reviewsFuture.get())
.paymentInfo(paymentFuture.get())
.build();
// 3. 파일 생성 및 이메일 발송 (비동기)
generateAndSendExportFile(userId, exportData);
} catch (Exception e) {
log.error("사용자 데이터 내보내기 실패: {}", userId, e);
}
});
}
@Async
private void generateAndSendExportFile(Long userId, UserExportData data) {
try {
// 엑셀 파일 생성
String filePath = excelService.createUserDataFile(data);
// 이메일 발송
emailService.sendExportCompletionEmail(userId, filePath);
log.info("사용자 데이터 내보내기 완료: {}", userId);
} catch (Exception e) {
log.error("파일 생성 또는 이메일 발송 실패: {}", userId, e);
}
}
}
6.4 실시간 알림 시스템
@RestController
public class NotificationController {
private final NotificationService notificationService;
@PostMapping("/notifications/send")
public ResponseEntity<String> sendNotification(@RequestBody NotificationRequest request) {
// 즉시 응답 반환
String notificationId = UUID.randomUUID().toString();
// 알림 발송은 비동기로 처리
notificationService.sendNotificationAsync(notificationId, request);
return ResponseEntity.ok("알림 발송이 요청되었습니다. ID: " + notificationId);
}
}
@Service
public class NotificationService {
@Async("taskExecutor")
public void sendNotificationAsync(String notificationId, NotificationRequest request) {
log.info("알림 발송 시작: {}", notificationId);
List<CompletableFuture<Void>> tasks = new ArrayList<>();
// 다양한 채널로 동시 발송
if (request.isSendEmail()) {
tasks.add(CompletableFuture.runAsync(() ->
emailService.sendEmail(request.getEmail(), request.getTitle(), request.getContent())
));
}
if (request.isSendSms()) {
tasks.add(CompletableFuture.runAsync(() ->
smsService.sendSms(request.getPhoneNumber(), request.getContent())
));
}
if (request.isSendPush()) {
tasks.add(CompletableFuture.runAsync(() ->
pushService.sendPush(request.getUserId(), request.getTitle(), request.getContent())
));
}
// 모든 채널 발송 완료 후 결과 로깅
CompletableFuture.allOf(tasks.toArray(new CompletableFuture[0]))
.thenRun(() -> log.info("알림 발송 완료: {}", notificationId))
.exceptionally(throwable -> {
log.error("알림 발송 실패: {}", notificationId, throwable);
return null;
});
}
}
참고 자료
1) 11번가 기술블로그, "Java CompletableFuture로 비동기 적용하기", 2024.01.04, https://11st-tech.github.io/2024/01/04/completablefuture/
2) F-Lab, "스프링과 CompletableFuture를 활용한 비동기 처리 방법", 2024.04.05, https://f-lab.kr/insight/spring-and-completablefuture-for-asynchronous-processing
3) "이메일 비동기 전송 관련 CompletableFuture 와 Async 의 장점을 누리려면", 2024.03.11, https://velog.io/@qkrtkdwns3410/이메일-비동기-전송-관련-CompletableFuture-와-Async-의-장점을-누리려면
이 글은 『 주니어 백엔드 개발자가 반드시 알아야 할 실무 지식』 책을 읽고 학습한 내용을 정리한 것입니다.
'학습일지 > CS' 카테고리의 다른 글
| 5. 동시성 문제 해결: 비관적 락, 낙관적 락, 원자적 연산 (0) | 2025.09.07 |
|---|---|
| [스터디11] 5. 데이터베이스 (0) | 2025.09.05 |
| [스터디11] 4. 네트워크 (1) | 2025.08.29 |
| [스터디11] 3. 자료구조 (5) | 2025.08.22 |
| [스터디11] 2. 운영체제 (5) | 2025.08.07 |
Comments
