Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
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 |
Tags
- 목록처리
- GIT
- 자바편
- 처음 만나는 AI 수학 with Python
- iterator
- resttemplate
- 리눅스
- 서버설정
- d
- 코드로배우는스프링웹프로젝트
- /etc/network/interfaces
- network configuration
- Kernighan의 C언어 프로그래밍
- 이터레이터
- 친절한SQL튜닝
- 티스토리 쿠키 삭제
- 페이징
- 자료구조와함께배우는알고리즘입문
- 데비안
- ㅒ
- baeldung
- 구멍가게코딩단
- 선형대수
- 스프링 시큐리티
- 네트워크 설정
- 처음 만나는 AI수학 with Python
- 자료구조와 함께 배우는 알고리즘 입문
- 스프링부트핵심가이드
- 코드로배우는스프링부트웹프로젝트
- 알파회계
Archives
- Today
- Total
bright jazz music
blog12: 예외처리 4 본문
현재 예외에 대한 응답은 아래와 같이 반환되고 있다.
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 = []
요청의 성공과 실패 여부만 나타낼 뿐, 검증에 관한 이유를 밝히고 있지는 않다. 지금까지 우리가 작성한 코드 중에서는 validation에 값을 넣어 준 적이 없기 때문이다. 따라서 이번에는 validation 필드에 값을 추가하여 반환하는 기능을 구현한다.
비효율적인 방법과 효율적인 방법이 있다.
비효율적인 방법을 먼저 해본다.
PostController로 PostCreate 타입의 요청이 올 것이니까 추가.
//PostCreate.java
public void validate() {
if(title.contains("바보")){
throw new InvalidRequest("title", "제목에 바보를 포함살 수 없습니다.");
}
// InvalidRequest 생성자에 메시지를 넣어줌.
}
}
//InvalidRequest.java
//status -> 400
package com.endofma.blog.exception;
import lombok.Getter;
//public class InvalidRequest extends RuntimeException {
@Getter
public class InvalidRequest extends BlogException {
private static final String MESSAGE ="잘못된 요청입니다.";
//final을 붙이면 오류 발생.
// 인자를 받지 않는 생성자가 있으므로 초기화가 안될 수 있기 때문.
public String fieldName;
public String message;
public InvalidRequest() {
super(MESSAGE);
}
public InvalidRequest(String fieldName, String message){
super(MESSAGE);
this.fieldName = fieldName;
this.message = message;
}
//글 작성 등에서 글 작성에서 @NotBlank와 같은 어노테이션으로
//해결할 수 없을 경우 추가적인 방법이 필요할 때.
//PostController의 post()에 보내는 요청의 제목에 "바보"라는 글자를 넣을 수 없다면.
@Override
public int getStatusCode() {
return 400;
}
}
//ExceptionController.java
/////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();
//응답 json validation필드에 값 추가. title을 키값으로.
//title: 제목에 바보를 포함할 수 없습니다.
if (e instanceof InvalidRequest){
InvalidRequest invalidRequest = (InvalidRequest) e;
String fieldName = invalidRequest.getFieldName();
String message = invalidRequest.getMessage();
body.addValidation(fieldName, message);
}
ResponseEntity<ErrorResponse> response = ResponseEntity.status(statusCode)
.body(body);
return response;
}
테스트 11
MockHttpServletResponse:
Status = 400
Error message = null
Headers = [Content-Type:"application/json"]
Content type = application/json
Body = {"code":"400","message":"제목에 바보를 포함살 수 없습니다.","validation":{"title":"제목에 바보를 포함살 수 없습니다."}}
Forwarded URL = null
Redirected URL = null
Cookies = []
그런데 이런 방법은 비효율적이다. 나중엔 if문이 너무 많아질 수도 있고 로직이 계속해서 추가되면 코드 가독성이 떨어질 수도 있다.
두 번째 방법.
//PostController.java
package com.endofma.blog.controller;
import com.endofma.blog.domain.Post;
import com.endofma.blog.exception.InvalidRequest;
import com.endofma.blog.request.PostCreate;
import com.endofma.blog.request.PostEdit;
import com.endofma.blog.request.PostSearch;
import com.endofma.blog.response.PostResponse;
import com.endofma.blog.service.PostService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.domain.Pageable;
import org.springframework.data.web.PageableDefault;
import org.springframework.validation.BindingResult;
import org.springframework.validation.FieldError;
import org.springframework.web.bind.annotation.*;
import javax.validation.Valid;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Slf4j
@RestController
@RequiredArgsConstructor
public class PostController {
//에러는 ExceptionController에서 처리한다.
private final PostService postService;
@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();
}
//단건 조회
@GetMapping("/posts/{postId}")
public PostResponse get(@PathVariable Long postId){
PostResponse response = postService.get(postId);
return response;
}
//여러 글 조회(글 목록 가져오기)
// /posts
@GetMapping("/posts")
//글이 너무 많은 경우 비용이 너무 많이 든다.
//DB가 뻗을 수 있음.
//DB -> 애플리케이션 서버로 전달하는 시간, 트래픽 비용이 많이 발생할 수 있다.
//따라서 페이지 설정
// 원래는 int로 받았음
// public List<PostResponse> getList(@RequestParam int page){
// 그러나 사용의 용이성을 위해 pageable을 사용함
// public List<PostResponse> getList(@PageableDefault Pageable pageable){ //1로 넘겨도 0으로 보정해서 넣어줌.
//근데 PageableDefault의 기본 size가 10이라 yml에서 설정해도 먹히지 않는다.
//이 떄는 어노테이션을 그대로 유지하면서 size를 파라미터로 넣어주는 방법이 있다.
//public List<PostResponse> getList(@PageableDefault(size=10) Pageable pageable){
//또는 어노테이션을 빼고 application.yml에서 default-page-size를 설정하여 해결할 수 수있다.
//전에는 Pageable을 사용했으나 여러 요구사항을 수용 할 수 있는 클래스를 사용하기 위해
//postSearch 클래스 사용
public List<PostResponse> getList(@ModelAttribute PostSearch postSearch){//따로 만든 요청클래스 사용하려고 함
// public List<PostResponse> getList(Pageable pageable){
return postService.getList(postSearch);
}
@PatchMapping("/posts/{postId}")
public void edit(@PathVariable Long postId, @RequestBody @Valid PostEdit request){
postService.edit(postId, request);
}
@DeleteMapping("/posts/{postId}")
public void delete(@PathVariable Long postId){
postService.delete(postId);
}
}
//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("title", "제목에 바보를 포함살 수 없습니다.");
}
//boolean을 통해 검증 실패 시 false, 성공시 true를 컨트롤러로 반환하고
//컨트롤러에서 에러를 생성해 줄 수도 있지만 사실 그럴 필요도 없다.
//여기서 검증하고 예외를 죽이는 게 더 효율적이기 때문이다.
//이런 역할을 하는 라이브러리를 사용해도 된다.
}
}
//InvalidRequest.java
//status -> 400
package com.endofma.blog.exception;
import lombok.Getter;
//public class InvalidRequest extends RuntimeException {
@Getter
public class InvalidRequest extends BlogException {
private static final String MESSAGE ="잘못된 요청입니다.";
public InvalidRequest() {
super(MESSAGE);
}
public InvalidRequest(String fieldName, String message){
super(MESSAGE);
addValidation(fieldName, message); //생성 시점에 설정. BlogException으로부터 상속
}
//글 작성 등에서 글 작성에서 @NotBlank와 같은 어노테이션으로
//해결할 수 없을 경우 추가적인 방법이 필요할 때.
//PostController의 post()에 보내는 요청의 제목에 "바보"라는 글자를 넣을 수 없다면.
@Override
public int getStatusCode() {
return 400;
}
}
//BlogException.java
package com.endofma.blog.exception;
import lombok.Getter;
import java.util.HashMap;
import java.util.Map;
@Getter
public abstract class BlogException extends RuntimeException{
//validation값 추가 위해 작성
public final Map<String, String> validation = new HashMap<>();
public BlogException(String message){
super(message);
}
public BlogException(String message, Throwable cause){
super(message, cause);
}
//추상메서드: 반드시 구현 필요
public abstract int getStatusCode();
public void addValidation(String fieldName, String message){
this.validation.put(fieldName, message);
}
}
//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())
//응답 json validation필드에 값 추가. title을 키값으로. title: 제목에 바보를 포함할 수 없습니다.
.validation(e.getValidation())
.build();
ResponseEntity<ErrorResponse> response = ResponseEntity.status(statusCode)
.body(body);
return response;
}
}
//ErrorResponse.java
package com.endofma.blog.response;
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Getter;
import lombok.RequiredArgsConstructor;
import java.util.HashMap;
import java.util.Map;
/*
* {
* "code": "400",
* "message": "잘못된 요청입니다."
* "validation": {
* "title": "값을 입력해주세요"
* }
* }
*/
@Getter //게터가 없으면 값을 가져올 수 없어서 ResponseBody에 넣어주질 못함.(빈 바디)
//@RequiredArgsConstructor
//@JsonInclude(value = JsonInclude.Include.NON_EMPTY) //비어있지 않은 데이터만 보낸다.
public class ErrorResponse {
private final String code;
private final String message;
private final 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){
// ValidationTuple validationTuple = new ValidationTuple(fieldName, errorMessage);
this.validation.put(fieldName, errorMessage);
}
// Map을 안 쓰기 위해서 내부 클래스를 만들었으니 이걸로 위의 Map을 대체해 보자..현재는 안 되어 있음
// @RequiredArgsConstructor
// private class ValidationTuple{
// private final String fieldName;
// private final String errorMessage;
//}
}
. ____ _ __ _ _
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
' |____| .__|_| |_|_| |_\__, | / / / /
=========|_|==============|___/=/_/_/_/
:: Spring Boot :: (v2.7.6)
2023-01-14 23:37:53.307 INFO 36980 --- [ Test worker] c.e.blog.controller.PostControllerTest : Starting PostControllerTest using Java 11.0.12 on DESKTOP-Q7HBM41 with PID 36980 (started by user in D:\personal\blog)
2023-01-14 23:37:53.309 INFO 36980 --- [ Test worker] c.e.blog.controller.PostControllerTest : No active profile set, falling back to 1 default profile: "default"
2023-01-14 23:37:54.311 INFO 36980 --- [ Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2023-01-14 23:37:54.390 INFO 36980 --- [ Test worker] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 68 ms. Found 1 JPA repository interfaces.
2023-01-14 23:37:55.122 INFO 36980 --- [ Test worker] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Starting...
2023-01-14 23:37:55.464 INFO 36980 --- [ Test worker] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Start completed.
2023-01-14 23:37:55.583 INFO 36980 --- [ Test worker] o.hibernate.jpa.internal.util.LogHelper : HHH000204: Processing PersistenceUnitInfo [name: default]
2023-01-14 23:37:55.671 INFO 36980 --- [ Test worker] org.hibernate.Version : HHH000412: Hibernate ORM core version 5.6.14.Final
2023-01-14 23:37:55.981 INFO 36980 --- [ Test worker] o.hibernate.annotations.common.Version : HCANN000001: Hibernate Commons Annotations {5.1.2.Final}
2023-01-14 23:37:56.191 INFO 36980 --- [ Test worker] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
2023-01-14 23:37:57.084 INFO 36980 --- [ 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 23:37:57.095 INFO 36980 --- [ Test worker] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2023-01-14 23:37:57.959 WARN 36980 --- [ 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 23:37:58.390 INFO 36980 --- [ Test worker] o.s.b.a.h2.H2ConsoleAutoConfiguration : H2 console available at '/h2-console'. Database available at 'jdbc:h2:mem:blog'
2023-01-14 23:37:58.815 INFO 36980 --- [ Test worker] o.s.b.t.m.w.SpringBootMockServletContext : Initializing Spring TestDispatcherServlet ''
2023-01-14 23:37:58.815 INFO 36980 --- [ Test worker] o.s.t.web.servlet.TestDispatcherServlet : Initializing Servlet ''
2023-01-14 23:37:58.816 INFO 36980 --- [ Test worker] o.s.t.web.servlet.TestDispatcherServlet : Completed initialization in 1 ms
2023-01-14 23:37:58.855 INFO 36980 --- [ Test worker] c.e.blog.controller.PostControllerTest : Started PostControllerTest in 6.07 seconds (JVM running for 8.924)
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":{"title":"제목에 바보를 포함살 수 없습니다."}}
Forwarded URL = null
Redirected URL = null
Cookies = []
2023-01-14 23:37:59.576 INFO 36980 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
2023-01-14 23:37:59.577 INFO 36980 --- [ionShutdownHook] .SchemaDropperImpl$DelayedDropActionImpl : HHH000477: Starting delayed evictData of schema as part of SessionFactory shut-down'
2023-01-14 23:37:59.581 WARN 36980 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 90121, SQLState: 90121
2023-01-14 23:37:59.581 ERROR 36980 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper : Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-214]
2023-01-14 23:37:59.582 WARN 36980 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper : SQL Error: 90121, SQLState: 90121
2023-01-14 23:37:59.583 ERROR 36980 --- [ionShutdownHook] o.h.engine.jdbc.spi.SqlExceptionHelper : Database is already closed (to disable automatic closing at VM shutdown, add ";DB_CLOSE_ON_EXIT=FALSE" to the db URL) [90121-214]
2023-01-14 23:37:59.583 WARN 36980 --- [ionShutdownHook] o.s.b.f.support.DisposableBeanAdapter : Invocation of destroy method failed on bean with name 'entityManagerFactory': org.hibernate.exception.JDBCConnectionException: Unable to release JDBC Connection used for DDL execution
2023-01-14 23:37:59.584 INFO 36980 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown initiated...
2023-01-14 23:37:59.590 INFO 36980 --- [ionShutdownHook] com.zaxxer.hikari.HikariDataSource : HikariPool-1 - Shutdown completed.
'Projects > blog' 카테고리의 다른 글
blog13: Spring REST Docs2 - 요청, 응답필드 (0) | 2023.01.16 |
---|---|
blog13: Spring REST Docs 1 -기본설정 (0) | 2023.01.15 |
blog12: 예외처리 3 (0) | 2023.01.14 |
blog12: 예외처리2 (0) | 2023.01.13 |
blog12: 예외처리1 (0) | 2023.01.10 |
Comments