[Spring Data JPA Tutorial] 14. Swagger v3 상세설정 및 request 유효성 검증

2022. 4. 20. 17:46Spring/Spring Data JPA Tutorial

반응형
  1. 필요성
  2. OpenAPI doc 관련 Annotation
    1. operation 그룹 설정: @Tag
    2. Schema 설정 : @Schema
    3. 상세 정보 설정 : @Operation
    4. response 설정 : @ApiResponse
    5. parameter 설정 : @Parameter
  3. 기존 api에 적용하기
    1. @Tag 설정
    2. @Schema 및 유효성 검증 설정
    3. @Operation 설정
    4. @ApiResponse 설정
    5. @Parameter 설정
  4. Exception 처리 보완

1. 필요성

Swagger를 이용하여 API를 자동 웹문서화 하였고, Response로 받게될 Example Value 형태까지 맞췄봤습니다.

그러나, 아직 Swagger 페이지 만으로 api 기능을 한눈에 알아보기 어렵습니다.

04-4

각 operation들에 대한 부연설명도 없고, operation 묶음에 대한 설명 역시 쓰여진게 없어 API를 사용할 client 개발자가 이용하는데에 불편함이 있습니다.

springdoc-openapi 라이브러리에서는 각 operation(= 메서드)과 tag(= 메서드 묶음)에 대한 설명을 작성할 수 있도록 각종 annotation을 제공하고 있습니다.

이번시간에는, 다양한 OpenAPI 3 문서를 더욱 완성도 있게 꾸미는 방법을 알아볼 것입니다.


2. OpenAPI doc 관련 Annotation

2.1. api operation 그룹 설정

@Tag

Target : ANNOTATION_TYPE, METHOD, TYPE

  • name : 태그의 이름
  • description : 태그에 대한 설명

@Tag에 설정된 name이 같은 것 끼리 하나의 api 그룹으로 묶습니다.
주로 Controller Class 나 Controller Method 영역에 설정합니다.

import io.swagger.v3.oas.annotations.tags.Tag;

@Tag(name = "user", description = "사용자 API")
@RequiredArgsConstructor
@RequestMapping(path = "/api/users")
@RestController
public class UserController {
  ...
}
@Tag(name = "store", description = "가게 API")
@RequiredArgsConstructor
@RequestMapping(path = "/api/stores")
@RestController
public class StoreController {
	...
}

UserController와 StoreController에 @Tag를 설정해 보았습니다.


@Tag 애너테이션 설정을 완료한 후 Swagger UI 화면입니다.
태그명과 설명이 각 태그에 설정되었습니다.

01-1


2.2. api Schema 설정

@Schema

Target : ANNOTATION_TYPE, FIELD, METHOD, PARAMETER, TYPE

  • description : 한글명
  • defaultValue : 기본값
  • allowableValues : 허용가능한 값(열거형으로 정의가능할 경우 설정합니다)

Schema에 대한 정보를 작성하는 곳입니다.
Schema를 설정할 시, 더욱 완성도 높은 Swagger UI 문서를 만들 수 있습니다.

아래 그림은 @Schema 설정을 하지 않은 UserRequest 입니다.

01-2

필드에 대한 설명도 부족하고, 들어갈 수 있는 값에 대한 정보가 너무 부족합니다.

입력해야할 값에 대한 정의를 @Schema에 설정하는것이 api 문서의 완성도를 높이는 길입니다.

@Schema 를 javax.validation.constraints 와 함께 사용할 경우, api 입력값에대한 다양한 유효성 검증을 간단하게 처리를 할 수 있습니다.

아래의 코드는 UserRequest의 type 필드값에 설정한 @Schema 정의 예시입니다.

@NotBlank
@Pattern(regexp = "^(BASIC|OWNER)$")
@Schema(description = "타입", defaultValue = "BASIC", allowableValues = {"BASIC", "OWNER"})
private String type;

아래 그림은 @Schema 및 기타 유효성 검증을 적용한 UserRequest 입니다.
설정하는 방법은 아래에서 더 자세히 다룰 것입니다.

04-5


2.3. api 상세 정보 설정 : @Operation

@Operation

