GPT 요약
My-Books 프로젝트에서 Spring Security를 검토했지만, MSA 환경에서의 인증정보 불일치와 자원 소모 문제로 사용하지 않았습니다.
대신, 간단한 인증/인가 요구사항을 충족하기 위해 Spring AOP를 활용해 효율적인 인가 처리를 설계했습니다.
이를 통해 사용자 경험을 개선하고 시스템 자원 소비를 최소화했습니다.
My-Books 프로젝트에서의 경험을 다룬 글입니다.
My-Books 프로젝트에서 저의 주된 역할은 인증/인가 프로세스를 설계하고 구현하는 것이었습니다.
처음에는 당연히 Spring Security를 기반으로 구현하려 했지만 결과적으로는 선택하지 않았는데요 그 과정을 얘기하려 합니다.
Spring Security란?
Spring Security는 Spring Framework 기반의 애플리케이션에서 인증(Authentication)과 인가(Authorization)를 손쉽게 구현할 수 있도록 제공되는 강력한 보안 프레임워크입니다.
사용자가 애플리케이션에 로그인하면, Authentication 객체가 생성되며 Authentication 객체는 Spring Security의 SecurityContext에 저장되어 애플리케이션 전반에서 사용자의 인증 상태를 관리합니다.
또한 Filter Chain을 통해 모든 HTTP 요청을 가로채고, 보안 검사를 수행합니다.

사용하지 않은 이유
Spring Framework에 완벽하게 호환되면서 쉽게 구현이 가능하다니.. 사용하지 않을 이유가 없어 보입니다.
하지만 다음과 같은 이유로 사용하지 않았습니다.
기존의 Spring Security를 활용한 접근 방식에서는 다음과 같은 문제가 있었습니다.
Security Context의 오버헤드

Security Context는 기본적으로 세션에 저장하여 상태를 유지하기 때문에 MSA로 구성된 My-Books 프로젝트에서는 인증정보 불일치 문제가 발생 가능합니다.
이 문제는 SessionCreationPolicy를 Stateless하게 설정하면 해결이 가능하지만
세션을 사용하지 않을 뿐 매 요청마다 SecurityContext를 생성하기 때문에 추가적인 자원 소비가 이뤄지게 됩니다.
이는 서버에 불필요한 오버헤드를 발생시켰고, MSA 환경에서 효율적이지 않다고 판단했습니다.
과정의 복잡성
Spring Security는 내부적으로 DelegatingFilterProxy가 요청을 먼저 받고, 이후 여러 Security 필터를 거치는 구조로 동작합니다. 이 과정이 간단한 인가 로직을 구현하는 데 불필요하게 복잡하다고 생각했습니다.
테스트 코드의 생산성 저하
Security가 활성화된 상태에서는 테스트 코드 작성 시 Security 옵션을 비활성화하거나, Security가 제공하는 User 객체를 주입해야 했습니다. 이는 유닛 테스트 시 불필요한 설정을 요구해 생산성을 떨어뜨렸습니다.
해결 방법
위 문제를 해결하기 위해 다음과 같이 인증/인가 프로세스를 다음과 같이 설계했습니다.
1. JWT 기반 인증:
• 로그인 시, 액세스 토큰을 쿠키에 저장하고 리프레시 토큰을 Redis에 저장.
• 모든 요청 시 클라이언트는 액세스 토큰을 헤더에 담아 전송.
2. Spring Gateway에서 인가 처리
• Gateway의 AbstractGatewayFilterFactory를 상속받아 커스텀 필터를 구현
• 이 필터는 헤더에서 JWT를 추출하고, 유효성을 검증한 후 권한을 확인
하지만 이 과정에서 예외사항에 대한 처리의 필요성이 생겼고(Invalid 토큰,유효기간 만료,권한 불일치..) 이를 해결하기 위해 횡단 관심사를 처리할 수단을 찾게됐습니다.
Filter vs Interceptor vs AOP
Spring Security의 대안으로 생각한 것은 Filter, Interceptor, AOP였습니다.
위에서 언급한 것들은 모두 횡단 관심사를 처리할 수 있는 수단으로, 인가 처리에 사용 가능합니다.
이들은 비슷해 보이지만 차이점이 존재했는데요. Filter는 Spring 스펙이 아닌 Servlet의 스펙으로, DispatcherServlet 이전에 실행되어 모든 HTTP 요청을 대상으로 작동합니다. 그렇기 때문에 Filter는 Spring 컨테이너와 무관하게 작동하므로, Spring Bean이나 컨트롤러 또는 서비스 계층의 로직과 연동된 처리는 어렵습니다.
Interceptor는 Spring MVC에서 제공하는 기능으로, 컨트롤러 실행 전후에 추가 로직을 삽입하는 데 사용됩니다.
Spring MVC 컨트롤러 수준에서 동작하기 때문에 Filter보다 Spring Application과 밀접하게 협력한다는 장점은 있지만, 컨트롤러보다 추상화 정도가 낮은(실제 비즈니스 로직이 존재하는) 서비스 계층에서의 상세한 처리는 불가능합니다.
또한, Filter와 Interceptor 모두 요청 전과 후에 대한 처리를 지원하지만, 이를 하나의 메서드에서 전후를 동시에 처리하는 방법은 없기 때문에 AOP를 선택하게 됐습니다.
내가 사용한 Spring AOP
현재 My-Books는 Front Server에서 사용자 요청 시 Gateway Server로 전달되어 인가 처리를 하는 구조를 가집니다.

