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
+215
View File
@@ -0,0 +1,215 @@
@startuml
!theme mono
title Auth Service - Simple Class Design
package "com.unicorn.phonebill.auth" {
package "controller" {
class AuthController {
+login()
+logout()
+verifyToken()
+refreshToken()
+getUserPermissions()
+checkPermission()
+getUserInfo()
}
}
package "dto" {
class LoginRequest
class LoginResponse
class RefreshTokenRequest
class RefreshTokenResponse
class TokenVerifyResponse
class PermissionCheckRequest
class PermissionCheckResponse
class PermissionsResponse
class UserInfoResponse
class UserInfo
class Permission
class SuccessResponse
}
package "service" {
interface AuthService
class AuthServiceImpl
interface TokenService
class TokenServiceImpl
interface PermissionService
class PermissionServiceImpl
}
package "domain" {
class User
enum UserStatus
class UserSession
class AuthenticationResult
class DecodedToken
class PermissionResult
class TokenRefreshResult
class UserInfoDetail
}
package "repository" {
interface UserRepository
interface UserPermissionRepository
interface LoginHistoryRepository
package "entity" {
class UserEntity
class UserPermissionEntity
class LoginHistoryEntity
}
package "jpa" {
interface UserJpaRepository
interface UserPermissionJpaRepository
interface LoginHistoryJpaRepository
}
}
package "config" {
class SecurityConfig
class JwtConfig
class RedisConfig
}
}
' Common Base Classes
package "Common Module" <<External>> {
class ApiResponse<T>
class ErrorResponse
abstract class BaseTimeEntity
enum ErrorCode
class BusinessException
}
' 관계 정의 (간단화)
AuthController --> AuthService
AuthController --> TokenService
AuthServiceImpl --> UserRepository
AuthServiceImpl --> TokenService
AuthServiceImpl --> PermissionService
AuthServiceImpl --> LoginHistoryRepository
PermissionServiceImpl --> UserPermissionRepository
UserRepository --> UserEntity
UserPermissionRepository --> UserPermissionEntity
LoginHistoryRepository --> LoginHistoryEntity
UserEntity --|> BaseTimeEntity
UserPermissionEntity --|> BaseTimeEntity
LoginHistoryEntity --|> BaseTimeEntity
AuthService <|-- AuthServiceImpl
TokenService <|-- TokenServiceImpl
PermissionService <|-- PermissionServiceImpl
UserRepository <|-- UserJpaRepository
UserPermissionRepository <|-- UserPermissionJpaRepository
LoginHistoryRepository <|-- LoginHistoryJpaRepository
User --> UserStatus
' API 매핑표
note as N1
<b>AuthController API Mapping</b>
===
<b>POST /auth/login</b>
- Method: login(LoginRequest)
- Response: ApiResponse<LoginResponse>
- Description: 사용자 로그인 처리
<b>POST /auth/logout</b>
- Method: logout()
- Response: ApiResponse<SuccessResponse>
- Description: 사용자 로그아웃 처리
<b>GET /auth/verify</b>
- Method: verifyToken()
- Response: ApiResponse<TokenVerifyResponse>
- Description: JWT 토큰 검증
<b>POST /auth/refresh</b>
- Method: refreshToken(RefreshTokenRequest)
- Response: ApiResponse<RefreshTokenResponse>
- Description: 토큰 갱신
<b>GET /auth/permissions</b>
- Method: getUserPermissions()
- Response: ApiResponse<PermissionsResponse>
- Description: 사용자 권한 조회
<b>POST /auth/permissions/check</b>
- Method: checkPermission(PermissionCheckRequest)
- Response: ApiResponse<PermissionCheckResponse>
- Description: 특정 서비스 접근 권한 확인
<b>GET /auth/user-info</b>
- Method: getUserInfo()
- Response: ApiResponse<UserInfoResponse>
- Description: 사용자 정보 조회
end note
N1 .. AuthController
' 패키지 구조 설명
note as N2
<b>패키지 구조 (Layered Architecture)</b>
===
<b>controller</b>
- AuthController: REST API 엔드포인트
<b>dto</b>
- Request/Response 객체들
- API 계층과 Service 계층 간 데이터 전송
<b>service</b>
- AuthService: 인증/인가 비즈니스 로직
- TokenService: JWT 토큰 관리
- PermissionService: 권한 관리
<b>domain</b>
- 도메인 모델 및 비즈니스 엔티티
- 비즈니스 로직 포함
<b>repository</b>
- 데이터 접근 계층
- entity: JPA 엔티티
- jpa: JPA Repository 인터페이스
<b>config</b>
- 설정 클래스들 (Security, JWT, Redis)
end note
N2 .. "com.unicorn.phonebill.auth"
' 핵심 기능 설명
note as N3
<b>핵심 기능</b>
===
<b>인증 (Authentication)</b>
- 로그인/로그아웃 처리
- JWT 토큰 생성/검증/갱신
- 세션 관리 (Redis 캐시)
- 로그인 실패 횟수 관리 (5회 실패 시 30분 잠금)
<b>인가 (Authorization)</b>
- 서비스별 접근 권한 확인
- 권한 캐싱 (Redis, TTL: 4시간)
- Cache-Aside 패턴 적용
<b>보안</b>
- bcrypt 패스워드 해싱
- JWT 토큰 기반 인증
- Redis 세션 캐싱 (TTL: 30분/24시간)
- IP 기반 로그인 이력 추적
end note
N3 .. AuthServiceImpl
@enduml
+564
View File
@@ -0,0 +1,564 @@
@startuml
!theme mono
title Auth Service - Detailed Class Design
package "com.unicorn.phonebill.auth" {
package "controller" {
class AuthController {
-authService: AuthService
-tokenService: TokenService
+login(request: LoginRequest): ApiResponse<LoginResponse>
+logout(): ApiResponse<SuccessResponse>
+verifyToken(): ApiResponse<TokenVerifyResponse>
+refreshToken(request: RefreshTokenRequest): ApiResponse<RefreshTokenResponse>
+getUserPermissions(): ApiResponse<PermissionsResponse>
+checkPermission(request: PermissionCheckRequest): ApiResponse<PermissionCheckResponse>
+getUserInfo(): ApiResponse<UserInfoResponse>
}
}
package "dto" {
class LoginRequest {
-userId: String
-password: String
-autoLogin: boolean
+getUserId(): String
+getPassword(): String
+isAutoLogin(): boolean
+validate(): void
}
class LoginResponse {
-accessToken: String
-refreshToken: String
-expiresIn: int
-user: UserInfo
+getAccessToken(): String
+getRefreshToken(): String
+getExpiresIn(): int
+getUser(): UserInfo
}
class RefreshTokenRequest {
-refreshToken: String
+getRefreshToken(): String
+validate(): void
}
class RefreshTokenResponse {
-accessToken: String
-expiresIn: int
+getAccessToken(): String
+getExpiresIn(): int
}
class TokenVerifyResponse {
-valid: boolean
-user: UserInfo
-expiresIn: int
+isValid(): boolean
+getUser(): UserInfo
+getExpiresIn(): int
}
class PermissionCheckRequest {
-serviceType: String
+getServiceType(): String
+validate(): void
}
class PermissionCheckResponse {
-serviceType: String
-hasPermission: boolean
-permissionDetails: Permission
+getServiceType(): String
+isHasPermission(): boolean
+getPermissionDetails(): Permission
}
class PermissionsResponse {
-userId: String
-permissions: List<Permission>
+getUserId(): String
+getPermissions(): List<Permission>
}
class UserInfoResponse {
-userId: String
-userName: String
-phoneNumber: String
-email: String
-status: String
-lastLoginAt: LocalDateTime
-permissions: List<String>
+getUserId(): String
+getUserName(): String
+getPhoneNumber(): String
+getEmail(): String
+getStatus(): String
+getLastLoginAt(): LocalDateTime
+getPermissions(): List<String>
}
class UserInfo {
-userId: String
-userName: String
-phoneNumber: String
-permissions: List<String>
+getUserId(): String
+getUserName(): String
+getPhoneNumber(): String
+getPermissions(): List<String>
}
class Permission {
-permission: String
-description: String
-granted: boolean
+getPermission(): String
+getDescription(): String
+isGranted(): boolean
}
class SuccessResponse {
-message: String
+getMessage(): String
}
}
package "service" {
interface AuthService {
+authenticateUser(userId: String, password: String): AuthenticationResult
+getUserInfo(userId: String): UserInfoDetail
+refreshUserToken(userId: String): TokenRefreshResult
+checkServicePermission(userId: String, serviceType: String): PermissionResult
+invalidateUserPermissions(userId: String): void
}
class AuthServiceImpl {
-userRepository: UserRepository
-tokenService: TokenService
-permissionService: PermissionService
-redisTemplate: RedisTemplate
-passwordEncoder: PasswordEncoder
-loginHistoryRepository: LoginHistoryRepository
+authenticateUser(userId: String, password: String): AuthenticationResult
+getUserInfo(userId: String): UserInfoDetail
+refreshUserToken(userId: String): TokenRefreshResult
+checkServicePermission(userId: String, serviceType: String): PermissionResult
+invalidateUserPermissions(userId: String): void
-validateLoginAttempts(user: User): void
-handleFailedLogin(userId: String): void
-handleSuccessfulLogin(user: User): void
-createUserSession(user: User, autoLogin: boolean): void
-saveLoginHistory(userId: String, ipAddress: String): void
}
interface TokenService {
+generateAccessToken(userInfo: UserInfoDetail): String
+generateRefreshToken(userId: String): String
+validateAccessToken(token: String): DecodedToken
+validateRefreshToken(token: String): boolean
+extractUserId(token: String): String
+getTokenExpiration(token: String): LocalDateTime
}
class TokenServiceImpl {
-jwtSecret: String
-accessTokenExpiry: int
-refreshTokenExpiry: int
+generateAccessToken(userInfo: UserInfoDetail): String
+generateRefreshToken(userId: String): String
+validateAccessToken(token: String): DecodedToken
+validateRefreshToken(token: String): boolean
+extractUserId(token: String): String
+getTokenExpiration(token: String): LocalDateTime
-createJwtToken(subject: String, claims: Map<String, Object>, expiry: int): String
-parseJwtToken(token: String): Claims
}
interface PermissionService {
+validateServiceAccess(permissions: List<String>, serviceType: String): PermissionResult
+getUserPermissions(userId: String): List<Permission>
+cacheUserPermissions(userId: String, permissions: List<Permission>): void
+invalidateUserPermissions(userId: String): void
}
class PermissionServiceImpl {
-userPermissionRepository: UserPermissionRepository
-redisTemplate: RedisTemplate
+validateServiceAccess(permissions: List<String>, serviceType: String): PermissionResult
+getUserPermissions(userId: String): List<Permission>
+cacheUserPermissions(userId: String, permissions: List<Permission>): void
+invalidateUserPermissions(userId: String): void
-mapServiceTypeToPermission(serviceType: String): String
-checkPermissionGranted(permissions: List<String>, requiredPermission: String): boolean
}
}
package "domain" {
class User {
-userId: String
-userName: String
-phoneNumber: String
-email: String
-passwordHash: String
-salt: String
-status: UserStatus
-loginAttemptCount: int
-lockedUntil: LocalDateTime
-lastLoginAt: LocalDateTime
-createdAt: LocalDateTime
-updatedAt: LocalDateTime
+getUserId(): String
+getUserName(): String
+getPhoneNumber(): String
+getEmail(): String
+getPasswordHash(): String
+getSalt(): String
+getStatus(): UserStatus
+getLoginAttemptCount(): int
+getLockedUntil(): LocalDateTime
+getLastLoginAt(): LocalDateTime
+isAccountLocked(): boolean
+canAttemptLogin(): boolean
+incrementLoginAttempt(): void
+resetLoginAttempt(): void
+lockAccount(duration: Duration): void
+updateLastLoginAt(loginTime: LocalDateTime): void
}
enum UserStatus {
ACTIVE
INACTIVE
LOCKED
+getValue(): String
}
class UserSession {
-userId: String
-sessionId: String
-userInfo: UserInfoDetail
-permissions: List<String>
-lastAccessTime: LocalDateTime
-createdAt: LocalDateTime
-ttl: Duration
+getUserId(): String
+getSessionId(): String
+getUserInfo(): UserInfoDetail
+getPermissions(): List<String>
+getLastAccessTime(): LocalDateTime
+getCreatedAt(): LocalDateTime
+getTtl(): Duration
+updateLastAccessTime(): void
+isExpired(): boolean
}
class AuthenticationResult {
-success: boolean
-accessToken: String
-refreshToken: String
-userInfo: UserInfoDetail
-errorMessage: String
+isSuccess(): boolean
+getAccessToken(): String
+getRefreshToken(): String
+getUserInfo(): UserInfoDetail
+getErrorMessage(): String
}
class DecodedToken {
-userId: String
-permissions: List<String>
-expiresAt: LocalDateTime
-issuedAt: LocalDateTime
+getUserId(): String
+getPermissions(): List<String>
+getExpiresAt(): LocalDateTime
+getIssuedAt(): LocalDateTime
+isExpired(): boolean
}
class PermissionResult {
-granted: boolean
-serviceType: String
-reason: String
-permissionDetails: Permission
+isGranted(): boolean
+getServiceType(): String
+getReason(): String
+getPermissionDetails(): Permission
}
class TokenRefreshResult {
-newAccessToken: String
-expiresIn: int
+getNewAccessToken(): String
+getExpiresIn(): int
}
class UserInfoDetail {
-userId: String
-userName: String
-phoneNumber: String
-email: String
-status: UserStatus
-lastLoginAt: LocalDateTime
-permissions: List<String>
+getUserId(): String
+getUserName(): String
+getPhoneNumber(): String
+getEmail(): String
+getStatus(): UserStatus
+getLastLoginAt(): LocalDateTime
+getPermissions(): List<String>
}
}
package "repository" {
interface UserRepository {
+findUserById(userId: String): Optional<User>
+save(user: User): User
+incrementLoginAttempt(userId: String): void
+resetLoginAttempt(userId: String): void
+lockAccount(userId: String, duration: Duration): void
+updateLastLoginAt(userId: String, loginTime: LocalDateTime): void
}
interface UserPermissionRepository {
+findPermissionsByUserId(userId: String): List<UserPermission>
+save(userPermission: UserPermission): UserPermission
+deleteByUserId(userId: String): void
}
interface LoginHistoryRepository {
+save(loginHistory: LoginHistory): LoginHistory
+findByUserIdOrderByLoginTimeDesc(userId: String, pageable: Pageable): List<LoginHistory>
}
package "entity" {
class UserEntity {
-id: Long
-userId: String
-userName: String
-phoneNumber: String
-email: String
-passwordHash: String
-salt: String
-status: String
-loginAttemptCount: int
-lockedUntil: LocalDateTime
-lastLoginAt: LocalDateTime
-createdAt: LocalDateTime
-updatedAt: LocalDateTime
+getId(): Long
+getUserId(): String
+getUserName(): String
+getPhoneNumber(): String
+getEmail(): String
+getPasswordHash(): String
+getSalt(): String
+getStatus(): String
+getLoginAttemptCount(): int
+getLockedUntil(): LocalDateTime
+getLastLoginAt(): LocalDateTime
+getCreatedAt(): LocalDateTime
+getUpdatedAt(): LocalDateTime
+toDomain(): User
}
class UserPermissionEntity {
-id: Long
-userId: String
-permissionCode: String
-status: String
-createdAt: LocalDateTime
-updatedAt: LocalDateTime
+getId(): Long
+getUserId(): String
+getPermissionCode(): String
+getStatus(): String
+getCreatedAt(): LocalDateTime
+getUpdatedAt(): LocalDateTime
+toDomain(): UserPermission
}
class LoginHistoryEntity {
-id: Long
-userId: String
-loginTime: LocalDateTime
-ipAddress: String
-userAgent: String
-success: boolean
-failureReason: String
-createdAt: LocalDateTime
+getId(): Long
+getUserId(): String
+getLoginTime(): LocalDateTime
+getIpAddress(): String
+getUserAgent(): String
+isSuccess(): boolean
+getFailureReason(): String
+getCreatedAt(): LocalDateTime
+toDomain(): LoginHistory
}
}
package "jpa" {
interface UserJpaRepository {
+findByUserId(userId: String): Optional<UserEntity>
+save(userEntity: UserEntity): UserEntity
+existsByUserId(userId: String): boolean
}
interface UserPermissionJpaRepository {
+findByUserIdAndStatus(userId: String, status: String): List<UserPermissionEntity>
+save(userPermissionEntity: UserPermissionEntity): UserPermissionEntity
+deleteByUserId(userId: String): void
}
interface LoginHistoryJpaRepository {
+save(loginHistoryEntity: LoginHistoryEntity): LoginHistoryEntity
+findByUserIdOrderByLoginTimeDesc(userId: String, pageable: Pageable): List<LoginHistoryEntity>
}
}
}
package "config" {
class SecurityConfig {
-jwtSecret: String
-accessTokenExpiry: int
-refreshTokenExpiry: int
+passwordEncoder(): PasswordEncoder
+corsConfigurationSource(): CorsConfigurationSource
+filterChain(http: HttpSecurity): SecurityFilterChain
+authenticationManager(): AuthenticationManager
}
class JwtConfig {
-secret: String
-accessTokenExpiry: int
-refreshTokenExpiry: int
+getSecret(): String
+getAccessTokenExpiry(): int
+getRefreshTokenExpiry(): int
+jwtEncoder(): JwtEncoder
+jwtDecoder(): JwtDecoder
}
class RedisConfig {
-host: String
-port: int
-password: String
-database: int
+redisConnectionFactory(): RedisConnectionFactory
+redisTemplate(): RedisTemplate<String, Object>
+cacheManager(): RedisCacheManager
+sessionRedisTemplate(): RedisTemplate<String, UserSession>
}
}
}
' Common Base Classes 사용
package "Common Module" <<External>> {
class ApiResponse<T>
class ErrorResponse
abstract class BaseTimeEntity
enum ErrorCode
class BusinessException
}
' 관계 정의
AuthController --> AuthService : uses
AuthController --> TokenService : uses
AuthController ..> LoginRequest : uses
AuthController ..> LoginResponse : creates
AuthController ..> UserInfoResponse : creates
AuthController ..> PermissionCheckResponse : creates
AuthServiceImpl --> UserRepository : uses
AuthServiceImpl --> TokenService : uses
AuthServiceImpl --> PermissionService : uses
AuthServiceImpl --> LoginHistoryRepository : uses
TokenServiceImpl ..> DecodedToken : creates
TokenServiceImpl ..> AuthenticationResult : creates
PermissionServiceImpl --> UserPermissionRepository : uses
UserRepository --> UserEntity : works with
UserPermissionRepository --> UserPermissionEntity : works with
LoginHistoryRepository --> LoginHistoryEntity : works with
UserRepository --> User : returns
UserPermissionRepository --> UserPermission : returns
LoginHistoryRepository --> LoginHistory : returns
UserEntity ..> User : converts to
UserPermissionEntity ..> UserPermission : converts to
LoginHistoryEntity ..> LoginHistory : converts to
UserJpaRepository --> UserEntity : manages
UserPermissionJpaRepository --> UserPermissionEntity : manages
LoginHistoryJpaRepository --> LoginHistoryEntity : manages
User --> UserStatus : has
UserSession --> UserInfoDetail : contains
AuthServiceImpl ..> AuthenticationResult : creates
AuthServiceImpl ..> UserInfoDetail : creates
PermissionServiceImpl ..> PermissionResult : creates
' Inheritance
UserEntity --|> BaseTimeEntity
UserPermissionEntity --|> BaseTimeEntity
LoginHistoryEntity --|> BaseTimeEntity
AuthService <|-- AuthServiceImpl : implements
TokenService <|-- TokenServiceImpl : implements
PermissionService <|-- PermissionServiceImpl : implements
UserRepository <|-- UserJpaRepository : implements
UserPermissionRepository <|-- UserPermissionJpaRepository : implements
LoginHistoryRepository <|-- LoginHistoryJpaRepository : implements
' Notes
note top of AuthController : "REST API 엔드포인트 제공\n- 로그인/로그아웃\n- 토큰 검증/갱신\n- 권한 확인"
note top of AuthServiceImpl : "인증/인가 비즈니스 로직\n- 사용자 인증 처리\n- 세션 관리\n- 권한 검증"
note top of TokenServiceImpl : "JWT 토큰 관리\n- 토큰 생성/검증\n- 페이로드 추출"
note top of UserEntity : "사용자 정보 저장\n- 로그인 시도 횟수 관리\n- 계정 잠금 처리"
note top of RedisConfig : "Redis 캐시 설정\n- 세션 캐싱\n- 권한 캐싱"
@enduml
@@ -0,0 +1,138 @@
@startuml
!theme mono
title Bill-Inquiry Service - 간단한 클래스 설계
package "com.unicorn.phonebill.bill" {
package "controller" {
class BillController {
-billService: BillService
-jwtTokenUtil: JwtTokenUtil
}
note right of BillController : "API 매핑표\n\nGET /bills/menu → getBillMenu()\nPOST /bills/inquiry → inquireBill()\nGET /bills/inquiry/{requestId} → getBillInquiryStatus()\nGET /bills/history → getBillHistory()\n\n모든 메소드는 JWT 인증 필요\nController에는 API로 정의된 메소드만 존재"
}
package "dto" {
class BillMenuData
class CustomerInfo
class BillInquiryRequest
class BillInquiryData
class BillInquiryAsyncData
class BillInquiryStatusData
class BillHistoryData
class BillHistoryItem
class PaginationInfo
}
package "service" {
interface BillService
class BillServiceImpl
interface KosClientService
class KosClientServiceImpl
interface BillCacheService
class BillCacheServiceImpl
interface KosAdapterService
class KosAdapterServiceImpl
interface CircuitBreakerService
class CircuitBreakerServiceImpl
interface RetryService
class RetryServiceImpl
interface MvnoApiClient
class MvnoApiClientImpl
}
package "domain" {
class BillInfo
class DiscountInfo
class UsageInfo
class PaymentInfo
class KosRequest
class KosResponse
class KosData
class KosUsage
class KosPaymentInfo
class MvnoRequest
enum CircuitState
enum BillInquiryStatus
}
package "repository" {
interface BillHistoryRepository
interface KosInquiryHistoryRepository
package "entity" {
class BillHistoryEntity
class KosInquiryHistoryEntity
}
package "jpa" {
interface BillHistoryJpaRepository
interface KosInquiryHistoryJpaRepository
}
}
package "config" {
class RestTemplateConfig
class BillCacheConfig
class KosConfig
class MvnoConfig
class CircuitBreakerConfig
class AsyncConfig
class JwtTokenUtil
}
}
' 관계 설정
' Controller Layer
BillController --> BillService : "uses"
BillController --> JwtTokenUtil : "uses"
' Service Layer Relationships
BillServiceImpl ..|> BillService : "implements"
BillServiceImpl --> BillCacheService : "uses"
BillServiceImpl --> KosClientService : "uses"
BillServiceImpl --> BillHistoryRepository : "uses"
BillServiceImpl --> MvnoApiClient : "uses"
KosClientServiceImpl ..|> KosClientService : "implements"
KosClientServiceImpl --> KosAdapterService : "uses"
KosClientServiceImpl --> CircuitBreakerService : "uses"
KosClientServiceImpl --> RetryService : "uses"
KosClientServiceImpl --> KosInquiryHistoryRepository : "uses"
BillCacheServiceImpl ..|> BillCacheService : "implements"
BillCacheServiceImpl --> BillHistoryRepository : "uses"
KosAdapterServiceImpl ..|> KosAdapterService : "implements"
KosAdapterServiceImpl --> KosConfig : "uses"
CircuitBreakerServiceImpl ..|> CircuitBreakerService : "implements"
RetryServiceImpl ..|> RetryService : "implements"
MvnoApiClientImpl ..|> MvnoApiClient : "implements"
' Domain Relationships
BillInfo --> DiscountInfo : "contains"
BillInfo --> UsageInfo : "contains"
BillInfo --> PaymentInfo : "contains"
KosResponse --> KosData : "contains"
KosData --> KosUsage : "contains"
KosData --> KosPaymentInfo : "contains"
MvnoRequest --> BillInfo : "contains"
' Repository Relationships
BillHistoryRepository --> BillHistoryJpaRepository : "uses"
KosInquiryHistoryRepository --> KosInquiryHistoryJpaRepository : "uses"
' Entity Relationships
BillHistoryEntity --|> BaseTimeEntity : "extends"
KosInquiryHistoryEntity --|> BaseTimeEntity : "extends"
' DTO Relationships
BillMenuData --> CustomerInfo : "contains"
BillInquiryData --> BillInfo : "contains"
BillInquiryStatusData --> BillInfo : "contains"
BillHistoryData --> BillHistoryItem : "contains"
BillHistoryData --> PaginationInfo : "contains"
@enduml
+676
View File
@@ -0,0 +1,676 @@
@startuml
!theme mono
title Bill-Inquiry Service - 상세 클래스 설계
' 패키지별 클래스 구조
package "com.unicorn.phonebill.bill" {
package "controller" {
class BillController {
-billService: BillService
-jwtTokenUtil: JwtTokenUtil
+getBillMenu(authorization: String): ResponseEntity<ApiResponse<BillMenuData>>
+inquireBill(request: BillInquiryRequest, authorization: String): ResponseEntity<ApiResponse<BillInquiryData>>
+getBillInquiryStatus(requestId: String, authorization: String): ResponseEntity<ApiResponse<BillInquiryStatusData>>
+getBillHistory(lineNumber: String, startDate: String, endDate: String, page: int, size: int, status: String, authorization: String): ResponseEntity<ApiResponse<BillHistoryData>>
-extractUserInfoFromToken(authorization: String): JwtTokenVerifyDTO
-validateRequestParameters(request: Object): void
}
}
package "dto" {
' API Request/Response DTOs
class BillMenuData {
-customerInfo: CustomerInfo
-availableMonths: List<String>
-currentMonth: String
+BillMenuData(customerInfo: CustomerInfo, availableMonths: List<String>, currentMonth: String)
+getCustomerInfo(): CustomerInfo
+getAvailableMonths(): List<String>
+getCurrentMonth(): String
}
class CustomerInfo {
-customerId: String
-lineNumber: String
+CustomerInfo(customerId: String, lineNumber: String)
+getCustomerId(): String
+getLineNumber(): String
}
class BillInquiryRequest {
-lineNumber: String
-inquiryMonth: String
+BillInquiryRequest()
+getLineNumber(): String
+setLineNumber(lineNumber: String): void
+getInquiryMonth(): String
+setInquiryMonth(inquiryMonth: String): void
+isValid(): boolean
}
class BillInquiryData {
-requestId: String
-status: String
-billInfo: BillInfo
+BillInquiryData(requestId: String, status: String)
+BillInquiryData(requestId: String, status: String, billInfo: BillInfo)
+getRequestId(): String
+getStatus(): String
+getBillInfo(): BillInfo
+setBillInfo(billInfo: BillInfo): void
}
class BillInquiryAsyncData {
-requestId: String
-status: String
-estimatedTime: String
+BillInquiryAsyncData(requestId: String, status: String, estimatedTime: String)
+getRequestId(): String
+getStatus(): String
+getEstimatedTime(): String
}
class BillInquiryStatusData {
-requestId: String
-status: String
-progress: Integer
-billInfo: BillInfo
-errorMessage: String
+BillInquiryStatusData(requestId: String, status: String)
+getRequestId(): String
+getStatus(): String
+getProgress(): Integer
+setProgress(progress: Integer): void
+getBillInfo(): BillInfo
+setBillInfo(billInfo: BillInfo): void
+getErrorMessage(): String
+setErrorMessage(errorMessage: String): void
}
class BillHistoryData {
-items: List<BillHistoryItem>
-pagination: PaginationInfo
+BillHistoryData(items: List<BillHistoryItem>, pagination: PaginationInfo)
+getItems(): List<BillHistoryItem>
+getPagination(): PaginationInfo
}
class BillHistoryItem {
-requestId: String
-lineNumber: String
-inquiryMonth: String
-requestTime: LocalDateTime
-processTime: LocalDateTime
-status: String
-resultSummary: String
+BillHistoryItem()
+getRequestId(): String
+setRequestId(requestId: String): void
+getLineNumber(): String
+setLineNumber(lineNumber: String): void
+getInquiryMonth(): String
+setInquiryMonth(inquiryMonth: String): void
+getRequestTime(): LocalDateTime
+setRequestTime(requestTime: LocalDateTime): void
+getProcessTime(): LocalDateTime
+setProcessTime(processTime: LocalDateTime): void
+getStatus(): String
+setStatus(status: String): void
+getResultSummary(): String
+setResultSummary(resultSummary: String): void
}
class PaginationInfo {
-currentPage: int
-totalPages: int
-totalItems: long
-pageSize: int
-hasNext: boolean
-hasPrevious: boolean
+PaginationInfo(currentPage: int, totalPages: int, totalItems: long, pageSize: int)
+getCurrentPage(): int
+getTotalPages(): int
+getTotalItems(): long
+getPageSize(): int
+isHasNext(): boolean
+isHasPrevious(): boolean
}
}
package "service" {
interface BillService {
+getBillMenuData(userId: String, lineNumber: String): BillMenuData
+inquireBill(lineNumber: String, inquiryMonth: String, userId: String): BillInquiryData
+getBillInquiryStatus(requestId: String, userId: String): BillInquiryStatusData
+getBillHistory(lineNumber: String, startDate: String, endDate: String, page: int, size: int, status: String, userId: String): BillHistoryData
}
class BillServiceImpl {
-billCacheService: BillCacheService
-kosClientService: KosClientService
-billRepository: BillHistoryRepository
-mvnoApiClient: MvnoApiClient
+getBillMenuData(userId: String, lineNumber: String): BillMenuData
+inquireBill(lineNumber: String, inquiryMonth: String, userId: String): BillInquiryData
+getBillInquiryStatus(requestId: String, userId: String): BillInquiryStatusData
+getBillHistory(lineNumber: String, startDate: String, endDate: String, page: int, size: int, status: String, userId: String): BillHistoryData
-generateRequestId(): String
-getCurrentMonth(): String
-getAvailableMonths(): List<String>
-processCurrentMonthInquiry(lineNumber: String, userId: String): BillInquiryData
-processSpecificMonthInquiry(lineNumber: String, inquiryMonth: String, userId: String): BillInquiryData
-saveBillInquiryHistoryAsync(userId: String, lineNumber: String, inquiryMonth: String, requestId: String, status: String): void
-sendResultToMvnoAsync(billInfo: BillInfo): void
}
interface KosClientService {
+getBillInfo(lineNumber: String, inquiryMonth: String): BillInfo
+isServiceAvailable(): boolean
}
class KosClientServiceImpl {
-kosAdapterService: KosAdapterService
-circuitBreakerService: CircuitBreakerService
-retryService: RetryService
-billRepository: KosInquiryHistoryRepository
+getBillInfo(lineNumber: String, inquiryMonth: String): BillInfo
+isServiceAvailable(): boolean
-executeWithCircuitBreaker(lineNumber: String, inquiryMonth: String): BillInfo
-executeWithRetry(lineNumber: String, inquiryMonth: String): BillInfo
-saveKosInquiryHistory(lineNumber: String, inquiryMonth: String, status: String, errorMessage: String): void
}
interface BillCacheService {
+getCachedBillInfo(lineNumber: String, inquiryMonth: String): BillInfo
+cacheBillInfo(lineNumber: String, inquiryMonth: String, billInfo: BillInfo): void
+getCustomerInfo(userId: String): CustomerInfo
+cacheCustomerInfo(userId: String, customerInfo: CustomerInfo): void
+evictBillInfoCache(lineNumber: String, inquiryMonth: String): void
}
class BillCacheServiceImpl {
-redisTemplate: RedisTemplate<String, Object>
-billRepository: BillHistoryRepository
+getCachedBillInfo(lineNumber: String, inquiryMonth: String): BillInfo
+cacheBillInfo(lineNumber: String, inquiryMonth: String, billInfo: BillInfo): void
+getCustomerInfo(userId: String): CustomerInfo
+cacheCustomerInfo(userId: String, customerInfo: CustomerInfo): void
+evictBillInfoCache(lineNumber: String, inquiryMonth: String): void
-buildBillInfoCacheKey(lineNumber: String, inquiryMonth: String): String
-buildCustomerInfoCacheKey(userId: String): String
-isValidCachedData(cachedData: Object): boolean
}
interface KosAdapterService {
+callKosBillInquiry(lineNumber: String, inquiryMonth: String): KosResponse
}
class KosAdapterServiceImpl {
-restTemplate: RestTemplate
-kosConfig: KosConfig
+callKosBillInquiry(lineNumber: String, inquiryMonth: String): KosResponse
-buildKosRequest(lineNumber: String, inquiryMonth: String): KosRequest
-convertToKosResponse(responseEntity: ResponseEntity<String>): KosResponse
-handleKosError(statusCode: HttpStatus, responseBody: String): void
}
interface CircuitBreakerService {
+isCallAllowed(): boolean
+recordSuccess(): void
+recordFailure(): void
+getCircuitState(): CircuitState
}
class CircuitBreakerServiceImpl {
-failureThreshold: int
-recoveryTimeout: long
-successThreshold: int
-failureCount: AtomicInteger
-successCount: AtomicInteger
-lastFailureTime: AtomicLong
-circuitState: CircuitState
+isCallAllowed(): boolean
+recordSuccess(): void
+recordFailure(): void
+getCircuitState(): CircuitState
-transitionToOpen(): void
-transitionToHalfOpen(): void
-transitionToClosed(): void
}
interface RetryService {
+executeWithRetry(operation: Supplier<T>): T
}
class RetryServiceImpl {
-maxRetries: int
-retryDelayMs: long
+executeWithRetry(operation: Supplier<T>): T
-shouldRetry(exception: Exception, attemptCount: int): boolean
-calculateDelay(attemptCount: int): long
}
interface MvnoApiClient {
+sendBillResult(billInfo: BillInfo): void
}
class MvnoApiClientImpl {
-restTemplate: RestTemplate
-mvnoConfig: MvnoConfig
+sendBillResult(billInfo: BillInfo): void
-buildMvnoRequest(billInfo: BillInfo): MvnoRequest
}
}
package "domain" {
class BillInfo {
-productName: String
-contractInfo: String
-billingMonth: String
-totalAmount: Integer
-discountInfo: List<DiscountInfo>
-usage: UsageInfo
-terminationFee: Integer
-deviceInstallment: Integer
-paymentInfo: PaymentInfo
+BillInfo()
+getProductName(): String
+setProductName(productName: String): void
+getContractInfo(): String
+setContractInfo(contractInfo: String): void
+getBillingMonth(): String
+setBillingMonth(billingMonth: String): void
+getTotalAmount(): Integer
+setTotalAmount(totalAmount: Integer): void
+getDiscountInfo(): List<DiscountInfo>
+setDiscountInfo(discountInfo: List<DiscountInfo>): void
+getUsage(): UsageInfo
+setUsage(usage: UsageInfo): void
+getTerminationFee(): Integer
+setTerminationFee(terminationFee: Integer): void
+getDeviceInstallment(): Integer
+setDeviceInstallment(deviceInstallment: Integer): void
+getPaymentInfo(): PaymentInfo
+setPaymentInfo(paymentInfo: PaymentInfo): void
+isComplete(): boolean
}
class DiscountInfo {
-name: String
-amount: Integer
+DiscountInfo()
+DiscountInfo(name: String, amount: Integer)
+getName(): String
+setName(name: String): void
+getAmount(): Integer
+setAmount(amount: Integer): void
}
class UsageInfo {
-voice: String
-sms: String
-data: String
+UsageInfo()
+UsageInfo(voice: String, sms: String, data: String)
+getVoice(): String
+setVoice(voice: String): void
+getSms(): String
+setSms(sms: String): void
+getData(): String
+setData(data: String): void
}
class PaymentInfo {
-billingDate: String
-paymentStatus: String
-paymentMethod: String
+PaymentInfo()
+PaymentInfo(billingDate: String, paymentStatus: String, paymentMethod: String)
+getBillingDate(): String
+setBillingDate(billingDate: String): void
+getPaymentStatus(): String
+setPaymentStatus(paymentStatus: String): void
+getPaymentMethod(): String
+setPaymentMethod(paymentMethod: String): void
}
' KOS 연동 도메인 모델
class KosRequest {
-lineNumber: String
-inquiryMonth: String
-requestTime: LocalDateTime
+KosRequest(lineNumber: String, inquiryMonth: String)
+getLineNumber(): String
+getInquiryMonth(): String
+getRequestTime(): LocalDateTime
+toKosFormat(): Map<String, Object>
}
class KosResponse {
-resultCode: String
-resultMessage: String
-data: KosData
-responseTime: LocalDateTime
+KosResponse()
+getResultCode(): String
+setResultCode(resultCode: String): void
+getResultMessage(): String
+setResultMessage(resultMessage: String): void
+getData(): KosData
+setData(data: KosData): void
+getResponseTime(): LocalDateTime
+setResponseTime(responseTime: LocalDateTime): void
+isSuccess(): boolean
+toBillInfo(): BillInfo
}
class KosData {
-productName: String
-contractInfo: String
-billingMonth: String
-charge: Integer
-discountInfo: String
-usage: KosUsage
-estimatedCancellationFee: Integer
-deviceInstallment: Integer
-billingPaymentInfo: KosPaymentInfo
+KosData()
+getProductName(): String
+setProductName(productName: String): void
+getContractInfo(): String
+setContractInfo(contractInfo: String): void
+getBillingMonth(): String
+setBillingMonth(billingMonth: String): void
+getCharge(): Integer
+setCharge(charge: Integer): void
+getDiscountInfo(): String
+setDiscountInfo(discountInfo: String): void
+getUsage(): KosUsage
+setUsage(usage: KosUsage): void
+getEstimatedCancellationFee(): Integer
+setEstimatedCancellationFee(estimatedCancellationFee: Integer): void
+getDeviceInstallment(): Integer
+setDeviceInstallment(deviceInstallment: Integer): void
+getBillingPaymentInfo(): KosPaymentInfo
+setBillingPaymentInfo(billingPaymentInfo: KosPaymentInfo): void
}
class KosUsage {
-voice: String
-data: String
+KosUsage()
+getVoice(): String
+setVoice(voice: String): void
+getData(): String
+setData(data: String): void
+toUsageInfo(): UsageInfo
}
class KosPaymentInfo {
-billingDate: String
-paymentStatus: String
+KosPaymentInfo()
+getBillingDate(): String
+setBillingDate(billingDate: String): void
+getPaymentStatus(): String
+setPaymentStatus(paymentStatus: String): void
+toPaymentInfo(): PaymentInfo
}
' MVNO 연동 도메인 모델
class MvnoRequest {
-billInfo: BillInfo
-timestamp: LocalDateTime
+MvnoRequest(billInfo: BillInfo)
+getBillInfo(): BillInfo
+getTimestamp(): LocalDateTime
+toRequestBody(): Map<String, Object>
}
enum CircuitState {
CLOSED
OPEN
HALF_OPEN
+valueOf(name: String): CircuitState
+values(): CircuitState[]
}
enum BillInquiryStatus {
PROCESSING("처리중")
COMPLETED("완료")
FAILED("실패")
-description: String
+BillInquiryStatus(description: String)
+getDescription(): String
}
}
package "repository" {
interface BillHistoryRepository {
+findByUserIdAndLineNumberOrderByRequestTimeDesc(userId: String, lineNumber: String, pageable: Pageable): Page<BillHistoryEntity>
+findByUserIdAndRequestTimeBetweenOrderByRequestTimeDesc(userId: String, startDate: LocalDateTime, endDate: LocalDateTime, pageable: Pageable): Page<BillHistoryEntity>
+findByUserIdAndLineNumberAndStatusOrderByRequestTimeDesc(userId: String, lineNumber: String, status: String, pageable: Pageable): Page<BillHistoryEntity>
+save(entity: BillHistoryEntity): BillHistoryEntity
+getCustomerInfo(userId: String): CustomerInfo
}
interface KosInquiryHistoryRepository {
+save(entity: KosInquiryHistoryEntity): KosInquiryHistoryEntity
+findByLineNumberAndInquiryMonthOrderByRequestTimeDesc(lineNumber: String, inquiryMonth: String): List<KosInquiryHistoryEntity>
}
package "entity" {
class BillHistoryEntity {
-id: Long
-userId: String
-lineNumber: String
-inquiryMonth: String
-requestId: String
-requestTime: LocalDateTime
-processTime: LocalDateTime
-status: String
-resultSummary: String
-billInfoJson: String
+BillHistoryEntity()
+getId(): Long
+setId(id: Long): void
+getUserId(): String
+setUserId(userId: String): void
+getLineNumber(): String
+setLineNumber(lineNumber: String): void
+getInquiryMonth(): String
+setInquiryMonth(inquiryMonth: String): void
+getRequestId(): String
+setRequestId(requestId: String): void
+getRequestTime(): LocalDateTime
+setRequestTime(requestTime: LocalDateTime): void
+getProcessTime(): LocalDateTime
+setProcessTime(processTime: LocalDateTime): void
+getStatus(): String
+setStatus(status: String): void
+getResultSummary(): String
+setResultSummary(resultSummary: String): void
+getBillInfoJson(): String
+setBillInfoJson(billInfoJson: String): void
+toBillHistoryItem(): BillHistoryItem
+fromBillInfo(billInfo: BillInfo): void
}
class KosInquiryHistoryEntity {
-id: Long
-lineNumber: String
-inquiryMonth: String
-requestTime: LocalDateTime
-responseTime: LocalDateTime
-resultCode: String
-resultMessage: String
-errorDetail: String
+KosInquiryHistoryEntity()
+getId(): Long
+setId(id: Long): void
+getLineNumber(): String
+setLineNumber(lineNumber: String): void
+getInquiryMonth(): String
+setInquiryMonth(inquiryMonth: String): void
+getRequestTime(): LocalDateTime
+setRequestTime(requestTime: LocalDateTime): void
+getResponseTime(): LocalDateTime
+setResponseTime(responseTime: LocalDateTime): void
+getResultCode(): String
+setResultCode(resultCode: String): void
+getResultMessage(): String
+setResultMessage(resultMessage: String): void
+getErrorDetail(): String
+setErrorDetail(errorDetail: String): void
}
}
package "jpa" {
interface BillHistoryJpaRepository {
+findByUserIdAndLineNumberOrderByRequestTimeDesc(userId: String, lineNumber: String, pageable: Pageable): Page<BillHistoryEntity>
+findByUserIdAndRequestTimeBetweenOrderByRequestTimeDesc(userId: String, startDate: LocalDateTime, endDate: LocalDateTime, pageable: Pageable): Page<BillHistoryEntity>
+findByUserIdAndLineNumberAndStatusOrderByRequestTimeDesc(userId: String, lineNumber: String, status: String, pageable: Pageable): Page<BillHistoryEntity>
+countByUserIdAndLineNumber(userId: String, lineNumber: String): long
}
interface KosInquiryHistoryJpaRepository {
+findByLineNumberAndInquiryMonthOrderByRequestTimeDesc(lineNumber: String, inquiryMonth: String): List<KosInquiryHistoryEntity>
+countByResultCode(resultCode: String): long
}
}
}
package "config" {
class RestTemplateConfig {
+kosRestTemplate(): RestTemplate
+mvnoRestTemplate(): RestTemplate
+kosHttpMessageConverters(): List<HttpMessageConverter<?>>
+kosRequestInterceptors(): List<ClientHttpRequestInterceptor>
+kosConnectionPoolConfig(): HttpComponentsClientHttpRequestFactory
}
class BillCacheConfig {
+billInfoCacheConfiguration(): RedisCacheConfiguration
+customerInfoCacheConfiguration(): RedisCacheConfiguration
+billCacheKeyGenerator(): KeyGenerator
+cacheErrorHandler(): CacheErrorHandler
}
class KosConfig {
-baseUrl: String
-connectTimeout: int
-readTimeout: int
-maxRetries: int
-retryDelay: long
+getBaseUrl(): String
+setBaseUrl(baseUrl: String): void
+getConnectTimeout(): int
+setConnectTimeout(connectTimeout: int): void
+getReadTimeout(): int
+setReadTimeout(readTimeout: int): void
+getMaxRetries(): int
+setMaxRetries(maxRetries: int): void
+getRetryDelay(): long
+setRetryDelay(retryDelay: long): void
+getBillInquiryEndpoint(): String
}
class MvnoConfig {
-baseUrl: String
-connectTimeout: int
-readTimeout: int
+getBaseUrl(): String
+setBaseUrl(baseUrl: String): void
+getConnectTimeout(): int
+setConnectTimeout(connectTimeout: int): void
+getReadTimeout(): int
+setReadTimeout(readTimeout: int): void
+getSendResultEndpoint(): String
}
class CircuitBreakerConfig {
-failureThreshold: int
-recoveryTimeoutMs: long
-successThreshold: int
+getFailureThreshold(): int
+setFailureThreshold(failureThreshold: int): void
+getRecoveryTimeoutMs(): long
+setRecoveryTimeoutMs(recoveryTimeoutMs: long): void
+getSuccessThreshold(): int
+setSuccessThreshold(successThreshold: int): void
}
class AsyncConfig {
+billTaskExecutor(): TaskExecutor
+kosTaskExecutor(): TaskExecutor
+asyncExceptionHandler(): AsyncUncaughtExceptionHandler
}
class JwtTokenUtil {
-secretKey: String
-tokenExpiration: long
+extractUserId(token: String): String
+extractLineNumber(token: String): String
+extractPermissions(token: String): List<String>
+validateToken(token: String): boolean
+isTokenExpired(token: String): boolean
+parseToken(token: String): Claims
}
}
}
' 관계 설정
' Controller Layer
BillController --> BillService : "uses"
BillController --> JwtTokenUtil : "uses"
' Service Layer Relationships
BillServiceImpl ..|> BillService : "implements"
BillServiceImpl --> BillCacheService : "uses"
BillServiceImpl --> KosClientService : "uses"
BillServiceImpl --> BillHistoryRepository : "uses"
BillServiceImpl --> MvnoApiClient : "uses"
KosClientServiceImpl ..|> KosClientService : "implements"
KosClientServiceImpl --> KosAdapterService : "uses"
KosClientServiceImpl --> CircuitBreakerService : "uses"
KosClientServiceImpl --> RetryService : "uses"
KosClientServiceImpl --> KosInquiryHistoryRepository : "uses"
BillCacheServiceImpl ..|> BillCacheService : "uses"
BillCacheServiceImpl --> BillHistoryRepository : "uses"
KosAdapterServiceImpl ..|> KosAdapterService : "implements"
KosAdapterServiceImpl --> KosConfig : "uses"
CircuitBreakerServiceImpl ..|> CircuitBreakerService : "implements"
RetryServiceImpl ..|> RetryService : "implements"
MvnoApiClientImpl ..|> MvnoApiClient : "implements"
' Domain Relationships
BillInfo --> DiscountInfo : "contains"
BillInfo --> UsageInfo : "contains"
BillInfo --> PaymentInfo : "uses"
KosResponse --> KosData : "contains"
KosData --> KosUsage : "contains"
KosData --> KosPaymentInfo : "contains"
MvnoRequest --> BillInfo : "contains"
' Repository Relationships
BillHistoryRepository --> BillHistoryJpaRepository : "uses"
KosInquiryHistoryRepository --> KosInquiryHistoryJpaRepository : "uses"
' Entity Relationships
BillHistoryEntity --|> BaseTimeEntity : "extends"
KosInquiryHistoryEntity --|> BaseTimeEntity : "extends"
' DTO Relationships
BillMenuData --> CustomerInfo : "contains"
BillInquiryData --> BillInfo : "contains"
BillInquiryStatusData --> BillInfo : "contains"
BillHistoryData --> BillHistoryItem : "contains"
BillHistoryData --> PaginationInfo : "contains"
@enduml
+242
View File
@@ -0,0 +1,242 @@
# Product-Change Service 클래스 설계서
## 1. 개요
### 1.1 설계 목적
Product-Change Service의 상품변경 기능을 구현하기 위한 클래스 구조를 설계합니다.
### 1.2 설계 원칙
- **아키텍처 패턴**: Layered Architecture 적용
- **패키지 구조**: com.unicorn.phonebill.product 하위 계층별 구조
- **KOS 연동**: Circuit Breaker 패턴으로 외부 시스템 안정성 확보
- **캐시 전략**: Redis를 활용한 성능 최적화
- **예외 처리**: 계층별 예외 처리 및 비즈니스 예외 정의
### 1.3 주요 기능
- UFR-PROD-010: 상품변경 메뉴 조회
- UFR-PROD-020: 상품변경 화면 데이터 조회
- UFR-PROD-030: 상품변경 요청 및 사전체크
- UFR-PROD-040: KOS 연동 상품변경 처리
## 2. 패키지 구조도
```
com.unicorn.phonebill.product
├── controller/ # 컨트롤러 계층
│ └── ProductController # 상품변경 API 컨트롤러
├── dto/ # 데이터 전송 객체
│ ├── *Request # 요청 DTO 클래스들
│ ├── *Response # 응답 DTO 클래스들
│ └── *Enum # DTO 관련 열거형
├── service/ # 서비스 계층
│ ├── ProductService # 상품변경 서비스 인터페이스
│ ├── ProductServiceImpl # 상품변경 서비스 구현체
│ ├── ProductValidationService # 상품변경 검증 서비스
│ ├── ProductCacheService # 상품 캐시 서비스
│ ├── KosClientService # KOS 연동 서비스
│ ├── CircuitBreakerService # Circuit Breaker 서비스
│ └── RetryService # 재시도 서비스
├── domain/ # 도메인 계층
│ ├── Product # 상품 도메인 모델
│ ├── ProductChangeHistory # 상품변경 이력 도메인 모델
│ ├── ProductChangeResult # 상품변경 결과 도메인 모델
│ └── ProductStatus # 상품 상태 도메인 모델
├── repository/ # 저장소 계층
│ ├── ProductRepository # 상품 저장소 인터페이스
│ ├── ProductChangeHistoryRepository # 상품변경 이력 저장소 인터페이스
│ ├── entity/ # JPA 엔티티
│ │ └── ProductChangeHistoryEntity
│ └── jpa/ # JPA Repository
│ └── ProductChangeHistoryJpaRepository
├── config/ # 설정 계층
│ ├── RestTemplateConfig # REST 통신 설정
│ ├── CacheConfig # 캐시 설정
│ ├── CircuitBreakerConfig # Circuit Breaker 설정
│ └── KosProperties # KOS 연동 설정
├── external/ # 외부 연동 계층
│ ├── KosRequest # KOS 요청 모델
│ ├── KosResponse # KOS 응답 모델
│ └── KosAdapterService # KOS 어댑터 서비스
└── exception/ # 예외 계층
├── ProductChangeException # 상품변경 예외
├── ProductValidationException # 상품변경 검증 예외
├── KosConnectionException # KOS 연결 예외
└── CircuitBreakerException # Circuit Breaker 예외
```
## 3. 계층별 클래스 설계
### 3.1 Controller Layer
#### ProductController
- **역할**: 상품변경 관련 REST API 엔드포인트 제공
- **주요 메소드**:
- `getProductMenu()`: 상품변경 메뉴 조회 (GET /products/menu)
- `getCustomerInfo(lineNumber)`: 고객 정보 조회 (GET /products/customer/{lineNumber})
- `getAvailableProducts()`: 변경 가능한 상품 목록 조회 (GET /products/available)
- `validateProductChange(request)`: 상품변경 사전체크 (POST /products/change/validation)
- `requestProductChange(request)`: 상품변경 요청 (POST /products/change)
- `getProductChangeResult(requestId)`: 상품변경 결과 조회 (GET /products/change/{requestId})
- `getProductChangeHistory()`: 상품변경 이력 조회 (GET /products/history)
### 3.2 Service Layer
#### ProductService / ProductServiceImpl
- **역할**: 상품변경 비즈니스 로직 처리
- **의존성**: KosClientService, ProductValidationService, ProductCacheService, ProductChangeHistoryRepository
- **주요 기능**: 상품변경 프로세스 전체 조율, 캐시 무효화 처리
#### ProductValidationService
- **역할**: 상품변경 사전체크 로직 처리
- **주요 검증**: 판매중인 상품 확인, 사업자 일치 확인, 회선 사용상태 확인
- **의존성**: ProductRepository, ProductCacheService, KosClientService
#### ProductCacheService
- **역할**: Redis 캐시를 활용한 성능 최적화
- **주요 캐시**: 고객상품정보(4시간), 현재상품정보(2시간), 가용상품목록(24시간), 상품상태(1시간), 회선상태(30분)
- **캐시 키 전략**: `{cache_type}:{identifier}` 형식
#### KosClientService
- **역할**: KOS 시스템과의 연동 처리
- **의존성**: CircuitBreakerService, RetryService, KosAdapterService
- **주요 기능**: KOS API 호출, Circuit Breaker 상태 관리, 재시도 로직
#### CircuitBreakerService / RetryService
- **역할**: 외부 시스템 연동 안정성 보장
- **패턴**: Circuit Breaker, Retry 패턴 적용
- **설정**: 실패율 임계값, 재시도 횟수, 대기 시간 등
### 3.3 Domain Layer
#### Product
- **역할**: 상품 정보 도메인 모델
- **주요 속성**: productCode, productName, monthlyFee, dataAllowance, voiceAllowance, smsAllowance, status, operatorCode
- **비즈니스 메소드**: `canChangeTo()`, `isSameOperator()`
#### ProductChangeHistory
- **역할**: 상품변경 이력 도메인 모델
- **주요 속성**: requestId, userId, lineNumber, currentProductCode, targetProductCode, processStatus, requestedAt, processedAt
- **상태 관리**: `markAsCompleted()`, `markAsFailed()`
#### ProductChangeResult
- **역할**: 상품변경 처리 결과 도메인 모델
- **팩토리 메소드**: `createSuccessResult()`, `createFailureResult()`
### 3.4 Repository Layer
#### ProductRepository
- **역할**: 상품 데이터 접근 인터페이스
- **주요 메소드**: 상품상태 조회, 상품변경 요청 저장, 상태 업데이트
#### ProductChangeHistoryRepository
- **역할**: 상품변경 이력 데이터 접근 인터페이스
- **JPA Repository**: ProductChangeHistoryJpaRepository 활용
- **Entity**: ProductChangeHistoryEntity (BaseTimeEntity 상속)
### 3.5 Config Layer
#### RestTemplateConfig
- **역할**: REST 통신 설정
- **설정 요소**: Connection Pool, Timeout, HTTP Client 설정
#### CacheConfig
- **역할**: Redis 캐시 설정
- **설정 요소**: Redis 연결, Cache Manager, 직렬화 설정
#### CircuitBreakerConfig
- **역할**: Circuit Breaker 및 Retry 설정
- **설정 요소**: 실패율 임계값, 최소 호출 수, 대기 시간
#### KosProperties
- **역할**: KOS 연동 설정 프로퍼티
- **설정 요소**: baseUrl, connectTimeout, readTimeout, maxRetries, retryDelay
### 3.6 External Layer
#### KosAdapterService
- **역할**: KOS 시스템 연동 어댑터
- **주요 기능**: KOS API 호출, 요청/응답 데이터 변환, HTTP 헤더 설정
- **의존성**: KosProperties, RestTemplate
#### KosRequest / KosResponse
- **역할**: KOS 시스템 연동을 위한 요청/응답 모델
- **변환**: 내부 도메인 모델 ↔ KOS API 모델
### 3.7 Exception Layer
#### ProductChangeException
- **역할**: 상품변경 관련 비즈니스 예외
- **상속**: BusinessException 상속
#### ProductValidationException
- **역할**: 상품변경 검증 실패 예외
- **추가 정보**: 검증 상세 정보 목록 포함
#### KosConnectionException
- **역할**: KOS 연동 관련 예외
- **추가 정보**: 연동 서비스명 포함
#### CircuitBreakerException
- **역할**: Circuit Breaker Open 상태 예외
- **추가 정보**: 서비스명, 상태 정보 포함
## 4. 주요 설계 특징
### 4.1 Layered Architecture 적용
- **Controller**: API 엔드포인트 및 HTTP 요청/응답 처리
- **Service**: 비즈니스 로직 처리 및 트랜잭션 관리
- **Domain**: 핵심 비즈니스 모델 및 도메인 규칙
- **Repository**: 데이터 접근 및 영속성 관리
### 4.2 캐시 전략
- **다층 캐시**: Redis를 활용한 성능 최적화
- **TTL 차등 적용**: 데이터 특성에 따른 캐시 수명 관리
- **캐시 무효화**: 상품변경 완료 시 관련 캐시 제거
### 4.3 외부 연동 안정성
- **Circuit Breaker**: KOS 시스템 장애 시 빠른 실패 처리
- **Retry**: 일시적 네트워크 오류에 대한 재시도 로직
- **Timeout**: 응답 시간 초과 방지
### 4.4 예외 처리 전략
- **계층별 예외**: 각 계층의 책임에 맞는 예외 정의
- **비즈니스 예외**: 도메인 규칙 위반에 대한 명확한 예외
- **인프라 예외**: 외부 시스템 연동 실패에 대한 예외
## 5. API와 클래스 매핑
| API 엔드포인트 | HTTP Method | Controller 메소드 | 주요 Service |
|---|---|---|---|
| `/products/menu` | GET | `getProductMenu()` | ProductService |
| `/products/customer/{lineNumber}` | GET | `getCustomerInfo()` | ProductService, ProductCacheService |
| `/products/available` | GET | `getAvailableProducts()` | ProductService, ProductCacheService |
| `/products/change/validation` | POST | `validateProductChange()` | ProductValidationService |
| `/products/change` | POST | `requestProductChange()` | ProductService, KosClientService |
| `/products/change/{requestId}` | GET | `getProductChangeResult()` | ProductService |
| `/products/history` | GET | `getProductChangeHistory()` | ProductService, ProductChangeHistoryRepository |
## 6. 시퀀스와 클래스 연관관계
### 6.1 상품변경 요청 시퀀스 매핑
- **ProductController** → **ProductServiceImpl****ProductValidationService****KosClientService****KosAdapterService**
- **캐시 처리**: ProductCacheService를 통한 Redis 연동
- **이력 관리**: ProductChangeHistoryRepository를 통한 DB 저장
### 6.2 KOS 연동 시퀀스 매핑
- **KosClientService** → **CircuitBreakerService****RetryService****KosAdapterService**
- **상태 관리**: ProductChangeHistory 도메인 모델을 통한 상태 추적
- **결과 처리**: ProductChangeResult를 통한 성공/실패 처리
## 7. 설계 파일
- **상세 클래스 설계**: [product-change.puml](./product-change.puml)
- **간단 클래스 설계**: [product-change-simple.puml](./product-change-simple.puml)
## 8. 관련 문서
- **API 설계서**: [product-change-service-api.yaml](../api/product-change-service-api.yaml)
- **내부 시퀀스 설계서**:
- [product-상품변경요청.puml](../sequence/inner/product-상품변경요청.puml)
- [product-KOS연동.puml](../sequence/inner/product-KOS연동.puml)
- **유저스토리**: [userstory.md](../../userstory.md)
- **공통 기반 클래스**: [common-base.puml](./common-base.puml)
+176
View File
@@ -0,0 +1,176 @@
@startuml
!theme mono
title Common Base Classes - 통신요금 관리 서비스
package "Common Module" {
package "dto" {
class ApiResponse<T> {
-success: boolean
-message: String
-data: T
-timestamp: LocalDateTime
+of(data: T): ApiResponse<T>
+success(data: T, message: String): ApiResponse<T>
+error(message: String): ApiResponse<T>
+getSuccess(): boolean
+getMessage(): String
+getData(): T
+getTimestamp(): LocalDateTime
}
class ErrorResponse {
-code: String
-message: String
-details: String
-timestamp: LocalDateTime
+ErrorResponse(code: String, message: String, details: String)
+getCode(): String
+getMessage(): String
+getDetails(): String
+getTimestamp(): LocalDateTime
}
class JwtTokenDTO {
-accessToken: String
-refreshToken: String
-tokenType: String
-expiresIn: long
+JwtTokenDTO(accessToken: String, refreshToken: String, expiresIn: long)
+getAccessToken(): String
+getRefreshToken(): String
+getTokenType(): String
+getExpiresIn(): long
}
class JwtTokenVerifyDTO {
-userId: String
-lineNumber: String
-permissions: List<String>
-expiresAt: LocalDateTime
+JwtTokenVerifyDTO(userId: String, lineNumber: String, permissions: List<String>)
+getUserId(): String
+getLineNumber(): String
+getPermissions(): List<String>
+getExpiresAt(): LocalDateTime
}
}
package "entity" {
abstract class BaseTimeEntity {
#createdAt: LocalDateTime
#updatedAt: LocalDateTime
+getCreatedAt(): LocalDateTime
+getUpdatedAt(): LocalDateTime
+{abstract} getId(): Object
}
}
package "exception" {
enum ErrorCode {
AUTH001("인증 실패")
AUTH002("토큰이 유효하지 않음")
AUTH003("권한이 부족함")
AUTH004("계정이 잠겨있음")
AUTH005("토큰이 만료됨")
BILL001("요금 조회 실패")
BILL002("KOS 연동 실패")
BILL003("조회 이력 없음")
PROD001("상품변경 실패")
PROD002("사전체크 실패")
PROD003("상품정보 없음")
SYS001("시스템 오류")
SYS002("외부 연동 실패")
-code: String
-message: String
+ErrorCode(code: String, message: String)
+getCode(): String
+getMessage(): String
}
class BusinessException {
-errorCode: ErrorCode
-details: String
+BusinessException(errorCode: ErrorCode)
+BusinessException(errorCode: ErrorCode, details: String)
+getErrorCode(): ErrorCode
+getDetails(): String
}
class InfraException {
-errorCode: ErrorCode
-details: String
+InfraException(errorCode: ErrorCode)
+InfraException(errorCode: ErrorCode, details: String)
+getErrorCode(): ErrorCode
+getDetails(): String
}
}
package "util" {
class DateUtil {
+{static} getCurrentDateTime(): LocalDateTime
+{static} formatDate(date: LocalDateTime, pattern: String): String
+{static} parseDate(dateString: String, pattern: String): LocalDateTime
+{static} getStartOfMonth(date: LocalDateTime): LocalDateTime
+{static} getEndOfMonth(date: LocalDateTime): LocalDateTime
+{static} isWithinRange(date: LocalDateTime, start: LocalDateTime, end: LocalDateTime): boolean
}
class SecurityUtil {
+{static} encryptPassword(password: String): String
+{static} verifyPassword(password: String, encodedPassword: String): boolean
+{static} generateSalt(): String
+{static} maskPhoneNumber(phoneNumber: String): String
+{static} maskUserId(userId: String): String
}
class ValidatorUtil {
+{static} isValidPhoneNumber(phoneNumber: String): boolean
+{static} isValidUserId(userId: String): boolean
+{static} isValidPassword(password: String): boolean
+{static} isNotEmpty(value: String): boolean
+{static} isValidDateRange(startDate: LocalDateTime, endDate: LocalDateTime): boolean
}
}
package "config" {
class JpaConfig {
+auditorProvider(): AuditorAware<String>
+entityManagerFactory(): LocalContainerEntityManagerFactoryBean
+transactionManager(): PlatformTransactionManager
}
interface CacheConfig {
+redisConnectionFactory(): RedisConnectionFactory
+redisTemplate(): RedisTemplate<String, Object>
+cacheManager(): CacheManager
+redisCacheConfiguration(): RedisCacheConfiguration
}
}
package "aop" {
class LoggingAspect {
-logger: Logger
+logExecutionTime(joinPoint: ProceedingJoinPoint): Object
+logMethodEntry(joinPoint: JoinPoint): void
+logMethodExit(joinPoint: JoinPoint, result: Object): void
+logException(joinPoint: JoinPoint, exception: Exception): void
}
}
}
' 관계 설정
ApiResponse --> ErrorResponse : "contains"
BusinessException --> ErrorCode : "uses"
InfraException --> ErrorCode : "uses"
' 노트 추가
note top of ApiResponse : "모든 API 응답의 표준 구조\n제네릭을 사용한 타입 안전성 보장"
note top of BaseTimeEntity : "모든 엔티티의 기본 클래스\nJPA Auditing을 통한 생성/수정 시간 자동 관리"
note top of ErrorCode : "시스템 전체의 오류 코드 표준화\n서비스별 오류 코드 체계"
note top of LoggingAspect : "AOP를 통한 로깅 처리\n실행 시간 측정 및 예외 로깅"
@enduml
+176
View File
@@ -0,0 +1,176 @@
@startuml
!theme mono
title KOS-Mock Service 클래스 설계 (간단)
package "com.unicorn.phonebill.kosmock" {
package "controller" {
class KosMockController <<Controller>> {
}
}
package "service" {
class KosMockService <<Service>> {
}
class BillDataService <<Service>> {
}
class ProductDataService <<Service>> {
}
class ProductValidationService <<Service>> {
}
class MockScenarioService <<Service>> {
}
}
package "dto" {
class KosBillRequest <<DTO>> {
}
class KosProductChangeRequest <<DTO>> {
}
class MockBillResponse <<DTO>> {
}
class MockProductChangeResponse <<DTO>> {
}
class KosCustomerResponse <<DTO>> {
}
class KosProductResponse <<DTO>> {
}
class BillInfo <<Model>> {
}
class ProductChangeResult <<Model>> {
}
}
package "repository" {
interface MockDataRepository <<Repository>> {
}
class MockDataRepositoryImpl <<Repository>> {
}
}
package "repository.entity" {
class KosCustomerEntity <<Entity>> {
}
class KosProductEntity <<Entity>> {
}
class KosBillEntity <<Entity>> {
}
class KosUsageEntity <<Entity>> {
}
class KosContractEntity <<Entity>> {
}
class KosInstallmentEntity <<Entity>> {
}
class KosProductChangeHistoryEntity <<Entity>> {
}
}
package "repository.jpa" {
interface KosCustomerJpaRepository <<JPA Repository>> {
}
interface KosProductJpaRepository <<JPA Repository>> {
}
interface KosBillJpaRepository <<JPA Repository>> {
}
interface KosUsageJpaRepository <<JPA Repository>> {
}
interface KosContractJpaRepository <<JPA Repository>> {
}
interface KosInstallmentJpaRepository <<JPA Repository>> {
}
interface KosProductChangeHistoryJpaRepository <<JPA Repository>> {
}
}
package "config" {
class MockProperties <<Configuration>> {
}
class KosMockConfig <<Configuration>> {
}
}
}
package "Common Module" {
class ApiResponse<T> <<DTO>> {
}
class BaseTimeEntity <<Entity>> {
}
class BusinessException <<Exception>> {
}
}
' 관계 설정
KosMockController --> KosMockService
KosMockService --> BillDataService
KosMockService --> ProductDataService
KosMockService --> MockScenarioService
BillDataService --> MockDataRepository
ProductDataService --> MockDataRepository
ProductDataService --> ProductValidationService
ProductValidationService --> MockDataRepository
MockScenarioService --> MockProperties
MockDataRepositoryImpl ..|> MockDataRepository
MockDataRepositoryImpl --> KosCustomerJpaRepository
MockDataRepositoryImpl --> KosProductJpaRepository
MockDataRepositoryImpl --> KosBillJpaRepository
MockDataRepositoryImpl --> KosUsageJpaRepository
MockDataRepositoryImpl --> KosContractJpaRepository
MockDataRepositoryImpl --> KosInstallmentJpaRepository
MockDataRepositoryImpl --> KosProductChangeHistoryJpaRepository
KosCustomerJpaRepository --> KosCustomerEntity
KosProductJpaRepository --> KosProductEntity
KosBillJpaRepository --> KosBillEntity
KosUsageJpaRepository --> KosUsageEntity
KosContractJpaRepository --> KosContractEntity
KosInstallmentJpaRepository --> KosInstallmentEntity
KosProductChangeHistoryJpaRepository --> KosProductChangeHistoryEntity
KosCustomerEntity --|> BaseTimeEntity
KosProductEntity --|> BaseTimeEntity
KosBillEntity --|> BaseTimeEntity
KosUsageEntity --|> BaseTimeEntity
KosContractEntity --|> BaseTimeEntity
KosInstallmentEntity --|> BaseTimeEntity
KosProductChangeHistoryEntity --|> BaseTimeEntity
KosMockController --> ApiResponse
note top of KosMockController : **API 매핑표**\n\nPOST /kos/bill/inquiry\n- getBillInfo()\n- 요금조회 시뮬레이션\n\nPOST /kos/product/change\n- processProductChange()\n- 상품변경 시뮬레이션\n\nGET /kos/customer/{customerId}\n- getCustomerInfo()\n- 고객정보 조회\n\nGET /kos/products/available\n- getAvailableProducts()\n- 변경가능 상품목록\n\nGET /kos/line/{lineNumber}/status\n- getLineStatus()\n- 회선상태 조회
note right of MockScenarioService : **Mock 시나리오 규칙**\n\n요금조회:\n- 01012345678: 정상응답\n- 01012345679: 데이터없음\n- 01012345680: 시스템오류\n- 01012345681: 타임아웃\n\n상품변경:\n- 01012345678: 정상변경\n- 01012345679: 변경불가\n- 01012345680: 시스템오류\n- 01012345681: 잔액부족\n- PROD001→PROD999: 호환불가
note right of MockDataRepository : **데이터 접근 인터페이스**\n\n주요 메소드:\n- getMockBillTemplate()\n- getProductInfo()\n- getCustomerInfo()\n- saveProductChangeResult()\n- checkProductCompatibility()\n- getCustomerBalance()\n- getContractInfo()
note bottom of KosMockConfig : **Mock 설정**\n\n환경별 시나리오 설정:\n- mock.scenario.success.delay=500ms\n- mock.scenario.error.rate=5%\n- mock.scenario.timeout.enabled=true\n\n스레드풀 설정:\n- 비동기 로깅 및 메트릭 처리
@enduml
+588
View File
@@ -0,0 +1,588 @@
@startuml
!theme mono
title KOS-Mock Service 클래스 설계 (상세)
package "com.unicorn.phonebill.kosmock" {
package "controller" {
class KosMockController {
-kosMockService: KosMockService
+getBillInfo(lineNumber: String, inquiryMonth: String): ResponseEntity<ApiResponse<MockBillResponse>>
+processProductChange(changeRequest: KosProductChangeRequest): ResponseEntity<ApiResponse<MockProductChangeResponse>>
+getCustomerInfo(customerId: String): ResponseEntity<ApiResponse<KosCustomerResponse>>
+getAvailableProducts(): ResponseEntity<ApiResponse<List<KosProductResponse>>>
+getLineStatus(lineNumber: String): ResponseEntity<ApiResponse<KosLineStatusResponse>>
-validateBillRequest(lineNumber: String, inquiryMonth: String): void
-validateProductChangeRequest(request: KosProductChangeRequest): void
}
}
package "service" {
class KosMockService {
-billDataService: BillDataService
-productDataService: ProductDataService
-mockScenarioService: MockScenarioService
+getBillInfo(lineNumber: String, inquiryMonth: String): MockBillResponse
+processProductChange(changeRequest: KosProductChangeRequest): MockProductChangeResponse
+getCustomerInfo(customerId: String): KosCustomerResponse
+getAvailableProducts(): List<KosProductResponse>
+getLineStatus(lineNumber: String): KosLineStatusResponse
-logMockRequest(requestType: String, requestData: Object): void
-updateMetrics(requestType: String, scenario: String, responseTime: long): void
}
class BillDataService {
-mockDataRepository: MockDataRepository
+generateBillData(lineNumber: String, inquiryMonth: String): BillInfo
-calculateDynamicCharges(lineNumber: String, inquiryMonth: String): BillAmount
-generateUsageData(lineNumber: String, inquiryMonth: String): UsageInfo
-applyDiscounts(billAmount: BillAmount, lineNumber: String): List<DiscountInfo>
}
class ProductDataService {
-mockDataRepository: MockDataRepository
-productValidationService: ProductValidationService
+executeProductChange(changeRequest: KosProductChangeRequest): ProductChangeResult
+getProductInfo(productCode: String): KosProduct
+getCustomerProducts(customerId: String): List<KosProduct>
-calculateNewMonthlyFee(newProductCode: String): Integer
}
class ProductValidationService {
-mockDataRepository: MockDataRepository
+validateProductChange(changeRequest: KosProductChangeRequest): ValidationResult
+checkProductCompatibility(currentProduct: String, newProduct: String): Boolean
+checkCustomerEligibility(customerId: String, newProductCode: String): Boolean
-validateContractConstraints(customerId: String): Boolean
-validateBalance(customerId: String): Boolean
}
class MockScenarioService {
-properties: MockProperties
+determineScenario(lineNumber: String, inquiryMonth: String): MockScenario
+determineProductChangeScenario(lineNumber: String, changeRequest: KosProductChangeRequest): MockScenario
+simulateDelay(scenario: MockScenario): void
-getScenarioByLineNumber(lineNumber: String): String
-getScenarioByProductCodes(currentCode: String, newCode: String): String
}
}
package "dto.request" {
class KosBillRequest {
+lineNumber: String
+inquiryMonth: String
+validate(): void
}
class KosProductChangeRequest {
+transactionId: String
+lineNumber: String
+currentProductCode: String
+newProductCode: String
+changeReason: String
+effectiveDate: String
+validate(): void
}
}
package "dto.response" {
class MockBillResponse {
+resultCode: String
+resultMessage: String
+billInfo: BillInfo
}
class MockProductChangeResponse {
+resultCode: String
+resultMessage: String
+transactionId: String
+changeInfo: ProductChangeResult
}
class KosCustomerResponse {
+customerId: String
+phoneNumber: String
+customerName: String
+customerType: String
+status: String
+currentProduct: KosProduct
}
class KosProductResponse {
+productCode: String
+productName: String
+monthlyFee: Integer
+dataLimit: Integer
+voiceLimit: Integer
+saleStatus: String
}
class KosLineStatusResponse {
+lineNumber: String
+status: String
+activationDate: LocalDate
+contractInfo: ContractInfo
}
}
package "dto.model" {
class BillInfo {
+phoneNumber: String
+billMonth: String
+productName: String
+contractInfo: ContractInfo
+billAmount: BillAmount
+discountInfo: List<DiscountInfo>
+usage: UsageInfo
+installment: InstallmentInfo
+terminationFee: TerminationFeeInfo
+billingPaymentInfo: BillingPaymentInfo
}
class ProductChangeResult {
+lineNumber: String
+newProductCode: String
+newProductName: String
+changeDate: String
+effectiveDate: String
+monthlyFee: Integer
+processResult: String
+resultMessage: String
}
class ContractInfo {
+contractType: String
+contractStartDate: LocalDate
+contractEndDate: LocalDate
+remainingMonths: Integer
+penaltyAmount: Integer
}
class BillAmount {
+basicFee: Integer
+callFee: Integer
+dataFee: Integer
+smsFee: Integer
+additionalFee: Integer
+discountAmount: Integer
+totalAmount: Integer
}
class UsageInfo {
+voiceUsage: Integer
+dataUsage: Integer
+smsUsage: Integer
+voiceLimit: Integer
+dataLimit: Integer
+smsLimit: Integer
}
class DiscountInfo {
+discountType: String
+discountName: String
+discountAmount: Integer
+discountRate: BigDecimal
}
class InstallmentInfo {
+deviceModel: String
+totalAmount: Integer
+monthlyAmount: Integer
+paidAmount: Integer
+remainingAmount: Integer
+remainingMonths: Integer
}
class TerminationFeeInfo {
+contractPenalty: Integer
+installmentRemaining: Integer
+otherFees: Integer
+totalFee: Integer
}
class BillingPaymentInfo {
+dueDate: LocalDate
+paymentDate: LocalDate
+paymentStatus: String
}
class ValidationResult {
+valid: Boolean
+errorCode: String
+errorMessage: String
+errorDetails: String
}
class MockScenario {
+type: String
+delay: Long
+errorCode: String
+errorMessage: String
}
class KosProduct {
+productCode: String
+productName: String
+productType: String
+monthlyFee: Integer
+dataLimit: Integer
+voiceLimit: Integer
+smsLimit: Integer
+saleStatus: String
}
}
package "repository" {
interface MockDataRepository {
+getMockBillTemplate(lineNumber: String): Optional<KosCustomerEntity>
+getProductInfo(productCode: String): Optional<KosProductEntity>
+getAvailableProducts(): List<KosProductEntity>
+getCustomerInfo(customerId: String): Optional<KosCustomerEntity>
+saveProductChangeResult(changeRequest: KosProductChangeRequest, result: ProductChangeResult): KosProductChangeHistoryEntity
+checkProductCompatibility(currentProductCode: String, newProductCode: String): Boolean
+getCustomerBalance(customerId: String): Integer
+getContractInfo(customerId: String): Optional<KosContractEntity>
}
class MockDataRepositoryImpl {
-customerJpaRepository: KosCustomerJpaRepository
-productJpaRepository: KosProductJpaRepository
-billJpaRepository: KosBillJpaRepository
-usageJpaRepository: KosUsageJpaRepository
-discountJpaRepository: KosDiscountJpaRepository
-contractJpaRepository: KosContractJpaRepository
-installmentJpaRepository: KosInstallmentJpaRepository
-terminationFeeJpaRepository: KosTerminationFeeJpaRepository
-changeHistoryJpaRepository: KosProductChangeHistoryJpaRepository
+getMockBillTemplate(lineNumber: String): Optional<KosCustomerEntity>
+getProductInfo(productCode: String): Optional<KosProductEntity>
+getAvailableProducts(): List<KosProductEntity>
+getCustomerInfo(customerId: String): Optional<KosCustomerEntity>
+saveProductChangeResult(changeRequest: KosProductChangeRequest, result: ProductChangeResult): KosProductChangeHistoryEntity
+checkProductCompatibility(currentProductCode: String, newProductCode: String): Boolean
+getCustomerBalance(customerId: String): Integer
+getContractInfo(customerId: String): Optional<KosContractEntity>
-buildBillInfo(customer: KosCustomerEntity, inquiryMonth: String): BillInfo
-calculateUsage(customer: KosCustomerEntity, inquiryMonth: String): UsageInfo
}
}
package "repository.entity" {
class KosCustomerEntity {
+customerId: String
+phoneNumber: String
+customerName: String
+customerType: String
+status: String
+regDate: LocalDate
+currentProductCode: String
+createdAt: LocalDateTime
+updatedAt: LocalDateTime
}
class KosProductEntity {
+productCode: String
+productName: String
+productType: String
+monthlyFee: Integer
+dataLimit: Integer
+voiceLimit: Integer
+smsLimit: Integer
+saleStatus: String
+saleStartDate: LocalDate
+saleEndDate: LocalDate
+createdAt: LocalDateTime
+updatedAt: LocalDateTime
}
class KosBillEntity {
+billId: Long
+customerId: String
+phoneNumber: String
+billMonth: String
+productCode: String
+productName: String
+basicFee: Integer
+callFee: Integer
+dataFee: Integer
+smsFee: Integer
+additionalFee: Integer
+discountAmount: Integer
+totalAmount: Integer
+paymentStatus: String
+dueDate: LocalDate
+paymentDate: LocalDate
+createdAt: LocalDateTime
}
class KosUsageEntity {
+usageId: Long
+customerId: String
+phoneNumber: String
+usageMonth: String
+voiceUsage: Integer
+dataUsage: Integer
+smsUsage: Integer
+voiceLimit: Integer
+dataLimit: Integer
+smsLimit: Integer
+createdAt: LocalDateTime
}
class KosDiscountEntity {
+discountId: Long
+customerId: String
+phoneNumber: String
+billMonth: String
+discountType: String
+discountName: String
+discountAmount: Integer
+discountRate: BigDecimal
+applyStartDate: LocalDate
+applyEndDate: LocalDate
+createdAt: LocalDateTime
}
class KosContractEntity {
+contractId: Long
+customerId: String
+phoneNumber: String
+contractType: String
+contractStartDate: LocalDate
+contractEndDate: LocalDate
+contractStatus: String
+penaltyAmount: Integer
+remainingMonths: Integer
+createdAt: LocalDateTime
}
class KosInstallmentEntity {
+installmentId: Long
+customerId: String
+phoneNumber: String
+deviceModel: String
+totalAmount: Integer
+monthlyAmount: Integer
+paidAmount: Integer
+remainingAmount: Integer
+installmentMonths: Integer
+remainingMonths: Integer
+startDate: LocalDate
+endDate: LocalDate
+status: String
+createdAt: LocalDateTime
}
class KosTerminationFeeEntity {
+feeId: Long
+customerId: String
+phoneNumber: String
+contractPenalty: Integer
+installmentRemaining: Integer
+otherFees: Integer
+totalFee: Integer
+calculatedDate: LocalDate
+createdAt: LocalDateTime
}
class KosProductChangeHistoryEntity {
+historyId: Long
+customerId: String
+phoneNumber: String
+requestId: String
+beforeProductCode: String
+afterProductCode: String
+changeStatus: String
+changeDate: LocalDate
+processResult: String
+resultMessage: String
+requestDatetime: LocalDateTime
+processDatetime: LocalDateTime
+createdAt: LocalDateTime
}
}
package "repository.jpa" {
interface KosCustomerJpaRepository {
+findByPhoneNumber(phoneNumber: String): Optional<KosCustomerEntity>
+findByCustomerId(customerId: String): Optional<KosCustomerEntity>
}
interface KosProductJpaRepository {
+findByProductCode(productCode: String): Optional<KosProductEntity>
+findBySaleStatus(saleStatus: String): List<KosProductEntity>
}
interface KosBillJpaRepository {
+findByPhoneNumberAndBillMonth(phoneNumber: String, billMonth: String): Optional<KosBillEntity>
+findByCustomerIdAndBillMonth(customerId: String, billMonth: String): Optional<KosBillEntity>
}
interface KosUsageJpaRepository {
+findByPhoneNumberAndUsageMonth(phoneNumber: String, usageMonth: String): Optional<KosUsageEntity>
}
interface KosDiscountJpaRepository {
+findByPhoneNumberAndBillMonth(phoneNumber: String, billMonth: String): List<KosDiscountEntity>
}
interface KosContractJpaRepository {
+findByCustomerId(customerId: String): Optional<KosContractEntity>
+findByPhoneNumber(phoneNumber: String): Optional<KosContractEntity>
}
interface KosInstallmentJpaRepository {
+findByCustomerIdAndStatus(customerId: String, status: String): List<KosInstallmentEntity>
}
interface KosTerminationFeeJpaRepository {
+findByCustomerId(customerId: String): Optional<KosTerminationFeeEntity>
}
interface KosProductChangeHistoryJpaRepository {
+findByRequestId(requestId: String): Optional<KosProductChangeHistoryEntity>
+findByPhoneNumberOrderByRequestDatetimeDesc(phoneNumber: String): List<KosProductChangeHistoryEntity>
}
}
package "config" {
class MockProperties {
+scenario: MockScenarioProperties
+delay: MockDelayProperties
+error: MockErrorProperties
}
class MockScenarioProperties {
+successLineNumbers: List<String>
+noDataLineNumbers: List<String>
+systemErrorLineNumbers: List<String>
+timeoutLineNumbers: List<String>
}
class MockDelayProperties {
+billInquiry: Long
+productChange: Long
+timeout: Long
}
class MockErrorProperties {
+rate: Double
+enabled: Boolean
}
class KosMockConfig {
+mockProperties(): MockProperties
+mockScenarioService(properties: MockProperties): MockScenarioService
+taskExecutor(): ThreadPoolTaskExecutor
}
}
}
package "Common Module" {
package "dto" {
class ApiResponse<T> {
-success: boolean
-message: String
-data: T
-timestamp: LocalDateTime
}
class ErrorResponse {
-code: String
-message: String
-details: String
-timestamp: LocalDateTime
}
}
package "entity" {
abstract class BaseTimeEntity {
#createdAt: LocalDateTime
#updatedAt: LocalDateTime
}
}
package "exception" {
enum ErrorCode {
BILL002("KOS 연동 실패")
PROD001("상품변경 실패")
SYS002("외부 연동 실패")
}
class BusinessException {
-errorCode: ErrorCode
-details: String
}
}
}
' 관계 설정
KosMockController --> KosMockService : uses
KosMockService --> BillDataService : uses
KosMockService --> ProductDataService : uses
KosMockService --> MockScenarioService : uses
BillDataService --> MockDataRepository : uses
ProductDataService --> MockDataRepository : uses
ProductDataService --> ProductValidationService : uses
ProductValidationService --> MockDataRepository : uses
MockScenarioService --> MockProperties : uses
MockDataRepositoryImpl ..|> MockDataRepository : implements
MockDataRepositoryImpl --> KosCustomerJpaRepository : uses
MockDataRepositoryImpl --> KosProductJpaRepository : uses
MockDataRepositoryImpl --> KosBillJpaRepository : uses
MockDataRepositoryImpl --> KosUsageJpaRepository : uses
MockDataRepositoryImpl --> KosDiscountJpaRepository : uses
MockDataRepositoryImpl --> KosContractJpaRepository : uses
MockDataRepositoryImpl --> KosInstallmentJpaRepository : uses
MockDataRepositoryImpl --> KosTerminationFeeJpaRepository : uses
MockDataRepositoryImpl --> KosProductChangeHistoryJpaRepository : uses
KosCustomerJpaRepository --> KosCustomerEntity : manages
KosProductJpaRepository --> KosProductEntity : manages
KosBillJpaRepository --> KosBillEntity : manages
KosUsageJpaRepository --> KosUsageEntity : manages
KosDiscountJpaRepository --> KosDiscountEntity : manages
KosContractJpaRepository --> KosContractEntity : manages
KosInstallmentJpaRepository --> KosInstallmentEntity : manages
KosTerminationFeeJpaRepository --> KosTerminationFeeEntity : manages
KosProductChangeHistoryJpaRepository --> KosProductChangeHistoryEntity : manages
' BaseTimeEntity 상속
KosCustomerEntity --|> BaseTimeEntity
KosProductEntity --|> BaseTimeEntity
KosBillEntity --|> BaseTimeEntity
KosUsageEntity --|> BaseTimeEntity
KosDiscountEntity --|> BaseTimeEntity
KosContractEntity --|> BaseTimeEntity
KosInstallmentEntity --|> BaseTimeEntity
KosTerminationFeeEntity --|> BaseTimeEntity
KosProductChangeHistoryEntity --|> BaseTimeEntity
' DTO 관계
KosMockController --> KosBillRequest : uses
KosMockController --> KosProductChangeRequest : uses
KosMockController --> MockBillResponse : creates
KosMockController --> MockProductChangeResponse : creates
KosMockController --> KosCustomerResponse : creates
KosMockController --> KosProductResponse : creates
KosMockController --> KosLineStatusResponse : creates
MockBillResponse --> BillInfo : contains
MockProductChangeResponse --> ProductChangeResult : contains
BillInfo --> ContractInfo : contains
BillInfo --> BillAmount : contains
BillInfo --> UsageInfo : contains
BillInfo --> InstallmentInfo : contains
BillInfo --> TerminationFeeInfo : contains
BillInfo --> BillingPaymentInfo : contains
BillInfo --> DiscountInfo : contains
' 공통 모듈 사용
KosMockController --> ApiResponse : uses
KosMockService --> BusinessException : throws
ProductValidationService --> ValidationResult : creates
MockScenarioService --> MockScenario : creates
@enduml
+302
View File
@@ -0,0 +1,302 @@
# 패키지 구조도 - 통신요금 관리 서비스
## 전체 패키지 구조
```
com.unicorn.phonebill/
├── common/ # 공통 모듈
│ ├── dto/
│ │ ├── ApiResponse.java # 표준 API 응답 구조
│ │ ├── ErrorResponse.java # 오류 응답 구조
│ │ ├── JwtTokenDTO.java # JWT 토큰 정보
│ │ └── JwtTokenVerifyDTO.java # JWT 토큰 검증 결과
│ ├── entity/
│ │ └── BaseTimeEntity.java # 기본 엔티티 클래스
│ ├── exception/
│ │ ├── BusinessException.java # 비즈니스 예외
│ │ ├── InfraException.java # 인프라 예외
│ │ └── ErrorCode.java # 오류 코드 열거형
│ ├── util/
│ │ ├── DateUtil.java # 날짜 유틸리티
│ │ ├── SecurityUtil.java # 보안 유틸리티
│ │ └── ValidatorUtil.java # 검증 유틸리티
│ ├── config/
│ │ └── JpaConfig.java # JPA 설정
│ └── aop/
│ └── LoggingAspect.java # 로깅 AOP
├── auth/ # 인증 서비스
│ ├── AuthApplication.java # Spring Boot 메인 클래스
│ ├── controller/
│ │ └── AuthController.java # 인증 API 컨트롤러
│ ├── dto/
│ │ ├── LoginRequest.java # 로그인 요청
│ │ ├── LoginResponse.java # 로그인 응답
│ │ ├── LogoutRequest.java # 로그아웃 요청
│ │ ├── TokenRefreshRequest.java # 토큰 갱신 요청
│ │ ├── TokenRefreshResponse.java # 토큰 갱신 응답
│ │ ├── PermissionRequest.java # 권한 확인 요청
│ │ ├── PermissionResponse.java # 권한 확인 응답
│ │ ├── UserInfoResponse.java # 사용자 정보 응답
│ │ └── TokenVerifyResponse.java # 토큰 검증 응답
│ ├── service/
│ │ ├── AuthService.java # 인증 서비스 인터페이스
│ │ ├── AuthServiceImpl.java # 인증 서비스 구현체
│ │ ├── TokenService.java # 토큰 서비스 인터페이스
│ │ ├── TokenServiceImpl.java # 토큰 서비스 구현체
│ │ ├── PermissionService.java # 권한 서비스 인터페이스
│ │ └── PermissionServiceImpl.java # 권한 서비스 구현체
│ ├── domain/
│ │ ├── User.java # 사용자 도메인 모델
│ │ ├── UserSession.java # 사용자 세션 도메인 모델
│ │ ├── LoginResult.java # 로그인 결과
│ │ ├── TokenInfo.java # 토큰 정보
│ │ ├── Permission.java # 권한 정보
│ │ └── UserInfo.java # 사용자 상세 정보
│ ├── repository/
│ │ ├── UserRepository.java # 사용자 리포지토리 인터페이스
│ │ ├── UserRepositoryImpl.java # 사용자 리포지토리 구현체
│ │ ├── SessionRepository.java # 세션 리포지토리 인터페이스
│ │ ├── SessionRepositoryImpl.java # 세션 리포지토리 구현체
│ │ ├── entity/
│ │ │ ├── UserEntity.java # 사용자 엔티티
│ │ │ ├── UserSessionEntity.java # 사용자 세션 엔티티
│ │ │ └── UserPermissionEntity.java # 사용자 권한 엔티티
│ │ └── jpa/
│ │ ├── UserJpaRepository.java # 사용자 JPA 리포지토리
│ │ ├── UserSessionJpaRepository.java # 세션 JPA 리포지토리
│ │ └── UserPermissionJpaRepository.java # 권한 JPA 리포지토리
│ └── config/
│ ├── SecurityConfig.java # 보안 설정
│ ├── JwtConfig.java # JWT 설정
│ └── RedisConfig.java # Redis 설정
├── bill/ # 요금조회 서비스
│ ├── BillApplication.java # Spring Boot 메인 클래스
│ ├── controller/
│ │ └── BillController.java # 요금조회 API 컨트롤러
│ ├── dto/
│ │ ├── BillMenuResponse.java # 요금조회 메뉴 응답
│ │ ├── BillInquiryRequest.java # 요금조회 요청
│ │ ├── BillInquiryResponse.java # 요금조회 응답
│ │ ├── BillStatusResponse.java # 요금조회 상태 응답
│ │ ├── BillHistoryRequest.java # 요금조회 이력 요청
│ │ ├── BillHistoryResponse.java # 요금조회 이력 응답
│ │ ├── BillDetailInfo.java # 요금 상세 정보
│ │ ├── DiscountInfo.java # 할인 정보
│ │ └── UsageInfo.java # 사용량 정보
│ ├── service/
│ │ ├── BillService.java # 요금조회 서비스 인터페이스
│ │ ├── BillServiceImpl.java # 요금조회 서비스 구현체
│ │ ├── BillCacheService.java # 요금 캐시 서비스 인터페이스
│ │ ├── BillCacheServiceImpl.java # 요금 캐시 서비스 구현체
│ │ ├── KosClientService.java # KOS 클라이언트 서비스 인터페이스
│ │ ├── KosClientServiceImpl.java # KOS 클라이언트 서비스 구현체
│ │ ├── BillHistoryService.java # 요금조회 이력 서비스 인터페이스
│ │ └── BillHistoryServiceImpl.java # 요금조회 이력 서비스 구현체
│ ├── domain/
│ │ ├── BillInfo.java # 요금 정보 도메인 모델
│ │ ├── BillHistory.java # 요금조회 이력 도메인 모델
│ │ ├── KosBillRequest.java # KOS 요금조회 요청
│ │ ├── KosBillResponse.java # KOS 요금조회 응답
│ │ ├── BillInquiryResult.java # 요금조회 결과
│ │ ├── BillStatus.java # 요금조회 상태 열거형
│ │ └── RequestStatus.java # 요청 상태 열거형
│ ├── repository/
│ │ ├── BillHistoryRepository.java # 요금조회 이력 리포지토리 인터페이스
│ │ ├── BillHistoryRepositoryImpl.java # 요금조회 이력 리포지토리 구현체
│ │ ├── entity/
│ │ │ ├── BillHistoryEntity.java # 요금조회 이력 엔티티
│ │ │ └── BillRequestEntity.java # 요금조회 요청 엔티티
│ │ └── jpa/
│ │ ├── BillHistoryJpaRepository.java # 요금조회 이력 JPA 리포지토리
│ │ └── BillRequestJpaRepository.java # 요금조회 요청 JPA 리포지토리
│ └── config/
│ ├── RestTemplateConfig.java # RestTemplate 설정
│ ├── CacheConfig.java # 캐시 설정
│ ├── CircuitBreakerConfig.java # Circuit Breaker 설정
│ ├── RetryConfig.java # 재시도 설정
│ ├── AsyncConfig.java # 비동기 설정
│ ├── KosApiConfig.java # KOS API 설정
│ └── SwaggerConfig.java # Swagger 설정
├── product/ # 상품변경 서비스
│ ├── ProductApplication.java # Spring Boot 메인 클래스
│ ├── controller/
│ │ └── ProductController.java # 상품변경 API 컨트롤러
│ ├── dto/
│ │ ├── ProductMenuResponse.java # 상품변경 메뉴 응답
│ │ ├── CustomerInfoResponse.java # 고객정보 응답
│ │ ├── AvailableProductsResponse.java # 변경가능 상품 응답
│ │ ├── ProductValidationRequest.java # 상품변경 사전체크 요청
│ │ ├── ProductValidationResponse.java # 상품변경 사전체크 응답
│ │ ├── ProductChangeRequest.java # 상품변경 요청
│ │ ├── ProductChangeResponse.java # 상품변경 응답
│ │ ├── ProductChangeResultResponse.java # 상품변경 결과 응답
│ │ ├── ProductChangeHistoryRequest.java # 상품변경 이력 요청
│ │ ├── ProductChangeHistoryResponse.java # 상품변경 이력 응답
│ │ ├── ProductInfo.java # 상품 정보
│ │ ├── CustomerInfo.java # 고객 정보
│ │ ├── ValidationResult.java # 검증 결과
│ │ ├── ChangeResult.java # 변경 결과
│ │ ├── ProductStatus.java # 상품 상태 열거형
│ │ ├── ChangeStatus.java # 변경 상태 열거형
│ │ └── ValidationStatus.java # 검증 상태 열거형
│ ├── service/
│ │ ├── ProductService.java # 상품변경 서비스 인터페이스
│ │ ├── ProductServiceImpl.java # 상품변경 서비스 구현체
│ │ ├── ProductValidationService.java # 상품변경 검증 서비스 인터페이스
│ │ ├── ProductValidationServiceImpl.java # 상품변경 검증 서비스 구현체
│ │ ├── ProductCacheService.java # 상품 캐시 서비스 인터페이스
│ │ ├── ProductCacheServiceImpl.java # 상품 캐시 서비스 구현체
│ │ ├── KosClientService.java # KOS 클라이언트 서비스 인터페이스
│ │ ├── KosClientServiceImpl.java # KOS 클라이언트 서비스 구현체
│ │ ├── ProductHistoryService.java # 상품변경 이력 서비스 인터페이스
│ │ ├── ProductHistoryServiceImpl.java # 상품변경 이력 서비스 구현체
│ │ ├── AsyncService.java # 비동기 서비스 인터페이스
│ │ └── AsyncServiceImpl.java # 비동기 서비스 구현체
│ ├── domain/
│ │ ├── Product.java # 상품 도메인 모델
│ │ ├── Customer.java # 고객 도메인 모델
│ │ ├── ProductChangeHistory.java # 상품변경 이력 도메인 모델
│ │ ├── ProductValidation.java # 상품변경 검증 도메인 모델
│ │ ├── KosProductChangeRequest.java # KOS 상품변경 요청
│ │ ├── KosProductChangeResponse.java # KOS 상품변경 응답
│ │ ├── ProductChangeResult.java # 상품변경 결과
│ │ ├── ChangeRequestStatus.java # 변경요청 상태 열거형
│ │ └── ValidationErrorType.java # 검증 오류 타입 열거형
│ ├── repository/
│ │ ├── ProductChangeHistoryRepository.java # 상품변경 이력 리포지토리 인터페이스
│ │ ├── ProductChangeHistoryRepositoryImpl.java # 상품변경 이력 리포지토리 구현체
│ │ ├── ProductRepository.java # 상품 리포지토리 인터페이스
│ │ ├── ProductRepositoryImpl.java # 상품 리포지토리 구현체
│ │ ├── entity/
│ │ │ ├── ProductChangeHistoryEntity.java # 상품변경 이력 엔티티
│ │ │ └── ProductEntity.java # 상품 엔티티
│ │ └── jpa/
│ │ ├── ProductChangeHistoryJpaRepository.java # 상품변경 이력 JPA 리포지토리
│ │ └── ProductJpaRepository.java # 상품 JPA 리포지토리
│ ├── external/
│ │ ├── KosApiClient.java # KOS API 클라이언트
│ │ ├── KosAdapterService.java # KOS 어댑터 서비스
│ │ └── CircuitBreakerService.java # Circuit Breaker 서비스
│ ├── config/
│ │ ├── RestTemplateConfig.java # RestTemplate 설정
│ │ ├── CacheConfig.java # 캐시 설정
│ │ ├── CircuitBreakerConfig.java # Circuit Breaker 설정
│ │ ├── AsyncConfig.java # 비동기 설정
│ │ ├── RetryConfig.java # 재시도 설정
│ │ ├── KosApiConfig.java # KOS API 설정
│ │ └── SwaggerConfig.java # Swagger 설정
│ └── exception/
│ ├── ProductNotFoundException.java # 상품 없음 예외
│ ├── ProductValidationException.java # 상품변경 검증 예외
│ ├── ProductChangeException.java # 상품변경 예외
│ └── KosIntegrationException.java # KOS 연동 예외
└── kosmock/ # KOS Mock 서비스
├── KosMockApplication.java # Spring Boot 메인 클래스
├── controller/
│ └── KosMockController.java # KOS Mock API 컨트롤러
├── service/
│ ├── KosMockService.java # KOS Mock 서비스 인터페이스
│ ├── KosMockServiceImpl.java # KOS Mock 서비스 구현체
│ ├── BillDataService.java # 요금 데이터 서비스 인터페이스
│ ├── BillDataServiceImpl.java # 요금 데이터 서비스 구현체
│ ├── ProductDataService.java # 상품 데이터 서비스 인터페이스
│ ├── ProductDataServiceImpl.java # 상품 데이터 서비스 구현체
│ ├── MockScenarioService.java # Mock 시나리오 서비스 인터페이스
│ ├── MockScenarioServiceImpl.java # Mock 시나리오 서비스 구현체
│ ├── ProductValidationService.java # 상품 검증 서비스 인터페이스
│ └── ProductValidationServiceImpl.java # 상품 검증 서비스 구현체
├── dto/
│ ├── KosBillRequest.java # KOS 요금조회 요청
│ ├── KosBillResponse.java # KOS 요금조회 응답
│ ├── KosProductChangeRequest.java # KOS 상품변경 요청
│ ├── KosProductChangeResponse.java # KOS 상품변경 응답
│ ├── KosCustomerInfoResponse.java # KOS 고객정보 응답
│ ├── KosAvailableProductsResponse.java # KOS 변경가능 상품 응답
│ ├── KosLineStatusResponse.java # KOS 회선상태 응답
│ ├── MockScenario.java # Mock 시나리오
│ ├── KosBillInfo.java # KOS 요금 정보
│ ├── KosProductInfo.java # KOS 상품 정보
│ ├── KosCustomerInfo.java # KOS 고객 정보
│ ├── KosUsageInfo.java # KOS 사용량 정보
│ ├── KosDiscountInfo.java # KOS 할인 정보
│ ├── KosContractInfo.java # KOS 약정 정보
│ ├── KosInstallmentInfo.java # KOS 할부 정보
│ ├── KosTerminationFeeInfo.java # KOS 해지비용 정보
│ └── KosValidationResult.java # KOS 검증 결과
├── repository/
│ ├── MockDataRepository.java # Mock 데이터 리포지토리 인터페이스
│ ├── MockDataRepositoryImpl.java # Mock 데이터 리포지토리 구현체
│ ├── entity/
│ │ ├── KosCustomerEntity.java # KOS 고객정보 엔티티
│ │ ├── KosProductEntity.java # KOS 상품정보 엔티티
│ │ ├── KosBillEntity.java # KOS 요금정보 엔티티
│ │ ├── KosUsageEntity.java # KOS 사용량정보 엔티티
│ │ ├── KosDiscountEntity.java # KOS 할인정보 엔티티
│ │ ├── KosContractEntity.java # KOS 약정정보 엔티티
│ │ ├── KosInstallmentEntity.java # KOS 할부정보 엔티티
│ │ ├── KosTerminationFeeEntity.java # KOS 해지비용정보 엔티티
│ │ └── KosProductChangeHistoryEntity.java # KOS 상품변경이력 엔티티
│ └── jpa/
│ ├── KosCustomerJpaRepository.java # KOS 고객정보 JPA 리포지토리
│ ├── KosProductJpaRepository.java # KOS 상품정보 JPA 리포지토리
│ ├── KosBillJpaRepository.java # KOS 요금정보 JPA 리포지토리
│ ├── KosUsageJpaRepository.java # KOS 사용량정보 JPA 리포지토리
│ ├── KosDiscountJpaRepository.java # KOS 할인정보 JPA 리포지토리
│ ├── KosContractJpaRepository.java # KOS 약정정보 JPA 리포지토리
│ ├── KosInstallmentJpaRepository.java # KOS 할부정보 JPA 리포지토리
│ ├── KosTerminationFeeJpaRepository.java # KOS 해지비용정보 JPA 리포지토리
│ └── KosProductChangeHistoryJpaRepository.java # KOS 상품변경이력 JPA 리포지토리
└── config/
├── MockDataConfig.java # Mock 데이터 설정
├── MockDelayConfig.java # Mock 지연 설정
└── SwaggerConfig.java # Swagger 설정
```
## 패키지 구성 요약
### 📊 서비스별 클래스 수
| 서비스 | 총 클래스 수 | Controller | DTO | Service | Domain | Repository | Config/기타 |
|--------|-------------|------------|-----|---------|--------|------------|------------|
| Common | 14개 | - | 4개 | - | - | 1개 | 9개 |
| Auth | 26개 | 1개 | 9개 | 6개 | 6개 | 7개 | 3개 |
| Bill-Inquiry | 29개 | 1개 | 9개 | 8개 | 7개 | 4개 | 7개 |
| Product-Change | 44개 | 1개 | 17개 | 12개 | 9개 | 4개 | 7개 |
| KOS-Mock | 39개 | 1개 | 16개 | 10개 | - | 20개 | 3개 |
| **전체** | **152개** | **4개** | **55개** | **36개** | **22개** | **36개** | **29개** |
### 🏗️ 아키텍처 패턴별 구성
**Layered 아키텍처 (Auth, Bill-Inquiry, Product-Change)**
- Controller → Service → Domain → Repository → Entity 계층 구조
- 각 계층별 명확한 책임 분리
- 인터페이스 기반 의존성 주입
**간단한 Layered 아키텍처 (KOS-Mock)**
- Controller → Service → Repository → Entity 구조
- Mock 데이터 제공에 특화된 단순 구조
- 시나리오 기반 응답 처리
### 🔗 주요 공통 컴포넌트 활용
**모든 서비스에서 공통 사용**
- `ApiResponse<T>`: 표준 API 응답 구조
- `BaseTimeEntity`: 생성/수정 시간 자동 관리
- `ErrorCode`: 표준화된 오류 코드 체계
- `BusinessException`/`InfraException`: 계층별 예외 처리
**공통 설정 및 유틸리티**
- `JpaConfig`: JPA 설정 통합
- `LoggingAspect`: AOP 기반 로깅
- `DateUtil`, `SecurityUtil`, `ValidatorUtil`: 공통 유틸리티
### 📝 설계 원칙 준수 현황
**유저스토리 완벽 매칭**: 10개 유저스토리의 모든 요구사항 반영
**API 설계서 완전 일치**: Controller 메소드가 API 엔드포인트와 정확히 매칭
**내부시퀀스 반영**: Service, Repository 클래스가 시퀀스 다이어그램과 일치
**아키텍처 패턴 적용**: 서비스별 지정된 아키텍처 패턴 정확히 구현
**관계 표현 완료**: 상속, 구현, 의존성, 연관, 집약, 컴포지션 관계 모두 표현
**공통 컴포넌트 활용**: BaseTimeEntity, ApiResponse 등 공통 클래스 적극 활용
이 패키지 구조는 마이크로서비스 아키텍처에 최적화되어 있으며, 각 서비스의 독립성과 확장성을 보장합니다.
@@ -0,0 +1,255 @@
@startuml
!theme mono
title Product-Change Service - 간단 클래스 설계
' ============= 패키지 정의 =============
package "com.unicorn.phonebill.product" {
' ============= Controller Layer =============
package "controller" {
class ProductController {
' API 매핑 정보는 아래 Note에 표시
}
}
' ============= DTO Layer =============
package "dto" {
' Request DTOs
class ProductChangeValidationRequest
class ProductChangeRequest
' Response DTOs
class ProductMenuResponse
class CustomerInfoResponse
class AvailableProductsResponse
class ProductChangeValidationResponse
class ProductChangeResponse
class ProductChangeResultResponse
class ProductChangeHistoryResponse
' Data DTOs
class ProductInfo
class CustomerInfo
class ContractInfo
class MenuItem
class ValidationDetail
class ProductChangeHistoryItem
class PaginationInfo
' Enums
enum ValidationResult {
SUCCESS
FAILURE
}
enum ProcessStatus {
PENDING
PROCESSING
COMPLETED
FAILED
}
enum LineStatus {
ACTIVE
SUSPENDED
TERMINATED
}
enum CheckType {
PRODUCT_AVAILABLE
OPERATOR_MATCH
LINE_STATUS
}
enum CheckResult {
PASS
FAIL
}
}
' ============= Service Layer =============
package "service" {
interface ProductService
class ProductServiceImpl
class ProductValidationService
class ProductCacheService
class KosClientService
class CircuitBreakerService
class RetryService
}
' ============= Domain Layer =============
package "domain" {
class Product
class ProductChangeHistory
class ProductChangeResult
class ProductStatus
enum ProductStatus {
ACTIVE
INACTIVE
DISCONTINUED
}
enum CacheType {
CUSTOMER_PRODUCT
CURRENT_PRODUCT
AVAILABLE_PRODUCTS
PRODUCT_STATUS
LINE_STATUS
}
}
' ============= Repository Layer =============
package "repository" {
interface ProductRepository
interface ProductChangeHistoryRepository
package "entity" {
class ProductChangeHistoryEntity
}
package "jpa" {
interface ProductChangeHistoryJpaRepository
}
}
' ============= Config Layer =============
package "config" {
class RestTemplateConfig
class CacheConfig
class CircuitBreakerConfig
class KosProperties
}
' ============= External Interface =============
package "external" {
class KosRequest
class KosResponse
class KosAdapterService
}
' ============= Exception Classes =============
package "exception" {
class ProductChangeException
class ProductValidationException
class KosConnectionException
class CircuitBreakerException
}
}
' Import Common Classes
class "com.unicorn.phonebill.common.dto.ApiResponse" as ApiResponse
class "com.unicorn.phonebill.common.entity.BaseTimeEntity" as BaseTimeEntity
class "com.unicorn.phonebill.common.exception.BusinessException" as BusinessException
' ============= 관계 설정 =============
' Controller Layer Relationships
ProductController --> ProductService : "uses"
ProductController --> ApiResponse : "returns"
' DTO Layer Relationships
ProductMenuResponse --> ProductInfo : "contains"
CustomerInfoResponse --> CustomerInfo : "contains"
CustomerInfo --> ProductInfo : "contains"
CustomerInfo --> ContractInfo : "contains"
AvailableProductsResponse --> ProductInfo : "contains"
ProductChangeValidationResponse --> ValidationDetail : "contains"
ProductChangeResponse --> ProductInfo : "contains"
ProductChangeHistoryResponse --> ProductChangeHistoryItem : "contains"
ProductChangeHistoryResponse --> PaginationInfo : "contains"
' Service Layer Relationships
ProductService <|.. ProductServiceImpl : "implements"
ProductServiceImpl --> KosClientService : "uses"
ProductServiceImpl --> ProductValidationService : "uses"
ProductServiceImpl --> ProductCacheService : "uses"
ProductServiceImpl --> ProductChangeHistoryRepository : "uses"
ProductValidationService --> ProductRepository : "uses"
ProductValidationService --> ProductCacheService : "uses"
ProductValidationService --> KosClientService : "uses"
ProductCacheService --> KosClientService : "uses"
KosClientService --> CircuitBreakerService : "uses"
KosClientService --> RetryService : "uses"
KosClientService --> KosAdapterService : "uses"
' Domain Layer Relationships
ProductChangeResult --> Product : "contains"
' Repository Layer Relationships
ProductRepository <-- ProductServiceImpl : "uses"
ProductChangeHistoryRepository <-- ProductServiceImpl : "uses"
ProductChangeHistoryRepository --> ProductChangeHistoryJpaRepository : "uses"
ProductChangeHistoryEntity --|> BaseTimeEntity : "extends"
' Config Layer Relationships
RestTemplateConfig --> KosClientService : "configures"
CacheConfig --> ProductCacheService : "configures"
CircuitBreakerConfig --> CircuitBreakerService : "configures"
KosProperties --> KosClientService : "configures"
' External Interface Relationships
KosAdapterService --> KosRequest : "creates"
KosAdapterService --> KosResponse : "processes"
KosClientService --> KosAdapterService : "uses"
' Exception Relationships
ProductChangeException --|> BusinessException : "extends"
ProductValidationException --|> BusinessException : "extends"
KosConnectionException --|> BusinessException : "extends"
CircuitBreakerException --|> BusinessException : "extends"
ProductValidationException --> ValidationDetail : "contains"
' ============= API 매핑표 =============
note top of ProductController
**ProductController API 매핑표**
┌─────────────────────────────────────────────────────────────────────────────┐
│ HTTP Method │ URL Path │ Method Name │
├─────────────────────────────────────────────────────────────────────────────┤
│ GET │ /products/menu │ getProductMenu() │
│ GET │ /products/customer/{line} │ getCustomerInfo(lineNumber) │
│ GET │ /products/available │ getAvailableProducts() │
│ POST │ /products/change/validation │ validateProductChange() │
│ POST │ /products/change │ requestProductChange() │
│ GET │ /products/change/{requestId} │ getProductChangeResult() │
│ GET │ /products/history │ getProductChangeHistory() │
└─────────────────────────────────────────────────────────────────────────────┘
**주요 기능**
• UFR-PROD-010: 상품변경 메뉴 조회
• UFR-PROD-020: 상품변경 화면 데이터 조회
• UFR-PROD-030: 상품변경 요청 및 사전체크
• UFR-PROD-040: KOS 연동 상품변경 처리
**설계 특징**
• Layered 아키텍처 패턴 적용
• KOS 연동을 위한 Circuit Breaker 패턴
• Redis 캐시를 활용한 성능 최적화
• 비동기 이력 저장 처리
end note
@enduml
+722
View File
@@ -0,0 +1,722 @@
@startuml
!theme mono
title Product-Change Service - 상세 클래스 설계
' ============= 패키지 정의 =============
package "com.unicorn.phonebill.product" {
' ============= Controller Layer =============
package "controller" {
class ProductController {
-productService: ProductService
-log: Logger
+getProductMenu(): ResponseEntity<ApiResponse<ProductMenuResponse>>
+getCustomerInfo(lineNumber: String): ResponseEntity<ApiResponse<CustomerInfoResponse>>
+getAvailableProducts(currentProductCode: String, operatorCode: String): ResponseEntity<ApiResponse<AvailableProductsResponse>>
+validateProductChange(request: ProductChangeValidationRequest): ResponseEntity<ApiResponse<ProductChangeValidationResponse>>
+requestProductChange(request: ProductChangeRequest): ResponseEntity<ApiResponse<ProductChangeResponse>>
+getProductChangeResult(requestId: String): ResponseEntity<ApiResponse<ProductChangeResultResponse>>
+getProductChangeHistory(lineNumber: String, startDate: LocalDate, endDate: LocalDate, page: int, size: int): ResponseEntity<ApiResponse<ProductChangeHistoryResponse>>
-extractUserIdFromToken(): String
}
}
' ============= DTO Layer =============
package "dto" {
' Request DTOs
class ProductChangeValidationRequest {
-lineNumber: String
-currentProductCode: String
-targetProductCode: String
+getLineNumber(): String
+getCurrentProductCode(): String
+getTargetProductCode(): String
}
class ProductChangeRequest {
-lineNumber: String
-currentProductCode: String
-targetProductCode: String
-requestDate: LocalDateTime
-changeEffectiveDate: LocalDate
+getLineNumber(): String
+getCurrentProductCode(): String
+getTargetProductCode(): String
+getRequestDate(): LocalDateTime
+getChangeEffectiveDate(): LocalDate
}
' Response DTOs
class ProductMenuResponse {
-customerId: String
-lineNumber: String
-currentProduct: ProductInfo
-menuItems: List<MenuItem>
+getCustomerId(): String
+getLineNumber(): String
+getCurrentProduct(): ProductInfo
+getMenuItems(): List<MenuItem>
}
class CustomerInfoResponse {
-customerInfo: CustomerInfo
+getCustomerInfo(): CustomerInfo
}
class AvailableProductsResponse {
-products: List<ProductInfo>
-totalCount: int
+getProducts(): List<ProductInfo>
+getTotalCount(): int
}
class ProductChangeValidationResponse {
-validationResult: ValidationResult
-validationDetails: List<ValidationDetail>
-failureReason: String
+getValidationResult(): ValidationResult
+getValidationDetails(): List<ValidationDetail>
+getFailureReason(): String
}
class ProductChangeResponse {
-requestId: String
-processStatus: ProcessStatus
-resultCode: String
-resultMessage: String
-changedProduct: ProductInfo
-processedAt: LocalDateTime
+getRequestId(): String
+getProcessStatus(): ProcessStatus
+getResultCode(): String
+getResultMessage(): String
+getChangedProduct(): ProductInfo
+getProcessedAt(): LocalDateTime
}
class ProductChangeResultResponse {
-requestId: String
-lineNumber: String
-processStatus: ProcessStatus
-currentProductCode: String
-targetProductCode: String
-requestedAt: LocalDateTime
-processedAt: LocalDateTime
-resultCode: String
-resultMessage: String
-failureReason: String
+getRequestId(): String
+getLineNumber(): String
+getProcessStatus(): ProcessStatus
+getCurrentProductCode(): String
+getTargetProductCode(): String
+getRequestedAt(): LocalDateTime
+getProcessedAt(): LocalDateTime
+getResultCode(): String
+getResultMessage(): String
+getFailureReason(): String
}
class ProductChangeHistoryResponse {
-history: List<ProductChangeHistoryItem>
-pagination: PaginationInfo
+getHistory(): List<ProductChangeHistoryItem>
+getPagination(): PaginationInfo
}
' Data DTOs
class ProductInfo {
-productCode: String
-productName: String
-monthlyFee: BigDecimal
-dataAllowance: String
-voiceAllowance: String
-smsAllowance: String
-isAvailable: boolean
-operatorCode: String
+getProductCode(): String
+getProductName(): String
+getMonthlyFee(): BigDecimal
+getDataAllowance(): String
+getVoiceAllowance(): String
+getSmsAllowance(): String
+isAvailable(): boolean
+getOperatorCode(): String
}
class CustomerInfo {
-customerId: String
-lineNumber: String
-customerName: String
-currentProduct: ProductInfo
-lineStatus: LineStatus
-contractInfo: ContractInfo
+getCustomerId(): String
+getLineNumber(): String
+getCustomerName(): String
+getCurrentProduct(): ProductInfo
+getLineStatus(): LineStatus
+getContractInfo(): ContractInfo
}
class ContractInfo {
-contractDate: LocalDate
-termEndDate: LocalDate
-earlyTerminationFee: BigDecimal
+getContractDate(): LocalDate
+getTermEndDate(): LocalDate
+getEarlyTerminationFee(): BigDecimal
}
class MenuItem {
-menuId: String
-menuName: String
-available: boolean
+getMenuId(): String
+getMenuName(): String
+isAvailable(): boolean
}
class ValidationDetail {
-checkType: CheckType
-result: CheckResult
-message: String
+getCheckType(): CheckType
+getResult(): CheckResult
+getMessage(): String
}
class ProductChangeHistoryItem {
-requestId: String
-lineNumber: String
-processStatus: ProcessStatus
-currentProductCode: String
-currentProductName: String
-targetProductCode: String
-targetProductName: String
-requestedAt: LocalDateTime
-processedAt: LocalDateTime
-resultMessage: String
+getRequestId(): String
+getLineNumber(): String
+getProcessStatus(): ProcessStatus
+getCurrentProductCode(): String
+getCurrentProductName(): String
+getTargetProductCode(): String
+getTargetProductName(): String
+getRequestedAt(): LocalDateTime
+getProcessedAt(): LocalDateTime
+getResultMessage(): String
}
class PaginationInfo {
-page: int
-size: int
-totalElements: long
-totalPages: int
-hasNext: boolean
-hasPrevious: boolean
+getPage(): int
+getSize(): int
+getTotalElements(): long
+getTotalPages(): int
+isHasNext(): boolean
+isHasPrevious(): boolean
}
' Enum Classes
enum ValidationResult {
SUCCESS
FAILURE
}
enum ProcessStatus {
PENDING
PROCESSING
COMPLETED
FAILED
}
enum LineStatus {
ACTIVE
SUSPENDED
TERMINATED
}
enum CheckType {
PRODUCT_AVAILABLE
OPERATOR_MATCH
LINE_STATUS
}
enum CheckResult {
PASS
FAIL
}
}
' ============= Service Layer =============
package "service" {
interface ProductService {
+getProductMenuData(userId: String): ProductMenuResponse
+getCustomerInfo(lineNumber: String): CustomerInfo
+getAvailableProducts(currentProductCode: String, operatorCode: String): List<ProductInfo>
+validateProductChange(request: ProductChangeValidationRequest): ProductChangeValidationResponse
+requestProductChange(request: ProductChangeRequest, userId: String): ProductChangeResponse
+getProductChangeResult(requestId: String): ProductChangeResultResponse
+getProductChangeHistory(lineNumber: String, startDate: LocalDate, endDate: LocalDate, pageable: Pageable): ProductChangeHistoryResponse
}
class ProductServiceImpl {
-kosClientService: KosClientService
-productValidationService: ProductValidationService
-productCacheService: ProductCacheService
-productChangeHistoryRepository: ProductChangeHistoryRepository
-log: Logger
+getProductMenuData(userId: String): ProductMenuResponse
+getCustomerInfo(lineNumber: String): CustomerInfo
+getAvailableProducts(currentProductCode: String, operatorCode: String): List<ProductInfo>
+validateProductChange(request: ProductChangeValidationRequest): ProductChangeValidationResponse
+requestProductChange(request: ProductChangeRequest, userId: String): ProductChangeResponse
+getProductChangeResult(requestId: String): ProductChangeResultResponse
+getProductChangeHistory(lineNumber: String, startDate: LocalDate, endDate: LocalDate, pageable: Pageable): ProductChangeHistoryResponse
-filterAvailableProducts(products: List<ProductInfo>, currentProductCode: String): List<ProductInfo>
-invalidateCustomerCache(userId: String): void
}
class ProductValidationService {
-productRepository: ProductRepository
-productCacheService: ProductCacheService
-kosClientService: KosClientService
-log: Logger
+validateProductChange(request: ProductChangeValidationRequest): ValidationResult
+validateProductAvailability(productCode: String): ValidationDetail
+validateOperatorMatch(customerOperatorCode: String, productCode: String): ValidationDetail
+validateLineStatus(lineNumber: String): ValidationDetail
-createValidationDetail(checkType: CheckType, result: CheckResult, message: String): ValidationDetail
}
class ProductCacheService {
-redisTemplate: RedisTemplate<String, Object>
-kosClientService: KosClientService
-log: Logger
+getCustomerProductInfo(userId: String): CustomerInfo
+getCurrentProductInfo(userId: String): ProductInfo
+getAvailableProducts(): List<ProductInfo>
+getProductStatus(productCode: String): ProductStatus
+getLineStatus(lineNumber: String): LineStatus
+invalidateCustomerCache(userId: String): void
+cacheCustomerProductInfo(userId: String, customerInfo: CustomerInfo): void
+cacheAvailableProducts(products: List<ProductInfo>): void
-getCacheKey(prefix: String, identifier: String): String
-getCacheTTL(cacheType: CacheType): Duration
}
class KosClientService {
-restTemplate: RestTemplate
-circuitBreakerService: CircuitBreakerService
-retryService: RetryService
-kosProperties: KosProperties
-log: Logger
+getCustomerInfo(userId: String): CustomerInfo
+getCurrentProduct(userId: String): ProductInfo
+getAvailableProducts(): List<ProductInfo>
+getLineStatus(lineNumber: String): LineStatus
+processProductChange(changeRequest: ProductChangeRequest): ProductChangeResult
-buildKosRequest(request: Object): KosRequest
-handleKosResponse(response: ResponseEntity<String>): KosResponse
-mapToCustomerInfo(kosResponse: KosResponse): CustomerInfo
-mapToProductInfo(kosResponse: KosResponse): ProductInfo
}
class CircuitBreakerService {
-circuitBreakerRegistry: CircuitBreakerRegistry
-log: Logger
+isCallAllowed(serviceName: String): boolean
+recordSuccess(serviceName: String): void
+recordFailure(serviceName: String): void
+getCircuitBreakerState(serviceName: String): CircuitBreaker.State
-configureCircuitBreaker(serviceName: String): CircuitBreaker
}
class RetryService {
-retryRegistry: RetryRegistry
-log: Logger
+<T> executeWithRetry(operation: Supplier<T>, serviceName: String): T
+<T> executeProductChangeWithRetry(operation: Supplier<T>): T
-configureRetry(serviceName: String): Retry
-isRetryableException(exception: Exception): boolean
}
}
' ============= Domain Layer =============
package "domain" {
class Product {
-productCode: String
-productName: String
-monthlyFee: BigDecimal
-dataAllowance: String
-voiceAllowance: String
-smsAllowance: String
-status: ProductStatus
-operatorCode: String
-isAvailable: boolean
+getProductCode(): String
+getProductName(): String
+getMonthlyFee(): BigDecimal
+getDataAllowance(): String
+getVoiceAllowance(): String
+getSmsAllowance(): String
+getStatus(): ProductStatus
+getOperatorCode(): String
+isAvailable(): boolean
+canChangeTo(targetProduct: Product): boolean
+isSameOperator(operatorCode: String): boolean
}
class ProductChangeHistory {
-requestId: String
-userId: String
-lineNumber: String
-currentProductCode: String
-targetProductCode: String
-processStatus: ProcessStatus
-requestedAt: LocalDateTime
-processedAt: LocalDateTime
-resultCode: String
-resultMessage: String
-failureReason: String
+getRequestId(): String
+getUserId(): String
+getLineNumber(): String
+getCurrentProductCode(): String
+getTargetProductCode(): String
+getProcessStatus(): ProcessStatus
+getRequestedAt(): LocalDateTime
+getProcessedAt(): LocalDateTime
+getResultCode(): String
+getResultMessage(): String
+getFailureReason(): String
+isCompleted(): boolean
+isFailed(): boolean
+markAsCompleted(resultCode: String, resultMessage: String): void
+markAsFailed(failureReason: String): void
}
class ProductChangeResult {
-requestId: String
-success: boolean
-resultCode: String
-resultMessage: String
-newProduct: Product
-processedAt: LocalDateTime
+getRequestId(): String
+isSuccess(): boolean
+getResultCode(): String
+getResultMessage(): String
+getNewProduct(): Product
+getProcessedAt(): LocalDateTime
+createSuccessResult(requestId: String, newProduct: Product, message: String): ProductChangeResult
+createFailureResult(requestId: String, errorCode: String, errorMessage: String): ProductChangeResult
}
class ProductStatus {
-productCode: String
-status: String
-salesStatus: String
-operatorCode: String
+getProductCode(): String
+getStatus(): String
+getSalesStatus(): String
+getOperatorCode(): String
+isAvailableForSale(): boolean
+isActive(): boolean
}
' Enum Classes
enum ProductStatus {
ACTIVE
INACTIVE
DISCONTINUED
}
enum CacheType {
CUSTOMER_PRODUCT(Duration.ofHours(4))
CURRENT_PRODUCT(Duration.ofHours(2))
AVAILABLE_PRODUCTS(Duration.ofHours(24))
PRODUCT_STATUS(Duration.ofHours(1))
LINE_STATUS(Duration.ofMinutes(30))
-ttl: Duration
+CacheType(ttl: Duration)
+getTtl(): Duration
}
}
' ============= Repository Layer =============
package "repository" {
interface ProductRepository {
+getProductStatus(productCode: String): ProductStatus
+saveChangeRequest(changeRequest: ProductChangeHistory): ProductChangeHistory
+updateProductChangeStatus(requestId: String, status: ProcessStatus, resultCode: String, resultMessage: String): void
+findProductChangeHistory(lineNumber: String, startDate: LocalDate, endDate: LocalDate, pageable: Pageable): Page<ProductChangeHistory>
}
interface ProductChangeHistoryRepository {
+save(history: ProductChangeHistory): ProductChangeHistory
+findByRequestId(requestId: String): Optional<ProductChangeHistory>
+findByLineNumberAndRequestedAtBetween(lineNumber: String, startDate: LocalDateTime, endDate: LocalDateTime, pageable: Pageable): Page<ProductChangeHistory>
+findByUserIdAndRequestedAtBetween(userId: String, startDate: LocalDateTime, endDate: LocalDateTime, pageable: Pageable): Page<ProductChangeHistory>
+existsByRequestId(requestId: String): boolean
}
package "entity" {
class ProductChangeHistoryEntity {
-id: Long
-requestId: String
-userId: String
-lineNumber: String
-currentProductCode: String
-currentProductName: String
-targetProductCode: String
-targetProductName: String
-processStatus: ProcessStatus
-requestedAt: LocalDateTime
-processedAt: LocalDateTime
-resultCode: String
-resultMessage: String
-failureReason: String
-createdAt: LocalDateTime
-updatedAt: LocalDateTime
+getId(): Long
+getRequestId(): String
+getUserId(): String
+getLineNumber(): String
+getCurrentProductCode(): String
+getCurrentProductName(): String
+getTargetProductCode(): String
+getTargetProductName(): String
+getProcessStatus(): ProcessStatus
+getRequestedAt(): LocalDateTime
+getProcessedAt(): LocalDateTime
+getResultCode(): String
+getResultMessage(): String
+getFailureReason(): String
+getCreatedAt(): LocalDateTime
+getUpdatedAt(): LocalDateTime
+toDomain(): ProductChangeHistory
+fromDomain(history: ProductChangeHistory): ProductChangeHistoryEntity
}
}
package "jpa" {
interface ProductChangeHistoryJpaRepository {
+findByRequestId(requestId: String): Optional<ProductChangeHistoryEntity>
+findByLineNumberAndRequestedAtBetween(lineNumber: String, startDate: LocalDateTime, endDate: LocalDateTime, pageable: Pageable): Page<ProductChangeHistoryEntity>
+findByUserIdAndRequestedAtBetween(userId: String, startDate: LocalDateTime, endDate: LocalDateTime, pageable: Pageable): Page<ProductChangeHistoryEntity>
+existsByRequestId(requestId: String): boolean
+countByProcessStatus(status: ProcessStatus): long
}
}
}
' ============= Config Layer =============
package "config" {
class RestTemplateConfig {
+restTemplate(): RestTemplate
+kosRestTemplate(): RestTemplate
+connectionPoolTaskExecutor(): ThreadPoolTaskExecutor
-createConnectionPoolManager(): PoolingHttpClientConnectionManager
-createRequestConfig(): RequestConfig
}
class CacheConfig {
+redisConnectionFactory(): LettuceConnectionFactory
+redisTemplate(): RedisTemplate<String, Object>
+cacheManager(): RedisCacheManager
+redisCacheConfiguration(): RedisCacheConfiguration
-createRedisConfiguration(): RedisStandaloneConfiguration
}
class CircuitBreakerConfig {
+circuitBreakerRegistry(): CircuitBreakerRegistry
+retryRegistry(): RetryRegistry
+kosCircuitBreaker(): CircuitBreaker
+kosRetry(): Retry
-createCircuitBreakerConfig(): CircuitBreakerConfig
-createRetryConfig(): RetryConfig
}
class KosProperties {
-baseUrl: String
-connectTimeout: Duration
-readTimeout: Duration
-maxRetries: int
-retryDelay: Duration
-circuitBreakerFailureRateThreshold: float
-circuitBreakerMinimumNumberOfCalls: int
-circuitBreakerWaitDurationInOpenState: Duration
+getBaseUrl(): String
+getConnectTimeout(): Duration
+getReadTimeout(): Duration
+getMaxRetries(): int
+getRetryDelay(): Duration
+getCircuitBreakerFailureRateThreshold(): float
+getCircuitBreakerMinimumNumberOfCalls(): int
+getCircuitBreakerWaitDurationInOpenState(): Duration
}
}
' External Interface Classes (KOS 연동)
package "external" {
class KosRequest {
-transactionId: String
-lineNumber: String
-currentProductCode: String
-newProductCode: String
-changeReason: String
-effectiveDate: String
+getTransactionId(): String
+getLineNumber(): String
+getCurrentProductCode(): String
+getNewProductCode(): String
+getChangeReason(): String
+getEffectiveDate(): String
}
class KosResponse {
-resultCode: String
-resultMessage: String
-transactionId: String
-data: Object
+getResultCode(): String
+getResultMessage(): String
+getTransactionId(): String
+getData(): Object
+isSuccess(): boolean
+getErrorDetail(): String
}
class KosAdapterService {
-kosProperties: KosProperties
-restTemplate: RestTemplate
-objectMapper: ObjectMapper
-log: Logger
+callKosProductChange(changeRequest: ProductChangeRequest): ProductChangeResult
+getCustomerInfoFromKos(userId: String): CustomerInfo
+getAvailableProductsFromKos(): List<ProductInfo>
+getLineStatusFromKos(lineNumber: String): LineStatus
-buildKosUrl(endpoint: String): String
-createHttpHeaders(): HttpHeaders
-handleKosError(response: ResponseEntity<String>): void
}
}
' Exception Classes
package "exception" {
class ProductChangeException {
-errorCode: String
-details: String
+ProductChangeException(message: String)
+ProductChangeException(errorCode: String, message: String, details: String)
+getErrorCode(): String
+getDetails(): String
}
class ProductValidationException {
-validationErrors: List<ValidationDetail>
+ProductValidationException(message: String, validationErrors: List<ValidationDetail>)
+getValidationErrors(): List<ValidationDetail>
}
class KosConnectionException {
-serviceName: String
+KosConnectionException(serviceName: String, message: String, cause: Throwable)
+getServiceName(): String
}
class CircuitBreakerException {
-serviceName: String
+CircuitBreakerException(serviceName: String, message: String)
+getServiceName(): String
}
}
}
' Import Common Classes
class "com.unicorn.phonebill.common.dto.ApiResponse" as ApiResponse
class "com.unicorn.phonebill.common.entity.BaseTimeEntity" as BaseTimeEntity
class "com.unicorn.phonebill.common.exception.ErrorCode" as ErrorCode
class "com.unicorn.phonebill.common.exception.BusinessException" as BusinessException
' ============= 관계 설정 =============
' Controller Layer Relationships
ProductController --> ProductService : "uses"
ProductController --> ApiResponse : "returns"
' DTO Layer Relationships
ProductMenuResponse --> ProductInfo : "contains"
CustomerInfoResponse --> CustomerInfo : "contains"
CustomerInfo --> ProductInfo : "contains"
CustomerInfo --> ContractInfo : "contains"
AvailableProductsResponse --> ProductInfo : "contains"
ProductChangeValidationResponse --> ValidationDetail : "contains"
ProductChangeResponse --> ProductInfo : "contains"
ProductChangeHistoryResponse --> ProductChangeHistoryItem : "contains"
ProductChangeHistoryResponse --> PaginationInfo : "contains"
ValidationDetail --> CheckType : "uses"
ValidationDetail --> CheckResult : "uses"
' Service Layer Relationships
ProductService <|.. ProductServiceImpl : "implements"
ProductServiceImpl --> KosClientService : "uses"
ProductServiceImpl --> ProductValidationService : "uses"
ProductServiceImpl --> ProductCacheService : "uses"
ProductServiceImpl --> ProductChangeHistoryRepository : "uses"
ProductValidationService --> ProductRepository : "uses"
ProductValidationService --> ProductCacheService : "uses"
ProductValidationService --> KosClientService : "uses"
ProductCacheService --> KosClientService : "uses"
KosClientService --> CircuitBreakerService : "uses"
KosClientService --> RetryService : "uses"
KosClientService --> KosAdapterService : "uses"
' Domain Layer Relationships
ProductChangeHistory --> ProcessStatus : "uses"
Product --> ProductStatus : "uses"
ProductChangeResult --> Product : "contains"
ProductStatus --> ProductStatus : "uses"
' Repository Layer Relationships
ProductRepository <-- ProductServiceImpl : "uses"
ProductChangeHistoryRepository <-- ProductServiceImpl : "uses"
ProductChangeHistoryRepository --> ProductChangeHistoryJpaRepository : "uses"
ProductChangeHistoryEntity --|> BaseTimeEntity : "extends"
ProductChangeHistoryEntity --> ProcessStatus : "uses"
' Config Layer Relationships
RestTemplateConfig --> KosClientService : "configures"
CacheConfig --> ProductCacheService : "configures"
CircuitBreakerConfig --> CircuitBreakerService : "configures"
KosProperties --> KosClientService : "configures"
' External Interface Relationships
KosAdapterService --> KosRequest : "creates"
KosAdapterService --> KosResponse : "processes"
KosClientService --> KosAdapterService : "uses"
' Exception Relationships
ProductChangeException --|> BusinessException : "extends"
ProductValidationException --|> BusinessException : "extends"
KosConnectionException --|> BusinessException : "extends"
CircuitBreakerException --|> BusinessException : "extends"
ProductValidationException --> ValidationDetail : "contains"
ProductChangeException --> ErrorCode : "uses"
@enduml