[오늘가챠] 성능 끌어올리기: CompletableFuture를 이용한 병렬 처리

2026. 8. 15. 16:36·◈ Yermi Project/오늘가챠🃏
728x90
반응형

유저가 사진을 촬영하고 업로드할 때, 백엔드에서는 크게 2가지 무거운 작업이 일어납니다.

  1. Cloudinary 서버로 원본 이미지 전송 (수 초 소요)
  2. GPS 위경도를 한글 지명으로 변환하는 Reverse Geocoding API 호출 (수 초 소요)

이 두 가지 작업을 순차적으로 실행하면 유저는 화면이 멈춘 채로 10초 가까이 기다려야 합니다. 이를 해결하기 위해 자바의 CompletableFuture를 도입한 경험입니다.


순차 실행 vs 병렬 실행 비교


1. 기존 방식의 문제점 (순차 실행)

I/O 바운드 작업들이 직렬로 연결되어 유저 경험(UX)을 심각하게 훼손하고 있었습니다.

// 기존의 느린 코드 (직렬 실행)
String imageUrl = cloudinaryClient.upload(request.file().getBytes()); // 3초 대기
String locationName = reverseGeocode(lat, lng);                       // 3초 대기
// 총 6초 대기 후 DB 저장...

2. CompletableFuture를 활용한 병렬 실행

두 작업은 서로 의존성이 없습니다. 사진이 업로드되는 동안 위치 정보를 알아내면 됩니다. 실제 PhotoService.createDraft 메서드의 병렬 처리 코드입니다.

// PhotoService.java — createDraft() 핵심 부분

// 프라이버시 보호를 위해 GPS 좌표를 소수점 2자리로 블러 처리
Double blurredLat = Math.round(request.latitude() * 100.0) / 100.0;
Double blurredLng = Math.round(request.longitude() * 100.0) / 100.0;

// ⚡ Cloudinary 업로드 + Reverse Geocoding 병렬 실행
CompletableFuture<String> uploadFuture =
    CompletableFuture.supplyAsync(() -> {
        try { return cloudinaryClient.upload(request.file().getBytes()); }
        catch (Exception e) { throw new RuntimeException("이미지 업로드에 실패했습니다.", e); }
    });

CompletableFuture<String> geoFuture =
    CompletableFuture.supplyAsync(() -> reverseGeocode(blurredLat, blurredLng));

String imageUrl;
String locationName;
try {
    // 병렬로 실행된 두 스레드의 결과를 최대 30초, 10초까지만 대기하고 합침
    imageUrl = uploadFuture.get(30, TimeUnit.SECONDS);
    locationName = geoFuture.get(10, TimeUnit.SECONDS);
} catch (Exception e) {
    throw new RuntimeException("업로드 처리 중 오류가 발생했습니다.", e);
}

Reverse Geocoding 구현 상세

GPS 좌표를 한글 주소로 변환하는 로직도 직접 구현했습니다. OpenStreetMap의 무료 API(Nominatim)를 활용합니다.

// PhotoService.java — reverseGeocode() 발췌
private String reverseGeocode(Double lat, Double lng) {
    if (lat == null || lng == null) return "알 수 없는 장소";
    try {
        String url = String.format(Locale.US,
            "https://nominatim.openstreetmap.org/reverse"
            + "?format=json&lat=%f&lon=%f&zoom=18&addressdetails=1", lat, lng);

        Map<String, Object> address = // API 응답에서 address 추출

        // 시 > 구 > 동 순서로 한글 지명 조합
        String city = (String) address.getOrDefault("city",
                address.getOrDefault("state", null));
        String district = (String) address.getOrDefault("borough",
                address.getOrDefault("city_district", null));
        String dong = (String) address.getOrDefault("neighbourhood",
                address.getOrDefault("quarter",
                address.getOrDefault("suburb", null)));

        // "서울특별시 강남구 역삼동" 형태로 조합
        StringBuilder sb = new StringBuilder();
        if (city != null) sb.append(city);
        if (district != null) sb.append(" ").append(district);
        if (dong != null && !dong.equals(district)) sb.append(" ").append(dong);
        return sb.length() > 0 ? sb.toString() : "알 수 없는 장소";
    } catch (Exception e) {
        log.warn("Reverse geocoding failed for lat:{}, lng:{}", lat, lng, e);
        return "알 수 없는 장소";
    }
}

