토큰, 꼭 어떤 데이터를 담아야 할까?

이 글의 시작은 회원가입 관련 이메일 인증 과정을 진행하던 도중

A: "토큰 자체가 어떤 의미를 담고 있으니 ... "

B: "그냥 랜덤 생성한 값도 토큰 아니냐"

에 대한 논쟁에서 시작되었다.

 

오늘의 결론을 미리 스포일러하자면, 랜덤 생성해서 의미를 담지 않은 값도 토큰이다.

그러나 이 결론을 찾기 위해 분석한 과정이 꽤 도움이 되었기 때문에 분석 내용을 공유하고자 한다.

토큰이란?

토큰(Token). 많이 쓰지만 막상 그 단어가 무슨 뜻이냐? 라고 하면 혼란스러울 사람들이 있을 것이다. 오늘은 이에 대해 한 번 살펴보고자 한다.

 

위키피디아(https://en.wikipedia.org/wiki/Token) 에서는 다음과 같이 설명하고 있다.

Token, an object (in software or in hardware) which represents the right to perform some operation

어떤 작업을 수행할 수 있는 권한을 나타내는 객체

 

자주 들어본 용어인 Session, Security, Access Token이 있고 

그 외에도 Tokenization, Invitaion token, Token Ring, in Petri net 등의 표현으로 사용되는 듯 하다.

경제, 언어, 암호화폐 등에서도 사용되고 있다.

즉, 토큰이라는 단어의 쓰임새는 굉장히 넓기 때문에, 우리가 어떤 의미로 쓰는가가 중요하다고 볼 수 있겠다.

 

그렇다면 웹 인증 상황에서 토큰은 어떤 의미로 쓰일까?

웹 통신의 특징

웹 통신에서 가장 많이 사용하는 HTTP는 stateless한 특성이 있어 서버가 클라이언트의 이전 상태를 보존하지 않는다.

이 특성으로 인해 우리는 이전 상태를 보존하기 위해 많은 방법을 고민해왔고, 그 중 하나가 토큰 기반 인증 방식이다.

토큰 기반 인증 방식은 다른 방법 중 하나인 Session 기반 인증 방식의 단점을 보완하기 위해 나타났다.

 

Session 기반 인증 방식을 간단히 설명하자면, 각 유저의 세션 정보를 전부 서버의 메모리에 저장해서 관리하는 방식이다.

서버에서 계속 세션 정보를 유지해야 해서, DB 과부하 등의 문제가 있다.

 

토큰 기반 인증

토큰 기반 인증(Token-based authentication)은 두 개의 종류로 나눌 수 있다.

1. 일반 토큰 기반 인증

의미 없는 문자열로 구성되어 있다. 주로 DB에 저장해 일치하는지 아닌지를 통해 검사한다.

-> 의미가 없기 때문에 ID 정보, 만료 기간 등 인증에 필요한 정보를 담을 수 없다.

-> DB에 저장하기 때문에 검사하거나 처리할 때 DB I/O가 생겨 부담이 생긴다.

 

단, 무조건 나쁜 것은 아니고 단순한 프로젝트나 빠르게 개발할 경우에는 해당 방법이 효율적일 때도 있다.

2. 클레임(Claim) 토큰 기반 인증

토큰 자체에 의미를 담은 토큰(클레임이란 정보의 조각을 뜻한다.)이다. 이를 암호화/해석하는 알고리즘에는 여러 종류가 있으며, 가장 대표적으로 JWT가 있고, 이를 활용한 예제로 OAuth가 있다. 

 

활용 예시

그렇다면 각 인증을 어떤 경우에 사용하는 것이 효율적일까?

GPT에 의하면 다음 이메일 인증 / 비밀번호 재설정의 예시에서는 랜덤 토큰을 쓰는 것이 효율적이라고 하는데,
여러 사람들의 의견도 궁금하다.

✅ 어떤 방식이 좋을까?

기준 랜덤 토큰 (Stateful) JWT (Stateless)
🔐 보안성 ✅ 높음 (유효성 DB로 체크) ⚠️ 중복 클릭 막기 어려움
🚀 속도 ✅ 빠름 ✅ 빠름
🔁 재사용 방지 ✅ 토큰 삭제 가능 ❌ 서버가 통제 불가능
🧠 관리 ✅ 단순 복잡해질 수 있음

👉 이메일 인증 / 비밀번호 재설정은 거의 항상 랜덤 토큰 + DB 저장 방식 사용!

 

(보안성에 왜 중복 클릭 막기 어려움이 들어가 있는지는 의문이지만, GPT는 늘 이런식이니 넘어가자.)

기타

추가로 이런 저런 질문이 떠올라서, 간단하게 정리해봤다.

이메일 인증 / 비밀번호 재설정 상황에서 물어본 내용이다.

 

질문 답변
랜덤 토큰이면 충분한가요? ✅ 네, 32 bytes 정도면 안전하고 널리 사용됨
사용자 정보가 토큰에 포함돼야 하나요? ❌ 보통 필요 없음 (DB에서 토큰으로 사용자 찾으면 됨)
JWT를 써도 되나요? 🤔 가능은 하지만, 대부분의 인증 링크에는 부적합 (무효화 어려움)

 

추가로, 랜덤 토큰을 User table에 저장하는 것이 맞는지에 대한 질문도 해봤다.

토큰 관리 전략에는 사이즈 검토도 중요해 보인다.

상황 User 테이블에 넣기  별도 테이블로 분리
단순한 사이드 프로젝트 ✅ 괜찮음 ❌ 굳이 분리 X
보안이 중요한 상용 서비스 ⚠️ 비추천 ✅ 추천
토큰 종류가 많거나 로그 남겨야 하는 경우 ❌ 관리 어려움 ✅ 필수

 

결론

1. 의미를 담지 않은 값도 토큰이다.

2. 어떤 토큰 인증 방식을 사용할지는 프로젝트 규모나 토큰 활용 정도에 따라 구분하자.

기본 시스템 구조도

멤버 구조

사용자 로그인 방식은 OAuth로만.

사용자 별로 파티를 갖고 있으며, 파티에는 파티 호스트가 존재한다.

+ 사용자는 뱃지를 가지고 있음

파티 별로 챌린지가 존재하며, 챌린지 내에 사용자 별 세부 목표(타겟)이 존재한다.

파티 내에는 포스팅이 존재하며, 포스팅은 타겟과 챌린지를 갖는다.

코멘트를 통해 포스팅을 인증할 수 있다.

 

 

ERD

badge

user.badge

user

user.party

party

// 사용자 목록
Table user as U {
  id int [pk] //type 결정 필요
  user_id varchar //사용자 아이디
  name varchar //사용자 이름
  status varchar //사용자 상태
  created_at timestamp
  modified_at timestamp
}

// 사용자 - 뱃지 정보
Table user.badge as UB {
  id int [pk] //type 결정 필요
  user_id int //사용자 id
  badge_id int //뱃지 id
  created_at timestamp
  modified_at timestamp
}

// 뱃지 목록
Table badge as B { 
  id int [pk] //type 결정 필요
  name varchar //뱃지 이름
  description varchar //설명
  condition varchar //획득 조건 (form 지정 필요)
  created_at timestamp
  modified_at timestamp
}

// 파티 - 사용자 정보
Table user.party as UP {
  id int [pk] //type 결정 필요
  user_id int //사용자 id
  party_id int //파티 id
  party_alias varchar //파티 내에서 사용할 이름
  is_admin boolean //관리자 여부
  created_at timestamp
  modified_at timestamp
}

// 파티 목록
Table party as P {
  id int [pk] //type 결정 필요
  name varchar //파티 이름
  
  //입장관련
  party_code varchar //파티 코드
  question varchar //질문
  answer varchar //답변
  
  rules varchar //인증 룰 정보
  
  creater varchar //파티 생성자 
  created_at timestamp
  modified_at timestamp
 }

 Ref: UB.user_id > U.id
 Ref: UB.badge_id > B.id
 Ref: UP.user_id > U.id
 Ref: UP.party_id > P.id
 
// ----------------------------------------------
 
// 파티 관련 정보: 파티 단위 챌린지
Table party.challenge as PC {
  id int [pk]
  party_id int //파티 id
  name varchar //챌린지 이름
  description varchar //챌린지 설명

  start_at timestamp //챌린지 시작일
  end_at timestamp //챌린지 종료일
  
  created_at timestamp
  modified_at timestamp
}

 Ref: PC.party_id > P.id
 
// ----------------------------------------------
 
//유저 관련 정보: 개인 목표  
Table user.target as UT {
  id int [pk]
  name varchar //목표 이름
  description varchar //목표 설명
  
  party_id int //파티 id
  owner_id int //목표 소유자 id
  challenge_id int //생성 당시 파티 챌린지 id
  
  created_at timestamp
  modified_at timestamp
 }
  
// 유저 관련 정보: 인증
Table user.certification as UCE {
  id int [pk]
  is_ok boolean //인증 성공 여부
  post_id int //포스트 id
  granter_id int //인증 부여자 id
  
  party_id int //파티 id
  target_id int //개인 목표 id
  owner_id int //개인 목표의 사용자 id
  
  created_at timestamp
  modified_at timestamp
}

// 유저 관련 정보: 유저 포스팅
Table user.post as UPO {
  id int [pk]
  image varchar //이미지는 어떻게 저장하지 ???
  description varchar //포스팅 내용
  
  party_id int //파티 id
  owner_id int //글쓴 사용자 id
  target_id int //포스트가 속한 개인 목표 id
  
  created_at timestamp
  modified_at timestamp
 }
 
 // 유저 관련 정보: 코멘트
Table user.comment as UCO {
  id int [pk]
  title varchar //코멘트 제목 - 필요한지?
  description varchar //코멘트 내용
  
  party_id int //파티 id
  owner_id int //코멘트쓴 사용자 id
  post_id int //포스트 id
  certification_id int //코멘트에서 판단한 인증 id
  
  created_at timestamp
  modified_at timestamp
 }
 
 
 Ref: UT.party_id > P.id
 Ref: UT.owner_id > UP.id
 Ref: UT.challenge_id > PC.id

 Ref: UCE.party_id > P.id
 Ref: UCE.target_id > UT.id
 Ref: UCE.owner_id > UP.id
 
 Ref: UCE.post_id > UPO.id
 Ref: UCE.granter_id > UP.id
 
 Ref: UPO.party_id > P.id
 Ref: UPO.owner_id > UP.id
 Ref: UPO.target_id > UT.id
 
 Ref: UCO.party_id > P.id
 Ref: UCO.owner_id > UP.id
 Ref: UCO.post_id > UPO.id
 Ref: UCO.certification_id > UCE.id

 
//----------------------------------------------//

기본적으로 user Table을 기준으로 설계하였다.

유저 개인이 파티, 챌린지, 타겟 상관 없이 히스토리들을 볼 수 있었으면 좋겠다고 생각해서 되도록 하위 정보(post, comment, certification에 유저 정보들을 넣도록 설계하였다.

+ Recent posts