<사용자의 로그인 상태 유지에 대한 고민>
북마크 생성하기 같은 인증 / 인가 작업이 선행되어야하는 요청이 왔을 때, 사용자가 누구인지 식별하고 북마크 생성을 위한 권한이 있는지 검사하기 위해, 사용자에게 로그인을 먼저 하도록 요구해야 한다.
하지만 무상태 프로토콜인 HTTP를 사용할 경우 별도의 추가 조치가 없다면 사용자가 북마크 생성을 요청할 때 마다, 로그인 절차를 반복해야 한다. 사용자의 로그인 상태를 유지하기 위해서 별도의 조치가 필요하다.

가장 쉽게 시도할 수 있는 방법은 쿠키를 사용하는 것이다. 쿠키는 서버가 사용자의 웹 브라우저 전송해 두었다가, 사용자의 재 요청시 서버로 함께 전송되는 데이터 조각이다. 이를 사용자의 상태를 유지하는 데 사용할 수 있다.
하지만 이렇게 쿠키만으로 인증 상태를 유지하면 여러 문제가 발생한다. 클라이언트가 쉽게 쿠키를 조작할 수 있고, 쿠키가 탈취되면 대응이 어렵다. 또한 사용자 식별을 위해 민감한 개인 정보를 쿠키에 담아두면 네트워크 상에서 그대로 노출될 수 있다.

보통 세션을 쿠키와 함께 사용해서 로그인을 구현한다. 세션은 서버측의 별도 저장소에 사용자 상태 정보를 저장하고 클라이언트에서는 이를 식별할 수 있는 세션 ID를 갖고 서버와 통신하는 방법이다. 여기서 세션 저장소는 서버의 메모리 내에 위치할 수도 있고, DB나 별도의 저장소에 위치 할 수도 있다.
세션과 쿠키를 함께 사용하여 인증을 유지하면, 웹 브라우저 쿠키 저장소에 보관된 세션 키는 실제로 아무런 의미를 가지지 않는 랜덤 문자열이기 때문에, 악성 사용자가 다른 사람의 쿠키 값을 추측하기 어려워진다. 또한, 쿠키 값 자체에 민감정보가 포함되지 않기 때문에 보안성이 더욱 강화된다.
또한, 세션 저장소는 서버 측에 존재하기 때문에, 아이디가 해킹당했다고 신고를 받거나, 해당 아이디에서 광고 메일을 마구 전송하는 등의 이상 행동이 감지되는 경우, 서버는 세션 저장소에서 해당 사용자의 세션 키를 제거하여, 더 이상 탈취된 쿠키를 사용해 요청을 보낼 수 없게 하고, 해당 사용자를 자동으로 로그아웃시키는 등의 추가적인 보호 조치를 취할 수 있다.

그런데 이번 프로젝트는 트래픽이 증가할 경우, AWS 오토 스케일링을 활용한 Scale Out으로 대응할 생각이라 항상 단일 서버 인스턴스 환경이 아님을 생각하고 모든 구현을 진행해야 했다.

세션 저장소가 각각의 서버 내부에 존재할 경우 위와 같은 문제가 발생 할 수 있다.

이를 해결하기 위해 서버 A에서 로그인한 사용자에게 Set-Cookie를 통해 "앞으로 너의 요청을 담당하는 서버는 무조건 서버 A야"라고 세션 정보를 기록하는 Sticky Session 방식을 사용할 수 있다.
하지만 이 방식은 로그인에 성공한 각각의 사용자들이 고정된 서버에 요청이 전달하기 때문에, 여러 대의 서버에서 트래픽을 분산시키려는 기존 목적에 부합하지 않는다.
예를 들어, 운이 나쁘게 서버 A에서 로그인 한 사용자들이 굉장히 활발하게 활동하고 서버 B에서 로그인 한 사용자들은 활동을 잘 하지 않는다면, 서버 A는 과부하가 걸리고 서버 B는 쉬게 되는 상황이 발생할 수 있다.
또한, 사용자의 담당 서버가 다운되면 해당 서버 메모리에 저장된 세션 정보가 사라져 사용자는 다시 로그인을 해야 하는 불편함이 생긴다.

다중 서버환경에서 세션 불일치 문제를 해결하기 위한 또 다른 방식에는, 사용자가 로그인하면 해당 서버에 세션을 저장하고, 그 세션을 다른 모든 서버에 복사하여 저장하는 Session Clustering이 있다.
하지만 동일한 세션을 복사해서 사용한다는 점이 저장공간 낭비 같고, 세션이 복사되는 동안 다른 요청이 들어오거나 세션 복사 중에 실패하는 예외 등의 여러 복잡한 문제가 발생할 수 있을 것이다.

