티스토리 뷰

대부분의 사이트는 회원을 위한 서비스를 제공하기 위해 회원가입/로그인 기능을 제공합니다. 롤문철 닷컴은 마이페이지, 투표하기, 댓글달기, 재판 상세내용 조회 등 여러가지 서비스가 로그인을 필요로합니다. 그래서 '로그인 기능'을 위한 2가지 방안을 검토했습니다.

 

◼︎ JWT 토큰

◼︎ 세션

 

어떤 방안을 선택할지 고민한 끝에 서비스의 보안성이 가장 중요하다고 생각하여 '세션'을 선택하게 되었습니다. '로그인 기능'의 요구사항은 다음과 같습니다.

 

로그인

로그아웃

악의적인 사용자 강제 로그아웃

동시 접속자 1,000명 처리

평균 응답시간 1,000ms 이내

 

위 요구사항을 JWT 토큰, 세션 두 가지 관점에서 설계/구현/성능검증(테스트)하고, 최종적으로 서비스의 '로그인 기능'에 세션을 활용하게 된 이유를 소개하려고 합니다.

 

 

[chapter 1] 기반 기술 조사

 

 

1. HTTP 프로토콜은 우리를 기억하지 않는다.

 

로그인을 하는 상황을 생각해봅시다. 로그인 화면에 아이디, 비밀번호를 입력하고, 로그인 버튼을 누르면 로그인이 됩니다. 그리고 로그인한 회원으로써 사이트를 마음껏 이용하죠. 로그인 여부를 누가 기억해주는 걸까요?

HTTP는 상태성이 없는 프로토콜이라서 서버가 클라이언트의 이전 요청을 기억하고 있지 않습니다. 우리가 로그인을 한 다음에 서버에게 어떤 작업을 요청했을 때, 서버는 우리가 로그인한 회원인지 알지 못합니다. 그래서 쿠키라는 것을 이용합니다. 쿠키는 HTTP 메세지에 상태를 부여해주는 수단입니다. 쿠키를 사용하면 HTTP 메세지에 우리가 누구인지 기록할 수 있습니다. 메세지를 전달받은 서버는 해당 메세지가 누구로부터 온 것인지 구분할 수 있게 됩니다. 쿠키를 이용한 로그인 시퀀스는 다음 그림을 보면 쉽게 이해할 수 있습니다.

 

그림1. 쿠키에 로그인한 회원(홍길동)임을 표시

 

쿠키는 서버-클라이언트 간 통신 과정에서, 클라이언트 측이 자신이 로그인한 사용자인지 입증하는 수단입니다. 예를 들어 롯데월드에 돈을 내고 입장하면 종이 팔찌를 채워주죠. 이게 쿠키발급입니다. 그리고 각 놀이기구마다 직원이 사람들의 손목을 보며 팔찌를 확인합니다. 이건 로그인 체크 과정이구요. 그런데 어떤 사람들은 팔찌를 위조해서 만들수도 있지 않을까요? 아마 롯데워드는 매일 팔찌의 디자인과 색상을 달리하거나 위조를 방지하기 위한 수단을 도입했겠죠. 팔찌를 어떻게 만드느냐가 롯데월드 맘대로인 것처럼, 쿠키에 무엇을 담아주느냐도 서버에 따라 다릅니다. 쿠키에 어떤 데이터를 담을 것인가에 따라 로그인 구현 방법은 세션 기반 인증 JWT 토큰 기반 인증으로 분류할 수 있습니다. 이외에도 더 많은 방식의 인증 방법이 있겠지만, 이 두가지 방식이 가장 보편적인 방법인 것 같습니다.

 

 

 

2. JWT 토큰을 이용한 로그인 기능

 

JWT 공식 홈페이지 Introduction과 변정훈님의 https://blog.outsider.ne.kr/1160의 글을 참고하였습니다.

