vault backup: 2026-05-05 21:27:37

This commit is contained in:
son
2026-05-05 21:27:37 +09:00
parent dc1b910d36
commit 957d24aa9f
39 changed files with 42394 additions and 1 deletions

View File

@@ -0,0 +1,191 @@
# Didit
## 1. 프로젝트 개요
GitHub 저장소를 사용하는 개발 팀은 이슈 우선순위를 수동으로 판단하고, 화상회의 · 채팅 · 이슈 트래킹이 각각 다른 도구에 흩어져 있어 워크플로우 단절이 반복적으로 발생합니다. 저는 이 문제를 해결하기 위해 **GitHub OAuth2 기반 인증, OpenVidu WebRTC 화상회의, AI 이슈 분석, SSE 실시간 이벤트**를 하나의 Spring Boot 서버로 통합한 팀 협업 플랫폼을 설계하고 구현했습니다.
---
## 2. 담당 역할
백엔드 개발자로 참여하여 다음을 담당했습니다.
- Spring Boot 4.0 기반 REST API 전체 설계 및 구현 (30개+ 엔드포인트)
- Redis 기반 AI 비동기 작업 큐 및 Pub/Sub 메시징 아키텍처 설계
- SSE(Server-Sent Events) 실시간 이벤트 시스템 구현
- Flyway 기반 데이터베이스 스키마 설계 및 마이그레이션 관리
- Docker Compose 인프라 구성 및 GitLab CI/CD 파이프라인 구축
---
## 3. 주요 기여
### 3.1 ML 추론 부하를 API 응답성으로부터 완전히 분리
HuggingFace 모델 추론은 수 초~수십 초가 소요되어 REST API의 요청-응답 사이클 안에서 처리할 수 없었습니다. 저는 이 문제를 해결하기 위해 Redis List를 작업 큐로, Redis Pub/Sub을 결과 전달 채널로 활용하는 비동기 파이프라인을 설계했습니다.
서버는 AI 분석 요청이 들어오면 `queue:issue:priority:single`에 작업을 `leftPush`한 뒤 즉시 응답합니다. Python AI Worker가 0.5초 간격으로 큐를 polling하여 작업을 소비하고, 완료 시 Redis Pub/Sub으로 결과를 발행합니다. 서버의 `RedisMessageListener`가 결과를 수신하여 DB를 업데이트하고 SSE로 클라이언트에 전달합니다.
```
클라이언트 → POST /issue/analyze → Redis leftPush → 즉시 200 응답
AI Worker polling
Redis Pub/Sub 결과 발행
RedisMessageListener 수신 → DB 저장 → SSE 전송
```
이 구조 덕분에 ML 추론 시간과 API 응답성이 완전히 분리되었으며, RabbitMQ나 Kafka 같은 별도 인프라 없이 기존 Redis 하나로 큐와 메시징을 통합하여 운영 복잡도를 낮췄습니다.
---
### 3.2 AI 결과를 요청한 특정 사용자에게만 실시간 전달
AI 분석 결과를 SSE로 전달할 때, 같은 프로젝트의 모든 구독자에게 브로드캐스트하면 다른 사용자에게 불필요한 이벤트가 전파되는 문제가 있었습니다. 저는 Redis에 `sse:client_key:{userId}` 형태로 clientKey를 저장하고, AI 결과 수신 시 `SseHub.broadcastToClient(projectId, clientKey, ...)` 를 호출하여 요청한 사용자에게만 결과를 전달하도록 구현했습니다.
이 방식은 동일 유저의 다중 탭/디바이스를 clientKey로 구분하면서도, Redis를 통해 무상태(stateless) 서버 구조를 유지할 수 있어 수평 확장 시에도 동일하게 동작합니다.
---
### 3.3 GitHub 이슈와 로컬 DB의 Upsert 기반 양방향 동기화
GitHub이 이슈의 Source of Truth이지만, AI 우선순위·담당자 매핑 등 로컬 메타데이터를 함께 관리해야 했습니다. 저는 `github_issue_id`를 natural key로 사용하는 Upsert 패턴을 구현하여, 기존 이슈는 title/body/status를 업데이트하고 신규 이슈는 INSERT하도록 처리했습니다.
동기화 완료 후 모든 이슈에 대해 AI 단일 분석 요청과 배치 정렬 큐를 자동으로 추가하여, GitHub에서 이슈를 가져오는 것만으로 AI 분석 파이프라인이 연계되도록 설계했습니다.
---
### 3.4 UK 제약을 유지하면서 탈퇴 사용자 재참여 처리
`project_users` 테이블에 `UNIQUE(project_id, user_id)` 제약이 있어, 탈퇴했던 사용자가 초대 링크로 재참여할 때 새 레코드를 INSERT하면 UK 위반이 발생했습니다. 저는 DB 레벨 제약을 제거하는 대신, 기존 레코드의 상태를 `LEFT → ACTIVE`로 복원하는 방식을 택했습니다.
`AddProjectUser` 로직에서 기존 레코드를 먼저 조회하고, `ACTIVE` 상태면 `ConflictError`, `LEFT` 상태면 role을 `MEMBER`로 초기화하고 `leftAt`을 null로 복원합니다. 이 방식은 UK 제약을 유지하면서도 사용자 참여 이력을 보존하여 감사 추적이 가능합니다.
---
### 3.5 Flyway + JPA validate로 스키마 정합성을 이중 보장
저는 DB 스키마 변경 이력을 코드로 추적하기 위해 Flyway를 도입하고, `V1__`부터 `V12__`까지 총 12개의 마이그레이션 파일로 스키마 변경을 관리했습니다. JPA의 `ddl-auto: validate` 설정을 함께 적용하여, 애플리케이션 시작 시 Entity와 실제 DB 스키마가 불일치하면 즉시 오류가 발생하도록 처리했습니다.
운영 환경에서 실수로 데이터가 전부 삭제되는 것을 방지하기 위해 `clean-disabled: true`도 명시적으로 설정했습니다.
---
### 3.6 Result 패턴으로 예외 흐름을 값으로 통일
저는 서비스 계층에서 예외를 던지는 대신, 자체 구현한 `Result<T>` 클래스로 성공/실패를 값으로 반환하도록 설계했습니다. `NotFoundError(404)`, `ForbiddenError(403)`, `ConflictError(409)`, `GoneError(410)`, `ServerError(500)` 등 의미 있는 에러 타입 계층을 정의하고, Controller에서 `result.isFailure()`를 체크하여 `ErrorResponse`로 일관되게 변환합니다.
---
## 4. 사용 기술 및 선택 이유
| 기술 | 선택 이유 |
| --- | --- |
| **Java 21 + Spring Boot 4.0** | 최신 LTS 버전의 강력한 생태계를 활용하고, DI·Security·Validation이 잘 통합된 환경에서 API 서버를 구조화하기 위해 선택했습니다. |
| **MySQL 8.0** | 사용자·프로젝트·이슈·회의처럼 관계가 명확한 데이터를 FK·UK 제약으로 DB 레벨에서 정합성을 보장하기 위해 선택했습니다. |
| **Redis 7.2** | 단순 캐시가 아닌 AI 작업 큐(List)와 결과 메시징(Pub/Sub)을 하나의 인프라로 처리하기 위해 선택했습니다. Kafka·RabbitMQ 없이 운영 복잡도를 낮출 수 있었습니다. |
| **GitHub OAuth2** | 개발자 타겟 서비스에서 별도 회원가입 없이 GitHub 계정으로 인증하고, repo·org 권한을 자연스럽게 연계하기 위해 선택했습니다. |
| **SSE** | WebSocket보다 가벼운 단방향 실시간 통신이 필요했고, 클라이언트가 별도 라이브러리 없이 브라우저 네이티브 API로 연결할 수 있어 선택했습니다. |
| **Flyway** | 모든 DB 스키마 변경을 버전 관리하고, 팀원이 동일한 DB 상태에서 개발할 수 있도록 선언적 마이그레이션 도구로 선택했습니다. |
| **OpenVidu 2.32.1** | WebRTC SFU 미디어 서버를 직접 구현하지 않고, 검증된 오픈소스를 활용하여 화상회의·녹화 기능을 빠르게 통합하기 위해 선택했습니다. |
| **Docker Compose** | MySQL·Redis·OpenVidu·Server·Client 5개 서비스를 로컬과 배포 환경에서 동일하게 실행하기 위해 선택했습니다. |
| **GitLab CI/CD + Portainer** | 코드 병합 시 Docker 이미지를 자동 빌드하고 Portainer Webhook으로 배포를 트리거하여 운영 자동화를 구성했습니다. |
---
## 5. 구현 사항
### 전체 아키텍처
```
외부 요청 (HTTPS)
Traefik (Reverse Proxy, HTTPS 종단)
├─► didit-client (React SPA)
└─► didit-server (Spring Boot :8080)
├─► MySQL 8.0 (internal 네트워크)
├─► Redis 7.2 (작업 큐 + Pub/Sub)
├─► OpenVidu (WebRTC SFU, internal 네트워크)
└─► SSE Emitter (클라이언트 실시간 이벤트)
AI Worker (Python FastAPI)
└─► Redis polling (큐 소비 + 결과 발행)
```
### 주요 도메인별 API
| 도메인 | 엔드포인트 수 | 주요 기능 |
| --- | --- | --- |
| 인증 | 3 | GitHub OAuth2 로그인/로그아웃, 현재 사용자 조회 |
| 프로젝트 | 12 | CRUD, 참여자 관리, 소유권 이전, GitHub 레포 연동 |
| 초대 | 3 | UUID 초대 코드 발급, 조회, 수락 |
| 회의/채널 | 12 | 회의 생성·예약·수정·삭제, WebRTC 연결, 녹화 관리 |
| 채팅 | 4 | 메시지 전송·조회·수정·삭제 (Soft Delete) |
| 이슈 | 7 | GitHub 이슈 동기화, AI 분석, CRUD |
| SSE | 2 | 채널/프로젝트 단위 실시간 이벤트 스트리밍 |
| 회의 요약 | 3 | AI 요약 조회·수정·삭제 |
### 데이터베이스 스키마 (주요 테이블 14개)
```
users ──────────────── user_github_auth (1:1, GitHub Token 별도 분리)
├─ projects ──────── project_users (N:M, status: ACTIVE/LEFT)
│ │ project_invites (UUID 초대 링크)
│ │ project_recents (최근 조회 4개)
│ │
│ ├─ meetings ─ meeting_users (참여자)
│ │ meeting_records (OpenVidu 녹화)
│ │ meeting_summary (AI 요약, version 관리)
│ │
│ └─ issues ─── issue_assignees (담당자 N:M)
└─ chats (project + meeting 복합 참조, Soft Delete)
```
### 인증/인가 흐름
```
1. GET /api/v1/auth/login → GitHub OAuth2 리다이렉트
2. GitHub 인증 완료 → CustomOAuth2UserService.joinOrUpdate() → DB 저장/갱신
3. 세션 Cookie(JSESSIONID, HttpOnly+Secure+SameSite=None) 발급
4. 이후 요청: @AuthenticationPrincipal CustomOAuth2User → userId + accessToken 추출
5. Service 계층: FindProjectUser(userId, projectId) → 멤버십 검증
6. OWNER 전용 작업: project.getOwner().getId().equals(userId) 검증
```
### 인프라 네트워크 구성
| 서비스 | internal 네트워크 | caddy_default (외부) | 포트 노출 |
| --- | --- | --- | --- |
| MySQL | ✅ | ❌ | 비공개 |
| Redis | ✅ | ❌ | 6379 (개선 필요) |
| OpenVidu | ✅ | ❌ | 비공개 |
| Server | ✅ | ✅ | 8080 (Proxy 경유) |
| Client | ✅ | ✅ | Proxy 경유 |
### CI/CD 파이프라인
```
MR 생성
└─► test_server: ./gradlew test → JUnit Report artifact
Master Push
├─► build_push_server: Docker multi-stage build → ghcr.io push
└─► deploy_portainer: Portainer Webhook → 컨테이너 재배포
```
---
## 6. 기술적 의사결정 및 회고
### Redis를 작업 큐와 메시지 브로커로 동시에 활용한 판단
ML 추론 비동기화를 위해 메시지 큐 도입이 필요했습니다. Kafka나 RabbitMQ를 추가하는 대신, 이미 인프라에 포함된 Redis의 List(작업 큐)와 Pub/Sub(결과 전달)을 조합하여 동일한 목적을 달성했습니다. 인프라 서비스를 늘리지 않고 운영 복잡도를 낮춘 실용적인 선택이었습니다. 다만 Redis가 단일 장애점이 될 수 있으므로, 트래픽이 증가하면 전용 메시지 큐로 분리하는 것을 고려해야 합니다.
### 모니터링 부재에 대한 인식
API 응답 시간 측정이나 APM 도구가 구성되어 있지 않아 성능 병목을 수치로 확인하기 어렵습니다. Lazy Loading 전략으로 인한 N+1 문제 가능성도 코드 분석으로만 파악한 상태입니다. 운영 레벨에서는 Spring Boot Actuator + Prometheus + Grafana 조합으로 요청/응답 지표를 수집하고, 주요 쿼리에 `@EntityGraph` 또는 JOIN FETCH를 적용하는 것이 필요합니다.