기록

테스트 환경에서의 In-Memory Map을 활용한 로그인/로그아웃 시스템 구현 본문

Web/Spring

테스트 환경에서의 In-Memory Map을 활용한 로그인/로그아웃 시스템 구현

youngyin 2024. 12. 30. 12:00

시작하면서

최근에 고민했던 것 중 하나는 "로그인 시 만들어둔 토큰을 어떻게 관리해야 할까?"라는 문제였습니다. 로그인 토큰은 인증과 권한 관리를 위해 반드시 필요한데, 이를 어디에 저장하고 어떻게 관리해야 하는지 명확히 정해야 했습니다.

 

처음에는 외부 저장소를 활용하는 방안을 떠올렸습니다. 예를 들어, Redis 기반의 Elastic Cache를 사용하면 분산 환경에서도 안정적으로 토큰을 관리할 수 있습니다. 하지만 여기서 예상치 못한 난관에 부딪혔습니다. Elastic Cache는 같은 VPC 내에서만 접근 가능하다는 제한이 있었습니다. 로컬에서 이를 테스트하려면 Bastion Host를 통해 접속하거나, VPN 같은 추가적인 네트워크 설정이 필요했습니다. 이 과정이 생각보다 복잡하고 번거로웠습니다.

 

대안을 고민하다가, 테스트와 개발 환경에서는 굳이 외부 서비스를 사용하지 않아도 되지 않을까?라는 생각이 들었습니다. 결국, 운영 환경에서는 Elastic Cache를 사용하더라도, 로컬에서는 인메모리 Map으로 간단히 토큰을 관리하는 방식을 선택했습니다.

이번 글에서는 외부 서비스를 사용하지 않고 프로젝트 내부에서 토큰을 저장하고 관리하는 가장 간단한 방법을 소개하려고 합니다.


1. 소개

로그인/로그아웃에서 토큰 관리의 중요성

  • 로그인: 사용자가 애플리케이션에 접근할 수 있도록 인증 토큰을 발급합니다.
  • 로그아웃: 인증 토큰을 삭제하여 사용자의 인증 상태를 종료합니다.
  • 이 과정에서 토큰을 안전하고 효율적으로 관리하는 것은 매우 중요합니다.

왜 인메모리 Map을 사용하는가?

  • 외부 캐시 시스템(예: Redis)을 사용하는 대신, 로컬 개발 환경에서는 인메모리 Map으로 간단하게 구현할 수 있습니다.
  • 빠른 성능과 간단한 설정으로 테스트와 개발 초기 단계에 적합합니다.

2. 설계와 흐름

토큰 관리의 기본 기능

  1. 토큰 저장: 로그인 시 생성된 토큰을 사용자 ID와 함께 저장합니다.
  2. 토큰 조회: 특정 사용자 ID의 토큰을 반환합니다.
  3. 토큰 삭제: 로그아웃 시 특정 사용자 ID의 토큰을 제거합니다.
  4. 전체 삭제: 시스템 초기화나 테스트 환경에서 모든 토큰을 삭제합니다.

설계 흐름

  1. 사용자가 이메일과 비밀번호로 로그인 요청을 보냅니다.
  2. 서버는 사용자의 정보를 확인하고 JWT 토큰을 생성합니다.
  3. 생성된 토큰은 사용자 ID와 함께 인메모리 Map에 저장됩니다.
  4. 로그아웃 요청이 오면, Map에서 해당 사용자의 토큰이 삭제됩니다.


3. 코드와 함께 보는 구현

토큰 관리 인터페이스 (RedisTokenService)

토큰 관리의 일관성을 유지하기 위해 인터페이스를 설계했습니다.

interface TokenService {
    fun saveToken(userId: String, token: String)
    fun getToken(userId: String): String?
    fun deleteToken(userId: String)
    fun deleteAllTokens()
}

이 인터페이스는 구현 방식(예: Redis 또는 인메모리 Map)에 관계없이 동일한 기능을 제공합니다.


인메모리 Map을 사용한 구현 (InMemoryTokenService)

ConcurrentHashMap을 사용하여 인메모리 저장소를 구성합니다.

private val tokenStore: MutableMap<String, String> = ConcurrentHashMap()

override fun saveToken(userId: String, token: String) {
    tokenStore[userId] = token
}

override fun getToken(userId: String): String? {
    return tokenStore[userId]
}

override fun deleteToken(userId: String) {
    tokenStore.remove(userId)
}

override fun deleteAllTokens() {
    tokenStore.clear()
}

위 코드는 사용자 ID를 키(key)로 사용하고, JWT 토큰을 값(value)으로 저장합니다.


로그인과 로그아웃 서비스 (AuthService)

AuthService는 로그인과 로그아웃 요청을 처리하며, 생성된 토큰을 TokenService를 통해 관리합니다.

fun login(req: LoginReq): AuthResp {
    val user = userService.findByEmail(req.email) ?: userService.registerMember(req)
    val token = jwtTokenService.generateToken(AuthReq(user, req.userType))
    tokenService.saveToken(user.userId!!, token)
    return AuthResp(authorization = token)
}

fun logout(authReq: AuthReq) {
    tokenService.deleteToken(authReq.userId!!)
}
  • login: 사용자를 인증한 후, JWT 토큰을 생성해 저장합니다.
  • logout: 저장된 사용자 토큰을 삭제합니다.

4. 결과 확인

테스트 코드 예제

테스트 코드를 통해 구현한 기능의 동작을 검증합니다.

@Test
fun `save and get token`() {
    redisTokenService.saveToken("user1", "token1")
    assertEquals("token1", redisTokenService.getToken("user1"))
}

@Test
fun `delete token`() {
    redisTokenService.saveToken("user1", "token1")
    redisTokenService.deleteToken("user1")
    assertNull(redisTokenService.getToken("user1"))
}

5. 결론

인메모리 Map을 활용한 토큰 관리는 간단하고 효과적인 방법으로, 특히 로컬 개발 및 테스트 환경에서 뛰어난 유용성을 제공합니다. 별도의 설정 없이 바로 사용할 수 있고, 빠른 속도로 인증 토큰을 관리할 수 있다는 점에서 초기 개발 단계에서 매력적인 선택입니다.

그러나 이 방식은 다음과 같은 한계점을 가집니다:

  1. 데이터 지속성 부족: 서버가 재시작되면 모든 토큰 데이터가 사라지므로, 지속적인 로그인 상태 유지가 어렵습니다.
  2. 스케일링 한계: 단일 서버에서만 동작하기 때문에, 분산 서버 환경에서는 제대로 작동하지 않습니다.
  3. 메모리 사용 제한: 사용자 수가 많아질수록 메모리 사용량이 급증하여 성능에 영향을 미칠 수 있습니다.

이러한 단점은 운영 환경에서 심각한 문제가 될 수 있으므로, 이후 단계에서는 Elastic Cache(예: Redis)와 같은 외부 캐시 시스템을 도입하는 방향으로 확장해야 합니다.

다음 포스팅에서는 이 프로젝트를 기반으로 Elastic Cache를 연동하여 분산 환경에서도 확장 가능한 토큰 관리 시스템을 구축하는 방법을 다룰 예정입니다. 이를 통해 더 안정적이고 유연한 인증 시스템을 설계할 수 있을 것입니다.

 
Comments