JWT 토큰은 JSON 형태의 데이터를 가공하여 문자열 형태로 변형한 것입니다. 중요한 것은 JWT 토큰이라고 불리는 데이터 내에 서명이 포함된다는 것입니다. 서명은 비밀키를 이용해 생성되는데, 비밀키는 JWT 토큰을 발급한 곳에서만 알고있는 비밀 정보입니다. 롯데월드의 팔찌가 위조되는 것을 막기 위해 여러 수단이 존재하는 것처럼, 이 서명은 토큰이 위조되는 것을 방지할 수 있습니다. JWT 토큰을 만드는 방법은 공개되어 있기 때문에 누군든지 토큰을 위조할 수 있습니다. 서버는 서명을 통해 위조된 토큰인지, 정상 발급된 것인지 확인이 가능합니다. 바로 이 로직을 통해 로그인을 구현할 수 있습니다. JWT 공식 문서에 따르면, JWT 토큰은 HTTP Authentication 헤더에 포함되어 인증 수단의 역할을 할 수 있다고 소개되어 있습니다. JWT 토큰을 이용하면 [그림1] 3번 과정 다음에 토큰 유효성 검사를 하는 단계가 추가됩니다. JWT 토큰을 이용한 로그인 시퀀스는 다음 그림과 같습니다.

 

그림2. JWT 토큰의 서명을 이용한 로그인

 

3. 세션을 이용한 로그인 기능

 

롯데월드의 팔찌 예시를 다시 들어서 설명해볼게요. 이용권 판매처에서 고객에게 팔찌를 채워줄때마다, 팔찌에 랜덤한 일련번호를 기록합니다. 그리고 놀이기구 직원들이 볼 수 있는 시스템에 (팔찌 일련번호, 고객의 성함) 쌍을 기록합니다. 놀이기구 직원들은 고객이 팔찌를 들이밀었을 때, 해당 팔찌의 일련번호, 고객의 성함을 확인하고 일치한다면 정당한 이용권 구매자라고 판단합니다.

 

위 예시에서 (팔찌 일련번호, 고객의 성함)이 등장합니다. 세션은 바로 이 데이터 쌍을 일컫는데요. 대부분의 서버에는 세션을 저장할 수 있는 저장소가 마련되어 있습니다. 유명한 서버 중 하나인 톰캣에서는 팔찌 일련번호를 JSESSIONID 라고 부릅니다.

 

톰캣 공식문서에 따르면 JSESSIONID는 생성된 세션 쿠키의 이름이라고 소개되어 있습니다. 세션 쿠키가 무엇인지 알기 위해선 쿠키의 속성 중 만료시간(expires) 이라는 것을 알아야 합니다. 만료시간이 되면 쿠키는 효과를 잃고 사라지게되는데요.

만료시간에 따라 쿠키는 두 가지로 분류됩니다.

 

◼︎ 세션 쿠키: 만료 날짜를 생략하면 브라우저 종료시까지만 유지

◼︎ 영속 쿠키: 만료 날짜를 입력하면 해당 날짜까지 유지

 

정리하면, JSESSIONID는 세션 쿠키에 붙는 일련번호이며, 해당 쿠키는 만료날짜가 생략되어 있어 브라우저가 종료되면 사라지게 됩니다. 세션을 이용한 로그인 시퀀스는 다음과 같습니다.

 

그림3. 세션을 이용한 로그인

 

 

[chapter 2] 구현 및 성능 비교 테스트

테스트 환경
Jmeter
  • 맥북 (M1, 메모리 16GB)
  • 스레드 그룹 1000개
  • Ramp Time 1sec
  • Thinking Time 0sec

Application Server
  • 맥북 (M1, 메모리 16GB)
  • Java 17
  • Spring boot 3.1
Session Storage
  • 네이버클라우드 (2CPU, 메모리 4GB)
  • Redis 6.2.5
테스트1
  1. GET /jwt/login 
  2. GET /jwt/home
    1-2 요청 반복
테스트2
  1. GET /session/login 
  2. GET /session/home
    1-2 요청 반복

 

