세션 기반 인증의 한계
프로젝트 초기에는 스프링 시큐리티를 기본 기능인 세션 기반 인증을 사용했습니다.
세션 기반 인증은 사용자의 로그인 상태를 서버 메모리에 저장하는 방식으로 간단하지만, 세션은 서버에 저장되기 때문에 사용자가 증가함에 따라 서버 메모리에 저장되는 세션 데이터도 증가하고 이로 인해 서버의 부하가 커질 것이라고 생각했습니다. 또한, 세션 하이재킹과 같은 보안 위협에 노출될 가능성이 있었습니다.
이러한 문제를 해결하기 위해 JWT를 도입하기로 결정했습니다. JWT는 클라이언트 측에서 토큰을 저장하고, 서버는 상태를 유지하지 않는 방식으로 동작합니다. 때문에 세션 기반을 사용했을 때의 한계를 극복할 수 있었습니다.
하지만 세션 기반을 버리고 토큰 기반으로 사용을 한다고 해서 모든 문제점이 해결되는 것은 아니었습니다.
먼저, JWT는 사용자 정보를 포함하기 때문에 토큰의 크기가 커질 수 있다는 점에서 이를 해결하기 위해 페이로드에는 필요한 최소한의 정보만 포함하도록 설계해야 했습니다. 또한, JWT는 한 번 발급되면 서버에서 강제로 만료시킬 수 없다는 단점이 존재했습니다.
그래서 AuthUser 도메인을 생성하여 필요한 정보만을 가질 수 있도록 구현을 했습니다.

이를 통해 사용자의 이메일과 닉네임만을 저장하도록 구현을 했고, 해당 정보를 가지고 현재 사용자가 인증된 사용자인지 확인할 수 있었습니다.

그리고 JWT는 강제로 만료시킬 수 없다는 점으로 인해서 발급되는 액세스 토큰의 만료 시간을 짧게 설정하고 리프레시 토큰을 도입하게 되었습니다.
리프레시 토큰은 데이터베이스에 저장하고, 액세스 토큰이 만료되었을 때 새로운 액세스 토큰을 발급하는 방식으로 문제를 해결했습니다.

토큰 도입과 그에 따른 성능 하락
리프레시 토큰 도입을 통해 강제로 토큰을 만료시킬 수 없다는 점과 액세스 토큰이 탈취되었을 때, 리프레시 토큰을 통해 보안적으로 대처할 수 있도록 했습니다.
하지만 리프레시 토큰을 데이터베이스에 저장을 하고 있기 때문에 사용자가 로그인을 하거나 액세스 토큰이 만료되었을 때 계속해서 데이터베이스에 접근을 해야 한다는 문제점이 존재했습니다.
때문에 지속적으로 데이터베이스에 접근을 하게 되면 서버 측의 부하가 쌓여 장애로 발생할 수 있다고 생각했습니다.
이를 해결하기 위해서 Redis를 도입하여 리프레시 토큰을 저장 및 관리하도록 하였습니다. 추가적으로 인메모리 기반 데이터베이스로, RDB에 비해 훨씬 빠른 속도를 제공했습니다. 그리고 TTL 설정을 통해 리프레시 토큰 또한 자동으로 만료시킬 수 있어서 관리가 용이했습니다.

Redis는 Hash 기반의 자료구조를 사용하여 토큰을 저장 및 만료할 수 있도록 하였습니다.

