728x90
1. 도입
사용자 인증은 대부분의 서비스에서 핵심이자 기본 기능이다.
이 글에서는 JWT 기반 인증 시스템을 설계하면서, 다음과 같은 질문을 해결하고자 했다:
- AccessToken과 RefreshToken은 어떻게 분리 설계해야 할까?
- 멀티 디바이스 환경에서 리프레시 토큰을 어떻게 안전하게 관리할 수 있을까?
- 발급 및 검증 실패 상황은 어떻게 핸들링해야 할까?
2. 설계 목표
- Stateless AccessToken, Stateful RefreshToken
- 멀티 디바이스 지원 (웹, 앱 등 기기별 로그인 상태 분리)
- 보안/예외 케이스에 강한 설계
- 단순한 테스트와 확장 가능성을 고려한 코드 구조
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
'BindProject' 카테고리의 다른 글
| [Backend] Auth모듈: JWT 로그아웃 설계와 Redis 기반 블랙리스트 구현 (1) | 2025.06.20 |
|---|---|
| [기획] 추후 목표 :Kafka 기반 이벤트 아키텍처 이식 설계 (0) | 2025.06.19 |
| [Backend] Auth모듈 : 유저 탈퇴 처리 (1) | 2025.06.19 |
| [Backend] Auth모듈 : 역할(Role) 부여 기능 구현기 (0) | 2025.06.19 |
| [Backend] Auth모듈 :사용자 등록 로직의 책임 분리와 유효성 검사 개선 (2) | 2025.06.18 |