본문 바로가기

비교 시리즈/세션과 JWT 그래서 뭐가 더 좋은데?

세션과 JWT 그래서 뭐가 더 좋은데?

목차

  1. 서론
    1. 들어가며
    2. 목적 및 환경
  2. 본론
    1. 주의사항
    2. 테스트 시나리오
    3. 테스트 코드 (gatling)
  3. 결론
    1. 테스트 수행 결과
    2. 정리
  4. 후기
    1. 좋았던 점
    2. 아쉬웠던 점
    3. 생각할 거리
  5. 레퍼런스

1. 서론

1-1. 들어가며

저는 컴퓨터학부를 전공하였습니다. 전공 특성상 수업에서의 내용을 토대로 진행하거나 비슷한 실력을 지닌 학우 간 진행하는 토이프로젝트가 많았습니다. 대다수의 토이프로젝트는 사용자가 있는 것을 가정하고 진행하였기에 사용자를 구분하게끔 하는 로그인 기능은 필수였습니다.

 

당시 저는 로그인 기능을 위해 구글링을 하고, 다른 사람들이 구현한 토이프로젝트의 로그인 방법를 참고하고자 보아도 모두가 하나 같이 로그인 방법으로 Jwt를 사용하는 것을 확인할 수 있었습니다.

세션은 서버의 메모리에 세션을 저장하고 있어 서버에 부담이 되지만
Jwt는 상태를 저장하지 않기에 서버에 부담이 가지 않는다.

 

이론적으로 위의 말이 맞습니다. 그러나 '세션은 서버에 얼마나 부담이 가지?', '세션을 이용하였을때 사용자는 최대 몇 명까지 감당이 가능한 걸까?', 'Jwt는 토큰이 탈취되었을 때 서버에서 조치할 수 없음에도 불구하고 세션보다 압도적으로 좋을 걸까?'와 같은 깊은 고민 없이 로그인 방법을 선택해서는 안된다 생각이 들었습니다.

 

이 때문에 세션과 JWT를 비교한 글을 찾아보았으나 모두 이론적인 이야기를 할 뿐 데이터를 통해 비교 분석하는 글을 찾지 못하였습니다. 그렇기에 이번 기회를 통해 제가 직접 데이터를 통해 각자의 장단점을 확인하고자 하고자 합니다.

 

 

❗주의

다른 블로그 글들이 나쁘고 잘못되었다는 말이 아닙니다. 단지 제가 원하는 데이터를 기반으로 비교하는 글이 없어 제가 직접 비교하고 이를 결과로 남기고 싶을 뿐입니다.

 

1-2. 목적 및 환경

로그인 처리하는 방법(인터셉터를 이용한 로그인처리, 시큐리티 + 세션을 이용한 로그인 처리, 세션시큐리티 + JWT를 이용한 로그인처리)을 다르게 구현한 서버에 테스트 시나리오에 따른 요청을 보내고 결과 값을 확인하고자 합니다.

  • CPU 사용량
  • JVM 메모리 사용량
  • DB 커넥션
  • Was 스레드
  • 요청당 평균 응답속도
  • 서버가 감당할 수 있는 최대 요청수
  • Throughput

인터셉터

 

시큐리티(세션, JWT)

자바 버전: JDK17

서버: Spring Boot

유저 저장용 DB: mariaDB

JWT: Spring Security

인메모리 DB: Redis

메트릭 수집: Prometheus

메트릭 확인: Grafana

테스팅 툴: Gatling

가상환경: Docker

2. 본론

2-1. 주의사항

제한된 컨테이너 스펙

테스트는 저의 맥북에서 수행했습니다. 제 맥북은 m2칩을 사용하고 있으며 메모리는 16GB입니다. 이러한 환경에서 테스트를 수행할 경우 로컬 환경으로 인해 로그인방법별 성능지표를 확인하기 어렵다 생각하였습니다. 이러한 문제에 대한 해결책으로 Docker의 가상화 환경을 사용하기로 하였으며 컨테이너 스펙은 설정할 수 있는 최솟값인 CPU는 1 Core, Memory는 1GB로 설정하였습니다.

 

JVM 내부 최적화

저는 Java와 Spring Boot를 이용해 테스트를 수행했고 JVM을 사용할 수 밖에 없었습니다. 그리고 JVM은 JIT컴파일러, 바이트 코드 최적화등 사용자가 설정해주지 않아도 다양한 최적화를 수행합니다. 그러나 해당 프로젝트에서는 JVM내부적으로 최적화를 할 경우 프로젝트 기존의 목적을 해친다고 판단하였습니다. 그렇기에 저는 최적화가 되지 않는 결과를 얻기 위해 매 테스트 별 서버를 재구동시켰습니다.

