로그인은 어떤 방식으로 개발해야 안전할까?

반응형

로그인을 처리하는 방식에는 크게 세션 방식과 토큰 방식이 있습니다. 토큰 방식을 사용하는 대표적인 이유는 stateless하기 때문입니다. 여기서 stateless하다는 것은 서버가 로그인 상태를 직접 저장하지 않아도 된다는 뜻입니다. 즉, 사용자가 로그인한 뒤 요청을 보낼 때마다 토큰을 함께 전달하고, 서버는 그 토큰이 유효한지만 검증하면 됩니다. 토큰 방식의 장점은 트래픽이 많아졌을 때 조금 더 잘 드러납니다. 세션 방식은 로그인 상태를 서버나 세션 저장소에 보관해야 하기 때문에, 요청이 많아질수록 세션 조회 비용이 계속 발생합니다. 반면, 토큰 방식은 서버가 별도의 로그인 상태를 저장하지 않아도 되기 때문에, 많은 요청이 동시에 들어와도 상대적으로 서버 부담이 적을 수 있습니다. 물론 실제 서비스에서는 중복 로그인 확인, 로그아웃 처리, refresh token 관리 등을 위해 데이터베이스나 Redis 같은 저장소를 함께 사용하는 경우도 많습니다. 하지만 여기서는 그런 예외 상황은 제외하고, 순수하게 세션 방식과 토큰 방식 중 트래픽이 많을 때 어떤 차이가 발생하는지를 비교해보겠습니다.

간단하게 세션과 토큰 방식에 대해 이해 해봅시다.

세션 방식

세션 방식은 사용자가 로그인에 성공하면 서버가 세션을 생성하고 저장하는 방식입니다. 이후 서버는 사용자를 식별할 수 있는 고유한 세션 ID를 발급하며, 클라이언트는 이 값을 쿠키에 저장하게 됩니다. 사용자가 요청을 보낼 때마다 쿠키에 저장된 세션 ID가 함께 전달되고, 서버는 해당 세션 ID를 이용해 저장된 세션 정보를 조회한 뒤 인증 여부를 확인합니다.

즉, 실제 로그인 정보는 서버가 관리하고 있으며, 클라이언트는 단순히 세션을 식별하기 위한 키(Session ID)만 가지고 있는 구조입니다.
간단히 표현하면 다음과 같습니다.

세션 방식의 특징은 서버가 사용자의 로그인 상태를 직접 관리하기 때문에 강제 로그아웃이나 중복 로그인 제어가 비교적 쉽다는 점입니다. 반면 사용자가 많아질수록 서버 또는 세션 저장소가 관리해야 하는 데이터도 함께 증가하게 됩니다.

토큰 방식

토큰 방식은 사용자가 로그인에 성공하면 서버가 토큰을 발급하고, 이후 사용자는 요청마다 해당 토큰을 함께 전달하는 방식입니다.
서버는 별도의 로그인 상태를 저장하지 않고 토큰의 유효성만 검증하기 때문에 Stateless한 구조를 만들 수 있습니다.

즉, 세션 방식은 서버가 로그인 상태를 관리하고, 토큰 방식은 사용자가 인증 정보를 직접 가지고 다니는 방식이라고 이해하면 됩니다. 토큰 방식은 서버에 로그인 상태를 저장하지 않기 때문에 서버를 여러 대로 확장하기 쉽다는 특징이 있습니다.

그럼 어떻게 테스트를 할까?

세션 방식과 토큰 방식의 공통점은 모두 인증을 위해 사용되는 기술이라는 점입니다. 따라서 회원가입과 로그인을 제공하는 대부분의 서비스에서 사용됩니다.

하지만 모든 기술이 하나의 목적에만 사용되는 것은 아닙니다.

예를 들어 세션 방식은 반드시 로그인에만 사용되는 것이 아닙니다. 대표적으로 장바구니 기능이 있습니다. 사용자가 로그인하지 않았더라도 세션을 이용해 장바구니 정보를 저장할 수 있습니다. 이 경우 세션은 인증이 아닌 사용자의 상태를 유지하기 위한 용도로 활용됩니다.

토큰 방식 역시 로그인에만 사용되는 것은 아닙니다. 외부 API를 호출할 때 사용하는 API Key나 Access Token도 넓은 의미에서는 토큰 기반 인증 방식에 해당합니다. 사용자가 아닌 시스템 간 통신에서도 토큰은 자주 활용됩니다.

