12-2. (실습) 스프링 V3 (예외처리)

박은서's avatar
Feb 24, 2026
12-2. (실습) 스프링 V3 (예외처리)

1. 예외(Exception)처리

notion image

1) GlobalExceptionHandler.java

① 코드

C:\workspace\spring_lab\boardv1\src\main\java\com\example\boardv1\_core\errors\GlobalExceptionHandler.java
package com.example.boardv1._core.errors; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(exception = RuntimeException.class) public String ex(RuntimeException e) { return e.getMessage(); } }

② 테스트

notion image

2) GlobalExceptionHandler.java - 수정(알림창 띄우기)

① 코드

package com.example.boardv1._core.errors; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(exception = RuntimeException.class) // 어떤 예외인지 지정하기 public String ex(RuntimeException e) { String html = String.format(""" <script> alert('%s'); history.back(); </script> """, e.getMessage()); return html; } }

② 테스트

확인 누르면 다시 이전 화면으로 돌아감
확인 누르면 다시 이전 화면으로 돌아감

3) Exception400.java

notion image
C:\workspace\spring_lab\boardv1\src\main\java\com\example\boardv1\_core\errors\ex\Exception400.java
package com.example.boardv1._core.errors.ex; // 유효성검사 실패시 / 중복 public class Exception400 extends RuntimeException { public Exception400(String message) { super(message); } }

4) Exception401.java

C:\workspace\spring_lab\boardv1\src\main\java\com\example\boardv1\_core\errors\ex\Exception401.java
package com.example.boardv1._core.errors.ex; // 인증 실패시 public class Exception401 extends RuntimeException { public Exception401(String message) { super(message); } }

5) Exception403.java

C:\workspace\spring_lab\boardv1\src\main\java\com\example\boardv1\_core\errors\ex\Exception403.java
package com.example.boardv1._core.errors.ex; // 권한 실패시 public class Exception403 extends RuntimeException { public Exception403(String message) { super(message); } }

6) Exception404.java

C:\workspace\spring_lab\boardv1\src\main\java\com\example\boardv1\_core\errors\ex\Exception404.java
package com.example.boardv1._core.errors.ex; // 자원을 찾을 수 없을 때 public class Exception404 extends RuntimeException { public Exception404(String message) { super(message); } }

7) Exception500.java

C:\workspace\spring_lab\boardv1\src\main\java\com\example\boardv1\_core\errors\ex\Exception500.java
package com.example.boardv1._core.errors.ex; // 서버측 에러(미리 설계할 수 없음) -> 만들어놨다가 나중에 예외가 터지면 잡으면 됨 public class Exception500 extends RuntimeException { public Exception500(String message) { super(message); } }

8) GlobalExceptionHandler.java - 수정(Exception 종류별로 메서드 만들기)

package com.example.boardv1._core.errors; import org.springframework.web.bind.annotation.ExceptionHandler; import org.springframework.web.bind.annotation.RestControllerAdvice; import com.example.boardv1._core.errors.ex.Exception400; import com.example.boardv1._core.errors.ex.Exception401; import com.example.boardv1._core.errors.ex.Exception403; import com.example.boardv1._core.errors.ex.Exception404; import com.example.boardv1._core.errors.ex.Exception500; @RestControllerAdvice public class GlobalExceptionHandler { @ExceptionHandler(exception = Exception400.class) // 어떤 예외인지 지정하기 public String ex400(Exception400 e) { String html = String.format(""" <script> alert('%s'); history.back(); </script> """, e.getMessage()); return html; } @ExceptionHandler(exception = Exception401.class) // 어떤 예외인지 지정하기 public String ex401(Exception401 e) { String html = String.format(""" <script> alert('%s'); location.href = '/login-form'; </script> """, e.getMessage()); return html; } @ExceptionHandler(exception = Exception403.class) // 어떤 예외인지 지정하기 public String ex403(Exception403 e) { String html = String.format(""" <script> alert('%s'); history.back(); </script> """, e.getMessage()); // 나중에는 log도 남겨야 함 return html; } @ExceptionHandler(exception = Exception404.class) // 어떤 예외인지 지정하기 public String ex404(Exception404 e) { String html = String.format(""" <script> alert('%s'); history.back(); </script> """, e.getMessage()); return html; } @ExceptionHandler(exception = Exception500.class) // 어떤 예외인지 지정하기 public String ex500(Exception500 e) { String html = String.format(""" <script> alert('%s'); history.back(); </script> """, e.getMessage()); return html; } @ExceptionHandler(exception = Exception.class) // 어떤 예외인지 지정하기 public String exUnknown(Exception e) { String html = String.format(""" <script> alert('%s'); history.back(); </script> """, "관리자에게 문의하세요"); System.out.println("error : " + e.getMessage()); // 1. 로그 // 2. SMS 알림 -> e.getMessage() return html; } }

2. 기존 코드 수정 (예외 처리 반영)

1) UserService.java - 예외 처리 수정

