Notice
Recent Posts
Recent Comments
Link
| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
Tags
- multi-threading
- this예약어
- 포함관계
- for문
- break문
- continue문
- 상수
- JAVA객체지향
- Java
- While
- Java데이터 타입
- 연관관계
- 집합관계
- 자바 멀티스레딩
- Thread
- 메서드
- IntelliJ IDEA
- 생성자
- 컴파일
- 접근제어지시자
- java변수
- 메서드 오버로딩
- 형 변환
- 인텔리제이 기초 설정
- JAVA기초
- 인텔리제이 한글 깨짐 해결법
- OPP개념
- function
- 시스템 환경 변수 편집
- 반복문
Archives
- Today
- Total
최원종의 개발 블로그
스프링부트 V6- OSIV 본문
OSIV( Open Session in View)

HTTP 요청 시작부터 응답 완료까지 데이터베이스 세션(연결)을 유지하는 패턴
OSIV는 "데이터베이스 커넥션(영속성 컨텍스트)을 언제까지 붙잡고 있을 것인가?"에 대한 전략.
스프링 부트는 기본적으로 spring.jpa.open-in-view=true로 켜져 있음
OSIV 동작 흐름
HTTP 요청 시작
↓
데이터베이스 세션 열기
↓
Controller 실행
↓
Service 실행 (트랜잭션 시작/종료)
↓
View 렌더링 (DB 세션 유지) ← 지연 로딩 가능!
↓
데이터베이스 세션 닫기
↓
HTTP 응답 완료
OSIV가 꺼져 있을 때

