ย ๋ฐฐ๊ฒฝ
์ด๋ API์์ ํ์ผ๊ณผ DTO๋ฅผ ๊ฐ์ด ๋ฐ๊ธฐ ์ํด Multipart/form-data ๋ผ๋ content-type์ ์ฌ์ฉํ์์ต๋๋ค.
"file": ํ์ผ์ blob
"json": stringfy๋ json
Plain Text
๋ณต์ฌ
form-data์ ์ ๋ฐฉ์๋๋ก ๋ฐ๊ณ ์์๊ณ ๊ฐ key-value์์ ํํธ๋ก ๊ตฌ๋ถ๋์์ต๋๋ค.
ํด๋ผ์ด์ธํธ์ธก์๋ json ํํธ์ content-type์ application/json์ผ๋ก ๋ฐ๋ก ์ง์ ํด์ ์์ฒญํด๋ฌ๋ผ๊ณ ํ์์ง๋ง
์ด๋ฅผ ์ด์ฉํ๋๋ฐ ์ด๋ ค์์ด ์์ด์ ๋ค๋ฅธ ๋ฐฉ๋ฒ์ ์ฐพ๊ฒ ๋์์ต๋๋ค.
์ฌ๊ธฐ์ ์ ํ์ง๋ 2๊ฐ์ง์์ต๋๋ค.
1.
json์ ๊ฐ ํ๋๋ฅผ ๋ฐฑ์๋์ธก์์ Map์ผ๋ก ๋ฐ๋ ๋ฐฉ๋ฒ
2.
text/plain ํ์
์ผ๋ก ๋ฐ์ ๋ค ObjectMapper๋ Gson์ ์ฌ์ฉํด DTO๋ก ๋งคํํ๋ ๋ฐฉ๋ฒ
1๋ฒ์ ๊ฒฝ์ฐ๋ ํ๋๊ฐ ๋ง์์ง๊ณ value์ ํ์
์ด ๋ค์ํด์ง๋ฉด Map<String, Object>๋ก ๋ฐ์์ผํด์ ๊บผ๋ด๋ ๊ณผ์ ์์ ๊ต์ฅํ ๋ฒ๊ฑฐ๋ก์๊ณผ ํ์
์๋ฌ๊ฐ ๋ฐ์ํ ๊ฒ ๊ฐ๋ค๊ณ ์๊ฐํ์ต๋๋ค.
๊ทธ๋์ 2๋ฒ์ ์ฌ์ฉํ๊ธฐ๋ก ๊ฒฐ์ ํ ์ด์ผ๊ธฐ์
๋๋ค..
ย ์ฌ๊ฑด์ ๋ฑ์ฅ
@PutMapping(value = "/{companyId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<ResponseDTO> updateCompany(HttpServletRequest request,
@RequestPart(required = false) MultipartFile logo,
@Parameter(schema = @Schema(implementation = CompanyUpdateDTOforBiz.class)) @RequestPart String json,
@PathVariable Long companyId) throws IOException {
CompanyUpdateDTOforBiz dto = objectMapper.readValue(json, CompanyUpdateDTOforBiz.class);
return new ResponseEntity<>(companyService.updateCompany(request, dto, logo, companyId), HttpStatus.OK);
}
Java
๋ณต์ฌ
Controller์ ์ฝ๋๋ ์์ ๊ฐ์๊ณ json์ DTO๊ฐ ์๋ String์ผ๋ก ๋ฐ๋๋ก ๋ณ๊ฒฝํ์์ต๋๋ค.
ObjectMapper๋ฅผ ์ด์ฉํ์ฌ DTO๋ก ๋งคํํ๋๋ฐ๊น์ง ๋ฌธ์ ๊ฐ ์์์ง๋ง ํ
์คํธ๋ฅผ ๋๋ฆฌ์ ์ฌ๊ฑด์ด ๋ฐ์ํ์ต๋๋ค.
์คํจํ๋ ์กฐ๊ฑด์ ํ
์คํธ๋ง ๋ชจ์กฐ๋ฆฌ ์คํจํ๋ ๊ฒ์ ๋ณด๋ Validation์ด ์ ๋๋ก ์๋ํ์ง ์๊ณ ์๋ ๊ฒ์ ์์์ฐจ๋ ธ์ต๋๋ค.
์ฌ๊ธฐ์ ์ ๊น ์คํ๋ง์ WebDataBind ๊ณผ์ ์ ์ดํด๋ณด๊ณ ๊ฐ๊ฒ ์ต๋๋ค.
์ด ์ฌ์ง์ Spring MVC ๊ตฌ์กฐ์์ HTTP ์์ฒญ์์๋ถํฐ ์ปจํธ๋กค๋ฌ๊น์ง๋ฅผ ํฌ๊ฒ ๋ณด์ฌ์ฃผ๋ ์ฌ์ง์
๋๋ค.
HTTP ์์ฒญ์ด ๋ค์ด์ค๋ฉด DispatcherServlet์ ํธ๋ค๋ฌ ๋งคํ์๊ฒ ์ด๋ค ์ปจํธ๋กค๋ฌ๊ฐ ์ฒ๋ฆฌํ ์ง ์ฐพ์ผ๋ผ๊ณ ์ํต๋๋ค.
ํธ๋ค๋ฌ ๋งคํ์ DispatcherServlet์๊ฒ ์ ๋นํ ์ปจํธ๋กค๋ฌ๋ฅผ ์ฐพ์์ ์๋ ค์ฃผ๊ณ ํธ๋ค๋ฌ ์ด๋ํฐ์๊ฒ ๊ทธ ์ปจํธ๋กค๋ฌ๋ฅผ ์ฐพ์์ ์์ฒญ์ ์ฒ๋ฆฌํ๋ผ๊ณ ์ ๋ฌํฉ๋๋ค.
์ด์ ์ปจํธ๋กค๋ฌ๋ ์ฌ์ง์ ํ์๋์ง ์์ Service ๋ ์ด์ด์ Repository ๋ ์ด์ด๋ฅผ ๊ฑฐ์ณ ๋ฐ์ดํฐ๋ฅผ ๋ฐํํฉ๋๋ค.
์ด์ ํธ๋ค๋ฌ ์ด๋ํฐ๊ฐ ์ด๋ ํ ์ผ์ ํ๋์ง ๋ ์์ธํ๊ฒ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
์ ํฌ๋ Controller ๋ ์ด์ด์ ๋ฉ์๋๋ฅผ ์์ฑํ ๋ HttpServletRequest, @RequestBody, @RequestParam, @ModelAttribute ๋ฑ ๋ง์ ์ข
๋ฅ์ ํ๋ผ๋ฏธํฐ์ ์ด๋
ธํ
์ด์
์ ์ฌ์ฉํฉ๋๋ค.
DispatcherServlet์๊ฒ ํน์ ์ปจํธ๋กค๋ฌ๋ฅผ ํตํด HTTP ์์ฒญ์ ์ฒ๋ฆฌํ๋ผ๊ณ ์ ๋ฌ ๋ฐ์ ํธ๋ค๋ฌ ์ด๋ํฐ๋ ์ฌ๋ฌ ํ๋ผ๋ฏธํฐ์ ์ด๋
ธํ
์ด์
์ ๋ณด๋ฅผ ๊ธฐ๋ฐ์ผ๋ก ์ปจํธ๋กค๋ฌ์ ์ ๋ฌํ ๋ฐ์ดํฐ๋ฅผ ์์ฑํฉ๋๋ค.
์ด ๋ ๋ง์ ์ข
๋ฅ์ ํ๋ผ๋ฏธํฐ๋ฅผ ์ฒ๋ฆฌํด์ฃผ๋ ๊ฒ์ ๋ฐ๋ก ArgumentResolver์
๋๋ค.
ArgumentResolver๋ ์ ํฌ๊ฐ @RequestBody ์ด๋
ธํ
์ด์
์ ๋ถ์ฌ DTO๋ก ๋ฐ๊ณ ์ถ์ดํ๋ ํ๋ผ๋ฏธํฐ์ ๋ํ ๋งคํ์ HttpMessageConverter๋ฅผ ํตํด์ ์ฒ๋ฆฌํฉ๋๋ค.
ArgumentResolver ๋ฟ๋ง ์๋๋ผ HttpMessageConverter ๋ํ ์ธํฐํ์ด์ค๋ก ์ฌ๋ฌ ๊ฐ์ ๊ตฌํ์ฒด๋ฅผ ๊ฐ๊ณ ์์ต๋๋ค.
์ ์ ์ํฉ์์ DTO๋ก ๋ฐ์ ๋ HttpMessageConverter์ ๊ตฌํ์ฒด ์ค Jackson2HttpMessageConverter๋ฅผ ์ฌ์ฉํ๋ ๊ฒ์ String์ผ๋ก ๋ฐ๋ ๋ฐฉ์์ผ๋ก ๋ณ๊ฒฝํ๋ฉด์ StringHttpMessageConverter๋ฅผ ์ฌ์ฉํ๊ฒ๋์์ต๋๋ค.
์ด์ ์ ๊ฐ ์์ฑํ DTO๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
@Getter @Setter
@NoArgsConstructor
public class CompanyUpdateDTOforBiz {
@NotNull(message = "์ง์ ์๊ฐ ์
๋ ฅ๋์ง ์์์ต๋๋ค.", groups = ValidationGroups.NullCheckGroup.class)
@Min(value = 1, message = "์ง์ ์๋ ์ต์ 1๋ช
์ด์์ด์ด์ผํฉ๋๋ค.")
@Schema(name = "employee_number", description = "์ง์ ์", example = "100", required = true)
@SerializedName("employee_number")
private Integer employeeNumber;
@NotBlank(message = "์ฃผ์๊ฐ ๊ณต๋ฐฑ์ด๊ฑฐ๋ ์
๋ ฅ๋์ง ์์์ต๋๋ค.")
@Schema(description = "์ฃผ์", example = "์์ธ ๊ฐ๋จ๊ตฌ ํ
ํค๋๋ก 217", required = true)
private String address;
@NotBlank(message = "ํ ์ค ์๊ฐ๊ฐ ๊ณต๋ฐฑ์ด๊ฑฐ๋ ์
๋ ฅ๋์ง ์์์ต๋๋ค.", groups = ValidationGroups.NullCheckGroup.class)
@Length(max = 120, message = "ํ ์ค ์๊ฐ๊ฐ 120์๋ฅผ ์ด๊ณผํ์์ต๋๋ค.")
@Schema(description = "ํ ์ค ์๊ฐ", example = "์์ฐ/์ ๋ฌธ์ง ์ ์ฉ ์ฑ์ฉ ํ๋ซํผ", required = true)
private String intro;
...
}
Java
๋ณต์ฌ
์ ๊ฐ ์์ฑํ DTO๋ ์์ ๊ฐ์ด validation group์ ์ฌ์ฉํ๊ณ ์์๊ธฐ ๋๋ฌธ์ @Valid ๋์ @Validated๋ฅผ ์ฌ์ฉํด์ผํ์ต๋๋ค.
@Validated๋ javax ๋ผ์ด๋ธ๋ฌ๋ฆฌ๊ฐ ์๋ ์คํ๋ง ํ๋ ์์ํฌ์์ ์ ๊ณตํ๋ ์ด๋
ธํ
์ด์
์ผ๋ก ArgumentResolver๋ฅผ ํตํด ๊ตฌํ๋๋ ๋ก์ง์ด ์๋ AOP๋ฅผ ํตํด์ ๋์ํ๋ ๋ฐฉ์์
๋๋ค.
์ฌ์ค @Valid๋ฅผ ์ฌ์ฉํ๋ @Validated๋ฅผ ์ฌ์ฉํ๋ ์ ๊ฐ ๋ฐ๋ json์ String ํ์
์ด๊ธฐ ๋๋ฌธ์ validation์ด ์ ๋๋ก ์๋ํ์ง ์์์ ๊ฒ์
๋๋ค.
ย ํด๊ฒฐํ ๋ฐฉ๋ฒ
์ด๋ฏธ String์ผ๋ก ๋ฐ๊ธฐ ์์ํ ์ด์ Controller ๋ ์ด์ด ์ดํ์์ Validation ๊ณผ์ ์ ์ง์ ํ๊ธฐ๋ก ํ์์ต๋๋ค.
์ฐ์ String์์ DTO๋ก ๋งคํํ๋ ๊ณผ์ ์ ํ๋ก์ ํธ์์ Gson๊ณผ ObjectMapper ๋ ๊ฐ์ง๋ฅผ ๋ชจ๋ ์ฌ์ฉํ๊ณ ์์ด์ MapperConfig ํด๋์ค๋ฅผ ๋ง๋ค์ด ๊ฐ๊ฐ์ ๋ํ ์ค์ ์ ํด์ฃผ๊ธฐ ์ํด ๋น์ผ๋ก ๋ฑ๋กํด์ค๋๋ค.
@Configuration
public class MapperConfig {
@Bean
public ObjectMapper objectMapper() {
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.registerModule(new JavaTimeModule());
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
return objectMapper;
}
@Bean
public Gson gson() {
return new GsonBuilder()
.registerTypeAdapter(LocalDate.class, new LocalDateSerializer())
.registerTypeAdapter(LocalDate.class, new LocalDateDeserializer())
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeSerializer())
.registerTypeAdapter(LocalDateTime.class, new LocalDateTimeDeserializer())
.create();
}
public static class LocalDateSerializer implements JsonSerializer<LocalDate> {
@Override
public JsonElement serialize(LocalDate src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.format(DateTimeFormatter.ofPattern("yyyy-MM-dd")));
}
}
public static class LocalDateDeserializer implements JsonDeserializer<LocalDate> {
@Override
public LocalDate deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return LocalDate.parse(json.getAsString(), DateTimeFormatter.ofPattern("yyyy-MM-dd"));
}
}
public static class LocalDateTimeSerializer implements JsonSerializer<LocalDateTime> {
@Override
public JsonElement serialize(LocalDateTime src, Type typeOfSrc, JsonSerializationContext context) {
return new JsonPrimitive(src.format(DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss")));
}
}
public static class LocalDateTimeDeserializer implements JsonDeserializer<LocalDateTime> {
@Override
public LocalDateTime deserialize(JsonElement json, Type typeOfT, JsonDeserializationContext context) throws JsonParseException {
return LocalDateTime.parse(json.getAsString(), DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"));
}
}
}
Java
๋ณต์ฌ
์ ์ฝ๋์์ ๋จผ์ ObjectMapper๋ฅผ ๋ณด๋ฉด ๋ ์ง/์๊ฐ์ ๋ํ ๋ชจ๋์ ์ถ๊ฐํด์ค ์ง๋ ฌํ/์ญ์ง๋ ฌํ ๊ณผ์ ์์ ์๋ฌ๊ฐ ๋ฐ์ํ์ง ์๊ฒ ํด์ค๋๋ค.
๋ํ PropertyNamingStrategy๋ฅผ ์ง์ ํด์ฃผ์ด ๊ฐ๋ฐํ ๋ด์์ ์ฝ์ํ ๊ท์น์ธ SNAKE_CASE๋ก ์ค์ ํด์ค๋๋ค.
๋ง์ง๋ง์ผ๋ก @JsonIgnore๊ฐ ๋ถ์ ํ๋กํผํฐ๋ฅผ ๋ฌด์ํ๊ธฐ ์ํ ์ค์ ๊น์ง ํด์ค๋๋ค.
๊ทธ ์๋๋ถํฐ๋ Gson์ ๋ํด ์ง๋ ฌํ/์ญ์ง๋ ฌํ ๊ณผ์ ์ค ๋ ์ง/์๊ฐ์ ๋ํ ํฌ๋งท ์ค์ ์ ํด์ฃผ๋ ์ฝ๋์
๋๋ค.
๋งคํผ๋ค์ ๋ํ ์ค์ ์ด ๋๋ฌ์ผ๋ ์ด์ Controller๋ฅผ ์ดํด๋ณด๊ฒ ์ต๋๋ค.
@PutMapping(value = "/{companyId}", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
public ResponseEntity<ResponseDTO> updateCompany(HttpServletRequest request,
@RequestPart(required = false) MultipartFile logo,
@Parameter(schema = @Schema(implementation = CompanyUpdateDTOforBiz.class)) @RequestPart String json,
@PathVariable Long companyId) throws IOException {
CompanyUpdateDTOforBiz dto = objectMapper.readValue(json, CompanyUpdateDTOforBiz.class);
Validator validator = validatorFactory.getValidator();
Set<ConstraintViolation<CompanyUpdateDTOforBiz>> constraints = validator.validate(dto);
if (!constraints.isEmpty()) {
String message = constraints.stream().map(ConstraintViolation::getMessage).findFirst().get();
throw new CustomException(Arrays.stream(ErrorCode.values()).filter(e -> e.getErrorMessage().equals(message)).findFirst().get());
}
return new ResponseEntity<>(companyService.updateCompany(request, dto, logo, companyId), HttpStatus.OK);
}
Java
๋ณต์ฌ
๋จผ์ objectMapper๋ฅผ DI ๋ฐ์์ String์ผ๋ก ๋ฐ์์จ json์ DTO๋ก ๋งคํํด์ค๋๋ค.
๊ทธ ๋ค์๋ถํด Validation์ ์ง์ ํด์ฃผ๋ ๊ณผ์ ์ผ๋ก ์ ๋ฅผ ๊ดด๋กญํ๋ ๋ฌธ์ ๋ฅผ ํด๊ฒฐํ ๋ฐฉ๋ฒ์
๋๋ค.
ValidatorFactory๋ฅผ DI ๋ฐ์์ factory๋ก๋ถํฐ validator๋ฅผ ๊ฐ์ ธ์จ ํ validate() ๋ฉ์๋๋ฅผ ์ด์ฉํด ๊ฒ์ฆ ๊ณผ์ ์ ๊ฑฐ์ณ์ค๋๋ค.
๊ฒ์ฆ ๊ณผ์ ์ ๊ฒฐ๊ณผ๋ฌผ๋ก Set<ConstraintViolation<DTO>> ๋ฅผ ๋ฐ๋๋ฐ ์ด ์์๋ ๊ฒ์ฆ์์ ๋ฐ์ํ Validation ์๋ฌ๋ค์ด ๋ค์ด์์ต๋๋ค.
์ด ์ดํ๋ถํฐ๋ ๊ฐ์ธ์ ์ทจํฅ์ ๋ฐ๋ผ ์ฒ๋ฆฌํ๊ธฐ ๋๋ฆ์ด๋ผ๊ณ ์๊ฐํฉ๋๋ค.
@Getter
@AllArgsConstructor
public enum ErrorCode {
BLANK_MEMBER(404, "BLANK_MEMBER", "ํด๋น ์ ์ ๊ฐ ์กด์ฌํ์ง ์์ต๋๋ค.", HttpStatus.NOT_FOUND.name()),
WRONG_EMAIL(400, "WRONG_EMAIL", "์ฌ๋ฐ๋ฅธ ์ด๋ฉ์ผ ํ์์ ์
๋ ฅํด์ฃผ์ธ์.", HttpStatus.BAD_REQUEST.name()),
BLANK_EMAIL(400, "BLANK_EMAIL", "์ด๋ฉ์ผ์ด ๊ณต๋ฐฑ์ด๊ฑฐ๋ ์
๋ ฅ๋์ง ์์์ต๋๋ค.", HttpStatus.BAD_REQUEST.name()),
DUPLICATED_EMAIL(400, "DUPLICATED_EMAIL", "์ด๋ฏธ ๊ฐ์
๋ ์ด๋ฉ์ผ ์
๋๋ค.", HttpStatus.BAD_REQUEST.name()),
...
private final int status;
private final String errorCode;
private final String errorMessage;
private final String statusName;
}
Java
๋ณต์ฌ
์ ์ ๊ฒฝ์ฐ์๋ ๋ชจ๋ ์๋ฌ์ฝ๋์ ๋ํด์ Enum ํด๋์ค ํ๋๋ก ๊ด๋ฆฌํ๊ณ ์๊ธฐ ๋๋ฌธ์
๋ง์ฝ ๊ฒ์ฆ๊ณผ์ ์์ ์๋ฌ๊ฐ ์กด์ฌํ๋ค๋ฉด ํด๋น ์๋ฌ์ฝ๋๋ฅผ ์ฐพ์ Exception์ ํฐํธ๋ ค ์ฃผ๋ ๋ฐฉ์์ผ๋ก ์ฒ๋ฆฌํ์์ต๋๋ค.
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(CustomException.class)
protected ResponseEntity<ErrorResponseDTO> handleCustomException(CustomException e, HttpServletRequest request) {
ErrorResponseDTO error = ErrorResponseDTO.builder()
.status(e.getErrorCode().getStatus())
.errorMessage(e.getErrorCode().getErrorMessage())
.errorCode(e.getErrorCode().getErrorCode())
.statusName(e.getErrorCode().getStatusName())
.path(request.getRequestURI())
.build();
return new ResponseEntity<>(error, HttpStatus.valueOf(e.getErrorCode().getStatus()));
}
}
Java
๋ณต์ฌ
CustomException(ErrorCode errorCode) ๋ก ์์ธ๊ฐ ๋ฐ์ํ๋ฉด ๋ฏธ๋ฆฌ ์ค์ ํด์ค ExceptionHandler๊ฐ ErrorResponse๋ฅผ ๋ง๋ค์ด ์๋ตํด์ค๋๋ค.
์ด๋ ๊ฒํด์ Multipart/form-data ๋ฅผ ์ฌ์ฉํ๋ฉฐ ํด๋ผ์ด์ธํธ์ ์์ฒญ์ ๋ฐ๋ผ json์ ๊ฑด๋ผ ๋์ ํ์
์ ๋ณ๊ฒฝํ ์ ์์์ต๋๋ค.
๋ํ ์๋์น์๊ฒ ๊ทธ ๋์ Swagger์์ application/octet-stream์ผ๋ก ์ค์ ๋ผ์ ์ ์์ ์ธ ์์ฒญ์ด ์๋๋ ๊ฒ๋ ํด๊ฒฐ๋์์ต๋๋ค.