package com.example.boardv1.user; import java.util.Optional; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.example.boardv1._core.errors.ex.Exception400; import com.example.boardv1._core.errors.ex.Exception401; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor @Service public class UserService { private final UserRepository userRepository; @Transactional public void 회원가입(String username, String password, String email) { // 1. username 중복 체크 Optional<User> optUser = userRepository.findByUsername(username); if (optUser.isPresent()) throw new Exception400("유저네임이 중복되었습니다"); // 정확히는 409! 일단 400으로 처리 // 2. 비영속 객체 User user = new User(); user.setUsername(username); user.setPassword(password); user.setEmail(email); // 3. save() 호출 userRepository.save(user); } public User 로그인(String username, String password) { User findUser = userRepository.findByUsername(username) .orElseThrow(() -> new Exception401("username을 찾을 수 없어요")); if (!findUser.getPassword().equals(password)) throw new Exception401("패스워드가 일치하지 않아요"); return findUser; } }

2) BoardController.java - 예외 처리 수정

package com.example.boardv1.board; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.ResponseBody; import com.example.boardv1._core.errors.ex.Exception401; import com.example.boardv1.reply.ReplyRequest; import com.example.boardv1.user.User; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor // final이 적혀있는 애로 생성자 만들어줌 @Controller // @Controller 적어야 리턴값이 파일이 됨 (외부진입점) public class BoardController { private final BoardService boardService; private final HttpSession session; // title : title=title7&content=content7 (x-www-form) @PostMapping("/boards/save") public String save(BoardRequest.SaveOrUpdateDTO reqDTO){ // new해서 넣어줌! 필드가 많을 때 상태만 추가하면 되기 때문에 매우 편함! 재사용 가능!(필드명 잘 적어야 함) // 인증(v) 권한(x) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new Exception401("인증되지 않았습니다."); boardService.게시글쓰기(reqDTO.getTitle(), reqDTO.getContent(), sessionUser.getId()); return "redirect:/"; } // body : title=제목&content=내용 @PostMapping("/boards/{id}/update") public String update(@PathVariable("id") int id, BoardRequest.SaveOrUpdateDTO reqDTO){ // 인증(v) 권한(v) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new Exception401("인증되지 않았습니다."); boardService.게시글수정(id,reqDTO.getTitle(), reqDTO.getContent(), sessionUser.getId()); return "redirect:/boards/"+id; } @GetMapping("/") public String index(HttpServletRequest req){ List<Board> list = boardService.게시글목록(); req.setAttribute("models", list); return "index"; } @GetMapping("/boards/save-form") public String saveForm(){ // 인증(v) 권한(x) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new Exception401("인증되지 않았습니다."); return "board/save-form"; } @GetMapping("/boards/{id}/update-form") public String updateForm(@PathVariable("id") int id, HttpServletRequest req){ // 인증(v) 권한(v) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new Exception401("인증되지 않았습니다."); Board board = boardService.수정폼게시글정보(id, sessionUser.getId()); req.setAttribute("model", board); return "board/update-form"; } @GetMapping("/boards/{id}") public String detail(@PathVariable("id") int id, HttpServletRequest req){ User sessionUser = (User) session.getAttribute("sessionUser"); // Object타입으로 반환하기 때문에 다운캐스팅해야 함 Integer sessionUserId = sessionUser == null ? null : sessionUser.getId(); BoardResponse.DetailDTO dto = boardService.상세보기(id, sessionUserId); req.setAttribute("model", dto); return "board/detail"; // mustache 파일의 경로 } @PostMapping("/boards/{id}/delete") public String delete(@PathVariable("id") int id){ // 인증(v) 권한(v) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new Exception401("인증되지 않았습니다."); boardService.게시글삭제(id, sessionUser.getId()); return "redirect:/"; } @GetMapping("/api/boards/{id}") public @ResponseBody BoardResponse.DetailDTO apiDetail(@PathVariable("id") int id) { User sessionUser = (User) session.getAttribute("sessionUser"); Integer sessionUserId = sessionUser == null ? null : sessionUser.getId(); BoardResponse.DetailDTO dto = boardService.상세보기(id, sessionUserId); return dto; } }

3) BoardService.jave - 예외 처리 수정