Target : ANNOTATION_TYPE, METHOD

  • summary : api에 대한 간략 설명
  • description : api에 대한 상세 설명
  • responses : api Response 리스트
  • parameters : api 파라미터 리스트

애너테이션으로 api 동작에 대한 명세를 작성하는 애너테이션으로, Controller method에 설정합니다.

Swagger UI가 fold상태일때도 간략히 확인할 수 있는 간략정보는 summary에 작성하고, 필요에 따라 상세 정보를 표기하고자 한다면 description에 설명을 추가하면 됩니다.

responses는 아래에서 설명할 @ApiResponse 리스트들 설정하는 요소입니다.
parameters는 path, query, header, cookie 등의 형태로 들어오는 파라미터에 대한 정보를 설정하는 요소입니다.

아래 코드는 summary와 description을 설정한 가게 상세 조회 api입니다.

@Operation(summary = "가게 상세 조회", description = "가게의 상세정보와 해당 가게를 보유하고 있는 회원 정보를 조회합니다.")
@GetMapping("/{id}")
public DataResponse<StoreData.Store> select(@PathVariable("id") long id) {
    return new DataResponse<>(storeService.select(id));
}

@Operation 설정 후, Swagger 웹문서에 메서드에 대한 설명이 추가되었습니다.

04-6


2.4. api response 설정 : @ApiResponse

@ApiResponse

Target : ANNOTATION_TYPE, METHOD, TYPE

  • responseCode : http 상태코드
  • description : response에 대한 설명
  • content : Response payload 구조
    • schema : payload에서 이용하는 Schema
      • hidden : Schema 숨김여부
      • implementation : Schema 대상 클래스

@ApiResponse 는 Swagger 웹문서를 통해 상황에 따른 response 구조를 미리 확인할 수 있게 해줍니다.
@ApiResponse를 설정하지 않으면 아래와 같이 200 OK 에 대한 Response 예시만 보여줍니다.

04-7


가게 상세 조회 API 에 @ApiResponse 설정을 추가해봅니다.

존재하지 않은 가게를 조회할 때엔 400 Http Status와 함께 BaseResponse 객체를 내보내도록 이전시간에 @ControllerAdvice 및 @ExceptionHandler 를 설정하였습니다.

기본 Response와 다를 경우 content 속성에 스키마를 설정해주면, Swagger 문서상으로 상태코드별 Response를 정의할 수 있습니다.

2.4.1. @ApiResponses에 설정

@ApiResponses 은 @ApiResponse과 마찬가지로 ANNOTATION_TYPE, METHOD, TYPE에 설정할 수 있습니다.
아래 코드는 메서드에 설정한 예제입니다.

@Operation(summary = "가게 상세정보 조회", description = "가게의 상세정보와 해당 가게를 보유하고 있는 회원 정보를 조회합니다.")
@ApiResponses(value = {
        @ApiResponse(responseCode = "200", description = "가게 상세정보 조회 성공"),
        @ApiResponse(responseCode = "400", description = MessageUtils.INVALID_STORE_ID,
                content = @Content(schema = @Schema(implementation = BaseResponse.class))) })
@GetMapping("/{id}")
public DataResponse<StoreData.Store> select(@PathVariable("id") long id) {
    return new DataResponse<>(storeService.select(id));
}

2.4.2. Operation에 설정

@Operation(summary = "가게 상세정보 조회", description = "가게의 상세정보와 해당 가게를 보유하고 있는 회원 정보를 조회합니다.",
    responses = {
        @ApiResponse(responseCode = "200", description = "가게 상세정보 조회 성공"),
        @ApiResponse(responseCode = "400", description = MessageUtils.INVALID_STORE_ID,
            content = @Content(schema = @Schema(implementation = BaseResponse.class))) })
@GetMapping("/{id}")
public DataResponse<StoreData.Store> select(@PathVariable("id") long id) {
    return new DataResponse<>(storeService.select(id));
}

@ApiResponse 를 적용한 후의 Swagger 화면입니다.

04-8


2.5. api parameter 설정 : @Parameter

Target : ANNOTATION_TYPE, FIELD, METHOD, PARAMETER

  • name : 파라미터 이름
  • description : 파라미터 설명
  • in : 파라미터 위치
    • query, header, path, cookie

