Skip to content

[ASSIGNMENT] 7차 과제 구현 - #18

Open
Kimgyuilli wants to merge 9 commits into
mainfrom
feat/#16/assignment7
Open

[ASSIGNMENT] 7차 과제 구현#18
Kimgyuilli wants to merge 9 commits into
mainfrom
feat/#16/assignment7

Conversation

@Kimgyuilli

Copy link
Copy Markdown
Collaborator

🔥Pull requests

close #16

👷 과제 구현

아티클 기반 개선

  • DelegatingPasswordEncoder 전환 (알고리즘 회전 지원)
  • Access Token 블랙리스트 Caffeine 로컬 캐시 전환
  • Refresh Token Reuse Detection 적용
  • Refresh Token Grace Period 적용
  • 사용자 역할 조회 Caffeine 캐싱 (매 요청 DB 조회 절감)
  • 사용자 역할 캐시 원자적 로딩 개선
  • AuthService 책임 분리 (Application Service / Domain Service)
  • 회원 탈퇴 시 인증 세션 회수 포트 분리
  • 도메인 계층 패키지 분리 (domain/service, domain/port)

구현한 내용에 대해서 설명해주세요

DelegatingPasswordEncoder 전환

  • BCryptPasswordEncoderPasswordEncoderFactories.createDelegatingPasswordEncoder()로 교체했습니다.
  • 새로 저장되는 비밀번호에 {bcrypt} 프리픽스가 붙어 알고리즘을 식별하고, 향후 Argon2id 같은 알고리즘으로 점진적 마이그레이션이 가능해집니다.
  • 아티클에서 다룬 "알고리즘 회전" 패턴을 Spring Security의 DelegatingPasswordEncoder로 적용한 형태입니다.

Access Token 블랙리스트 Caffeine 전환

  • 기존에는 블랙리스트를 RDB에 저장했기 때문에 매 요청마다 DB 조회가 필요했고 만료된 레코드가 쌓이는 문제도 있었습니다.
  • Caffeine의 per-entry TTL(Expiry)을 토큰의 잔여 유효 시간에 정렬하여, 토큰이 자연 만료되는 시점에 블랙리스트 항목도 함께 제거되도록 했습니다.
  • 트레이드오프로 서버 재시작 시 캐시가 날아가지만, Access Token 만료 시간(30분)이 짧아 실질적 위험은 낮다고 판단했습니다. Redis를 띄우는 인프라 비용 대비 현재 규모에서는 Caffeine이 합리적이라고 생각했습니다.

Refresh Token Reuse Detection + Grace Period

  • Reuse Detection: 이미 회전된 구 토큰이 Grace Period 이후에 재사용되면 해당 사용자의 토큰을 전체 무효화하고 보안 경고 로그를 남깁니다. 아티클에서 다룬 "Rotation + Reuse Detection"(OAuth2 BCP 권고) 패턴입니다.
  • Grace Period: 회전 직후 10초간 구 토큰 해시를 Caffeine에 보관하여 동시 요청(탭 여러 개, 네트워크 재시도)에 의한 정상 사용자 세션 끊김을 방지합니다. ConcurrentHashMap.remove()의 원자성으로 이중 발급을 막습니다.
  • 재발급 흐름: ①DB 해시 일치 → 정상 회전 + Grace Period 등록 → ②Grace Period에 존재 → 동시 요청 허용 → ③양쪽 실패 → Reuse 감지, 전체 무효화

사용자 역할 Caffeine 캐싱

  • JwtAuthenticationFilter에서 매 요청마다 수행하던 userRepository.findById() DB 조회를 Caffeine 캐시(5분 TTL, max 10K)로 절감했습니다.
  • evictUserRole() 메서드로 사용자 삭제나 역할 변경 시 캐시를 즉시 무효화할 수 있습니다.
  • cache.get(userId, this::loadUserRole)을 사용해 같은 사용자 ID에 대한 동시 캐시 미스가 발생해도 로딩 함수가 중복 실행되지 않도록 했습니다.
  • 존재하지 않거나 삭제된 사용자도 짧은 TTL 동안 같은 방식으로 캐시할 수 있도록 Optional<UserRole>을 캐시 값으로 사용했습니다.

AuthService 책임 분리

  • 기존 AuthService는 9개 의존성을 가지고 자격증명 검증, 소셜 로그인, 토큰 생명주기를 모두 담당하고 있었습니다.
  • 토큰 생명주기를 AuthTokenService(Domain Service)로, 소셜 로그인을 SocialLoginService(Domain Service)로 추출했습니다.
  • AuthService는 자격증명 검증과 흐름 조율만 담당하는 Application Service(Use Case)로 남겼습니다.
  • Service→Service 의존이 같은 계층 간(peer-to-peer)이 아닌 Application→Domain 정방향 의존임을 패키지 구조로 명시했습니다.

