@startuml !theme mono title Bill-Inquiry Service - 상세 클래스 설계 ' 패키지별 클래스 구조 package "com.unicorn.phonebill.bill" { package "controller" { class BillController { -billService: BillService -jwtTokenUtil: JwtTokenUtil +getBillMenu(authorization: String): ResponseEntity> +inquireBill(request: BillInquiryRequest, authorization: String): ResponseEntity> +getBillInquiryStatus(requestId: String, authorization: String): ResponseEntity> +getBillHistory(lineNumber: String, startDate: String, endDate: String, page: int, size: int, status: String, authorization: String): ResponseEntity> -extractUserInfoFromToken(authorization: String): JwtTokenVerifyDTO -validateRequestParameters(request: Object): void } } package "dto" { ' API Request/Response DTOs class BillMenuData { -customerInfo: CustomerInfo -availableMonths: List -currentMonth: String +BillMenuData(customerInfo: CustomerInfo, availableMonths: List, currentMonth: String) +getCustomerInfo(): CustomerInfo +getAvailableMonths(): List +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 -pagination: PaginationInfo +BillHistoryData(items: List, pagination: PaginationInfo) +getItems(): List +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 -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 -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): 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 } class RetryServiceImpl { -maxRetries: int -retryDelayMs: long +executeWithRetry(operation: Supplier): 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 -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 +setDiscountInfo(discountInfo: List): 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 } 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 } 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 +findByUserIdAndRequestTimeBetweenOrderByRequestTimeDesc(userId: String, startDate: LocalDateTime, endDate: LocalDateTime, pageable: Pageable): Page +findByUserIdAndLineNumberAndStatusOrderByRequestTimeDesc(userId: String, lineNumber: String, status: String, pageable: Pageable): Page +save(entity: BillHistoryEntity): BillHistoryEntity +getCustomerInfo(userId: String): CustomerInfo } interface KosInquiryHistoryRepository { +save(entity: KosInquiryHistoryEntity): KosInquiryHistoryEntity +findByLineNumberAndInquiryMonthOrderByRequestTimeDesc(lineNumber: String, inquiryMonth: String): List } 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 +findByUserIdAndRequestTimeBetweenOrderByRequestTimeDesc(userId: String, startDate: LocalDateTime, endDate: LocalDateTime, pageable: Pageable): Page +findByUserIdAndLineNumberAndStatusOrderByRequestTimeDesc(userId: String, lineNumber: String, status: String, pageable: Pageable): Page +countByUserIdAndLineNumber(userId: String, lineNumber: String): long } interface KosInquiryHistoryJpaRepository { +findByLineNumberAndInquiryMonthOrderByRequestTimeDesc(lineNumber: String, inquiryMonth: String): List +countByResultCode(resultCode: String): long } } } package "config" { class RestTemplateConfig { +kosRestTemplate(): RestTemplate +mvnoRestTemplate(): RestTemplate +kosHttpMessageConverters(): List> +kosRequestInterceptors(): List +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 +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