2차 프로젝트 코드 실행 흐름 — JwtFilter 실행 순서 · JWT 생성/검증 메모리 · 1차와 무엇이 달라졌나
코드 흐름을 보기 전에 JWT 자체가 어떻게 생겼고 어떻게 동작하는지 이해해야 합니다.
eyJhbGciOiJIUzI1NiJ9 · eyJzdWIiOiJob25nIn0 · SflKxwRJSMeKKF2QT4f
Header: 알고리즘 정보 (HS256) — Base64URL 인코딩
Payload: sub(username), nickname, iat(발급시각), exp(만료시각) — Base64URL 인코딩
Signature: Header+Payload를 secret 키로 HMAC-SHA256 서명
Payload는 누구나 디코딩 가능 (암호화 X). Signature로 위변조 여부만 검증.
public String createToken(String username, String nickname) { // Stack: createToken() 프레임 push // username, nickname → Stack의 파라미터 (참조값) return Jwts.builder() // Heap: JwtBuilder 객체 생성 .subject(username) // Payload의 sub 클레임 세팅 .claim("nickname", nickname) // 커스텀 클레임 .issuedAt(new Date()) // Heap: new Date() 생성 .expiration(new Date( System.currentTimeMillis() + expiration)) .signWith(getSigningKey()) // HMAC-SHA256 서명 계산 .compact(); // compact(): Header.Payload.Signature 문자열 생성 // → Heap에 String 객체로 생성 // → 반환되면 Stack의 호출자 프레임에서 참조 // 메서드 끝: Stack 프레임 pop // JwtBuilder 객체 → GC 대상 }
api.post("/api/user/login", { username, password })@Override public void doFilter(ServletRequest req, ...) { // Stack: doFilter() 프레임 push HttpServletRequest request = (HttpServletRequest) req; String header = request.getHeader("Authorization"); // header = null (로그인 요청은 토큰 없음) if (header != null && header.startsWith("Bearer ")) { // null이므로 이 블록 건너뜀 } chain.doFilter(request, response); // 다음 Filter로 넘김 → 결국 Controller 도달 // Stack: doFilter() 프레임 pop }
@PostMapping("/api/user/login") 매핑 → login() 실행public String login(String username, String password) { // [1] Stack: login() 프레임 push // [2] DB에서 유저 조회 — SELECT 쿼리 User user = userRepository.findByUsername(username) .orElseThrow(...); // user → Stack의 지역변수 (Heap의 User 객체 참조) // [3] BCrypt 검증 — CPU 연산 (메모리 변화 없음) if (!bCryptPasswordEncoder.matches(password, user.getPassword())) throw new ...; // [4] JWT 생성 — jwtUtil.createToken() 호출 String token = jwtUtil.createToken( user.getUsername(), user.getNickname()); // token → Stack의 지역변수 (Heap의 JWT 문자열 참조) return token; // [5] Stack: login() 프레임 pop // user 지역변수 소멸 (Heap의 User 객체는 GC 대상) // token은 Controller로 반환 → Controller 스택에서 참조 유지 }
Map.of("token", token) → Heap에 Map 객체 생성 →
Jackson이 JSON 문자열로 직렬화 → HTTP 응답 body에 포함 →
Map, JSON 문자열 모두 GC 대상.
localStorage.setItem("token", response.data.token)Authorization: Bearer {token} 자동 첨부.
1차: 로그인하면 서버 Heap에 HttpSession 객체 생성 → 서버가 유저 상태 기억
2차: 로그인하면 JWT 문자열만 반환 → 브라우저 localStorage에 저장 → 서버 메모리 사용 없음
JWT는 서버가 아무것도 기억하지 않고, 매 요청마다 토큰을 검증해서 누군지 확인합니다.
config.headers.Authorization = `Bearer ${token}`Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
@Override public void doFilter(ServletRequest req, ...) { // [1] Stack: doFilter() 프레임 HttpServletRequest request = (HttpServletRequest) req; // [2] 헤더에서 토큰 추출 String header = request.getHeader("Authorization"); // header → Stack 지역변수 if (header != null && header.startsWith("Bearer ")) { // [3] "Bearer " 제거 → 순수 토큰 String token = header.substring(7); // token → Stack 지역변수 // [4] 토큰 유효성 검증 if (jwtUtil.validateToken(token)) { // jwtUtil은 기동 시 Heap에 있는 Singleton Bean // validateToken() → Stack에 프레임 push → 서명 검증 → pop // [5] 토큰에서 username 추출 String username = jwtUtil.getUsername(token); // [6] DB에서 유저 조회 — SELECT 쿼리 User user = userRepository.findByUsername(username) .orElse(null); // user → Stack 지역변수, Heap의 User 객체 참조 // [7] request에 유저 저장 if (user != null) { request.setAttribute("loginUser", user); // request 객체(Heap)의 속성 맵에 user 참조 저장 // 이 request는 이 요청 처리 내내 Heap에 존재 } } } chain.doFilter(request, response); // [8] 다음 Filter → Controller로 넘김 // Stack: doFilter() 프레임 pop }
@PostMapping("/api/board/write") public ResponseEntity<?> write( @RequestBody BoardDto boardDto, HttpServletRequest request) { // Heap의 request 객체에서 user 참조 꺼내기 User loginUser = (User) request.getAttribute("loginUser"); // loginUser → Stack 지역변수 (Heap의 User 객체 참조) // JwtFilter에서 저장한 바로 그 User 객체 if (loginUser == null) return ResponseEntity.status(401).build(); boardService.write(boardDto, loginUser); return ResponseEntity.ok(...); }
2차: request.setAttribute("loginUser", user)
— request 객체(Heap)의 내부 Map에 저장. 이 요청 내에서만 접근 가능.
Controller에서 직접 꺼내야 하고, null 체크도 직접 해야 합니다.
3차: SecurityContextHolder.getContext().setAuthentication(auth)
— Thread-Local에 저장. @AuthenticationPrincipal로 자동 주입.
null 체크 없이 Spring이 보장. 훨씬 표준적이고 깔끔합니다.
| 영역 | 항상 상주 | 요청 처리 중 | 1차 대비 변화 |
|---|---|---|---|
| Heap (서버) | Bean들, JwtUtil, DB 연결풀 | request 객체, User/Board 엔티티, JWT 문자열 | HttpSession 없음 — STATELESS |
| Stack (서버) | — | doFilter, login, write 프레임 + 지역변수 | JwtFilter 프레임 추가 |
| 브라우저 | localStorage (token) | React 상태(useState), axios 요청/응답 | localStorage에 토큰 저장 |