45 KiB
Didit
본 문서는 Write-docs.md 템플릿에 따라 실제 코드에서 확인된 내용만을 바탕으로 작성되었다. 확인되지 않는 내용은 "확인 필요"로 표기한다.
1. 프로젝트 목적
해결하려는 문제
- 분산된 팀이 GitHub 이슈 기반으로 협업할 때, 이슈 우선순위 판단이 수동적이고 회의 내용이 체계적으로 기록/공유되지 않는 문제
- 실시간 화상회의 + 채팅 + GitHub 이슈 관리가 각각 분리되어 워크플로우 단절이 발생하는 문제
주요 사용자
- GitHub 저장소를 사용하는 개발 팀
- 실시간 화상회의로 소통하면서 이슈 트래킹이 필요한 애자일/스크럼 팀
프로젝트 목표
- 하나의 플랫폼에서 화상회의 + 채팅 + GitHub 이슈 관리 + AI 기반 요약/우선순위 분석을 통합 제공
백엔드가 담당하는 핵심 책임
- RESTful API 제공 (프로젝트 CRUD, 이슈 관리, 회의 관리, 채팅)
- GitHub OAuth2 인증/인가 처리
- OpenVidu WebRTC 미디어 서버 연동 (세션/커넥션/녹화 관리)
- Redis 기반 비동기 작업 큐잉 (AI 분석 요청 → 응답 수신)
- SSE(Server-Sent Events) 기반 실시간 이벤트 스트리밍
- Flyway 기반 DB 마이그레이션 관리
인프라 구성이 필요한 이유
- 5개 이상의 서비스(서버, DB, Redis, OpenVidu, AI Worker, 클라이언트)가 유기적으로 연동되어야 하므로 컨테이너 오케스트레이션이 필요
- 내부 네트워크 분리로 서비스 간 보안 격리 필요
- Reverse Proxy를 통한 도메인 라우팅 및 HTTPS 처리 필요
한 줄 설명
"GitHub OAuth 기반의 실시간 팀 협업 플랫폼으로, AI가 이슈 우선순위를 분석하고 회의 내용을 자동 요약한다."
2. 전체 아키텍처
전체 구성 요소
| 계층 | 기술 스택 | 역할 |
|---|---|---|
| Reverse Proxy | Traefik | 도메인 라우팅, HTTPS 종단 |
| Frontend | React | SPA 클라이언트 |
| Backend | Java 21 + Spring Boot 4.0 | REST API, 인증/인가, SSE |
| Media Server | OpenVidu 2.32.1 | WebRTC 화상회의 (SFU) |
| AI Worker | Python (FastAPI) + HuggingFace | 이슈 우선순위 분석, 회의 요약 (Whisper STT + Claude API) |
| Message Broker | Redis 7.2 | 작업 큐, Pub/Sub, SSE client key 저장 |
| Database | MySQL 8.0 | Source of Truth |
| 검색 엔진 | Elasticsearch | (의존성만 존재, 실제 사용 확인 필요) |
프론트엔드/백엔드/DB/Redis/Storage/Worker/Reverse Proxy 관계
외부 요청 (HTTPS)
│
▼
Traefik (Reverse Proxy)
├─► didit-client (React 정적 서빙)
├─► didit-server (Spring Boot :8080)
│ ├─► MySQL 8.0 (:3306, internal 네트워크만)
│ ├─► Redis 7.2 (:6379)
│ ├─► OpenVidu (:4443, internal 네트워크만)
│ └─► SSE Emitter → 클라이언트 직접 연결
└─► (AI Worker는 HTTP 외부 노출 없음, Redis 큐로만 통신)
외부 요청이 내부 서비스로 전달되는 흐름
- 클라이언트 →
https://did-it.xyz→ Traefik →didit-client(정적 파일) 또는didit-server(API) didit-server는internal네트워크를 통해db:3306,redis:6379,openvidu:4443와 통신- AI 분석 요청: 서버가 Redis List(
queue:issue:priority:single)에 작업 Push → AI Worker가 polling → 결과 Redis Pub/Sub 발행 → 서버 Listener가 수신 → SSE로 클라이언트 전달
서비스 간 책임 분리
- didit-server: 모든 비즈니스 로직, 인증, API, SSE, 외부 연동 조율
- AI Worker: 순수 ML 추론 작업만 수행, HTTP 엔드포인트 없이 Redis로만 통신
- OpenVidu: WebRTC 미디어 스트리밍만 담당, 시그널링/녹화는 서버가 조율
- MySQL: 모든 영속 데이터 저장
- Redis: 캐시/세션보다 비동기 작업 큐 + Pub/Sub 메시징이 주 용도
외부 공개 서비스와 내부 서비스 구분
| 공개 (외부) | 비공개 (내부) |
|---|---|
| Traefik (443) | MySQL (3306) |
| didit-server (8080, Caddy 네트워크 경유) | Redis (6379, 포트는 외부 노출되어 있음 - 개선 여지 있음) |
| didit-client | OpenVidu (4443) |
| AI Worker |
아키텍처 유형
모듈러 모놀리스에 가깝다. 하나의 Spring Boot 애플리케이션이 API + SSE + 보안을 모두 담당하며, AI 분석만 별도 Python Worker로 분리되어 있다. 패키지 구조는 전통적인 3계층(api/service/data)으로 설계되어 있다.
아키텍처 설계 의도
- SSE + Redis Pub/Sub으로 실시간성을 확보하면서도 WebSocket보다 가벼운 인프라 구성
- AI Worker를 별도 프로세스로 분리해 ML 추론 부하가 API 응답성에 영향을 주지 않도록 설계
- GitHub OAuth2 + Session Cookie 방식으로 SPA에서도 안전한 인증 구현
3. API 설계
API 버전 관리
- URL 경로 기반 버저닝:
/api/v1/ - Base URL:
https://api.did-it.xyz/api/v1
Swagger/OpenAPI 문서화
- SpringDoc OpenAPI 3.0.1 (Swagger UI 연동)
openapi.yaml파일 제공 (/openapi.yaml,/v3/api-docs/swagger-config)- 운영 환경(
prod)에서는 Swagger UI / API Docs 비활성화
주요 API 목록 (총 30개+ 엔드포인트)
인증 (Auth)
| Method | Endpoint | 인증 | Request DTO | Response DTO | 성공 | 실패 |
|---|---|---|---|---|---|---|
| GET | /api/v1/auth/login |
불필요 | - | Redirect | 302 | - |
| POST | /api/v1/auth/logout |
불필요 | - | - | 200 | - |
| GET | /api/v1/auth/me |
필요 | - | UserResponse | 200 | 401 |
프로젝트 (Projects)
| Method | Endpoint | 인증 | 권한 | Request DTO | Response DTO |
|---|---|---|---|---|---|
| GET | /api/v1/projects?page={n} |
필요 | 멤버 | - | ProjectResponse[] |
| POST | /api/v1/projects |
필요 | - | AddProjectRequest | 200 empty |
| GET | /api/v1/projects/{projectId} |
필요 | 멤버 | - | ProjectResponse |
| DELETE | /api/v1/projects/{projectId} |
필요 | OWNER | - | 200 empty |
| DELETE | /api/v1/projects/{projectId}/leave |
필요 | 멤버 | - | 200 empty |
| GET | /api/v1/projects/recents |
필요 | - | - | ProjectRecentResponse[] |
| GET | /api/v1/projects/{projectId}/participants |
필요 | - | - | UserResponse[] |
| DELETE | /api/v1/projects/{projectId}/participants/{userId} |
필요 | OWNER | - | 200 empty |
| PATCH | /api/v1/projects/{projectId} |
필요 | OWNER | UpdateRepoRequest | 200 empty |
| PATCH | /api/v1/projects/{projectId}/owner |
필요 | OWNER | TransferOwnerRequest | 200 empty |
| PATCH | /api/v1/projects/{projectId}/name |
필요 | OWNER | UpdateProjectNameRequest | 200 empty |
| POST | /api/v1/projects/{projectId}/github/validate |
필요 | OWNER | ValidateGithubRequest | GithubRepoResponse |
초대 (Invites)
| Method | Endpoint | 인증 | 권한 | Request DTO | Response DTO |
|---|---|---|---|---|---|
| POST | /api/v1/projects/invites |
필요 | ADMIN | AddProjectInviteRequest | UUID String |
| GET | /api/v1/projects/invites/{inviteCode} |
필요 | - | - | ProjectResponse |
| POST | /api/v1/projects/invites/{inviteCode} |
필요 | - | - | 200 empty |
회의 (Meetings/Channels)
| Method | Endpoint | 인증 | 권한 | Request DTO | Response DTO |
|---|---|---|---|---|---|
| POST | /api/v1/projects/{projectId}/add-channel |
필요 | ADMIN | CreateMeetingRequest | Long (meetingId) |
| POST | /api/v1/projects/{projectId}/book-channel |
필요 | ADMIN | BookMeetingRequest | Long (meetingId) |
| GET | /api/v1/projects/{projectId}/channels?status&cursor |
필요 | - | - | MeetingResponse[] |
| GET | /api/v1/projects/{projectId}/channels/date?start&end |
필요 | 멤버 | - | MeetingResponse[] |
| GET | /api/v1/channels/{channelId} |
필요 | 멤버 | - | MeetingResponse |
| PATCH | /api/v1/channels/{channelId}?title&start&due |
필요 | 멤버 | - | 200 empty |
| DELETE | /api/v1/channels/{channelId} |
필요 | 멤버 | - | 200 empty |
| POST | /api/v1/channels/{channelId}/webrtc |
필요 | 멤버(VOICE) | - | String (token) |
| DELETE | /api/v1/channels/{channelId}/webrtc |
필요 | 멤버(VOICE) | - | 200 empty |
| GET | /api/v1/channels/{channelId}/webrtc/users |
필요 | - | - | UserResponse[] |
| POST | /api/v1/channels/{channelId}/recording/start |
필요 | 멤버(VOICE) | - | Long (recordId) |
| POST | /api/v1/channels/{channelId}/recording/stop/{recordId} |
필요 | - | - | 200 empty |
채팅 (Chat)
| Method | Endpoint | 인증 | 권한 | Request DTO | Response DTO |
|---|---|---|---|---|---|
| POST | /api/v1/channels/{channelId}/chats |
필요 | 멤버 | SendMessageRequest | 200 empty |
| GET | /api/v1/channels/{channelId}/chats?lastId |
필요 | 멤버 | - | ChatResponse[] |
| PATCH | /api/v1/channels/chats/{chatId} |
필요 | 작성자 | UpdateChatMessage | 200 empty |
| DELETE | /api/v1/channels/chats/{chatId} |
필요 | 작성자 | - | 200 empty |
SSE (Server-Sent Events)
| Method | Endpoint | 인증 | 권한 | 설명 |
|---|---|---|---|---|
| GET | /api/v1/channels/{channelId}/stream |
필요 | 멤버 | 채널 단위 실시간 이벤트 |
| GET | /api/v1/projects/{projectId}/stream |
필요 | 멤버 | 프로젝트 단위 실시간 이벤트 |
이슈 (Issues)
| Method | Endpoint | 인증 | 권한 | Request DTO | Response DTO |
|---|---|---|---|---|---|
| GET | /api/v1/projects/{projectId}/issues/active |
필요 | 멤버 | - | IssueResponse[] |
| GET | /api/v1/projects/{projectId}/issues |
필요 | 멤버 | - | IssueResponse[] |
| POST | /api/v1/projects/{projectId}/issue/analyze |
필요 | 멤버 | AnalyzeIssueRequest | AiResponse |
| POST | /api/v1/projects/{projectId}/issues |
필요 | 작성자 | CreateIssueRequest | IssueResponse[] |
| PATCH | /api/v1/projects/{projectId}/issues/{issueId} |
필요 | 작성자 | UpdateIssueRequest | 200 empty |
| DELETE | /api/v1/projects/{projectId}/issues/{issueId} |
필요 | 작성자 | - | 200 empty |
| GET | /api/v1/projects/{projectId}/github/issues |
필요 | 멤버 | - | IssueResponse[] |
웹훅 (Webhooks)
| Method | Endpoint | 인증 | 설명 |
|---|---|---|---|
| POST | /api/v1/webhooks/openvidu |
Token 검증 | OpenVidu 이벤트 수신 (세션/참가자/녹화 이벤트) |
회의 요약 (Meeting Summary)
| Method | Endpoint | 인증 | 권한 |
|---|---|---|---|
| GET | /api/v1/projects/{projectId}/summary?page |
필요 | 멤버 |
| PATCH | /api/v1/projects/{projectId}/summary/{summaryId} |
필요 | 멤버 |
| DELETE | /api/v1/projects/{projectId}/summary/{summaryId} |
필요 | ADMIN |
페이징 방식
- Spring Pageable (
page,size,sort쿼리 파라미터) - 회의 목록, 채팅 목록 - 커서 기반 (
cursor/lastId) - 일부 엔드포인트에서 사용
공통 응답 패턴
- 성공:
200 OK+ DTO (빈 body인 경우도 있음) - 실패:
ErrorResponse(statusCode, message)+ 해당 HTTP Status
4. 인증/인가 설계
로그인 방식
- GitHub OAuth2 (Authorization Code Grant)
- Spring Security의
oauth2Login()사용 - 요청 권한 범위(scope):
read:user,user:email,repo,read:org
세션 관리
- Session Cookie (
JSESSIONID) 기반 - Cookie 설정:
SameSite=None,Secure=True,HttpOnly=True - 로그아웃 시 세션 무효화 + 쿠키 삭제
현재 사용자 식별 방식
@AuthenticationPrincipal CustomOAuth2User어노테이션으로 Controller에서 접근CustomOAuth2User는 Spring의DefaultOAuth2User를 확장하여 내부 DB PK(id)와 GitHubaccessToken을 포함- OAuth2 로그인 성공 시
CustomOAuth2UserService에서 사용자 정보를 DB에 저장/갱신 (joinOrUpdate)
Role/Permission 구조
| Role | 범위 | 설명 |
|---|---|---|
ADMIN |
프로젝트 | 프로젝트 소유자(최초 생성자), 모든 관리 권한 보유 |
MEMBER |
프로젝트 | 일반 참여자, 읽기/쓰기 가능하나 관리 기능 제한 |
| User Status | 설명 |
|---|---|
PENDING |
초대 수락 전 (사용하지 않는 것으로 보임) |
ACTIVE |
활성 멤버 |
LEFT |
탈퇴/추방된 멤버 (soft 상태 변경) |
리소스 소유자 검증 방식
- 프로젝트 소유자(ADMIN) 검증:
project.getOwner().getId().equals(userId)→ 실패 시ForbiddenError("권한이 부족합니다.") - 이슈 작성자 검증:
entity.getAuthor().getId() != userId→ 실패 시ForbiddenError("권한이 없습니다.") - 프로젝트 멤버십 검증: 모든 요청 전
FindProjectUser(userId, projectId)호출 → 실패 시NotFoundError또는ForbiddenError
권한 검증 위치
- Controller 계층:
@AuthenticationPrincipal으로 사용자 식별 - Service 계층: 모든 권한 검증이 Service 구현체 내에서 수행됨 (Global Filter/AOP 미사용)
인증 실패/권한 실패 응답 방식
- 인증 실패: Spring Security 기본 동작 (401 Unauthorized 또는 로그인 페이지로 리다이렉트)
- 권한 실패:
Result.fail(new ForbiddenError("메시지"))→ Controller에서ErrorResponse로 변환 (HTTP 403)
인증/인가 설계 판단
장점: GitHub OAuth2로 별도 회원가입 절차 불필요, 개발자 타겟에 적합 개선 여지:
- Role이 ADMIN/MEMBER 2개로 단순하나, 요약 삭제(DELETE summary)만 ADMIN 전용이고 대부분 MEMBER에게 개방되어 있어 세분화 부족
@PreAuthorize등 선언적 인가보다 Service 코드 내 if문 검증이 많아 보안 정책이 코드에 흩어져 있음- OpenVidu Webhook 엔드포인트만 Token 헤더 검증 (공개 URL + Secret Token)
5. 데이터베이스 설계
주요 Entity 목록 (총 14개 테이블)
| Entity | Table | 설명 |
|---|---|---|
| UserEntity | users |
GitHub OAuth 사용자 |
| UserGithubAuthEntity | user_github_auth |
GitHub Access Token 저장 (1:1 users) |
| ProjectEntity | projects |
프로젝트/워크스페이스 |
| ProjectUserEntity | project_users |
프로젝트-사용자 멤버십 (N:M) |
| ProjectInviteEntity | project_invites |
UUID 기반 초대 링크 |
| ProjectRecentEntity | project_recents |
최근 조회 프로젝트 (최대 4개 노출) |
| MeetingEntity | meetings |
회의/채널 |
| MeetingUserEntity | meeting_users |
회의 참여자 |
| MeetingRecordEntity | meeting_records |
OpenVidu 녹화 기록 |
| MeetingSummaryEntity | meeting_summary |
AI 회의 요약 (버전 관리) |
| IssueEntity | issues |
GitHub 연동 이슈 |
| IssueAssigneeEntity | issue_assignees |
이슈 담당자 (N:M) |
| ChatEntity | chats |
채팅 메시지 (Soft Delete) |
| ProjectUserReadEntity | project_user_reads |
사용자별 읽음 상태 |
테이블 간 관계 (주요 FK)
users (1) ────── (1) user_github_auth [FK: user_id, ON DELETE CASCADE]
users (1) ────── (N) projects [FK: owner_id, ON DELETE RESTRICT]
users (1) ────── (N) project_users [FK: user_id, ON DELETE RESTRICT]
projects (1) ──── (N) project_users [FK: project_id, ON DELETE CASCADE]
projects (1) ──── (N) project_invites [FK: project_id, ON DELETE CASCADE]
projects (1) ──── (N) meetings [FK: project_id, ON DELETE CASCADE]
users (1) ──────── (N) meetings [FK: created_by, ON DELETE RESTRICT]
meetings (1) ──── (N) meeting_users [FK: meeting_id, ON DELETE CASCADE]
users (1) ──────── (N) meeting_users [FK: user_id, ON DELETE RESTRICT]
meetings (1) ──── (N) meeting_records [FK: meeting_id, ON DELETE CASCADE] (추정)
meetings (1) ──── (N) meeting_summary [FK: meeting_id, ON DELETE CASCADE]
projects (1) ──── (N) issues [FK: project_id, ON DELETE CASCADE]
users (1) ──────── (N) issues [FK: author_id, ON DELETE RESTRICT]
issues (1) ────── (N) issue_assignees [FK: issue_id, ON DELETE CASCADE]
users (1) ──────── (N) issue_assignees [FK: user_id, ON DELETE RESTRICT]
projects (1) ──── (N) chats [FK: project_id, ON DELETE CASCADE]
users (1) ──────── (N) chats [FK: user_id, ON DELETE RESTRICT]
meetings (1) ──── (N) chats [FK: meeting_id, ON DELETE SET NULL]
주요 FK 제약 특징
ON DELETE RESTRICT: 사용자(users) 삭제 방지 (관련 엔티티가 있으면 삭제 불가)ON DELETE CASCADE: 프로젝트/회의/이슈 삭제 시 하위 데이터 자동 삭제ON DELETE SET NULL: 채팅의 meeting_id, 요약의 meeting_id/edited_by → 참조 레코드 삭제 시 null
주요 Unique / Index 제약
| 테이블 | Type | 컬럼 | 설명 |
|---|---|---|---|
| users | UK | github_id | GitHub 계정 중복 방지 |
| users | UK | github_login | GitHub 로그인 중복 방지 |
| projects | UK | repo_full_name, active_key | 동일 레포지토리 중복 등록 방지 (Soft Delete 고려) |
| project_users | UK | (project_id, user_id) | 중복 멤버십 방지 |
| issues | UK | (project_id, github_issue_id) | 프로젝트 내 동일 GitHub 이슈 중복 방지 |
| issue_assignees | UK | (issue_id, user_id) | 이슈별 중복 담당자 방지 |
| meeting_summary | UK | (meeting_id, version) | 동일 회의 동일 버전 요약 중복 방지 |
| meeting_records | UK | recording_session_id | 녹화 세션 중복 방지 |
| project_recents | UK | (user_id, project_id) | 최근 조회 중복 방지 |
| project_user_reads | UK | (project_id, user_id) | 읽음 상태 중복 방지 |
Soft Delete 여부
projects: Soft Delete (deleted_at컬럼 존재, 그러나ProjectRepository.delete()가 실제 삭제하는 것으로 보임 - 확인 필요)chats: Soft Delete (deleted_at컬럼 존재, 실제 deleteMessage() 동작은 확인 필요)project_users: 상태 변경 방식 (status = LEFT,left_at기록) - 실제 삭제 없이 상태만 변경
CreatedAt/UpdatedAt 관리 방식
- JPA Entity에서
@CreationTimestamp,@UpdateTimestamp사용 - DB 레벨:
DEFAULT CURRENT_TIMESTAMP,ON UPDATE CURRENT_TIMESTAMP
Migration 관리 방식
- Flyway 사용
- 마이그레이션 파일:
apps/server/src/main/resources/db/migration/V{번호}__{설명}.sql - 총 12개 마이그레이션 파일 (V1~V12)
clean-disabled: true(운영에서 실수로 clean 방지)- JPA:
ddl-auto: validate(Entity-DB 불일치 시 즉시 오류)
데이터 정합성 설계
- Unique Constraint + FK 제약으로 애플리케이션 레벨 + DB 레벨 이중 방어
@Transactional로 서비스 단위 작업 원자성 보장
6. 비즈니스 로직 / UseCase 구조
주요 UseCase 목록
프로젝트 관리
AddProject: 프로젝트 생성 + 생성자 ADMIN 등록 + 최근 조회 upsertAddInviteCode: UUID 초대 코드 생성 (ADMIN 전용, 만료일 설정)AddProjectUser: 초대 코드로 프로젝트 참여 (LEFT 상태였으면 ACTIVE로 복원)DeleteProject: 프로젝트 삭제 (OWNER 전용)LeaveProject: 프로젝트 탈퇴 (OWNER 불가), 상태 LEFT로 변경transferOwnership: OWNER 권한 이전 (ADMIN↔MEMBER 역할 교환)removeParticipant: 참여자 추방 (OWNER 전용)
회의 관리
createMeeting: 회의 생성 (ADMIN 전용, RUNNING 상태)bookMeeting: 회의 예약 (ADMIN 전용, SCHEDULED 상태, 시간 검증 포함)UpdateMeeting: 회의명/시간 수정DeleteMeeting: 회의 삭제
미디어 관리 (OpenVidu 연동)
createConnection: OpenVidu 세션/커넥션 생성 → 토큰 발급closeConnection: 커넥션 종료 + 사용자 left 처리StartRecording: 녹화 시작StopRecording: 녹화 중단
이슈 관리 (GitHub 연동)
AnalyzeIssue: 이슈 초안 생성 → Redis 큐에 AI 분석 요청CreateIssue: AI 분석 완료된 초안 → GitHub 실제 이슈 생성 + 우선순위 설정updateIssue: 이슈 수정 (작성자만, GitHub 동기화 포함)deleteIssue: 이슈 삭제 (작성자만, GitHub 이슈는 CLOSE)getGithubIssues: GitHub에서 이슈 동기화 (신규 등록 + 상태 업데이트)
Command/Query 구조
service/command/:AddProjectCommand,BookMeetingCommand,UpdateRecordingCommand,UpdateSummaryCommand- 요청 DTO → Controller에서 Command 변환 → Service 호출
- CQS(Command-Query Separation)가 엄격하게 적용되지는 않음 (대부분 Service가 CRUD 혼합)
요청 처리 순서 (예: CreateIssue)
- Controller:
@AuthenticationPrincipal으로 userId 추출,@RequestBody CreateIssueRequest바인딩 - Service:
issueRepository.findById(issueId)→ 드래프트 이슈 존재 확인 - 작성자 검증 (
entity.getAuthor().getId() != userId) - GitHub 인증 정보 조회 (
userGithubAuthRepository) - GitHub API 호출:
githubService.createIssue(accessToken, ...) - 로컬 Entity 업데이트 (title, body, priority, githubIssueId, issueNo, assignees)
issueRepository.save(entity)
검증 로직
- Bean Validation:
@Valid+ Jakarta Validation (@NotBlank,@Size,@URL) - Service 내 비즈니스 검증: 존재 확인 →
NotFoundError, 권한 확인 →ForbiddenError, 중복 확인 →ConflictError
성공/실패 반환 방식
- 모든 Service 메서드가
Result<T>반환 - Controller는
result.isFailure()→ResultError.getResponse()(ErrorResponse DTO + HTTP Status) - 성공 시
ResponseEntity.ok(data)또는.ok().build()(empty body)
API 계층과 UseCase 계층의 책임 분리
- Controller: URL 라우팅,
@Valid검증,@AuthenticationPrincipal추출, Result→HTTP 변환, DTO 변환 - Service: 모든 비즈니스 로직, 권한 검증, 트랜잭션 관리, 외부 API 호출
7. 트랜잭션과 동시성
트랜잭션이 필요한 기능
AddProject: 프로젝트 생성 + ADMIN 등록 (2개 Entity)CreateIssue: 이슈 Entity 업데이트 + Assignee 교체 + GitHub API 호출transferOwnership: OWNER/MEMBER 역할 교환 (3개 Entity 동시 변경)LeaveProject: 상태 변경 (UPDATE only)updateProjectRepo: 프로젝트 레포지토리 정보 변경
트랜잭션 범위
@Transactional이 Service 구현체 메서드 단위로 적용ProjectServiceImpl:@Transactional(readOnly = true)(조회성),@Transactional(쓰기)IssueServiceImpl:@jakarta.transaction.Transactional(Jakarta EE, Spring과 혼용 - 개선 여지)
동시 요청 시 발생할 수 있는 문제
중복 참여 (AddProjectUser)
// LEFT 상태 유저 → ACTIVE로 복원, 이미 ACTIVE → ConflictError
if (entity.getStatus() == ProjectUserStatus.ACTIVE) {
return Result.fail(new ConflictError("이미 존재하는 유저입니다."));
}
- 동시에 2개 요청이 들어오면 둘 다 LEFT 상태를 읽고 ACTIVE로 변경 → 2개 ACTIVE 레코드 가능성 없음 (UK 제약
(project_id, user_id)이 방어)
중복 초대 코드 사용
POST /api/v1/projects/invites/{inviteCode}: 경쟁 상태 가능성 낮음 (멱등성 - ACTIVE로 상태 변경만 수행)
중복 생성 방지 방식
- UK Constraint: DB에서 중복 생성 방지
uk_project_users_project_user: 동일 프로젝트 중복 멤버십uk_issues_project_github_issue_id: 동일 GitHub 이슈 중복uk_issue_assignees_issue_user: 동일 이슈 중복 담당자
- 사전 중복 체크:
existsByRepoFullName(),existsByProject_IdAndUser_IdAndStatus()
Lock / CAS / Optimistic Concurrency
- 명시적 Lock 사용: 코드에서 확인되지 않음
- Optimistic Concurrency:
MeetingSummaryEntity에@Version과 혼동될 수 있는version컬럼이 있으나 JPA의@Version은 아님 (단순 데이터 컬럼)
Idempotency 고려
- Redis 큐(
leftPush)로 AI 작업을 중복 적재할 가능성 있음 (이슈 조회 시 항상 큐에 push) - Idempotency key 미사용, 별도 중복 방지 메커니즘 없음 → 개선 여지
8. 예외 처리와 응답 구조
Result 패턴
- 자체 구현
Result<T>클래스 사용 (C#의 Result 패턴 차용) - 예외를 던지지 않고 성공/실패를 값으로 반환
Result.ok(value)/Result.fail(error)/Result.fail(errors)
ResultError 계층
ResultError (인터페이스)
├── SimpleError (단순 code + message)
├── NotFoundError (404)
├── ForbiddenError (403)
├── ConflictError (409)
├── BadRequestError (400)
├── GoneError (410)
└── ServerError (500)
getCode()= HTTP Status Code (예: 404, 403, 409)getStatus()=HttpStatus.valueOf(code)getResponse()=new ErrorResponse(status, message)
Global Exception Middleware
- 코드에서 확인되지 않음 (
@ControllerAdvice없음) - Controller가 직접
result.isFailure()를 체크하고ErrorResponse로 변환 ResultException(runtime)은 존재하나 이를 처리하는 Global Handler 미확인
공통 ErrorResponse 구조
{
"StatusCode": "NOT_FOUND", // HttpStatus enum name
"Message": "..."
}
StatusCode필드: PascalCase 사용 (일관성 이슈)Message필드: 한글/영문 혼용
에러 코드 체계
- HTTP Status를 에러 코드로 사용
- Custom 에러 코드 체계 없음 (예:
ERR-001같은 비즈니스 에러 코드)
로그 레벨 구분
log.info: 정상 흐름 (깃허브 데이터, AI 요청 페이로드)log.warn: OpenVidu webhook 파싱 실패log.error: Redis 메시지 처리 실패, 엔티티 미발견, 작업 처리 실패
프론트엔드 일관 처리
- 모든 실패가
ErrorResponse(statusCode, message)+ HTTP Status로 통일 → 일관된 처리 가능 - SSE Error 이벤트에도 동일한
ErrorResponse구조 사용
9. 인프라 구성
Dockerfile 존재 여부
- didit-server: 있음 (
apps/server/Dockerfile) - Multi-stage build (Build:eclipse-temurin:21-jdk, Runtime:eclipse-temurin:21-jre) - didit-client: 있음 (
apps/client/Dockerfile) - (내용 확인 필요) - AI Worker: 확인 필요 (Dockerfile 미발견)
Docker Compose 구성
로컬 개발용 (apps/server/docker-compose.yml)
services:
db: MySQL 8.0 (3306 노출)
openvidu: OpenVidu dev 2.32.1 (4443 노출)
redis: Redis 7.2-alpine (6379 노출)
- 백엔드 서버 자체는 로컬 IDE에서 실행
healthcheck: mysqladmin ping (MySQL only)
배포용 (infra/docker-compose/docker-compose.yml)
| 서비스 | 이미지 | 네트워크 | 포트 | 설명 |
|---|---|---|---|---|
| db | mysql:8.0 | internal only | expose 없음 | MySQL, healthcheck 적용 |
| server | ghcr.io/pjtdidit/server-image:latest | internal + caddy_default | expose 8080 | Spring Boot |
| client | ghcr.io/pjtdidit/client-image:latest | internal + caddy_default | - | React 정적 서빙 |
| openvidu | openvidu/openvidu-dev:2.32.1 | internal only | expose 없음 | WebRTC SFU |
| redis | redis:7.2-alpine | internal only | 6379 (외부 노출) | Message Broker |
서비스별 컨테이너 역할
- db: 영속 데이터 저장 (MySQL 8.0, utf8mb4, Asia/Seoul timezone)
- server: API 서버 (HTTP API, SSE, Webhook 수신)
- client: 정적 파일 서빙 (React SPA)
- openvidu: WebRTC 미디어 서버 (SFU, recording)
- redis: 작업 큐, Pub/Sub, client key 저장소
내부 네트워크/외부 네트워크 구성
internal(bridge): db, server, client, openvidu, redis 간 통신caddy_default(external): server ↔ client ↔ Reverse Proxy 통신용
Volume 마운트 구조
mysql-data:/var/lib/mysql(MySQL 데이터 영속화)redis_data:/data(Redis AOF 데이터 영속화)
Healthcheck
- MySQL:
mysqladmin ping(10s interval, 5s timeout, 10 retries) - Server: depends_on db(healthy), 자체 healthcheck 없음 → 개선 여지
- Redis: 별도 healthcheck 없음
depends_on 조건
serverdepends_ondbwithcondition: service_healthy- 나머지 서비스 간 의존성 명시 안 됨
포트 노출 정책
- 배포 환경: 모든 DB/서버 포트가 주석 처리 (외부 직접 접근 불가)
- 단, Redis 6379가 환경변수로 외부 노출 가능 → 보안상 개선 여지 있음
인프라 구성 설계 의도
- 단일 서버에 Docker Compose로 올인원 배포 (소규모 프로젝트에 적합)
internal네트워크로 서비스 간 통신 격리caddy_defaultexternal 네트워크로 Reverse Proxy와만 API 통신 허용
10. Reverse Proxy / 도메인 / HTTPS
Caddy/Nginx 사용 여부
- Traefik 사용 (설계 문서상 명시)
- 실제 배포 docker-compose에는
caddy_defaultexternal 네트워크로 연결 → Caddy 사용 가능성도 있음 (네트워크명이 caddy_default)
도메인 라우팅 구조
- Frontend:
https://did-it.xyz - API:
https://api.did-it.xyz/api/v1 - CORS 허용 Origin:
https://did-it.xyz
subdomain 구성
did-it.xyz→ 클라이언트api.did-it.xyz→ 백엔드 API 서버
HTTPS 적용 방식
- Traefik/Caddy에서 HTTPS 종단 처리
- Spring Boot 자체는 HTTP(8080), Cookie
Secure=true→ Reverse Proxy가 HTTPS 처리하고 있음을 전제
reverse_proxy 대상
/및 정적 리소스 →didit-client/api/v1/*→didit-server:8080/oauth2/*→didit-server:8080
외부 포트 노출 최소화
- Docker Compose에서 MySQL, OpenVidu, Server의 포트가 명시적으로 주석 처리되어 외부에서 직접 접근 불가
- Redis 포트(
6379)는 환경변수로 노출되도록 열려 있음 → 개선 필요
11. 환경변수와 설정 관리
.env 구조
.env.example파일 존재 (14개 변수)- 주요 변수:
MYSQL_*,GITHUB_*,OPENVIDU_*,REDIS_*
개발/운영 설정 분리
application.yml(공통)application-prod.yml(운영 전용: Swagger 비활성화, HikariCP Pool 튜닝, Tomcat Thread 설정)SPRING_PROFILES_ACTIVE=prod환경변수로 활성화
DB 연결 문자열 구성 방식
url: jdbc:mysql://${MYSQL_HOST:db}:${MYSQL_PORT:3306}/${MYSQL_DATABASE:didit}?serverTimezone=Asia/Seoul&characterEncoding=utf8
- 환경변수 우선, 기본값 fallback (
MYSQL_HOST:db) MYSQL_USER,MYSQL_PASSWORD도 환경변수에서 주입
Redis 연결 설정
- 환경변수:
REDIS_HOST,REDIS_PORT,REDIS_PASSWORD - timeout: 2s
- Password 기반 인증 (
requirepass)
Secret 관리 방식
- GitHub Client ID/Secret, OpenVidu Secret: 환경변수로 주입
.env.example은 Git에 커밋되어 있으나, 실제 값은 배포 환경에서 별도 관리- Vault/Secret Manager 미사용 → 개선 여지
ASP.NET Options 바인딩
- 해당 없음 (Java / Spring Boot 프로젝트)
- Spring Boot
@ConfigurationProperties사용 (OpenViduProperties,OpenViduWebhookProperties)
설정 검증
- Bean Validation을 통한 설정 검증 미확인
@Validated+@ConfigurationProperties조합 사용 가능하나 구현되지 않음
12. 로그 / 모니터링 / 운영
로깅 구조
- SLF4J + Logback (Spring Boot 기본)
@Slf4j(Lombok)으로 Logger 주입
구조화 로그 사용 여부
- JSON 구조화 로그: 미사용 (일반 텍스트 로그)
log.info("깃허브 데이터: {}", attributes)등
요청/응답 로그
- Spring MVC 요청 로깅 확인 필요 (Access Log 미구현 가능성)
예외 로그
log.error("Redis 메시지 처리 중 에러 발생: {}", e.getMessage())log.error("워커 실행 중 오류 발생: %s", str(e), exc_info=True)(Python)
로그 레벨 기준
- INFO: 정상 비즈니스 흐름
- WARN: 복구 가능한 오류 (webhook 파싱 실패)
- ERROR: 실패한 작업 처리
운영 도구 사용 여부
- Dozzle: 미확인
- Grafana/Loki: 미확인
- Portainer: 배포 트리거로 사용 (
PORTAINER_WEBHOOK_URL) - AI Worker:
/health,/health/redisHealthcheck 엔드포인트 존재
Healthcheck
- AI Worker:
GET /health,GET /health/redis→ FastAPI 제공 - Server: Spring Boot Actuator Healthcheck 확인 필요 (의존성 없음)
- DB: docker-compose healthcheck (mysqladmin ping)
- Redis: 별도 healthcheck 없음
13. CI/CD / 배포 자동화
GitLab CI/CD 구성
.gitlab-ci.yml (Root Pipeline)
- Trigger: Merge Request Event + Master Push
- Stages:
test→build→deploy - Docker BuildKit 활성화
Server Pipeline (.gitlab/ci/server.yml)
| Job | Stage | Trigger | Actions |
|---|---|---|---|
test_server |
test | MR (apps/server 변경 시) | ./gradlew --no-daemon test → JUnit report artifact |
build_push_server |
build | Master Push (apps/server 변경 시) | Docker build + push to ghcr.io/pjtdidit/server-image:latest |
Deploy Pipeline (.gitlab/deploy.yml)
| Job | Stage | Trigger | Actions |
|---|---|---|---|
deploy_portainer |
deploy | Master Push (변경 시) | POST to Portainer Webhook → Redeploy |
Docker image build
- Multi-stage Dockerfile (BuildKit 캐시 마운트 활용)
- Build Stage: Gradle wrapper → bootJar
- Runtime Stage:
eclipse-temurin:21-jre, non-root user (appuser) - JVM 옵션:
-XX:MaxRAMPercentage=75
Migration 실행 방식
- Flyway 자동 실행 (애플리케이션 시작 시)
- CI/CD에서 별도 migration step 없음
브랜치 전략
master= 기본 브랜치 (production)- Merge Request 기반 코드 리뷰 흐름
- Revert 이력 존재 (
revert-b0a13fa0)
배포 실패 시 대응 방식
- Portainer Webhook 호출 실패 시
set -eu로 Pipeline 중단 - Rollback 전략 확인 필요
향후 개선할 배포 구조
- AI Worker의 CI/CD 미구성 (
.gitlab/ci/ai.yml주석 처리) - Client의 CI/CD 존재 (
.gitlab/ci/client.yml) - Test 단계에서 Testcontainers 사용 → 실제 DB 의존성 없는 테스트 가능
14. 성능 최적화
Pagination 적용 여부
- 적용:
PageRequest.of(page, 10)(프로젝트 목록) - Spring Pageable: size 기본값 20
Projection DTO 조회 여부
- 미적용: Entity 전체 조회 후
stream().map(Response::fromEntity)로 변환 SELECT *형태로 모든 컬럼을 읽어옴 → 개선 여지
AsNoTracking 사용 여부
- JPA
@Transactional(readOnly = true)→ Hibernate가 flush 모드를 MANUAL로 설정하나 영속성 컨텍스트는 사용 AsNoTracking에 해당하는 명시적 설정 없음
Include/N+1 문제 처리
FetchType.LAZY사용 (기본 전략)findTop5ByProject_IdAndStatusOrderByUpdatedAtDesc: ProjectEntity, AuthorEntity lazy → 응답 변환 시 N+1 가능성 있음findAllByProject_Id: Assignee 컬렉션 @OneToMany → 응답 변환 시 N+1 가능성 있음@EntityGraph또는 JOIN FETCH 미사용 → 개선 여지
Index 적용 여부
- 외래키 컬럼 전반에 인덱스 적용:
idx_projects_repo_id,idx_issues_project_id,idx_meetings_project_id,idx_chats_project_created_at등 - 만료 시간 조회:
idx_project_invites_expires_at - 복합 인덱스:
(project_id, created_at),(meeting_id, created_at)
Cache 적용 여부
- 명시적 캐시: 미적용 (Spring Cache, @Cacheable 없음)
- Redis는 작업 큐/PubSub 용도로만 사용
응답 시간 측정 또는 개선 수치
- 확인 필요 (APM 도구 없음)
15. 설계 문서화
README 구조
readme.md: 저장소 구조, 디렉토리 가이드exec/readme.md: 실행 관련 (빌드/설정/시나리오)
API 문서
apps/server/docs/API.md: 상세 API 문서 (엔드포인트, DTO, SSE 이벤트)apps/server/docs/openapi.yaml: OpenAPI 3.0.3 명세docs/API/: Swagger/Redoc 관련 파일
ERD
docs/기획/ERD/: Logical ERD Model (Mermaid, SVG)docs/설계/Physical ERD Model.md: Mermaid ERD 다이어그램 + 테이블 상세 명세
아키텍처 다이어그램
docs/설계/Architecture.md: 기술 스택 구성도 (SVG)docs/설계/svg/architecture.svg: 전체 구성도docs/설계/svg/PERD.svg: 물리 ERD
시퀀스 다이어그램
- 확인 필요 (별도 시퀀스 다이어그램 미발견)
- SSE 이벤트 흐름:
docs/컨벤션/svg/sse_arch.svg,sse_flow.svg
기술 선택 이유 문서 / ADR
- 미발견 (ADR 디렉토리 없음)
Mermaid 다이어그램 사용 여부
- 사용: Physical ERD Model, SSE 문서 등에서 Mermaid 사용
16. 문제 해결 사례 후보
사례 1: AI 작업 결과의 실시간 클라이언트 전달
문제 상황: AI Worker가 이슈 우선순위 분석 완료 후, 특정 사용자에게만 결과를 전달해야 함 (다중 사용자 환경)
원인 분석: SSE broadcasting만으로는 모든 구독자에게 전파되므로, 특정 사용자에게만 선택적 전달이 필요
해결 방법:
- Redis에
sse:client_key:{userId}형태로 clientKey 저장 - AI 결과 수신 시
RedisSubscribeListener가 Redis에서 clientKey 조회 SseHub.broadcastToClient(projectId, clientKey, "ai_analysis_result", ...)→ 사용자 타겟팅
선택 이유: 동일 유저의 다중 탭/디바이스를 구분하면서도, Redis를 활용해 무상태(stateless) 서버 간 확장 가능
결과: AI 분석 결과가 요청한 사용자에게만 실시간 전달, 불필요한 브로드캐스트 방지
사례 2: LEFT 상태 사용자 재참여 처리
문제 상황: 탈퇴했던 사용자가 초대 링크로 재참여 시, UK 제약 위반 없이 기존 레코드를 재활용해야 함
원인 분석: project_users에 UK(project_id, user_id)가 있어 새 레코드 INSERT 불가
해결 방법: AddProjectUser 메서드에서:
- 기존 레코드 존재 확인 (
findByProject_IdAndUser_Id) ACTIVE상태면ConflictError,LEFT상태면 상태를ACTIVE로 복원 +role=MEMBER+leftAt=null- 레코드가 없으면 신규 생성
선택 이유: DB 레벨 UK 제약을 유지하면서도, Soft한 상태 변경으로 사용자 이력을 보존 (감사 추적 가능)
결과: 탈퇴했던 사용자도 초대 코드로 원활히 재참여 가능, 중복 참여 방지
사례 3: AI 분석 워크플로우의 비동기 처리
문제 상황: HuggingFace 모델 추론이 수 초~수십 초 소요되어, REST API 요청-응답 사이클 안에서 처리 불가
원인 분석: ML 추론은 HTTP 요청 타임아웃을 초과하는 장기 작업
해결 방법:
- Redis List를 작업 큐로 사용 (
queue:issue:priority:single,queue:issue:priority:sort) - 서버는
leftPush로 작업 등록 후 즉시 응답 - Python AI Worker가 0.5s polling으로 큐 소비
- 완료 시 Redis Pub/Sub으로 결과 발행 → 서버의
RedisMessageListener가 수신 → DB 업데이트 + SSE 전송
선택 이유: 추가 인프라(RabbitMQ/Kafka) 없이 기존 Redis로 큐/메시징 통합 구현, 운영 단순화
결과: ML 추론 시간과 API 응답성이 완전히 분리됨
사례 4: GitHub 이슈와 로컬 DB의 양방향 동기화
문제 상황: GitHub에서 이슈를 가져올 때, 기존 로컬 DB에 있는 이슈는 업데이트하고 없는 이슈는 새로 생성해야 함
원인 분석: GitHub이 Source of Truth지만, AI 우선순위 등 로컬 메타데이터를 추가로 관리해야 함
해결 방법: getGithubIssues() 메서드에서:
- GitHub API로 레포지토리 이슈 일괄 조회
existsByGithubIssueId(ghIssue.getId())→ 존재하면 title/body/status 업데이트- 미존재 시 신규 Entity 생성 (priority 기본값 MEDIUM)
- Assignee 정보 GitHub→로컬 UserEntity mapping
- 모든 이슈에 대해 AI 단일 분석 + 배치 정렬 큐 추가
선택 이유: Upsert 패턴으로 데이터 중복 없이 동기화, github_issue_id를 natural key로 사용
결과: GitHub과 로컬 DB 간 일관성 유지, 신규 이슈 자동 등록, AI 분석 파이프라인 연계
17. 포트폴리오 문장 초안
프로젝트 개요
"Didit은 GitHub 생태계에 특화된 실시간 팀 협업 플랫폼입니다. GitHub OAuth2 로그인, OpenVidu 기반 화상회의, Redis Pub/Sub 기반 AI 비동기 분석, SSE 실시간 이벤트 스트리밍을 하나의 Spring Boot 서버로 통합한 B2B SaaS 프로젝트입니다."
담당 역할
"백엔드 개발자로서 Spring Boot 4.0 서버의 전체 API 설계 및 구현, 인증/인가 체계 수립, 데이터베이스 스키마 설계(Flyway), Docker Compose 기반 인프라 구성, GitLab CI/CD 파이프라인 구축을 담당했습니다."
주요 기여
- 30개 이상의 RESTful API 엔드포인트 설계 및 구현
- GitHub OAuth2 + Session Cookie 인증 체계 구축
- Redis 기반 AI 작업 큐 + Pub/Sub 메시징 아키텍처 설계
- SSE(Server-Sent Events) 채널/프로젝트 단위 실시간 이벤트 시스템 구현
- Flyway + JPA validate 기반 데이터베이스 마이그레이션 관리
사용 기술 및 선택 이유
| 기술 | 선택 이유 |
|---|---|
| Java 21 + Spring Boot 4.0 | 최신 LTS 버전, Virtual Threads 등 최신 기능 활용, 강력한 생태계 |
| MySQL 8.0 | FK 제약, UK 제약으로 데이터 정합성 보장, utf8mb4 지원 |
| Redis 7.2 | 빠른 메모리 기반 작업 큐, Pub/Sub 메시징, 경량 인프라 |
| Flyway | 선언적 DB 마이그레이션, 모든 변경 이력 추적, Clean 비활성화로 안전 |
| GitHub OAuth2 | 개발자 타겟, 별도 회원가입 불필요, repo/org 권한 자연스러운 연계 |
| SSE + Redis Pub/Sub | WebSocket보다 가벼운 실시간 통신, Redis로 확장 가능한 이벤트 브로커 역할 |
| Docker Compose | 5개 서비스의 로컬/배포 환경 일관성 확보, 단일 서버 올인원 배포 |
| Portainer + GitLab CI/CD | Docker 이미지 자동 빌드 → Webhook 기반 배포 트리거, 운영 단순화 |
아키텍처 설계
"전통적인 3계층(api/service/data) 구조를 채택하여 HTTP/비즈니스/데이터 책임을 명확히 분리했습니다. AI 분석 작업은 별도 Python Worker로 분리하고 Redis를 중간 메시지 브로커로 사용하여, ML 추론 부하가 API 응답성에 영향을 주지 않도록 설계했습니다. 모든 서비스는 Docker Compose로 오케스트레이션되며, internal 네트워크로 보안 격리하고 Traefik Reverse Proxy를 통해 HTTPS를 종단 처리합니다."
API 설계
"RESTful 원칙에 따라 /api/v1/ 기반의 URL 버저닝을 적용하고, Spring Pageable과 커서 기반의 이중 페이징 전략을 사용했습니다. Java의 Result 패턴을 도입하여 성공/실패를 값으로 반환하며, NotFound/Forbidden/Conflict/Gone/ServerError 등 의미 있는 에러 타입을 체계화했습니다. OpenAPI 3.0.3 명세를 Swagger UI와 연동하여 API 문서화를 자동화했습니다."
인프라 구성
"MySQL 8.0(utf8mb4), Redis 7.2(AOF), OpenVidu 2.32.1, Spring Boot Server, React Client 등 5개 서비스를 Docker Compose로 구성했습니다. internal bridge 네트워크로 내부 서비스를 격리하고, MySQL healthcheck에 대한 depends_on 조건을 설정하여 서비스 시작 순서를 보장했습니다. Multi-stage Dockerfile로 이미지 크기를 최적화하고, BuildKit 캐시 마운트로 빌드 시간을 단축했습니다."
문제 해결 사례
- AI 결과의 사용자 타겟팅 전달: Redis에 clientKey를 저장하고, SSE Hub에서 대상 사용자에게만 broadcastToClient() 하는 방식으로 해결했습니다.
- 탈퇴 사용자 재참여 처리: UK 제약을 유지하면서 LEFT → ACTIVE 상태 변경으로 재참여를 처리하여 데이터 일관성을 보장했습니다.
- ML 추론-API 응답 분리: Redis List/ Pub/Sub을 활용한 비동기 작업 큐 패턴으로, 장시간 ML 추론이 API 응답에 영향을 주지 않도록 설계했습니다.
- GitHub-로컬DB 이슈 동기화:
github_issue_id를 natural key로 Upsert 패턴을 구현하여 GitHub↔로컬 데이터 일관성을 유지했습니다.
프로젝트 성과
- 단일 서버에서 화상회의 + 채팅 + GitHub 이슈 관리 + AI 분석 통합 제공
- Flyway로 12회 DB 마이그레이션을 무중단 적용
- Docker Compose 기반 원커맨드 배포 체계 구축 (CI/CD + Portainer)
- Spring Boot, Python, MySQL, Redis, OpenVidu 등 5+ 기술 스택 통합 경험
회고
"이번 프로젝트를 통해 단일 백엔드 서버가 API, SSE, Webhook 수신, 외부 API 연동 등 다양한 역할을 수행할 때의 설계적 고려사항을 깊이 있게 경험했습니다. 특히 Redis를 단순 캐시가 아닌 작업 큐+Pub/Sub 메시지 브로커로 활용한 점이 인상적이었습니다. 다만, Global Exception Handler나 선언적 인가(@PreAuthorize), API 응답 시간 모니터링 등 프로덕션 레벨에서 필요한 부가적인 부분들을 더 보강하고 싶습니다."
부록: 본 문서는
apps/server/,apps/ai/,infra/,.gitlab/,docs/디렉토리의 실제 코드 및 설정 파일을 분석하여 2026-05-05 기준으로 작성되었습니다.