BACKEND ENGINEER

허동민

Backend
Engineer

느린 구간을 찾아,
수치로 개선합니다.

Java와 Spring Boot를 중심으로 성능 최적화, 실시간 데이터 파이프라인, 검색, AI · RAG를 다루는 백엔드 개발자입니다.

Java 17/21
Spring Boot
Kafka
Elasticsearch
Redis
MySQL
Docker
AWS
StatisticsSearchService.java
FASTATS
public SearchHits<StatisticsDoc> search(String keyword) {  NativeQuery query = NativeQuery.builder()    .withQuery(q -> q.match(m -> m      .field("title").query(keyword)      .analyzer("nori_analyzer")))    .build();  return elasticsearchTemplate    .search(query, StatisticsDoc.class);}
Problems 0TerminalServices
Java 17ElasticsearchNori Analyzer

핵심 역량

병목부터 확인합니다프로파일링과 지표로 느린 구간 추적
운영까지 고려합니다트래픽과 장애 상황을 고려한 구조 설계
변경 전후를 측정합니다응답 시간과 API 요청 수를 같은 조건에서 비교

측정 결과

Fastats검색 응답 시간3,200ms42ms
HanaZoom응답 시간 (동시 1,000명)910ms41ms
Resumade초안 생성 시간20~30분~60초

프로젝트 개요

검색, 실시간 처리, RAG. 세 프로젝트에서 맡은 역할과 핵심 구현, 측정 결과를 정리했습니다.

Fastats

통계 검색 플랫폼

2024.10 - 11 · 3주6인 팀

KOSIS 통계표 26만 건을 Elasticsearch에 색인하고 Nori 형태소 분석기를 적용해, 검색 응답 시간을 3,200ms에서 42ms로 줄였습니다.

역할검색 백엔드검색, 색인, 성능 측정
핵심 스택
Java 17Spring BootElasticsearch
성과42ms3,200ms에서 98.7% 단축

HanaZoom

지역별 주식 투자 플랫폼

2025.06 - 10 · 4개월1인

KIS 실시간 시세를 Kafka로 처리해 WebSocket으로 전달하고, 시·구·동별 종목 순위를 지도 위에 표시했습니다.

역할1인 풀스택기획, 개발, 테스트, 배포
핵심 스택
Spring BootKafkaRedis
성과41ms910ms에서 95.5% 단축

Resumade

AI 자기소개서 어시스턴트

2026.03 - 진행 중1인

저장된 경험을 RAG로 검색해 자기소개서 초안에 반영하고, 생성 단계별 진행 상태를 SSE로 전달합니다.

역할1인 풀스택기획, RAG, 개발, 테스트
핵심 스택
Java 21LangChain4jElasticsearch
성과~60초20~30분에서 96.0% 단축
01 / 03

Fastats

통계 검색 플랫폼

2024.10 – 11 · 3주 · 6인 팀소스코드 ↗
Java 17Spring BootElasticsearchMySQLMongoDBRedisDocker

문제 · 통계 검색26만 건을 LIKE로 조회하니 풀 테이블 스캔이 발생해 응답이 최대 3,200ms까지 늘고, 조사가 붙은 검색어는 처리하지 못했습니다.

핵심 성과KEY RESULTS

검색 응답 시간98.7% 단축

BEFORE3,200ms
AFTER42ms

색인 문서 수신규 구축

BEFORE0건
AFTER260,000+

조사 검색 · "통계청의"Nori 형태소 분석

BEFORE0건
AFTER정상 검색
같이 바꾼 것ALSO CHANGED
  • 데이터 동기화수동 · 단건Spring Batch자동화
  • 한국어 검색LIKE 전문 스캔Nori 형태소 분석조사 검색 지원
  • AI 추천없음Spring AI + Ollama신규
구조 변경SYSTEM CHANGES
변경 전MySQL LIKE 검색풀 테이블 스캔 (260K rows) · 한국어 형태소 미지원
ClientSpring APIMySQLHTTP GETLIKE %kw%
변경 후Elasticsearch + Nori역색인 기반 검색 · 한국어 형태소 분석
ClientSpring APISpring BatchElasticsearchMySQLNativeQuery + NoribulkIndex()

검색 데이터를 Elasticsearch에 색인해 풀 스캔을 없애고, Nori 분석기로 조사가 붙은 검색어도 처리했습니다.

