- 마지막 업데이트
웹 서비스 세션 최적화
이해하기
세션 연동 최적화는 SUAT(Security User Access Token) 인증 토큰의 유효성 검증을 매 요청·액션마다 반복하지 않도록, 최근 검증 결과를 캐시에 저장해 재사용하는 기능이에요. 최초 진입에서 Accounts 인증 API(/auth/v5/user_token/check)로 SUAT를 검증한 뒤 결과를 캐시에 저장하고, 이후 같은 사용자의 요청에서는 캐시가 유효하면 인증 API 호출을 생략해요. 인증 API 호출을 최소화하면서도 최신 인증 상태를 유지할 수 있어요.
연동 방식은 렌더링 환경에 따라 두 가지로 나뉘어요. 서버에서 페이지를 렌더링하는 SSR 환경(백엔드)은 서버 측 Redis를 검증 캐시로 사용하고, 브라우저에서 화면을 렌더링하는 CSR·SPA 환경(프론트엔드)은 Session Storage를 검증 캐시로 사용해요.
적용 환경
캐시 저장소와 키만 다를 뿐 검증 API와 목적은 동일해요. 서비스의 렌더링 방식에 맞는 트랙을 선택하면 돼요.
| 구분 | SSR (백엔드) | CSR (프론트엔드) |
|---|---|---|
| 렌더링 위치 | 서버에서 페이지 렌더링 | 브라우저에서 화면 렌더링 |
| 대표 환경 | Spring 기반 SSR 서비스 | SPA(Client-side Routing) 서비스 |
| 검증 캐시 저장소 | Redis | Session Storage |
| 캐시 키 | SUAT 기반 안전한 키 (예: SUAT_CHECK:{SUAT_HASH}) |
STOVE_USER_TOKEN_CHECK |
| 캐시 TTL | 300초(5분) | 300초(5분) |
| 검증 API | /auth/v5/user_token/check |
/auth/v5/user_token/check |
구성 요소
| 구성 요소 | 역할 |
|---|---|
| Client (Browser) | SUAT 쿠키 전달(SSR), 사용자 액션에 따른 토큰 검증 요청(CSR) |
| SSR Service (BE) | 요청에서 SUAT 추출, Redis 검증 캐시 조회로 인증 API 호출 여부 결정, 페이지 렌더링 |
| SPA Service (FE) | Session Storage 검증 캐시 조회로 인증 API 호출 여부 결정, API 호출 전 공통 검증 수행 |
| Auth API | /auth/v5/user_token/check로 SUAT 유효성 검증 |
| Redis (SSR) | 최근 검증된 SUAT 결과를 TTL 동안 저장하는 서버 측 캐시 |
| Session Storage (CSR) | 검증 시각·만료 시각을 저장하는 브라우저 측 캐시 |
동작 원리
최초 진입 시 인증 API로 SUAT를 검증하고 결과를 캐시에 저장해요. 이후 같은 SUAT·세션의 요청에서는 캐시가 유효하면(5분 이내) 인증 API 호출을 생략하고, 캐시가 없거나 만료됐거나 SUAT가 바뀐 경우에만 재검증해요. 이렇게 동일한 SUAT에 대한 반복 검증 호출을 줄여 응답 성능을 높여요.
SSR은 서버 측 Redis에 검증 결과를 저장해 여러 요청과 다중 서버가 캐시를 공유해요. CSR은 브라우저 Session Storage에 저장해 새로고침(F5)에도 유지하되 탭을 종료하면 삭제돼요. 어느 방식이든 SUAT 원문을 캐시 키로 직접 사용하지 말고 SHA-256 등 해시값을 사용하는 것을 권장해요.
연동 가이드
사전 준비
SSR·CSR에 공통으로 필요한 준비 사항이에요. 방식별 세부 준비는 각 [개발하기] 트랙에서 안내해요.
| 항목 | 내용 | 비고 |
|---|---|---|
| SUAT 인증 토큰 | accounts.onstove.com/login을 통해 Client에 SUAT 인증 토큰 발급 완료 |
필수 |
| 인증 검증 API 연동 | /auth/v5/user_token/check 호출로 SUAT 유효성을 검증할 수 있는 경로 확보 |
필수 |
| 검증 캐시 저장소 | SSR은 Redis, CSR은 Session Storage 구성 | 필수 |
| 캐시 키·TTL 정책 | SUAT를 식별할 안전한 키와 TTL 300초(5분) 적용. 캐시가 있으면 검증 생략, 없거나 만료 시 재검증 | 필수 |
API 접근 권한은 퍼블리싱 기술 담당자와 협의해요
인증 API 접근 권한과 환경별 도메인은 담당자를 통해 확인해 주세요. 문의: sgp_publishtech_d@smilegate.com
기본 검증 흐름
요청·액션이 발생하면 SUAT를 추출해 검증 캐시를 조회하고, 캐시가 유효하면 인증 API 호출을 생략해요. 캐시가 없거나 만료됐거나 SUAT가 바뀐 경우에만 인증 API를 호출한 뒤 결과를 캐시에 저장해요. SSR·CSR 모두 아래 판단 흐름을 따라요.
시작 전 결정 항목
서비스 연동 전에 아래 항목을 결정해요. 결정 결과에 따라 캐시 구조와 인증 실패 처리 방식이 달라져요.
| 결정 항목 | 내용 |
|---|---|
| 캐시 키 구조 | SUAT를 식별할 안전한 키 형식 (SSR은 Redis Key, CSR은 Session Storage Key) |
| 캐시 TTL | 검증 결과 재사용 시간 (기본 300초) |
| 인증 API 호출 방식 | GET·POST 등 호출 방식과 SUAT 전달 방식 |
| 인증 성공·실패 기준 | HTTP Status·업무 코드로 성공·실패를 판단하는 기준 |
| 인증 실패 처리 정책 | 만료·변조·미인증 구분 및 후속 처리 방식 |
| 캐시 공유 방식 (SSR) | 다중 서버에서 공용 Redis 사용 여부 |
| 로그인 Redirect 정책 | 인증 실패 시 Redirect 또는 401 응답 처리 방식 |
환경 구분 안내
서비스의 렌더링 방식과 캐시 저장소 사용 가능 여부에 따라 적용 환경이 달라져요.
| 구분 | 적용 환경 |
|---|---|
| SSR 적용 가능 | Spring 기반 SSR 서비스, Redis 사용 가능 환경, 서버에서 SUAT Cookie 조회 가능 환경, Accounts 인증 API 호출 가능 환경 |
| CSR 적용 가능 | SPA(Client-side Routing) 서비스, Session Storage 사용 가능 환경, POST /auth/v5/user_token/check 호출 가능 환경 |
| 별도 검토 필요 | Redis를 사용할 수 없는 SSR 환경, 서버·브라우저 캐시를 모두 사용할 수 없는 환경 |
연동 체크리스트
연동 전에 아래 항목을 확인해요.
| 확인 항목 | 내용 |
|---|---|
| SUAT Cookie | 이름·Domain·Path 확인 |
| 인증 API | URL 및 HTTP Method 확인 |
| 인증 요청 방식 | Cookie 전달 방식 확인 |
| 성공 응답 | HTTP Status 및 업무 코드 확인 |
| 실패 응답 | 만료·변조·미인증 구분 확인 |
| Redis (SSR) | Key 구조 및 TTL 확인, 다중 서버 공용 Redis 사용 여부 |
| Session Storage (CSR) | 키·TTL 확인, 공통 검증 로직 적용 여부 |
| 로그인 처리 | Redirect 또는 401 정책 확인 |
| 네트워크 | 인증 API 접근 권한 확인 |
| 운영 모니터링 | 인증 API 호출량 및 실패율 모니터링 |
개발하기
SSR 연동 (백엔드)
서버에서 페이지를 렌더링하는 환경의 연동이에요. 요청에 포함된 SUAT를 서버가 추출해 Redis 검증 캐시로 인증 API 호출 여부를 결정하고, 캐시가 유효하면 검증을 생략한 채 페이지를 렌더링해요.
사전 준비
accounts.onstove.com/login을 통해 Client에 SUAT 인증 토큰이 발급되어 있어야 해요.- SSR 서비스가 요청 헤더에 포함된 SUAT Cookie를 수신할 수 있어야 해요.
/auth/v5/user_token/checkAPI와 연동해 SUAT 유효성을 검증할 수 있어야 해요.- 검증 결과를 저장할 Redis 캐시가 구성되어 있어야 하고, SUAT를 식별할 안전한 키와 TTL 300초(5분)를 사용해야 해요.
- Redis 캐시가 있으면 인증 검증 API 호출을 생략하고, 없거나 만료된 경우에만 재검증하도록 구현해요.
개발 흐름
SSR 서비스 진입 시 Client가 보유한 SUAT로 사용자 인증을 수행해요. 검증 결과를 Redis에 캐시해 동일 SUAT의 반복 검증 호출을 줄여요.
- SSR 서비스 진입 시 요청 헤더에서 SUAT를 추출해요.
- SUAT 기반 캐시 키(예:
SUAT_CHECK:{SUAT_HASH})로 Redis 검증 캐시를 조회해요. - 캐시가 존재하고 유효하면
/auth/v5/user_token/check호출 없이 요청을 처리해요. - 캐시가 없거나 만료됐으면
/auth/v5/user_token/check를 호출해 SUAT를 검증해요. - 검증에 성공하면 결과를 Redis에 저장하고 TTL을 300초(5분)로 설정해요.
- 이후 동일 SUAT 요청은 캐시를 재사용해 SSR 서비스 응답 성능을 높여요.
SUAT 원문을 Redis 키로 직접 쓰지 마세요
Redis는 SUAT 검증 결과를 일정 시간 재사용하기 위한 인증 검증 캐시예요. SUAT 원문을 키로 직접 사용하지 말고 SHA-256 등의 해시값을 사용하는 것을 권장해요.
트러블슈팅
응답 code별 처리 방안이에요. 상세 응답 코드·메시지 스펙은 API & SDK 레퍼런스 메뉴를 참고해 주세요.
| HTTP Status | 업무 코드 | 메시지 | 설명 |
|---|---|---|---|
| 200 | 0 | success | 성공 |
| 400 | 400 | bad request | 잘못된 요청 |
| 500 | 500 | unknown error | 서버 내부 오류 |
샘플 코드
Spring 기반 SSR 서비스에서 요청 SUAT와 캐시된 SUAT를 비교해, 값이 바뀌었을 때만 인증 API를 호출하는 예제예요. 다중 서버 환경에서는 로컬 세션 대신 공용 Redis를 캐시로 사용해 서버 간 검증 결과를 공유해 주세요.
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpSession;
import org.springframework.http.*;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.client.RestTemplate;
import java.util.Arrays;
import java.util.Objects;
@Controller
public class MainController {
private static final String AUTH_API =
"https://accounts.onstove.com/auth/v5/user_token/check";
private final RestTemplate restTemplate = new RestTemplate();
@GetMapping("/main")
public String main(HttpServletRequest request) {
HttpSession session = request.getSession();
// 검증 캐시 유효 시간 5분(300초)
session.setMaxInactiveInterval(300);
// 1. 요청 Cookie에서 SUAT 추출
String requestSuat = Arrays.stream(request.getCookies())
.filter(cookie -> "SUAT".equals(cookie.getName()))
.map(Cookie::getValue)
.findFirst()
.orElse(null);
// 2. 캐시에 저장된 SUAT 조회
String sessionSuat = (String) session.getAttribute("SUAT");
// 3. SUAT가 바뀌었거나 캐시가 없으면 인증 API 호출
if (!Objects.equals(requestSuat, sessionSuat)) {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.COOKIE, "SUAT=" + requestSuat);
HttpEntity<Void> entity = new HttpEntity<>(headers);
ResponseEntity<String> response = restTemplate.exchange(
AUTH_API,
HttpMethod.GET,
entity,
String.class);
if (!response.getStatusCode().is2xxSuccessful()) {
throw new RuntimeException("Invalid SUAT");
}
// 4. 검증 성공 시 SUAT를 캐시에 저장
session.setAttribute("SUAT", requestSuat);
}
// 5. 페이지 렌더링
return "main";
}
}
CSR 연동 (프론트엔드)
브라우저에서 화면을 렌더링하는 SPA 환경의 연동이에요. Session Storage 검증 캐시로 인증 API 호출 여부를 결정하고, 캐시가 유효하면 검증을 생략한 채 요청을 처리해요.
사전 준비
accounts.onstove.com/login을 통해 SUAT 인증 토큰이 발급되어 있어야 해요.- SPA 서비스가
POST /auth/v5/user_token/checkAPI로 사용자 토큰 유효성을 검증할 수 있어야 해요. - 검증 결과를 저장할 Session Storage를 사용할 수 있어야 해요.
- Session Storage에
STOVE_USER_TOKEN_CHECK키로 검증 캐시를 저장하고 TTL 300초(5분)로 관리해요. - 모든 API 호출은 검증 캐시를 확인하는 공통 로직(Interceptor 또는 API Wrapper)을 통해 처리하는 것을 권장해요.
개발 흐름
서비스 최초 진입과 사용자 액션마다 Session Storage의 검증 캐시를 확인해요. 캐시가 유효하면 인증 API 호출을 생략하고, 새로고침(F5)에도 캐시가 유지돼요.
- 서비스 최초 진입 시
POST /auth/v5/user_token/check로 사용자 토큰 유효성을 검증해요. - 검증이 완료되면 Session Storage(
STOVE_USER_TOKEN_CHECK)에 검증 시각(verifiedAt)과 만료 시각(expireAt)을 저장해요. - 사용자 액션으로 API를 호출하기 전에 Session Storage의 검증 캐시를 확인해요.
- 캐시가 유효하면(5분 이내) 인증 API 호출을 생략하고 요청을 처리해요.
- 캐시가 만료됐거나 없으면 인증 API를 다시 호출해 검증한 뒤 Session Storage를 갱신해요.
- 브라우저 새로고침(F5) 시에도 Session Storage가 유지되므로, 캐시가 유효하면 재검증하지 않아요.
Session Storage 캐시는 새로고침에는 유지되고 탭 종료 시 삭제돼요STOVE_USER_TOKEN_CHECK 키에 검증 시각(verifiedAt)과 만료 시각(expireAt)을 저장해요. verifiedAt부터 5분간 유효하며 expireAt 이후에는 재검증이 필요해요. 새로고침(F5) 시에도 유지되지만 브라우저(탭)를 종료하면 삭제돼요.
트러블슈팅
응답 code별 처리 방안이에요. 상세 응답 코드·메시지 스펙은 API & SDK 레퍼런스 메뉴를 참고해 주세요.
| HTTP Status | 업무 코드 | 메시지 | 설명 |
|---|---|---|---|
| 200 | 0 | success | 성공 |
| 400 | 400 | bad request | 잘못된 요청 |
| 500 | 500 | unknown error | 서버 내부 오류 |
샘플 코드
Session Storage 검증 캐시를 확인해 유효하면 인증 API 호출을 생략하고, API 호출 전 공통 래퍼로 토큰 검증을 수행하는 예제예요.
const STORAGE_KEY = "STOVE_USER_TOKEN_CHECK";
const CACHE_TTL = 5 * 60 * 1000; // 5분
/**
* 토큰 유효성 검증. 캐시가 유효하면 API 호출을 생략해요.
*/
async function validateUserToken() {
const cache = getTokenCheckCache();
// 캐시가 유효하면 검증 API 호출 생략
if (cache && cache.expireAt > Date.now()) {
console.log("Skip /auth/v5/user_token/check");
return true;
}
// 캐시가 없거나 만료 시 검증 API 호출
const response = await fetch("/auth/v5/user_token/check", {
method: "POST",
credentials: "include"
});
if (!response.ok) {
throw new Error("Invalid User Token");
}
saveTokenCheckCache();
return true;
}
/**
* 검증 결과를 Session Storage에 저장
*/
function saveTokenCheckCache() {
const now = Date.now();
sessionStorage.setItem(
STORAGE_KEY,
JSON.stringify({
verifiedAt: now,
expireAt: now + CACHE_TTL
})
);
}
/**
* Session Storage에서 검증 캐시 조회
*/
function getTokenCheckCache() {
const value = sessionStorage.getItem(STORAGE_KEY);
if (!value) {
return null;
}
try {
return JSON.parse(value);
} catch {
sessionStorage.removeItem(STORAGE_KEY);
return null;
}
}
/**
* API 호출 전 토큰 검증을 수행하는 공통 래퍼
*/
async function requestApi() {
await validateUserToken();
return fetch("/api/example", {
credentials: "include"
});
}