마지막으로 서버 외부에 별도의 세션 저장소를 두고 모든 서버가 이를 공유해서 사용하는 방식인 Session Storage 방식이 있다. 이 방식은 실제로 다중서버 환경에서 세션 불일치 문제를 해결하기위해서 가장 많이 사용된다고 한다.
이 방식에서는 별도의 세션 저장소 서버가 필요하다. 세션 저장소는 로그인 상태 확인이 필요한 모든 요청에서 접근되므로 빠른 응답 속도가 요구된다. 이를 위해 보통 Redis가 사용된다.
또한 Redis를 사용하면 세션 만료 관리를 위한 TTL 설정을 지원하기 때문에, 사용자의 로그인 상태를 일정 시간 후 자동으로 해제하는 관리가 편해진다.
다만, 세션 저장소가 마비되면 로그인이 필요한 모든 서비스가 마비 될 수 있고, Redis 노드가 다운시 AOF와 같은 데이터 복구 설정이 없다면, 모든 사용자가 다시 로그인을 해야 하는 상황이 발생할 수 있다.

세션을 이용한 방식 외에도 최근에는 토큰을 이용한 로그인 방식이 많이 사용되고 있다. 토큰의 종류, 클라이언트와 서버가 토큰을 주고 받는 방법들은 상황에 따라 다르다.
토큰 방식에서는 어떤 서버에서 로그인을 했든 클라이언트에서 보내온 토큰을 확인하여 인증과 인가를 진행하기 때문에, 별도의 세션 저장소를 두는 방식보다 인프라 구성이 쉽고, 서버 측에서 별도로 세션 저장소를 두지 않기 때문에 저장공간도 절약할 수 있다.
대신 세션 로그인처럼 서버에서 사용자를 제어하기 어렵다. 예를 들어, 같은 아이디로 동시에 로그인하는 수를 제어하거나 악성 행동 감지 시 강제 로그아웃을 시키기 위해 별도의 추가 로직이 필요하다. 또한, 토큰 자체가 탈취되면 해당 토큰의 만료 기간까지 제어할 수 없어, 이에 대한 대비책도 추가로 필요하다.
결론부터 얘기하자면, 나는 토큰 방식의 로그인을 선택했다. 이번에 만드는 서비스의 성격상, 활동 중인 사용자를 서버에서 직접 제어할 필요가 없을 것 같고, 토큰 방식이 세션 방식보다 인프라 측면에서 고민을 덜어줄 수 있을 것 같았다. 토큰으로는 JWT를 이용했다.
세션과 토큰을 적절히 결합하여 사용하는 방식도 있는 것으로 알고 있으나, 아직 이 방식에 대한 이해가 부족하고 이번 프로젝트에서 그 정도의 복잡도를 고려해서 도입 할 필요는 없을 것 같았다.
<사용자의 인증 방식에 대한 고민>
사용자의 로그인 상태를 어떻게 유지할지는 결정했고 이번에는 사용자의 인증 방식에 대해 어떻게 진행할지에 대해 고민해 봤다.

ID와 비밀번호를 직접 입력받는 폼 로그인이나, 대부분의 웹 브라우저에서 기본 제공하는 HTTP Basic 인증 방식을 사용할 수도 있다.
그러나 내가 만들고자 하는 서비스는 GitHub API를 활용해 GitHub 사용자들에게 더 재미있고 편리한 기능을 제공하는 서비스이고, 회원가입과 로그인 과정에서 발생하는 사용자의 거부감을 줄이기 위해, GitHub OAuth 기반 로그인 방식을 사용하기로 결정 했다.

만약 내 서비스에서 깃허브 연동 로그인을 구현하기 위해, 깃허브에 "지금 우리 서비스에 사용자가 자신의 깃허브 아이디와 비밀번호를 입력했어! 사용자의 아이디와 비밀번호가 일치하는지 너네가 확인해줘!" 같은 요청을 보내면, 깃허브 측에서는 이를 처리하지 않을 것이다.
절대로 해 줄 리가 없다. 깃허브 입장에서는 내가 만든 서비스에서 실제 사용자가 깃허브 아이디로 로그인을 시도하는 것인지, 아니면 내가 악의적인 요청을 보내는 것인지 알 수 없다. 만약 이런 무리한 요구에 응해 줬다가 보안 사고라도 발생하면 큰 문제가 될 것이다.
이럴 경우 OAuth 프로토콜을 활용할 수 있다. 여기서 사용자가 OAuth 인증 과정을 성공적으로 마치면, 해당 사용자의 OAuth Token이 발급되는데, 이를 이용해서 해당 OAuth Provider의 리소스 서버에 접근해 사용자에게 추가적인 기능을 제공할 수도 있다.
OAuth의 인증 방식에는 권한 부여 승인 코드 방식, 암묵적 승인 방식, 자원 소유자 자격증명 승인 방식, 클라이언트 자격증명 승인 방식 등 총 4가지가 존재하는데, 나는 이 중에서 웹 애플리케이션에서 가장 많이 사용되는 권한 부여 승인 코드 방식을 사용했다.

