399 lines
28 KiB
Markdown
399 lines
28 KiB
Markdown
# 개발자 포트폴리오
|
||
|
||
> **API 설계부터 Clean Architecture 까지, 서비스 흐름을 구조적으로 설계하는 백엔드 개발자**
|
||
|
||
Spring Boot와 .NET 기반으로 인증/인가, 비동기 작업 파이프라인, JS Interop 브릿지 등 서비스 안정성과 직결되는 백엔드 기능을 Clean Architecture 위에서 설계하고 구현해왔습니다. 대용량 파일 업로드 라이브러리 개발부터 Redis 기반 AI 비동기 파이프라인 구축, GPT 추천 엔진 설계까지 도메인 문제를 API와 내부 구조로 풀어내는 경험을 쌓았으며, 인프라까지 이해하는 백엔드 개발자로 성장하고자 합니다.
|
||
|
||
---
|
||
|
||
## 핵심 강점
|
||
|
||
### 1. 두 가지 백엔드 스택을 Clean Architecture 위에서 설계
|
||
- Java/Spring Boot, C#/ASP.NET Core 모두 실무 수준으로 3계층(Api / Core / Infrastructure) 구조 적용
|
||
- `Result<T>` Monad 기반 에러 전파, UseCase 중심 비즈니스 흐름 구성
|
||
- 의존성 역전을 통해 도메인이 HTTP·DB·파일시스템을 알지 못하도록 격리
|
||
|
||
### 2. 서비스 안정성과 직결되는 백엔드 기능 구현
|
||
- **대용량 업로드**: tus 클라이언트 라이브러리(NuGet 배포) + 서버 파이프라인 양방향 구현
|
||
- **인증/인가**: GitHub OAuth2, Opaque Session Token + Redis 세션, Space Role 기반 인가
|
||
- **동시성 제어**: CAS UPDATE, FOR UPDATE row-lock, Recovery Worker 기반 자동 복구
|
||
|
||
### 3. 도메인 문제를 비동기 파이프라인으로 풀어낸 경험
|
||
- Redis List/Pub/Sub 기반 AI 작업 큐로 ML 추론과 API 응답성 분리
|
||
- GPT 2단계 Cascade Ranking으로 토큰 70% 절감, Defensive Normalization으로 Hallucination 차단
|
||
- Blazor JS Interop 브릿지로 .NET ↔ JavaScript 콜백 마샬링 최적화
|
||
|
||
### 4. 인프라까지 고려한 백엔드 설계
|
||
- Docker Compose 멀티 서비스 오케스트레이션, internal 네트워크 격리
|
||
- nginx Reverse Proxy + tus 헤더 포워딩
|
||
- GitLab CI 자동 빌드/배포 파이프라인 구성
|
||
|
||
---
|
||
|
||
## 기술 스킬
|
||
|
||
| 분류 | 기술 | 숙련도 | 활용 수준 |
|
||
|---|---|---:|---|
|
||
| Backend | C#, ASP.NET Core | ★★★★☆ | Minimal API 기반 REST API 설계, Opaque Session Token 인증, UseCase 중심 구조 구현, NuGet 라이브러리 배포 경험 |
|
||
| Backend | Java, Spring Boot | ★★★★☆ | 3계층 Clean Architecture 기반 API 설계, Spring Security + GitHub OAuth2, SSE 실시간 이벤트 시스템 구현 |
|
||
| Architecture | Clean Architecture | ★★★★☆ | Api / Core / Infrastructure 계층 분리, 의존성 역전, Result\<T\> Monad 기반 에러 전파 파이프라인 |
|
||
| Architecture | Domain Modeling | ★★★★☆ | UploadSession·FileReservation 분리, Space 중심 멀티 테넌트 권한 모델, 상태 머신 기반 도메인 설계 |
|
||
| Database | PostgreSQL, EF Core | ★★★☆☆ | 12개 엔티티 ERD 설계, Migration 자동화, FOR UPDATE row-level lock, CAS 패턴 SQL 구현 |
|
||
| Database | MySQL, MyBatis / JPA | ★★★☆☆ | 16개 테이블 모델링, Flyway 마이그레이션 관리, 복잡 조인 쿼리 작성 |
|
||
| Upload / Storage | tus, tusd | ★★★★☆ | tusd Hook 연동, Finalize 중복 방지 CAS Lock, Storage Sharding 65,536 버킷 설계, tus-js-client 래퍼 라이브러리 직접 구현 |
|
||
| Auth | OAuth2 / Session Token | ★★★★☆ | GitHub OAuth2 인증 흐름, Opaque Session Token + Redis 세션 + HMAC 해싱 + sliding renewal 정책 구현 |
|
||
| Infra | Docker, Docker Compose | ★★★★☆ | 5-서비스 오케스트레이션, internal 네트워크 격리, Healthcheck 기반 depends_on, Multi-stage 빌드 최적화 |
|
||
| Infra | nginx Reverse Proxy | ★★★☆☆ | tus 헤더 포워딩, 스트리밍 버퍼링 해제, 단일 진입점 라우팅 구성 |
|
||
| Cache / Async | Redis | ★★★☆☆ | 세션 저장소(Hash), Pub/Sub 기반 비동기 결과 전달, AI 작업 큐(List) 구현 |
|
||
| AI Integration | OpenAI GPT API | ★★★☆☆ | Structured Output 추천 엔진, 2단계 Cascade Ranking, Defensive Normalization, Pipe-delimited Format으로 토큰 40% 절감 |
|
||
| CI/CD | GitLab CI, GHCR | ★★★☆☆ | PR 단위 테스트 자동화, 변경 경로 기반 조건부 빌드, Docker 이미지 자동 푸시 |
|
||
| Frontend | Blazor WASM, Vue 3 | ★★☆☆☆ | Blazor JS Interop 브릿지 설계, Vue 3 Composition API + Pinia 기반 지도 UI 구현 |
|
||
|
||
---
|
||
|
||
# 프로젝트
|
||
|
||
## 1. Cloud# (CloudSharp) — 셀프호스트 파일 서비스 플랫폼
|
||
|
||
> **Space 단위의 완전 격리형 저장 공간**을 제공하는 셀프호스트 파일 서비스로, tus 프로토콜 기반의 재개 가능한 대용량 업로드와 단명(短命) 다운로드 세션을 제공합니다.
|
||
|
||
### 프로젝트 개요
|
||
|
||
기존 클라우드 스토리지(Google Drive, Dropbox 등)는 개인 계정 중심으로 설계되어 팀/프로젝트 단위 협업에서 격리된 저장 공간, 대용량 중단 재개 업로드, 외부 공유 정책 분리 같은 요구를 충족하기 어려웠습니다. 이를 해결하기 위해 Space 단위 멀티 테넌트 모델 위에 tus 기반 업로드 파이프라인을 통합한 셀프호스트 파일 서비스를 설계하고 구현했습니다.
|
||
|
||
### 담당 역할
|
||
|
||
**1인 백엔드 + 인프라 + 설계 담당.** 프론트엔드를 제외한 전 영역.
|
||
|
||
- 백엔드 API: 인증·인가, Space 관리, 파일/폴더 CRUD, 업로드/다운로드 파이프라인, Quota, 공유 링크
|
||
- 데이터베이스 설계: 12개 엔티티 ERD, EF Core Configuration, Migration 자동화
|
||
- 인프라 구성: Docker Compose 5-서비스 오케스트레이션, nginx Reverse Proxy
|
||
- CI/CD: GitLab CI 파이프라인 (test → build → image push to GHCR)
|
||
- 설계 문서화: API/ERD/Conventions/ADR 등 살아있는 문서 체계 구축
|
||
|
||
### 아키텍처
|
||
|
||
**모듈러 모놀리스 + Clean Architecture.** MVP 단계에서 마이크로서비스의 운영 복잡도를 피하면서도, 코드 레벨에서 도메인 경계를 엄격히 분리해 추후 분리 가능성을 확보했습니다. 의존성 방향은 `Api → Core ← Infrastructure`로, 도메인이 HTTP나 DB, 파일시스템을 알지 못하게 했습니다.
|
||
|
||
```
|
||
Client → nginx :8080 (단일 외부 진입점)
|
||
├─► /api/* → ASP.NET Core API
|
||
└─► /files/* → tusd (Go 기반 청크 업로드)
|
||
↑ hook callback
|
||
API ──► PostgreSQL (메타데이터)
|
||
API ──► Redis (세션 + Pub/Sub)
|
||
API ──► Local FS (storage)
|
||
```
|
||
|
||
### 주요 기여
|
||
|
||
#### 1) Finalize 중복 실행 방지: CAS Lock
|
||
|
||
tusd hook 콜백과 Recovery Worker 두 경로에서 같은 UploadSession이 동시에 처리되어 FileItem이 중복 생성될 위험이 있었습니다. Finalize는 파일 이동(rename)이라는 느린 I/O를 포함하므로 일반 트랜잭션만으로는 부족했습니다.
|
||
|
||
분산 락(Redis Redlock 등) 도입 대신 단일 SQL UPDATE로 원자적 점유를 구현했습니다.
|
||
|
||
```sql
|
||
UPDATE upload_session
|
||
SET status = 'FINALIZING',
|
||
finalize_attempts = finalize_attempts + 1
|
||
WHERE id = :session_id
|
||
AND status = 'UPLOADING';
|
||
-- affected_rows = 1 → 점유 성공 / 0 → 이미 처리 중 (즉시 무시)
|
||
```
|
||
|
||
파일 I/O와 DB 트랜잭션을 분리하여 무거운 작업은 트랜잭션 밖에서 처리하고, DB 변경(FileItem INSERT, Quota 갱신)만 짧은 트랜잭션으로 감쌌습니다. Recovery Worker(5분 주기)가 10분 이상 `FINALIZING`에 머문 세션을 자동 보정하도록 구현하여 교착 상태도 자동 복구되도록 했습니다.
|
||
|
||
#### 2) Space Quota 경쟁 조건 방지
|
||
|
||
여러 멤버가 동시에 대용량 업로드를 시작할 때 quota 판정과 reserved 증가 사이의 race condition을 방지하기 위해 `SELECT ... FOR UPDATE` row-level lock을 적용했습니다. 업로드 세션 생성은 빈번하지 않고 락 지속시간이 짧아 적합했으며, Finalize 직전에도 quota를 재검사하여 이중 안전장치를 구성했습니다.
|
||
|
||
#### 3) Opaque Session Token 인증
|
||
|
||
한 사용자가 여러 Space에서 서로 다른 Role을 가지며 Role 변경이 즉시 반영되어야 했습니다. JWT는 토큰 만료 전까지 stale 권한이 유지되는 문제가 있어, 권한 정보를 토큰에 담지 않는 Opaque Session Token 방식을 선택했습니다.
|
||
|
||
```
|
||
Authorization: Bearer cs_sess_{base64url(CSPRNG 32bytes)}
|
||
```
|
||
|
||
Redis에 `HMAC-SHA-256(token, secret)` 해시만 저장하여 원문을 노출하지 않으며, 매 요청마다 DB에서 최신 SpaceMember를 조회합니다. UseCase 단계에서 리소스의 `space_id`를 재검증하여 IDOR 공격도 방어했습니다. 결과적으로 **Role 변경이 다음 API 요청부터 즉시 반영**되며, 강제 Revoke를 Redis key 삭제 한 번으로 처리할 수 있습니다.
|
||
|
||
#### 4) UploadSession ↔ FileReservation 1:1 분리
|
||
|
||
업로드 도메인을 두 엔티티로 분리하여 책임을 명확히 했습니다.
|
||
- **UploadSession**: 전송 상태 추적 (네트워크 관점, 7-state machine)
|
||
- **FileReservation**: 비즈니스 자원 선점 — quota·파일명 (도메인 관점, 6-state machine)
|
||
|
||
변경 주기와 실패 원인이 다른 두 관점을 분리하면서도 1:1로 연결하여, 네트워크 실패와 비즈니스 실패 경로를 독립적으로 복구할 수 있도록 했습니다.
|
||
|
||
#### 5) 파일명 충돌: 명시적 실패 반환
|
||
|
||
Google Drive처럼 자동 rename(`파일명(1).pdf`) 대신 `FILE_NAME_CONFLICT` 에러를 반환하도록 설계했습니다. 활성 FileItem과 활성 FileReservation을 함께 검사하고, DB UNIQUE 제약(`(space_id, folder_id, normalized_name) on active rows`)으로 이중 보장합니다. "모호함보다 명시적 실패가 낫다"는 설계 철학을 반영한 결정이었습니다.
|
||
|
||
### 인프라 구성
|
||
|
||
| 서비스 | 외부 노출 | 역할 |
|
||
|--------|-----------|------|
|
||
| postgres | ❌ (`expose`만) | 메타데이터 |
|
||
| redis | ❌ | 세션 + Pub/Sub |
|
||
| tusd | ❌ | tus 청크 업로드 |
|
||
| api | ❌ | ASP.NET 백엔드 |
|
||
| **nginx** | ✅ `:8080` | **유일한 외부 진입점** |
|
||
|
||
- **Storage Sharding**: hex hash 기반 `256 × 256 × 256 = 65,536` 버킷 분산으로 디렉토리 과밀 방지
|
||
- **Healthcheck + depends_on**: `condition: service_healthy`로 단순 실행 순서가 아닌 실제 준비 완료를 기다림
|
||
- **nginx tus 특수 설정**: `proxy_buffering off`, `client_max_body_size 0`, `proxy_read_timeout 600s`로 대용량 스트리밍 지원
|
||
|
||
### 사용 기술
|
||
|
||
| 기술 | 선택 이유 |
|
||
|------|-----------|
|
||
| ASP.NET Core 10 / Minimal API | 빠른 부트스트랩, Controller 오버헤드 없음, 최신 런타임 |
|
||
| PostgreSQL 16 | FK·Unique·CHECK 제약이 풍부, JSON 확장성 |
|
||
| tusd (Go) | tus 표준 구현체, ASP.NET보다 청크 업로드에 효율적 |
|
||
| FluentResults | 예외 아닌 비즈니스 실패의 명시적 표현, Bind 파이프라인 |
|
||
| FluentValidation | `.WithErrorCode()`로 ErrorCode 표준화 |
|
||
|
||
### 프로젝트 성과
|
||
|
||
- 12개 엔티티 ERD, 11개 EF Core Configuration 클래스
|
||
- 48개 기능 요구사항(SFR-001~048)을 OpenAPI + UseCase에 매핑
|
||
- UploadSession 7-state, FileReservation 6-state 상태 머신 설계
|
||
- 14개 컨벤션 문서, 3건의 ADR 작성
|
||
- PR 테스트 + master push 시 GHCR 이미지 빌드 자동화
|
||
|
||
### 회고
|
||
|
||
- **잘한 점**: 초기부터 엄격한 문서화와 Clean Architecture 적용으로 기능 확장 시 도메인 경계가 무너지지 않았습니다. JWT 대신 Opaque Session Token 선택이 멀티 Role 모델에 정확히 부합했습니다. UploadSession과 FileReservation의 1:1 분리로 실패 복구 경로가 단순해졌습니다.
|
||
- **아쉬운 점**: API IntegrationTests와 Architecture Tests가 CI 파이프라인에 미포함된 점, 운영 모니터링(OpenTelemetry, Grafana)이 설계 단계에 머문 점이 아쉽습니다.
|
||
- **향후 계획**: MinIO/S3 Storage Provider 구현, Worker에 ffmpeg 썸네일 파이프라인 적용, IntegrationTest CI 포함 및 자동 배포 파이프라인 완성.
|
||
|
||
---
|
||
|
||
## 2. Didit — GitHub 통합 팀 협업 플랫폼
|
||
|
||
### 프로젝트 개요
|
||
|
||
GitHub 저장소를 사용하는 개발 팀은 이슈 우선순위를 수동으로 판단하고, 화상회의·채팅·이슈 트래킹이 각각 다른 도구에 흩어져 있어 워크플로우 단절이 반복적으로 발생했습니다. 이를 해결하기 위해 **GitHub OAuth2 기반 인증, OpenVidu WebRTC 화상회의, AI 이슈 분석, SSE 실시간 이벤트**를 하나의 Spring Boot 서버로 통합한 팀 협업 플랫폼을 설계하고 구현했습니다.
|
||
|
||
### 담당 역할
|
||
|
||
백엔드 개발자로 참여하여 다음을 담당했습니다.
|
||
|
||
- Spring Boot 4.0 기반 REST API 전체 설계 및 구현 (30개+ 엔드포인트)
|
||
- Redis 기반 AI 비동기 작업 큐 및 Pub/Sub 메시징 아키텍처 설계
|
||
- SSE(Server-Sent Events) 실시간 이벤트 시스템 구현
|
||
- Flyway 기반 데이터베이스 스키마 설계 및 마이그레이션 관리
|
||
- Docker Compose 인프라 구성 및 GitLab CI/CD 파이프라인 구축
|
||
|
||
### 주요 기여
|
||
|
||
#### 1) ML 추론 부하를 API 응답성으로부터 완전히 분리
|
||
|
||
HuggingFace 모델 추론은 수 초~수십 초가 소요되어 REST API 요청-응답 사이클 안에서 처리할 수 없었습니다. Redis List를 작업 큐로, Redis Pub/Sub을 결과 전달 채널로 활용하는 비동기 파이프라인을 설계했습니다.
|
||
|
||
```
|
||
클라이언트 → POST /issue/analyze → Redis leftPush → 즉시 200 응답
|
||
↓
|
||
AI Worker polling
|
||
↓
|
||
Redis Pub/Sub 결과 발행
|
||
↓
|
||
RedisMessageListener 수신 → DB 저장 → SSE 전송
|
||
```
|
||
|
||
RabbitMQ나 Kafka 같은 별도 인프라 없이 기존 Redis 하나로 큐와 메시징을 통합하여 운영 복잡도를 낮췄습니다.
|
||
|
||
#### 2) AI 결과를 요청한 특정 사용자에게만 실시간 전달
|
||
|
||
같은 프로젝트 모든 구독자에게 브로드캐스트하면 다른 사용자에게 불필요한 이벤트가 전파되는 문제가 있었습니다. Redis에 `sse:client_key:{userId}` 형태로 clientKey를 저장하고, AI 결과 수신 시 `SseHub.broadcastToClient(projectId, clientKey, ...)`를 호출하여 요청한 사용자에게만 결과를 전달하도록 구현했습니다. 동일 유저의 다중 탭/디바이스를 clientKey로 구분하면서도 무상태 서버 구조를 유지하여 수평 확장 시에도 동일하게 동작합니다.
|
||
|
||
#### 3) GitHub 이슈와 로컬 DB의 Upsert 기반 양방향 동기화
|
||
|
||
GitHub이 이슈의 Source of Truth이지만 AI 우선순위·담당자 매핑 등 로컬 메타데이터를 함께 관리해야 했습니다. `github_issue_id`를 natural key로 사용하는 Upsert 패턴을 구현하여, 기존 이슈는 title/body/status를 업데이트하고 신규 이슈는 INSERT하도록 처리했습니다. 동기화 완료 후 모든 이슈에 대해 AI 단일 분석 요청과 배치 정렬 큐를 자동으로 추가하여, GitHub에서 이슈를 가져오는 것만으로 AI 분석 파이프라인이 연계되도록 설계했습니다.
|
||
|
||
#### 4) UK 제약을 유지하면서 탈퇴 사용자 재참여 처리
|
||
|
||
`project_users` 테이블의 `UNIQUE(project_id, user_id)` 제약 때문에 탈퇴 사용자가 재참여할 때 UK 위반이 발생했습니다. DB 제약을 제거하는 대신 기존 레코드의 상태를 `LEFT → ACTIVE`로 복원하는 방식을 택했습니다. 이를 통해 UK 제약을 유지하면서도 사용자 참여 이력을 보존하여 감사 추적이 가능하도록 했습니다.
|
||
|
||
#### 5) Flyway + JPA validate로 스키마 정합성 이중 보장
|
||
|
||
`V1__`부터 `V12__`까지 12개의 마이그레이션 파일로 스키마 변경을 관리하고, JPA `ddl-auto: validate`를 함께 적용하여 Entity-DB 스키마 불일치 시 즉시 오류가 발생하도록 했습니다. `clean-disabled: true`로 운영 데이터 실수 삭제도 방지했습니다.
|
||
|
||
#### 6) Result 패턴으로 예외 흐름을 값으로 통일
|
||
|
||
서비스 계층에서 예외를 던지는 대신 자체 구현한 `Result<T>` 클래스로 성공/실패를 값으로 반환하도록 설계했습니다. `NotFoundError(404)`, `ForbiddenError(403)`, `ConflictError(409)`, `GoneError(410)`, `ServerError(500)` 등 의미 있는 에러 타입 계층을 정의하고, Controller에서 `result.isFailure()` 체크로 일관되게 변환했습니다.
|
||
|
||
### 사용 기술
|
||
|
||
| 기술 | 선택 이유 |
|
||
| --- | --- |
|
||
| Java 21 + Spring Boot 4.0 | 최신 LTS, DI·Security·Validation이 잘 통합된 환경 |
|
||
| MySQL 8.0 | 사용자·프로젝트·이슈 관계를 FK·UK 제약으로 정합성 보장 |
|
||
| Redis 7.2 | 작업 큐(List)와 메시징(Pub/Sub)을 하나의 인프라로 통합 |
|
||
| GitHub OAuth2 | 별도 회원가입 없이 repo·org 권한 자연스럽게 연계 |
|
||
| SSE | WebSocket보다 가벼운 단방향 실시간 통신, 브라우저 네이티브 지원 |
|
||
| Flyway | DB 스키마 변경 버전 관리 |
|
||
| OpenVidu 2.32.1 | WebRTC SFU 직접 구현 없이 화상회의·녹화 통합 |
|
||
|
||
### 프로젝트 성과
|
||
|
||
- 30개+ REST API 엔드포인트, 8개 도메인 (인증/프로젝트/초대/회의/채팅/이슈/SSE/요약)
|
||
- 14개 주요 테이블 설계, GitHub Token 분리 보관
|
||
- MySQL·Redis·OpenVidu·Server·Client 5-서비스 Docker Compose 오케스트레이션
|
||
- GitLab CI + Portainer Webhook 기반 자동 배포 파이프라인 구축
|
||
|
||
### 회고
|
||
|
||
- **잘한 점**: Kafka·RabbitMQ를 추가하지 않고 Redis로 큐와 Pub/Sub을 통합한 실용적 선택. 인프라 서비스를 늘리지 않고 운영 복잡도를 낮췄습니다.
|
||
- **아쉬운 점**: APM 도구가 구성되어 있지 않아 성능 병목을 수치로 확인하기 어려웠고, N+1 문제 가능성도 코드 분석으로만 파악한 상태입니다. Spring Boot Actuator + Prometheus + Grafana 도입과 `@EntityGraph`/JOIN FETCH 적용이 필요합니다.
|
||
|
||
---
|
||
|
||
## 3. 술통여지도 (Sulmap) — AI 기반 술집 추천 플랫폼
|
||
|
||
### 프로젝트 개요
|
||
|
||
한국의 다회차 음주 문화(1차·2차·3차)에서 다음 장소 선정의 어려움을 해결하기 위해, GPT-5.2 기반 개인화 추천 엔진을 갖춘 지도형 풀스택 웹 애플리케이션을 구현했습니다. 사용자의 위치, 날씨, 시간대, 성별, 나이, 그룹 성격 등 컨텍스트 데이터를 종합하여 200개 후보 중 최적의 술집 Top 10을 자연어 이유와 함께 제공합니다.
|
||
|
||
### 담당 역할
|
||
|
||
- 백엔드: Spring Boot 3계층 Clean Architecture 설계, MyBatis 기반 DB 모델링, GPT API 연동 파이프라인 구현
|
||
- 데이터 파이프라인: .NET 9 기반 Qdrant + OpenAI Embedding 활용 공공데이터 ETL 자동화
|
||
|
||
### 주요 기여
|
||
|
||
#### 1) 2단계 Cascade Ranking 추천 엔진 설계
|
||
|
||
200개 후보 전체를 GPT에 한 번에 전달하면 토큰 비용이 폭증하고 attention 품질이 저하되는 문제가 있었습니다. 이를 해결하기 위해 2-Stage Cascade Architecture를 도입했습니다.
|
||
|
||
- **Stage 1 (GptMinorRecommendClient)**: 200개를 100개씩 배치로 나누어 각 배치에서 top5 선별 (간단한 출력: ID만)
|
||
- **Stage 2 (GptRecommendClient)**: 선별된 후보 + 보충 최대 40개를 정밀 랭킹 → top10 + reasons 생성
|
||
|
||
이 구조로 토큰 비용 약 70% 절감을 달성했으며, Google의 "Re-ranking with LLMs" 패턴을 참고했습니다.
|
||
|
||
#### 2) Defensive Normalization으로 GPT Hallucination 차단
|
||
|
||
GPT-5.2가 가끔 B 라인에 없는 barId를 생성하거나 학습 데이터의 술집을 추천하는 hallucination이 발생했습니다. 시스템 프롬프트만으로는 100% 방어할 수 없어 코드 레벨 검증을 병행했습니다.
|
||
|
||
1. **시스템 프롬프트**: "후보에 없는 술집을 만들거나 추측하지 마라" + "신규 barId 생성 금지" 명시
|
||
2. **허용 ID Set 검증**: 정규식 `(?m)^B\|id=(\d+)\b`로 입력 ID Set 구성 → 허용되지 않은 ID 제거
|
||
3. **중복 제거**: `LinkedHashMap`으로 순서 유지하며 중복 제거
|
||
4. **Reason 정제**: 개행/파이프 제거, 45자 truncate, 부족 시 폴백
|
||
|
||
결과적으로 잘못된 barId가 최종 결과에 포함되는 사고를 방지했습니다.
|
||
|
||
#### 3) Pipe-delimited Domain Format으로 토큰 40% 절감
|
||
|
||
JSON으로 후보 목록을 전달하면 토큰 비용이 크고, 술집 이름의 특수문자/개행이 파싱 오류를 유발했습니다. 도메인 특화 미니 포맷을 설계하여 이를 해결했습니다.
|
||
|
||
```
|
||
CTX|g=M|a=30|ts=2025-12-23T19:00+09:00|w=clear|md=2000|q=조용한 분위기의 이자카야
|
||
B|id=123|c=주점|oi=매일 18:00-02:00|n=포차|menu=닭발,오돌뼈
|
||
B|id=456|c=일식|oi=월-토 17:00-24:00|n=사케바|menu=사시미,사케
|
||
```
|
||
|
||
모든 필드를 `sanitize()`로 처리(파이프 → 공백, 개행 → 공백)하고 필드별 길이 truncate(이름 20자, 메뉴 60자, 사용자 프롬프트 180자 등)를 적용해 안정성을 확보했습니다. JSON 대비 약 40% 토큰 절감 효과를 얻었습니다.
|
||
|
||
#### 4) Graceful Degradation — AI 실패 시 폴백
|
||
|
||
GPT API 호출이 네트워크 이슈, 타임아웃, Rate Limit 등으로 실패할 경우를 대비해 Repository에서 `try-catch(Exception e)`로 감싸 실패 시 입력 순서(거리순)로 topK를 반환하도록 처리했습니다. Reason에 `"fallback:ai_fail"` 태그를 포함시켜 클라이언트가 구분할 수 있도록 했습니다. AI 장애 시에도 빈 화면이 아닌 기본 추천이 표시되어 서비스 사용성을 유지했습니다.
|
||
|
||
#### 5) Result<T> Monad 기반 전역 에러 처리
|
||
|
||
모든 서비스 계층에서 성공/실패를 타입으로 표현하여 예외 처리 누락을 방지했습니다. `NotFoundError`, `ConflictError`, `ValidationError`, `ServerError`, `SimpleError`가 각각 HTTP Status를 가지며, Controller에서 `result.isFailure()` 체크로 적절한 HTTP 상태로 응답합니다.
|
||
|
||
### 사용 기술
|
||
|
||
| 기술 | 선택 이유 |
|
||
|---|---|
|
||
| GPT-5.2 + Structured Output | JSON Schema 기반 출력으로 파싱 오류 방지 |
|
||
| Spring Boot 3.5.9 + MyBatis | Java 안정성 + SQL 직접 제어로 복잡한 도메인 쿼리 |
|
||
| Elasticsearch 8.15 | 전문검색 + 위치기반 검색, MySQL 보완 |
|
||
| Qdrant + text-embedding-3-small | 데이터 파이프라인에서 공공데이터 ↔ 술집 벡터 유사도 매칭 |
|
||
|
||
### 프로젝트 성과
|
||
|
||
- 평균 200개 후보에서 10개의 컨텍스트 인지적 추천 결과 생성
|
||
- GPT API 호출당 토큰 약 70% 절감 (Cascade Ranking + 40% Pipe Format)
|
||
- Defensive Normalization으로 잘못된 barId 노출 0건 달성
|
||
- 16개 테이블, 11개 REST API endpoint 구현
|
||
- 12개 백엔드 유닛 테스트 + Vitest/Playwright 프론트엔드 테스트
|
||
|
||
### 회고
|
||
|
||
- **구조적 안전장치의 중요성**: GPT Structured Output만으로 hallucination을 100% 방지할 수 없으며, 방어적 후처리가 필수임을 배웠습니다.
|
||
- **도메인 특화 포맷의 가치**: 범용 JSON보다 커스텀 pipe-delimited 포맷이 토큰 효율과 파싱 안정성 모두에서 우수했습니다.
|
||
- **개선이 필요한 지점**: AI 응답 회귀 테스트, 비용 모니터링, Rate limit 대응이 미구현 상태로 향후 과제입니다.
|
||
|
||
---
|
||
|
||
## 4. TusBlazorClient — Blazor WASM용 tus 프로토콜 클라이언트 라이브러리
|
||
|
||
### 프로젝트 개요
|
||
|
||
Blazor WebAssembly에서 순수 C# 코드로 대용량 파일을 전송할 경우, 브라우저 메모리 제약과 느린 I/O로 인해 전송 실패 또는 멈춤 현상이 발생하고, 네트워크 중단 시 처음부터 다시 업로드해야 하는 문제가 있었습니다. JavaScript의 `tus-js-client`를 C# API로 감싸, Blazor 개발자가 JavaScript를 직접 다루지 않고도 재개 가능한 대용량 파일 업로드를 사용할 수 있도록 설계하고 구현했습니다.
|
||
|
||
### 담당 역할
|
||
|
||
- 라이브러리 전체 설계 및 구현 (1인 개발)
|
||
- Public API 설계, JS Interop 브릿지 구현, DI 통합 구성, NuGet 배포
|
||
|
||
### 주요 기여
|
||
|
||
#### 1) C# 네이티브 API로 tus 프로토콜 추상화
|
||
|
||
`IJSRuntime` 호출을 직접 작성해야 하는 복잡성을 감추고 타입 세이프 API를 제공하기 위해 `TusClient` → `TusUpload` → `TusOptions` 구조로 계층을 나눠 설계했습니다.
|
||
|
||
```csharp
|
||
// Program.cs
|
||
builder.Services.AddTusBlazorClient();
|
||
|
||
// Component
|
||
var file = (await TusClient.GetFileInputElement(_fileElement).GetFiles()).First();
|
||
var upload = await TusClient.Upload(file, options);
|
||
await upload.Start();
|
||
```
|
||
|
||
DI 등록 한 줄과 직관적인 C# 코드만으로 업로드를 처리할 수 있습니다.
|
||
|
||
#### 2) JS → .NET 콜백 브릿지 설계
|
||
|
||
`OnProgress`, `OnError`, `OnSuccess` 등 tus의 이벤트 콜백을 C# 델리게이트로 받아야 했습니다. `TusOptionJsInvoke` 클래스에 `[JSInvokable]` 메서드를 정의하고 `DotNetObjectReference`로 JS에 전달하여, JS 이벤트 발생 시 .NET 델리게이트가 정확히 호출되도록 중계 구조를 구현했습니다. `TusOptionNullCheck`를 도입하여 사용자가 등록하지 않은 콜백에 대해 JS 측에서 불필요한 interop 호출이 발생하지 않도록 최적화했습니다.
|
||
|
||
#### 3) TusUpload 생성자를 internal로 제한하여 안전한 인스턴스 생성 강제
|
||
|
||
`TusUpload`가 외부에서 직접 생성될 경우 `DotNetObjectReference` 연결이 누락되어 콜백이 동작하지 않는 문제가 있었습니다. 생성자를 **internal**로 제한하고 반드시 `TusClient.Upload()`를 통해서만 인스턴스를 얻도록 강제하여, JS 콜백 브릿지가 항상 올바르게 연결되도록 보장했습니다.
|
||
|
||
#### 4) JS 모듈 Lazy 초기화로 불필요한 로드 방지
|
||
|
||
`TusJsInterop`에서 JS ES 모듈을 Lazy 초기화 방식으로 관리하여, 실제로 업로드가 필요한 시점에만 JS 모듈을 로드하도록 처리했습니다. `TusClient`는 Singleton으로 등록되어 모듈을 한 번만 로드하고 이후 모든 업로드가 공유합니다.
|
||
|
||
### 사용 기술
|
||
|
||
| 기술 | 선택 이유 |
|
||
|------|-----------|
|
||
| tus-js-client | tus 프로토콜의 검증된 JS 구현체로, 재개 가능한 업로드 로직을 직접 구현하지 않고 안정적으로 활용 |
|
||
| IJSRuntime / JS Interop | Blazor WASM에서 JS 라이브러리를 C#으로 연결하는 공식 메커니즘 |
|
||
|
||
### 회고
|
||
|
||
- **인터페이스 미분리에 대한 판단**: 라이브러리 규모가 작고 Selenium E2E 테스트로 커버하고 있어 인터페이스 분리의 실질적 이득이 크지 않다고 판단했습니다. 외부 사용자가 테스트 대역을 구성해야 하는 상황이 생기면 인터페이스 추출이 필요할 것입니다.
|
||
- **명령형 API 채택**: Fluent API 대신 명령형 API를 채택했는데, `FindPreviousUpload()`, `ResumeFromPreviousUpload()`, `Abort()` 등 업로드 생명주기 중간 단계에 개입해야 하는 시나리오에서 더 직관적이라고 판단했습니다.
|
||
- **콜백 JSON 직렬화 제외**: `TusOptions`의 콜백 프로퍼티는 `[JsonIgnore]`로 마킹하여 직렬화 시 오류를 방지했습니다.
|
||
|
||
---
|
||
|
||
## 종합 회고
|
||
|
||
네 프로젝트를 관통하는 공통된 기술적 관점은 다음과 같습니다.
|
||
|
||
1. **계층 분리와 의존성 역전**: Cloud#의 Clean Architecture, 술통여지도의 3계층 구조, Didit의 Service-Repository 분리 모두 도메인이 인프라를 알지 못하도록 설계했습니다.
|
||
|
||
2. **`Result<T>` Monad 기반 에러 처리**: 예외를 던지는 대신 성공/실패를 값으로 표현하여 에러 흐름을 명시적으로 관리했습니다. 이는 네 프로젝트 중 세 곳(Cloud#, 술통여지도, Didit)에서 일관되게 적용한 패턴입니다.
|
||
|
||
3. **방어적 설계와 자동 복구**: CAS Lock + Recovery Worker, Defensive Normalization, Graceful Degradation 등 외부 의존성(파일 I/O, AI API)이 실패해도 서비스가 안전하게 동작하도록 다층 방어 체계를 구축했습니다.
|
||
|
||
4. **인프라까지 고려한 백엔드**: Docker Compose 멀티 서비스 오케스트레이션, nginx 헤더 포워딩, GitLab CI/CD 자동화 등 코드 외 영역까지 책임을 가지고 설계했습니다.
|
||
|
||
앞으로는 운영 모니터링(OpenTelemetry, Prometheus, Grafana)과 통합 테스트 자동화 영역을 더 깊이 학습하여, 설계뿐 아니라 운영 단계에서도 안정성을 책임지는 백엔드 개발자로 성장하고자 합니다. |