- 회원가입: 사업자번호 암호화 위치 명시, 성능 지표 추가, 에러 코드 표준화 (USER_001, USER_002) - 로그인: 비동기 처리 설명 추가, Rate Limiting 보안 강화, 성능 목표 추가, 에러 코드 표준화 (AUTH_001) - 프로필수정: 트랜잭션 범위 명확화, Optimistic Locking 추가, 동시성 제어, 에러 코드 표준화 (USER_003~005) - 로그아웃: API Gateway 연계 시나리오, 로그아웃 이벤트 발행, 성능 목표 추가, 에러 코드 표준화 (AUTH_002) - .gitignore 추가: .npm-global 및 기타 임시 파일 제외 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
209 lines
7.5 KiB
Plaintext
209 lines
7.5 KiB
Plaintext
@startuml user-회원가입
|
|
!theme mono
|
|
|
|
title User Service - 회원가입 내부 시퀀스 (UFR-USER-010)
|
|
|
|
participant "UserController" as Controller <<API Layer>>
|
|
participant "UserService" as Service <<Business Layer>>
|
|
participant "BusinessValidator" as Validator <<Business Layer>>
|
|
participant "UserRepository" as UserRepo <<Data Layer>>
|
|
participant "StoreRepository" as StoreRepo <<Data Layer>>
|
|
participant "PasswordEncoder" as PwdEncoder <<Utility>>
|
|
participant "JwtTokenProvider" as JwtProvider <<Utility>>
|
|
participant "Redis\nCache" as Redis <<E>>
|
|
participant "User DB\n(PostgreSQL)" as UserDB <<E>>
|
|
participant "국세청 API" as NTSApi <<E>>
|
|
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: SELECT * FROM users\nWHERE phone_number = ?
|
|
activate UserDB
|
|
UserDB --> UserRepo: 조회 결과
|
|
deactivate UserDB
|
|
UserRepo --> Service: Optional<User>
|
|
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: BEGIN TRANSACTION
|
|
activate UserDB
|
|
|
|
Service -> UserRepo: save(User)\n(name, phoneNumber, email,\npasswordHash, createdAt)
|
|
activate UserRepo
|
|
UserRepo -> UserDB: INSERT INTO users\n(name, phone_number, email,\npassword_hash, created_at)\nRETURNING user_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: INSERT INTO stores\n(user_id, store_name, industry,\naddress, business_number_encrypted,\nbusiness_hours)\nRETURNING store_id
|
|
UserDB --> StoreRepo: store_id
|
|
StoreRepo --> Service: Store 엔티티\n(storeId 포함)
|
|
deactivate StoreRepo
|
|
|
|
Service -> UserDB: COMMIT TRANSACTION
|
|
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
|