즉, 세션과 토큰은 주로 인증에 사용되지만, 본질적으로는 사용자를 식별하거나 특정 권한을 증명하기 위한 수단이라고 볼 수 있습니다. 따라서 로그인뿐만 아니라 상태 유지, 시스템 간 인증, API 접근 제어 등 다양한 영역에서 활용될 수 있습니다.

하지만 저는 이 모든것을 구현하지 않을겁니다. 

계획은 단순합니다. 사용자가 API를 호출하면 세션 또는 토큰을 통해 인증을 수행하고, 인증에 성공한 경우 "hello" 문자열을 반환하도록 구현할 예정입니다.

이를 통해 동일한 기능을 세션 방식과 토큰 방식으로 각각 구현한 뒤, 트래픽이 증가했을 때 어떤 차이가 발생하는지 비교해보겠습니다.

예상대로라면 토큰 방식이 더 빠를 것이라 생각했습니다. 서버가 상태를 저장하지 않는 Stateless 구조이기 때문에, 세션 조회 비용이 존재하는 세션 방식보다 유리할 것이라고 예상했기 때문입니다. 하지만 실제 결과를 확인해보니 오히려 세션 방식이 더 빠르게 측정되었습니다. 특히 30K 트래픽 구간에서는 세션 방식이 토큰 방식보다 약 2배 이상 빠른 결과를 보여주었습니다.

그렇다면 왜 이런 결과가 발생한 것일까요?

그 이유는 인증 과정에서 수행되는 작업의 차이에 있습니다. 세션 방식은 클라이언트가 전달한 Session ID를 기반으로 서버에 저장된 세션 정보를 조회하면 인증이 완료됩니다. 반면 토큰 방식은 요청이 들어올 때마다 토큰의 서명을 검증하고, 만료 시간을 확인하며, 토큰의 무결성을 검증하는 과정을 수행해야 합니다.

특히 JWT를 사용한다면 Base64 디코딩, Signature 검증(HMAC, RSA, ECDSA 등), Claims 파싱 과정이 매 요청마다 발생하게 됩니다. 이 과정은 서버 메모리 조회보다 CPU 사용량이 더 높은 작업에 해당합니다.

즉, 흔히 토큰 방식이 더 빠를 것이라고 생각하기 쉽지만, 실제로는 세션 조회 비용보다 토큰 검증 비용이 더 클 수 있습니다. 따라서 단순히 인증 처리 성능만 비교한다면 세션 방식이 더 빠르게 측정되는 경우도 충분히 발생할 수 있습니다.

결국 토큰 방식의 장점은 인증 속도 자체가 아니라 Stateless 구조를 통한 확장성에 있습니다. 서버가 로그인 상태를 저장하지 않아도 되기 때문에 여러 대의 서버를 운영하거나 MSA 환경으로 확장할 때 관리가 쉬워지는 것이지, 반드시 요청 처리 속도가 더 빠르다는 의미는 아닙니다.

여기서 알수 있는 사실은 단순히 속도가 빨라서 토큰을 사용하는게 아니라는 뜻입니다.

그렇다면, 실행  환경이 늘어나면 어떻게 될까요?
토큰 방식의 장점은 stateless하다는 점입니다. 그렇다면, 이 점을 가장 극대화를 시키려면 어떻게 해야 할까요?

만약 실행 환경(Pod 또는 WAS)이 여러 개라고 가정해봅시다. 세션 방식은 기본적으로 서버가 로그인 상태를 저장합니다. 따라서 각 서버가 자신만의 세션 저장소를 가지고 있다면 문제가 발생할 수 있습니다. 예를 들어, 사용자가 실행 환경 1에서 로그인하여 세션이 생성되었다고 가정해보겠습니다. 이후 다음 요청이 로드밸런서에 의해 실행 환경 2로 전달된다면, 실행 환경 2는 해당 세션 정보를 가지고 있지 않기 때문에 사용자를 인증할 수 없습니다.

즉, 사용자는 이미 로그인했음에도 불구하고 다시 로그인을 요구받을 수 있습니다.

이러한 문제를 해결하기 위해 Sticky Session을 사용하거나, Redis와 같은 외부 세션 저장소를 두어 모든 실행 환경이 동일한 세션 정보를 조회할 수 있도록 구성합니다.