우선 OAuth를 이용하기 위해서는 크게 사용자, 내가 만든 서비스, OAuth를 제공하는 깃허브 같은 제3자 서비스가 필요하다. 여기서 Github OAuth를 이용하고 싶은 내 서비스를 Client, Github에 저장된 사용자의 실제 자원을 Resource, 실제 사용자를 Resource Owner, 사용자의 정보를 관리하는 서버를 Resource Server, OAuth에 필요한 인증 과정을 담당하는 서버를 Authorization Server라고 부른다.

우선 사용자는 더 이상 내가 만든 서비스에서 직접 로그인하는 것이 아니라, 깃허브와 같은 OAuth를 제공하는 서비스의 로그인 페이지에서 ID와 Password를 입력하여 깃허브에 직접 로그인하게 된다. 그럼 인증 서버는 사용자에게 OAuth 인증 과정에 필요한 Authorization Code라는 임시 코드를 넘겨준다.

그 후 사용자는 Authorization Server에서 받은 임시 코드를 내 서비스(Client) 에게 넘겨준다.

이제 내 서비스는 사용자에게서 전달 받은 임시 코드를 Authorization Server에 넘겨준다. 만약 Authorization Server가 사용자에게 전달했던 임시 코드와, 내 서비스에서 전달 받은 임시 코드가 일치하면, 내 서비스에서 Resource Server에 접근할 수 있는 Access Token을 넘겨준다.

해당 Access Token으로 내 서비스에서 사용자가 제공에 동의한 Resource에 접근할 수 있게 된다. 자세한 구현 과정은 더 복잡하지만, 간단히 요약하면 이와 같은 흐름이라고 이해하고 있다. 실제 구현은 뒤에서 자세히 다룰 생각이다.
<인증, 인가 관련 로직은 어디에서 처리할지에 대한 고민>
이제 사용자 인증을 어떻게 수행할지, 로그인 된 사용자의 상태를 어떻게 유지할지 모두 결정했다. 마지막으로 인증 / 인가와 관련된 로직을 어디에 구현할지 고민해 봤다.

사용자의 요청에 대한 인증과 인가 처리는 인증 및 인가가 필요한 API에서 공통으로 반복된다. 매번 인증과 인가 처리를 각 API 앞에 넣게 되면 코드를 작성하는 것도 어려워지고, 추후 로그인과 관련된 로직 변경이 발생했을 때 유지보수가 매우 어려워질 것이다.
따라서 로그인과 같은 공통 관심사를 분리하여 관리할 방법이 필요했다. 이를 위해 Filter, Interceptor, AOP 3가지의 선택지를 고민해 봤다.

톰캣 같은 서블릿 컨테이너에 Spring의 Dispatcher Servlet이 등록되고 해당 서블릿으로 들어오는 모든 요청은 서블릿 필터를 우선적으로 통과한다. 인증 인가 관련 필터를 서블릿 필터 사이에 끼워 넣어 공통 로직인 인증, 인가를 처리 할 수 있을 것이다.

인터셉터는 스프링에서 서블릿 컨테이너의 필터와 비슷한 역할을 하지만, 조금 더 정교하게 동작하며 요청 처리 과정의 다양한 시점에 개입할 수 있다. 이를 활용해도 HTTP 요청이 컨트롤러에 도달하기 전에 인증·인가 로직을 처리할 수도 있다.

