This commit is contained in:
hiondal
2025-09-09 01:12:14 +09:00
parent 7ec8a682c6
commit b489c73201
276 changed files with 43859 additions and 98 deletions
@@ -0,0 +1,133 @@
@startuml
!theme mono
title Auth Service - 권한 확인 내부 시퀀스
participant "API Gateway" as Gateway
participant "AuthController" as Controller
participant "AuthService" as Service
participant "PermissionService" as PermService
participant "Redis Cache<<E>>" as Redis
participant "UserRepository" as UserRepo
participant "Auth DB<<E>>" as AuthDB
== UFR-AUTH-020: 서비스별 접근 권한 확인 ==
Gateway -> Controller: GET /check-permission/{serviceType}\nAuthorization: Bearer {accessToken}\nPath: serviceType = "BILL_INQUIRY" | "PRODUCT_CHANGE"
activate Controller
Controller -> Controller: JWT 토큰에서 userId 추출\n(이미 Gateway에서 1차 검증 완료)
Controller -> Service: checkServicePermission(userId, serviceType)
activate Service
== Cache-First 패턴으로 권한 정보 조회 ==
Service -> Redis: getUserPermissions(userId)\nKey: user_permissions:{userId}
activate Redis
alt 권한 캐시 Hit
Redis --> Service: 권한 정보 반환\n{permissions: [BILL_INQUIRY, PRODUCT_CHANGE, ...]}
deactivate Redis
note right: 권한 캐시 히트\n- TTL: 4시간\n- 빠른 응답 < 10ms
else 권한 캐시 Miss
Redis --> Service: null (권한 캐시 없음)
deactivate Redis
Service -> UserRepo: getUserPermissions(userId)
activate UserRepo
UserRepo -> AuthDB: SELECT p.permission_code\nFROM user_permissions up\nJOIN permissions p ON up.permission_id = p.id\nWHERE up.user_id = ? AND up.status = 'ACTIVE'
activate AuthDB
AuthDB --> UserRepo: 권한 목록 반환
deactivate AuthDB
UserRepo --> Service: List<Permission>
deactivate UserRepo
Service -> Redis: cacheUserPermissions\nKey: user_permissions:{userId}\nValue: {permissions}\nTTL: 4시간
activate Redis
Redis --> Service: 권한 캐싱 완료
deactivate Redis
end
Service -> PermService: validateServiceAccess(permissions, serviceType)
activate PermService
PermService -> PermService: 서비스별 권한 매핑 확인
note right: 권한 매핑 규칙\n- BILL_INQUIRY: 요금조회 권한\n- PRODUCT_CHANGE: 상품변경 권한\n- 관리자는 모든 권한 보유
alt 요금조회 서비스 (BILL_INQUIRY)
PermService -> PermService: 권한 목록에서\n"BILL_INQUIRY" 또는 "ADMIN" 권한 확인
alt 권한 있음
PermService --> Service: PermissionResult{granted: true, serviceType: "BILL_INQUIRY"}
else 권한 없음
PermService --> Service: PermissionResult{granted: false, reason: "요금조회 권한이 없습니다"}
end
else 상품변경 서비스 (PRODUCT_CHANGE)
PermService -> PermService: 권한 목록에서\n"PRODUCT_CHANGE" 또는 "ADMIN" 권한 확인
alt 권한 있음
PermService --> Service: PermissionResult{granted: true, serviceType: "PRODUCT_CHANGE"}
else 권한 없음
PermService --> Service: PermissionResult{granted: false, reason: "상품변경 권한이 없습니다"}
end
else 잘못된 서비스 타입
PermService --> Service: PermissionResult{granted: false, reason: "올바르지 않은 서비스 타입입니다"}
end
deactivate PermService
== 권한 확인 결과 처리 ==
alt 접근 권한 있음
Service -> Service: 접근 로그 기록 (비동기)
note right: 접근 로그\n- userId, serviceType\n- 접근 시간, IP 주소
Service --> Controller: PermissionGranted{permission: "granted"}
deactivate Service
Controller --> Gateway: 200 OK\n{permission: "granted", serviceType: serviceType}
deactivate Controller
else 접근 권한 없음
Service -> Service: 권한 거부 로그 기록 (비동기)
note right: 권한 거부 로그\n- userId, serviceType\n- 거부 사유, 시간
Service --> Controller: PermissionDenied{reason: "서비스 이용 권한이 없습니다"}
deactivate Service
Controller --> Gateway: 403 Forbidden\n{permission: "denied", reason: "서비스 이용 권한이 없습니다"}
deactivate Controller
end
== 권한 캐시 무효화 처리 ==
note over Service, Redis
권한 변경 시 캐시 무효화
- 사용자 권한 변경
- 권한 정책 변경
- 관리자에 의한 권한 갱신
end note
Controller -> Service: invalidateUserPermissions(userId)
activate Service
Service -> Redis: deleteUserPermissions\nKey: user_permissions:{userId}
activate Redis
Redis --> Service: 캐시 삭제 완료
deactivate Redis
Service -> Redis: deleteUserSession\nKey: user_session:{userId}
activate Redis
Redis --> Service: 세션 삭제 완료
deactivate Redis
note right: 권한 변경 시\n세션도 함께 무효화
Service --> Controller: 권한 캐시 무효화 완료
deactivate Service
@enduml
@@ -0,0 +1,107 @@
@startuml
!theme mono
title Auth Service - 사용자 로그인 내부 시퀀스
participant "API Gateway" as Gateway
participant "AuthController" as Controller
participant "AuthService" as Service
participant "UserRepository" as UserRepo
participant "TokenService" as TokenService
participant "Redis Cache<<E>>" as Redis
participant "Auth DB<<E>>" as AuthDB
== UFR-AUTH-010: 사용자 로그인 처리 ==
Gateway -> Controller: POST /login\n{userId, password, autoLogin}
activate Controller
Controller -> Controller: 입력값 유효성 검사\n(userId, password 필수값 확인)
note right: 입력값 검증\n- userId: not null, not empty\n- password: not null, 최소 8자
alt 입력값 오류
Controller --> Gateway: 400 Bad Request\n"입력값을 확인해주세요"
else 입력값 정상
Controller -> Service: authenticateUser(userId, password)
activate Service
Service -> Service: 로그인 시도 횟수 체크
Service -> UserRepo: findUserById(userId)
activate UserRepo
UserRepo -> AuthDB: SELECT user_id, password_hash, salt,\nlocked_until, login_attempt_count\nWHERE user_id = ?
activate AuthDB
AuthDB --> UserRepo: 사용자 정보 반환
deactivate AuthDB
UserRepo --> Service: User Entity 반환
deactivate UserRepo
alt 사용자 없음
Service --> Controller: UserNotFoundException
Controller --> Gateway: 401 Unauthorized\n"ID 또는 비밀번호를 확인해주세요"
else 계정 잠김 (5회 연속 실패)
Service -> Service: 잠금 시간 확인\n(현재시간 < locked_until)
Service --> Controller: AccountLockedException
Controller --> Gateway: 401 Unauthorized\n"30분간 계정이 잠금되었습니다"
else 정상 계정
Service -> Service: 비밀번호 검증\nbcrypt.checkpw(password, storedHash)
alt 비밀번호 불일치
Service -> UserRepo: incrementLoginAttempt(userId)
activate UserRepo
UserRepo -> AuthDB: UPDATE users\nSET login_attempt_count = login_attempt_count + 1\nWHERE user_id = ?
AuthDB --> UserRepo: 업데이트 완료
deactivate UserRepo
alt 5회째 실패
Service -> UserRepo: lockAccount(userId, 30분)
activate UserRepo
UserRepo -> AuthDB: UPDATE users\nSET locked_until = NOW() + INTERVAL 30 MINUTE\nWHERE user_id = ?
deactivate UserRepo
Service --> Controller: AccountLockedException
Controller --> Gateway: 401 Unauthorized\n"5회 연속 실패하여 30분간 잠금"
else 1~4회 실패
Service --> Controller: AuthenticationException
Controller --> Gateway: 401 Unauthorized\n"ID 또는 비밀번호를 확인해주세요"
end
else 비밀번호 일치 (로그인 성공)
Service -> UserRepo: resetLoginAttempt(userId)
activate UserRepo
UserRepo -> AuthDB: UPDATE users\nSET login_attempt_count = 0\nWHERE user_id = ?
deactivate UserRepo
== 토큰 생성 및 세션 처리 ==
Service -> TokenService: generateAccessToken(userInfo)
activate TokenService
TokenService -> TokenService: JWT 생성\n(payload: {userId, permissions}\nexpiry: 30분)
TokenService --> Service: accessToken
deactivate TokenService
Service -> TokenService: generateRefreshToken(userId)
activate TokenService
TokenService -> TokenService: JWT 생성\n(payload: {userId}\nexpiry: 24시간 또는 autoLogin 기준)
TokenService --> Service: refreshToken
deactivate TokenService
Service -> Redis: setUserSession\nKey: user_session:{userId}\nValue: {userInfo, permissions}\nTTL: autoLogin ? 24시간 : 30분
activate Redis
Redis --> Service: 세션 저장 완료
deactivate Redis
Service -> UserRepo: saveLoginHistory(userId, ipAddress, loginTime)
activate UserRepo
UserRepo -> AuthDB: INSERT INTO login_history\n(user_id, login_time, ip_address)
note right: 비동기 처리로\n응답 성능에 영향 없음
deactivate UserRepo
Service --> Controller: AuthenticationResult\n{accessToken, refreshToken, userInfo}
deactivate Service
Controller --> Gateway: 200 OK\n{accessToken, refreshToken, userInfo}
deactivate Controller
end
end
end
@enduml
@@ -0,0 +1,147 @@
@startuml
!theme mono
title Auth Service - 토큰 검증 내부 시퀀스
participant "API Gateway" as Gateway
participant "AuthController" as Controller
participant "AuthService" as Service
participant "TokenService" as TokenService
participant "Redis Cache<<E>>" as Redis
participant "UserRepository" as UserRepo
participant "Auth DB<<E>>" as AuthDB
== UFR-AUTH-020: 사용자 정보 조회 및 토큰 검증 ==
Gateway -> Controller: GET /user-info\nAuthorization: Bearer {accessToken}
activate Controller
Controller -> TokenService: validateAccessToken(accessToken)
activate TokenService
TokenService -> TokenService: JWT 토큰 파싱 및 검증\n- 서명 검증\n- 만료 시간 확인\n- 토큰 구조 검증
alt 토큰 무효 (만료/변조/형식오류)
TokenService --> Controller: InvalidTokenException
Controller --> Gateway: 401 Unauthorized\n"토큰이 유효하지 않습니다"
else 토큰 유효
TokenService -> TokenService: 토큰에서 userId 추출
TokenService --> Controller: DecodedToken{userId, permissions, exp}
deactivate TokenService
Controller -> Service: getUserInfo(userId)
activate Service
== Cache-Aside 패턴으로 사용자 정보 조회 ==
Service -> Redis: getUserSession(userId)\nKey: user_session:{userId}
activate Redis
alt 캐시 Hit
Redis --> Service: 사용자 세션 데이터 반환\n{userInfo, permissions, lastAccess}
deactivate Redis
note right: 캐시 히트\n응답 시간 < 50ms
Service -> Service: 세션 유효성 확인\n(lastAccess 시간 체크)
else 캐시 Miss (세션 만료 또는 없음)
Redis --> Service: null (캐시 데이터 없음)
deactivate Redis
Service -> UserRepo: findUserById(userId)
activate UserRepo
UserRepo -> AuthDB: SELECT user_id, name, permissions, status\nWHERE user_id = ? AND status = 'ACTIVE'
activate AuthDB
AuthDB --> UserRepo: 사용자 정보 반환
deactivate AuthDB
alt 사용자 없음 또는 비활성
UserRepo --> Service: null
deactivate UserRepo
Service --> Controller: UserNotFoundException
Controller --> Gateway: 401 Unauthorized\n"사용자 정보를 찾을 수 없습니다"
else 사용자 정보 존재
UserRepo --> Service: User Entity
deactivate UserRepo
Service -> Service: UserInfo 및 Permission 매핑
Service -> Redis: setUserSession\nKey: user_session:{userId}\nValue: {userInfo, permissions, lastAccess}\nTTL: 30분
activate Redis
Redis --> Service: 세션 재생성 완료
deactivate Redis
end
end
alt 세션 정보 획득 성공
Service -> Service: lastAccess 시간 업데이트
Service -> Redis: updateLastAccess\nKey: user_session:{userId}
activate Redis
Redis --> Service: 업데이트 완료
deactivate Redis
Service --> Controller: UserInfoResponse\n{userInfo, permissions}
deactivate Service
Controller --> Gateway: 200 OK\n{userInfo, permissions}
deactivate Controller
else 세션 정보 획득 실패
Service --> Controller: SessionNotFoundException
Controller --> Gateway: 401 Unauthorized\n"세션이 만료되었습니다"
end
end
== 토큰 갱신 처리 ==
note over Gateway, AuthDB
토큰 갱신 요청 시 별도 엔드포인트 처리
POST /auth/refresh
end note
Gateway -> Controller: POST /refresh\n{refreshToken}
activate Controller
Controller -> TokenService: validateRefreshToken(refreshToken)
activate TokenService
TokenService -> TokenService: Refresh Token 검증\n- JWT 서명 확인\n- 만료 시간 확인\n- 토큰 타입 확인
alt Refresh Token 무효
TokenService --> Controller: InvalidTokenException
Controller --> Gateway: 401 Unauthorized\n"토큰 갱신이 필요합니다"
else Refresh Token 유효
TokenService -> TokenService: userId 추출
TokenService --> Controller: userId
deactivate TokenService
Controller -> Service: refreshUserToken(userId)
activate Service
Service -> Redis: getUserSession(userId)
activate Redis
Redis --> Service: 세션 데이터 확인
deactivate Redis
alt 세션 유효
Service -> TokenService: generateAccessToken(userInfo)
activate TokenService
TokenService --> Service: 새로운 AccessToken (30분)
deactivate TokenService
Service -> Redis: updateUserSession\n새로운 토큰 정보로 세션 업데이트
activate Redis
Redis --> Service: 세션 업데이트 완료
deactivate Redis
Service --> Controller: TokenRefreshResponse\n{newAccessToken}
deactivate Service
Controller --> Gateway: 200 OK\n{accessToken}
deactivate Controller
else 세션 무효
Service --> Controller: SessionExpiredException
Controller --> Gateway: 401 Unauthorized\n"재로그인이 필요합니다"
end
end
@enduml
@@ -0,0 +1,150 @@
@startuml
!theme mono
title Bill-Inquiry Service - KOS 연동 내부 시퀀스
participant "BillInquiryService" as Service
participant "KosClientService" as KosClient
participant "CircuitBreakerService" as CircuitBreaker
participant "RetryService" as RetryService
participant "KosAdapterService" as KosAdapter
participant "BillRepository" as BillRepo
participant "Bill DB<<E>>" as BillDB
participant "KOS-Mock Service<<E>>" as KOSMock
== UFR-BILL-030: KOS 요금조회 서비스 연동 ==
Service -> KosClient: getBillInfo(lineNumber, inquiryMonth)
activate KosClient
KosClient -> CircuitBreaker: isCallAllowed()
activate CircuitBreaker
alt Circuit Breaker - OPEN 상태 (장애 감지)
CircuitBreaker --> KosClient: Circuit Open\n"서비스 일시 장애"
deactivate CircuitBreaker
KosClient -> KosClient: Fallback 처리\n- 최근 캐시 데이터 확인\n- 기본 응답 준비
KosClient --> Service: FallbackException\n"일시적으로 서비스 이용이 어렵습니다"
note right: Circuit Breaker Open\n- 빠른 실패로 시스템 보호\n- 장애 전파 방지
else Circuit Breaker - CLOSED/HALF_OPEN 상태
CircuitBreaker --> KosClient: Call Allowed
deactivate CircuitBreaker
KosClient -> RetryService: executeWithRetry(kosCall)
activate RetryService
== Retry 패턴 적용 ==
loop 최대 3회 재시도
RetryService -> KosAdapter: callKosBillInquiry(lineNumber, inquiryMonth)
activate KosAdapter
KosAdapter -> KosAdapter: 요청 데이터 변환\n- lineNumber 포맷 검증\n- inquiryMonth 형식 변환\n- 인증 헤더 설정
== KOS-Mock Service 호출 ==
KosAdapter -> KOSMock: POST /kos/bill/inquiry\nContent-Type: application/json\n{\n "lineNumber": "01012345678",\n "inquiryMonth": "202412"\n}
activate KOSMock
note right: KOS-Mock 서비스\n- 실제 KOS 시스템 대신 Mock 응답\n- 타임아웃: 3초\n- 다양한 시나리오 시뮬레이션
alt KOS-Mock 정상 응답
KOSMock --> KosAdapter: 200 OK\n{\n "resultCode": "0000",\n "resultMessage": "성공",\n "data": {\n "productName": "5G 프리미엄",\n "contractInfo": "24개월 약정",\n "billingMonth": "202412",\n "charge": 75000,\n "discountInfo": "가족할인 10000원",\n "usage": {\n "voice": "250분",\n "data": "20GB"\n },\n "estimatedCancellationFee": 120000,\n "deviceInstallment": 35000,\n "billingPaymentInfo": {\n "billingDate": "2024-12-25",\n "paymentStatus": "완료"\n }\n }\n}
deactivate KOSMock
KosAdapter -> KosAdapter: 응답 데이터 변환\n- KOS 응답 → 내부 BillInfo 모델\n- 데이터 유효성 검증\n- Null 안전 처리
KosAdapter --> RetryService: BillInfo 객체
deactivate KosAdapter
break 성공 시 재시도 중단
else KOS-Mock 오류 응답 (4xx, 5xx)
KOSMock --> KosAdapter: 오류 응답\n{\n "resultCode": "E001",\n "resultMessage": "회선번호가 존재하지 않습니다"\n}
deactivate KOSMock
KosAdapter -> KosAdapter: 오류 코드별 예외 매핑\n- E001: InvalidLineNumberException\n- E002: DataNotFoundException\n- E999: SystemErrorException
KosAdapter --> RetryService: KosServiceException
deactivate KosAdapter
else 네트워크 오류 (타임아웃, 연결 실패)
KOSMock --> KosAdapter: IOException/TimeoutException
deactivate KOSMock
KosAdapter --> RetryService: NetworkException
deactivate KosAdapter
end
alt 재시도 가능한 오류 (네트워크, 일시적 오류)
RetryService -> RetryService: 재시도 대기\n- 1차: 1초 대기\n- 2차: 2초 대기\n- 3차: 3초 대기
note right: Exponential Backoff\n재시도 간격 증가
else 재시도 불가능한 오류 (비즈니스 로직 오류)
break 재시도 중단
end
end
alt 재시도 성공
RetryService --> KosClient: BillInfo
deactivate RetryService
KosClient -> CircuitBreaker: recordSuccess()
activate CircuitBreaker
CircuitBreaker -> CircuitBreaker: 성공 카운트 증가\nCircuit 상태 유지 또는 CLOSED로 변경
deactivate CircuitBreaker
== 연동 이력 저장 ==
KosClient -> BillRepo: saveKosInquiryHistory(lineNumber, inquiryMonth, "SUCCESS")
activate BillRepo
BillRepo -> BillDB: INSERT INTO kos_inquiry_history\n(line_number, inquiry_month, request_time, \n response_time, result_code, result_message)
activate BillDB
note right: 비동기 처리\n- 성능 최적화\n- 연동 추적
BillDB --> BillRepo: 이력 저장 완료
deactivate BillDB
deactivate BillRepo
KosClient --> Service: BillInfo 반환
deactivate KosClient
else 모든 재시도 실패
RetryService --> KosClient: MaxRetryExceededException
deactivate RetryService
KosClient -> CircuitBreaker: recordFailure()
activate CircuitBreaker
CircuitBreaker -> CircuitBreaker: 실패 카운트 증가\n임계값 초과 시 Circuit OPEN
deactivate CircuitBreaker
KosClient -> BillRepo: saveKosInquiryHistory(lineNumber, inquiryMonth, "FAILURE")
activate BillRepo
BillRepo -> BillDB: INSERT INTO kos_inquiry_history\n(line_number, inquiry_month, request_time, \n response_time, result_code, result_message, error_detail)
deactivate BillRepo
KosClient --> Service: KosConnectionException\n"KOS 시스템 연동 실패"
deactivate KosClient
end
end
== Circuit Breaker 상태 관리 ==
note over CircuitBreaker
Circuit Breaker 설정:
- 실패 임계값: 5회 연속 실패
- 타임아웃: 3초
- 반열림 대기시간: 30초
- 성공 임계값: 3회 연속 성공 시 복구
end note
== KOS-Mock 서비스 시나리오 ==
note over KOSMock
Mock 응답 시나리오:
1. 정상 케이스: 완전한 요금 정보 반환
2. 데이터 없음: 해당월 데이터 없음 (E002)
3. 잘못된 회선: 존재하지 않는 회선번호 (E001)
4. 시스템 오류: 일시적 장애 시뮬레이션 (E999)
5. 타임아웃: 응답 지연 시뮬레이션
end note
@enduml
@@ -0,0 +1,166 @@
@startuml
!theme mono
title Bill-Inquiry Service - 요금조회 요청 내부 시퀀스
participant "API Gateway" as Gateway
participant "BillController" as Controller
participant "BillInquiryService" as Service
participant "BillCacheService" as CacheService
participant "BillRepository" as BillRepo
participant "KosClientService" as KosClient
participant "Redis Cache<<E>>" as Redis
participant "Bill DB<<E>>" as BillDB
participant "MVNO AP Server<<E>>" as MVNO
== UFR-BILL-010: 요금조회 메뉴 접근 ==
Gateway -> Controller: GET /api/bill/menu\nAuthorization: Bearer {accessToken}
activate Controller
Controller -> Controller: 토큰에서 userId, 회선번호 추출
Controller -> Service: getBillMenuData(userId)
activate Service
Service -> CacheService: getCustomerInfo(userId)
activate CacheService
CacheService -> Redis: GET customer_info:{userId}
activate Redis
alt 고객 정보 캐시 Hit
Redis --> CacheService: 고객 정보 반환\n{lineNumber, customerName, serviceStatus}
deactivate Redis
note right: 캐시 히트\n- TTL: 4시간\n- 빠른 응답
else 고객 정보 캐시 Miss
Redis --> CacheService: null
deactivate Redis
CacheService -> BillRepo: getCustomerInfo(userId)
activate BillRepo
BillRepo -> BillDB: SELECT line_number, customer_name, service_status\nFROM customer_info\nWHERE user_id = ?
activate BillDB
BillDB --> BillRepo: 고객 정보
deactivate BillDB
BillRepo --> CacheService: CustomerInfo
deactivate BillRepo
CacheService -> Redis: SET customer_info:{userId}\nValue: customerInfo\nTTL: 4시간
activate Redis
Redis --> CacheService: 캐싱 완료
deactivate Redis
end
CacheService --> Service: CustomerInfo{lineNumber, customerName}
deactivate CacheService
Service -> Service: 요금조회 메뉴 데이터 구성\n- 회선번호 표시\n- 조회월 선택 옵션 (최근 12개월)\n- 기본값: 당월
Service --> Controller: BillMenuResponse\n{lineNumber, availableMonths, currentMonth}
deactivate Service
Controller --> Gateway: 200 OK\n요금조회 메뉴 데이터
deactivate Controller
== UFR-BILL-020: 요금조회 신청 처리 ==
Gateway -> Controller: POST /api/bill/inquiry\n{lineNumber, inquiryMonth?}\nAuthorization: Bearer {accessToken}
activate Controller
Controller -> Controller: 입력값 검증\n- lineNumber: 필수, 11자리 숫자\n- inquiryMonth: 선택, YYYYMM 형식
alt 입력값 오류
Controller --> Gateway: 400 Bad Request\n"입력값을 확인해주세요"
else 입력값 정상
Controller -> Service: inquireBill(lineNumber, inquiryMonth, userId)
activate Service
Service -> Service: 조회월 처리\ninquiryMonth가 null이면 현재월로 설정
== Cache-Aside 패턴으로 요금 정보 조회 ==
Service -> CacheService: getCachedBillInfo(lineNumber, inquiryMonth)
activate CacheService
CacheService -> Redis: GET bill_info:{lineNumber}:{inquiryMonth}
activate Redis
alt 요금 정보 캐시 Hit (1시간 TTL 내)
Redis --> CacheService: 캐시된 요금 정보\n{productName, billingMonth, charge, discount, usage...}
deactivate Redis
CacheService --> Service: BillInfo (캐시된 데이터)
deactivate CacheService
note right: 캐시 히트\n- KOS 호출 없이 즉시 응답\n- 응답 시간 < 100ms
Service -> Service: 캐시 데이터 유효성 확인\n(생성 시간, 데이터 완전성 체크)
else 요금 정보 캐시 Miss
Redis --> CacheService: null
deactivate Redis
CacheService --> Service: null (캐시 데이터 없음)
deactivate CacheService
== KOS 연동을 통한 요금 정보 조회 ==
Service -> KosClient: getBillInfo(lineNumber, inquiryMonth)
activate KosClient
note right: 다음 단계에서 상세 처리\n(bill-KOS연동.puml 참조)
KosClient --> Service: BillInfo 또는 Exception
deactivate KosClient
alt KOS 연동 성공
Service -> CacheService: cacheBillInfo(lineNumber, inquiryMonth, billInfo)
activate CacheService
CacheService -> Redis: SET bill_info:{lineNumber}:{inquiryMonth}\nValue: billInfo\nTTL: 1시간
activate Redis
Redis --> CacheService: 캐싱 완료
deactivate Redis
deactivate CacheService
else KOS 연동 실패
Service -> Service: 오류 로그 기록
Service --> Controller: BillInquiryException\n"요금 조회에 실패하였습니다"
Controller --> Gateway: 500 Internal Server Error
Gateway --> "Client": 오류 메시지 표시
end
end
alt 요금 정보 획득 성공
== 요금조회 결과 전송 (UFR-BILL-040) ==
Service -> MVNO: sendBillResult(billInfo)
activate MVNO
MVNO --> Service: 전송 완료 확인
deactivate MVNO
Service -> Service: 요금조회 이력 저장 준비\n{userId, lineNumber, inquiryMonth, resultStatus}
Service -> BillRepo: saveBillInquiryHistory(historyData)
activate BillRepo
note right: 비동기 처리\n응답 성능에 영향 없음
BillRepo -> BillDB: INSERT INTO bill_inquiry_history\n(user_id, line_number, inquiry_month, \n inquiry_time, result_status)
activate BillDB
BillDB --> BillRepo: 이력 저장 완료
deactivate BillDB
deactivate BillRepo
Service --> Controller: BillInquiryResult\n{productName, billingMonth, charge, discount, usage, \n estimatedCancellationFee, deviceInstallment, billingInfo}
deactivate Service
Controller --> Gateway: 200 OK\n요금조회 결과 데이터
deactivate Controller
end
end
== 오류 처리 및 로깅 ==
note over Controller, BillDB
각 단계별 오류 처리:
1. 입력값 검증 오류 → 400 Bad Request
2. 권한 없음 → 403 Forbidden
3. KOS 연동 오류 → Circuit Breaker 적용
4. 캐시 장애 → KOS 직접 호출로 우회
5. DB 오류 → 트랜잭션 롤백 후 재시도
end note
@enduml
@@ -0,0 +1,170 @@
@startuml
!theme mono
title KOS-Mock Service - 상품변경 내부 시퀀스
participant "Product-Change Service<<E>>" as ProductService
participant "KosMockController" as Controller
participant "KosMockService" as Service
participant "ProductDataService" as ProductDataService
participant "ProductValidationService" as ValidationService
participant "MockScenarioService" as ScenarioService
participant "MockDataRepository" as MockRepo
participant "Mock Data Store<<E>>" as MockDB
== KOS-Mock 상품변경 시뮬레이션 ==
ProductService -> Controller: POST /kos/product/change\nContent-Type: application/json\n{\n "transactionId": "TXN20241201001",\n "lineNumber": "01012345678",\n "currentProductCode": "PROD001",\n "newProductCode": "PROD002",\n "changeReason": "고객 요청",\n "effectiveDate": "20241201"\n}
activate Controller
Controller -> Controller: 요청 데이터 유효성 검사\n- transactionId: 필수, 중복 체크\n- lineNumber: 11자리 숫자 형식\n- productCode: 상품코드 형식\n- effectiveDate: YYYYMMDD 형식
alt 입력값 오류
Controller --> ProductService: 400 Bad Request\n{\n "resultCode": "E400",\n "resultMessage": "요청 데이터가 올바르지 않습니다"\n}
else 입력값 정상
Controller -> Service: processProductChange(changeRequest)
activate Service
== Mock 시나리오 결정 ==
Service -> ScenarioService: determineProductChangeScenario(lineNumber, changeRequest)
activate ScenarioService
ScenarioService -> ScenarioService: 회선번호 및 상품코드 기반 시나리오 결정
note right: Mock 상품변경 시나리오\n- 01012345678: 정상 변경\n- 01012345679: 변경 불가\n- 01012345680: 시스템 오류\n- 01012345681: 잔액 부족\n- PROD001→PROD999: 호환 불가\n- 기타: 정상 처리
alt 정상 변경 케이스
ScenarioService -> ScenarioService: 상품 호환성 확인
alt 호환 가능한 상품 변경
ScenarioService --> Service: MockScenario{type: "SUCCESS", delay: 2000ms}
else 호환 불가능한 상품 변경 (PROD001→PROD999)
ScenarioService --> Service: MockScenario{type: "INCOMPATIBLE", delay: 1000ms}
end
else 변경 불가 케이스 (01012345679)
ScenarioService --> Service: MockScenario{type: "NOT_ALLOWED", delay: 1500ms}
else 잔액 부족 케이스 (01012345681)
ScenarioService --> Service: MockScenario{type: "INSUFFICIENT_BALANCE", delay: 1200ms}
else 시스템 오류 케이스 (01012345680)
ScenarioService --> Service: MockScenario{type: "SYSTEM_ERROR", delay: 3000ms}
end
deactivate ScenarioService
Service -> Service: 시나리오별 처리 지연\n(실제 KOS 상품변경 처리 시간 모사)
note right: 상품변경은 복잡한 처리\n실제보다 긴 응답 시간
alt SUCCESS 시나리오
Service -> ValidationService: validateProductChange(changeRequest)
activate ValidationService
ValidationService -> MockRepo: getProductInfo(newProductCode)
activate MockRepo
MockRepo -> MockDB: SELECT product_name, price, features\nFROM mock_products\nWHERE product_code = ?
activate MockDB
MockDB --> MockRepo: 상품 정보
deactivate MockDB
MockRepo --> ValidationService: ProductInfo
deactivate MockRepo
ValidationService -> ValidationService: 상품변경 가능 여부 확인\n- 현재 상품에서 변경 가능한지\n- 고객 자격 조건 만족하는지\n- 계약 조건 확인
ValidationService --> Service: ValidationResult{valid: true}
deactivate ValidationService
Service -> ProductDataService: executeProductChange(changeRequest)
activate ProductDataService
ProductDataService -> MockRepo: saveProductChangeResult(changeRequest)
activate MockRepo
MockRepo -> MockDB: INSERT INTO mock_product_change_history\n(transaction_id, line_number, \n current_product_code, new_product_code,\n change_date, process_result)
activate MockDB
MockDB --> MockRepo: 변경 이력 저장 완료
deactivate MockDB
MockRepo --> ProductDataService: 저장 완료
deactivate MockRepo
ProductDataService -> ProductDataService: 상품변경 완료 정보 생성\n- 새로운 상품 정보\n- 변경 적용일\n- 변경 후 요금 정보
ProductDataService --> Service: ProductChangeResult\n{\n lineNumber: "01012345678",\n newProductCode: "PROD002",\n newProductName: "5G 프리미엄",\n changeDate: "20241201",\n effectiveDate: "20241201",\n monthlyFee: 75000,\n processResult: "정상"\n}
deactivate ProductDataService
Service --> Controller: MockProductChangeResponse\n{\n "resultCode": "0000",\n "resultMessage": "상품변경 완료",\n "transactionId": "TXN20241201001",\n "data": productChangeResult\n}
deactivate Service
Controller --> ProductService: 200 OK\n상품변경 성공 응답
deactivate Controller
else NOT_ALLOWED 시나리오
Service -> Service: 변경 불가 응답 구성
Service --> Controller: MockErrorResponse\n{\n "resultCode": "E101",\n "resultMessage": "현재 상품에서 요청한 상품으로 변경할 수 없습니다",\n "transactionId": "TXN20241201001",\n "errorDetail": "약정 기간 내 상품변경 제한"\n}
Controller --> ProductService: 400 Bad Request
else INCOMPATIBLE 시나리오
Service -> Service: 호환 불가 응답 구성
Service --> Controller: MockErrorResponse\n{\n "resultCode": "E102",\n "resultMessage": "호환되지 않는 상품입니다",\n "transactionId": "TXN20241201001",\n "errorDetail": "선택한 상품은 현재 단말기와 호환되지 않습니다"\n}
Controller --> ProductService: 400 Bad Request
else INSUFFICIENT_BALANCE 시나리오
Service -> Service: 잔액 부족 응답 구성
Service --> Controller: MockErrorResponse\n{\n "resultCode": "E103",\n "resultMessage": "잔액이 부족하여 상품변경을 할 수 없습니다",\n "transactionId": "TXN20241201001",\n "errorDetail": "미납금 정리 후 상품변경 가능"\n}
Controller --> ProductService: 400 Bad Request
else SYSTEM_ERROR 시나리오
Service -> Service: 시스템 오류 응답 구성
Service --> Controller: MockErrorResponse\n{\n "resultCode": "E999",\n "resultMessage": "시스템 일시 장애로 상품변경 처리를 할 수 없습니다",\n "transactionId": "TXN20241201001"\n}
Controller --> ProductService: 500 Internal Server Error
end
end
== Mock 상품 데이터 관리 ==
note over MockRepo, MockDB
Mock 상품변경 데이터:
1. mock_products: 상품 정보 및 요금
2. mock_product_compatibility: 상품 간 변경 가능 매트릭스
3. mock_customer_eligibility: 고객별 상품 변경 자격
4. mock_product_change_history: 변경 이력 추적
상품 변경 규칙:
- 기본 상품 → 프리미엄: 가능
- 프리미엄 → 기본: 약정 조건 확인 필요
- 5G → 4G: 단말기 호환성 확인
- 데이터 무제한 → 제한: 즉시 가능
end note
== Mock 비즈니스 로직 시뮬레이션 ==
Service -> Service: 추가 비즈니스 로직 처리 (비동기)
note right: Mock 비즈니스 시나리오\n1. 고객 알림 발송 시뮬레이션\n2. 정산 시스템 연동 시뮬레이션\n3. 단말기 설정 변경 시뮬레이션\n4. 부가서비스 자동 해지/가입
== 상품변경 고객 정보 조회 (UFR-PROD-020 지원) ==
note over Controller, MockDB
Mock 서비스는 상품변경 화면을 위한
고객 정보 및 상품 정보도 제공:
GET /kos/customer/{customerId}
- 고객 정보, 현재 상품 정보
GET /kos/products/available
- 변경 가능한 상품 목록
GET /kos/line/{lineNumber}/status
- 회선 상태 정보
end note
== Mock 상품변경 트랜잭션 추적 ==
Service -> Service: 트랜잭션 상태 추적 (비동기)
note right: Mock 트랜잭션 관리\n- 트랜잭션 ID별 상태 추적\n- 중복 요청 방지\n- 롤백 시나리오 시뮬레이션\n- 분산 트랜잭션 패턴 테스트
Service -> Service: Mock 메트릭 업데이트 (비동기)
note right: Mock 서비스 지표\n- 상품변경 성공/실패율\n- 시나리오별 처리 통계\n- 응답 시간 분포\n- 오류 패턴 분석
@enduml
@@ -0,0 +1,139 @@
@startuml
!theme mono
title KOS-Mock Service - 요금조회 내부 시퀀스
participant "Bill-Inquiry Service<<E>>" as BillService
participant "KosMockController" as Controller
participant "KosMockService" as Service
participant "BillDataService" as BillDataService
participant "MockScenarioService" as ScenarioService
participant "MockDataRepository" as MockRepo
participant "Mock Data Store<<E>>" as MockDB
== KOS-Mock 요금조회 시뮬레이션 ==
BillService -> Controller: POST /kos/bill/inquiry\nContent-Type: application/json\n{\n "lineNumber": "01012345678",\n "inquiryMonth": "202412"\n}
activate Controller
Controller -> Controller: 요청 데이터 유효성 검사\n- lineNumber: 11자리 숫자 형식\n- inquiryMonth: YYYYMM 형식\n- 필수값 확인
alt 입력값 오류
Controller --> BillService: 400 Bad Request\n{\n "resultCode": "E400",\n "resultMessage": "입력값이 올바르지 않습니다"\n}
else 입력값 정상
Controller -> Service: getBillInfo(lineNumber, inquiryMonth)
activate Service
== Mock 시나리오 결정 ==
Service -> ScenarioService: determineScenario(lineNumber, inquiryMonth)
activate ScenarioService
ScenarioService -> ScenarioService: 회선번호 기반 시나리오 결정
note right: Mock 시나리오 규칙\n- 01012345678: 정상 케이스\n- 01012345679: 데이터 없음\n- 01012345680: 시스템 오류\n- 01012345681: 타임아웃 시뮬레이션\n- 기타: 정상 케이스로 처리
alt 정상 케이스 (01012345678 또는 기타)
ScenarioService --> Service: MockScenario{type: "SUCCESS", delay: 500ms}
else 데이터 없음 케이스 (01012345679)
ScenarioService --> Service: MockScenario{type: "NO_DATA", delay: 300ms}
else 시스템 오류 케이스 (01012345680)
ScenarioService --> Service: MockScenario{type: "SYSTEM_ERROR", delay: 1000ms}
else 타임아웃 시뮬레이션 (01012345681)
ScenarioService --> Service: MockScenario{type: "TIMEOUT", delay: 5000ms}
end
deactivate ScenarioService
Service -> Service: 시나리오별 지연 처리\n(실제 KOS 응답 시간 시뮬레이션)
note right: Thread.sleep(scenario.delay)\n실제 KOS 응답 시간 모사
alt SUCCESS 시나리오
Service -> BillDataService: generateBillData(lineNumber, inquiryMonth)
activate BillDataService
BillDataService -> MockRepo: getMockBillTemplate(lineNumber)
activate MockRepo
MockRepo -> MockDB: SELECT * FROM mock_bill_templates\nWHERE line_number = ? OR is_default = true
activate MockDB
MockDB --> MockRepo: Mock 데이터 템플릿
deactivate MockDB
MockRepo --> BillDataService: BillTemplate
deactivate MockRepo
BillDataService -> BillDataService: 동적 데이터 생성\n- 조회월 기반 요금 계산\n- 사용량 랜덤 생성\n- 할인정보 적용
BillDataService --> Service: BillInfo\n{\n productName: "5G 프리미엄",\n contractInfo: "24개월 약정",\n billingMonth: "202412",\n charge: 75000,\n discountInfo: "가족할인 10000원",\n usage: {voice: "250분", data: "20GB"},\n estimatedCancellationFee: 120000,\n deviceInstallment: 35000,\n billingPaymentInfo: {\n billingDate: "2024-12-25",\n paymentStatus: "완료"\n }\n}
deactivate BillDataService
Service -> Service: 응답 데이터 구성
Service --> Controller: MockBillResponse\n{\n "resultCode": "0000",\n "resultMessage": "성공",\n "data": billInfo\n}
deactivate Service
Controller --> BillService: 200 OK\n정상 요금조회 응답
deactivate Controller
else NO_DATA 시나리오
Service -> Service: 데이터 없음 응답 구성
Service --> Controller: MockErrorResponse\n{\n "resultCode": "E002",\n "resultMessage": "해당 월의 요금 데이터가 존재하지 않습니다",\n "data": null\n}
Controller --> BillService: 200 OK\n(비즈니스 오류는 200으로 응답)
else SYSTEM_ERROR 시나리오
Service -> Service: 시스템 오류 응답 구성
Service --> Controller: MockErrorResponse\n{\n "resultCode": "E999",\n "resultMessage": "시스템 일시 장애가 발생했습니다",\n "data": null\n}
Controller --> BillService: 500 Internal Server Error
else TIMEOUT 시나리오
Service -> Service: 타임아웃 시뮬레이션\n(5초 대기 후 응답)
note right: KOS 타임아웃 시나리오\nCircuit Breaker 테스트용
alt 클라이언트가 타임아웃 전에 대기
Service --> Controller: 지연된 정상 응답
Controller --> BillService: 200 OK (지연 응답)
else 클라이언트 타임아웃 (3초)
note right: 클라이언트에서 타임아웃으로\n연결 종료됨
end
end
end
== Mock 데이터 관리 ==
note over MockRepo, MockDB
Mock 데이터베이스 구조:
1. mock_bill_templates: 요금 템플릿 데이터
2. mock_scenarios: 시나리오별 설정
3. mock_usage_patterns: 사용량 패턴 데이터
4. mock_products: 상품 정보 데이터
동적 데이터 생성:
- 회선번호별 고유 패턴
- 월별 사용량 변화
- 계절별 요금 변동
- 할인 정책 적용
end note
== Mock 시나리오 설정 ==
note over ScenarioService
Mock 시나리오 관리:
1. 환경변수로 시나리오 설정 가능
2. 회선번호 패턴 기반 동작 결정
3. 응답 지연 시간 조절
4. 오류율 시뮬레이션
5. 부하 테스트 지원
설정 예시:
- mock.scenario.success.delay=500ms
- mock.scenario.error.rate=5%
- mock.scenario.timeout.enabled=true
end note
== 로깅 및 모니터링 ==
Service -> Service: Mock 요청/응답 로깅 (비동기)
note right: Mock 서비스 모니터링\n- 요청 통계\n- 시나리오별 호출 현황\n- 응답 시간 분석\n- 오류 패턴 추적
Service -> Service: 메트릭 업데이트 (비동기)
note right: Mock 서비스 지표\n- 총 호출 횟수\n- 시나리오별 분포\n- 평균 응답 시간\n- 성공/실패 비율
@enduml
@@ -0,0 +1,183 @@
@startuml
!theme mono
title Product-Change Service - KOS 연동 내부 시퀀스
participant "ProductChangeService" as Service
participant "KosClientService" as KosClient
participant "CircuitBreakerService" as CircuitBreaker
participant "RetryService" as RetryService
participant "KosAdapterService" as KosAdapter
participant "ProductRepository" as ProductRepo
participant "Product DB<<E>>" as ProductDB
participant "KOS-Mock Service<<E>>" as KOSMock
participant "MVNO AP Server<<E>>" as MVNO
== UFR-PROD-040: KOS 상품변경 처리 ==
note over Service
사전체크가 통과된 상품변경 요청에 대해
KOS 시스템과 연동하여 실제 상품변경 처리
end note
Service -> KosClient: processProductChange(changeRequest)
activate KosClient
KosClient -> CircuitBreaker: isCallAllowed()
activate CircuitBreaker
alt Circuit Breaker - OPEN 상태
CircuitBreaker --> KosClient: Circuit Open\n"시스템 일시 장애"
deactivate CircuitBreaker
KosClient -> MVNO: sendSystemErrorNotification\n"시스템 일시 장애, 잠시 후 재시도"
activate MVNO
MVNO --> KosClient: 장애 안내 전송 완료
deactivate MVNO
KosClient --> Service: CircuitBreakerException\n"시스템 일시 장애, 잠시 후 재시도"
else Circuit Breaker - CLOSED/HALF_OPEN 상태
CircuitBreaker --> KosClient: Call Allowed
deactivate CircuitBreaker
KosClient -> RetryService: executeProductChangeWithRetry(changeRequest)
activate RetryService
loop 최대 3회 재시도 (상품변경은 중요한 거래)
RetryService -> KosAdapter: callKosProductChange(changeRequest)
activate KosAdapter
KosAdapter -> KosAdapter: 요청 데이터 변환\n- 회선번호 형식 검증\n- 상품코드 매핑\n- 거래ID 생성\n- 인증 헤더 설정
== KOS-Mock Service 상품변경 호출 ==
KosAdapter -> KOSMock: POST /kos/product/change\nContent-Type: application/json\n{\n "transactionId": "TXN20241201001",\n "lineNumber": "01012345678",\n "currentProductCode": "PROD001",\n "newProductCode": "PROD002",\n "changeReason": "고객 요청",\n "effectiveDate": "20241201"\n}
activate KOSMock
note right: KOS-Mock 상품변경 서비스\n- 실제 KOS 대신 Mock 처리\n- 타임아웃: 5초 (중요 거래)\n- 성공/실패 시나리오 시뮬레이션
alt KOS-Mock 상품변경 성공
KOSMock --> KosAdapter: 200 OK\n{\n "resultCode": "0000",\n "resultMessage": "상품변경 완료",\n "transactionId": "TXN20241201001",\n "data": {\n "lineNumber": "01012345678",\n "newProductCode": "PROD002",\n "newProductName": "5G 프리미엄",\n "changeDate": "20241201",\n "effectiveDate": "20241201",\n "processResult": "정상"\n }\n}
deactivate KOSMock
KosAdapter -> KosAdapter: 성공 응답 데이터 변환\n- KOS 응답 → ProductChangeResult\n- 상품변경 완료 정보 매핑
KosAdapter --> RetryService: ProductChangeResult{success: true}
deactivate KosAdapter
break 성공 시 재시도 중단
else KOS-Mock 상품변경 실패
KOSMock --> KosAdapter: 400 Bad Request\n{\n "resultCode": "E101",\n "resultMessage": "상품변경 처리 실패",\n "transactionId": "TXN20241201001",\n "errorDetail": "현재 상품에서 요청한 상품으로 변경할 수 없습니다"\n}
deactivate KOSMock
KosAdapter -> KosAdapter: 실패 응답 데이터 변환\n- 오류 코드별 예외 매핑\n- E101: ProductChangeNotAllowedException\n- E102: InsufficientBalanceException\n- E999: SystemErrorException
KosAdapter --> RetryService: ProductChangeException{reason: errorDetail}
deactivate KosAdapter
else 네트워크 오류 (타임아웃, 연결 실패)
KOSMock --> KosAdapter: IOException/TimeoutException
deactivate KOSMock
KosAdapter --> RetryService: NetworkException
deactivate KosAdapter
end
alt 재시도 가능한 오류 (네트워크, 일시적 오류)
RetryService -> RetryService: 재시도 대기\n- 1차: 2초 대기\n- 2차: 5초 대기\n- 3차: 10초 대기
note right: 상품변경은 중요한 거래\n재시도 간격을 길게 설정
else 재시도 불가능한 오류 (비즈니스 로직 오류)
break 재시도 중단
end
end
alt 상품변경 성공
RetryService --> KosClient: ProductChangeResult{success: true}
deactivate RetryService
KosClient -> CircuitBreaker: recordSuccess()
activate CircuitBreaker
CircuitBreaker -> CircuitBreaker: 성공 카운트 증가
deactivate CircuitBreaker
== UFR-PROD-040: 상품변경 완료 처리 ==
KosClient -> MVNO: sendProductChangeResult\n{newProductCode, processResult: "정상", message: "상품 변경이 완료되었다"}
activate MVNO
MVNO --> KosClient: 변경완료 결과 전송 완료
deactivate MVNO
KosClient -> ProductRepo: updateProductChangeStatus(transactionId, "COMPLETED", result)
activate ProductRepo
ProductRepo -> ProductDB: UPDATE product_change_request\nSET status = 'COMPLETED',\n completion_time = NOW(),\n new_product_code = ?,\n result_message = 'COMPLETED'\nWHERE transaction_id = ?
activate ProductDB
ProductDB --> ProductRepo: 상태 업데이트 완료
deactivate ProductDB
ProductRepo -> ProductDB: INSERT INTO product_change_history\n(transaction_id, line_number, \n current_product_code, new_product_code,\n change_date, process_result, result_message)
activate ProductDB
note right: 비동기 처리\n상품변경 이력 저장
ProductDB --> ProductRepo: 이력 저장 완료
deactivate ProductDB
deactivate ProductRepo
KosClient --> Service: ProductChangeSuccess\n{newProductCode, changeDate, message: "상품 변경이 완료되었다"}
deactivate KosClient
else 상품변경 실패
RetryService --> KosClient: ProductChangeException
deactivate RetryService
KosClient -> CircuitBreaker: recordFailure()
activate CircuitBreaker
CircuitBreaker -> CircuitBreaker: 실패 카운트 증가
deactivate CircuitBreaker
KosClient -> MVNO: sendProductChangeResult\n{processResult: "실패", failureReason, message: "상품 변경에 실패하여 실패 사유에 따라 문구를 화면에 출력한다"}
activate MVNO
MVNO --> KosClient: 변경실패 결과 전송 완료
deactivate MVNO
KosClient -> ProductRepo: updateProductChangeStatus(transactionId, "FAILED", errorReason)
activate ProductRepo
ProductRepo -> ProductDB: UPDATE product_change_request\nSET status = 'FAILED',\n completion_time = NOW(),\n failure_reason = ?,\n result_message = 'FAILED'\nWHERE transaction_id = ?
activate ProductDB
ProductDB --> ProductRepo: 상태 업데이트 완료
deactivate ProductDB
ProductRepo -> ProductDB: INSERT INTO product_change_history\n(..., process_result = 'FAILED', error_detail)
activate ProductDB
ProductDB --> ProductRepo: 실패 이력 저장 완료
deactivate ProductDB
deactivate ProductRepo
KosClient --> Service: ProductChangeFailure\n{reason, message: "상품 변경 요청을 실패하였다"}
deactivate KosClient
end
end
== 상품변경 결과 후처리 ==
alt 상품변경 성공
Service -> Service: 캐시 무효화 처리
Service -> "Redis Cache<<E>>": 고객 상품 정보 캐시 삭제\nDEL customer_product:{userId}\nDEL current_product:{userId}
note right: 변경된 상품 정보로\n캐시 갱신 필요
Service -> Service: 고객 알림 처리 (비동기)\n- SMS/Push 알림\n- 이메일 통지
note right: 상품변경 완료\n고객 안내 필요
else 상품변경 실패
Service -> Service: 실패 분석 및 로깅\n- 실패 패턴 분석\n- 모니터링 지표 업데이트
note right: 실패 원인 분석\n서비스 개선 활용
end
== 트랜잭션 무결성 보장 ==
note over Service, ProductDB
상품변경 트랜잭션 처리:
1. KOS 연동 성공 → 로컬 DB 상태 업데이트
2. 로컬 DB 실패 → KOS 보상 트랜잭션 (롤백)
3. 데이터 일관성 보장
4. 분산 트랜잭션 패턴 적용
end note
@enduml
@@ -0,0 +1,246 @@
@startuml
!theme mono
title Product-Change Service - 상품변경 요청 내부 시퀀스
participant "API Gateway" as Gateway
participant "ProductController" as Controller
participant "ProductChangeService" as Service
participant "ProductCacheService" as CacheService
participant "ProductValidationService" as ValidationService
participant "ProductRepository" as ProductRepo
participant "KosClientService" as KosClient
participant "Redis Cache<<E>>" as Redis
participant "Product DB<<E>>" as ProductDB
participant "MVNO AP Server<<E>>" as MVNO
== UFR-PROD-010: 상품변경 메뉴 접근 ==
Gateway -> Controller: GET /product/menu\nAuthorization: Bearer {accessToken}
activate Controller
Controller -> Controller: JWT 토큰에서 userId 추출
Controller -> Service: getProductMenuData(userId)
activate Service
Service -> CacheService: getCustomerProductInfo(userId)
activate CacheService
CacheService -> Redis: GET customer_product:{userId}
activate Redis
alt 고객 상품 정보 캐시 Hit
Redis --> CacheService: 고객 상품 정보 반환\n{lineNumber, customerId, currentProductCode, productName}
deactivate Redis
note right: 캐시 히트\n- TTL: 4시간\n- 빠른 응답
else 고객 상품 정보 캐시 Miss
Redis --> CacheService: null
deactivate Redis
CacheService -> KosClient: getCustomerInfo(userId)
activate KosClient
note right: KOS-Mock에서 고객 정보 조회\n(kos-mock-상품변경.puml 참조)
KosClient --> CacheService: CustomerProductInfo
deactivate KosClient
CacheService -> Redis: SET customer_product:{userId}\nValue: customerProductInfo\nTTL: 4시간
activate Redis
Redis --> CacheService: 캐싱 완료
deactivate Redis
end
CacheService --> Service: CustomerProductInfo
deactivate CacheService
Service --> Controller: ProductMenuResponse\n{lineNumber, customerId, currentProduct}
deactivate Service
Controller --> Gateway: 200 OK\n상품변경 메뉴 데이터
deactivate Controller
== UFR-PROD-020: 상품변경 화면 접근 ==
Gateway -> Controller: GET /product/change\nAuthorization: Bearer {accessToken}
activate Controller
Controller -> Service: getProductChangeScreen(userId)
activate Service
== 현재 상품 정보 및 변경 가능 상품 목록 조회 ==
Service -> CacheService: getCurrentProductInfo(userId)
activate CacheService
CacheService -> Redis: GET current_product:{userId}
activate Redis
alt 현재 상품 정보 캐시 Miss
Redis --> CacheService: null
deactivate Redis
CacheService -> KosClient: getCurrentProduct(userId)
activate KosClient
KosClient --> CacheService: CurrentProductInfo
deactivate KosClient
CacheService -> Redis: SET current_product:{userId}\nTTL: 2시간
activate Redis
Redis --> CacheService: 캐싱 완료
deactivate Redis
else 현재 상품 정보 캐시 Hit
Redis --> CacheService: CurrentProductInfo
deactivate Redis
end
CacheService --> Service: CurrentProductInfo
deactivate CacheService
Service -> CacheService: getAvailableProducts()
activate CacheService
CacheService -> Redis: GET available_products:all
activate Redis
alt 상품 목록 캐시 Miss
Redis --> CacheService: null
deactivate Redis
CacheService -> KosClient: getAvailableProducts()
activate KosClient
KosClient --> CacheService: List<AvailableProduct>
deactivate KosClient
CacheService -> Redis: SET available_products:all\nTTL: 24시간
activate Redis
Redis --> CacheService: 캐싱 완료
deactivate Redis
else 상품 목록 캐시 Hit
Redis --> CacheService: List<AvailableProduct>
deactivate Redis
end
CacheService --> Service: List<AvailableProduct>
deactivate CacheService
Service -> Service: 변경 가능한 상품 필터링\n- 현재 상품과 다른 상품\n- 판매중인 상품\n- 사업자 일치 상품
Service --> Controller: ProductChangeScreenResponse\n{currentProduct, availableProducts}
deactivate Service
Controller --> Gateway: 200 OK\n상품변경 화면 데이터
deactivate Controller
== UFR-PROD-030: 상품변경 요청 및 사전체크 ==
Gateway -> Controller: POST /product/request\n{\n "lineNumber": "01012345678",\n "currentProductCode": "PROD001",\n "newProductCode": "PROD002"\n}\nAuthorization: Bearer {accessToken}
activate Controller
Controller -> Controller: 입력값 검증\n- lineNumber: 11자리 숫자\n- productCode: 필수값, 형식 확인
alt 입력값 오류
Controller --> Gateway: 400 Bad Request\n"입력값을 확인해주세요"
else 입력값 정상
Controller -> Service: requestProductChange(changeRequest, userId)
activate Service
== 상품변경 사전체크 수행 ==
Service -> ValidationService: validateProductChange(changeRequest)
activate ValidationService
ValidationService -> ValidationService: 1. 판매중인 상품 확인
ValidationService -> CacheService: getProductStatus(newProductCode)
activate CacheService
CacheService -> Redis: GET product_status:{newProductCode}
alt 상품 상태 캐시 Miss
Redis --> CacheService: null
CacheService -> ProductRepo: getProductStatus(newProductCode)
activate ProductRepo
ProductRepo -> ProductDB: SELECT status, sales_status\nFROM products\nWHERE product_code = ?
activate ProductDB
ProductDB --> ProductRepo: 상품 상태 정보
deactivate ProductDB
ProductRepo --> CacheService: ProductStatus
deactivate ProductRepo
CacheService -> Redis: SET product_status:{newProductCode}\nTTL: 1시간
else 상품 상태 캐시 Hit
Redis --> CacheService: ProductStatus
end
deactivate Redis
CacheService --> ValidationService: ProductStatus
deactivate CacheService
alt 신규 상품이 판매 중이 아님
ValidationService --> Service: ValidationException\n"현재 판매중인 상품이 아닙니다"
else 신규 상품 판매 중
ValidationService -> ValidationService: 2. 사업자 일치 확인
ValidationService -> ValidationService: 고객 사업자와 상품 사업자 비교
alt 사업자 불일치
ValidationService --> Service: ValidationException\n"변경 요청한 사업자에서 판매중인 상품이 아닙니다"
else 사업자 일치
ValidationService -> ValidationService: 3. 회선 사용상태 확인
ValidationService -> CacheService: getLineStatus(lineNumber)
activate CacheService
CacheService -> Redis: GET line_status:{lineNumber}
activate Redis
alt 회선 상태 캐시 Miss
Redis --> CacheService: null
deactivate Redis
CacheService -> KosClient: getLineStatus(lineNumber)
activate KosClient
KosClient --> CacheService: LineStatus
deactivate KosClient
CacheService -> Redis: SET line_status:{lineNumber}\nTTL: 30분
activate Redis
Redis --> CacheService: 캐싱 완료
deactivate Redis
else 회선 상태 캐시 Hit
Redis --> CacheService: LineStatus
deactivate Redis
end
CacheService --> ValidationService: LineStatus
deactivate CacheService
alt 회선이 사용 중이 아님 (정지 상태)
ValidationService --> Service: ValidationException\n"변경 요청 회선은 사용 중인 상태가 아닙니다"
else 회선 사용 중 (정상)
ValidationService --> Service: ValidationResult{success: true}
deactivate ValidationService
Service -> ProductRepo: saveChangeRequest(changeRequest, "PRE_CHECK_PASSED")
activate ProductRepo
ProductRepo -> ProductDB: INSERT INTO product_change_request\n(user_id, line_number, current_product_code, \n new_product_code, request_time, status)
activate ProductDB
ProductDB --> ProductRepo: 요청 저장 완료
deactivate ProductDB
deactivate ProductRepo
Service --> Controller: PreCheckResult{success: true, message: "상품 변경이 진행되었다"}
deactivate Service
Controller --> Gateway: 200 OK\n{status: "PRE_CHECK_PASSED", message: "상품 사전 체크에 성공하였다"}
deactivate Controller
end
end
end
end
== 사전체크 실패 처리 ==
alt 사전체크 실패
Service -> ProductRepo: saveChangeRequest(changeRequest, "PRE_CHECK_FAILED")
activate ProductRepo
ProductRepo -> ProductDB: INSERT INTO product_change_request\n(..., status, failure_reason)
deactivate ProductRepo
Service --> Controller: PreCheckException{reason: failureReason}
Controller --> Gateway: 400 Bad Request\n{status: "PRE_CHECK_FAILED", message: "상품 사전 체크에 실패하였다"}
end
@enduml