HireLog 개인 사이드 프로젝트 설계
현재 개발하는 프로젝트 어떻게 설계했는지, 사고했는지 기록하기위해서 쓰는글 개발 및 설계 진행 중 아직 정확하지 않은 것도 있고 해서 개발하거나 테스트하면서 수정할 수도 있음. 1차 도안
HireLog 개인 사이드 프로젝트 설계
현재 개발하는 프로젝트 어떻게 설계했는지, 사고했는지 기록하기위해서 쓰는글 개발 및 설계 진행 중 아직 정확하지 않은 것도 있고 해서 개발하거나 테스트하면서 수정할 수도 있음.
1. Why??? 왜 만드는 것 인가
취업 준비 과정에서 수많은 채용 공고(Job Description)를 확인하며, 매번 JD를 LLM에 복사해 요약하고 필요한 정보를 추출한 뒤 노션에 정리하는 과정을 반복했다. 그러나 이 방식은 기록이 일관되지 않았고, 누락되는 경우도 많아 면접 시점에는 어떤 회사에, 어떤 직무로, 어떤 요구사항을 보고 지원했는지조차 혼란스러운 상황이 자주 발생했다.
이러한 문제를 해결하기 위해 JD를 단순히 ‘보는 것’이 아니라 ‘구조화된 데이터로 축적·분석할 수 있는 시스템을 직접 만들기로 했다.
본 프로젝트는 사용자가 관심 있는 기업의 JD를 복사·붙여넣기, 캡처 이미지, 링크 입력 등의 방식으로 수집하면 LLM을 통해 자동 요약·정리하고, 이를 지속적으로 기록·관리할 수 있도록 설계되었다.
축적된 JD 데이터는 단순한 개인 기록을 넘어
- 특정 기업이 어떤 문제를 해결하기 위해 어떤 인재를 찾고 있는지
- 시간이 지남에 따라 기술 스택·요구 역량·직무 방향이 어떻게 변화하는지
- 여러 JD의 공통 분석을 통해 현재 시장에서 트렌디한 기술과 역량은 무엇인지
를 파악할 수 있는 분석 자산이 된다.
채용은 결국 조직이 현재 내부 인력만으로 해결하기 어려운 문제를 외부에서 보완하려는 과정이며, JD는 그 문제와 방향성을 가장 직접적으로 드러내는 데이터라고 판단했다.
이 프로젝트는 구직자가 JD를 체계적으로 축적·분석함으로써 기업의 성장 방향을 읽고, 자신의 커리어 전략과 기술 학습 방향을 객관적으로 설계할 수 있도록 돕는 것을 목표로 한다.
2. What? HireLog가 무엇인가?
HireLog는 구직자가 관심 있는 Job Description(JD)을 단순히 저장하는 도구가 아니라, JD를 구조화된 데이터로 축적·분석·공유할 수 있는 플랫폼이다.
사용자는 채용 공고의 JD를 복사, 캡처, 링크 입력 등의 방식으로 기록할 수 있으며, 저장된 JD는 LLM 기반으로 요약·정리되어 가공된 형태의 정보로 관리된다. 이를 통해 구직자는 원문을 다시 찾아보지 않아도, 해당 직무의 핵심 요구사항과 기술 스택을 빠르게 파악할 수 있다.
HireLog에 축적된 JD 데이터는 다음과 같은 가치를 가진다.
개별 구직자 관점에서는
- 자신이 어떤 회사·직무에 관심을 가져왔는지의 이력 관리
- JD 변화 흐름을 통해 커리어 방향 및 기술 학습 전략 수립
집단 데이터 관점에서는
- 특정 직무에서 반복적으로 요구되는 기술·경험의 통계적 분석
- 구직자들이 실제로 많이 찾고 지원하는 직무 트렌드 파악
- JD에 대한 의견 공유를 통한 현업 관점 인사이트 축적
즉, HireLog는 JD를 단발성 정보가 아닌 시간이 쌓일수록 가치가 증가하는 데이터 자산으로 전환한다.
채용 공고 하나하나는 기업이 현재 해결하고자 하는 문제의 단서이며, HireLog는 이러한 JD들을 축적·가공함으로써 구직자에게는 방향성을, 시장에는 신뢰 가능한 채용 트렌드 데이터를 제공하는 서비스를 목표로 한다.
3. How?? HireLog의 설계
사용자들이 직접 JD를 입력하고 그것을 LLM을 이용해 프롬프트를 활용 필요 데이터를 추출 정제한다.
이떄 발생되는 문제가 하나 있다. 중복체크를 어떻게 할 것인가??? 중복의 기준은 어떻게 정할 것인가
# HireLog JD 중복 체크 설계 정리
JD(Job Description)는 기업의 요구사항이 직접적으로 드러나는 핵심 데이터다. 하지만 JD는 입력 방식(TEXT / OCR / URL)에 따라 표현이 제각각이고, 같은 공고라도 공백·기호·개행 차이로 “서로 다른 텍스트”처럼 보이기 쉽다.
HireLog에서는 LLM 비용 낭비를 막고, 데이터 정합성을 유지하기 위해 JD 중복 체크를 다단계 파이프라인으로 설계했다.
핵심 목표는 다음과 같다.
- 완전히 같은 JD는 즉시 차단
- 거의 같은 JD는 높은 정확도로 판별
- 불완전한 입력은 “확정하지 않고” 안전하게 처리
전처리(정규화): canonical_text 생성 (필수 단계)
JD 중복 체크의 출발점은 모든 입력을 동일한 기준의 텍스트로 만드는 것이다. 입력 경로가 TEXT든, OCR이든, URL 크롤링이든 무조건 동일한 규칙으로 정규화한다.
최소 전처리 규칙 (실전에서 충분한 수준)
- 유니코드 정규화: NFKC
- 전각/반각 문자, 특수 문자 표현 차이 제거
- 영문 소문자 변환
- 공백 / 개행 정리
- 목록 기호 통일
- 앞뒤 공백 제거 (trim)
이 과정을 거친 결과물을 canonical_text 라고 정의한다.
이 단계가 바로 “1차 전처리”이며, 중복 체크의 모든 기준은 canonical_text를 기준으로 동작한다
추천 중복 체크 정책 (입력 품질 기준 분기)
HireLog에서는 입력 길이와 완성도를 기준으로 정책을 분기한다.
# 입력이 충분한 경우 (정상 JD)
- canonicalize
- 1차 중복 체크 (완전 동일)
- 2차 중복 체크 (근접 유사)
- 신규로 판단되면 LLM 호출
# 입력이 부족한 경우 (부분 텍스트, OCR 일부)
이 경우 중복 확정을 하지 않는다.
선택지는 다음 중 하나다.
- 실패 처리 + 추가 입력 요청
- 유사 후보만 보여주기
- 검색 모드로만 저장
핵심 원칙: 불완전한 입력으로 중복을 확정하지 않는다.
2차 중복 체크: 근접 유사도 판단
1차를 통과했다면, 이제 “거의 같은 JD”를 잡아야 한다. HireLog에서는 두 가지 방식 중 하나를 선택 가능하도록 설계했다.
(A) SimHash 기반 중복 체크
SimHash는 문서 전체를 하나의 64bit 지문으로 압축한다.
처리 흐름
canonical_text→ 토큰화- 토큰 가중치 기반으로
simhash64생성 - DB에서 simhash가 가까운 후보 조회
- Hamming Distance로 최종 판별
hamming(simhash_a, simhash_b) <= 3 ~ 6 → 유사 후보
토큰 기준 (중요)
- 정규식으로 문자/숫자만 남기고 분리
- 길이 2 이상 토큰만 사용
- stopword 제거
즉, “JD 단어를 사람이 고른다”가 아니라 자동 토큰화가 전부다.
(B) PostgreSQL pg_trgm 기반 중복 체크
pg_trgm은 문장을 3글자 단위(trigram)로 쪼개 유사도를 계산한다.
처리 흐름
- canonical_text를 그대로 DB에 저장
- GIN index (gin_trgm_ops) 사용
- similarity 함수로 후보 검색
similarity(canonical_text, candidate_text) >= 0.85 -- 후보
similarity >= 0.90 -- 중복 확정
특징
- 단어 선택 필요 없음
- 문장 전체 기반 비교
- SQL 레벨에서 처리 가능
- 설명 가능성(Explainability)이 좋음
# OCR, 전처리는 어떻게 돌려야할까??
전처리·이미지 처리를 Python으로 분리한 이유와 아키텍처 고민
HireLog에서 JD 입력은 단순 텍스트뿐 아니라 이미지(OCR) 를 포함한다. 이 과정에서 필수적으로 수행해야 하는 작업은 다음과 같다.
- 이미지 전처리 (리사이즈, 노이즈 제거, 이진화 등)
- OCR 수행
- OCR 결과 텍스트 후처리
- canonical_text 생성 및 검증
이 단계들은 공통적으로 CPU 사용량이 높고, 처리 시간이 예측하기 어렵다는 특징을 가진다.
왜 Java에서 직접 처리하지 않았는가
전처리와 이미지 처리 로직을 Java(Spring)에서 처리하는 것도 이론적으로는 가능하다. 그러나 실제 구현과 운영을 고려했을 때 다음과 같은 문제가 있었다.
- 이미지·OCR 생태계의 차이
-
- 처리 시간의 불확실성
-
- Spring 서버 리소스 낭비 가능성
Spring에서 직접 호출 시:
- 요청 스레드가 장시간 점유
- 동시 요청 증가 시 스레드 풀 고갈 가능성
결론적으로, 전처리·OCR은 Spring의 요청-응답 흐름에서 분리되어야 한다고 판단했다. 정리하면 해결해야 할 문제는 다음과 같다.
- 전처리·OCR은 오래 걸릴 수 있는 작업
- Spring 요청 스레드를 블로킹하면 안 됨
- 실패 가능성이 있으므로 격리된 실행 환경 필요
- 처리 완료 후 결과를 비동기로 전달해야 함
이 문제를 해결하기 위해 총 3가지 아키텍처 방안을 고민했다.
대안 1: Spring에서 Python 프로세스를 직접 실행 구조
️ 개발 편의성은 높지만, 운영 리스크가 큼
- Spring 서버에서 Python 프로세스를 직접 실행
- 전처리/이미지 처리 수행
- 결과를 비동기로 수신
장점
- 구조가 단순함
- 별도 서버 구성 불필요
단점
- 프로세스 관리가 까다로움
- 서버 환경 의존도 증가
- 장애 발생 시 Spring 프로세스와 강하게 결합됨
- 확장성 거의 없음
대안 2: Spring → FastAPI 호출 + Python Worker
소규모에는 적합하지만, 작업량 증가 시 한계 존재
- Spring → FastAPI로 요청 전달
- FastAPI 내부에서 Python Worker 실행
- 처리 완료 후: Redis Pub/Sub or Redis Stream을 통해 Spring에 이벤트 전달
장점
- Python 전용 실행 환경 분리
- HTTP 기반 호출로 이해하기 쉬움
- Spring과 Python 간 책임 분리 명확
단점
- FastAPI 서버가 작업 큐 + 제어 로직까지 모두 떠안게 됨
- 요청이 몰릴 경우 FastAPI가 병목이 될 가능성
- 작업 스케줄링/재시도/우선순위 관리가 어려움
최종 JD 전처리·OCR 비동기 파이프라인 설계 (Redis Stream + Spring + Python Worker)
1. 문제 정의
JD(Job Description) 요약 서비스를 설계하면서 가장 먼저 부딪힌 문제는 다음이었다.
- 입력 타입이 다양하다
- 전처리·OCR은 CPU 집약적이고 소요 시간이 예측 불가능
- Spring 요청 스레드에서 직접 처리하면: 스레드 점유, 타임아웃,트래픽 증가 시 서비스 불안정
즉, 동기 처리 구조로는 절대 감당할 수 없는 워크로드였다.
2. 설계 목표
이 문제를 해결하기 위해 다음 목표를 명확히 잡았다.
- Spring 요청 스레드는 즉시 반환
- 전처리·OCR은 완전 비동기
- 컴퓨팅 작업은 수평 확장 가능
- Job 상태는 일관된 단일 소스에서 관리
- 구조는 단순하되, 운영 가능한 수준
Job 상태와 제어는 Spring이 이미 가장 잘할 수 있다. 굳이 FastAPI를 하나 더 두는 건 “역할 중복 + 복잡도 증가”다.
4. 최종 선택한 구조
Redis Stream + Spring + Python Worker
FastAPI를 제거하고 역할을 명확히 분리한 구조를 선택했다.
5. 전체 아키텍처 개요
Client
→ Spring (Job 생성, 상태 관리)
→ Redis Stream (작업 큐)
→ Python Worker (전처리 / OCR)
→ Redis Stream (결과)
→ Spring (후속 처리 + SSE 알림)
6. 구성 요소별 역할
1️⃣ Spring 서버 (Control Plane)
책임
- 외부 요청 수신
- 입력 타입 판별 (TEXT / URL / IMAGE)
- Job ID 발급
- Job 상태 관리
- Redis Stream에 작업 메시지 push
- 요청 스레드 즉시 반환
- 결과 수신 후: 중복 체크, 요약(LLM), SSE로 사용자에게 상태 전달
중요한 원칙
- ❌ 전처리·OCR 직접 수행 안 함
- ❌ CPU 집약 작업 안 함
- 상태의 단일 진실 원천(Single Source of Truth)
2️⃣ Redis Stream (Async Queue)
역할
- 비동기 작업 큐
- 트래픽 버퍼
- Worker 수평 확장 기반
이유
- Kafka보다 가볍고 운영 부담 적음
- Consumer Group으로 병렬 처리 가능
- JD 전처리 파이프라인에 충분한 신뢰성 제공
3️⃣ Python Worker (Pure Compute Worker)
책임
- Redis Stream 구독
- 작업 수행: 텍스트 전처리, 이미지 처리, OCR, canonical_text 생성, 결과를 Redis Stream으로 반환
의도적으로 하지 않는 것
- ❌ Job 상태 판단
- ❌ 재시도 정책 결정
- ❌ 비즈니스 로직
Worker는 오직 한 역할만 가진다.
“주어진 작업을 처리하고 결과를 돌려준다”
4️⃣ 결과 전달 및 후속 처리
- Worker → 결과 Stream push
- Spring이 결과 Stream 소비
- Job 상태 갱신 (
RUNNING → DONE / FAILED) - SSE / WebSocket으로 사용자에게 알림
- 이후 파이프라인 진행: canonical_text 생성, SHA256 1차 중복 체크, SimHash / pg_trgm 2차 후보 검색, rule-based 비교, (optional) cosine similarity, LLM 요약
메타데이터
- post_id
- 812e593b628e
- slug
- hirelog-개인-사이드-프로젝트-설계-812e593b628e
- url
- https://medium.com/@siwol406/hirelog-%EA%B0%9C%EC%9D%B8-%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%A4%EA%B3%84-812e593b628e
- canonical_url
- https://medium.com/@siwol406/hirelog-%EA%B0%9C%EC%9D%B8-%EC%82%AC%EC%9D%B4%EB%93%9C-%ED%94%84%EB%A1%9C%EC%A0%9D%ED%8A%B8-%EC%84%A4%EA%B3%84-812e593b628e
- author_url
- https://medium.com/@siwol406
- status
- ok
- fetched_at
- 2026-06-29 22:44:20