Files
irukseo/포트폴리오/projects/Didit.md
2026-05-05 21:27:37 +09:00

12 KiB

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를 적용하는 것이 필요합니다.