컨트롤러 소스코드

@RequiredArgsConstructor
@RestController
public class JwtLoginTestController {

	private final JwtTokenFactory jwtTokenFactory;
	private final JwtTokenDecoder jwtTokenDecoder;

	/**
	 * 토큰을 이용한 로그인
     	 * 테스트 방법: GET /jwt/login → GET /jwt/home 반복
	 */
	@GetMapping("/jwt/login")
	public String jwtLogin(HttpServletResponse response, @RequestParam String memberId) {
		String token = jwtTokenFactory.generate(memberId, System.currentTimeMillis());
		Cookie cookie = new Cookie("jwtToken", token);
		response.addCookie(cookie);
		return "로그인 성공";
	}

	@GetMapping("/jwt/home")
	public String jwtHome(@CookieValue(value = "jwtToken", required = true) String jwtToken) {
		if (jwtTokenDecoder.verify(jwtToken)) {
			return "환영합니다 회원님";
		}
		return "로그인해주세요";
	}
}


@RequiredArgsConstructor
@RestController
public class SessionTestController {
	/**
     	* 세션을 이용한 로그인
     	* 테스트 방법: GET /session/login → GET /session/home 요청을 반복
	*/
	@GetMapping("/session/login")
	public String sessionLogin(HttpSession httpSession, @RequestParam String memberId) {
		httpSession.setAttribute("loginId", memberId);
		return "로그인 성공";
	}

	@GetMapping("/session/home")
	public String sessionHome(HttpSession httpSession) {
		if (httpSession.getAttribute("loginId") != null) {
			return "환영합니다 회원님";
		}
		return "로그인해주세요";
	}
}

 

전체 소스 코드

https://github.com/korjun1993/test-login

 

테스트 결과 요약

  평균 응답 시간 평균 CPU 사용률
테스트1 (JWT 기반 로그인) ◼︎ 로그인 요청: 74ms
◼︎ 로그인 확인 및 페이지 전달: 73ms
◼︎ 평균: 74ms
◼︎ 50~60%
테스트2 (세션 기반 로그인) ◼︎ 로그인 요청: 671ms
◼︎ 로그인 확인 및 페이지 전달: 209ms
◼︎ 평균: 440ms
◼︎ 15~20%

 

테스트1 결과에 대한 해석

테스트1 결과 첨부 자료. Jmeter 평균 응답 시간
  • 테스트 1의 평균 응답 시간은 74ms로 테스트2의 평균 응답시간 440ms에 비해 5배 이상 빠른 모습을 보입니다.
  • 테스트1은 네트워크 및 디스크 I/O 작업을 필요로 하지 않기 때문입니다.

 

테스트1 결과 첨부 자료. Visual GC CPU 사용률
  • 테스트1은 JWT 토큰을 생성하고, 서명을 확인하는 작업을 필요로합니다.
  • 이 작업들은 공개키 알고리즘 기반으로 동잡합니다. 따라서 CPU 사용률이 50~60%로 테스트2에 비해 다소 높은 모습을 보입니다.

 

테스트2 성능 결과에 대한 해석

테스트2 결과 첨부 자료. Jmeter 응답 시간
  • 테스트2는 외부 세션 저장소를 이용합니다. 따라서 네트워크 및 디스크 I/O가 발생하여 응답 시간이 느린 것을 확인할 수 있습니다.
  • 첨부자료는 없지만, 약 15분 동안 시간이 지날수록 응답 시간이 커지는 모습을 보였습니다. 그리고 15분 후에는 응답 시간이 수렴하는 형태를 보였습니다.
  • 추측하건데, 15분동안은 데이터가 쌓여서 조회/쓰기 성능이 저하되서 그런 것 같습니다. (15분이 지난 세션 데이터는 만료됨)
  • 즉, 세션의 유효시간도 로그인 성능에 영향을 줄 수 있는 것으로 보입니다.

 

