일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
- 코드로배우는스프링웹프로젝트
- 이터레이터
- GIT
- d
- 자료구조와 함께 배우는 알고리즘 입문
- 네트워크 설정
- 티스토리 쿠키 삭제
- /etc/network/interfaces
- 자료구조와함께배우는알고리즘입문
- 친절한SQL튜닝
- 선형대수
- 리눅스
- 구멍가게코딩단
- 서버설정
- resttemplate
- 데비안
- 알파회계
- iterator
- 스프링부트핵심가이드
- 스프링 시큐리티
- 자바편
- 코드로배우는스프링부트웹프로젝트
- 목록처리
- 처음 만나는 AI 수학 with Python
- baeldung
- network configuration
- 처음 만나는 AI수학 with Python
- 페이징
- Kernighan의 C언어 프로그래밍
- ㅒ
- Today
- Total
bright jazz music
blog12: 예외처리 3 본문
그런데 원래대로라면 InvalidRequest를 ExceptionController에 추가해 줘야한다.그러나 애플리케이션이 커지면서 예외도 늘어나게 된다. 따라서 다른 방법을 사용해 본다.
- 자바에서 기본 제공해주는 예외들은 기존처럼 따로 만들어준다.
- 그리고 자바에서 제공되지는 않지만 애플리케이션 운영에서 발생하는 공통적으로 처리해야 하는 예외들은 하나의 클래스를 만들어 대응한다.
프로그램 전체에서 사용할 Exception클래스를 하나 만들어서 그것으로 하여금 에러들을 처리하게 하는 것이다. 여기서는 그 BlogException이라는 추상 클래스를 생성하여 그것으로 하여금 모든 에러를 처리하게 할 것이다. 앞선 예제들에서는 각각의 에러 클래스들이 RuntimeException 클래스를 상속하였다. 이제부터는 그 클래스들이 BlogException을 상속한다. 그리고 BlogException만이 RuntimeException을 상속한다.
따라서 PostNotFound 또는 InvalidRequest의 생성자에 들어가는 String 타입의 메시지는 super()를 통해 BlogException을 거쳐 RuntimeException 까지 전달된다.
//BlogException.java
package com.endofma.blog.exception;
public abstract class BlogException extends RuntimeException{
public BlogException(String message){
super(message);
}
public BlogException(String message, Throwable cause){
super(message, cause);
}
//추상메서드: 반드시 구현 필요
public abstract int getStatusCode();
}
//PostNotFound.java
//status -> 404
package com.endofma.blog.exception;
//public class PostNotFound extends Exception{
//public class PostNotFound extends RuntimeException{ //uncheckedException
public class PostNotFound extends BlogException{ //BlogException은 RuntimeException을 상속했다.
//생성자 오버로딩을 해서 실제로 발생한 예외에게 메시지를 부여하면 된다.
private static final String MESSAGE = "PostNotFound=>존재하지 않는 글입니다.";
public PostNotFound(){
super(MESSAGE); //이 메시지는 BlogException(String message)를 초기화한다.
}
public PostNotFound(Throwable cause){
super(MESSAGE, cause);
}
@Override
public int getStatusCode() {
return 404;
}
}
//InvalidRequest.java
//status -> 400
package com.endofma.blog.exception;
//public class InvalidRequest extends RuntimeException {
public class InvalidRequest extends BlogException {
private static final String MESSAGE ="잘못된 요청입니다.";
public InvalidRequest() {
super(MESSAGE);
}
//글 작성 등에서 글 작성에서 @NotBlank와 같은 어노테이션으로
//해결할 수 없을 경우 추가적인 방법이 필요할 때.
//PostController의 post()에 보내는 요청의 제목에 "바보"라는 글자를 넣을 수 없다면.
@Override
public int getStatusCode() {
return 400;
}
}
//PostController.java
@PostMapping("/posts")
public void post(@RequestBody @Valid PostCreate request) {
//PostController의 post()에 보내는 요청의 제목에 "바보"라는 글자를 넣을 수 없다면.
if(request.getTitle().contains("바보")){
throw new InvalidRequest();
}
postService.write(request);
// return Map.of();
}
예외를 처리할 수 있도록 ExceptionController에 등
//ExceptionController.java
package com.endofma.blog.controller;
import com.endofma.blog.exception.BlogException;
import com.endofma.blog.exception.InvalidRequest;
import com.endofma.blog.exception.PostNotFound;
import com.endofma.blog.response.ErrorResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Map;
@Slf4j
@ControllerAdvice
public class ExceptionController {
@ResponseStatus(HttpStatus.BAD_REQUEST) //400
@ResponseBody //응답을 json으로 보내기 위해 추가
@ExceptionHandler(MethodArgumentNotValidException.class)
public ErrorResponse invalidRequestHandler(MethodArgumentNotValidException e) {
// ErrorResponse response = new ErrorResponse("400", "잘못된 요청입니다!");
ErrorResponse response = ErrorResponse.builder()
.code("400")
.message("잘못된 요청입니다!")
.build();
for(FieldError fieldError : e.getFieldErrors()){
response.addValidation(fieldError.getField(), fieldError.getDefaultMessage());
}
return response;
}
@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;
}
// @ResponseBody
// @ResponseStatus(HttpStatus.NOT_FOUND)
// @ExceptionHandler(PostNotFound.class)
// public ErrorResponse invalidRequest(InvalidRequest e) {
// ErrorResponse response = ErrorResponse.builder()
// .code("404")
// .message(e.getMessage()) //익셉션으로 받은 에러 메시지
// .build();
//
// return response;
// }
/////BlogException을 사용하여 에러처리
@ResponseBody
// @ResponseStatus(HttpStatus.NOT_FOUND) //헤더스테이터스. 동적으로 변경하기 어려움.
//빼면 기본값 200으로 내려감
@ExceptionHandler(BlogException.class)
//BlogException을 상속하는 에러클래스가 호출되었을 경우 무조건 이 메소드가 호출된다.
public ResponseEntity<ErrorResponse> blogException(BlogException e) {
//ResponseEntity는 스프링에서 기본적으로 제공하는 객체로 반환한다.
int statusCode = e.getStatusCode();
ErrorResponse body = ErrorResponse.builder()
.code(String.valueOf(statusCode)) //String.valuOf()사용해서 int를 String으로 변경
.message(e.getMessage())
.build();
ResponseEntity<ErrorResponse> response = ResponseEntity.status(statusCode)
.body(body);
return response;
}
}
@ResponseStatus()를 제거한 것을 볼 수 있다. 저기에 헤더 상태가 정해져 있으면 아래 코드에서 변경하더라도 효과가 없다. 그러면 @ResponseStatus() 안의 값을 변경해 주면 되지 않느냐고 반문할 수 있다. 물론 그렇게 할 순 있지만 안에서 로직이 복잡해질 수도 있고 바뀔 수도 있기 때문에 애노테이션을 사용해 고정하는 것은 권장하지 않는다.
문제는 @RequestStatus가 없으면 기본으로 200을 내려준다는 것이다. 이를 방지하기 위해 ResponseEntity<>로 ErrorResponse를 감싸서 반환한다.
에러를 코드를 매번 수정해 줄 수 없으므로 에러 코드를 에러 클래스로부터 직접 가져와서 넣어주는 로직을 추가하였다.
int statusCode = e.getStatusCode(); , String.valueOf(statusCode)
이를 추가히 위해 BlogException에 아래와 같은 추상 메서드를 추가하였다.
//BlogException.java
//추상메서드: 반드시 구현 필요
public abstract int getStatusCode();
따라서 이 클래스를 상속한 클래스에서는 이 메소드 구현한다
//PostNotFound.java
@Override
public int getStatusCode() {
return 404;
}
//InvalidRequest.java
@Override
public int getStatusCode() {
return 400;
}
이를 테스트 하면
//PostControllerTest.java
@Test
@DisplayName("게시글 작성 시 제목에 '바보'는 포함될 수 없다.")
void test11() throws Exception{
//given
PostCreate request = PostCreate.builder()
.title("나는 바보입니다.")
.content("제이드빌.")
.build();
String json = objectMapper.writeValueAsString(request);
//when
mockMvc.perform(post("/posts")
.contentType(APPLICATION_JSON)
.content(json))
.andExpect(status().isBadRequest())
.andDo(print());
}
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.6)
2023-01-14 22:12:08.180 INFO 38940 --- [ Test worker] c.e.blog.controller.PostControllerTest : Starting PostControllerTest using Java 11.0.12 on DESKTOP-Q7HBM41 with PID 38940 (started by user in D:\personal\blog)
2023-01-14 22:12:08.183 INFO 38940 --- [ Test worker] c.e.blog.controller.PostControllerTest : No active profile set, falling back to 1 default profile: "default"
2023-01-14 22:12:09.194 INFO 38940 --- [ Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2023-01-14 22:12:09.272 INFO 38940 --- [ Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 67 ms. Found 1 JPA repository interfaces.
2023-01-14 22:12:09.977 INFO 38940 --- [ Test worker] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2023-01-14 22:12:10.312 INFO 38940 --- [ Test worker] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2023-01-14 22:12:10.410 INFO 38940 --- [ Test worker] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2023-01-14 22:12:10.488 INFO 38940 --- [ Test worker] org.hibernate.Version : HHH000412: Hibernate ORM core version 5.6.14.Final
2023-01-14 22:12:10.756 INFO 38940 --- [ Test worker] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2023-01-14 22:12:10.956 INFO 38940 --- [ Test worker] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2023-01-14 22:12:11.831 INFO 38940 --- [ Test worker] o.h.e.t.j.p.i.JtaPlatformInitiator : HHH000490: Using JtaPlatform implementation: [org.hibernate.engine.transaction.jta.platform.internal.NoJtaPlatform]
2023-01-14 22:12:11.846 INFO 38940 --- [ Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2023-01-14 22:12:12.714 WARN 38940 --- [ Test worker] JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default. Therefore, database queries may be performed during view rendering. Explicitly configure spring.jpa.open-in-view to disable this warning
2023-01-14 22:12:13.164 INFO 38940 --- [ Test worker] o.s.b.a.h2.H2ConsoleAutoConfiguration : H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:blog'
2023-01-14 22:12:13.663 INFO 38940 --- [ Test worker] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
2023-01-14 22:12:13.664 INFO 38940 --- [ Test worker] o.s.t.web.servlet.TestDispatcherServlet : Initializing Servlet ''
2023-01-14 22:12:13.665 INFO 38940 --- [ Test worker] o.s.t.web.servlet.TestDispatcherServlet : Completed initialization in 1 ms
2023-01-14 22:12:13.698 INFO 38940 --- [ Test worker] c.e.blog.controller.PostControllerTest : Started PostControllerTest in 6.05 seconds (JVM running for 8.927)
MockHttpServletRequest:
HTTP Method = POST
Request URI = /posts
Parameters = {}
Headers = [Content-Type:"application/json;charset=UTF-8", Content-Length:"61"]
Body = {"title":"나는 바보입니다.","content":"제이드빌."}
Session Attrs = {}
Handler:
Type = com.endofma.blog.controller.PostController
Method = com.endofma.blog.controller.PostController#post(PostCreate)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = com.endofma.blog.exception.InvalidRequest
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 400
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"code":"400","message":"잘못된 요청입니다.","validation":{}}
Forwarded URL = null
Redirected URL = null
Cookies = []
2
--
지금까지는 타이틀에 "바보"가 있는지 controller에서 체크했다. 그러나 이렇게 꺼내서 로직을 구현하는 것보다는 호출하고 메시지만 받아서 처리하는 것을 권장한다.
//PostController.java
@PostMapping("/posts")
public void post(@RequestBody @Valid PostCreate request) {
//PostController의 post()에 보내는 요청의 제목에 "바보"라는 글자를 넣을 수 없다면.
// if(request.getTitle().contains("바보")){
// throw new InvalidRequest();
// } 이렇게 데이터를 꺼내서 분리해서 재조립하는 방식보다는 아래처럼 메시지를 통해 가져오는 방식 권장
request.validate(); //검증
postService.write(request);
// return Map.of();
}
//PostCreate.java
package com.endofma.blog.request;
import com.endofma.blog.exception.InvalidRequest;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;
import javax.validation.constraints.NotBlank;
@Setter
@Getter
@ToString
public class PostCreate {
@NotBlank(message = "타이틀을 입력하세요!")
//@Valid를 @RequestBody 옆에 @valid를 붙임. 그러면 바인딩 되면서 빈 값이 넘어오면 에러발생시킴.
private String title;
@NotBlank(message = "내용을 입력하세요!")
private String content;
@Builder //클래스에 달면 다른 생성자와 모순이 발생할 수 있기 때문에 생성자에 다는 것을 추천. builder + NoArgsConstructor 에러
public PostCreate(String title, String content) {
this.title = title;
this.content = content;
}
//빌더의 장점
// - 가독성 up (값 생성에 대한 유연함)
// - 필요한 값만 받을 수 있다. // -> (오버로딩 가능한 조건 찾아보기)
// - 객체의 불변성
public void validate() {
if(title.contains("바보")){
throw new InvalidRequest();
}
//boolean을 통해 검증 실패 시 false, 성공시 true를 컨트롤러로 반환하고
//컨트롤러에서 에러를 생성해 줄 수도 있지만 사실 그럴 필요도 없다.
//여기서 검증하고 예외를 죽이는 게 더 효율적이기 때문이다.
//이런 역할을 하는 라이브러리를 사용해도 된다.
}
}
당연히 테스트 결과는 같다.
'Projects > blog' 카테고리의 다른 글
blog13: Spring REST Docs 1 -기본설정 (0) | 2023.01.15 |
---|---|
blog12: 예외처리 4 (0) | 2023.01.14 |
blog12: 예외처리2 (0) | 2023.01.13 |
blog12: 예외처리1 (0) | 2023.01.10 |
blog11: 게시글 삭제 (0) | 2023.01.10 |