[트러블슈팅] JWT + Spring Security 기반 인증/인가 시스템 구현 후기 (프로젝트 정리본)
프로젝트 개요
TaskFlow 백엔드 개발 프로젝트에서 인증/인가 + 유저 도메인을 담당하여 총 5개의 API를 설계하고 구현했습니다. 단순한 로그인 기능을 넘어서 실무에서 요구되는 보안성, 확장성, 유지보수성을 모두 고려한 시스템을 구축하는 것이 목표였습니다.
구현 완료 API 목록
- 회원가입 API
- 로그인 API
- 로그아웃 API
- 토큰 갱신 API
- 내 정보 조회 API
핵심 설계 원칙
1. JWT + Spring Security 통합 인증/인가 시스템
Stateless 아키텍처 선택 이유
기존 세션 기반 인증의 한계를 극복하고자 JWT 기반 Stateless 아키텍처를 채택했습니다. 서버가 사용자 상태를 저장하지 않아 수평 확장이 용이하고, 서버 재시작 시에도 사용자 세션이 유지되는 장점을 얻을 수 있었습니다.
이중 토큰 전략으로 보안과 편의성 균형
JWT의 근본적인 딜레마인 "보안 vs 사용자 편의성"을 해결하기 위해 이중 토큰 전략을 도입했습니다.
- Access Token: 15분 짧은 수명으로 보안성 확보
- Refresh Token: 14일 긴 수명으로 사용자 편의성 확보
이를 통해 사용자는 14일간 재로그인 없이 서비스를 이용할 수 있으면서도, 토큰 탈취 시 피해를 최소화할 수 있습니다.
JWT 즉시 무효화 문제 해결
JWT의 Stateless 특성상 발급된 토큰을 즉시 무효화하기 어려운 문제가 있었습니다. 이를 해결하기 위해 JTI(JWT ID) 기반 블랙리스트를 구현했습니다.
- 로그아웃 시 토큰의 JTI를 블랙리스트에 등록
- 스케줄러를 통한 만료된 블랙리스트 토큰 자동 정리
- 토큰 검증 시 블랙리스트 확인 과정 추가
2. Event-Driven 도메인 아키텍처
도메인 분리를 통한 단일 책임 원칙 적용
인증 시스템을 설계하면서 Auth와 User 도메인을 명확히 분리했습니다.
- Auth 도메인: 로그인, 비밀번호 등 인증 정보 관리
- User 도메인: 이름, 이메일 등 프로필 정보 관리
이러한 분리를 통해 각 도메인의 책임을 명확히 하고, 향후 마이크로서비스 전환 시에도 대응할 수 있는 구조를 만들었습니다.
비동기 이벤트 처리로 데이터 일관성 보장
도메인 분리 시 발생하는 데이터 동기화 문제를 이벤트 기반으로 해결했습니다.
// 회원가입 시 이벤트 발행
eventPublisher.publishEvent(new UserRegisteredEvent(userId, name, email));
// 독립적인 트랜잭션으로 User 프로필 생성
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleUserRegistered(UserRegisteredEvent event) {
User user = new User(event.getUserId(), event.getName(), event.getEmail());
userRepository.save(user);
}
REQUIRES_NEW 전파 옵션을 사용하여 Auth 저장 실패 시에도 User 도메인에 영향을 주지 않도록 설계했습니다. 핵심 인증 정보는 반드시 보존하면서, 부가 정보는 별도로 처리할 수 있는 구조입니다.
3. 다층 보안 시스템 구현
Rate Limiting을 통한 무차별 공격 방어
브루트포스 공격을 방어하기 위해 ConcurrentHashMap 기반의 Rate Limiting을 구현했습니다.
- 5회 로그인 실패 시 15분간 계정 차단
- 메모리 기반 처리로 빠른 응답 속도 확보
- 성공 로그인 시 자동 초기화
강력한 비밀번호 정책
보안성을 높이기 위해 다단계 비밀번호 검증을 적용했습니다.
- 정규식 기반 복잡도 검증: 대소문자, 숫자, 특수문자 조합 강제
- BCrypt 암호화: 단방향 해시로 안전한 저장
Spring Security 커스터마이징
기본 설정에 의존하지 않고 프로젝트 요구사항에 맞는 구조를 직접 설계했습니다.
- JwtAuthenticationFilter: JWT 토큰 추출 및 검증
- JwtAuthenticationProvider: Authentication 객체 생성
- UserPrincipal: 인증 주체 정보 캡슐화
4. 감사 추적 시스템
Spring Data JPA Auditing 활용
데이터 변경 이력을 자동으로 추적하기 위해 JPA Auditing을 설정했습니다.
- SpringSecurityAuditorAware: 현재 인증된 사용자 정보 자동 주입
- 생성자, 수정자, 생성일, 수정일 자동 관리
- Soft Delete 패턴으로 데이터 보존
핵심 구현 클래스 설계
인증/인가 핵심 클래스
JwtTokenProvider JWT 생성, 파싱, 검증의 모든 로직을 담당하는 핵심 클래스입니다. 토큰 생성 시 JTI를 포함하여 블랙리스트 관리가 가능하도록 설계했습니다.
JwtAuthenticationFilter Spring Security Filter Chain에 등록되어 모든 요청에서 JWT 토큰을 검증합니다. 토큰이 유효하지 않을 때는 SecurityContext를 초기화하여 인증되지 않은 상태로 처리합니다.
UserPrincipal Spring Security의 UserDetails를 구현하여 인증된 사용자 정보를 캡슐화합니다. 컨트롤러에서 @AuthenticationPrincipal 어노테이션으로 간편하게 사용자 정보에 접근할 수 있습니다.
보안 강화 클래스
LoginAttemptService 메모리 기반으로 로그인 시도를 추적하고 차단하는 서비스입니다. ConcurrentHashMap을 사용하여 멀티스레드 환경에서도 안전하게 동작합니다.
TokenBlacklistService JTI 기반 토큰 블랙리스트를 관리합니다. 스케줄러와 연동하여 만료된 토큰을 주기적으로 정리하여 메모리 효율성을 확보했습니다.
RefreshTokenService Refresh Token의 전체 생명주기를 관리합니다. 생성, 검증, 갱신, 정리의 모든 과정을 담당하며, 사용자별로 하나의 Refresh Token만 유지하도록 설계했습니다.
기술적 도전과 해결 과정
문제 1: JWT 즉시 무효화의 어려움
상황: 로그아웃 시 발급된 JWT 토큰이 만료 시간까지 계속 유효한 문제
해결: JTI 기반 블랙리스트 도입
- 토큰 생성 시 고유 ID(JTI) 부여
- 로그아웃 시 해당 JTI를 블랙리스트에 등록
- 토큰 검증 시 블랙리스트 확인 과정 추가
문제 2: 도메인 분리 시 데이터 일관성
상황: Auth와 User 도메인 분리 시 한쪽 실패가 전체에 영향을 주는 문제
해결: 이벤트 기반 비동기 처리 + 트랜잭션 전파
- 핵심 Auth 정보는 반드시 보존
- User 프로필은 별도 트랜잭션으로 처리
- 실패 시 재시도 가능한 구조 확보
문제 3: 보안과 사용성의 균형
상황: 강한 보안 정책이 사용자 경험을 해치는 문제
해결: 이중 토큰 전략 + 적응형 Rate Limiting
- 짧은 Access Token + 긴 Refresh Token
- 성공 로그인 시 제한 해제
- 명확한 에러 메시지로 사용자 가이드
결과 및 성과
기술적 성과
- 100% API 구현 완료: 설계한 5개 API 모두 완성
- 무중단 토큰 갱신: 사용자 재로그인 없이 연속 사용 가능
- 실시간 보안 대응: 즉시 로그아웃, 계정 차단 기능
아키텍처적 성과
- 확장 가능한 구조: 마이크로서비스 전환 준비 완료
- 도메인 독립성: 각 도메인의 명확한 책임 분리
- 느슨한 결합: 이벤트 기반 도메인 간 통신
보안적 성과
- 다층 방어 체계: 암호화, Rate Limiting, 토큰 관리
- 완전한 감사 추적: 모든 사용자 활동 기록
회고 및 개선점
이번 프로젝트를 통해 단순한 기능 구현을 넘어서 아키텍처 설계와 문제 해결 능력을 크게 향상시킬 수 있었습니다. 특히 도메인 분리와 이벤트 기반 처리를 통해 확장 가능한 시스템을 구축하는 경험을 쌓았습니다.
향후 개선하고 싶은 부분은 Redis를 활용한 분산 환경에서의 Rate Limiting과 토큰 블랙리스트 관리입니다. 현재는 메모리 기반으로 구현했지만, 실제 운영 환경에서는 여러 서버 인스턴스 간 상태 공유가 필요할 것입니다.
이러한 경험을 바탕으로 앞으로도 사용자와 비즈니스 가치를 모두 고려한 기술적 의사결정을 내릴 수 있는 개발자로 성장하고 싶습니다.