@Paramater 도 @Paraments에 정의하거나, @Operation 의 parameters 요소에 설정할 수 있습니다.

@Operation(summary = "가게 상세정보 조회", description = "가게의 상세정보와 해당 가게를 보유하고 있는 회원 정보를 조회합니다.",
    responses = {
        @ApiResponse(responseCode = "200", description = "가게 상세정보 조회 성공"),
        @ApiResponse(responseCode = "400", description = MessageUtils.INVALID_STORE_ID,
            content = @Content(schema = @Schema(implementation = BaseResponse.class))) })
@GetMapping("/{id}")
public DataResponse<StoreData.Store> select(
        @Parameter(description = "가게 id", in = ParameterIn.PATH) @PathVariable("id") long id) {
    return new DataResponse<>(storeService.select(id));
}

path로부터 들어올 파라미터인 id에 대한 설명을 추가했습니다.
@PathVariable 설정 앞에 @Parameter를 정의할 때에는 name이나 in을 생략할 수 있습니다.

@Parameter(name = "id", description = "가게 id", in = ParameterIn.PATH) @PathVariable("id") long id


만일 @Parameters 나 @Operations에 파라미터를 설정할 경우에는 어떤 파라미터에 대한 설정인지 알수 없기 때문에 반드시 name을 설정해 줘야 합니다.


04-9

Swagger v3 애너테이션 적용을 마친 후의 Swagger UI 화면입니다.


3. 기존 api에 적용하기

3.1. @Tag 설정

@Tag는 같은 그룹으로 묶고 싶은 operation(메서드)들을 그룹화합니다.
컨트롤러 클래스나 메서드에 설정할 수 있는데 여기에서는 컨트롤러 클래스에 설정했습니다.

@Tag(name = "store", description = "가게 API")
@RequiredArgsConstructor
@RequestMapping(path = "/api/stores")
@RestController
public class StoreController {
	...
}
@Tag(name = "user", description = "사용자 API")
@RequiredArgsConstructor
@RequestMapping(path = "/api/users")
@RestController
public class UserController {
	...
}

operation 묶음인 Tag에 이름과 설명이 적용된 모습입니다.

04-20


3.2. @Schema 및 유효성 검증 설정

@Schema는 각 항목들에 대한 설명 또는 한글항목명이나 body에 설정될 기본 값, 허용가능한 입력값 enum 등을 정의할 수 있습니다.

3.2.1. UserRequest

사용자 추가에 이용되는 UserRequest에 @Schema를 설정하여 API 이용에 편의를 제공해봅시다.

description에는 한글항목명을 쓰면 되고, Swagger상에 기본적으로 보여줄 Example Value의 기본값은 defaultValue에 정의합니다.
만일, 특정 값들만 허용하는것을 Swagger문서상으로 보여주고 싶다면 allowableValues에 설정해주면 됩니다.

그리고, javax.validation.constraints 패키지 내에 있는 유효성 검증 체크 관련된 애너테이션을 함께 정의하면, 간단한 유효성 검증 체크를 DTO 클래스 설정만으로 끝낼 수 있습니다.

유효성 검증은, insert, update, delete같은 DB 수정과 관련된 일을 할때 필요한 과정으로. 검증하고자하는 인자앞에 @Valid 설정을 추가할 경우 필드에 설정된 설정을 기준으로 값을 확인합니다.

@Operation(summary = "사용자 추가", description = "사용자을 추가합니다.")
@PostMapping("")
public BaseResponse insert(@RequestBody @Valid final UserRequest user) {
    userService.insert(user);
    return new BaseResponse();
}

DTO에 설정된 @NotBlank, @Length, @Pattern 같은 유효성 검증을 이용하고 싶다면 api 메서드에 위와같이 @Valid 애너테이션을 추가해야합니다.


