똑같은 삽질은 2번 하지 말자
Spring으로 REST API No.3(입력값 제한, ModelMapper, @Valid, Errors, JsonSerializer ) 본문
Spring으로 REST API No.3(입력값 제한, ModelMapper, @Valid, Errors, JsonSerializer )
곽빵 2020. 6. 29. 17:40지정된 값 이외의 값이 들어왔을때, 지정된 값이 들어오지 않았을 때
@Test
public void createEvent_is_BadRequest() throws Exception {
Event event = Event.builder()
.id(100) // 들어오면 안되는 값
.name("Spring")
.description("REST API")
.beginEnrollmentDateTime(LocalDateTime.of(2020,6,28,14,11))
.closeEnrollmentDateTime(LocalDateTime.of(2020,6,29,14,11))
.beginEventDateTime(LocalDateTime.of(2020,6,30,14,11))
.endEventDateTime(LocalDateTime.of(2020,6,30,16,11))
.basePrice(100)
.maxPrice(200)
.limitOfEnrollment(100)
.location("부산역 어딘가")
.free(true) // 들어오면 안되는 값
.offline(false) // 들어오면 안되는 값
.build();
mockMvc.perform(post("/api/events")
.contentType(MediaType.APPLICATION_JSON_UTF8)
.accept(MediaTypes.HAL_JSON)
.content(objectMapper.writeValueAsString(event)))
.andExpect(status().isBadRequest());
}
id , free, offlline 은 클라이언트가 지정할 수 있는 값이 아닌데 들어온다 할때는
Entity가 아니고 일단 dto로 받는다.
EventDto.class
import java.time.LocalDateTime;
import javax.validation.constraints.NotEmpty;
import javax.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
@Data @Builder @NoArgsConstructor @AllArgsConstructor
public class EventDto {
@NotEmpty
private String name;
@NotEmpty
private String description;
@NotNull
private LocalDateTime beginEnrollmentDateTime;
@NotNull
private LocalDateTime closeEnrollmentDateTime;
@NotNull
private LocalDateTime beginEventDateTime;
@NotNull
private LocalDateTime endEventDateTime;
private String location; // (optional) 이게 없으면 온라인 모임
private int basePrice;
private int maxPrice;
private int limitOfEnrollment;
}
들어온 dto는 Entity로 Mapping을 해줘야 영속성 컨테스트가 값을 DB에 넣을 수 있는데,
그때 사용할 수 있는 친구가
ModelMapper
<dependency>
<groupId>org.modelmapper</groupId>
<artifactId>modelmapper</artifactId>
<version>2.3.1</version>
</dependency>
@Bean
public ModelMapper modelMapper() {
return new ModelMapper();
}
빈으로 등록한 뒤, mapping뒤 저장해주면 된다.
Event.class
@Builder
@AllArgsConstructor @NoArgsConstructor // public default 생성자
@Getter @Setter // @Data에는 EqualsAndHashCode가 기본적으로 정의되어있어서 겹침
@EqualsAndHashCode(of = "id")
// 기본적으로 객체의 모든 필드로 Equals, HashCode를 비교하는데 그럼
// 나중에 연관관계에서 (상호참조하는) StackOverFlow가 발생할 수 있으므로 ID로만 비교한다.
@Entity
public class Event {
@Id @GeneratedValue
private Integer id;
private String name;
private String description;
private LocalDateTime beginEnrollmentDateTime;
private LocalDateTime closeEnrollmentDateTime;
private LocalDateTime beginEventDateTime;
private LocalDateTime endEventDateTime;
private String location; // (optional) 이게 없으면 온라인 모임
private int basePrice;
private int maxPrice;
private int limitOfEnrollment;
private boolean offline;
private boolean free;
@Enumerated(EnumType.STRING)
private EventStatus eventStatus;
// @ManyToOne
// private Account manager;
public void update() {
// Update Free
if(this.basePrice == 0 && this.maxPrice == 0) {
this.free = true;
} else {
this.free = false;
}
//Update offline
if(this.location == null || this.location.trim().isEmpty())
this.offline = false;
else
this.offline = true;
}
}
그리고 이렇게 Mapping
Event event = modelMapper.map(eventDto,Event.class);
eventRepository.save(event); // Entity로 넣을 수 있다.
그리고 받을때 EventDto객체로 받는데 모르는 속성(입력되면 안되는 속성)이 들어올 때,
따로 처리를 안해줘도 되는 설정을 안하고 BadRequest를 날릴리면 SpringBoot가 지원하는 jackson에 좋은 친구가 있다.
application.properties.xml
spring.jackson.deserialization.fail-on-unknown-properties=true
객체를 JSON으로 바꾸는데 매칭이 안되는 프로퍼티가 들어오면 BadRequest를 날려주는 설정
EventController
import static org.springframework.hateoas.mvc.ControllerLinkBuilder.linkTo;
import java.net.URI;
import javax.validation.Valid;
import org.modelmapper.ModelMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.hateoas.MediaTypes;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.validation.Errors;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
@Controller
@RequestMapping(value = "/api/events", produces = MediaTypes.HAL_JSON_UTF8_VALUE)
public class EventController {
@Autowired
EventRepository eventRepository;
@Autowired
ModelMapper modelMapper;
@Autowired
EventValidator eventValidator;
@PostMapping
public ResponseEntity createEvent(@RequestBody @Valid EventDto eventDto, Errors errors) {
if(errors.hasErrors())
return ResponseEntity.badRequest().build();
eventValidator.validate(eventDto, errors);
if(errors.hasErrors())
return ResponseEntity.badRequest().build();
Event event = modelMapper.map(eventDto,Event.class);
Event newEvent = this.eventRepository.save(event);
URI createUri =
linkTo(EventController.class).slash(newEvent.getId()).toUri();
return ResponseEntity.created(createUri).body(event);
// event는 자바빈 스펙을 준수하므로 BeanSerializer로 json으로 변환해서 자동으로 넘어간다.
}
}
지정된 값이 들어오지 않았을 때는 @Valid @NotEmpty로 체크해준다.
이런것을 Dto가 대신해서 해주는중
잘못된 값이 들어왔을때(비즈니스 로직상 맞지않는 값)
따로 Validator 를 만들어서 그때 그때 맞춰서 사용해 준다.
import org.springframework.stereotype.Component;
import org.springframework.validation.Errors;
@Component
public class EventValidator {
public void validate(EventDto eventDto, Errors errors) {
if(eventDto.getBasePrice() > eventDto.getMaxPrice() && eventDto.getMaxPrice() > 0) {
errors.rejectValue("basePrice", "wrongValue","BasePrice is Wrong");
errors.rejectValue("maxPrice", "wrongValue","MaxPrice is Wrong");
}
if(eventDto.getBeginEventDateTime().isAfter(eventDto.getEndEventDateTime())){
errors.rejectValue("endEventTime", "wrongValue","EndEventTime is Wrong");
}
}
}
Dto와 errors 객체를 받아서 검사를 하고 잘못된 값이 있으면 errors에 내용을 담아 넘겨준다.
Errors 의 내용을 ResponseBody에 넣어서 JSON의 형태로 넘겨줄려면 Serialize가 필요하다.
그럼 Event 도메인을 넘길때는 왜 그냥 넣기만해도 됬냐? 라고 한다면, Java Bean 준수하기 때문에
BeanSerializer가 Serialize해서 넘겨주므로 따로 필요가 없다.
그러므로 따로 ErrorSerializer를 만들어서 컴포넌트 등록도 해줘야 한다.
ErrorSerializer(JsonSerializer)
import java.io.IOException;
import org.springframework.boot.jackson.JsonComponent;
import org.springframework.validation.Errors;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializerProvider;
@JsonComponent
public class ErrorSerializer extends JsonSerializer<Errors>{
@Override
public void serialize(Errors errors, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeStartArray();
errors.getFieldErrors().stream().forEach(e->{
try {
gen.writeStartObject();
gen.writeStringField("field", e.getField());
gen.writeStringField("objectName",e.getObjectName());
gen.writeStringField("code", e.getCode());
gen.writeStringField("defaultMessage", e.getDefaultMessage());
Object rejectedValue = e.getRejectedValue();
if(rejectedValue != null)
gen.writeStringField("rejectedValue", (String)rejectedValue);
gen.writeEndObject();
}catch(Exception er) {
System.out.println(er);
}
});
errors.getGlobalErrors().stream().forEach(e->{
try {
gen.writeStartObject();
gen.writeStringField("objectName",e.getObjectName());
gen.writeStringField("code", e.getCode());
gen.writeStringField("defaultMessage", e.getDefaultMessage());
gen.writeEndObject();
}catch(Exception er) {
System.out.println(er);
}
});
gen.writeEndArray();
}
}
이렇게 JsonComponent로 등록하면 ObjectMapper가 Errors라는 객체를 Serialize할때 이 ErrorsSerializer를 사용한다.
이제 return 할때 응답본문에 그냥 객체 통짜로 넣어도 제대로 반환이 된다.
'Spring > Spring Boot' 카테고리의 다른 글
Spring으로 REST API No.5(Spring REST Docs ) (0) | 2020.07.09 |
---|---|
Spring으로 REST API No.4(Spring HATEOAS, ) (0) | 2020.07.02 |
Spring으로 REST API No.2(도메인구현, 기본 요청 응답(201)테스트, ) (0) | 2020.06.28 |
Spring으로 REST API No.1(REST API , 프로젝트 구성) (0) | 2020.06.28 |
Spring Boot 개념다지기 No.20(Actuator) (0) | 2020.05.06 |