JPA

SpringBoot JPA 게시판 CRUD 구현(예외 처리)

빈코 2022. 7. 27. 14:39

오늘은 지금까지 구현했던 WAS 단 로직들 안에서 예외처리들을 커스터마이징 하려 합니다. 로직 수정에 앞서 예외(Exception)오류(Error)에 대한 간단한 개념을 먼저 알아보겠습니다😀

 

로고
Exception

에러(Error)📙

컴퓨터 하드웨어의 오동작 또는 고장으로 인해 응용프로그램 실행 오류가 발생하는 것을 Error라고 합니다. Error는 JVM 실행에 문제가 생긴 것으로, 프로그램은 실행 불능이 됩니다. 즉, 시스템이 종료되어야 할 수준의 상황과 같이 수습할 수 없는 심각한 문제를 의미합니다.

 

📌Error는 개발자가 미리 예측하여 방지할 수 없습니다.

 

예외(Exception)📒

자바에서는 에러 이외에 예외라고 부르는 오류가 있습니다. 예외란 사용자의 잘못된 조작 또는 개발자의 잘못된 코딩으로 인해 발생하는 오류입니다. 예외가 발생되면 프로그램이 종료된다는 점에서 에러와 동일하지만, 예외는 예외처리(Exception Handling)를 통해 프로그램을 종료하지 않고 정상 실행 상태를 유지할 수 있습니다.

 

📌오류와 달리 개발자가 미리 예측하여 방지할 수 있습니다.

 

조금 더 부연 설명을 하자면, 예외 처리는 흔히 아는 방법으로 try/catch문, if문으로 잡든 상위 메서드로 위임하든 간에 코드는 복잡해질 수밖에 없습니다. 더욱 안정화된 프로그램을 위해서 사소한 예외까지 잡다 보면 비즈니스 로직보다는 예외처리 로직이 더 많아질 가능성도 많습니다.

 

그렇기에 @ExceptionHandler@ControllerAdvice를 사용합니다.

 

  • @ExceptionHandler : @Controller와 @RestController가 적용된 Bean 내에서 발생하는 예외를 잡아서 하나의 메서드에서 처리해주는 기능
  • @ControllerAdvice : Controller 전역에서 발생하는 예외를 잡아 처리해주는 기능

 

예외 Controller📘

@Slf4j
@ControllerAdvice
public class ExceptionController {

    @ResponseBody
    @ResponseStatus(HttpStatus.NOT_FOUND)
    @ExceptionHandler(PostNotFound.class)
    public ErrorResponse postNotFound(PostNotFound e) {
        ErrorResponse response = ErrorResponse.builder()
                .code("404")
                .message(e.getMessage())
                .build();

        return response;
    }
}

 

ControllerAdvice 클래스를 생성하였습니다. @ExceptionHandler에 들어가는 PostNotFound 클래스는 포스팅 하단에서 구현 예정입니다. 지금은 눈으로 흐름만 보시면 될 것 같네요😄

 

게시판에서 가장 흔히 발생할 수 있는 예외는 찾는 게시글이 없을 경우입니다. 글을 조회하거나, 삭제, 수정 로직에서는 게시글을 찾는 로직은 무조건 들어갈 수 밖에 없습니다. 하지만 해당 글이 없을 경우에 예외처리가 없다면 서비스는 먹통이 돼버리고 말겠죠?

 

그렇기에 PostNotFound 클래스를 추후에 만들것이며, 지금은 그 클래스를 매개변수로 받아서 처리해주는 메서드를 구현한 것입니다. 

 

하지만, 예외처리는 프로그램이 커질수록 비례적으로 증가할 수밖에 없습니다. 그러면 ControllerAdvice 클래스에 수 없이 많은 예외처리를 등록해주어야 할 것입니다. 이런 방법은 비효율적이기에 가장 상위 레벨의 Exception을 만들고 하위 레벨들이 상속받아 구현되는 식으로 진행할 예정입니다.

 

커스텀 예외📗

위에서 언급한 가장 상위 레벨의 Exception을 만들어야 합니다. 예외의 종류 중 개발자가 직접 정의해서 만들어야 하는 사용자 정의 예외커스텀 예외라 칭합니다. 컴파일러가 체크하지 않는 실행 예외로 선언할 수 있는데, 이럴 경우 RuntimeException을 상속해야 합니다. 

 

@Getter
public abstract class BincologException extends RuntimeException{
    public final Map<String, String> validation = new HashMap<>();
    
    public BincologException(String message) {
        super(message);
    }

    public BincologException(String message, Throwable cause) {
        super(message, cause);
    }
    
    public abstract int getStatusCode();
    