2-2. 테스트 시나리오

테스트 시나리오는 총 2개로 서버가 다운되는 임계지점까지 수행하였습니다.(Breaking Point Test)

부하를 주는 방법으로는 요청하는 유저의 수를 올리는 방법으로 선택하였습니다.

  1. 로그인 시나리오
    1. 유저는 로그인을 시도한다.
  2. 단순 요청 시나리오
    1. 유저는 로그인을 시도한다.
    2. 유저가 비즈니스 로직을 호출한다.(여기서는 단순 문자열만을 반환하는 로직을 비즈니스 로직이라 가정하였습니다.)
  1.  

2-3. 테스트 코드 (gatling)

setUp(
	scenario1.injectOpen(
		incrementUsersPerSec(n) // 각 환경에 맞게끔 수정
        	.times(t) // 테스트가 끝날 때까지 사용자 수를 지속적으로 증가시킴
            .eachLevelLasting(1) // 각 사용자 수를 1초 동안 유지
            .startingFrom(1) // 사용자 수를 1명부터 시작
	)
                
    ,scenario2.injectOpen(
    	incrementUsersPerSec(n) // 각 환경에 맞게끔 수정
        	.times(t) // 테스트가 끝날 때까지 사용자 수를 지속적으로 증가시킴
            .eachLevelLasting(1) // 각 사용자 수를 1초 동안 유지
            .startingFrom(1) // 사용자 수를 1명부터 시작
	)
).protocols(httpProtocol);

 

3. 결론

3-1. 테스트 수행 결과

각 구현 방법별 5번 시행한 결과 값의 평균을 모았습니다.

3-2. 정리

  • 이번 프로젝트를 통해 세션을 통한 로그인 구현방식과 Jwt를 이용한 구현 방식사이 응답속도, 감당할 수 있는 최대 요청수, Throuput큰 차이가 존재하는 것을 확인할 수 있었습니다.
  • 세션을 통한 로그인 구현 방식이 DB커넥션과 Was 스레드의 점유율에 있어선 우위에 있으나 이를 무시해도 좋을 만큼 Jwt를 이용한 로그인 구현방식이 최대 요청수평균 응답속도압도하는 것을 확인할 수 있었습니다.
  • Spring Security를 이용하더라도 Jwt를 이용해 로그인을 구현한다면 Spring Boot의 인터셉터비슷한 성능을 낼 수 있음을 확인할 수 있었습니다.
  • Spring Security의 세션방식보다 Jwt방식을 이용해 로그인을 구현 하는 상황에서 약 120배 , 로그인과 단순 요청을 하는 상황에선 약 60배 가량 더 높은 Throughput을 확인할 수 있었습니다

이러한 실험 값을 토대로 인증/인가 방식에 대한 저만의 선택 기준을 정립할 수 있게 되었습니다.

1. 압도적으로 많은  동시 사용자를 수용해야 한다. -> 인터셉터

2. 사용자를 기억해야하고 이에 대한 비즈니스 로직이 필요하다 -> 세션

3. 그 외의 경우 -> Jwt

로그인 구현 방법 선택 기준

4. 후기

4-1. 좋았던 점

최선의 결과를 위한 노력

처음은 로그인 방식을 단순 비교해 우위를 가리고 싶은 마음이 전부였지만 프로젝트를 진행할수록 스프링 시큐리티, jwt에 대해 깊게 공부하고 최선의 결과를 도출하기 위해 고민하는 저를 발견할 수 있었습니다. 프로젝트가 끝난 이후에도 테스트 결과를 해석하는 과정에서 사고의 폭이 넓어지는 경험을 할 수 있었습니다.

 

새로운 것을 배우는 배우는 방법

이번 프로젝트에서는 프로젝트의 목적과 가까운 인터셉터, 시큐리티, 세션, Jwt과 같은 기술과는 달리 목적과 거리가 먼 Docker, Grafana, Prometheus, nGrinder, gatling와 같이 익숙지 않은 기술들이 훨씬 많았습니다. 그렇기에 이것들을 익히기 위해 다양한 레퍼런스를 찾아보고 레퍼런스가 없을 경우 공식 doc까지 읽어가며 기술을 이해하려고 노력하였습니다. 프로젝트를 준비할 때는 여간 힘들었는 게 아니었지만 프로젝트가 끝난 지금 다시 돌아보면 다양한 레퍼런스들을 찾아보고 커뮤니티에 검색을 하고 공식 문서를 찾아보는 경험을 통해 새로운 것을 배우는 방법을 익힐 수 있었던 것 같습니다.

 

 