기존 UserRequest에 아래와 같은 입력만을 허용하도록 validation을 설정하고, 설명도 붙여봅시다.

  • type에는 BASIC 또는 OWNER만 들어갈 수 있습니다.
  • email에는 이메일 타입만 들어갑니다.
  • name에는 한글또는 영문만 들어가며, 2자 이상입니다.
  • sex에는 M 또는 F가 들어갑니다.
  • birthDate은 yyyy-MM-dd 형식
  • phoneNumber는 01x-xxx-xxxx 또는 01x-xxxx-xxxx 형식
  • password는 문자열
  • type, email, name, password 는 null이나 빈문자열이 들어갈 수 없습니다.

@Getter @Setter
@Schema(description = "사용자")
public class UserRequest {

    @NotBlank
    @Pattern(regexp = "^(BASIC|OWNER)$")
    @Schema(description = "사용자 유형(BASIC|OWNER)", defaultValue = "BASIC", allowableValues = {"BASIC", "OWNER"})
    private String type;

    @Length(min = 1, max = 50)
    @NotBlank
    @Email
    @Schema(description = "이메일", example = "abc@jiniworld.me")
    private String email;

    @NotBlank
    @Pattern(regexp = "^[a-zA-Z가-힣]{2,100}$")
    @Schema(description = "이름")
    private String name;

    @Pattern(regexp = "^(M|F)$")
    @Schema(description = "성별(M|F)", defaultValue = "M", allowableValues = {"M", "F"})
    private String sex;

    @Pattern(regexp = "^(19[0-9][0-9]|20\\d{2})(0[0-9]|1[0-2])(0[1-9]|[1-2][0-9]|3[0-1])$")
    @Schema(description = "생년월일(yyyy-MM-dd)")
    private String birthDate;

    @Pattern(regexp = "^01[0179][0-9]{7,8}$")  
    @Schema(description = "전화번호")
    private String phoneNumber;

    @Length(min = 6, max = 150)
    @NotBlank
    @Schema(description = "비밀번호")
    private String password;
}

3.2.2. UserData

사용자 조회에 이용되는 Response용 DTO에도 @Schema를 설정해줍니다.

그런데, UserRequest에서도 이용했었던 중복된 항목들이 있습니다.
공통적인 String요소는 별도로 상수로 정의해서 관리하면, 한글항목명 관리가 더 편리해질 것입니다.

별도의 클래스를 만들어서, UserRequest에서 설정했던 @Schema description을 정의해두고

public class SchemaDescriptionUtils {
    public static final String name = "이름";

    public static class User {
        public static final String type = "사용자 유형(BASIC|OWNER)";
        public static final String email = "이메일";
        public static final String sex = "성별(M|F)";
        public static final String birthDate = "생년월일(yyyy-MM-dd)";
        public static final String phoneNumber = "전화번호";
        public static final String password = "비밀번호";
        public static final String stores = "보유 가게";
    }
}

기존의 UserData에 @Schema를 설정했습니다.

※ User Entity와 UserData DTO의 birthDate 필드값의 타입은 기능상의 이유로 LocalDate 타입으로 변경했습니다.

@Data
@NoArgsConstructor
public class UserData {

    @Data
    @NoArgsConstructor
    @Schema(description = "사용자 간략정보")
    public static class UserSimple {
        private Long id;

        @Schema(description = SchemaDescriptionUtils.User.type)
        private String type;

        @Schema(description = SchemaDescriptionUtils.User.email)
        private String email;

        @Schema(description = SchemaDescriptionUtils.name)
        private String name;

        public UserSimple(me.jiniworld.demo.domain.entity.User u) {
            this.id = u.getId();
            this.type = u.getType();
            this.email = u.getEmail();
            this.name = u.getName();
        }
    }

    @Data
    @Schema(description = "사용자 상세정보")
    public static class User extends UserSimple {
        @Schema(description = SchemaDescriptionUtils.User.sex)
        private String sex;

        @Schema(description = SchemaDescriptionUtils.User.birthDate)
        private LocalDate birthDate;

        @Schema(description = SchemaDescriptionUtils.User.phoneNumber)
        private String phoneNumber;

        @Schema(description = SchemaDescriptionUtils.User.stores)
        private List<StoreData.StoreSimple> stores;