내 쿠키 돌려줘…
테스트 코드와 포스트맨을 통해 리프레시 토큰이 쿠키에 담겨서 정상적으로 넘어가는 것을 확인했습니다. 남은 과정은 클라이언트와의 통신을 통해서 리프레시 토큰을 확인하는 과정만 남았습니다.
그러나 클라이언트 측에서는 쿠키에 리프레시 토큰이 담겨서 넘어오긴 하지만 찾을 수 없고, 새로고침을 하면 리프레시 토큰이 사라진다는 문제점을 발견하게 되었습니다.
그래서 리프레시 토큰이 사라지는 문제가 CORS 정책 때문일 수도 있겠다는 생각을 했습니다. 클라이언트가 다른 도메인에서 API 요청을 보낼 때, 서버에서 Access-control-Allow-Credentials 헤더를 true로 설정하도록 하기 위해 allowCredentials 옵션을 설정해 주었다. 해당 설정을 통해 클라이언트가 인증 정보를 포함한 요청을 할 수 있도록 허용하지만, 여전히 동일한 문제가 지속되었습니다.
문제가 지속되자, 쿠키의 설정과 관련이 있을 수도 있다고 생각하여, Secure와 SameSite 속성을 설정하여 구현했습니다. 하지만 CORS 설정으로 해준 allowCredentials 옵션과 쿠키 설정으로 해준 secure 속성은 HTTPS 환경에서만 정상적으로 동작하도록 해주는 속성입니다.
현재 프로젝트는 HTTP로 배포된 상태였기 때문에, 해당 설정들이 제대로 작동하지 않아서 리프레시 토큰이 포함된 쿠키가 사라지거나 전달되지 않을 수 있다는 점을 깨닫게 되었습니다. 1차 프로젝트는 한 달 동안 구현을 진행해야 하는 프로젝트로 시간적인 여유는 존재하지 않기 때문에 배포를 위한 작업을 할 수도 없는 상황이었으므로… 아쉽지만 지금까지 구현했던 서비스는 모두 철거를 해야 했습니다.
마치며
이번 프로젝트를 되돌아봤을 때, 짧은 시간 동안 수많은 난관에 부딪히며 정말 많은 성장을 이룰 수 있었습니다.
아무것도 모르는 상태에서 시작했던 회원 인증 및 인가 서비스를 구현하기 위해 밤낮 없이 달려온 시간들이 떠오릅니다.
그럴 수 있었던 이유는 이 프로젝트가 혼자만의 프로젝트가 아닌 팀 프로젝트였기 때문이라고 생각합니다.
나로 인해 팀원들이 피해를 보게 하면 안되다는 책임감이 있었고, 그 책임감이 끝까지 포기하지 않고 문제를 해결하려고 하는 원동력이 되었다고 생각합니다.
또한, 팀원들과 함께 스크럼을 통해 각자의 진행 상황과 이슈를 공유하면서 서로의 생각을 나누는 과정에서 많은 인사이트를 얻을 수 있었습니다.
특히, 코드 리뷰를 통해 팀원들의 코드를 보면서 배울 점이 많았고, 코드의 품질을 더욱 신경 쓰며 구현해야겠다는 깨달음을 얻었습니다.
처음 사용하는 기술이라서 어려움도 컸고, 원하는 자료를 찾지 못해 갈증을 느낀 순간도 많았습니다.
하지만 그만큼 많은 애정을 쏟아 부었고, 그 과정에서 얻은 지식과 경험은 저에게 큰 자신이 되었다고 생각합니다.
물론, 열심히 구현한 서비스를 철거해야 한다는 점은 안타깝고 아쉽습니다만… 이번 프로젝트를 통해 배운 점들이 많았고, 그 과정에서의 성장이 더 값지다고 느껴졌습니다. 이번 경험을 바탕으로, 앞으로 회원 관련 서비스를 구현하게 된다면 분명 더 나은 결과물을 만들어낼 수 있을 것이라고 생각합니다.
'REFLECTION > 7️⃣ LUCKY7 트러블 슈팅' 카테고리의 다른 글
| [ 7️⃣ LUCKY-SEVEN ] JMeter와 함께한 병목 추적기 (0) | 2025.04.09 |
|---|---|
| [ 7️⃣ LUCKY-SEVEN ] 실시간 채팅 시스템 구조 재설계 이야기 (0) | 2025.03.08 |
| [ 7️⃣ LUCKY-SEVEN ] 배포 성공을 위한 삽질의 기록 (0) | 2025.02.26 |