테스트2 결과 첨부자료. Redis 서버 메모리 사용률
  • 테스트2는 네이버클라우드의 세션 저장소를 이용합니다.
  • 세션 저장소의 메모리 모니터링 결과, 세션 유효시간(=15분) 동안, 시간이 지날수록 메모리 사용률이 올라갑니다.

 

테스트2 결과 첨부 자료. VIsual GC CPU 사용률
  • 테스트2는 테스트1과 같이 CPU Bound 작업을 하지 않습니다.
  • 따라서 테스트1(JWT 기반 로그인 방식)에 비해서 CPU 사용률이 확연히 낮은 모습입니다.

 

 

[chapter 3] 결론

테스트 결과, JWT 기반의 로그인이 세션 기반의 로그인보다 5배 가량 빨랐습니다. 하지만, 두 방식 모두 동시접속자가 1,000명인 상황에서 평균 응답속도 1,000ms를 충족하기 때문에 응답 속도 외에 다른 관점에서 생각을 해보았는데요. 보안성이 유리한 방식은 무엇일지, 개발 속도가 빠른 방식은 무엇일지 고민한 끝에 세션을 선택하였습니다. 만약 쿠키가 제 3자에 의해 탈취된다면 어떻게 해야될까요? 탈취된 쿠키 속의 인증 수단이 더 이상 사용되지 못하도록 조치를 취해야할 것입니다.

 

토큰 방식의 경우, 일단 토큰이 발급되면 해당 토큰을 관리하는 것은 클라이언트의 몫입니다. 서버에서 토큰을 만료시킬 방법이 없습니다. 서버에서 어떤 고객의 토큰이 탈취되었다는 사실을 알더라도, 탈취된 토큰이 사용되는 것을 막을 수 없다는 치명적인 문제가 있습니다. 토큰 인증을 구현할 때, 이런 문제를 극복하기 위한 몇 가지 방법이 존재합니다. 토큰의 만료 시간을 짧게 설정하고 Refresh 토큰을 발급하는 것입니다. 구체적인 내용은 inpa님의 블로그 포스트 Acccess Token & Refresh Token 원리 를 참조하면 좋을 것 같습니다.

 

제가 개발 중인 서비스는 실제 운영을 목표로 하고 있습니다. 저의 개발 커리어중 첫 운영인만큼 최대한 안전한 사이트를 구축하고 싶기 때문에 보안을 신경쓰고 있습니다. 그런데 토큰 인증 방식은 보안을 위해 신경 쓸게 꽤 많다는 생각이 들었습니다. 근본적인 이유는 인증 수단의 통제 책임이 클라이언트에게 있기 때문입니다. 토큰 인증 방식의 안전성을 높이려면 서버에서 토큰을 통제할 수 있어야 합니다. 토큰 방식으로 로그인 기능을 구현했을지라도, 보안적인 측면을 강화하다보면 세션 인증과 유사해진다는 느낌을 받았습니다. 예를 들어, Refresh 토큰을 구현할 경우, 서버 데이터베이스에서 Refresh 토큰을 관리하게됩니다. 사용자는 Access 토큰을 발급받기 위해 Refresh 토큰을 서버에게 전달하고, 서버는 데이터베이스에서 Refresh 토큰을 조회하게 됩니다. I/O Access가 필요하지 않다는 토큰 기반 인증의 장점이 사라지고 세션과 비슷해지는 것이죠.

 

이처럼 보안 장치라든지 로그아웃 기능을 구현하다보면 점차 세션과 닮아갑니다. 토큰 인증은 취약점을 대비하기 위해 별도로 여러가지 보안 장치를 구현해야 한다는 점, 그리고 보안을 강화할수록 토큰의 장점이 사라진다는 단점이 있습니다. 따라서 세션 인증이 토큰 방식에 비해 안전하다는 생각에 세션 기반으로 로그인을 구현하자고 결정하였습니다.

 

 

참고 자료

 

댓글