반면, 토큰 방식은 서버가 로그인 상태를 저장하지 않습니다. 사용자가 토큰만 가지고 있다면 어떤 실행 환경으로 요청이 전달되더라도 동일한 방식으로 인증을 수행할 수 있습니다. 이러한 특징 때문에 분산 환경에서는 토큰 방식이 확장성 측면에서 유리하다고 평가받습니다.

하지만 현재 테스트 방식은 실제 로그인 방식과는 다릅니다. 사용자가 로그인하고 인증 정보를 발급받는 구조가 아니라, 단순히 API를 요청하면 인증 과정을 거친 뒤 "hello" 문자열을 반환하는 단순한 코드입니다. 따라서 세션 방식과 토큰 방식을 더 정확하게 비교하려면, 실제 로그인 과정까지 포함하도록 코드를 변경할 필요가 있다고 생각합니다. 그렇다고 해서 위 테스트가 의미 없다는 뜻은 아닙니다. 단일 실행 환경에서는 세션 방식과 토큰 방식 모두 큰 구조적 차이 없이 동작하기 때문에, 결과적으로는 지연 시간 차이만 발생하고 전체적인 흐름은 비슷하게 나올 것이라고 예상할 수 있습니다.

로그인 방식으로 변경해봅시다.

실행 환경을 3개로 늘려서 테스트를 진행해보겠습니다. 그렇다고 해서 

이렇게 테스트하면 곤란해집니다. 왜냐하면 실행 환경이 여러 개인 것처럼 보이더라도, 실제로는 실행 환경이 한 개인 것과 큰 차이가 없기 때문입니다.

그래서 저는 중간에 Nginx를 두고 테스트를 진행할 예정입니다. 이렇게 하면 사용자는 하나의 주소로만 접근하지만, Nginx가 요청을 여러 서버로 분산시켜 실제 운영 환경과 유사한 구조를 만들 수 있습니다.

이제 똑같이 부하를 주면서 재 테스트를 진행해보겠습니다. 어떤 결과가 나올까요?

지연시간을 확인해보니 생각보다 흥미로운 결과가 나왔습니다. 1,000건까지는 토큰 방식의 지연시간이 세션 방식보다 조금 더 높게 나타났습니다. 하지만 5,000건부터 10,000건 구간에서는 오히려 토큰 방식의 지연시간이 더 낮게 측정되었습니다.

그런데 트래픽이 20,000건을 넘어가자 다시 토큰 방식의 지연시간이 증가하면서 세션 방식보다 느린 결과가 나타났습니다.

일반적으로 토큰 방식은 매 요청마다 토큰을 검증해야 하므로 세션 방식보다 불리할 것이라고 예상했습니다. 하지만 실제 결과는 단순하지 않았습니다. 특정 구간에서는 토큰 방식이 더 좋은 성능을 보여주었고, 트래픽이 더욱 증가하자 다시 역전되는 모습을 확인할 수 있었습니다.
이는 단순히 "세션이 빠르다", "토큰이 빠르다"로 결론 내릴 수 있는 문제가 아니라, 트래픽 규모와 실행 환경에 따라 결과가 달라질 수 있다는 점을 보여주는 사례라고 생각합니다.

그렇다면, 인증 성공률은 어떨까요?

100 - 인증 성공률 = 커넥션 에러

인증 성공률을 확인해보니 더욱 흥미로운 결과가 나왔습니다. 트래픽이 10건에 불과한 상황에서도 세션 방식의 인증 성공률은 16.8%로 매우 낮게 측정되었습니다. 반면 토큰 방식은 85%가 넘는 인증 성공률을 보여주었습니다.

이러한 차이가 발생한 이유는 인증 방식 자체의 문제가 아니라 분산 환경에서의 동작 방식 차이 때문입니다.

세션 방식은 로그인 정보가 각 서버 내부에 저장됩니다. 따라서 사용자가 서버 A에서 로그인하더라도 이후 요청이 서버 B로 전달되면 서버 B는 해당 세션 정보를 알 수 없어 인증에 실패하게 됩니다.

