RuntimeException의 자식들
→ Exception400 ~ 500 (Handler 분기 시킴)
1. 상태코드
10X → wait : 서버가 바쁘거나 트래픽이 몰렸을 때. (프로세스는 계속 진행 상태)
20X → GOOD (201 = create) 성공적으로 요청됐으며, 서버가 새 리소스를 작성.
30X → 요청한 거 외에 다른걸 돌려줌 → 요청 완료를 위해 추가 작업 조치 필요 (Redirection)
40X → 클라이언트 오류 : 요청의 문법이 잘못됐거나 요청 처리 X
50X → 서버 오류 (에러 로그 남김 → why? 계속 모니터링 하는게 아니기에, 안하면 디버깅 힘듦)
1-1. 전체적인 구조
Throwable → getMessage();
Exception
RuntimeException
Exception400
2. Exception 관리
2-1. GlobalExceptionHandler
→ 모든 예외를 중앙에서 처리하여 일관된 응답 제공.
각 예외에 대해 별도로 작성된 핸들러 메소드에서 발생한 예외 처리
package shop.mtcoding.blog.core.error;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import shop.mtcoding.blog.core.error.ex.*;
import shop.mtcoding.blog.core.util.Script;
@RestControllerAdvice // 모든 throw가 이쪽으로 날라옴, 무조건 데이터로 응답
public class GlobalExceptionHandler {
// 유효성 검사 실패 (잘못된 클라이언트의 요청)
@ExceptionHandler(Exception400.class)
public String ex400(Exception e) {
return Script.back(e.getMessage());
}
// 인증 실패 (클라이언트가 인증 없이 요청했거나, 인증 도중 실패
@ExceptionHandler(Exception401.class)
public String ex401(Exception e) {
return Script.href("인증되지 않았습니다", "/login-form");
}
// 권한 실패 (인증은 되어있으나, 삭제하려는 게시글이 내가 적은 글이 아님)
@ExceptionHandler(Exception403.class)
public String ex403(Exception e) {
return Script.back(e.getMessage());
}
// 서버에서 리소스(자원)를 찾을 수 없을 때
@ExceptionHandler(Exception404.class)
public String ex404(Exception e) {
return Script.back(e.getMessage());
}
// 서버에서 심각한 오류 발생시 (알고 있을 때)
@ExceptionHandler(Exception500.class)
public String ex500(Exception e) {
return Script.back(e.getMessage());
}
// 서버에서 심각한 오류 발생시 (모를 때)
@ExceptionHandler(Exception.class)
public String ex(Exception e) {
return Script.back(e.getMessage());
}
}
2-2. Test 코드 (게시글 삭제 테스트)
→ 예외가 발생할 경우와 발생하지 않을 경우의 처리 확인
- try-catch 사용 테스트
@Test
public void deleteById_test() {
int id = 6;
try {
boardRepository.deleteById(id);
boardRepository.findById(id);
} catch (Exception e) {
Assertions.assertThat(e.getMessage()).isEqualTo("게시글 id를 찾을 수 없습니다");
}
}
// deleteById 메소드 호출 후, 해당 게시글이 삭제되었는지 확인하기 위해 findById 메소드 호출.
// 만약 게시글이 존재하지 않으면 Exception404가 발생해야 하며, 이를 try-catch 블록으로 잡아서 테스트.
결과 → 예외가 정상적으로 발생하고, 기대한 대로 메시지를 반환하는지 확인.
이는 예외 처리가 의도한 대로 동작하고 있음을 보여줌
- Service & Repository 레이어에서의 예외 처리 및 트랜잭션 관리
@Transactional
public void 게시글삭제(int id, User sessionUser) {
Board board = boardRepository.findById(id);
if (sessionUser.getId() != board.getUser().getId()) {
throw new Exception403("권한이 없습니다");
}
boardRepository.deleteById(id);
}
// '게시글삭제' 메소드는 먼저 게시글의 존재 여부를 확인하고, 권한이 있는지 검사한 후에 삭제 작업.
// 각 단계에서 조건이 만족되지 않으면 적절한 예외를 발생시킴
트랜잭션 관리 :
@Transactional
어노테이션을 사용하여 DB 작업이 실패할 경우,
롤백해서 데이터 일관성 유지.- 게시글 조회 Repository (
BoardRepository
의findById
메소드)
public Board findById(int id) {
Query query = em.createQuery("SELECT b FROM Board b JOIN fetch b.user u WHERE b.id = :id", Board.class);
query.setParameter("id", id);
try {
Board board = (Board) query.getSingleResult();
return board;
} catch (Exception e) {
e.printStackTrace();
throw new Exception404("게시글 id를 찾을 수 없습니다");
}
}
// 'findById' 메소드는 JPQL을 사용하여 게시글을 조회, 게시글이 존재하지 않으면 Exception404 발생 시킴.
예외 처리 :
catch
블록에서 예외 캐치 후, stackTrace를 출력 후 커스텀 예외를 다시 던짐.- Service 테스트
@Test
public void 게시글삭제_test() {
int id = 1;
User sessionUser = new User();
sessionUser.setId(2); // 권한이 없는 사용자
try {
boardService.게시글삭제(id, sessionUser);
} catch (Exception e) {
Assertions.assertThat(e).isInstanceOf(Exception403.class);
}
}
// '게시글삭제' 메소드가 사용자의 권한을 확인하여 권한이 없을 경우 Exception403이 발생하는지 테스트
- Repository 테스트
@Test
public void findById_test() {
int id = 1;
Assertions.assertThatThrownBy(() -> boardRepository.findById(id))
.isInstanceOf(Exception404.class)
.hasMessage("게시글 id를 찾을 수 없습니다");
}
// 'findById' 메소드가 존재하지 않는 게시글 조회시, 적절한 예외를 발생시키는지 확인
결론
- try-catch의 중요성
try-catch 블록은 예외 상황을 처리 및, 프로그램이 중단되지 않고 정상적으로 동작하도록 도움.
특히 테스트 과정에서는 예상되는 예외 상황 확인 후 코드가 제대로 작동하는지를 검증.
- 예외 처리 & 단위 테스트
각 예외 처리 로직을 단위 테스트와 연관 지어 테스트함으로써 코드의 안정성을 높임.
각 단계에서 발생할 수 있는 예외 상황을 고려하여 테스트 작성 후, 이를 통해 코드의 완성도를 높이는 것이 중요.
Share article