    public void addValidation(String fieldName, String message){
        validation.put(fieldName,message);
    }
}

 

첫 번째로 exception이라는 directory를 만들어서 예외처리 관련 클래스는 exception 하위에 관리를 할 예정입니다. 그리고 주축이 되는 BincologExceptionabstract으로 만들고, 나중에 하위 예외처리 class들을 BincologException을 상속받아 만들 예정입니다.

 

생성자는 오류 메시지만 포함하는 생성자와, 원인을 같이 담을 수 있는 생성자 2개를 미리 만들어 두었습니다. Throwable 클래스는 모든 오류의 부모 클래스라고 생각하시면 됩니다.

 

그리고 getStatusCode 메소드를 통해 해당 예외가 어떤 코드인지 알 수 있게 구현하였습니다. 예를 들어  HTTP에서 파일을 찾지 못할 때 볼 수 있는 404 코드가 있겠죠?

 

마지막으로 addValidation은 추가적으로 예외처리를 할 수 있게 구현해 놓았습니다. 예를 들어 제목에 특정 단어를 포함할 수 없게 하고 싶을 때 예외를 발생시킬 수 있습니다.

 

📌상위 레벨의 Exception을 만들었기 때문에, ControllerAdvice 로직도 Exception의 상위 레벨인 BincologException에 대한 처리가 필요합니다.

 

@Slf4j
@ControllerAdvice
public class ExceptionController {

    @ResponseBody
    @ExceptionHandler(BincologException.class)
    public ResponseEntity<ErrorResponse> bincologException(BincologException e) {
        int statusCode = e.getStatusCode();

        ErrorResponse body = ErrorResponse.builder()
                .code(String.valueOf(statusCode))
                .message(e.getMessage())
                .validation(e.getValidation())
                .build();

        ResponseEntity<ErrorResponse> response = ResponseEntity.status(statusCode)
                .body(body);

        return response;
    }
}

한 가지 주의사항은 기존에는 @ResponseStatus를 통해 어떤 코드에 대응하는 메서드인지 헤더를 통해 알 수 있었습니다. 하지만 이렇게 통합적으로 코드를 구성하면 각각에 맞는 상태 코드들에 대한 로직을 작성할 수 없습니다.

 

그렇기에 Spring에서는 ResponseEntity를 기본적으로 제공해줍니다. status와 body를 다 담을 수 있는 Entity입니다. 

 

 

@ExceptionHandler는 당연하게 상위 레벨인 BincologException으로 잡고, 매개변수로 각각 상황에 맞는 에러들을 받습니다. (지금은 게시글을 못찾는 예외입니다.) 매개변수로 받은 예외 값에서 statusCode를 추출하고, ErrorResponse DTO에 맞게 Builder 패턴을 이용하여 객체를 생성합니다.

 

그리고 방금 전 언급한 ResponseEntity에 해당 값들을 담아 return 합니다.

 

저는 ErrorResponse상태값, 메시지, 유효성 검사를 넣었습니다. 유효성 검사는 추후에 특정 로직이 추가될 것을 고려하여 추가한 것이므로 생략하셔도 됩니다.

 

@Getter
public class ErrorResponse {
    private final String code;
    private final String message;
    private Map<String, String> validation;

    @Builder
    public ErrorResponse(String code, String message, Map<String, String> validation) {
        this.code = code;
        this.message = message;
        this.validation = validation;
    }

    public void addValidation(String fieldName, String errorMessage){
        this.validation.put(fieldName,errorMessage);
    }
}

 

하위 Exception 생성📚

하위 Exception은 예제가 너무 많아서 게시글이 없을 경우만 예로 들려고 합니다. 하위 예외는 아까 만든 상위 레벨의 예외 클래스를 상속받아 구현합니다.

 

public class PostNotFound extends BincologException{

    private static final String MESSAGE = "존재하지 않는 글입니다";

    public PostNotFound() {
        super(MESSAGE);
    }

    @Override
    public int getStatusCode() {
        return 404;
    }

}

 

📌단순 글의 유무만 판단하는 예외이기 때문에 아까 언급했던 유효성 검사인 validation은 들어가지 않습니다. 


Test Code📔

public void delete(Long id) {
    Post post = postRepository.findById(id)
            .orElseThrow(PostNotFound::new);

    postRepository.delete(post);
}

테스트 코드 작성에 앞서 위 코드는 PostService의 delete 메서드인데, 만약 이번 패캐지성 게시판 만들기를 따라 하신 분이라면 예외처리 부분이 PostNotFound가 아닌 RuntimeException으로 되어 있을 것입니다. 

 