// Service에서 트랜잭션 종료 시 DB 연결도 종료
Board board = boardService.findByIdWithReplies(id, sessionUser);
// 뷰 렌더링 중 지연 로딩 시
{{#board.replies}} // LazyInitializationException 발생!
{{comment}}
{{/board.replies}}
OSIV 설정법
# JPA 설정
jpa:
hibernate:
# create: 애플리케이션 시작시 테이블을 새로 생성
# 기존 데이터는 모두 삭제됨 (개발용)
ddl-auto: create
# SQL 쿼리를 콘솔에 출력 (개발용 디버깅)
show-sql: true
# Open Session in View를 false로 설정
# - true (기본값): 뷰 렌더링까지 세션 유지 (LAZY 로딩 가능)
# - false: 트랜잭션 종료 시 세션 종료 (LAZY 로딩 불가, 명시적 조회 필요)
# - false 설정 시 Service에서 필요한 데이터를 모두 조회하고 DTO로 변환해야 함
open-in-view: false
OSIV 핵심 개념 상세 분석
1. OSIV가 켜져 있을 때 (True - 스프링 부트 기본값)
동작: 클라이언트의 요청이 들어와서 컨트롤러,
서비스를 거쳐 다시 컨트롤러를 지나 브라우저에 HTML(Mustache)을 다 그려서 보낼 때까지
DB 커넥션과 영속성 컨텍스트를 살려둠.
장점: 개발이 압도적으로 편함.
Board 엔티티의 User가 LAZY(지연 로딩)로 되어 있어도,
서비스 레이어가 끝난 뒤 컨트롤러나 Mustache 템플릿 안에서
board.getUser().getUsername()을 호출하면 아무 에러 없이 데이터를 알아서 DB에서 가져옴.
단점: 사용자가 엄청 많은 서비스라면,
HTML을 그리고 있는 그 미세한 시간 동안에도 DB 커넥션을 계속 붙잡고 있기 때문에
DB 커넥션 풀이 순식간에 말라버려 서버가 먹통이 되는 대형 장애가 발생할 수 있음.
2. OSIV가 꺼져 있을 때 (False - 프로젝트 요구사항)
동작: 서비스 레이어에 붙은 @Transactional 메서드가 종료되는 순간,
영속성 컨텍스트도 문을 닫고 데이터베이스 커넥션도 풀(Pool)에 즉시 반환.
장점: 비즈니스 로직이 끝나자마자 자원을 반환하므로 DB 커넥션 마름 현상이 발생하지 않음.
실무 대용량 트래픽 환경에서 서버의 생존력과 실시간 성능 최적화에 절대적으로 유리함.
단점: 트랜잭션 범위 밖인 Controller나 View(Mustache) 단에서는 지연 로딩(LAZY)을 절대로 할 수 없음.
만약 호출하면 백엔드 에러 로그에 그 유명한 org.hibernate.LazyInitializationException이 도배됨.
BoardResponse 코드⬇️
더보기
package com.tenco.blog.board;
import com.tenco.blog.user.User;
import com.tenco.blog.util.MyDateUtil;
import lombok.Data;
/**
* 게시글 응답 DTO
* <p>
* Open Session In View 가 false 일 때
* 트랜잭션이 종료 되는 시점에 LAZY 로딩이 불가능하다.
* Service 단에서 필요한 데이터를 모두 조회 또는 일부로 호출(트리거)해서 응답 DTO 변환해서 반환
* 엔티티를 직접 반환하지 않고(Controller,View) 서비스단에서 DTO 내려줄 예정(결합도 감소)
*/
public class BoardResponse {
// 게시글 목록 응답 DTO
@Data
public static class ListDTO {
private Integer id;
private String title;
// username 평탄화 작업 : SSR 설계시 권장 방법, CSR 일 경우는 계층구조로 내려주는게 좋다
private String username;
private String createdAt;
public ListDTO(Board board) {
this.id = board.getId();
this.title = board.getTitle();
// 방어적 코드 활용
if (board.getUser() != null) {
this.username = board.getUser().getUsername();
}
if (board.getCreatedAt() != null) {
this.createdAt = MyDateUtil.timestampFormat(board.getCreatedAt());
}
}
} // end of ListDTO inner class
// 게시글 상세 보기 응답 DTO
@Data
public static class DetailDTO {
private Integer id; // board PK
private String title;
private String content;
private String username;
private Integer userId; // user PK
public DetailDTO(Board board) {
this.id = board.getId();
this.title = board.getTitle();
this.content = board.getContent();
if(board.getUser() != null) {
this.username = board.getUser().getUsername();
this.userId = board.getUser().getId();
}
}
} // end of DetailDTO inner class
} // end of outer class
BoardController코드(한글명 수정 완료)⬇️
더보기
package com.tenco.blog.board;
import com.tenco.blog.user.User;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import java.util.List;
@Slf4j
@Controller // IoC
@RequiredArgsConstructor // DI
public class BoardController {
private final BoardService boardService;
/**
* 게시글 작성 화면 요청
* @return 페이지 반환
* 주소설계 : http://localhost:8080/board/save-form
*/
@GetMapping("/board/save-form")
public String saveForm(HttpSession httpSession) {
return "board/save-form";
}
/**
* 게시글 작성 기능 요청
* @return 페이지 반환
* 주소설계 : http://localhost:8080/board/save-form
*/
@PostMapping("/board/save")
public String saveProc(BoardRequest.SaveDTO saveDTO, HttpSession session) {
User sessionUser = (User) session.getAttribute("sessionUser");
saveDTO.validate();
boardService.게시글작성(saveDTO, sessionUser);
return "redirect:/";
}
/**
* 게시글 목록 화면 요청
* 주소설계 : http://localhost:8080/
*/
@GetMapping({"/", "index"})
public String list(Model model) {
List<BoardResponse.ListDTO> boardList = boardService.게시글목록();
// OSIV 개념을 false 설정했기 때문에 여기서 LAZY 요청을 하면 터져 버린다.
///boardList.get(0).getUser().getUsername();
model.addAttribute("boardList", boardList);
return "board/list";
}
// 게시글 상세보기 화면 요청
// http://localhost:8080/board/1
@GetMapping("/board/{id}")
public String detailPage(@PathVariable(name = "id") Integer id, Model model) {
BoardResponse.DetailDTO detailDTO = boardService.게시글상세조회(id);
model.addAttribute("board", detailDTO);
return "board/detail";
}
// 삭제 기능 요청
@PostMapping("/board/{id}/delete")
public String deleteProc(@PathVariable(name = "id") Integer id, HttpSession session) {
User sessionUser = (User) session.getAttribute("sessionUser");
boardService.게시글삭제(id, sessionUser);
return "redirect:/";
}
// http://localhost:8080/board/1/update-form
// 게시글 수정 화면 요청
@GetMapping("/board/{id}/update-form")
public String updateFormPage(@PathVariable(name = "id") Integer id, Model model, HttpSession session) {
User sessionUser = (User) session.getAttribute("sessionUser");
BoardResponse.DetailDTO detailDTO = boardService.게시글상세화면및인가처리(id, sessionUser);
model.addAttribute("board", detailDTO);
return "board/update-form";
}
@PostMapping("/board/{id}/update")
public String updateProc(@PathVariable(name = "id") Integer id,
BoardRequest.UpdateDTO updateDTO, HttpSession session) {
User sessionUser = (User) session.getAttribute("sessionUser");
updateDTO.validate();
boardService.게시글수정(id, updateDTO, sessionUser);
return "redirect:/board/" + id;
}
}
BoardService 코드( 한글 메서드 명으로 전부 수정 )⬇️
더보기
package com.tenco.blog.board;
import com.tenco.blog._core.errors.Exception403;
import com.tenco.blog._core.errors.Exception404;
import com.tenco.blog.user.User;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.stream.Collectors;
/**
* 서비스 레이어
* 핵심 개념 :
* 1. 서비스 레이어의 역할:
* - 비즈니스 로직을 처리하는 계층
* - Controller와 Response 사이에서 중간 계층 담당
* - 트랜잭션 관리
* - 여러 Repository를 조합해서 복잡한 비즈니스 로직 처리
* <p>
* 2. 계층 구조 (3Tier 아키텍처)
* Controller -> Service -> Repository-> DB
* <p>
* 3. @Service 어노테이션 사용
* - Spring이 이 어노테이션을 확인해서 Bean(빈) 등록 한다.
*/
@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class BoardService {
private final BoardRepository boardRepository;
/**
* 게시글 목록 조회
* OSIV false 환경 대응 - 응답 DTO 설계
*/
public List<BoardResponse.ListDTO> 게시글목록() {
log.info("게시글 목록 조회 서비스");
List<Board> boardList = boardRepository.findAllJoinUser();
log.info("게시글 목록 조회 완료 - 총 : {}", boardList.size());
return boardList.stream()
.map(BoardResponse.ListDTO::new) // map 닫기
.collect(Collectors.toList());
}
/**
* 게시글 상세 조회
* @param id (Board PK)
* @return DetailDTO 처리 (OSIV 대응)
*/
public BoardResponse.DetailDTO 게시글상세조회(Integer id) {
log.info("게시글 상세 조회 서비스");
// N + 1 문제를 해결하기 위해 한번에 Board, User 가지고 옴
Board boardEntity = boardRepository.findByIdJoinUser(id).orElseThrow(() -> {
log.warn("게시글 조회 실패 - ID: {}", id);
return new Exception404("해당하는 게시글을 찾을 수 없습니다");
});
log.info("게시글 조회 완료 - 제목: {}, 작성자: {}",
boardEntity.getTitle(), boardEntity.getUser().getUsername());
return new BoardResponse.DetailDTO(boardEntity);
}
/**
* 게시글 작성
* @param saveDTO
* @param sessionUser (세션에서 가져온 사용자 정보)
*/
@Transactional
public void 게시글작성(BoardRequest.SaveDTO saveDTO, User sessionUser) {
log.info("게시글 저장 서비스 시작 - 제목 : {}, 작성자 : {}",
saveDTO.getTitle(), sessionUser.getUsername());
Board board = saveDTO.toEntity(sessionUser);
Board savedBoardEntity = boardRepository.save(board);
log.info("게시글 저장 완료 - ID : {}, 제목 : {}",
savedBoardEntity.getId(), savedBoardEntity.getTitle());
}
/**
* 게시글 상세 화면 요청(인가 처리)
*
* @param id (Board PK)
* @param sessionUser (로그인한 사용자 정보)
* @return BoardResponse.DetailDTO
*/
public BoardResponse.DetailDTO 게시글상세화면및인가처리(Integer id, User sessionUser) {
log.info("게시글 상세 화면 및 인가 확인");
BoardResponse.DetailDTO detailDTO = 게시글상세조회(id);
if(!detailDTO.getUserId().equals(sessionUser.getId())) {
throw new Exception403("권한없음");
}
log.info("게시글 수정 조회 완료 - 제목: {}, 작성자: {}",
detailDTO.getTitle(), detailDTO.getUsername());
return detailDTO;
}
/**
* 게시글 수정 기능 처리
* @param id (Board PK)
* @param updateDTO
* @param sessionUser
* @return
*/
@Transactional
public void 게시글수정(Integer id, BoardRequest.UpdateDTO updateDTO, User sessionUser) {
log.info("게시글 수정 서비스");
Board boardEntity = boardRepository.findByIdJoinUser(id).orElseThrow(() -> {
throw new Exception404("해당 게시글을 찾을 수 없습니다");
});
// 영속화 되어 있었던 객체의 title, content 의 내용이 변경 됨.
boardEntity.update(updateDTO);
log.info("게시글 수정 완료 - ID : {}, 새 제목: {}",
boardEntity.getId(), boardEntity.getTitle());
}
/**
* 게시글 삭제 요청
* @param id (Board PK)
* @param sessionUser
*/
@Transactional
public void 게시글삭제(Integer id, User sessionUser) {
log.info("게시글 삭제 서비스");
Board boardEntity = boardRepository.findById(id).orElseThrow(
() -> new Exception404("게시글을 찾을 수 없습니다")
);
boardEntity.isOwner(sessionUser.getId());
boardRepository.deleteById(id);
log.info("게시글 삭제 완료 - ID : {}", id);
}
}
list.mustache 코드⬇️
더보기
{{> layout/header}}
<div class="container p-5 flex-grow-1">
{{#boardList}}
<div class="card mb-3">
<div class="card-body">
<h4 class="card-title mb-3">{{title}}</h4>
<!-- 머스태치 문법은 getter 메서드를 가지고 오고 get은 탈락 가능 -->
<div> 작성자 : {{username}}| 작성일 : {{createdAt}}</div>
<a href="/board/{{id}}" class="btn btn-primary">상세보기</a>
</div>
</div>
{{/boardList}}
<ul class="pagination d-flex justify-content-center">
<li class="page-item"><a href="" class="page-link">Previous</a></li>
<li class="page-item"><a href="" class="page-link"> Next</a></li>
</ul>
</div>
{{> layout/footer}}
update-form.mustache 코드⬇️
더보기
{{> layout/header}}
<div class="container p-5 flex-grow-1">
<div class="card">
<div class="card-header"><b>글수정 화면입니다</b></div>
<div class="card-body">
<!-- 익명 게시글 작성 -->
<form action="/board/{{id}}/update" method="post">
<div class="mb-3">
<input type="text" name="username" class="form-control"
placeholder="enter username" value="{{board.username}}" readonly>
</div>
<div class="mb-3">
<input type="text" name="title" class="form-control"
placeholder="enter title" value="{{board.title}}">
</div>
<div class="mb-3">
<textarea type="text" name="content" rows="5" class="form-control" placeholder="enter title">
{{board.content}}
</textarea>
</div>
<button class="btn btn-primary">글수정 완료</button>
</form>
</div>
</div>
</div>
{{> layout/footer}}
UserResponse 코드⬇️
더보기
package com.tenco.blog.user;
import lombok.Data;
public class UserResponse {
@Data
public static class JoinDTO {
private Integer id;
private String username;
private String email;
public JoinDTO(User user) {
this.id = user.getId();
this.username = user.getUsername();
this.email = user.getEmail();
}
} // end of JoinDTO inner class
@Data
public static class LoginDTO {
private Integer id;
private String username;
private String email;
// password 처럼 민감한 정보는 내려주지 않는다.
public LoginDTO(User user) {
this.id = user.getId();
this.username = user.getUsername();
this.email = user.getEmail();
}
} // end of LoginDTO inner class
@Data
public static class UpdateFormDTO {
private Integer id;
private String username;
private String email;
public UpdateFormDTO(User user) {
this.id = user.getId();
this.username = user.getUsername();
this.email = user.getEmail();
}
}
} // end of outer class
package com.tenco.blog.user;
import lombok.Data;
public class UserResponse {
@Data
public static class JoinDTO {
private Integer id;
private String username;
private String email;
public JoinDTO(User user) {
this.id = user.getId();
this.username = user.getUsername();
this.email = user.getEmail();
}
} // end of JoinDTO inner class
@Data
public static class SessionDTO extends User {
// password 처럼 민감한 정보는 내려주지 않는다.
public SessionDTO(User user) {
super.setId(user.getId());
super.setUsername(user.getUsername());
super.setEmail(user.getEmail());
}
} // end of LoginDTO inner class
} // end of outer class
UserController코드⬇️
더보기
package com.tenco.blog.user;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Slf4j
@Controller // IoC
@RequiredArgsConstructor // DI 처리
public class UserController {
private final UserService userService;
// 프로필 수정 기능 요청
@PostMapping("/user/update")
public String updateProc(UserRequest.UpdateDTO updateDTO, HttpSession session) {
updateDTO.validate();
UserResponse.SessionDTO sessionUser = (UserResponse.SessionDTO) session.getAttribute("sessionUser");
userService.회원정보수정(sessionUser.getId(), updateDTO, session);
return "redirect:/";
}
// 프로필 화면 요청
@GetMapping("/user/update-form")
public String updateFormPage(HttpSession session, Model model) {
UserResponse.SessionDTO sessionUser = (UserResponse.SessionDTO) session.getAttribute("sessionUser");
UserResponse.SessionDTO sessionDTO = userService.회원정보수정화면(sessionUser.getId());
model.addAttribute("user", sessionDTO);
return "user/update-form";
}
// 로그인 화면 요청
// 주소 설계 - http://localhost:8080/login-form
@GetMapping("/login-form")
public String loginFormPage() {
// 인증 검사 x , 유효성 x
return "user/login-form";
}
// 로그인 기능 요청
@PostMapping("/login")
public String loginProc(UserRequest.LoginDTO reqLoginDTO, HttpSession session) {
// 인증 검사 x, 유효성 검사 o
reqLoginDTO.validate();
UserResponse.SessionDTO sessionDTO = userService.로그인(reqLoginDTO);
session.setAttribute("sessionUser", sessionDTO);
return "redirect:/";
}
// 로그아웃 기능 요청
@GetMapping("/logout")
public String logout(HttpSession session) {
// 세션 메모리에 내 정보를 없애 버림
session.invalidate();
return "redirect:/";
}
// 회원 가입 화면 요청
// 주소 설계 - http://localhost:8080/join-form
@GetMapping("/join-form")
public String joinFormPage() {
return "user/join-form";
}
// 회원 가입 기능 요청
// 주소 설계 - http://localhost:8080/join
@PostMapping("/join")
public String joinProc(UserRequest.JoinDTO joinDTO) {
// 인증검사 x, 유효성 검사 하기 o
joinDTO.validate();
userService.회원가입(joinDTO);
return "redirect:/login-form";
}
}
UserService 코드⬇️
더보기
package com.tenco.blog.user;
import com.tenco.blog._core.errors.Exception400;
import com.tenco.blog._core.errors.Exception404;
import jakarta.servlet.http.HttpSession;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* User 관련 비즈니스 로직을 처리하는 Service 계층
* Controller 와 Repository 사이에서 실제 업무 로직을 담당
*/
@Slf4j
@Service // IoC
@RequiredArgsConstructor // DI
@Transactional(readOnly = true) // 기본적인 읽기 전용 트랜잭션 처리 , 조회시 더티 체킹 안 일어남
public class UserService {
private final UserRepository userRepository;
/**
* 회원 가입 처리
* @param joinDTO (사용자 회원가입 요청 정보)
* @return User (저장된 사용자 정보)
*/
@Transactional
public UserResponse.JoinDTO 회원가입(UserRequest.JoinDTO joinDTO) {
log.info("회원가입 서비스 시작");
userRepository.findByUsername(joinDTO.getUsername()).ifPresent(user -> {
log.warn("회원가입 실패 - 중복된 사용자명 : {}", user.getUsername());
throw new Exception400("이미 존재하는 사용자 이름입니다");
});
User user = joinDTO.toEntity();
User savedUserEntity = userRepository.save(user);
log.info("회원 가입 서비스 완료 - id : {}", savedUserEntity.getId());
return new UserResponse.JoinDTO(savedUserEntity);
}
/**
* 로그인 처리
* @param loginDTO (사용자가 요청한 로그인 정보)
* @return User(조회된 정보 세션 저장용)
*/
public UserResponse.SessionDTO 로그인(UserRequest.LoginDTO loginDTO) {
log.info("로그인 서비스 시작");
User userEntity = userRepository.findByUsernameAndPassword(loginDTO.getUsername(), loginDTO.getPassword())
.orElseThrow(() -> {
log.warn("로그인 실패 - 사용자 이름 또는 사용자 비번 잘못 입력");
return new Exception400("사용자명 또는 비밀번호가 올바르지 않습니다");
});
log.info("로그인 성공 - 사용자명 : {} ", loginDTO.getUsername());
return new UserResponse.SessionDTO(userEntity);
}
/**
* 사용자 정보 조회 (프로필 정보 보기 활용)
* @param id (User PK)
* @return UserEntity
*/
public UserResponse.SessionDTO 회원정보수정화면(Integer id) {
log.info("사용자 정보 서비스 시작");
User userEntity = userRepository.findById(id).orElseThrow(() -> {
log.warn("사용자 정보 조회 실패");
return new Exception404("사용자 정보를 찾을 수 없습니다");
});
return new UserResponse.SessionDTO(userEntity);
}
/**
* 사용자 정보 수정 처리 (프로필 업데이트)
* @param id (User PK)
* @param updateDTO (사용자가 요청한 데이터)
* @return User
*/
@Transactional
public UserResponse.SessionDTO 회원정보수정(Integer id, UserRequest.UpdateDTO updateDTO, HttpSession session) {
log.info("회원정보 서비스 시작");
User userEntity = userRepository.findById(id).orElseThrow(
() -> new Exception404("사용자 정보를 찾을 수 없습니다"));
// 더티 체킹 활용
userEntity.update(updateDTO);
log.info("회원정보 수정 완료 - 사용 ID : {}", userEntity.getId());
UserResponse.SessionDTO sessionDTO = new UserResponse.SessionDTO(userEntity);
// 세션 동기화 처리
session.setAttribute("sessionUser", sessionDTO);
return sessionDTO;
}
}
update-form.mustache 코드⬇️
더보기
{{> layout/header}}
<div class="container p-5 flex-grow-1">
<div class="card">
<div class="card-header"><b>회원수정을해주세요</b></div>
<div class="card-body">
<form action="/user/update" method="post">
<div class="mb-3">
<input type="text" class="form-control" name="username" value="{{user.username}}" disabled>
</div>
<div class="mb-3">
<input type="password" class="form-control" name="password" value="" >
</div>
<div class="mb-3">
<input type="text" class="form-control" name="email" value="{{user.email}}" disabled>
</div>
<button class="btn btn-primary form-control">회원정보수정</button>
</form>
</div>
</div>
</div>
{{> layout/footer}}
GlobalExceptionHandler 코드⬇️
더보기
package com.tenco.blog._core.errors;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestControllerAdvice;
// 모든 컨트롤러에서 발생하는 예외를 이 클래스에서 처리 하겠다.
// RuntimeException 이 발생되면 해당 이 파일로 예외 처리가 오게 됨.
@Slf4j
@ControllerAdvice // IoC -> 에러 페이지 찾아 가능 녀석
// @RestControllerAdvice // 에러를 데이터로 반환할 때 사용
public class GlobalExceptionHandler {
@ExceptionHandler(Exception400.class)
public String ex400(Exception400 e, HttpServletRequest request) {
log.warn("=== 400 Bad Request 에러 발생 ===");
log.warn("요청 URL: {}", request.getRequestURL());
log.warn("에러메시지: {}", e.getMessage());
request.setAttribute("msg", e.getMessage());
return "err/400";
}
@ExceptionHandler(Exception401.class)
public String ex401(Exception401 e, HttpServletRequest request) {
log.warn("=== 401 Unauthorized 에러 발생 ===");
log.warn("요청 URL: {}", request.getRequestURL());
log.warn("에러메시지: {}", e.getMessage());
request.setAttribute("msg", e.getMessage());
return "err/401";
}
// @ExceptionHandler(Exception403.class)
// public String ex403(Exception403 e, HttpServletRequest request) {
// log.warn("=== 403 Forbidden 에러 발생 ===");
// log.warn("요청 URL: {}", request.getRequestURL());
// log.warn("에러메시지: {}", e.getMessage());
//
// request.setAttribute("msg", e.getMessage());
// return "err/403";
// }
@ExceptionHandler(Exception403.class)
@ResponseBody // 파일 찾지 말고 데이터 반환
public String ex403(Exception403 e, HttpServletRequest request) {
// String script = "<script>alert(' " + e.getMessage() + " ');" +
// "history.back();" +
// "</script>";
String script = """
<script>
alert('%s');
history.back();
</script>
""".formatted(e.getMessage());
return script;
}
@ExceptionHandler(Exception404.class)
public String ex404(Exception404 e, HttpServletRequest request) {
log.warn("=== 404 Not Found 에러 발생 ===");
log.warn("요청 URL: {}", request.getRequestURL());
log.warn("에러메시지: {}", e.getMessage());
request.setAttribute("msg", e.getMessage());
return "err/404";
}
@ExceptionHandler(Exception500.class)
public String ex500(Exception500 e, HttpServletRequest request) {
log.warn("=== 500 Internal Server Error 에러 발생 ===");
log.warn("요청 URL: {}", request.getRequestURL());
log.warn("에러메시지: {}", e.getMessage());
request.setAttribute("msg", e.getMessage());
return "err/500";
}
// 기타 모든 RuntimeException 처리 (최후의 보루)
@ExceptionHandler(RuntimeException.class)
public String handleRuntimeException(RuntimeException e, HttpServletRequest request) {
log.warn("=== 예상치 못한 런타임 에러 발생 ===");
log.warn("요청 URL: {}", request.getRequestURL());
log.warn("에러메시지: {}", e.getMessage());
request.setAttribute("msg", "시스템 오류가 발생했습니다. 관리자에게 문의해주세요");
return "err/500";
}
}
'Spring boot 입문' 카테고리의 다른 글
| 스프링부트 V7 댓글 기능 추가 (User 도메인 리팩토링 ) (0) | 2026.05.20 |
|---|---|
| V6-2 expose-session-attributes 의 false 사용 (0) | 2026.05.20 |
| V5-2 서비스 계층 만들어 보기 (BoardService, UserService) + 리팩토링 (0) | 2026.05.20 |
| 스프링부트 V5 JPARepository + Service(JpaRepository 인터페스와 Optional 타입) (0) | 2026.05.19 |
| Spring Boot 필수 어노테이션(Annotation) 총정리 (0) | 2026.05.18 |
