User 서비스 내부 시퀀스 다이어그램 개선

- 회원가입: 사업자번호 암호화 위치 명시, 성능 지표 추가, 에러 코드 표준화 (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>
This commit is contained in:
박세원 2025-10-22 14:37:25 +09:00
parent d876763477
commit f7f16e2d18
5 changed files with 196 additions and 47 deletions

32
.gitignore vendored Normal file
View File

@ -0,0 +1,32 @@
# Node
node_modules/
npm-debug.log*
yarn-debug.log*
yarn-error.log*
.npm-global/
# IDE
.idea/
.vscode/
*.swp
*.swo
*~
# OS
.DS_Store
Thumbs.db
# Build
dist/
build/
*.log
# Environment
.env
.env.local
.env.*.local
# Temporary
tmp/
temp/
*.tmp

View File

@ -36,7 +36,7 @@ deactivate JwtProvider
alt JWT 토큰 무효 alt JWT 토큰 무효
AuthService --> Controller: throw InvalidTokenException\n("유효하지 않은 토큰입니다") AuthService --> Controller: throw InvalidTokenException\n("유효하지 않은 토큰입니다")
Controller --> Client: 401 Unauthorized\n{"error": "유효하지 않은 토큰입니다"} Controller --> Client: 401 Unauthorized\n{"code": "AUTH_002",\n"error": "유효하지 않은 토큰입니다"}
deactivate AuthService deactivate AuthService
deactivate Controller deactivate Controller
@ -70,6 +70,14 @@ else JWT 토큰 유효
- 만료되지 않은 JWT 토큰을 강제로 무효화 - 만료되지 않은 JWT 토큰을 강제로 무효화
- Redis에 토큰을 Blacklist에 추가 (TTL: 남은 만료 시간) - Redis에 토큰을 Blacklist에 추가 (TTL: 남은 만료 시간)
- API Gateway에서 Blacklist 확인 - API Gateway에서 Blacklist 확인
**API Gateway 연계 시나리오**
1. 로그아웃: AuthService가 Blacklist에 토큰 추가
2. 후속 API 요청: API Gateway가 Blacklist 확인
- Redis GET jwt:blacklist:{token}
- 존재하면: 401 Unauthorized 즉시 반환
- 존재하지 않으면: 백엔드 서비스로 라우팅
3. 만료 시간 도달: Redis TTL 만료로 자동 삭제
end note end note
AuthService -> JwtProvider: getRemainingExpiration(token) AuthService -> JwtProvider: getRemainingExpiration(token)
@ -85,9 +93,26 @@ else JWT 토큰 유효
deactivate Redis deactivate Redis
end end
== 4단계: 응답 반환 == == 4단계: 로그아웃 이벤트 발행 (선택적) ==
AuthService -> AuthService: 로그아웃 성공 로그 기록\n(userId, timestamp) note right of AuthService
**로그아웃 로깅 및 이벤트**
- 감사 로그 기록: userId, timestamp, IP
- 이벤트 발행: LOGOUT_SUCCESS
- 분석 데이터 수집: 세션 지속 시간, 활동 통계
end note
AuthService -> AuthService: 로그아웃 성공 로그 기록\n(userId, timestamp, sessionDuration)
AuthService ->> AuthService: publishEvent(LOGOUT_SUCCESS)
note right of AuthService
**이벤트 활용**
- 비동기 이벤트 처리
- 분석 시스템 연동
- 감사 로그 저장소 전송
end note
== 5단계: 응답 반환 ==
AuthService --> Controller: LogoutResponse\n(success: true) AuthService --> Controller: LogoutResponse\n(success: true)
deactivate AuthService deactivate AuthService
@ -101,14 +126,30 @@ note over Controller, Redis
- JWT 토큰 Blacklist: 만료 전 토큰 강제 무효화 - JWT 토큰 Blacklist: 만료 전 토큰 강제 무효화
- 멱등성 보장: 중복 로그아웃 요청에 안전 - 멱등성 보장: 중복 로그아웃 요청에 안전
- 세션 완전 삭제: Redis에서 세션 정보 제거 - 세션 완전 삭제: Redis에서 세션 정보 제거
- 감사 로그: userId, timestamp, IP, sessionDuration 기록
**API Gateway 연계**
- Blacklist 확인: GET jwt:blacklist:{token}
- 존재 시: 401 Unauthorized 즉시 반환
- TTL 자동 관리: 만료 시간 도달 시 자동 삭제
**클라이언트 측 처리** **클라이언트 측 처리**
- 프론트엔드: LocalStorage 또는 Cookie에서 JWT 토큰 삭제 - 프론트엔드: LocalStorage 또는 Cookie에서 JWT 토큰 삭제
- 로그인 화면으로 리다이렉트 - 로그인 화면으로 리다이렉트
- 모든 인증 헤더 제거
**성능 최적화** **성능 목표**
- Redis 삭제 연산: O(1) 시간 복잡도 - Redis 삭제 연산: O(1) 시간 복잡도
- 응답 시간: 0.1초 이내 - 평균 응답 시간: 0.1초 이내
- P95 응답 시간: 0.2초 이내
**이벤트 처리**
- LOGOUT_SUCCESS 이벤트 발행 (비동기)
- 감사 로그 저장소 전송
- 분석 데이터 수집
**에러 코드**
- AUTH_002: JWT 토큰 무효
end note end note
@enduml @enduml

View File

@ -47,7 +47,7 @@ deactivate Service
alt 사용자 없음 alt 사용자 없음
AuthService --> Controller: throw AuthenticationFailedException\n("전화번호 또는 비밀번호를 확인해주세요") AuthService --> Controller: throw AuthenticationFailedException\n("전화번호 또는 비밀번호를 확인해주세요")
Controller --> Client: 401 Unauthorized\n{"error": "전화번호 또는 비밀번호를\n확인해주세요"} Controller --> Client: 401 Unauthorized\n{"code": "AUTH_001",\n"error": "전화번호 또는 비밀번호를\n확인해주세요"}
deactivate AuthService deactivate AuthService
deactivate Controller deactivate Controller
@ -63,7 +63,7 @@ else 사용자 존재
alt 비밀번호 불일치 alt 비밀번호 불일치
AuthService --> Controller: throw AuthenticationFailedException\n("전화번호 또는 비밀번호를 확인해주세요") AuthService --> Controller: throw AuthenticationFailedException\n("전화번호 또는 비밀번호를 확인해주세요")
Controller --> Client: 401 Unauthorized\n{"error": "전화번호 또는 비밀번호를\n확인해주세요"} Controller --> Client: 401 Unauthorized\n{"code": "AUTH_001",\n"error": "전화번호 또는 비밀번호를\n확인해주세요"}
deactivate AuthService deactivate AuthService
deactivate Controller deactivate Controller
@ -92,6 +92,7 @@ else 사용자 존재
**비동기 처리** **비동기 처리**
- @Async 어노테이션 사용 - @Async 어노테이션 사용
- 로그인 응답 지연 방지 - 로그인 응답 지연 방지
- 별도 스레드풀에서 실행
end note end note
Service ->> UserRepo: updateLastLoginAt(userId) Service ->> UserRepo: updateLastLoginAt(userId)
activate UserRepo activate UserRepo
@ -104,6 +105,12 @@ else 사용자 존재
Service -->> AuthService: void (비동기 완료) Service -->> AuthService: void (비동기 완료)
deactivate Service deactivate Service
note over AuthService, Service
**비동기 화살표 설명**
- `->>`: 비동기 호출 (호출 후 즉시 반환)
- `-->>`: 비동기 응답 (별도 스레드에서 완료)
end note
== 6단계: 응답 반환 == == 6단계: 응답 반환 ==
AuthService -> AuthService: 응답 DTO 생성\n(LoginResponse) AuthService -> AuthService: 응답 DTO 생성\n(LoginResponse)
@ -118,12 +125,23 @@ end
note over Controller, UserDB note over Controller, UserDB
**보안 처리** **보안 처리**
- 비밀번호: bcrypt compare (원본 노출 안 됨) - 비밀번호: bcrypt compare (원본 노출 안 됨)
- 에러 메시지: 전화번호/비밀번호 구분 없이 동일 메시지 반환 (보안 강화) - 에러 메시지: 전화번호/비밀번호 구분 없이 동일 메시지 반환 (Timing Attack 방어)
- JWT 토큰: 7일 만료, 서버 세션과 동기화 - JWT 토큰: 7일 만료, 서버 세션과 동기화
**보안 강화 (향후 구현)**
- Rate Limiting: IP당 5분에 5회 로그인 실패 시 임시 차단 (15분)
- Account Lockout: 동일 계정 10회 실패 시 계정 잠금 (관리자 해제)
- MFA: 2단계 인증 추가 (SMS/TOTP)
- Anomaly Detection: 비정상 로그인 패턴 감지 (지역, 디바이스 변경)
**성능 최적화** **성능 최적화**
- 최종 로그인 시각 업데이트: 비동기 처리 (@Async) - 최종 로그인 시각 업데이트: 비동기 처리 (@Async)
- 응답 시간: 0.5초 목표 - 평균 응답 시간: 0.5초 이내
- P95 응답 시간: 1.0초 이내
- Redis 세션 조회: 0.1초 이내
**에러 코드**
- AUTH_001: 인증 실패 (전화번호 또는 비밀번호 불일치)
end note end note
@enduml @enduml

View File

@ -43,7 +43,7 @@ deactivate UserRepo
alt 사용자 없음 alt 사용자 없음
Service --> Controller: throw UserNotFoundException\n("사용자를 찾을 수 없습니다") Service --> Controller: throw UserNotFoundException\n("사용자를 찾을 수 없습니다")
Controller --> Client: 404 Not Found\n{"error": "사용자를 찾을 수 없습니다"} Controller --> Client: 404 Not Found\n{"code": "USER_003",\n"error": "사용자를 찾을 수 없습니다"}
deactivate Service deactivate Service
deactivate Controller deactivate Controller
@ -62,7 +62,7 @@ else 사용자 존재
alt 현재 비밀번호 불일치 alt 현재 비밀번호 불일치
Service --> Controller: throw InvalidPasswordException\n("현재 비밀번호가 일치하지 않습니다") Service --> Controller: throw InvalidPasswordException\n("현재 비밀번호가 일치하지 않습니다")
Controller --> Client: 400 Bad Request\n{"error": "현재 비밀번호가\n일치하지 않습니다"} Controller --> Client: 400 Bad Request\n{"code": "USER_004",\n"error": "현재 비밀번호가\n일치하지 않습니다"}
deactivate Service deactivate Service
deactivate Controller deactivate Controller
@ -79,7 +79,14 @@ else 사용자 존재
end end
end end
== 3단계: 기본 정보 업데이트 == == 3단계: 엔티티 수정 준비 (메모리상 변경) ==
note right of Service
**JPA Dirty Checking**
- 트랜잭션 시작 전 엔티티 수정 (메모리상)
- 트랜잭션 커밋 시 변경 감지하여 UPDATE 자동 실행
- 변경된 필드만 UPDATE 쿼리에 포함
end note
alt 이름 변경 alt 이름 변경
Service -> Service: user.setName(newName) Service -> Service: user.setName(newName)
@ -98,7 +105,7 @@ else 사용자 존재
Service -> Service: user.setEmail(newEmail) Service -> Service: user.setEmail(newEmail)
end end
== 4단계: 매장 정보 업데이트 == == 4단계: 매장 정보 수정 준비 (메모리상 변경) ==
Service -> StoreRepo: findByUserId(userId) Service -> StoreRepo: findByUserId(userId)
activate StoreRepo activate StoreRepo
@ -130,23 +137,41 @@ else 사용자 존재
Service -> UserDB: BEGIN TRANSACTION Service -> UserDB: BEGIN TRANSACTION
activate UserDB activate UserDB
note right of Service
**Optimistic Locking**
- @Version 필드로 동시 수정 감지
- 다른 트랜잭션이 먼저 수정한 경우
- OptimisticLockException 발생
end note
Service -> UserRepo: save(user) Service -> UserRepo: save(user)
activate UserRepo activate UserRepo
UserRepo -> UserDB: UPDATE users\nSET name = ?, phone_number = ?,\nemail = ?, password_hash = ?,\nupdated_at = NOW()\nWHERE user_id = ? UserRepo -> UserDB: UPDATE users\nSET name = ?, phone_number = ?,\nemail = ?, password_hash = ?,\nupdated_at = NOW(),\nversion = version + 1\nWHERE user_id = ? AND version = ?
UserDB --> UserRepo: 업데이트 완료 UserDB --> UserRepo: 업데이트 완료 (1 row affected)
UserRepo --> Service: User 엔티티 UserRepo --> Service: User 엔티티
deactivate UserRepo deactivate UserRepo
alt 동시성 충돌 (version 불일치)
UserRepo --> Service: throw OptimisticLockException
Service --> Controller: throw ConcurrentModificationException\n("다른 사용자가 수정 중입니다")
Controller --> Client: 409 Conflict\n{"code": "USER_005",\n"error": "다른 세션에서 프로필을\n수정했습니다.\n새로고침 후 다시 시도하세요"}
Service -> UserDB: ROLLBACK TRANSACTION
deactivate UserDB
deactivate Service
deactivate Controller
else 정상 업데이트
Service -> StoreRepo: save(store) Service -> StoreRepo: save(store)
activate StoreRepo activate StoreRepo
StoreRepo -> UserDB: UPDATE stores\nSET store_name = ?, industry = ?,\naddress = ?, business_hours = ?,\nupdated_at = NOW()\nWHERE store_id = ? StoreRepo -> UserDB: UPDATE stores\nSET store_name = ?, industry = ?,\naddress = ?, business_hours = ?,\nupdated_at = NOW(),\nversion = version + 1\nWHERE store_id = ? AND version = ?
UserDB --> StoreRepo: 업데이트 완료 UserDB --> StoreRepo: 업데이트 완료 (1 row affected)
StoreRepo --> Service: Store 엔티티 StoreRepo --> Service: Store 엔티티
deactivate StoreRepo deactivate StoreRepo
Service -> UserDB: COMMIT TRANSACTION Service -> UserDB: COMMIT TRANSACTION
UserDB --> Service: 트랜잭션 커밋 완료 UserDB --> Service: 트랜잭션 커밋 완료
deactivate UserDB deactivate UserDB
end
== 6단계: 캐시 무효화 (선택적) == == 6단계: 캐시 무효화 (선택적) ==
@ -171,21 +196,39 @@ else 사용자 존재
Controller --> Client: 200 OK\n{"userId": 123,\n"userName": "홍길동",\n"email": "hong@example.com",\n"storeId": 456,\n"storeName": "맛있는집"} Controller --> Client: 200 OK\n{"userId": 123,\n"userName": "홍길동",\n"email": "hong@example.com",\n"storeId": 456,\n"storeName": "맛있는집"}
deactivate Controller deactivate Controller
end
end end
note over Controller, UserDB note over Controller, UserDB
**Transaction Rollback 처리** **Transaction Rollback 처리**
- 트랜잭션 실패 시 자동 Rollback - 트랜잭션 실패 시 자동 Rollback
- User/Store UPDATE 중 하나라도 실패 시 전체 롤백 - User/Store UPDATE 중 하나라도 실패 시 전체 롤백
- OptimisticLockException 발생 시 409 Conflict 반환
**동시성 제어**
- Optimistic Locking: @Version 필드로 동시 수정 감지
- 충돌 감지 시: 409 Conflict 반환 (사용자에게 재시도 안내)
- Lost Update 방지: version 필드 자동 증가
**보안 처리** **보안 처리**
- 비밀번호 변경: 현재 비밀번호 확인 필수 - 비밀번호 변경: 현재 비밀번호 확인 필수
- JWT 인증: Controller에서 @AuthenticationPrincipal로 userId 추출 - JWT 인증: Controller에서 @AuthenticationPrincipal로 userId 추출
- 권한 검증: 본인만 수정 가능 - 권한 검증: 본인만 수정 가능
**성능 목표**
- 평균 응답 시간: 0.3초 이내
- P95 응답 시간: 0.5초 이내
- 트랜잭션 격리 수준: READ_COMMITTED
**향후 개선사항** **향후 개선사항**
- 전화번호 변경: SMS/이메일 재인증 구현 - 전화번호 변경: SMS/이메일 재인증 구현
- 이메일 변경: 이메일 인증 구현 - 이메일 변경: 이메일 인증 구현
- 변경 이력 추적: Audit Log 기록
**에러 코드**
- USER_003: 사용자 없음
- USER_004: 현재 비밀번호 불일치
- USER_005: 동시성 충돌 (다른 세션에서 수정)
end note end note
@enduml @enduml

View File

@ -45,7 +45,7 @@ deactivate UserRepo
alt 중복 사용자 존재 alt 중복 사용자 존재
Service --> Controller: throw DuplicateUserException\n("이미 가입된 전화번호입니다") Service --> Controller: throw DuplicateUserException\n("이미 가입된 전화번호입니다")
Controller --> Client: 400 Bad Request\n{"error": "이미 가입된 전화번호입니다"} Controller --> Client: 400 Bad Request\n{"code": "USER_001",\n"error": "이미 가입된 전화번호입니다"}
deactivate Service deactivate Service
deactivate Controller deactivate Controller
else 신규 사용자 else 신규 사용자
@ -104,7 +104,7 @@ else 신규 사용자
deactivate Validator deactivate Validator
Service --> Controller: BusinessNumberInvalidException Service --> Controller: BusinessNumberInvalidException
Controller --> Client: 400 Bad Request\n{"error": "유효하지 않은 사업자번호입니다.\n휴폐업 여부를 확인해주세요."} Controller --> Client: 400 Bad Request\n{"code": "USER_002",\n"error": "유효하지 않은 사업자번호입니다.\n휴폐업 여부를 확인해주세요."}
deactivate Service deactivate Service
deactivate Controller deactivate Controller
@ -122,7 +122,13 @@ else 신규 사용자
== 4단계: 사업자번호 암호화 == == 4단계: 사업자번호 암호화 ==
Service -> Service: encryptBusinessNumber(businessNumber)\n(AES-256 암호화) Service -> Service: EncryptionUtil 호출 준비
note right of Service
**암호화 처리**
- AES-256-GCM 모드 사용
- 환경변수에서 암호화 키 로드
end note
Service -> Service: encryptedBusinessNumber =\nEncryptionUtil.encrypt(businessNumber)
== 5단계: 데이터베이스 트랜잭션 == == 5단계: 데이터베이스 트랜잭션 ==
@ -187,7 +193,16 @@ note over Controller, NTSApi
**보안 처리** **보안 처리**
- 비밀번호: bcrypt 해싱 (Cost Factor 10) - 비밀번호: bcrypt 해싱 (Cost Factor 10)
- 사업자번호: AES-256 암호화 - 사업자번호: AES-256-GCM 암호화 (EncryptionUtil)
**성능 목표**
- 평균 응답 시간: 2.0초 이내 (국세청 API 포함)
- P95 응답 시간: 3.0초 이내
- 캐시 HIT 시: 0.8초 이내
**에러 코드**
- USER_001: 중복 사용자
- USER_002: 사업자번호 검증 실패
end note end note
@enduml @enduml