* 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 |