Security 프론트📚 전체 맵실행흐름 조감도
05e · 3차 프로젝트 · 완결편

Full Flow

요청별 전체 흐름 완전 추적 + 1차 vs 2차 vs 3차 최종 비교표

시리즈 5/5 · 최종편 · 05a~05d 모두 읽은 후 정리용

이 파일의 학습 계획
05e에서 다루는 것들

코드를 다 읽었으니 이제 실제 요청이 들어왔을 때 어떤 순서로 뭐가 실행되는지 처음부터 끝까지 따라갑니다. React 버튼 클릭 → axios → Filter Chain → Controller → Service → DB → 응답 → 화면 반영까지 전부. 마지막으로 1~3차 전체를 한 표에서 비교해서 흐름을 완전히 정리합니다.

#시나리오핵심 포인트
1로그인토큰 + role 발급 → localStorage 저장
2게시글 목록 조회토큰 검증 → SecurityContext → Controller 도달
3게시글 작성@AuthenticationPrincipal → User 엔티티 자동 주입
4관리자 페이지 접근AdminRoute → hasRole → 이중 보안
5관리자 유저 삭제게시글 먼저 삭제 → 유저 삭제 → @Transactional
61~3차 전체 비교표인증방식·로그인·게시글·설정 방식 전체 비교
Scenario 01
로그인 전체 흐름
1
React · Login.jsx
로그인 버튼 클릭
handleLogin() 실행 → api.post("/api/user/login", { username, password })
axios interceptor: localStorage에 token 없으면 Authorization 헤더 없이 전송 (로그인 전이므로)
2
Spring Security · JwtAuthenticationFilter
Authorization 헤더 없음 → 건너뜀
authHeader == null → if 조건 불통과 → SecurityContext에 아무것도 저장 안 함
filterChain.doFilter()로 다음 Filter에 넘김
3
Spring Security · AuthorizationFilter
/api/user/login → permitAll() → 통과
SecurityConfig에서 .requestMatchers("/api/user/login").permitAll() 설정
인증 없어도 허용 → Controller로 진행
4
Controller · UserController
login() 메서드 실행
@PostMapping("/api/user/login")
userService.login(userDto.getUsername(), userDto.getPassword()) 호출
5
Service · UserService
비밀번호 검증 → JWT 생성 → Map 반환
DB에서 username으로 유저 조회 → bCryptPasswordEncoder.matches()로 비밀번호 검증 → jwtUtil.createToken(username, nickname, role)Map.of("token", token, "role", role) 반환
6
Controller → React
ResponseEntity.ok(map) 반환 → JSON 응답
{ "token": "eyJhbGci...", "role": "ROLE_USER" } JSON으로 React에 전달
7
React · Login.jsx
token + role → localStorage 저장 → 이동
localStorage.setItem("token", response.data.token)
localStorage.setItem("role", response.data.role) ← 3차 추가
navigate("/board/list")
Scenario 02
게시글 목록 조회 — 토큰 검증 핵심 흐름
1
React · BoardList.jsx
useEffect → api.get("/api/board/list")
axios interceptor가 localStorage에서 token 꺼내서 자동으로 헤더 첨부:
Authorization: Bearer eyJhbGciOiJIUzI1NiJ9...
2
JwtAuthenticationFilter
토큰 검증 → SecurityContext에 인증 저장
헤더에서 "Bearer " 제거 → jwtUtil.validateToken(token) → 유효
jwtUtil.getUsername(token) → "hong"
customUserDetailsService.loadUserByUsername("hong") → DB 조회 → CustomUserDetails
UsernamePasswordAuthenticationToken 생성 → SecurityContextHolder에 저장
3
AuthorizationFilter
/api/board/list → authenticated() → 인증 있음 → 통과
SecurityContext에 Authentication 있음 → .anyRequest().authenticated() 조건 충족 → Controller로
4
BoardController · list()
boardService.getBoardList(page) 호출
로그인 유저 정보 필요 없음 → @AuthenticationPrincipal 파라미터 없음
페이지 번호만 받아서 목록 조회
5
BoardService → DB → React
페이지 조회 → DTO 변환 → JSON 응답
boardRepository.findAllByOrderByCreatedAtDesc(pageable) → Stream으로 DTO 변환 → ResponseEntity.ok() → React에서 setBoards(response.data) → 화면 렌더링
Scenario 03
게시글 작성 — @AuthenticationPrincipal 주입 흐름
1
React · BoardWrite.jsx
api.post("/api/board/write", { title, content })
axios interceptor → Authorization: Bearer 토큰 자동 첨부
2
JwtAuthenticationFilter
토큰 검증 → SecurityContext에 CustomUserDetails 저장
시나리오 2와 동일한 흐름. SecurityContextHolder에 Authentication(principal=CustomUserDetails) 저장
3
AuthorizationFilter
authenticated() 확인 → 통과
SecurityContext에 인증 있음 → Controller로 진행
4
BoardController · write()
@AuthenticationPrincipal이 CustomUserDetails 자동 주입
Spring이 SecurityContext.getAuthentication().getPrincipal()을 꺼내서 파라미터에 주입
userDetails.getUser()로 실제 User 엔티티 접근
null 체크 불필요 — Security가 이미 인증 보장
5
BoardService → DB
Board 엔티티 생성 → save()
Board 객체에 title, content, user(FK) 세팅 → boardRepository.save(board) → INSERT 쿼리
6
React
{ "message": "글쓰기 성공" } 수신 → 목록으로 이동
navigate("/board/list")
Scenario 04 + 05
관리자 페이지 접근 + 유저 삭제
4
관리자 페이지 접근 — 이중 보안
1
React · BoardList.jsx → /admin 링크 클릭
AdminRoute에서 role 체크
localStorage.getItem("role") → "ROLE_ADMIN"이면 AdminPage 렌더링
"ROLE_USER"면 → <Navigate to="/board/list" /> 리다이렉트
2
React · AdminPage.jsx
useEffect → api.get("/api/admin/users") + "/api/admin/boards"
axios interceptor → Bearer 토큰 자동 첨부
3
JwtAuthenticationFilter → AuthorizationFilter
토큰 검증 → /api/admin/** → hasRole("ADMIN") 체크
토큰의 ROLE이 "ROLE_USER"면 → 여기서 403 Forbidden 자동 반환 (Controller 못 감)
"ROLE_ADMIN"이면 → AdminController로 진행
→ 이게 이중 보안의 백엔드 담당 부분
4
AdminController → AdminService → DB
getAllUsers() / getAllBoards() → Stream → DTO → JSON
userRepository.findAll() → stream().map(user → UserDto) → collect → 반환
React: setUsers(response.data), setBoards(response.data) → 화면 렌더링
5
관리자 유저 삭제 — FK 제약 + @Transactional
1
React · AdminPage.jsx
삭제 버튼 → confirm → api.delete("/api/admin/users/{id}")
window.confirm("삭제하시겠습니까?") → 확인 시
api.delete(`/api/admin/users/${userId}`) → Bearer 토큰 자동 첨부
2
Security Filter Chain
ROLE_ADMIN 확인 → 통과
JwtAuthenticationFilter → AuthorizationFilter → hasRole("ADMIN") 통과
3
AdminController · deleteUser()
adminService.deleteUser(userId) 호출
@PathVariable Long userId → Service로 전달
4
AdminService · deleteUser() — @Transactional
게시글 먼저 삭제 → 유저 삭제
@Transactional 시작 → 트랜잭션 묶음
boardRepository.deleteByUser(user) → DELETE FROM board WHERE user_id = ?
userRepository.delete(user) → DELETE FROM users WHERE id = ?
순서 중요: Board가 User를 FK로 참조 → Board 먼저 지워야 FK 제약 위반 없음
둘 중 하나 실패 → @Transactional이 전체 롤백
5
React
삭제 성공 → fetchUsers() 재호출 → 목록 갱신
삭제 후 API 재호출로 최신 목록 반영
Final Comparison
1차 vs 2차 vs 3차 — 전체 비교표
전체 구조 비교
항목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
로그인 코드 비교
Controller — login()
// ===== 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);
}
게시글 작성 — 로그인 유저 꺼내기 비교
Controller — write()
// ===== 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 관련 클래스 분리
✅ 시리즈 완료 — 05a~05e 전체 정리
  • 05a: 개요·기술스택·파일구조·용어사전 — 전체 그림 파악
  • 05b: Filter Chain·SecurityContext·UserDetails·@AuthenticationPrincipal — 동작 원리
  • 05c: 백엔드 12개 파일 한 줄씩 해부 — 코드 이해
  • 05d: React 8개 파일 해부 + 핵심 문법 — 프론트 이해
  • 05e: 시나리오별 전체 흐름 추적 + 1~3차 비교 — 통합 정리
Security 프론트📚 전체 맵실행흐름 조감도