게시글 삭제 로직 뿐만 아니라, 게시글을 찾아오는 모든 로직에서 RuntimeException이 아닌 PostNotFound로 변경해주셔야 합니다.

 

또한, 예제로는 게시글 삭제로만 테스트를 진행하지만 게시글 수정, 게시글 조회에 대한 테스트도 진행해 보시는 것을 추천드립니다😃

 

Service Test

@Test
@DisplayName("게시글 삭제 - 존재하지 않는 글")
void test8() {
    //expected
    Assertions.assertThrows(PostNotFound.class, () -> {
       postService.delete(1L);
    });
}

 

assertThrows를 이용하여 두 번째 인자값, 즉 게시글 삭제 메서드를 실행하였을 때 예외처리와 첫 번째 인자 값에 해당하는 예외처리가 동일한지 테스트를 진행합니다.

 

이전 포스팅을 보면 @BeforeEach를 통해 테스트를 실행할 때마다 DB를 지워서 아무런 게시글이 없는 상태이기 때문에, delete() 메서드에는 아무 값(1L)을 넣어주었습니다.

 

테스트 결과
성공!

 

Controller Test

@Test
@DisplayName("존재하지 않는 글 삭제")
void test9() throws Exception{
    //expected
    mockMvc.perform(MockMvcRequestBuilders.delete("/posts/{postId}", 1L)
                    .contentType(APPLICATION_JSON))
            .andExpect(status().isNotFound())
            .andDo(print());
}

Controller 테스트도 Service 테스트와 많이 유사하게 작성이 됩니다. MockMvc를 이용하여 url을 매핑해주고, 기대 값으로 상태 값이 404인지 체크해주었습니다.

 

테스트 결과
성공!

 


👨‍👩‍👦‍👦 오픈채팅방 운영

취업을 준비하는 예비 개발자분들을 위한 질문&답변할 수 있는 공간을 만들었습니다. 취업과 이직을 하기 위해서 어떤 걸 중점적으로 준비해야 하는지부터 포트폴리오&이력서 작성법 등 다양한 질문들을 받고 답변을 드립니다. 참여하셔서 다양한 정보 얻고 가시면 좋을 것 같네요😁

 

참여코드 : 456456

https://open.kakao.com/o/gVHZP8dg

 

비전공 개발자 취업 준비방(질문&답변)

#비전공 #개발자 #취업 #멘토링 #부트캠프 #국비지원 #백엔드 #프론트엔드 #중소기업 #중견기업 #자바 #Java #sql

open.kakao.com

 


👨‍💻 전자책 출간

아울러 제가  🌟비전공자에서 2년만에 보안 전문 중견기업으로 이직 한 방법들을 정리한 전자책을 출간 하게 되었습니다. 어떤 걸 공부해야 하는지, 이직을 위해서 무엇을 준비해야 하는지, 제가 받았던 기술 면접 리스트 등 다양한 목차로 구성되어 있습니다. 또한, 구매 시 1:1 채팅을 이용하여 포트폴리오 첨삭을 도와드리고 있습니다. 🐕전자책으로 얻은 모든 수익은 유기견 센터 '팅*벨 입양센터'에 후원될 예정입니다. 관심 있으신 분들은 아래 링크를 참고해주세요😁

https://kmong.com/gig/480954

 

비전공개발자 2년만에 중견기업 들어간 방법 | 14000원부터 시작 가능한 총 평점 0점의 전자책, 취

0개 총 작업 개수 완료한 총 평점 0점인 Binco의 전자책, 취업·이직 전자책 서비스를 0개의 리뷰와 함께 확인해 보세요. 전자책, 취업·이직 전자책 제공 등 14000원부터 시작 가능한 서비스

kmong.com


 

마치며

지금까지 JPA와 SpringBoot를 이용하여 게시판의 CRUD를 구현하였습니다. 마지막 예외처리 부분인 지금 포스팅이 설명하기 제일 어려웠던 만큼 이해하시기에 어려움이 있을 것 같다는 생각이 드네요. 혹여나 이해 안 가는 부분들은 댓글로 남겨주시면 아는 선에서 성심성의껏 답변 달아보도록 할게요 :)

 

이전 포스팅

* [ SpringBoot JPA 게시판 CRUD(Create) ]

* [ SpringBoot JPA 게시판 CRUD(Create-TDD) ]

* [ SpringBoot JPA 게시판 CRUD(Read 단건 조회 및 TDD) ]

* [ SpringBoot JPA 게시판 CRUD(Read 다중 조회 및 페이징 처리 / TDD) ]

* [ SpringBoot JPA 게시판 CRUD(Update-TDD) ]

* [ SpringBoot JPA 게시판 CRUD(Delete-TDD) ]

 

 

 

 

반응형