        public User(me.jiniworld.demo.domain.entity.User u) {
            super(u);
            this.sex = u.getSex();
            this.birthDate = u.getBirthDate();
            this.phoneNumber = u.getPhoneNumber();
            this.stores = u.getStores().stream().map(StoreData.StoreSimple::new).collect(Collectors.toList());
        }
    }
}

3.2.3. StoreData

기존의 StoreData에 @Schema를 설정했습니다.

※ Store Entity 및 StoreData DTO의 industry 필드는 business로 변경했습니다.

public class SchemaDescriptionUtils {
    ...

    public static class Store {
        public static final String business = "가게 업종";
        public static final String user = "가게 소유주";
    }
}
@Data
@NoArgsConstructor
public class StoreData {

    @Data
    @NoArgsConstructor
    public static class StoreSimple {

        private Long id;

        @Schema(description = SchemaDescriptionUtils.name)
        private String name;

        @Schema(description = SchemaDescriptionUtils.Store.business)
        private String business;

        public StoreSimple(me.jiniworld.demo.domain.entity.Store s) {
            this.id = s.getId();
            this.name = s.getName();
            this.business = s.getBusiness();
        }
    }

    @Data
    public static class Store extends StoreSimple {

        @Schema(description = SchemaDescriptionUtils.Store.user)
        private UserData.UserSimple user;

        public Store(me.jiniworld.demo.domain.entity.Store s) {
            super(s);
            this.user = new UserData.UserSimple(Optional.ofNullable(s.getUser()).orElse(null));
        }

    }
}

3.2.4. Swagger 적용화면

04-5

사용자 추가의 Request body에서 확인할 수 있는 UserRequest의 Schema입니다.


04-10

사용자 전체 조회 Responses Schema에서 확인할 수 있는 UserData.UserSimple의 Schema입니다.


04-12

사용자 단건 조회 Responses Schema에서 확인할 수 있는 UserData.User의 Schema입니다.


04-11

가게 전체 조회 Responses Schema에서 확인할 수 있는 StoreData.StoreSimple의 Schema입니다.


04-13

가게 단건 조회 Responses Schema에서 확인할 수 있는 StoreData.Store의 Schema입니다.


3.3. @Operation 설정

@Operation은 api에 대한 요약과 설명을 추가하는 곳입니다.
description 상에는 간단한 markdown문법이나 html 코드도 설정할 수 있기 때문에 필요하다면 api에 대한 상세 설정을 추가할 수도 있습니다.

사용자와 가게 api에도 @Operation 을 설정해봅니다.

@Tag(name = "user", description = "사용자 API")
@RequiredArgsConstructor
@RequestMapping(path = "/api/users")
@RestController
public class UserController {

	private final UserService userService;

	@Operation(summary = "사용자 전체 조회", description = "사용자 간략정보를 모두 조회합니다.")
	@GetMapping("")
	public DataResponse<List<UserData.UserSimple>> selectAll() {
		List<UserData.UserSimple> users = userService.selectAll();
		return new DataResponse<>(users);
	}

	@Operation(summary = "사용자 추가", description = "사용자을 추가합니다.")
	@PostMapping("")
	public BaseResponse insert(@RequestBody @Valid final UserRequest user) {
		userService.insert(user);
		return new BaseResponse();
	}

	@Operation(summary = "사용자 상세 조회", description = "사용자의 상세정보와 보유하고 있는 가게 정보를 조회합니다.")
	@GetMapping("/{id}")
	public DataResponse<UserData.User> select(@PathVariable("id") long id) {
		return new DataResponse<>(userService.select(id));
	}

	@Operation(summary = "사용자 수정", description = "사용자 정보를 전체 수정합니다.")
	@PutMapping("/{id}")
	public BaseResponse update(@PathVariable("id") long id, @RequestBody @Valid final UserRequest user) {
		userService.update(id, user);
		return new BaseResponse();
	}

	@Operation(summary = "사용자 수정(patch)", description = "사용자 정보를 일부 수정합니다.<br>설정하지 않은 값은 수정하지 않습니다.")
	@PatchMapping("/{id}")
	public BaseResponse partialUpdate(@PathVariable("id") long id, @RequestBody final UserRequest user) {
		userService.partialUpdate(id, user);
		return new BaseResponse();
	}

