@startuml !theme mono title Product-Change Service - 상세 클래스 설계 ' ============= 패키지 정의 ============= package "com.unicorn.phonebill.product" { ' ============= Controller Layer ============= package "controller" { class ProductController { -productService: ProductService -log: Logger +getProductMenu(): ResponseEntity> +getCustomerInfo(lineNumber: String): ResponseEntity> +getAvailableProducts(currentProductCode: String, operatorCode: String): ResponseEntity> +validateProductChange(request: ProductChangeValidationRequest): ResponseEntity> +requestProductChange(request: ProductChangeRequest): ResponseEntity> +getProductChangeResult(requestId: String): ResponseEntity> +getProductChangeHistory(lineNumber: String, startDate: LocalDate, endDate: LocalDate, page: int, size: int): ResponseEntity> -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 +getCustomerId(): String +getLineNumber(): String +getCurrentProduct(): ProductInfo +getMenuItems(): List } class CustomerInfoResponse { -customerInfo: CustomerInfo +getCustomerInfo(): CustomerInfo } class AvailableProductsResponse { -products: List -totalCount: int +getProducts(): List +getTotalCount(): int } class ProductChangeValidationResponse { -validationResult: ValidationResult -validationDetails: List -failureReason: String +getValidationResult(): ValidationResult +getValidationDetails(): List +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 -pagination: PaginationInfo +getHistory(): List +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 +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 +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, currentProductCode: String): List -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 -kosClientService: KosClientService -log: Logger +getCustomerProductInfo(userId: String): CustomerInfo +getCurrentProductInfo(userId: String): ProductInfo +getAvailableProducts(): List +getProductStatus(productCode: String): ProductStatus +getLineStatus(lineNumber: String): LineStatus +invalidateCustomerCache(userId: String): void +cacheCustomerProductInfo(userId: String, customerInfo: CustomerInfo): void +cacheAvailableProducts(products: List): 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 +getLineStatus(lineNumber: String): LineStatus +processProductChange(changeRequest: ProductChangeRequest): ProductChangeResult -buildKosRequest(request: Object): KosRequest -handleKosResponse(response: ResponseEntity): 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 + executeWithRetry(operation: Supplier, serviceName: String): T + executeProductChangeWithRetry(operation: Supplier): 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 } interface ProductChangeHistoryRepository { +save(history: ProductChangeHistory): ProductChangeHistory +findByRequestId(requestId: String): Optional +findByLineNumberAndRequestedAtBetween(lineNumber: String, startDate: LocalDateTime, endDate: LocalDateTime, pageable: Pageable): Page +findByUserIdAndRequestedAtBetween(userId: String, startDate: LocalDateTime, endDate: LocalDateTime, pageable: Pageable): Page +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 +findByLineNumberAndRequestedAtBetween(lineNumber: String, startDate: LocalDateTime, endDate: LocalDateTime, pageable: Pageable): Page +findByUserIdAndRequestedAtBetween(userId: String, startDate: LocalDateTime, endDate: LocalDateTime, pageable: Pageable): Page +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 +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 +getLineStatusFromKos(lineNumber: String): LineStatus -buildKosUrl(endpoint: String): String -createHttpHeaders(): HttpHeaders -handleKosError(response: ResponseEntity): 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 +ProductValidationException(message: String, validationErrors: List) +getValidationErrors(): List } 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