@startuml user-회원가입 !theme mono title User Service - 회원가입 내부 시퀀스 (UFR-USER-010) participant "UserController" as Controller <> participant "UserService" as Service <> participant "BusinessValidator" as Validator <> participant "UserRepository" as UserRepo <> participant "StoreRepository" as StoreRepo <> participant "PasswordEncoder" as PwdEncoder <> participant "JwtTokenProvider" as JwtProvider <> participant "Redis\nCache" as Redis <> participant "User DB\n(PostgreSQL)" as UserDB <> participant "국세청 API" as NTSApi <> actor Client note over Controller, NTSApi **UFR-USER-010: 회원가입** - 기본 정보: 이름, 전화번호, 이메일, 비밀번호 - 매장 정보: 매장명, 업종, 주소, 영업시간, 사업자번호 - 사업자번호 검증 (국세청 API) - 트랜잭션 처리 - JWT 토큰 발급 end note Client -> Controller: POST /api/users/register\n(RegisterRequest DTO) activate Controller Controller -> Controller: @Valid 어노테이션 검증\n(이메일 형식, 비밀번호 8자 이상 등) Controller -> Service: register(RegisterRequest) activate Service == 1단계: 중복 사용자 확인 == Service -> UserRepo: findByPhoneNumber(phoneNumber) activate UserRepo UserRepo -> UserDB: 전화번호로 사용자 조회\n(중복 가입 확인) activate UserDB UserDB --> UserRepo: 조회 결과 deactivate UserDB UserRepo --> Service: Optional deactivate UserRepo alt 중복 사용자 존재 Service --> Controller: throw DuplicateUserException\n("이미 가입된 전화번호입니다") Controller --> Client: 400 Bad Request\n{"code": "USER_001",\n"error": "이미 가입된 전화번호입니다"} deactivate Service deactivate Controller else 신규 사용자 == 2단계: 사업자번호 검증 == Service -> Validator: validateBusinessNumber(businessNumber) activate Validator Validator -> Redis: GET user:business:{사업자번호} activate Redis Redis --> Validator: 캐시 확인 결과 deactivate Redis alt 캐시 HIT (검증 결과 있음) Validator -> Validator: 캐시된 검증 결과 사용\n(응답 시간: 0.1초) else 캐시 MISS (검증 필요) note right of Validator **Circuit Breaker 설정** - 실패율: 50% 초과 시 Open - 타임아웃: 5초 - Half-Open: 30초 후 전환 end note Validator -> NTSApi: POST /사업자번호_검증\n(사업자번호)\n[Circuit Breaker, Timeout 5초] activate NTSApi alt 국세청 API 정상 응답 NTSApi --> Validator: 200 OK\n{"valid": true, "status": "영업중"} deactivate NTSApi Validator -> Redis: SET user:business:{사업자번호}\n검증 결과 (TTL 7일) activate Redis Redis --> Validator: 캐싱 완료 deactivate Redis else 국세청 API 장애 (Circuit Breaker Open) NTSApi --> Validator: 500 Internal Server Error\n또는 Timeout deactivate NTSApi note right of Validator **Resilience 패턴 적용** - Circuit Breaker: Open - Retry: 최대 3회 (1초, 2초, 4초) - Fallback: 검증 스킵 (수동 확인 안내) end note Validator -> Validator: Fallback 실행:\n사업자번호 검증 스킵\n(수동 확인 안내 플래그 설정) end end alt 사업자번호 검증 실패 (휴폐업 등) Validator --> Service: throw BusinessNumberInvalidException\n("유효하지 않은 사업자번호입니다") deactivate Validator Service --> Controller: BusinessNumberInvalidException Controller --> Client: 400 Bad Request\n{"code": "USER_002",\n"error": "유효하지 않은 사업자번호입니다.\n휴폐업 여부를 확인해주세요."} deactivate Service deactivate Controller else 사업자번호 검증 성공 Validator --> Service: ValidationResult\n(valid: true, needsManualCheck: false) deactivate Validator == 3단계: 비밀번호 해싱 == Service -> PwdEncoder: encode(rawPassword) activate PwdEncoder PwdEncoder -> PwdEncoder: bcrypt 해싱\n(Cost Factor 10) PwdEncoder --> Service: passwordHash deactivate PwdEncoder == 4단계: 사업자번호 암호화 == Service -> Service: EncryptionUtil 호출 준비 note right of Service **암호화 처리** - AES-256-GCM 모드 사용 - 환경변수에서 암호화 키 로드 end note Service -> Service: encryptedBusinessNumber =\nEncryptionUtil.encrypt(businessNumber) == 5단계: 데이터베이스 트랜잭션 == Service -> UserDB: 트랜잭션 시작 activate UserDB Service -> UserRepo: save(User)\n(name, phoneNumber, email,\npasswordHash, createdAt) activate UserRepo UserRepo -> UserDB: 사용자 정보 저장\n(이름, 전화번호, 이메일,\n비밀번호해시, 생성일시)\n사용자ID 반환 UserDB --> UserRepo: user_id UserRepo --> Service: User 엔티티\n(userId 포함) deactivate UserRepo Service -> StoreRepo: save(Store)\n(userId, storeName, industry,\naddress, businessNumberEncrypted,\nbusinessHours) activate StoreRepo StoreRepo -> UserDB: 매장 정보 저장\n(사용자ID, 매장명, 업종,\n주소, 암호화된사업자번호,\n영업시간)\n매장ID 반환 UserDB --> StoreRepo: store_id StoreRepo --> Service: Store 엔티티\n(storeId 포함) deactivate StoreRepo Service -> UserDB: 트랜잭션 커밋 UserDB --> Service: 트랜잭션 커밋 완료 deactivate UserDB == 6단계: JWT 토큰 생성 == Service -> JwtProvider: generateToken(userId, role) activate JwtProvider JwtProvider -> JwtProvider: JWT 토큰 생성\n(Claims: userId, role=OWNER,\nexp=7일) JwtProvider --> Service: JWT 토큰 deactivate JwtProvider == 7단계: 세션 저장 == Service -> Redis: SET user:session:{token}\n(userId, role, TTL 7일) activate Redis Redis --> Service: 세션 저장 완료 deactivate Redis == 8단계: 응답 반환 == Service -> Service: 응답 DTO 생성\n(RegisterResponse) Service --> Controller: RegisterResponse\n(token, userId, userName,\nstoreId, storeName) deactivate Service Controller --> Client: 201 Created\n{"token": "jwt_token",\n"userId": 123,\n"userName": "홍길동",\n"storeId": 456,\n"storeName": "맛있는집"} deactivate Controller end end note over Controller, NTSApi **Transaction Rollback 처리** - 트랜잭션 실패 시 자동 Rollback - User/Store INSERT 중 하나라도 실패 시 전체 롤백 - 예외: DataAccessException, ConstraintViolationException **Resilience 패턴 요약** - Circuit Breaker: 국세청 API (실패율 50% 초과 시 Open) - Retry: 최대 3회 (지수 백오프: 1초, 2초, 4초) - Timeout: 5초 - Fallback: 사업자번호 검증 스킵 (수동 확인 안내) **보안 처리** - 비밀번호: bcrypt 해싱 (Cost Factor 10) - 사업자번호: AES-256-GCM 암호화 (EncryptionUtil) **성능 목표** - 평균 응답 시간: 2.0초 이내 (국세청 API 포함) - P95 응답 시간: 3.0초 이내 - 캐시 HIT 시: 0.8초 이내 **에러 코드** - USER_001: 중복 사용자 - USER_002: 사업자번호 검증 실패 end note @enduml