package com.example.boardv1.board; import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.example.boardv1._core.errors.ex.Exception403; import com.example.boardv1._core.errors.ex.Exception404; import com.example.boardv1.reply.Reply; import com.example.boardv1.user.User; import com.example.boardv1.user.UserRepository; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; // 책임 : 트랜잭션 관리 + DTO 만들기 + 권한 체크(DB정보가 필요하기 때문) @RequiredArgsConstructor @Service public class BoardService { private final BoardRepository boardRepository; private final EntityManager em; public List<Board> 게시글목록() { return boardRepository.findAll(); } public Board 수정폼게시글정보(int id, int sessionUserId) { Board board = boardRepository.findById(id) .orElseThrow(() -> new Exception404("게시글을 찾을 수 없어요")); // 권한 if (sessionUserId != board.getUser().getId()) throw new Exception403("수정할 권한이 없습니다"); return board; } public BoardResponse.DetailDTO 상세보기(int id, Integer sessionUserId) { Board board = boardRepository.findById(id) .orElseThrow(() -> new Exception404("게시글을 찾을 수 없어요")); return new BoardResponse.DetailDTO(board, sessionUserId); } @Transactional // update, delete, insert 할때 붙이세요!! public void 게시글수정(int id, String title, String content, int sessionUserId) { Board board = boardRepository.findById(id) .orElseThrow(() -> new Exception404("수정할 게시글을 찾을 수 없어요")); // 권한 if (sessionUserId != board.getUser().getId()) { throw new Exception403("수정할 권한이 없습니다"); } board.setTitle(title); board.setContent(content); } // 원자성(모든게 다되면 commit, 하나라도 실패하면 rollback) // 트랜잭션 종료시 flush 됨. @Transactional public void 게시글쓰기(String title, String content, int sessionUserId) { User user = em.getReference(User.class, sessionUserId); // 1. 비영속 객체 Board board = new Board(); board.setTitle(title); board.setContent(content); board.setUser(user); System.out.println("before persist " + board.getId()); // 2. persist boardRepository.save(board); System.out.println("after persist " + board.getId()); } @Transactional public void 게시글삭제(int id, int sessionUserId) { // 영속화 Board board = boardRepository.findById(id) .orElseThrow(() -> new Exception404("삭제할 게시글을 찾을 수 없어요")); // 권한 if (sessionUserId != board.getUser().getId()) throw new Exception403("삭제할 권한이 없습니다"); boardRepository.delete(board); } // 자동 flush }

4) 테스트

① 중복된 username으로 회원 가입

notion image

② 로그인 시 username 잘못 입력

notion image

③ 로그인 시 password 잘못 입력

notion image

④ 다른 유저가 등록한 게시글 수정 요청

notion image

⑥ 등록되지 않은 게시글 요청

notion image

3. 댓글이 등록된 게시물 삭제

문제 상황

댓글이 등록된 게시물은 삭제 불가 상태!
notion image
notion image

1) BoardController.java - Exception500으로 처리

package com.example.boardv1.board; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.ResponseBody; import com.example.boardv1._core.errors.ex.Exception401; import com.example.boardv1._core.errors.ex.Exception500; import com.example.boardv1.reply.ReplyRequest; import com.example.boardv1.user.User; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpSession; import lombok.RequiredArgsConstructor; @RequiredArgsConstructor // final이 적혀있는 애로 생성자 만들어줌 @Controller // @Controller 적어야 리턴값이 파일이 됨 (외부진입점) public class BoardController { private final BoardService boardService; private final HttpSession session; // title : title=title7&content=content7 (x-www-form) @PostMapping("/boards/save") public String save(BoardRequest.SaveOrUpdateDTO reqDTO){ // new해서 넣어줌! 필드가 많을 때 상태만 추가하면 되기 때문에 매우 편함! 재사용 가능!(필드명 잘 적어야 함) // 인증(v) 권한(x) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new Exception401("인증되지 않았습니다."); boardService.게시글쓰기(reqDTO.getTitle(), reqDTO.getContent(), sessionUser.getId()); return "redirect:/"; } // body : title=제목&content=내용 @PostMapping("/boards/{id}/update") public String update(@PathVariable("id") int id, BoardRequest.SaveOrUpdateDTO reqDTO){ // 인증(v) 권한(v) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new Exception401("인증되지 않았습니다."); boardService.게시글수정(id,reqDTO.getTitle(), reqDTO.getContent(), sessionUser.getId()); return "redirect:/boards/"+id; } @GetMapping("/") public String index(HttpServletRequest req){ List<Board> list = boardService.게시글목록(); req.setAttribute("models", list); return "index"; } @GetMapping("/boards/save-form") public String saveForm(){ // 인증(v) 권한(x) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new Exception401("인증되지 않았습니다."); return "board/save-form"; } @GetMapping("/boards/{id}/update-form") public String updateForm(@PathVariable("id") int id, HttpServletRequest req){ // 인증(v) 권한(v) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new Exception401("인증되지 않았습니다."); Board board = boardService.수정폼게시글정보(id, sessionUser.getId()); req.setAttribute("model", board); return "board/update-form"; } @GetMapping("/boards/{id}") public String detail(@PathVariable("id") int id, HttpServletRequest req){ User sessionUser = (User) session.getAttribute("sessionUser"); // Object타입으로 반환하기 때문에 다운캐스팅해야 함 Integer sessionUserId = sessionUser == null ? null : sessionUser.getId(); BoardResponse.DetailDTO dto = boardService.상세보기(id, sessionUserId); req.setAttribute("model", dto); return "board/detail"; // mustache 파일의 경로 } @PostMapping("/boards/{id}/delete") public String delete(@PathVariable("id") int id){ // 인증(v) 권한(v) // 인증 User sessionUser = (User) session.getAttribute("sessionUser"); if (sessionUser == null) throw new Exception401("인증되지 않았습니다."); try { boardService.게시글삭제(id, sessionUser.getId()); } catch (Exception e) { throw new Exception500("댓글이 있는 게시글을 삭제할 수 없습니다"); } return "redirect:/"; } @GetMapping("/api/boards/{id}") public @ResponseBody BoardResponse.DetailDTO apiDetail(@PathVariable("id") int id) { User sessionUser = (User) session.getAttribute("sessionUser"); Integer sessionUserId = sessionUser == null ? null : sessionUser.getId(); BoardResponse.DetailDTO dto = boardService.상세보기(id, sessionUserId); return dto; } }
notion image

