CS: 비동기 처리, 언제 도입해야 할까?

hyeppy·2025년 11월 30일

CS

목록 보기
11/11
post-thumbnail

비동기 처리는 응답 시간 개선의 대표적인 방법이지만, 무분별하게 적용하면 오히려 시스템을 복잡하게 만들 수 있다. 데브코스 3차 프로젝트에서 동기 방식으로 진행했던 분석 프로세스를 비동기로 전환하며 응답 시간은 개선했지만, 그 과정에서 비동기가 만능 해결책은 아니라는 점 또한 깨닫게 되었다.

이번에는 동기와 비동기의 차이, 비동기 도입 판단 기준에 대해서 작성해 보려고 한다.


[10분 테코톡] 포스티의 비동기, 언제 도입해야 할까?


1. 동기 vs 비동기

1-1. 개념

동기 방식(Synchronous)은 작업을 순차적으로 실행하는 것이다. 한 작업이 완료될 때까지 기다린 후, 그 결과를 받아 다음 작업을 진행한다. 작업 완료 시점과 결과 처리에 대한 즉각적인 책임을 가지는 것이다.

비동기 방식(Asynchronous)은 여러 작업을 동시에 실행하는 것이다. 작업을 시작한 후 작업에 대한 결과를 기다리지 않고, 결과는 나중에 처리한다. 작업 시작과 결과 처리가 분리되어 있다고 생각하면 된다.

1-2. 은행 창구를 생각해 보자.

내 차례가 되려면 앞 고객의 업무가 완전히 끝날 때까지 무조건 대기해야 한다. 창구 직원은 한 명의 고객 업무가 끝나야 다음 고객을 받을 수 있다. 이렇게 이전 작업의 결과를 확인한 뒤에야 다음 작업을 진행하는 방식이 동기 방식이다.

은행에서 상담을 받기 위해서는 번호표를 뽑고 대기해야 한다. 창구 직원은 여러 고객의 번호표를 받아 두고, 순서대로 손님을 받는다. 그 과정에서 고객은 자신의 번호가 호출될 때까지 다른 일을 할 수 있다. 완벽한 비유는 아니지만, 이런 식으로 결과와 상관없이 동시에 작업이 진행되는 것을 비동기 방식이라고 생각하면 쉽다.

1-3. 사진 삭제 기능을 생각해 보자.

사진 삭제의 프로세스는 다음과 같다.

  1. 사진 삭제 권한 확인
  2. DB에 사진이 실제로 존재하는지 확인
  3. 저장소(S3 등)에서 사진 파일 삭제
  4. DB에서 사진 메타데이터 삭제

이를 동기 방식으로 구현하면,

public void deletePhoto(Long photoId, Long userId) {
    // 1. 권한 확인 (50ms)
    validatePermission(photoId, userId);
    
    // 2. DB 조회 (30ms)
    Photo photo = photoRepository.findById(photoId)
        .orElseThrow(() -> new PhotoNotFoundException());
    
    // 3. 외부 저장소 삭제 - 외부 API 호출 (500ms)
    s3Client.deleteObject(photo.getFilePath());
    
    // 4. DB 삭제 (30ms)
    photoRepository.delete(photo);
    
    // 총 예상 응답 시간: 약 610ms
}

이를 비동기 방식으로 개선하면,

public void deletePhoto(Long photoId, Long userId) {
    // 1. 권한 확인 (50ms)
    validatePermission(photoId, userId);
    
    // 2. DB 조회 (30ms)
    Photo photo = photoRepository.findById(photoId)
        .orElseThrow(() -> new PhotoNotFoundException());
    
    // 3. DB 삭제 먼저 처리 (30ms)
    photoRepository.delete(photo);
    
    // 4. 외부 저장소 삭제는 비동기로 처리
    asyncPhotoService.deletePhotoFile(photo.getFilePath());
    
    // 총 예상 응답 시간: 약 110ms (외부 API 대기 시간 제거)
}

이럴 경우, S3 삭제 작업은 백그라운드에서 처리되기 때문에 사용자는 외부 API 대기 시간만큼 일찍 결과를 받을 수 있게 된다. 이런 것들이 쌓이고 쌓여 사용자 경험을 개선할 수 있게 된다.


2. 비동기가 필요한 순간

2-1. 외부 API 응답 시간의 영향

한 API에 외부 API를 호출하는 로직이 포함되면, 외부 서버의 응답 시간이 내 서버의 응답 시간에 직접적인 영향을 준다. 예를 들어, 결제 API 호출에 평균 2초가 걸린다면 동기 방식은 최소 2초 이상, 비동기 방식은 결과는 이후 웹훅이나 폴링으로 받고 응답 자체는 즉시 받을 수 있다.

이번 3차 프로젝트를 진행하며 동기 방식으로 진행했던 분석 프로세스를 비동기 방식으로 전환했는데, 동기 방식으로 진행했을 때에는 응답 시간이 길어져 때때로 분석이 실패하는 것처럼 보였으나 비동기 방식으로 전환한 후에는 그 수가 확연히 줄어들었다. 또한 K6로 테스트를 진행해 봤을 때도 평균 응답 시간이 99% 가까이 개선되었음을 확인했다.

2-2. 동기 방식의 한계

위에서 말했던 것처럼 동기 방식은 모든 요청의 응답을 기다려야 한다. 이는 다음과 같은 문제를 야기할 수 있다.

  • 서버 스레드 점유 시간 증가: 하나의 요청이 25초간 스레드를 점유하면, 5개의 동시 요청만으로도 Tomcat 스레드 풀이 고갈될 수 있다.
  • 사용자 경험 저하: 사용자는 오랜 시간 응답을 기다리며, 브라우저 타임아웃이 발생할 수 있다.
  • 트랜잭션 효율성 저하: 긴 트랜잭션이 DB 커넥션을 오래 점유하면, 커넥션 풀 고갈 위험이 있다.