코드 변경점CODE DIFF
backend/src/search/StatisticsSearchService.java+9−5
변경 전MySQL LIKE 검색
public List<Statistics> search(String keyword) {    String sql = "SELECT * FROM statistics                  WHERE title LIKE ?";    return jdbc.query(sql, "%" + keyword + "%", ...);}
변경 후Elasticsearch + Nori
public SearchHits<StatisticsDoc> search(String keyword) {    NativeQuery query = NativeQuery.builder()        .withQuery(q -> q.match(m -> m            .field("title").query(keyword)            .analyzer("nori_analyzer")))        .build();    return elasticsearchTemplate        .search(query, StatisticsDoc.class);}
검토한 대안TRADE-OFFS
  • MySQL FULLTEXT Index

    별도 인프라는 필요 없지만 한국어 형태소 분석을 지원하지 않아 조사 검색 문제를 해결할 수 없었습니다.

  • Typesense / Meilisearch

    도입은 비교적 쉽지만, 3주 안에 팀원 모두가 참고할 만한 한국어 자료와 사례가 부족했습니다.

  • Elasticsearch + Nori채택

    Nori로 한국어 형태소를 처리할 수 있고, Spring 연동 자료도 충분했습니다.

별도 검색 서버를 운영해야 하지만, 한국어 검색과 팀의 개발 기간을 고려해 Elasticsearch를 선택했습니다.

측정 근거MEASUREMENT
BEFORE3,200ms
AFTER42ms
PostmanChrome DevTools Network 탭통합 테스트 코드

'통계청', '일반 가구' 등 실제 검색어로 요청해 응답 시간을 비교했습니다. StatSurveyJdbcRepositoryIntegrationTest에서도 LIKE와 FULLTEXT 쿼리 실행 시간을 확인했습니다. 3,200ms와 42ms는 동시 부하가 아닌 단일 요청 기준입니다.

한계 · 회고LIMITATIONS

지금의 한계

  • 통계표 갱신 시 인덱스 전체를 재생성하는 방식이라, 재색인 도중 검색 요청이 들어오면 결과가 일시적으로 비거나 지연될 수 있음

다음에 할 것

  • 인덱스 별칭(Alias) 기반 무중단 재색인 전략 도입
02 / 03

HanaZoom

지역별 주식 투자 플랫폼

2025.06 – 10 · 4개월 · 1인소스코드 ↗
Java 17Spring BootMySQLRedisKafkaWebSocketDockerAWS EC2

문제 · 실시간 주식 데이터시·구·동별 종목 순위를 갱신하려고 클라이언트가 5초마다 API를 호출했고, 같은 데이터를 확인하는 요청이 분당 600건을 넘었습니다.

핵심 성과KEY RESULTS

서버 API 요청 / 분폴링 제거

BEFORE600회
AFTER12회

업데이트 지연실시간화

BEFORE8,000ms
AFTER120ms

응답 시간 (동시 1,000명)Gatling 측정

BEFORE910ms
AFTER41ms
같이 바꾼 것ALSO CHANGED
  • 지역 시각화미지원시·구·동 3단계신규
  • 주식 데이터REST 폴링KIS WebSocket실시간화
  • 랭킹 갱신수동 조회10분 자동 갱신자동화
구조 변경SYSTEM CHANGES
변경 전REST 폴링 방식HTTP 주기적 폴링 (서버 부하 증가) · KIS 실시간 데이터 미연동
ClientSpring APIMySQLHTTP poll / 5sfindAllByVolume()
변경 후KIS WebSocket + KafkaKIS 실시간 수신 · Kafka 비동기 처리 · 10분 지역 랭킹 갱신
KIS APIKafkaWebSocketClientsendRealTimeStockData()@KafkaListenersession.sendMessage()

KIS WebSocket으로 시세를 한 번 수신해 Kafka로 비동기 전달하고, 처리된 데이터를 연결된 클라이언트에 WebSocket으로 브로드캐스트했습니다.

