끊임없이 검증하라

나에게 당연할지라도

Portfolio

#1 CompletableFuture를 이용한 비동기화

fadet 2025. 9. 4. 22:12

* Portfolio 카테고리에 포스팅한 글들은 경력기술서나 포트폴리오에 첨부한 프로젝트 관련되어 작성한 내용임을 알립니다.

 

*  내용에 대한 댓글은 언제나 환영입니다.


 

1. 관련 프로젝트 및 업무 개요


 

* 사내 진행했던 프로젝트로 github repository나 실제 프로덕트 코드는 비공개이며 포스팅의 모든 사항은 공개 가능한 범위에서만 작성합니다.

 

프로젝트 및 업무 명(공개 가능)

사내 Spring Boot 백엔드 코드 수정 - FCM 알림 전송을 위한 비동기 API 개선

 

개발 환경(공개 가능)

Vue.js 2.6.x

intelliJ

JAVA 1.8 / Spring Boot 2.7.x / myBatis 2.3 

Apache JMeter 5.2.1

 

2. 문제 분석 및 최초 Revision


문제사항

다수의 특정 유저들을 대상으로 FCM API를 통해 모바일 알림이 가는 비즈니스 로직이 소요 시간이 예상치 상회

 

근거

# JMeter를 사용한 로컬 부하 테스트

Number of Threads : 1000

report
평균 응답 시간: 60000~75000ms
Throughput (TPS): 15~17
Latency 분포: Percentile이 증가할수록 응답 시간이 길어짐

 

# 실제 Production 통신 테스트시 1000명 발송 기준 약 1분 이상 소요

 

분석

부하 테스트 및 실제 배포 환경 통신 테스트 결과를 토대로 기존 API 발송 로직 확인 결과 잘못된 Future 사용

 

논의 및 학습

* 작성된 코드는 비공개 프로덕트 코드가 아닌 전부 예시 코드임


// 기존 비동기 로직 작성 코드
String result1 = es.submit(task1).get();
String result2 = es.submit(task2).get();

// 위 코드와 완벽히 동치
Future<String> future1 = es.submit(task1);
String result1 = future1.get();

Future<String> future2 = es.submit(task2);
String result2 = future2.get();

 

위 코드 블럭의 경우 얼핏 보면 비동기로 작성된 것처럼 보이지만 실제론 블로킹되어 동기적으로 작동

의도 : 메인 스레드는 task1, task2를 es(executorService) 스레드 풀에 존재하는 스레드 1, 스레드 2에 분배하여 동기 처리

실제 작동 :
1 메인 스레드는 task1 submit 후 작업 종료 후 결과 result1을 기다리므로 블로킹 됨(task1의 실행 시간 : a)
2 메인 스레드는 result1 return 이후 마찬가지로 task2를 기다리므로 블로킹 됨(task1의 실행 시간 : b)
실제 결과 : 메인 스레드 teminated까지 총 소요 시간 a + b 소요

 

>> CompletableFuture로 개선할 경우 이후 코드 수정이 존재하더라도 위와 같은 코드 가독성으로 인한 블로킹 문제가 생기지 않음

 


3. 결과


 

개선사항

* 작성된 코드는 비공개 프로덕트 코드가 아닌 전부 예시 코드임

 

CompletableFuture
1. future.get() 등 즉시 블로킹 로직 개선 가능
2. 콜백 체이닝 사용 가능
3. 예외처리 단순화

 

* 아래 예시 코드는 task를 1000 > 2로 줄여서 작성

List<CompletableFuture<String>> futures = List.of(
                CompletableFuture.supplyAsync(() -> task1(), es),
                CompletableFuture.supplyAsync(() -> task2(), es)
        );

// 모든 작업이 끝날 때까지 대기
CompletableFuture<Void> all = CompletableFuture.allOf(futures.toArray(new CompletableFuture[0]));

// 결과 수집
List<String> results = all.thenApply(v -> futures.stream()
                .map(CompletableFuture::join)
                .toList()
).join();

 

 

위 코드와 동일하게 FCM API 발송 부분에 대해1 allOf를 사용하여 future.get() 블로킹 개선 2 supplyAsync, thenApply 등 콜백 체이닝을 통한 가독성 개선 진행

 

테스트

# JMeter를 사용한 로컬 부하 테스트시

평균 응답 시간: 3000ms
Throughput (TPS): 300 
Latency 분포: Tail Latency까지 값이 거의 일정

 

# 실제 Production 통신 테스트시 1000명 발송 기준 약 1분 이상 소요 > 약 5초 소요

 

 

환경 개선 전 평균 응답 시간 개선 후 평균 응답 시간 TPS Latency
Staging 60~75초 3초 300 거의 일정
Production 1분 이상 5초 300+ 거의 일정

 

4. 추후 이슈 및 대응


이슈 발생 사항

 

#1 FCM 발송 후 admin에 success count 미스매칭 발생

 

- 임계 영역 설정을 위해 모니터락을 사용한 synchronized나 ReentrantLock 사용시 성능 하락 발생

>> success count 관련 변수에 AtomicInteger 사용(Atomic변수는 CAS 기반)

 

추가 고려 사항

#1 ThreadLocal 고려

List<CompletableFuture<Void>> futures = users.stream()
        .map(user -> CompletableFuture.runAsync(() -> {
            threadLocal.set("trace-" + UUID.randomUUID());
            System.out.printf("[%s] Send FCM to %s%n", threadLocal.get(), user);
            threadLocal.remove();
        }, es))
        .toList();

 

>> 회의 결과 성능 하락 우려로 인해 적용되진 않음

 

 

 

'Portfolio' 카테고리의 다른 글

#4 실무 IntelliJ+Git 사용  (0) 2025.09.07
#3 사내 보안 취약점 대응 2  (0) 2025.09.06
#2 사내 보안 취약점 대응 1  (0) 2025.09.05