JSON 개요
@RestController JSON 응답 처리
@RequestBody JSON 요청 처리
JSON 데이터의 날짜 형식 다루기
ResponseEntity 객체 리턴, 응답 코드 지정
요청 객체 검증하기
JSON(JavaScript Object Notation)은 간단한 형식을 갖는 문자열로 데이터 교환에 주로 사용한다.
JSON 형식으로 표현한 데이터의 예)
{
"name": "새싹이들",
"birth": 2022,
"member": ["유즈", "otcr", "파오리", "스머프"]
}
Jackson은 자바 객체와 <=> JSON 형식 문자열 간 변환을 처리하는 라이브러리이다.
public class Person {
private String name;
private int age;
}
{
"name": "이름",
"age": 10
}
스프링 MVC에서 사용하려면 pom.xml에 의존을 추가한다.
<!--Jackson core와 Jackson Annotation 의존 추가-->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.4</version>
</dependency>
<!-- java8 date/time 지원하는 Jackson 모듈 -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.9.4</version>
</dependency>
스프링 MVC에서 JSON 형식으로 데이터를 응답하려면 @Controller 애노테이션 대신 @RestController를 사용한다.
스프링 MVC가 @RestController가 붙은 컨트롤러 클래스의 요청 매핑 (@___Mapping 애노테이션이 붙은) 메서드가 리턴한 객체를 알맞은 형식*으로 변환해 응답 데이터로 전송한다.
* 예) 클래스 패스에 Jackson이 존재하면 JSON 형식의 문자열로 변환
@RestController // @Controller 대신 @RestController 사용
public class RestMemberController {
private MemberDao memberDao;
private MemberRegisterService registerService;
// 요청 매핑 애노테이션 적용 메서드의 리턴 타입으로 일반 객체(List<Memeber>) 사용
@GetMapping("/api/members")
public List<Member> members() {
return memberDao.selectAll();
}
// 요청 매핑 애노테이션 적용 메서드의 리턴 타입으로 일반 객체(Memeber) 사용
@GetMapping("/api/members/{id}")
public Member member(@PathVariable Long id, HttpServeltResponse response) throws IOException {
Member member memberDao.selectById(id);
if (member == null) {
...
}
return member; // 일반 객체 리턴
}
}
...
}
응답 결과(response)에 사용자 암호 등의 민감한 정보는 포함시키면 안된다.
이때 Jackson이 제공하는 @JsonIgnore 애노테이션으로 특정 데이터를 제외할 수 있다.
예) password를 제외
import com.fasterxml.jackson.annotation.JsonIgnore;
public class Member {
private Long id;
private String email;
@JsonIgnore
private String password;
private String name;
private LocalDateTime registerDateTime;
}
지금까지 응답을 위한 자바 객체 -> JSON 변환 방법을 살펴보았다.
이제 JSON 형식의 요청 데이터 -> 자바 객체로 변환하는 방법을 알아본다.
요청 매핑 메소드의 인자 커맨드 객체에 @RequestBody 애노테이션을 붙이기만 하면 된다.
@RestController
public class RestMemberController {
private MemberDao memberDao;
private MemberRegisterService registerService;
...
@PostMapping("/api/members")
public ResponseEntity<Object> newMember(
// JSON 형식의 문자열을 RegisterRequest 타입 객체로 변환
@RequestBody @Valid RegisterRequest regReq,
HttpServletResponse response) throws IOException {
try {
Long newMemberId = registerService.regist(regReq);
response.setHeader("Location", "/api/members/" + newMemberId);
response.setStatus(HttpServletResponse.SC_CREATED);
} catch (DuplicateMemberException dupEx) {
response.sendError(HttpServletResponse.SC_CONFLICT);
}
}
...
}
자바의 LocalDateTime 타입이 JSON으로 변환될 때 배열 형태가 되기도 한다. 예) "registerDateTime": [2018, 3, 1, 11, 7, 49]
Jackson에서 날짜나 시간 값을 특정 형식으로 표현하는 방법으로 @JsonFormat 애노테이션을 사용한다.
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Shape;
public class Member {
private Long id;
private String email;
private String name;
@JsonFormat(shape = Shape.STRING) // ISO-8601 형식으로 변환
private LocalDateTime registerDateTime;
@JsonFormat(pattern = "yyyyMMddHHmmss") // 패턴 사용
private LocalDateTime registerDateTime2;
}
별도의 설정을 하지 않으면 yyyy-MM-ddTHH:mm:ss(시간대가 없는 JSR-8601 형식)의 문자열을 LocalDateTime과 Date로 변환한다.
특정 패턴을 가진 문자열을 LocalDateTime이나 Date 타입으로 변환하려면 @JsonFormat 애노테이션의 pattern 속성을 사용해서 패턴을 지정한다.
@JsonFormat(pattern="yyyyMMddHHmmss")
private LocalDateTime birthDateTime;
@JsonFormat(pattern="yyyyMMdd HHmmss")
private Date birthDate;
날짜 형식을 pattern 속성으로 매번 지정하지 않고 일괄적으로 변경하려면 스프링 MVC 설정 클래스에서 설정한다.
패턴 적용 우선순위 : 스프링 설정 클래스의 설정 < 개별 속성에 적용한 @JsonFormat 애노테이션
스프링 MVC는 자바 객체를 HTTP 응답으로 변환할 때 HttpMessageConverter를 사용한다.
예1) 자바 객체 -> Jackon -> JSON 변환 시: MappingJackson2HttpMessageConverter 사용
예2) 자바 객체 -> Jaxb -> XML 변환 시: Jaxb2RootElementHttpMessageConverter 사용
∴ 스프링 설정 클래스에 MappingJackson2HttpMessageConverter를 새롭게 등록해서 날짜 형식을 원하는 형식으로 변환하도록 설정하면 일괄적으로 적용할 수 있다.
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
...
// WebMvcConfigurer 인터페이스에 정의된 메서드로 HttpMessageConverter를 추가로 설정할 때 사용
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
// (형식 설정 예 1) ObjectMapper를 쉽게 생성하기 위해 스프링이 제공하는 클래스
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
.json()
// Jackson이 날짜 형식을 출력할 때 유닉스 타임 스탬프로 출력하는 것 비활성화
// 비활성화하면 ObjectMapper는 날짜 타입의 값을 ISO-8601 형식으로 출력함
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();
// (형식 설정 예 2) ISO-8601 형식 대신 다른 패턴 직접 설정
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
.json()
// LocalDateTime 타입 -> JSON
.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(formatter))
// JSON -> Date 타입
.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(formatter))
.simpleDateFormat("yyyyMMdd HHmmss")
.build();
// 새로 생성한 HttpMessageConverter를 목록의 제일 앞에 위치시켜서 Jackson 보다 먼저 적용되도록함
converters.add(0, new MappingJackson2HttpMessageConverter(objectMapper));
}
}
지금까지 예제 코드는 상태 코드를 지정하기 위해 HttpServletResponse의 setStatus() 메서드와 sendError() 메서드를 사용했다. 문제는 이와 같이 404 응답을 하면 JSON 형식이 아닌 HTML을 응답 결과로 전송한다.
404나 500과 같이 처리에 실패한 경우 HTML 응답 데이터 대신에 JSON 형식의 응답 데이터를 전송해야 API 호출 프로그램이 일관된 방법으로 응답을 처리할 수 있을 것이다.
정상인 경우, 비정상인 경우 모두 JSON 응답을 전송하는 방법이다.
다음은 에러 상황일 때 응답으로 사용할 ErrorResponse 클래스이다.
package controller;
public class ErrorResponse {
private String message;
public ErrorResponse(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
이를 이용해 member() 메서드를 다음과 같이 구현할 수 있다.
스프링 MVC는 리턴 타입이 ResponseEntity이면 ResponseEntity의 body로 지정한 객체를 사용해 변환을 처리한다.
예) member를 body를 지정했으므로 member 객체가 JSON으로 변환됨
@RestController
public class RestMemberController {
private MemberDao memberDao;
private MemberRegisterService registerService;
@GetMapping("/api/members/{id}")
public ResponseEntity<Object> member(@PathVariable Long id) {
Member member = memberDao.selectById(id);
if (member == null) {
return ResponseEntity
.status(HttpStatus.NOT_FOUND) // 404(NOT_FOUND) 응답 상태 코드로 지정
.body(new ErrorResponse("no member")); // ErrorResponse 객체를 JSON으로 변환
}
// (1) 응답 상태 코드 지정, body 지정
return ResponseEntity
.status(HttpStatus.OK) // 200(OK)를 응답 상태 코드로 지정
.body(member); // member 객체를 body로 지정. member 객체를 JSON으로 변환
// (2) 위와 동일한 코드
return ResponseEntity.ok(member);
// (3) body가 없는 경우 build()
return ResponseEntity.status(HttpStatus.NOT_FOUND).build();
// (4) 위와 동일한 코드
return ResponseEntity.notFound().build();
}
...
}
상태 코드와 함께 헤더를 전송하기
response.setHeader("Location", "/api/members/" + newMemberId);
response.setStatus(HttpServletResponse.SC_CREATED);
한 메서드에서 정상 응답과 에러 응답을 ResponseBody로 생성하면 중복되는 코드가 많아질 수 있다.
@ExceptionHandler 애노테이션을 적용한 메서드에서 에러 응답을 처리하도록 구현하면 중복을 없앨 수 있다.
익셉션이 발생하면 아래 메서드가 에러를 처리하는 방식이다.
@GetMapping("/api/members/{id}")
public Member member(@PathVariable Long id) {
Member member = memberDao.selectById(id);
if (member == null) {
throw new MemberNotFoundException();
}
return member;
}
@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNoData() {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorRespsone("no member"));
}
@RestControllerAdvice 애노테이션을 이용해 에러 처리 코드를 별도 클래스로 분리할 수도 있다. (@ExceptionHandler만 모아두는 클래스를 만드는 것이다.)
@ControllerAdvice와 동일하며, @RestController와 비슷하게 응답을 JSON이나 XML 형식으로 변환한다.
이를 이용하면 에러 처리 코드를 한 곳에 모아 효과적으로 에러 응답을 관리할 수 있다.
@RestControllerAdvice("controller")
public class ApiExceptionAdvice {
@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNoData() {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("no member"));
}
}
JSON 형식으로 전송한 데이터를 변환한 객체도 동일한 방식으로 @Valid 애노테이션이나 별도 Validator를 사용해 검증할 수 있다.
@Valid 애노테이션을 사용한 경우 검증에 실패하면 400(Bad Request) 상태 코드를 응답한다.
@PostMapping("/api/members")
public ResponseEntity<Object> newMember(
@RequestBody @Valid RegisterRequest regReq,
HttpServletResponse response) throws IOException {
...
}
@Valid 검증 실패 시 400 코드를 응답하며 이때 HTML을 기본으로 제공한다.
대신 JSON 형식 응답을 제공하려면 Errors 타입 파라미터를 추가해 직접 에러 응답을 생성하면 된다.
@PostMapping("/api/members")
public ResponseEntity<Object> newMember(
@RequestBody @Valid RegisterRequest regReq, Errors errors) {
// 검증 에러가 존재하는지 확인
if (errors.hasErrors()) {
// 모든 에러 정보를 구하고, 각 에러 코드 값을 연결한 문자열을 생성, errorCodes 변수에 할당
String errorCodes = errors.getAllErrors() // List<ObjectError>
.stream()
.map(error -> error.getCodes()[0]) // error는 ObjectError
.collect(Collectors.joining(","));
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("errorCodes = " + errorCodes));
}
...
}
@ExceptionHandler 애노테이션을 이용해 검증 실패 시의 에러 응답을 생성할 수도 있다.
@RestControllerAdvice("controller")
public class ApiExceptionAdvice {
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleBindException(MethodArgumentNotValidException ex) {
String errorCodes = ex.getBindingResult().getAllErrors()
.stream()
.map(error -> error.getCodes()[0])
.collect(Collectors.joining(","));
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("errorCodes = " + errorCodes));
}
}
1. JSON(JavaScript Object Notation)은 간단한 형식을 갖는 문자열로 (데이터 교환)에 주로 사용한다. 중괄호를 사용하여 객체를 표현하고, 객체는 (이름, 값) 쌍을 갖는다.
2. 스프링 MVC에서 JSON 형식으로 데이터를 응답하려면 (@Controller) 애노테이션 대신 (@RestController)를 사용한다.
3. JSON 형식의 요청 데이터를 자바 객체로 변환하는 방법: 요청 매핑 메소드의 인자 (커맨드 객체)에 (@RequestBody) 애노테이션을 붙인다.
4. 특정 패턴을 가진 문자열을 LocalDateTime이나 Date 타입으로 변환하려면 (@JsonFormat) 애노테이션의 (pattern) 속성을 사용해서 패턴을 지정한다.
5. 스프링 MVC는 리턴 타입이 (ResponseEntity)이면 그것의 (body)로 지정한 객체를 사용해 변환을 처리한다.
6. (@ExceptionHandler) 애노테이션을 적용한 메서드에서 에러 응답을 처리하도록 구현하면 중복을 없앨 수 있다.
7. (@RestControllerAdvice) 애노테이션을 이용해 에러 처리 코드를 별도 클래스로 분리할 수도 있다. 이는 (@ControllerAdvice)와 동일하며, (@RestController)와 비슷하게 응답을 JSON이나 XML 형식으로 변환한다.
8. @Valid 검증 실패 시 400 코드를 응답하며 이때 (HTML)을 기본으로 제공한다.
대신 JSON 형식 응답을 제공하려면 (Errors 타입 파라미터)를 추가해 직접 에러 응답을 생성하면 된다.
Corner Spring #2
Editor : 유즈
[스프링2] 15장. 간단한 웹 어플리케이션의 구조 (0) | 2023.02.02 |
---|---|
[스프링2] 14장. MVC 4: 날짜 값 변환, @PathVariable, 익셉션 처리 (0) | 2023.01.12 |
[스프링2] 13. MVC 3: 세션, 인터셉터, 쿠키 (0) | 2023.01.05 |
[스프링2] 12장. MVC 2: 메시지, 커맨드 객체 검증 (0) | 2022.12.29 |
[스프링2] 11장. MVC 1 : 요청, 매핑, 커맨드 객체, 리다이렉트, 폼 태그, 모델 (2) (0) | 2022.12.22 |