또한 인가 처리 실패 시 에러가 발생하고 Front Server는 그에 따른 알맞은 응답을 반환해 사용자의 사용성을 높이는 목표가 존재했습니다.따라서 호출 후 에러발생에 따른 추가적인 로직 실행이 가능해야 됐으므로 Spring AOP의 @Around를 사용하기로 결정했습니다.

인가 처리가 실패하는 경우에 따라서 액세스 토큰을 재발급하거나 특정 페이지로 보내는 등의 작업을 상세 설계하여
사용자의 사용성을 높일 수 있었습니다.
이와 관련해 추가적인 링크 남기면서 마무리 하겠습니다.
------------------------------------
2025/09/02 추가
이 글이 조회수가 많이 나오는 글이라, 추가적으로 작성합니다.
위처럼 코드짜면 큰일납니다.
문자열 매칭 기반 예외 처리, ContextHolder 사용, 난잡한 책임 등의 문제가 있습니다.
문자열 매칭 보다는 에러코드등으로 대신할 수 있으며, 에러또한 ResponseErrorHandler 를 통해 커스텀 예외로 바로 잡을 수 있습니다.
더불어 ContextHolder은 ThreadLocal 기반으로, 비동기처리나 Vitrtual Thread 사용시 문제가 발생할 수 있습니다.
만약 다시 구현한다면
이미 중간에 Gateway를 두고있고 거기서 인가처리를 하고있기 때문에 이런 방법을 생각할 것 같습니다.
1. Gateway에서 인가처리 -> 실패 -> Gateway 에서 리프래시 확인 -> 리프래시 없으면(실패)/있으면 재발급 후 요청 마저 처리
2. 지금처럼 Gateway에서 에러 던짐 -> ResponseErrorHandler 받음 -> JWT 관련 처리하고 재시도
다른 방법도 여러가지 많겠지만 일단, AOP의 책임을 줄이는 방식으로 구현하는것은 확실하다 생각합니다.
지금은 너무 책임이 장황하고 많이 가지고 있는 것 같아요
또한 JWT를 사용하면서 Redis에 저장해 사용하고있는데, 이는 보안성을 높이기위한 방법입니다.
이러면 세션과 똑같이 구현되는거 아니야? 왜 JWT써? 라고 한다면 Qeury Ratio가 달라집니다.
세션은 항상 Session DB (지금 경우에는 Redis)를 찔러야하지만 JWT의 경우 클라이언트에게 토큰정보가 존재하기 때문에
AccessToken이 만료된 경우에만 DB를 찌르게 됩니다.
DB에 쿼리날리는 빈도 자체가 달라지기 때문에 충분히 의미가 있다고 생각하며
RefreshToken(RT) 자체도 JWT 토큰의 형식이 아닌 커스텀해서 가져갈 수 있습니다.
RT의 JWT형식을 유지하는 이유는 보편적으로는 클라이언트에게 RT가 있기 때문에 이를 검증하기 위해 쓰는 것 인데
지금같은 상황에서는 RT를 서버에서 관리하고 있어, Redis TTL 로 만료 처리를 하고 RT는 간단한 문자열 혹은 정수값이어도 상관 없습니다.
Redis TTL의 경우 Policy가 Active/Lazy 하게 만료되는 정책이 있으며
기본적으로 두 정책을 섞어 사용하는 것으로 압니다.
Active = 조회시 TTL 체크 넘었으면 삭제 (조회안된 것들은 계속 남아있어 메모리 먹는 문제 발생)
Lazy = 조회안된 것들을 백그라운드에서 스케줄러 돌려 삭제처리
그 외에 메모리가 꽉 찼을때 어떤 데이터를 삭제할것이냐 라는 삭제 정책도 있지만, 이는 TTL과는 관련없기 때문에
필요하다면 찾아보시길 바랍니다.
또한 여전히 Spring Security를 사용해 강결합되는 것은 고민이 필요한 문제입니다.
몇가지 Interface나, 유용한 어노테이션을 제공해주긴 하지만.. 대부분 커스텀해서 == 구현해서 사용해야하고
동작하는 방식도 복잡하며 무거운 프레임워크인 만큼 AutoConfig 되는 빈들이 많은데 충분한 이해가 없는 상태에서 사용하기에는
이점이 없다 생각합니다.
추가적으로 데스트코드 작성할때도 불편해져요
동작 방식 복잡함 (Filter는 원래 Servlet Spec이라 Container를 왕복해줘야 합니다.)
(ServletContainer)FilterChain -> DelegateFilterProxy -> (여기서부터 SpringContainer) Security Filter Chain
-> (Filter 순회 다 끝남 이제 SevletContainer 로 돌아가서 요청 진행) DispathcerServlet -> Handler Mapping -> AdpatorMapping -> (SpringContainer)Controller 호출 및 로직처리
따라서.. 간단하게 RBAC 정도만 하면 된다.
관리자/유저 정도고 디테일한 인증/인가 처리가 필요하진 않다.
나는 Spring Security에 대한 완벽한 이해가 없다
그렇다면 커스텀하게 인증/인가 처리하는게 더 좋다고 생각합니다.
오히려 현업에서는 Security와 강결합된 코드를 레거시로 보는 경우도 많은 것 같아요
지금 다시 보니 너무나 부족한 글인데.. 읽어주셔서 감사합니다.
'프로젝트 > My-Books' 카테고리의 다른 글
| BCrypt로 완성한 안전한 로그인 설계 (3) | 2025.01.05 |
|---|---|
| Gateway로 인가 로직, 어떻게 최적화했을까? (1) | 2024.12.22 |
| JWT 만료기간, 보안과 사용자 경험의 Trade-Off (4) | 2024.12.21 |
| JWT 안전성 개선기 (0) | 2024.12.20 |
| JWT는 왜 사용할까? (1) | 2024.12.20 |