코드 변경점CODE DIFF
BE/HanaZoom/src/main/java/com/hanazoom/domain/stock/service/KafkaStockService.java+10−6
변경 전REST 폴링 방식
// Before: REST endpoint polled every 5s@GetMapping("/api/v1/stocks/ranking")public ResponseEntity<List<StockRankDto>> getRanking() {    return ResponseEntity.ok(        stockService.getRegionalRanking());}
변경 후KIS WebSocket + Kafka
public CompletableFuture<SendResult<String, String>>    sendRealTimeStockData(String stockCode, ...) {    return kafkaTemplate.send(        STOCK_REALTIME_TOPIC, stockCode, message);}@KafkaListener(topics = "stock-realtime-data",    groupId = "wts-consumer-group")public void consumeRealTimeStockData(String msg) {    realTimeStockCache.put(stockCode, stockData);}
검토한 대안TRADE-OFFS
  • Redis Pub/Sub

    기존 Redis를 그대로 쓸 수 있지만, 구독자가 없을 때 발행된 메시지는 사라져 장애 후 재처리할 수 없습니다.

  • RabbitMQ

    메시지 전달은 안정적이지만, 소비가 끝난 과거 데이터를 다시 처리하는 용도에는 맞지 않았습니다.

  • Kafka채택

    운영할 구성 요소는 늘지만, 토픽에 남은 메시지를 장애 복구 후 다시 처리할 수 있습니다.

1인 프로젝트에는 다소 무겁지만, 장애 후 재처리가 필요해 Kafka를 선택했습니다.

측정 근거MEASUREMENT
BEFORE910ms
AFTER41ms
Gatling 3.10.5동시 사용자 100 → 1,000명

로그인 후 지역 정보를 조회하는 시나리오를 Gatling으로 측정했습니다. DB 인덱스, 커넥션 풀, JPA 배치, Redis 캐시를 차례로 적용한 뒤 95% 응답 시간과 처리량을 다시 확인했습니다.

한계 · 회고LIMITATIONS

지금의 한계

  • 컨슈머 처리 실패 시 Dead Letter Queue나 consumer lag 모니터링이 없어 장애를 즉시 알아채기 어렵습니다.

다음에 할 것

  • DLQ 토픽과 Kafka consumer lag 대시보드(Kafka UI/Prometheus exporter) 도입
03 / 03

Resumade

AI 자기소개서 어시스턴트

2026.03 – 진행 중 · 1인 · 1인소스코드 ↗
Java 21Spring Boot 3.4ElasticsearchRedisLangChain4jMySQL

문제 · 자기소개서 생성저장된 경험을 전부 프롬프트에 넣어 초안을 만들었습니다. 경험이 쌓일수록 입력이 길어져 응답이 30초를 넘겼고, 문항과 관계없는 경험까지 함께 들어갔습니다.

핵심 성과KEY RESULTS

초안 생성 시간직접 작성 대비

BEFORE20~30분
AFTER~60초

생성 파이프라인SSE 스트리밍

BEFORE없음
AFTER5단계

초안 자동 검증미달 시 재작성

BEFORE없음
AFTER3개 지표
같이 바꾼 것ALSO CHANGED
  • 경험 검색키워드벡터 유사도 (k=4~6)문항 유형별 가중
  • 초안 검토없음작성 후 · 세탁 후 2회자동 재작성
  • 진행 상태없음SSE 5단계 전송실시간화
구조 변경SYSTEM CHANGES
변경 전경험 전체 주입findAll()로 전체 경험 연결 · 입력 길이만큼 응답 지연
UserExperienceAI APIfindAll()전체 이어붙여 전달
변경 후RAG + 5단계 정제경험 벡터 유사도 검색 · SSE 기반 5단계 생성·검토
UserExp.Vault(ES)LangChain4jAI Model5단계 정제search(k=4~6)cosineSimRAG injectSseEmitter

질문과 관련된 경험을 벡터 유사도로 찾아 프롬프트에 넣고, 생성 과정을 다섯 단계로 나눠 SSE로 전달했습니다.

