!theme mono skinparam sequenceArrowThickness 2 skinparam sequenceParticipantBorderThickness 2 skinparam sequenceActorBorderThickness 2 skinparam sequenceGroupBorderThickness 2 title Goal Service 내부 시퀀스 다이어그램 (역설계 - 개발 소스 기반) participant "GoalController" as GoalCtrl participant "GoalUseCase" as GoalUC participant "GoalDomainService" as GoalDomainSvc participant "MissionProgressDomainService" as ProgressDomainSvc participant "GoalRepository" as GoalRepo participant "MissionProgressRepository" as ProgressRepo participant "IntelligenceServiceAdapter" as IntelAdapter participant "CacheAdapter" as CacheAdapter participant "EventPublisherAdapter" as EventAdapter participant "PostgreSQL" as PostgreSQL participant "Redis Cache" as Redis participant "Azure Service Bus" as ServiceBus == 1. POST /api/goals/missions/select (미션 선택 및 목표 설정) == GoalCtrl -> GoalUC: selectMissions(missionSelectionRequest) note right: {userId, selectedMissionIds} GoalUC -> GoalDomainSvc: validateMissionSelection(userId, missionIds) GoalDomainSvc -> GoalDomainSvc: checkMaximumMissions(selectedMissionIds) note right: 최대 5개 미션 제한 검증 GoalUC -> GoalRepo: findActiveGoalByUserId(userId) GoalRepo -> PostgreSQL: SELECT * FROM user_mission_goals WHERE user_id = ? AND is_active = true PostgreSQL -> GoalRepo: 기존 활성 목표 조회 alt 기존 활성 목표가 있는 경우 GoalUC -> GoalDomainSvc: deactivateExistingGoal(existingGoal) GoalDomainSvc -> GoalRepo: updateGoalStatus(goalId, false) GoalRepo -> PostgreSQL: UPDATE user_mission_goals SET is_active = false end GoalUC -> GoalDomainSvc: createNewGoalWithMissions(userId, selectedMissionIds) GoalDomainSvc -> GoalDomainSvc: generateGoalId() GoalDomainSvc -> GoalDomainSvc: createUserMissionEntities(goalId, missionIds) GoalDomainSvc -> GoalRepo: saveGoalWithMissions(goalEntity, missionEntities) GoalRepo -> PostgreSQL: BEGIN TRANSACTION GoalRepo -> PostgreSQL: INSERT INTO user_mission_goals GoalRepo -> PostgreSQL: INSERT INTO user_missions (batch) GoalRepo -> PostgreSQL: COMMIT GoalUC -> CacheAdapter: invalidateActiveMissionsCache(userId) CacheAdapter -> Redis: DEL goals:active:{userId} GoalUC -> EventAdapter: publishGoalSetEvent(userId, goalId, selectedMissions) EventAdapter -> ServiceBus: 목표 설정 이벤트 발행 GoalUC -> GoalCtrl: GoalSetupResponse 반환 note right: {goalId, selectedMissions, message, setupCompletedAt} == 2. GET /api/goals/missions/active (활성 미션 조회) == GoalCtrl -> GoalUC: getActiveMissions(userId) GoalUC -> CacheAdapter: getCachedActiveMissions(userId) CacheAdapter -> Redis: GET goals:active:{userId} Redis -> CacheAdapter: 캐시된 활성 미션 또는 null alt 캐시 미스인 경우 GoalUC -> GoalRepo: findActiveMissionsByUserId(userId) GoalRepo -> PostgreSQL: SELECT um.*, mp.* FROM user_missions um LEFT JOIN mission_progress mp ON um.mission_id = mp.mission_id WHERE um.user_id = ? AND um.is_active = true PostgreSQL -> GoalRepo: 활성 미션 및 진행 상황 GoalUC -> ProgressDomainSvc: calculateMissionProgress(activeMissions) ProgressDomainSvc -> ProgressDomainSvc: calculateCompletionRate(missions) ProgressDomainSvc -> ProgressDomainSvc: calculateStreakDays(missions) ProgressDomainSvc -> ProgressDomainSvc: determineTodayStatus(missions) GoalUC -> CacheAdapter: cacheActiveMissions(userId, processedMissions) CacheAdapter -> Redis: SETEX goals:active:{userId} 1800 {data} note right: 30분 TTL로 캐싱 end GoalUC -> GoalCtrl: ActiveMissionsResponse 반환 note right: {dailyMissions, totalMissions, todayCompletedCount, completionRate} == 3. PUT /api/goals/missions/{missionId}/complete (미션 완료 처리) == GoalCtrl -> GoalUC: completeMission(missionId, missionCompleteRequest) note right: {userId, completed, completedAt, notes} GoalUC -> GoalDomainSvc: validateMissionCompletion(userId, missionId) GoalDomainSvc -> GoalRepo: findUserMission(userId, missionId) GoalRepo -> PostgreSQL: SELECT * FROM user_missions WHERE user_id = ? AND mission_id = ? AND is_active = true PostgreSQL -> GoalRepo: 사용자 미션 정보 GoalUC -> ProgressRepo: findTodayProgress(userId, missionId) ProgressRepo -> PostgreSQL: SELECT * FROM mission_progress WHERE user_id = ? AND mission_id = ? AND DATE(completed_at) = CURRENT_DATE PostgreSQL -> ProgressRepo: 오늘의 진행 상황 alt 오늘 이미 완료한 경우 GoalUC -> GoalCtrl: 409 Conflict 응답 note right: "오늘 이미 완료된 미션입니다" else 새로운 완료 기록 GoalUC -> ProgressDomainSvc: recordMissionCompletion(userId, missionId, completionData) ProgressDomainSvc -> ProgressDomainSvc: calculateNewStreakDays(previousProgress) ProgressDomainSvc -> ProgressDomainSvc: calculateEarnedPoints(mission, streakDays) ProgressDomainSvc -> ProgressRepo: saveMissionProgress(progressEntity) ProgressRepo -> PostgreSQL: INSERT INTO mission_progress PostgreSQL -> ProgressRepo: 진행 기록 저장 완료 GoalUC -> ProgressDomainSvc: updateMissionStatistics(userId, missionId) ProgressDomainSvc -> GoalRepo: updateMissionStats(missionId, newStats) GoalRepo -> PostgreSQL: UPDATE user_missions SET total_completed_count = ?, current_streak_days = ? end GoalUC -> CacheAdapter: invalidateUserCaches(userId) CacheAdapter -> Redis: DEL goals:active:{userId} goals:history:{userId} GoalUC -> EventAdapter: publishMissionCompletedEvent(userId, missionId, achievementData) EventAdapter -> ServiceBus: 미션 완료 이벤트 발행 GoalUC -> GoalCtrl: MissionCompleteResponse 반환 note right: {message, status, achievementMessage, newStreakDays, totalCompletedCount, earnedPoints} == 4. GET /api/goals/missions/history (미션 달성 이력) == GoalCtrl -> GoalUC: getMissionHistory(userId, startDate, endDate, missionIds) GoalUC -> CacheAdapter: getCachedMissionHistory(userId, period) CacheAdapter -> Redis: GET goals:history:{userId}:{period} Redis -> CacheAdapter: 캐시된 이력 또는 null alt 캐시 미스인 경우 GoalUC -> ProgressRepo: findMissionHistoryByPeriod(userId, startDate, endDate, missionIds) ProgressRepo -> PostgreSQL: SELECT mp.*, um.mission_title FROM mission_progress mp JOIN user_missions um ON mp.mission_id = um.mission_id WHERE mp.user_id = ? AND mp.completed_at BETWEEN ? AND ? PostgreSQL -> ProgressRepo: 기간별 미션 이력 데이터 GoalUC -> ProgressDomainSvc: analyzeAchievementStatistics(historyData) ProgressDomainSvc -> ProgressDomainSvc: calculateAchievementRates(histories) ProgressDomainSvc -> ProgressDomainSvc: findBestStreak(histories) ProgressDomainSvc -> ProgressDomainSvc: generateInsights(statistics) ProgressDomainSvc -> ProgressDomainSvc: prepareChartData(histories) GoalUC -> CacheAdapter: cacheMissionHistory(userId, period, analysisResult) CacheAdapter -> Redis: SETEX goals:history:{userId}:{period} 3600 {data} note right: 1시간 TTL로 캐싱 end GoalUC -> GoalCtrl: MissionHistoryResponse 반환 note right: {totalAchievementRate, periodAchievementRate, bestStreak, missionStats, chartData, period, insights} == 5. POST /api/goals/missions/reset (목표 재설정) == GoalCtrl -> GoalUC: resetMissions(missionResetRequest) note right: {userId, reason, currentMissionIds} GoalUC -> IntelAdapter: requestNewMissionRecommendations(userId, reason) IntelAdapter -> GoalUC: 새로운 추천 미션 목록 GoalUC -> GoalDomainSvc: deactivateCurrentGoal(userId) GoalDomainSvc -> GoalRepo: updateGoalStatus(userId, false) GoalRepo -> PostgreSQL: UPDATE user_mission_goals SET is_active = false GoalUC -> CacheAdapter: invalidateAllUserCaches(userId) CacheAdapter -> Redis: DEL goals:active:{userId} goals:history:{userId}* GoalUC -> EventAdapter: publishGoalResetEvent(userId, reason, newRecommendations) EventAdapter -> ServiceBus: 목표 재설정 이벤트 발행 GoalUC -> GoalCtrl: MissionResetResponse 반환 note right: {message, newRecommendations, resetCompletedAt} == 예외 처리 == alt 미션 개수 초과 GoalDomainSvc -> GoalUC: MissionLimitExceededException GoalUC -> GoalCtrl: 400 Bad Request note right: "미션은 최대 5개까지 선택 가능합니다" end alt 미션 권한 없음 GoalDomainSvc -> GoalUC: UnauthorizedMissionException GoalUC -> GoalCtrl: 403 Forbidden end alt 이미 완료된 미션 ProgressDomainSvc -> GoalUC: MissionAlreadyCompletedException GoalUC -> GoalCtrl: 409 Conflict end == 트랜잭션 및 캐싱 == note over GoalUC, CacheAdapter **트랜잭션 관리** - @Transactional 어노테이션 적용 - 미션 선택/완료 시 원자성 보장 - 예외 발생 시 자동 롤백 **캐싱 전략** - 활성 미션: 30분 TTL - 이력 데이터: 1시간 TTL - 완료/재설정 시 관련 캐시 무효화 - 사용자별 캐시 키 분리 end note