요청별 전체 흐름 완전 추적 + 1차 vs 2차 vs 3차 최종 비교표
시리즈 5/5 · 최종편 · 05a~05d 모두 읽은 후 정리용
코드를 다 읽었으니 이제 실제 요청이 들어왔을 때 어떤 순서로 뭐가 실행되는지 처음부터 끝까지 따라갑니다. React 버튼 클릭 → axios → Filter Chain → Controller → Service → DB → 응답 → 화면 반영까지 전부. 마지막으로 1~3차 전체를 한 표에서 비교해서 흐름을 완전히 정리합니다.
| # | 시나리오 | 핵심 포인트 |
|---|---|---|
| 1 | 로그인 | 토큰 + role 발급 → localStorage 저장 |
| 2 | 게시글 목록 조회 | 토큰 검증 → SecurityContext → Controller 도달 |
| 3 | 게시글 작성 | @AuthenticationPrincipal → User 엔티티 자동 주입 |
| 4 | 관리자 페이지 접근 | AdminRoute → hasRole → 이중 보안 |
| 5 | 관리자 유저 삭제 | 게시글 먼저 삭제 → 유저 삭제 → @Transactional |
| 6 | 1~3차 전체 비교표 | 인증방식·로그인·게시글·설정 방식 전체 비교 |
handleLogin() 실행 →
api.post("/api/user/login", { username, password })authHeader == null → if 조건 불통과 → SecurityContext에 아무것도 저장 안 함filterChain.doFilter()로 다음 Filter에 넘김
.requestMatchers("/api/user/login").permitAll() 설정@PostMapping("/api/user/login")userService.login(userDto.getUsername(), userDto.getPassword()) 호출
bCryptPasswordEncoder.matches()로 비밀번호 검증 →
jwtUtil.createToken(username, nickname, role) →
Map.of("token", token, "role", role) 반환
{ "token": "eyJhbGci...", "role": "ROLE_USER" } JSON으로 React에 전달
localStorage.setItem("token", response.data.token)localStorage.setItem("role", response.data.role) ← 3차 추가navigate("/board/list")
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
jwtUtil.validateToken(token) → 유효jwtUtil.getUsername(token) → "hong"customUserDetailsService.loadUserByUsername("hong") → DB 조회 → CustomUserDetailsUsernamePasswordAuthenticationToken 생성 → SecurityContextHolder에 저장
.anyRequest().authenticated() 조건 충족 → Controller로
@AuthenticationPrincipal 파라미터 없음boardRepository.findAllByOrderByCreatedAtDesc(pageable) →
Stream으로 DTO 변환 → ResponseEntity.ok() → React에서 setBoards(response.data) → 화면 렌더링
SecurityContext.getAuthentication().getPrincipal()을 꺼내서 파라미터에 주입userDetails.getUser()로 실제 User 엔티티 접근boardRepository.save(board) → INSERT 쿼리
navigate("/board/list")localStorage.getItem("role") → "ROLE_ADMIN"이면 AdminPage 렌더링<Navigate to="/board/list" /> 리다이렉트
window.confirm("삭제하시겠습니까?") → 확인 시api.delete(`/api/admin/users/${userId}`) → Bearer 토큰 자동 첨부
@PathVariable Long userId → Service로 전달@Transactional 시작 → 트랜잭션 묶음boardRepository.deleteByUser(user) → DELETE FROM board WHERE user_id = ?userRepository.delete(user) → DELETE FROM users WHERE id = ?| 항목 | 1차 (Thymeleaf + Session) | 2차 (React + JWT) | 3차 (Spring Security) |
|---|---|---|---|
| 화면 렌더링 | Spring이 HTML 생성 (Thymeleaf) | React가 화면 담당 | React가 화면 담당 (동일) |
| 인증 방식 | HttpSession (서버 메모리) | JWT 토큰 (직접 구현) | JWT + Spring Security 표준 |
| 토큰 저장 | 서버 세션 (JSESSIONID 쿠키) | 브라우저 localStorage | 브라우저 localStorage (동일) |
| 인증 필터 | 없음 | 직접 만든 JwtFilter | JwtAuthenticationFilter (Security 표준) |
| 인증 저장소 | HttpSession | request.setAttribute() | SecurityContextHolder (표준) |
| 로그인 유저 꺼내기 | session.getAttribute("loginUser") |
request.getAttribute("loginUser") |
@AuthenticationPrincipal (자동 주입) |
| 로그인 체크 | Controller마다 직접 체크 | Controller마다 직접 체크 | SecurityConfig 한 곳에서 선언 |
| 역할 기반 권한 | 없음 | 없음 | ROLE_USER / ROLE_ADMIN + hasRole() |
| 관리자 기능 | 없음 | 없음 | AdminController + AdminService |
| CORS 설정 | 없음 (프론트 분리 안됨) | 별도 CorsConfig.java | SecurityConfig 내 corsConfigurationSource() |
| 세션 정책 | STATEFUL (서버 세션) | STATELESS (직접 구현) | STATELESS (SecurityConfig에서 명시) |
| 포트 | Spring 8081 | Spring 8082 / React 3001 | Spring 8083 / React 3002 |
| Git 브랜치 | master | feature/react | feature/security |
// ===== 1차 ===== @Controller @PostMapping("/user/login") public String login(@ModelAttribute UserDto userDto, HttpSession session) { User user = userService.login(userDto.getUsername(), userDto.getPassword()); session.setAttribute("loginUser", user); // 서버 메모리에 저장 return "redirect:/board/list"; // HTML redirect } // ===== 2차 ===== @RestController @PostMapping("/api/user/login") public ResponseEntity<?> login(@RequestBody UserDto userDto) { String token = userService.login(...); // JWT 토큰(String) 반환 return ResponseEntity.ok(Map.of("token", token)); // JSON 반환 } // ===== 3차 ===== @RestController @PostMapping("/api/user/login") public ResponseEntity<?> login(@RequestBody UserDto userDto) { Map<String,String> result = userService.login(...); // token + role 반환 return ResponseEntity.ok(result); }
// ===== 1차 — 세션에서 꺼내기 ===== @PostMapping("/board/write") public String write(@ModelAttribute BoardDto boardDto, HttpSession session) { User loginUser = (User) session.getAttribute("loginUser"); // 세션에서 if (loginUser == null) return "redirect:/user/login"; // 직접 체크 boardService.write(boardDto, loginUser); return "redirect:/board/list"; } // ===== 2차 — request 속성에서 꺼내기 ===== @PostMapping("/api/board/write") public ResponseEntity<?> write(@RequestBody BoardDto boardDto, HttpServletRequest request) { User loginUser = (User) request.getAttribute("loginUser"); // request에서 if (loginUser == null) // 직접 체크 return ResponseEntity.status(401).build(); boardService.write(boardDto, loginUser); return ResponseEntity.ok(...); } // ===== 3차 — @AuthenticationPrincipal 자동 주입 ===== @PostMapping("/write") public ResponseEntity<?> write(@RequestBody BoardDto boardDto, @AuthenticationPrincipal CustomUserDetails userDetails) { // null 체크 없음 — SecurityConfig가 이미 처리 boardService.write(boardDto, userDetails.getUser()); return ResponseEntity.ok(Map.of("message", "글쓰기 성공")); }
| 변경 포인트 | 2차 방식 | 3차 방식 | 이점 |
|---|---|---|---|
| 인증 필터 | JwtFilter (비표준) | JwtAuthenticationFilter (Security 표준) | @AuthenticationPrincipal 사용 가능 |
| 인증 저장 | request.setAttribute() | SecurityContextHolder | Security 생태계와 통합 |
| 로그인 유저 주입 | 직접 getAttribute() + null 체크 | @AuthenticationPrincipal 자동 | 코드 간결, 실수 방지 |
| URL 접근 제어 | Controller마다 반복 코드 | SecurityConfig 한 곳에서 선언 | 중앙 관리, 누락 방지 |
| 역할 제어 | 없음 | hasRole("ADMIN") | URL 레벨 권한 제어 |
| JWT 클레임 | username, nickname | username, nickname, role | role 기반 프론트 분기 가능 |
| 로그인 반환 | token (String) | Map (token + role) | 프론트에서 AdminRoute 판별 |
| BCryptPasswordEncoder | BoardApplication.java에서 @Bean | SecurityConfig에서 @Bean | 보안 관련 설정 한 곳 집중 |
| 신규 패키지 | 없음 | security/ 패키지 전체 신규 | Security 관련 클래스 분리 |