BindProject

[Backend]Auth모듈: 인증 시스템 설계와 구현 -FIN-!!

dding-shark 2025. 6. 19. 20:12
728x90

1. 도입

사용자 인증은 대부분의 서비스에서 핵심이자 기본 기능이다.
이 글에서는 JWT 기반 인증 시스템을 설계하면서, 다음과 같은 질문을 해결하고자 했다:

  • AccessToken과 RefreshToken은 어떻게 분리 설계해야 할까?
  • 멀티 디바이스 환경에서 리프레시 토큰을 어떻게 안전하게 관리할 수 있을까?
  • 발급 및 검증 실패 상황은 어떻게 핸들링해야 할까?

2. 설계 목표

  1. Stateless AccessToken, Stateful RefreshToken
  2. 멀티 디바이스 지원 (웹, 앱 등 기기별 로그인 상태 분리)
  3. 보안/예외 케이스에 강한 설계
  4. 단순한 테스트와 확장 가능성을 고려한 코드 구조

3. 설계 고려사항

항목 선택 이유

AccessToken JWT + Stateless 빠른 인증, 서버 무부하
RefreshToken 저장소 Redis TTL 관리 용이, 속도 빠름
기기 구분 userId + deviceId 복합키 멀티 디바이스 지원
토큰 검증 방식 예외 기반 (JwtException → TokenException) 디버깅 용이, 확장성 확보
예외 코드 구조 enum TokenErrorCode implements CustomErrorCode HTTP 상태 코드 매핑 가능

4. 핵심 구현 설명

4.1 JwtTokenProvider

  • Access / Refresh 발급 메서드 분리
  • addClaims() 사용으로 subject 유실 방지
  • TTL 외부 노출로 Redis 연동 시 TTL 동기화 가능
public String issueAccessToken(String subject, Map<String, Object> claims) {
    try {
        return buildToken(subject, claims, accessTokenExpirationSeconds);
    } catch (JwtException e) {
        throw new TokenException(TokenErrorCode.TOKEN_CREATION_FAILED, e);
    }
}

4.2 RefreshTokenService (Redis 기반)

  • "refresh:{userId}:{deviceId}" 형태로 저장
  • TTL 자동 관리
  • Rotation 검증도 가능하도록 확장 설계
public void save(String userId, String deviceId, String refreshToken, Duration ttl) {
    redisTemplate.opsForValue().set(getKey(userId, deviceId), refreshToken, ttl);
}

4.3 AuthService.login()

  • 비밀번호 검증은 PasswordEncoder.matches() 사용
  • 로그인 성공 시 AccessToken + RefreshToken 발급 및 저장
  • 실패 케이스에 대해 적절한 AuthException, TokenException 던짐
if (!passwordEncoder.matches(req.getPassword(), user.getPassword())) {
    throw new AuthException(AuthErrorCode.INVALID_PASSWORD);
}

4.4 TokenErrorCode

TOKEN_CREATION_FAILED("TOKEN-CRE-001", "토큰 생성 실패", HttpStatus.INTERNAL_SERVER_ERROR)

→ 컨트롤러 예외 처리에서 바로 HTTP 응답으로 매핑 가능.


5. 테스트 전략

  • JwtTokenProviderTest로 발급/파싱/예외 케이스 검증
  • 만료된 토큰, 위조된 토큰, 클레임 유효성 등 테스트 커버리지 확대
  • TokenException과 AuthException 각각 분리 검증
assertThatThrownBy(() -> tokenProvider.issueAccessToken(null, Map.of()))
    .isInstanceOf(TokenException.class);

6. 회고 및 확장 가능성

  • 초기에 setClaims()로 인한 subject == null 버그가 발생했지만, addClaims()로 구조적으로 해결함
  • RefreshToken을 Redis에 저장하는 구조는 Rotation 구현까지 대비되어 있어 이후 토큰 재사용 방지도 쉽게 가능
  • 예외 기반 구조로 바꾸면서 디버깅이 쉬워졌고, CustomErrorCode 구조는 HTTP 계층과 분리된 설계가 가능해졌다

확장 아이디어

아이디어 설명

RefreshToken Rotation 재발급 시 이전 토큰 폐기
Key Rolling (kid) 키 교체 대비 JWT 헤더에 kid 필드 추가
토큰 블랙리스트 로그아웃 시 AccessToken도 차단하는 전략
디바이스 로그아웃 API 특정 기기만 RefreshToken 삭제

마치며

토큰 기반 인증 시스템은 단순히 JWT 발급만으로 끝나지 않는다.
토큰 저장 전략, 예외 처리, 멀티 디바이스 구조까지 설계에 포함되어야 확장성과 보안성을 동시에 잡을 수 있다.
이번 설계를 통해 기초 구조를 다졌고, 이후 운영 환경에서 확장 가능한 기반을 마련했다.


728x90