회원 탈퇴 인증 세션 회수 포트 분리

  • UserCommandServiceAuthTokenServiceUserRoleCache를 직접 의존하지 않도록 AuthSessionPort를 추가했습니다.
  • 회원 탈퇴 유스케이스는 사용자 삭제 이후 authSessionPort.revoke(authenticatedUser)만 호출하고, 실제 refresh token 삭제, access token 블랙리스트 등록, 역할 캐시 무효화는 AuthSessionPortAdapter가 처리합니다.
  • 이를 통해 user application 계층은 인증 세션 회수라는 의도만 알고, auth domain service와 security cache 같은 구현 세부사항은 infrastructure adapter 뒤로 숨겼습니다.

고민했던 포인트

Redis vs Caffeine

  • 블랙리스트와 Grace Period 저장소로 Redis 대신 Caffeine을 선택했습니다.
  • Redis는 다중 인스턴스 환경에서 공유하는 저장소일 때 강력한 No-SQL 도구입니다. 현재 단일 인스턴스 환경에서 Redis 인프라를 별도로 띄우는 것은 오버헤드가 크고 Caffeine만으로도 아티클의 핵심 패턴(TTL 정렬, Grace Period, Reuse Detection)을 모두 구현할 수 있다고 생각했습니다.
  • MSA나 다중 인스턴스로 확장할 때는 Caffeine → Redis로 구현체만 교체하면 되도록 추상화(인터페이스)를 유지해 전환도 고려했습니다.

Service 계층 분리

  • 처음에는 AuthService에서 책임만 분리했는데 Service→Service 의존이 같은 패키지 안에서 peer-to-peer처럼 보이는 문제가 있었습니다.
  • Application Service(유스케이스 조율)와 Domain Service(도메인 로직)를 패키지로 분리하여 의존 방향이 presentation → application → domain ← infrastructure로 명확하게 배치했습니다.
  • OAuth 포트 인터페이스(OAuthProviderClient)도 application.client에서 domain.port로 옮겨 domain service가 application을 역참조하지 않도록 정리했습니다.
  • 회원 탈퇴처럼 user 유스케이스에서 인증 세션 회수가 필요한 경우에는 application port를 통해 의존성을 분리해, user application service가 auth 구현체와 security cache를 직접 참조하지 않도록 정리했습니다.

프리픽스 기반 알고리즘 식별을 도입하여 향후 Argon2id 등으로
점진적 마이그레이션할 수 있는 경로를 확보한다.
RDB 블랙리스트 엔티티와 JPA 레포지토리를 제거하고,
per-entry TTL을 토큰 잔여 유효 시간에 정렬하는 Caffeine 캐시로 대체한다.
매 요청 DB 조회를 인메모리 조회로 절감하고, 만료된 항목은 자동 제거된다.
사용자 역할 조회도 Caffeine 캐시(5분 TTL)를 적용하여 DB 부하를 줄인다.
회전된 구 토큰이 재사용되면 사용자의 토큰을 전체 무효화하여
탈취를 감지·차단한다. 회전 직후 10초간 구 토큰을 Grace Period로
허용하여 동시 요청에 의한 정상 사용자 세션 끊김을 방지한다.
9개 의존성을 가진 AuthService를 3개 서비스로 분리한다.
- AuthTokenService: 토큰 발급, 회전(Rotation + Grace Period), 회수
- SocialLoginService: OAuth 프로필 검증과 자동 회원가입
- AuthService: 자격증명 검증과 흐름 조율
UserCommandService도 AuthTokenService.revoke()를 사용하도록 전환한다.
Application Service(AuthService)와 Domain Service(AuthTokenService,
SocialLoginService)의 계층을 패키지로 명시한다.
- domain/service: 토큰 생명주기, 소셜 로그인 도메인 로직
- domain/port: OAuth 클라이언트 포트 인터페이스
- domain/model: AuthTokenResult 값 객체
- application/service: 유스케이스 조율 (AuthService)
Grace Period 경로에서 issueAndSaveTokens를 재호출하면 정상 회전 시
발급된 refresh token이 덮어써져 첫 번째 응답의 토큰이 무효화되는
문제를 수정한다. Grace Period 캐시에 발급 결과를 함께 저장하여
동시 요청 시 동일한 토큰을 반환하도록 변경한다.

사용자 삭제 시 역할 캐시를 즉시 무효화하여, 삭제된 사용자가
캐시 TTL 동안 인증되는 문제를 수정한다. 캐시를 UserRoleCache
컴포넌트로 추출하여 필터와 서비스 양쪽에서 접근할 수 있도록 한다.
- UserRoleCache에서 Caffeine cache.get()이 null 반환 시 NPE를
  던지는 버그를 수정한다. getIfPresent + 수동 put으로 전환.
- AuthTokenService(domain)이 infrastructure를 직접 참조하던 역방향
  의존을 RefreshTokenHashPort, RefreshTokenGracePeriodPort
  인터페이스로 추상화하여 제거한다.
- AuthErrorCode 선언 순서를 코드 번호순으로 정렬한다.
- reissue()의 동시성 제약을 Javadoc에 명시한다.
8개 파일이 혼재된 infrastructure 패키지를 정리한다.
- persistence: JPA 레포지토리와 어댑터
- cache: Caffeine 기반 블랙리스트·Grace Period 구현체
- oauth: Google OAuth 클라이언트
- security: Refresh Token HMAC 해셔
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

[ASSIGNMENT] 7차 과제 구현

1 participant