1. "로그인했는데 왜 또 하래요?"
서비스 초창기, 저는 세션(Session) 기반 인증을 쓰고 있었습니다.
사용자가 로그인하면 서버 메모리에 session_id를 저장하고, 사용자 브라우저 쿠키에 그 ID를 심어줬죠.
아주 전통적이고, 안전하고, 구현도 쉬웠습니다.
사용자가 늘어나서 서버를 1대에서 2대로 늘렸습니다(Scale-out). L4 스위치(로드밸런서)가 트래픽을 반반씩 나눠주게 설정했죠.
그런데 난리가 났습니다. "방금 로그인했는데 페이지 옮기니까 로그아웃됐어요!"
원인은 간단했습니다.
- 철수가 서버 A에서 로그인했습니다. (서버 A 메모리에 세션 저장)
- 철수가 다음 페이지를 클릭했는데, 로드밸런서가 요청을 서버 B로 보냈습니다.
- 서버 B는 철수를 모릅니다. "누구세요? 로그인하세요."
2. 세션의 배신과 극복 (Sticky Session & Redis)
시도 1 - 끈적한 세션 (Sticky Session)
로드밸런서 설정을 바꿔서, "철수의 요청은 무조건 서버 A로만 보내라"고 했습니다. 문제는 서버 A가 터지면, 거기에 붙어있던 모든 사용자의 로그인이 다 풀려버립니다. 그리고 특정 서버에만 사람이 몰리면 부하 분산의 의미가 없죠.
시도 2 - 세션 저장소 분리 (Redis)
서버 메모리 대신, Redis라는 별도의 인메모리 DB를 둬서 세션을 공유했습니다. 서버 A도, 서버 B도 Redis를 바라보니까 문제가 해결되었습니다! 하지만... Redis가 터지면 전 국민 로그아웃이라는 단일 실패 지점(SPOF)이 생겼고, 모든 요청마다 Redis를 조회하니 네트워크 비용이 들었습니다.
3. 토큰의 등장 - "신분증을 직접 들고 다녀라" (JWT)
선배 개발자가 조언했습니다. "야, 서버가 기억하게 하지 말고, 클라이언트가 직접 증명하게 해."
이게 바로 토큰(Token) 기반 인증, 그중에서도 JWT(Json Web Token)입니다.
- 세션: 서버가 장부를 갖고 있음. (철수 = 1번)
- 토큰: 철수에게 도장이 찍힌 신분증을 발급해 줌. (이름: 철수, 발급자: 서버장, 유효기간: 1시간)
이제 철수가 서버 B로 가도, 서버 B는 신분증의 도장(서명)만 확인하면 됩니다. 서버끼리 데이터를 공유할 필요도, Redis를 조회할 필요도 없어졌습니다. Stateless(무상태)의 자유를 얻은 것이죠!
JWT의 해부 (Anatomy)
JWT는 aaaaa.bbbbb.ccccc 처럼 세 부분으로 나뉩니다.
- Header: 알고리즘 정보 (
{"alg": "HS256"}) - Payload: 실제 데이터 (
{"userId": 1, "role": "admin"}). 주의: 암호화된 게 아니라 Base64로 인코딩된 것뿐입니다. 누구나 풀어볼 수 있으니 비밀번호는 절대 넣지 마세요. - Signature: 위 두 가지를 비밀키로 지지고 볶은 서명. 위변조를 막습니다.
해커가 Payload를 조작해서 admin 권한을 얻으려 해도, 서명 값이 달라지므로 서버는 "가짜 신분증이네?" 하고 쳐냅니다.
4. 저장소 전쟁: LocalStorage vs Cookie
토큰을 어디에 저장할까요?
옵션 A: LocalStorage
- 장점: 쓰기 편함.
localStorage.getItem - 단점: XSS(스크립트 삽입 공격)에 취약함. 해커가 JS를 주입하면 토큰을 탈취당함.
옵션 B: HttpOnly Cookie
- 장점: JS로 접근 불가. XSS 방어.
- 단점: CSRF(위조 요청) 공격에 취약함. (SameSite 설정 등으로 방어 필요)
결론: 금융권 수준의 보안이 필요하면 HttpOnly Cookie를 쓰세요. 일반적인 앱은 XSS 방어책(Content Security Policy)을 잘 세우고 LocalStorage를 써도 무방하지만, 쿠키가 더 안전한 편입니다.
5. 고급 기술 - 슬라이딩 세션 (Sliding Session)
토큰의 단점은 유효기간입니다. 사용자가 열심히 글을 쓰고 있는데 "30분 지났으니 나가세요" 하면 화나겠죠?
슬라이딩 세션 전략:
- 사용자가 유효한 토큰으로 API를 호출함.
- 토큰 만료가 5분 남았다? 응답 헤더에 새 토큰을 실어서 보내줌.
- 클라이언트는 자연스럽게 새 토큰으로 갈아끼움.
이렇게 하면 "활동 중인 사용자"는 계속 로그인을 유지할 수 있습니다.
4. 토큰의 치명적 단점 - "잃어버리면 끝장이다"
JWT는 너무 편했지만, 무서운 점이 하나 있었습니다. "토큰을 탈취당하면, 해커를 막을 방법이 없다."
세션이라면 서버에서 delete session 해버리면 되지만, 이미 발급된 토큰은 유효기간이 끝날 때까지 회수할 수 없습니다.
그래서 유효기간을 1년으로 했다가는 큰일 납니다. 그렇다고 5분으로 하면 사용자가 5분마다 로그인을 다시 해야 하죠.
5. 최종 해결책 - Refresh Token 전략
그래서 나온 것이 Access Token + Refresh Token 조합입니다.
- Access Token (출입증): 유효기간 30분. 짧게 설정. 탈취당해도 금방 만료됨.
- Refresh Token (재발급권): 유효기간 2주. DB에 저장.
사용자는 평소에 Access Token을 씁니다. 만료되면? 서버에 Refresh Token을 보내서 "새 출입증 주세요"라고 합니다. 이때 서버는 DB를 확인합니다. "이 Refresh Token 아직 유효한가? 혹시 사용자가 로그아웃하거나 신고당해서 정지된 건 아닌가?"
이 방식은 JWT의 확장성(검증 시 DB 조회 X)과 세션의 제어권(갱신 시 DB 조회 O/차단 가능)을 모두 잡은 하이브리드 전략입니다.
6. 마무리 - 정답은 없다 (Trade-off)
- 동시 접속자 차단, 관리자 강제 로그아웃이 중요하다? -> 세션이 맞습니다.
- 마이크로서비스(MSA) 환경이거나 모바일 앱이다? -> 토큰(JWT)이 필수입니다.
- 둘 다 잡고 싶다? -> Refresh Token을 쓰세요. (구현은 복잡합니다).
"요즘은 다 JWT 쓴대요"라며 무지성으로 도입하지 마세요. 여러분의 서비스가 서버 1개짜리 어드민 페이지라면, 세션이 훨씬 안전하고 편합니다.
I Added a Server, and Everyone Got Logged Out (Session vs Token)
1. "Why Do I Have to Log In Again?"
In the early days, I used Session-based Authentication.
When a user logged in, I stored a session_id in the server memory and planted it in the user's browser cookie.
It was traditional, secure, and easy to implement.
As users grew, I scaled out from 1 server to 2. I configured the L4 switch (Load Balancer) to split traffic 50/50.
Then chaos ensued. "I just logged in, clicked a page, and got logged out!"
The cause was simple.
- Alice logged in on Server A. (Session saved in Server A's memory)
- Alice clicked next, and the Load Balancer sent her to Server B.
- Server B doesn't know Alice. "Who are you? Please log in."
2. Betrayal of Session & Overcoming It (Sticky Session & Redis)
Attempt 1: Sticky Session
I changed the Load Balancer config to "Always send Alice's requests to Server A". Problem: If Server A crashes, all users attached to it are logged out instantly. And if everyone flocks to Server A, load balancing is useless.
Attempt 2: Separate Storage (Redis)
Instead of server memory, I used Redis (In-Memory DB) to share sessions. Server A and Server B both look at Redis, so the problem was solved! But... If Redis dies, the whole nation logs out. A Single Point of Failure (SPOF). And every request hitting Redis incurs network latency.
3. Enter Token: "Carry Your Own ID" (JWT)
A senior developer advised: "Hey, don't make the server remember. Make the client prove themselves."
This is Token-based Authentication, specifically JWT (Json Web Token).
- Session: Server holds the ledger. (Alice = #1)
- Token: Issue a stamped ID card to Alice. (Name: Alice, Issuer: Server Admin, Exp: 1 hour)
Now if Alice goes to Server B, Server B just checks the stamp (signature) on her ID card. No need to share data between servers, no need to query Redis. We achieved the freedom of Statelessness!
The Anatomy of a JWT
A JWT looks like aaaaa.bbbbb.ccccc. It has three parts:
- Header: Metadata about the algorithm (e.g.,
{"alg": "HS256", "typ": "JWT"}). - Payload: The data you want to store (e.g.,
{"userId": 1, "role": "admin"}). Warning: This is just Base64 encoded, not encrypted. Anyone can read it. Never put passwords here. - Signature: Hashed combination of Header + Payload + Secret Key. This prevents tampering.
If a hacker changes the payload (e.g., "role": "user" -> "role": "admin"), the signature won't match, and the server will reject it.
4. Storage Wars: LocalStorage vs Cookie
Where should you store this token? This is the eternal debate.
Option A: LocalStorage
- Pros: Easy to use (
localStorage.setItem). - Cons: Vulnerable to XSS (Cross-Site Scripting). If a hacker injects JS into your site, they can read
localStorageand steal the token.
Option B: HttpOnly Cookie
- Pros: JS cannot read it. Immune to XSS.
- Cons: Vulnerable to CSRF (Cross-Site Request Forgery). You need SameSite settings and CSRF tokens to prevent it.
Verdict: For critical apps (Banking), use HttpOnly Cookies with strict SameSite policies. For general apps, LocalStorage is okay if you trust your Content Security Policy (CSP), but Cookies are generally safer against token theft.
5. Advanced: Sliding Sessions
One major UX problem with Tokens is heavy expiry. If I'm actively using the site, I shouldn't be logged out just because 30 minutes passed.
Sliding Session logic:
- User makes a request with a valid Access Token.
- If the token is close to expiry (e.g., 5 mins left), the server issues a new Access Token in the response header automatically.
- The client engages the new token seamlessly.
This keeps the user logged in as long as they are active, but kicks them out if they are idle.
6. The Fatal Flaw of Token: "Lost = Game Over"
JWT was convenient, but scary. "If a token is stolen, you can't stop the hacker."
With sessions, you can just delete session on the server. But an issued token cannot be revoked until it expires.
Setting expiration to 1 year is suicide. Setting it to 5 minutes annoys users endlessly.
7. The Final Solution: Refresh Token Strategy
So we use the Access Token + Refresh Token combo.
- Access Token (Entry Pass): Expires in 30 mins. Short. Even if stolen, it expires soon.
- Refresh Token (Renewal Ticket): Expires in 2 weeks. Stored in DB.
Users use Access Token normally. When it expires? Send Refresh Token to server: "Give me a new pass." Server checks DB: "Is this Refresh Token valid? Did the user log out or get banned?"
This is a hybrid strategy capturing both JWT's Scalability (No DB on verify) and Session's Control (DB check on renew/Revokable).
8. The Hybrid Approach: BFF Pattern (Backend For Frontend)
Security experts often say: "Don't store tokens in the browser at all." Then where? In a lightweight Node.js server that sits between React and the API.
- Browser talks to BFF using Cookies (HttpOnly).
- BFF holds the JWT in memory.
- BFF talks to API attaching the JWT.
The browser never sees the JWT. XSS is impossible because there is no token to steal. This is the Gold Standard for enterprise security.
9. Conclusion: No Silver Bullet (Trade-off)
- Need Concurrent Login Limits or Force Logout? -> Session is better.
- Microservices (MSA) or Mobile App? -> Token (JWT) is essential.
- Want both? -> Use Refresh Token. (Complexity spikes).
Don't blindly use JWT just because "everyone does it." If you're building a single-server admin panel, Sessions are much safer and easier.