Spring Boot 프로젝트에서 AOP·인증/인가·Redis 분산락까지 — 트러블슈팅과 설계 기록
목표
단순 기능 구현을 넘어서, 왜 이런 구조를 선택했는지, 어떤 문제를 겪었고 어떻게 해결했는지를 기록하기 위한 글이다.
1. 프로젝트 배경
이 프로젝트는 공연 예매 서비스로,
- 동시성 이슈가 명확한 좌석 선택 도메인
- JWT 기반 인증/인가
- Redis를 활용한 캐싱과 분산 락
을 핵심 기술 포인트로 삼았다.
특히 튜터님·코드리뷰 상황에서
“왜 이렇게 설계했나요?”
라는 질문에 답할 수 있는 구조를 만드는 것을 목표로 했다.
2. 인증 / 인가 설계 (JWT)
2-1. 왜 세션이 아닌 JWT인가?
- 서버 확장에 유리한 Stateless 구조
- 모바일/프론트엔드와의 연동 용이
- 인증 정보를 토큰 자체에 포함 가능
단점으로는 앞으로 구현할 로그아웃과 토큰 무효화가 어렵다는 점이었다.
2-2. AuthenticationManager를 사용한 이유
AuthenticationManager는 인증의 진입점 역할을 한다.
- Controller → Service가 인증을 직접 처리하지 않게 함
- 인증 로직을 Spring Security에게 위임
인증 책임을 프레임워크에 맡기고, 비즈니스 로직과 분리
2-3. JWT 인증 필터를 UsernamePasswordAuthenticationFilter 앞에 둔 이유
- JWT는 로그인 이후 요청을 검증
- UsernamePasswordAuthenticationFilter는 로그인 요청 전용
[JWT 인증 필터] → [UsernamePasswordAuthenticationFilter]
이미 토큰이 있는 요청은 굳이 로그인 필터를 탈 필요가 없다..!
2-4. UserDetails / UserDetailsService를 분리한 이유
| 구성요소 | 역할 |
|---|
| UserDetails | Spring Security가 이해하는 사용자 모델 |
| UserDetailsService | 사용자 조회 책임 |
도메인 User와 보안 User를 분리해 결합도 감소
3. 로그아웃과 Blacklist 설계
3-1. JWT 로그아웃의 근본적인 문제
- JWT는 서버에 저장되지 않음(
Stateless 특성)
- 한 번 발급되면 만료 전까지 유효
단순히 "로그아웃" API 호출로는 토큰을 무효화할 수 없음
3-2. Blacklist를 도입한 이유
이미 발급된 토큰을 서버 기준으로 차단하기 위함
- 로그아웃 시 토큰을 Blacklist에 저장
- 이후 요청마다 Blacklist 확인
- 존재하면 인증 실패
Stateless 구조를 유지하면서도 즉시 무효화 보장
3-3. User 테이블과 분리한 이유
- 인증 상태 ≠ 사용자 정보
- 인증 관련 데이터는 조회 빈도가 높음
- Redis 전환을 고려한 구조
책임 분리 + 확장성 확보
3-4 위 설계로 인한 차후 생각해야될 모델
수많은 유저가 로그아웃을 거치기 때문에 이에따른 병목 현상에 대해 고려
- 일정 데이터가 쌓이면 삭제하도록 설계
- 다른 방도가 별도로 필요할 것으로 보임..(;´д`)ゞ
4. 동시성 문제와 Redis 분산 락
4-1. 좌석 선택에서 발생하는 문제
- 동시에 같은 좌석을 선택하는 요청
- DB 트랜잭션만으로는 한계
4-2. Redis 분산 락을 선택한 이유
- 멀티 인스턴스 환경 대응
- DB 락보다 빠른 처리
- TTL로 데드락 방지 가능
4-3. 락 구현 방식
SETNX + TTL
Lua Script로 안전한 해제
- 락 획득 실패 시 즉시 예외
- 락은 반드시 finally에서 해제
5. AOP로 락 로직 분리
5-1. 왜 AOP인가?
- 락 로직은 횡단 관심사
- 서비스 코드가 지저분해지는 문제
비즈니스 로직에서 락 코드 제거
5-2. @RedisLock 어노테이션 설계
- SpEL을 활용한 동적 key 생성 (SpEL에 대해서는 차후 따로 다뤄볼 계획이다!!(⌐■_■))
- TTL 정책을 어노테이션으로 명시
@RedisLock(key = "'seat:' + #seatId", ttl = 3000)
5-3. RuntimeException 문제와 개선 포인트
초기에는 AOP 내부에서 RuntimeException으로 감싸며 예외를 던졌다.
문제점:
- 서비스 레벨의 CustomException이 그대로 전달되지 않음
개선 방향:
- AOP에서는 예외를 감싸지 않고 그대로 throw
- 또는 공통 인터페이스 기반 예외 처리
6. Scheduler + 분산 락
6-1. 왜 스케줄러에도 락이 필요한가?
- 서버가 여러 대일 경우
- 동일 스케줄이 중복 실행 가능
6-2. RedisSchedulerLockAspect 도입
-
@Scheduled 메서드에 어노테이션 적용
-
이미 실행 중이면 그냥 종료
중복 실행 방지 + 안정성 확보
7. 트러블슈팅 요약
| 이슈 | 해결 |
|---|
| JWT 로그아웃 | Blacklist 도입 |
| 좌석 중복 선택 | Redis 분산 락 |
| 락 코드 중복 | AOP 분리 |
| 스케줄 중복 실행 | Scheduler Lock |
| 예외 전파 문제 | AOP 예외 정책 개선 |
8. 마무리
이번 프로젝트를 통해 느낀 점은
기술 선택의 이유를 설명할 수 있어야 진짜 내 코드가 된다
라는 것이었다.
단순 구현이 아니라,
- 왜 이 구조인지
- 어떤 대안이 있었는지
- 왜 이걸 선택했는지
를 계속 고민한 경험이 큰 자산이 되었다.
다음으로 정리하고 싶은 주제
- Redis vs DB Lock 비교 실험
- JWT + Refresh Token 고도화
- 트래픽 테스트와 병목 분석
꼭 해보고 싶다!!!!!!!~(≧▽≦)/~ 프로젝트 마무리한 모두 고생했고 좋은 결과만 있기를 (❁´◡`❁)
TIL 작성 기원 13일차