4-2. 아쉬웠던 점

공식과 현실의 차이

인터셉터를 이용해 로그인 시나리오를 테스트할 때 적정 스레드를 구하는 공식을 통해 적정 스레드의 수를 구하였습니다. 그 결과 14개의 was 스레드 수가 나왔으며 최대 스레드의 수는 적정 스레드의 두 배인 약 30개가 나왔습니다. 그러나 스레드의 수를 조정하지 않아도 200개까지 스레드를 늘리며 요청을 무리 없이 다루는 tomcat서버를 보며 스레드를 구하는 공식과 실제 테스트 결과 사이에 존재하는 차이점이 어디에서 오는지 이해하기 어려웠습니다. 또한 이에 대해 마땅히 자문을 구할 곳이 없어 아쉬웠습니다.

 

불완전한 테스트 환경

gatling의 두 번째 시나리오(단순 요청 시나리오)에서 Jwt를 사용할 때 요청이 많아지면 두 번째 요청에서 헤더에 토큰이 설정되지 않아 에러가 발생했습니다. 이에 대한 원인으로 첫 번째 요청(로그인 시도)에 대한 응답이 오지 않았지만 두 번째 요청을 보내 생기는 문제라 생각하여 요청 간 일정 간격을 두었으나 여전히 같은 에러가 발생하였습니다. 이후 구글링을 해봤지만 이렇다 할 원인은 찾지 못하였습니다. 테스트 환경을 완벽하게 통제하지 못해 아쉬움이 남습니다.

→ 이후 gatling이 동작하는 과정에 대해 자세히 찾아보고 에러를 자세히 보았습니다. 그 결과 제가 내린 결론은 다음과 같았습니다.

두번째 시나리오는 첫번째 요청이후 응답값에 담긴 토큰을 두번째 요청의 헤더에 담아 진행했습니다. 그러나 다수의 요청이 서버에 몰리며 첫번째 요청을 적절하게 처리하지 못하였습니다. 이 때문에 첫번째 요청에 대한 응답값을 올바르게 받지 못하였으며 두번째 요청 역시 오류가 났던 것입니다.

첫번째 요청중 발생한 에러의 수
두번째 요청중 발생한 에러의 수



4-3. 생각할 거리

로그아웃을 구현하지 않았다.

이번 세션과 JWT를 비교하는 프로젝트는 티켓팅이나 수강신청과 같은 "많은 수의 유저가 동시에 접근하는 상황에서 어떤 인증/인가 방식이 제일 좋을까?"라는 생각에서 시작했습니다. 이러한 상황에서 로그아웃을 하는 것은 이러한 상황에 적절하지 않다 생각이 들어 구현하지 않았습니다.

 

JWT를 구현할 때 리프레시 토큰을 구현하지 않았다.

"로그아웃을 구현하지 않았다."라는 생각과 비슷한 맥락에서 구현하지 않았습니다. JWT에서 리프레시 토큰은 액세스 토큰이 만료되었을 때 사용됩니다. 액세스 토큰 만료 후 재발급은 로그인하고 오랜 시간이 지난 후 다시 서버에 접근을 해야만 합니다. 이는 프로젝트의 초기 목표인 "많은 수의 유저가 동시에 접근하는 상황에서의 인증/인가 비교"와 방향성이 다르다고 생각이 들어 구현하지 않았습니다.

5. 레퍼런스

https://github.com/sami355-24/login_performance_test

 

GitHub - sami355-24/login_performance_test

Contribute to sami355-24/login_performance_test development by creating an account on GitHub.

github.com

 

https://notspoon.tistory.com/48

https://ksh-coding.tistory.com/128#google_vignette

https://premika-17.medium.com/implementing-redis-in-spring-boot-3d2756e5ab69

https://junuuu.tistory.com/799

https://github.com/brettwooldridge/HikariCP/wiki/About-Pool-Sizing

https://github.com/sami355-24/login_performance_test

https://github.com/sami355-24/login_performance_gatling

https://github.com/sami355-24/login_performance_nGrinder

 

https://goto-pangyo.tistory.com/271
https://goto-pangyo.tistory.com/272

https://goto-pangyo.tistory.com/273

https://goto-pangyo.tistory.com/275

https://goto-pangyo.tistory.com/277

https://goto-pangyo.tistory.com/278

https://goto-pangyo.tistory.com/281

https://goto-pangyo.tistory.com/282

https://goto-pangyo.tistory.com/286