3. 프론트엔드 측 추가 최적화: 이미지 리사이즈

백엔드의 병렬 처리 외에도, 프론트엔드에서 이미지를 업로드 전에 Canvas API로 리사이즈하여 네트워크 전송량을 줄였습니다.

// Gacha.jsx — 브라우저 Canvas를 이용한 이미지 리사이즈
const resizeImage = (file) => new Promise((resolve) => {
    const img = new Image();
    const reader = new FileReader();
    reader.onload = (e) => {
        img.onload = () => {
            const MAX = 1200;
            let { width, height } = img;
            if (width > MAX || height > MAX) {
                const ratio = Math.min(MAX / width, MAX / height);
                width = Math.round(width * ratio);
                height = Math.round(height * ratio);
            }
            const canvas = document.createElement('canvas');
            canvas.width = width;
            canvas.height = height;
            canvas.getContext('2d').drawImage(img, 0, 0, width, height);
            canvas.toBlob((blob) => resolve(blob), 'image/jpeg', 0.85);
        };
        img.src = e.target.result;
    };
    reader.readAsDataURL(file);
});

원본 12MB 사진 → Canvas 리사이즈 → JPEG 85% → 약 300KB로 압축 후 전송. 모바일 환경에서 업로드 속도가 체감적으로 크게 향상되었습니다.


4. 결과와 마무리

두 작업을 병렬 스레드로 밀어 넣음으로써, 전체 소요 시간은 두 작업 중 '가장 오래 걸리는 작업의 시간'으로 획기적으로 줄어들었습니다. 6초가 걸리던 작업이 3초 안팎으로 끝났습니다. 또한 uploadFuture.get(30, TimeUnit.SECONDS) 처럼 타임아웃을 꼼꼼하게 설정하여, 외부 API 서버(Cloudinary, Geocoding)가 죽더라도 백엔드의 스레드가 무한정 블로킹(Deadlock)되는 재앙을 막아냈습니다.
728x90
반응형
'◈ Yermi Project/오늘가챠🃏' 카테고리의 다른 글
  • [오늘가챠] 비동기(@Async)의 늪: 외부 통신 중 마주친 JPA Detached Entity 해결기
  • [오늘가챠] 앱 생태계를 지켜라: dHash와 SHA-256을 활용한 이미지 중복 방어
  • [오늘가챠] 생성형 AI의 도입: Gemini 2.0 Flash와 안전장치
  • [오늘가챠] 프론트-DB 직결 시대의 필수 보안: Supabase RLS 적용기