	@Operation(summary = "사용자 삭제", description = "사용자를 삭제합니다.")
	@DeleteMapping("/{id}")
	public BaseResponse delete(@PathVariable("id") long id) {
		userService.delete(id);
		return new BaseResponse();
	}

}
@Tag(name = "store", description = "가게 API")
@RequiredArgsConstructor
@RequestMapping(path = "/api/stores")
@RestController
public class StoreController {

	private final StoreService storeService;

	@Operation(summary = "가게 간략정보 전체 조회", description = "가게의 간략정보를 모두 조회합니다.")
	@GetMapping("")
	public DataResponse<List<StoreData.StoreSimple>> selectAll() {
		List<StoreData.StoreSimple> stores = storeService.select();
		return new DataResponse<>(stores);
	}

	@Operation(summary = "가게 상세정보 조회", description = "가게의 상세정보와 해당 가게를 보유하고 있는 회원 정보를 조회합니다.")
	@GetMapping("/{id}")
	public DataResponse<StoreData.Store> select(@PathVariable("id") long id) {
		return new DataResponse<>(storeService.select(id));
	}
}

@Operation의 summary는 아래와 같이 api 오른쪽에 표시됩니다.

04-21


description은 api를 펼쳤을 때 내부에 표시됩니다.

04-22


3.4. @ApiResponse 설정

@ApiResponse를 Controller의 클래스에 정의할 경우, 컨트롤러 내의 메서드에 공통적으로 정의할 수 있습니다.

기존에 만들었던 api들은 모두 실행중 에러가 발생되면 400 Http Status 코드와 함께 BaseResponse 객체를 리턴하는 형식이었기 때문에 공통적으로 정의해도 무방합니다.

@ApiResponses({
		@ApiResponse(responseCode = "200", description = MessageUtils.SUCCESS),
		@ApiResponse(responseCode = "400", description = MessageUtils.FAIL,
				content = @Content(schema = @Schema(implementation = BaseResponse.class)))})
@Tag(name = "user", description = "사용자 API")
@RequiredArgsConstructor
@RequestMapping(path = "/api/users")
@RestController
public class UserController {
    ...
}
@ApiResponses({
		@ApiResponse(responseCode = "200", description = MessageUtils.SUCCESS),
		@ApiResponse(responseCode = "400", description = MessageUtils.FAIL,
				content = @Content(schema = @Schema(implementation = BaseResponse.class)))})
@Tag(name = "store", description = "가게 API")
@RequiredArgsConstructor
@RequestMapping(path = "/api/stores")
@RestController
public class StoreController {
    ...
}

3.4. @Parameter 설정

공통으로 이용되는 @Pramater description은 상수화 하고

public class ParameterDescriptionUtils {
    public static final String USER_ID = "사용자 id";
    public static final String STORE_ID = "가게 id";
}

@Parameter를 설정해줍니다.
@PathVariable이나 @RequestParam 앞에 설정하는 경우에는 in이나 name은 생략해도 됩니다.

@Tag(name = "user", description = "사용자 API")
@RequiredArgsConstructor
@RequestMapping(path = "/api/users")
@RestController
public class UserController {

	private final UserService userService;

	@Operation(summary = "사용자 전체 조회", description = "사용자 간략정보를 모두 조회합니다.")
	@GetMapping("")
	public DataResponse<List<UserData.UserSimple>> selectAll() {
		List<UserData.UserSimple> users = userService.selectAll();
		return new DataResponse<>(users);
	}

	@Operation(summary = "사용자 추가", description = "사용자을 추가합니다.")
	@PostMapping("")
	public BaseResponse insert(@RequestBody @Valid final UserRequest user) {
		userService.insert(user);
		return new BaseResponse();
	}

	@Operation(summary = "사용자 상세 조회", description = "사용자의 상세정보와 보유하고 있는 가게 정보를 조회합니다.")
	@GetMapping("/{id}")
	public DataResponse<UserData.User> select(
			@Parameter(description = ParameterDescriptionUtils.USER_ID) @PathVariable("id") long id) {
		return new DataResponse<>(userService.select(id));
	}