반면 토큰 방식은 서버가 로그인 상태를 저장하지 않습니다. 사용자가 토큰만 가지고 있다면 어떤 서버로 요청이 전달되더라도 동일한 방식으로 인증을 수행할 수 있습니다. 따라서 여러 서버로 요청이 분산되는 환경에서도 비교적 안정적인 인증 성공률을 보여줄 수 있습니다.

결국 현재 테스트 환경에서 세션 방식의 인증 성공률을 높이기 위해서는 Sticky Session을 적용하거나, Redis와 같은 외부 세션 저장소를 구축하여 모든 서버가 동일한 세션 정보를 조회할 수 있도록 구성해야 할 것으로 보입니다.

참고로 단일 환경에서의 인증 성공률은 다음과 같습니다.

단일 환경에서는 세션이나 토큰이나 결과가 크게 다르지 않다는 것을 알 수 있었습니다.
다시 3파드로 넘어와서 위에서 100 - 인증 성공률은 커넥션 실패라고 하였습니다. 과연 그럴까요?

하지만 결과를 보니 토큰은 항상 100%를 기록하는거에 비해 세션 파트는 100%가 된적이 단 한번도 존재하지 않았습니다. 이런 현상이 발생한 이유가 무엇 일까요?

그 이유는 위에서 계속 언급했듯이 세션 정보의 저장 위치 때문입니다. 토큰 방식은 사용자가 직접 토큰을 가지고 요청하기 때문에 어떤 서버로 요청이 전달되더라도 동일한 방식으로 인증을 수행할 수 있습니다.

마무리

처음에는 토큰 방식이 Stateless 구조이기 때문에 세션 방식보다 처리 속도가 빠를 것이라고 생각했습니다. 하지만 실제 테스트 결과를 확인해보니 단일 환경에서는 오히려 세션 방식이 토큰 방식보다 더 빠르게 동작한다는 사실을 확인할 수 있었습니다.

그렇다면 실행 환경(Pod)이 여러 개로 늘어나면 어떤 현상이 발생할까요?

지연시간 측면에서는 단일 환경과 비교했을 때 눈에 띄는 차이를 발견하지 못했습니다. 하지만 인증 성공률을 기준으로 비교해보니 토큰 방식이 압도적으로 높은 결과를 보여주었습니다.

특히 세션 방식은 인증 성공률과 HTTP 연결 실패율을 합쳐도 100%가 되지 않는 현상을 발견하였습니다. 처음에는 원인을 이해하지 못했지만, 확인해보니 현재 테스트 환경의 세션 방식은 세션 정보를 서버 간에 공유하지 않고 있었습니다. 즉, 사용자가 로그인한 서버와 요청을 처리하는 서버가 달라질 경우 인증에 실패할 수 있었고, 이러한 구조 때문에 예상치 못한 결과가 발생한 것이었습니다.

저는 세션 방식이 구현해야 할 코드가 적기 때문에 토큰 방식보다 단순하다고 생각했습니다. 하지만 실행 환경이 여러 개로 늘어나는 순간 이야기가 달라졌습니다. 세션을 사용하기 위해서는 Sticky Session을 적용하거나 Redis와 같은 공통 세션 저장소를 구축해야 하며, 이 과정에서 추가적인 운영 및 관리 비용이 발생합니다.

반면 토큰 방식은 별도의 세션 공유 구조 없이도 여러 실행 환경에서 동일하게 인증을 수행할 수 있었습니다. 물론, 토큰 검증 과정에서 추가적인 연산 비용이 발생하지만, 분산 환경에서는 이러한 특징이 큰 장점으로 작용할 수 있다는 점을 확인할 수 있었습니다.

이번 학습을 통해 인증 방식은 단순히 처리 속도만으로 판단할 수 있는 문제가 아니라는 것을 깨달았습니다. 실제 서비스에서는 성능뿐만 아니라 확장성, 운영 편의성, 인증 성공률까지 함께 고려해야 합니다.

또한 토큰 방식에서 중복 로그인 관리, 로그아웃 처리, Refresh Token 관리 등을 위해 Redis나 데이터베이스를 사용하는 것이 Stateless라는 개념과 상충되는 것이 아니라는 점도 배울 수 있었습니다. 중요한 것은 데이터를 저장하느냐가 아니라, 인증 자체를 서버 세션 상태에 의존하지 않는다는 점이라는 사실을 이번 실습을 통해 확인할 수 있었습니다.

반응형

댓글

Designed by JB FACTORY