2) 오류 조치

① 조치방법

notion image
💡
reply는 board에 의존하고 있기 때문에
6번 board에 의존 중인 3개의 reply의 board_id를 null로 update하면 삭제 가능

② BoardService.java - 수정(게시글 삭제할 때 댓글 업데이트)

package com.example.boardv1.board; import java.util.List; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.example.boardv1._core.errors.ex.Exception403; import com.example.boardv1._core.errors.ex.Exception404; import com.example.boardv1._core.errors.ex.Exception500; import com.example.boardv1.reply.Reply; import com.example.boardv1.user.User; import com.example.boardv1.user.UserRepository; import jakarta.persistence.EntityManager; import lombok.RequiredArgsConstructor; // 책임 : 트랜잭션 관리 + DTO 만들기 + 권한 체크(DB정보가 필요하기 때문) @RequiredArgsConstructor @Service public class BoardService { private final BoardRepository boardRepository; private final EntityManager em; public List<Board> 게시글목록() { return boardRepository.findAll(); } public Board 수정폼게시글정보(int id, int sessionUserId) { Board board = boardRepository.findById(id) .orElseThrow(() -> new Exception404("게시글을 찾을 수 없어요")); // 권한 if (sessionUserId != board.getUser().getId()) throw new Exception403("수정할 권한이 없습니다"); return board; } public BoardResponse.DetailDTO 상세보기(int id, Integer sessionUserId) { Board board = boardRepository.findById(id) .orElseThrow(() -> new Exception404("게시글을 찾을 수 없어요")); return new BoardResponse.DetailDTO(board, sessionUserId); } @Transactional // update, delete, insert 할때 붙이세요!! public void 게시글수정(int id, String title, String content, int sessionUserId) { Board board = boardRepository.findById(id) .orElseThrow(() -> new Exception404("수정할 게시글을 찾을 수 없어요")); // 권한 if (sessionUserId != board.getUser().getId()) { throw new Exception403("수정할 권한이 없습니다"); } board.setTitle(title); board.setContent(content); } // 원자성(모든게 다되면 commit, 하나라도 실패하면 rollback) // 트랜잭션 종료시 flush 됨. @Transactional public void 게시글쓰기(String title, String content, int sessionUserId) { User user = em.getReference(User.class, sessionUserId); // 1. 비영속 객체 Board board = new Board(); board.setTitle(title); board.setContent(content); board.setUser(user); System.out.println("before persist " + board.getId()); // 2. persist boardRepository.save(board); System.out.println("after persist " + board.getId()); } @Transactional public void 게시글삭제(int id, int sessionUserId) { // 영속화 (EAGER전략 -> join) Board board = boardRepository.findById(id) .orElseThrow(() -> new Exception404("삭제할 게시글을 찾을 수 없어요")); // 권한 if (sessionUserId != board.getUser().getId()) throw new Exception403("삭제할 권한이 없습니다"); board.getReplies().forEach(r -> { r.setBoard(null); }); boardRepository.delete(board); } // 자동 flush }

③ 테스트

6번 게시물 삭제 완료
6번 게시물 삭제 완료
notion image
 
Share article