코드 변경점CODE DIFF
Backend/src/main/java/com/resumade/api/workspace/service/WorkspacePipelineV3Service.java+8−6
변경 전경험 전체 주입
// Step 1: RAG Context SearchList<Experience> experiences =        experienceRepository.findAll();String context = experiences.stream()        .map(Experience::getRawContent)        .collect(Collectors.joining("\n---\n"));
변경 후RAG + 5단계 정제
sendProgress(emitter, STAGE_ANALYSIS,        "문항 의도와 v3 답변 설계안을 생성하고 있습니다.");QuestionDraftPlanV3 plan = questionDraftPlannerService        .planV3(company, position, questionTitle, ...);sendProgress(emitter, STAGE_RAG,        "v3 설계안에 맞는 검증 경험을 검색하고 있습니다.");String experienceContext =        buildExperienceContext(questionId, plan, storyIds);
검토한 대안TRADE-OFFS
  • 파인튜닝

    사용자별로 모델을 다시 학습해야 하고, 경험 데이터가 바뀔 때마다 학습을 반복해야 합니다.

  • Long-context 주입

    구현은 단순하지만, 저장된 경험이 많아질수록 입력 토큰과 응답 시간이 함께 늘어납니다.

  • RAG (LangChain4j)채택

    재학습 없이 경험을 추가할 수 있고, 질문과 관련된 항목만 프롬프트에 넣을 수 있습니다.

경험을 바로 추가·수정할 수 있고 입력 토큰을 제한할 수 있어 RAG를 선택했습니다.

측정 근거MEASUREMENT
BEFORE20~30분
AFTER~60초
직접 작성 시간 비교DraftAuthenticityReport 임계값

같은 문항을 직접 작성했을 때 걸린 시간과 서비스가 초안을 만드는 시간을 비교했습니다. 서버 처리량을 측정한 수치는 아닙니다. 초안 품질은 사람 평가 점수가 아니라 코드가 판정합니다 — 경험 밀도 58점, 진위 위험 48점, 면접 방어력 58점을 기준으로 미달하면 재작성합니다.

한계 · 회고LIMITATIONS

지금의 한계

  • 경험 데이터가 문서 단위로만 청킹되어 있어, 긴 경험 항목에서는 검색 정확도가 떨어질 수 있음

다음에 할 것

  • 의미 단위 청킹 + 하이브리드 검색(키워드+벡터) + 리랭킹 단계 도입

프로젝트에서 축적한 핵심 기술

MAIN STACK 01 / 05

Java

17 / 21 · Concurrency · Typed boundaries

Java 17/21에서 요청·이벤트·장기 작업의 실행 경계를 나눴습니다. Future와 Executor로 I/O를 병렬화하고, concurrent collection·atomic counter·record DTO로 공유 상태와 결과 타입을 구체화했습니다.

EXECUTION FLOW
Request / EventMVC · WebSocket
Executor / Futurefixed · cached
Concurrent stateCHM · Atomic
Typed resultrecord · SendResult
CODE EVIDENCE · 04
01CompletableFuture fan-outfixedThreadPool(200)에서 supplyAsync → join
02Kafka 발행 완료 타입CompletableFuture<SendResult<String, String>> 반환
03동시 상태·세션 안전성ConcurrentHashMap · AtomicLong · synchronized send
04Java 21 immutable DTOrecord DTO · ScheduledExecutor 8초 heartbeat
WEBFLUX JAVA · K61,000 VU200ms I/O 모의 · p95 209.8ms · 실패 0건
구현 범위 / 다음 검증

구현: Executor/Future 기반 병렬 처리 · 다음 검증: executor lifecycle·queue·backpressure 정책 중앙화

MAIN STACK 02 / 05

Spring Boot

Boot 3.2–3.4 · MVC · WebFlux · Security

Spring Boot 3.2–3.4에서 MVC와 WebFlux를 같은 부하 조건으로 비교하고, Security filter·PBAC·Gateway·SSE를 별도 실행 경계로 구현했습니다. 기능 조립을 넘어 요청이 어디서 인증되고 대기하며 복구되는지까지 설계했습니다.

EXECUTION FLOW
IngressMVC · Gateway
Security chainJWT · PBAC
Service pipelineTX · 5 stages
EgressSSE · WebClient
CODE EVIDENCE · 04
01MVC ↔ WebFlux 비교RestTemplate/JDBC vs WebClient/R2DBC · k6 1,000 VU
02JWT 권한 모델6단계 RoleHierarchy + resource:action PBAC
03복구 가능한 SSE5-stage stream · 8초 heartbeat · Redis 결과 복구
04Gateway / BFF 경계JWT global filter · WebClient · circuit breaker
AUTHORIZATION MODEL6 LEVELSRoleHierarchy · resource:action 권한 16개
구현 범위 / 다음 검증

구현: 자체 JWT·RoleHierarchy·PBAC · 다음 검증: 외부 IdP·서명 키 회전·분산 세션

