실행흐름 1차📚 전체 맵실행흐름 3차
06c · 2차 프로젝트 · React + JWT

JWT
FLOW

2차 프로젝트 코드 실행 흐름 — JwtFilter 실행 순서 · JWT 생성/검증 메모리 · 1차와 무엇이 달라졌나

JWT Structure
JWT 토큰이 만들어지고 검증되는 원리

코드 흐름을 보기 전에 JWT 자체가 어떻게 생겼고 어떻게 동작하는지 이해해야 합니다.

JWT 구조 — Header.Payload.Signature

eyJhbGciOiJIUzI1NiJ9 · eyJzdWIiOiJob25nIn0 · SflKxwRJSMeKKF2QT4f

Header: 알고리즘 정보 (HS256) — Base64URL 인코딩
Payload: sub(username), nickname, iat(발급시각), exp(만료시각) — Base64URL 인코딩
Signature: Header+Payload를 secret 키로 HMAC-SHA256 서명

Payload는 누구나 디코딩 가능 (암호화 X). Signature로 위변조 여부만 검증.

JwtUtil.java — createToken() 실행 시 메모리
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 대상
}
Scenario 01
로그인 — JWT 발급 전체 흐름
1
React → axios.post("/api/user/login") → JWT 발급 → localStorage
1
React · Login.jsx
handleLogin() 실행 → api.post() 호출
브라우저 메모리(JS 힙)에서 실행. api.post("/api/user/login", { username, password })
axios interceptor: localStorage에 token 없으면 Authorization 헤더 없이 전송.
브라우저 메모리 (JS 엔진)
2
JwtFilter
Authorization 헤더 없음 → 건너뜀 → doFilter() 호출
JwtFilter.java — doFilter() 실행 순서
@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
}
STACK — doFilter() 프레임 push/pop
3
UserController
login() 실행 → userService.login() 호출
@PostMapping("/api/user/login") 매핑 → login() 실행
Stack에 login() 프레임 push
4
UserService
비밀번호 검증 → jwtUtil.createToken() 호출
UserService.java — 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 스택에서 참조 유지
}
STACK — user(참조), token(참조) 지역변수 HEAP — JWT 문자열 객체
5
Controller → React
ResponseEntity.ok(Map.of("token", token)) — JSON 직렬화
Map.of("token", token) → Heap에 Map 객체 생성 → Jackson이 JSON 문자열로 직렬화 → HTTP 응답 body에 포함 → Map, JSON 문자열 모두 GC 대상.
HEAP — Map 객체, JSON 문자열 (전송 후 GC)
6
React
token → localStorage 저장 → 이후 모든 요청 헤더에 자동 첨부
localStorage.setItem("token", response.data.token)
브라우저의 localStorage(persistent storage)에 저장 — 탭 닫아도 유지.
이후 axios interceptor가 모든 요청에 Authorization: Bearer {token} 자동 첨부.
브라우저 localStorage (서버 메모리 사용 없음)
📌 1차 Session vs 2차 JWT — 메모리 차이

1차: 로그인하면 서버 Heap에 HttpSession 객체 생성 → 서버가 유저 상태 기억
2차: 로그인하면 JWT 문자열만 반환 → 브라우저 localStorage에 저장 → 서버 메모리 사용 없음

JWT는 서버가 아무것도 기억하지 않고, 매 요청마다 토큰을 검증해서 누군지 확인합니다.

Scenario 02
게시글 작성 — JwtFilter 동작 상세 코드 실행 순서
2
POST /api/board/write — Bearer 토큰 검증 → request.setAttribute → Controller
1
React · axios interceptor
localStorage에서 token 꺼내서 헤더에 첨부
config.headers.Authorization = `Bearer ${token}`
HTTP 헤더: Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
2
JwtFilter
토큰 추출 → 검증 → username 꺼내기 → DB 조회 → request에 저장
JwtFilter.java — 인증 요청 처리 순서
@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
}
STACK — token, username, user 지역변수 HEAP — request 객체 속성에 user 참조 저장
3
BoardController
request.getAttribute("loginUser")로 유저 꺼내기
BoardController.java
@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(...);
}
STACK — loginUser 지역변수 (Heap User 참조)
📌 request.setAttribute vs 3차 SecurityContextHolder — 차이

2차: request.setAttribute("loginUser", user)
request 객체(Heap)의 내부 Map에 저장. 이 요청 내에서만 접근 가능.
Controller에서 직접 꺼내야 하고, null 체크도 직접 해야 합니다.

3차: SecurityContextHolder.getContext().setAuthentication(auth)
— Thread-Local에 저장. @AuthenticationPrincipal로 자동 주입.
null 체크 없이 Spring이 보장. 훨씬 표준적이고 깔끔합니다.

Memory Summary
2차 — 메모리 상태 요약
영역항상 상주요청 처리 중1차 대비 변화
Heap (서버) Bean들, JwtUtil, DB 연결풀 request 객체, User/Board 엔티티, JWT 문자열 HttpSession 없음 — STATELESS
Stack (서버) doFilter, login, write 프레임 + 지역변수 JwtFilter 프레임 추가
브라우저 localStorage (token) React 상태(useState), axios 요청/응답 localStorage에 토큰 저장
실행흐름 1차📚 전체 맵실행흐름 3차