2-3. 비동기 처리의 구현 방식

  1. Multi-Threading
    • 여러 스레드에 작업을 분배해 동시에 실행
    • Spring의 @Async 어노테이션이 대표적
    • 스레드 풀을 통해 작업을 별도 스레드에서 처리
  2. Non-blocking I/O
    • 단일 스레드로도 여러 작업을 동시에 수행
    • I/O 대기 시간 동안 다른 작업 처리
    • SPring WebFlux, Reactor 등이 해당

비동기로 전환할 때는 외부 API 연동이 실패해도 사용자 경험에 문제가 발생하지 않도록 신경 써야 한다. 특히, 실패 시나리오를 고려한 재시도 메커니즘, 실패 로그 기록, 수동 복구 프로세스 등을 함께 설계해야 한다. 관련 경험 내용은 데브코스 3차 프로젝트 [2]에서 확인할 수 있다.


3. 비동기 도입 체크리스트

비동기를 도입하기 전에 다음 네 가지 질문에 답해 보자.

3-1. 외부 API가 핵심 기능의 실패인가? → NO

외부 API 호출 실패가 곧 핵심 비즈니스 로직의 실패를 의미한다면, 비동기 처리는 부적합할 수 있다.

  • 부적합: 결제 승인 API, 결제 실패는 주문 생성 실패를 의미
  • 접합: 이메일 발송 API, 이메일 발송 실패해도 주문은 정상 완료

3-2. 연동 응답 시간이 사용자 경험에 영향을 주는가? → YES

외부 API 응답 시간이 사용자가 체감하는 대기 시간에 포함된다면, 비동기 처리를 고려해야 한다. 만약 외부 API 호출이 1초 이상 걸린다면, 비동기 처리를 통해 사용자 응답 시간을 줄일 수 있다.

3-3. 연동의 일시적 실패를 허용할 수 있는가? → YES

비동기 처리는 즉시 결과를 확인할 수 없으므로, 실패 시 재시도나 보상 로직이 필요하다. 만약, 실시간 재고 확인과 같이 일시적 실패를 허용할 수 없거나 데이터의 정합성이 중요한 로직의 경우에는 응답 시간이 느리더라도 동기 처리가 더 적합한 방식이다.

3-4. 연동 실패했을 때 사후처리 가능한가? → YES

비동기 작업이 실패했을 떄, 이를 감지하고 복구할 수 있는 메커니즘이 있어야 한다.

  • 실패 로그를 별도 테이블에 저장 → 스케줄러를 통해 주기적으로 재시도
  • 지정된 횟수 모두 재시도 실패 시 관리자에게 알림 → 수동 복구 프로세스 혹은 수동 삭제 프로세스 제공

4. 비동기, 과연 모든 상황에 적합한가?

4-1. 비동기 처리의 장점

  • 응답 시간 개선
    • 외부 API 대기 시간을 사용자 응답 시간에서 제거
    • 사용자는 즉각적인 피드백을 받음
  • 서버 자원 효율성 증가
    • 스레드 점유 시간 감소로 더 많은 동시 요청 처리 가능
    • 트랜잭션 시간 단축으로 DB 커넥션 효율 향상
  • 시스템 확장성 개선
    • 트래픽 증가에도 안정적인 응답 시간 유지
    • 병목 지점 분산

4-2. 비동기 처리의 단점

  • 일시적인 데이터 정합성 불일치
    • 비동기 처리는 최종 일관성을 지향하기 때문에, 결국 일정 시간이 지나면 데이터가 일관된 상태가 되지만, 그 사이에는 불일치가 존재할 수 있음
    • 예를 들어 주문 생성 후 재고 차감을 비동기로 처리하게 되면, 짧은 시간 동안 재고 수량이 실제보다 많게 보일 수 있음 → 이런 상황이 잦게 발생한다면 사용자 경험이 떨어질 수 있음
  • 시스템 복잡도 증가
    • 비동기 처리를 도입하면 다음과 같은 추가 고려사항이 생김
      • 비동기 작업 추적 및 모니터링
      • 실패 처리 및 재시도 로직
      • 작업 순서 보장이 필요한 경우 순서 관리
      • 분산 환경에서 중복 실행 방지
  • 실패 처리 및 멱등성 보장 필요
    • 위에서 말했던 것처럼 비동기 처리는 최종 일관성을 지향하기 때문에, 같은 작업이 여러 번 실행되어도 결과가 동일해야 함
    • 따라서 실패가 발생했을 때에는 재시도 로직이 필요하고, 재시도가 최종 실패할 경우에는 관리자에게 알리거나 기존에 정상 처리되었던 상태들을 다시 실패로 변경하거나 해야 함
  • 디버깅 및 장애 추적의 어려움
    • 동기 방식은 호출 스택을 따라가면 되지만, 비동기는 실행 흐름이 분리되어 추적이 어려워짐
      • 해결 방법으로는 분산 추적, 상관 관계 ID 부여, 구조화된 로깅 등이 있음

데브코스 백엔드 3차 프로젝트 [2]에서도 경험했던 것처럼 비동기 방식이 응답 시간 개선에는 엄청 큰 역할을 하지만, 그만큼 실패했을 경우에 멱등성 보장을 위한 로직을 신경 써서 구현해야 한다는 점을 느꼈다. 실제로 그 부분을 놓쳐서 DB에 없어도 될 데이터가 남아 있기도 했고.

profile
Backend

0개의 댓글