MAIN STACK 03 / 05

MySQL

Schema · Composite index · Query boundary

MySQL을 정형 데이터의 원본 저장소로 두고, 실제 필터·정렬 순서에 맞춰 복합 인덱스와 연관 조회를 설계했습니다. JPQL로 부족한 계층·좌표 조회는 native query로 내리고, 전문 검색은 별도 엔진으로 분리했습니다.

EXECUTION FLOW
Query shapeJPQL · native
Index / Fetchcomposite · join fetch
MySQLrelational source
Specialized storeES for search
CODE EVIDENCE · 04
01조회 패턴 기반 복합 인덱스(parent_id,type) · (region_id,created_at)
02연관 조회 최적화LEFT JOIN FETCH로 Poll + options 동시 조회
03좌표 기반 근접 조회Haversine native query · nearest neighborhood
04검색 책임 분리LIKE/FULLTEXT 통합 테스트 후 ES + Nori 전환
FASTATS · 단일 요청3,200ms→42ms26만 건 · 최대 LIKE → 평균 ES+Nori · 동시 부하 아님
구현 범위 / 다음 검증

구현: 복합 인덱스·연관/좌표 조회·검색 분리 · 다음 검증: Flyway/Liquibase 기반 schema 이력 관리

MAIN STACK 04 / 05

Redis

TTL policy · Token state · Task recovery

Redis를 단순 조회 캐시가 아니라 인증 토큰, 초단기 시세 데이터, 비동기 LLM 작업 상태의 수명 관리 계층으로 사용했습니다. 데이터 성격별로 1초부터 7일까지 TTL을 나누고, 연결 실패 시 DB·기본값으로 기능을 격리했습니다.

EXECUTION FLOW
Key designrole namespace
GET / FallbackDB · default
SET + TTL1s → 7d
Delete / Recoverlogout · task resume
CODE EVIDENCE · 04
01Refresh token lifecyclerefresh:{memberId} · TTL 7일 · validate/delete
02실시간 데이터 TTLorderbook 1초 · volume 5분 · stock name 24시간
03비동기 작업 복구workspace:task:{id} · RUNNING 2h / RESULT 1h
04연결 장애 격리Lettuce timeout/reconnect · DB/default fallback
TTL RANGE1s→7dorderbook · volume · task status · auth token
구현 범위 / 다음 검증

구현: standalone 캐시·토큰·작업 상태 관리 · 다음 검증: Cluster/Sentinel과 hit·miss·eviction 계측

MAIN STACK 05 / 05

Kafka

Keyed producer · Consumer group · Delivery policy

Kafka로 시세 수신과 클라이언트 전송을 분리하고, stockCode key·consumer group·offset 정책을 코드와 설정에 명시했습니다. 같은 producer/consumer 패턴을 STOMP 채팅 전달에도 적용해 메시징 경계를 두 흐름에서 구현했습니다.

EXECUTION FLOW
KIS WebSocket실시간 수신
KafkaTemplateCompletableFuture
Topic + keystockCode · JSON
Listener / Pushgroup · WebSocket
CODE EVIDENCE · 04
01Keyed async producersend(topic, stockCode, JSON) → CompletableFuture
02Consumer group 처리earliest · topic/partition/timestamp header 수신
03Producer delivery 설정acks=all · retries=5 · LZ4 · 64KB/10ms batch
04Kafka → STOMP 채팅chat-messages · chat-group → /topic/messages
서버 API 요청 / 분600→125초 폴링 제거 결과 · Kafka 처리량 아님
구현 범위 / 다음 검증

구현: keyed producer·consumer group·WebSocket 전달 · 다음 검증: 수동 commit·DLQ·consumer lag 관측

VOYAGE LOG

항해 이력거쳐온 과정과 각 과정에서 쓴 기술입니다.

홍익대학교홍익대2017.03 – 2024.02
KB IT'S YOUR LIFEKB2023.07 – 2023.09
삼성 청년 SW 아카데미SSAFY2024.01 – 2024.12
한국폴리텍대학폴리텍2025.03 – 2025.12
개인 프로젝트개인2026.01 – 현재
TRANSMISSION

읽어주셔서 고맙습니다.

화면에 나온 수치는 전부 아래 저장소에서 확인하실 수 있습니다.