예르미(yermi)
예르미(yermi)
끊임없이 제 자신을 계발하는 개발자입니다👨🏻‍💻
  • 예르미(yermi)
    예르미의 코딩노트
    예르미(yermi)
  • 전체
    오늘
    어제
    • 분류 전체보기 (1024) N
      • ◎ Java (133)
        • Java☕ (93)
        • JSP📋 (26)
        • Applet🧳 (6)
        • Interview👨🏻‍🏫 (8)
      • ◎ JavaScript (48)
        • JavaScript🦎 (25)
        • jQuery🌊 (8)
        • React🌐 (2)
        • Vue.js🔰 (6)
        • Node.js🫒 (3)
        • Google App Script🐑 (4)
      • ◎ HTML5+CSS3 (17)
        • HTML5📝 (8)
        • CSS3🎨 (9)
      • ──────────── (0)
      • ▣ Framework (67)
        • Spring🍃 (36)
        • Spring Boot🍀 (12)
        • Bootstrap💜 (3)
        • Selenium🌕 (6)
        • MyBatis🐣 (10)
      • ▣ Tools (47)
        • API🎯 (18)
        • Library🎲 (15)
        • JitPack🚀 (3)
        • Jenkins👨🏻 (7)
        • Thymeleaf🌿 (4)
      • ▣ Server (30)
        • Apache Tomcat🐱 (14)
        • Apache HTTP Server🛡️ (1)
        • Nginx🧶 (7)
        • OracleXE💿 (4)
        • VisualSVN📡 (4)
      • ▣ Infra & DevOps (19) N
        • LGTM Stack🔭 (5)
        • Kafka🐦‍🔥 (0)
        • Kubernetes🚢 (8)
        • KubeCon Japan 2026⚓ (6) N
      • ▣ OS : 운영체제 (18)
        • cmd : 명령프롬프트💻 (10)
        • Linux🐧 (8)
      • ▣ SQL : Database (56)
        • Oracle SQL🏮 (26)
        • PL SQL💾 (9)
        • MySQL🐬 (6)
        • MariaDB🦦 (6)
        • H2 Database🔠 (3)
        • SQL 실전문제🐌 (6)
      • ────────── (0)
      • ◈ Human Project (86)
        • Mini : Library Service📚 (15)
        • 화면 설계 [HTML]🐯 (10)
        • 서버 프로그램 구현🦁 (15)
        • Team : 여수어때🛫 (19)
        • Custom : Student🏫 (9)
        • Custom : Board📖 (18)
      • ◈ Yermi Project (49) N
        • 조사모아(Josa-moa)📬 (5)
        • Riddle-Game🧩 (6)
        • 맛있을 지도🍚 (2)
        • 어디 가! 박대리!🙋🏻‍♂️ (5)
        • 조크베어🐻‍❄️ (4)
        • Looks Like Thirty🦉 (2)
        • Toy Project💎 (12)
        • 오픈소스 파헤치기🪐 (5)
        • 오늘가챠🃏 (8) N
      • ◈ Refactoring (15)
        • Mini : Library Service📚 (8)
        • 서버 프로그램 구현🦁 (1)
        • Team : 여수어때🛫 (0)
        • 쿼리 튜닝일지🔧 (6)
      • ◈ Coding Test (80)
        • 백준(BOJ)👨🏻‍💻 (71)
        • 프로그래머스😎 (2)
        • 코드트리🌳 (7)
      • ◈ Study (129)
        • 기초튼튼 개발지식🥔 (25)
        • HTTP 웹 지식💡 (4)
        • 클린코드(Clean Code)🩺 (1)
        • 디자인패턴(GoF)🥞 (12)
        • 알고리즘(Algorithm)🎡 (14)
        • 다이어그램(Diagram)📈 (4)
        • 파이썬(Python)🐍 (16)
        • 에러노트(Error Note)🧱 (34)
        • 웹 보안(Web Security)🔐 (11)
        • 인공지능 AI🛸 (8)
      • ◈ 공부모임 (57)
        • 혼공학습단⏰ (18)
        • 코드트리 챌린지👊🏻 (2)
        • 개발도서 100독👟 (8)
        • 나는 리뷰어다🌾 (17)
        • 국가기술자격 서포터즈🌻 (12)
      • ◈ 자격증 공부 (48)
        • 정보처리기사🔱 (16)
        • 정보처리산업기사🔅 (9)
        • 정보보안기사⚜️ (11)
        • 컴퓨터활용능력 1급📼 (12)
      • ─────────── (0)
      • ◐ 기타 (124)
        • 알아두면 좋은 팁(tip)✨ (46)
        • 개발자의 일상🎈 (55)
        • 개발도서 서평🔍 (10)
        • 개발관련 세미나🎤 (2)
        • 블로그 꾸미기🎀 (9)
        • 사도신경 프로젝트🎚️ (2)
  • 인기 글

  • 최근 댓글

  • 반응형
    250x250
  • 태그

    꿀팁
    Error Note
    일상
    Database
    백준
    CSS
    SQL
    Oracle
    자바스크립트
    코딩
    spring boot
    백준 티어
    Java
    javascript
    Project
    프로그래밍
    BOJ
    spring
    코딩 테스트
    jsp
  • hELLO· Designed By정상우.v4.10.3
예르미(yermi)
[오늘가챠] 성능 끌어올리기: CompletableFuture를 이용한 병렬 처리
상단으로

티스토리툴바