	@Operation(summary = "사용자 수정", description = "사용자 정보를 전체 수정합니다.")
	@PutMapping("/{id}")
	public BaseResponse update(
			@Parameter(description = ParameterDescriptionUtils.USER_ID) @PathVariable("id") long id,
			@RequestBody @Valid final UserRequest user) {
		userService.update(id, user);
		return new BaseResponse();
	}

	@Operation(summary = "사용자 수정(patch)", description = "사용자 정보를 일부 수정합니다.<br>설정하지 않은 값은 수정하지 않습니다.")
	@PatchMapping("/{id}")
	public BaseResponse partialUpdate(
			@Parameter(description = ParameterDescriptionUtils.USER_ID) @PathVariable("id") long id,
			@RequestBody final UserRequest user) {
		userService.partialUpdate(id, user);
		return new BaseResponse();
	}

	@Operation(summary = "사용자 삭제", description = "사용자를 삭제합니다.")
	@DeleteMapping("/{id}")
	public BaseResponse delete(
			@Parameter(description = ParameterDescriptionUtils.USER_ID) @PathVariable("id") long id) {
		userService.delete(id);
		return new BaseResponse();
	}

}
@Tag(name = "store", description = "가게 API")
@RequiredArgsConstructor
@RequestMapping(path = "/api/stores")
@RestController
public class StoreController {

	private final StoreService storeService;

	@Operation(summary = "가게 간략정보 전체 조회", description = "가게의 간략정보를 모두 조회합니다.")
	@GetMapping("")
	public DataResponse<List<StoreData.StoreSimple>> selectAll() {
		List<StoreData.StoreSimple> stores = storeService.select();
		return new DataResponse<>(stores);
	}

	@Operation(summary = "가게 상세정보 조회", description = "가게의 상세정보와 해당 가게를 보유하고 있는 회원 정보를 조회합니다.")
	@GetMapping("/{id}")
	public DataResponse<StoreData.Store> select(
			@Parameter(description = ParameterDescriptionUtils.STORE_ID) @PathVariable("id") long id) {
		return new DataResponse<>(storeService.select(id));
	}
}

아래 화면은, @Parameter description이 적용된 사용자 수정 api 화면입니다.

04-23


4. Exception 처리 보완

@NotBlank나, @Pattern 같은 javax.validation.constraints 로 유효성 검증을 적용한 항목에 대한 유효성 문제가 발생되었을 때 아래와 같이 에러가 출력됩니다.

04-15

04-14

위와 같이 error trace를 모두 출력하는것은 우리가 원하는 결과가 아닙니다.
저 중에, 에러 원인만 간단하게 BaseResponse 형식으로 출력하고 싶다면 어떻게 해야할까요?

바로 이전 시간에 이용했었던 @ExceptionHandler 를 활용하면 됩니다.

※ 참고: @ControllerAdvice와 @ExceptionHandler를 이용한 전역 Error Handling


유효성 검증과 관련된 Exception을 @ExceptionHandler와 @ControllerAdvice를 이용하여 원하는 Response형태를 정의하면 됩니다.

@ControllerAdvice
public class CustomResponseEntityExceptionHandler {

    ...

    @ExceptionHandler(MethodArgumentNotValidException.class)
    protected ResponseEntity<BaseResponse> handleException(MethodArgumentNotValidException e) {
        String message = e.getBindingResult().getFieldErrors().stream()
                .findFirst().map(fieldError ->
                        String.format("%s 오류. %s", fieldError.getField(), fieldError.getDefaultMessage()))
                .orElse(e.getMessage());
        return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new BaseResponse(message));
    }

}

MethodArgumentNotValidException에 대한 핸들러를 정의한 후 에러발생시 응답화면들입니다.
이전보다 훨씬 깔끔해졌습니다.

04-16

04-17

04-18

04-19


GitHub에서 demo 프로젝트(v1.0.10)를 다운받아 볼 수 있습니다.


++

  • Swagger v3 상세설정 (springdoc-openapi)
  • @Valid와 javax.validation.constraints 하위 애너테이션 설정을 이용한 API 유효성 검증
  • @ControllerAdvice와 @ExceptionHandler를 이용하여 MethodArgumentNotValidException 익셉션 전역 처리
728x90
반응형