최원종의 개발 블로그

스프링부트 V6- OSIV 본문

Spring boot 입문

스프링부트 V6- OSIV

chl6698 2026. 5. 20. 15:33

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";
    }
}