스프링 AOP를 이용하여 특정 비즈니스 로직을 위한 메서드가 실행되기 직전에 인증, 인가를 처리하는 방법도 사용 할 수 있을 것이다.
결론부터 얘기하자면 필터를 사용하는 게 가장 좋아 보였다. 어떤 요청에서 인증과 인가를 실패하는 경우들도 많을 텐데, 해당 요청이 더 뒤로 전달되기 전에 빠르게 필터에서 걸러내는 게 가장 효율적일 것 같다고 생각했다. 또한 이번에 사용할 Spring Security도 필터 기반으로 인증과 인가를 처리한다.
예전에 카카오 OAuth를 이용한 로그인과 필터 기반의 인증 인가 처리를 직접 구현한 경험이 있다. 그 당시에는 굳이 러닝 커브가 높은 Spring Security를 도입할 필요가 없다고 생각했다.
하지만 직접 만든 인증, 인가, 보안 처리 필터는 실 서비스에 적용하기에는 Spring Security보다 미흡한 부분이 많았고, 카카오에 사용자가 로그인 후, Authorization Code를 들고 내 서비스로 돌아올 때, 해당 요청 자체를 탈취하는 공격에 대한 대응이나, 사용자가 인증이 필요한 요청을 보냈을 때 로그인 페이지로 넘어가고 돌아온 후 기존에 진행하던 요청을 바로 다시 진행하는 등의 요구사항이 추가될 때마다 확장에 어려움을 겪었다.
또한, 매번 막히는 부분이 있을 때마다 참고할 레퍼런스가 딱히 없어, Spring Security에서는 어떻게 해결했는지 찾아보게 되었고, 내가 만든 Security에 대해 모든 팀원들에게 설명해 줘야 하는 문제가 있었다.
이런 이유들로 애초에 Spring Security를 공부해서 인증, 인가, 보안 처리를 도입하는 게 처음에는 어렵지만, 훨씬 도움이 된다고 생각했다.
<참고 자료>
🌐 세션(Session) 불일치 문제 및 해결 방법
서버 다중화 환경에서의 세션 불일치 단일 서버 환경에서는 session을 통한 로그인을 구현할때 session 불일치 문제를 신경쓸 필요가 없다. 하지만 서비스가 커짐에 따라 한대의 서버로 운영하는것
inpa.tistory.com
스프링 MVC 2편 - 백엔드 웹 개발 활용 기술| 김영한 - 인프런 강의
현재 평점 5.0점 수강생 24,878명인 강의를 만나보세요. 웹 애플리케이션 개발에 필요한 모든 웹 기술을 기초부터 이해하고, 완성할 수 있습니다. MVC 2편에서는 MVC 1편의 핵심 원리와 구조 위에 실
www.inflearn.com
🌐 JWT 토큰 인증 이란? (쿠키 vs 세션 vs 토큰)
Cookie / Session / Token 인증 방식 종류 보통 서버가 클라이언트 인증을 확인하는 방식은 대표적으로 쿠키, 세션, 토큰 3가지 방식이 있다. JWT를 배우기 앞서 우선 쿠키와 세션의 통신 방식을 복습해
inpa.tistory.com
🌐 OAuth 2.0 개념 - 그림으로 이해하기 쉽게 설명
OAuth란? 웹 서핑을 하다 보면 Google과 Facebook 등의 외부 소셜 계정을 기반으로 간편히 회원가입 및 로그인할 수 있는 웹 어플리케이션을 쉽게 찾아볼 수 있다. 클릭 한 번으로 간편하게 로그인할
inpa.tistory.com
[Spring] 필터(Filter)가 스프링 빈 등록과 주입이 가능한 이유(DelegatingFilterProxy의 등장) - (2)
몇몇 포스팅과 조금 오래된 책들을 보면 필터(Filter)는 서블릿 기술이라서 Spring의 빈으로 등록할 수 없으며 빈을 주입받을수도 없다는 내용이 나옵니다. 하지만 실제로 테스트를 해보면 Filter 역
mangkyu.tistory.com
Architecture :: Spring Security
The Security Filters are inserted into the FilterChainProxy with the SecurityFilterChain API. Those filters can be used for a number of different purposes, like exploit protection, authentication, authorization, and more. The filters are executed in a spec
docs.spring.io
HTTP 쿠키 - HTTP | MDN
HTTP 요청을 수신할 때, 서버는 응답과 함께 Set-Cookie 헤더를 전송할 수 있습니다. 쿠키는 보통 브라우저에 의해 저장되며, 그 후 쿠키는 같은 서버에 의해 만들어진 요청(Request)들의 Cookie HTTP 헤더
developer.mozilla.org
'토이 프로젝트 > 깃허브 연동 로그인' 카테고리의 다른 글
| 깃허브 연동 로그인 [03] - JWT 도입 (0) | 2025.10.02 |
|---|---|
| 깃허브 연동 로그인 [02] - Spring Security를 활용한 로그인 구현 (0) | 2025.10.01 |