JSON(JavaScriprt Object Notation)은 Javascript 객체 문법으로 구조화된 데이터를 표현하기 위한 text 기반의 표준 포맷으로, 웹 어플리케이션에서 데이터를 전송할 때 일반적으로 사용된다.
JSON 사용 규칙
- 중괄호를 사용하여 객체를 표현한다.
- 객체는 (이름, 값) 쌍을 가지며, 이름과 값은 콜론(:)으로 구분한다.
- 값으로 사용 가능한 타입은 다음과 같다.
- 문자열, 숫자, boolean, null
- 배열: 대괄호로 표현
- 객체
Jackson은 Java 객체와 JSON 형식 문자열 간 변환을 처리하는 라이브러리이다. Spring MVC에서 java 객체를 JSON 형식으로 변환하기 위해서는 pom.xml에 아래와 같은 Jackson 관련 의존을 추가한다.
<!-- Jackson core와 Jackson Annotation 의존 추가 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.16.1</version>
</dependency>
<!-- java8 이후 date/time 지원용 Jackson 모듈 -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.16.1</version>
</dependency>
Spring MVC에서 java 객체를 JSON 형식으로 변환하는 작업을 직렬화(serialize)라고 하며, 이는 @RestController이 붙은 컨트롤러 클래스에 JSON 형식으로 변환해주는 요청 처리 메서드를 작성하는 방식으로 수행한다. 해당 요청 처리 메서드의 리턴값은 객체로, 이 리턴값을 JSON 형식의 객체 또는 객체 배열로 변환하는 방식으로 응답이 이루어진다.
주요 어노테이션
@RestController
이 어노테이션은@Controller대신 사용한다. 이 어노테이션이 붙은 컨트롤러 클래스에서는 요청 매핑 어노테이션을 붙인 메서드가 리턴한 객체를 JSON 형식의 응답 데이터로 전송한다.
RestMemberController
package controller;
... 코드 생략
/* @Controller 대신 @RestController 사용 */
@RestController
public class RestMemberController {
private MemberDao memberDao;
private MemberRegisterService registerService;
/* List 객체 리턴 시 JSON 형식 객체 배열로 변환 */
@GetMapping("/api/members")
public List<Member> members() {
return memberDao.selectAll();
}
... 코드 생략
/* 일반 객체 리턴 시 JSON 형식 객체로 변환 */
@GetMapping("/api/members/{id}")
public ResponseEntity<Object> member(@PathVariable(value = "id") Long id) {
Member member = memberDao.selectById(id);
if (member == null) {
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("no member"));
}
return ResponseEntity.status(HttpStatus.OK).body(member);
}
public void setMemberDao(MemberDao memberDao) {
this.memberDao = memberDao;
}
public void setRegisterService(MemberRegisterService registerService) {
this.registerService = registerService;
}
}
ControllerConfig
@Configuration
public class ControllerConfig {
... 코드 생략
@Bean
public RestMemberController restApi() {
RestMemberController cont = new RestMemberController();
cont.setMemberDao(memberDao);
cont.setRegisterService(memberRegSvc);
return cont;
}
}
@JsonIgnore
이 어노테이션이 붙은 필드는 JSON 응답에서 제외된다. 이 어노테이션은 암호와 같은 민감한 데이터를 응답 결과에서 제외할 때 활용된다.
주요 어노테이션
@JsonFormat
이 어노테이션을 날짜/시간 타입 필드에 사용하여 해당 필드의 값을 이 어노테이션에서 지정한 형식대로 변환한 다음 출력할 수 있다.
기본 적용 설정
날짜 형식을 변환할 모든 대상마다 @JsonFormat을 적용하는 작업은 매우 번거롭다. 이러한 번거로움을 피하려면 날짜/시간 타입인 모든 대상에게 동일한 변환 규칙을 적용할 수 있어야 한다. 이를 위해서는 WebMvcConfigurer 인터페이스를 상속한 Spring 설정 클래스를 통해 Spring MVC 설정을 변경해야 한다.
HttpMessageConverter 인터페이스
Spring MVC에서 Java 객체를 HTTP 응답으로 변환하는 기능을 제공하는 인터페이스로, HTTP 응답에 사용할 변환 형식에 따라 사용하는 구현 클래스가 다르다.
MappingJackson2HttpMessageConverter
: Jackson을 이용하여 java 객체를 JSON으로 변환할 때 사용Jaxb2RootElementHttpMessageConverter
: Jaxb를 이용하여 java 객체를 xml로 변환할 때 사용
MvcConfig
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
... 코드 생략
/* java 객체를 json으로 변환 시 날짜 데이터에 적용할 설정 */
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
/* 1. 사용하고자 하는 패턴 지정 */
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/* 2. JSON 변환에 사용될 ObjectMapper 생성 */
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
.json()
// UNIX timestamp 비활성화
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
// LocalDateTime 타입용 JSON 변환 형식
.serializerByType(LocalDateTime.class, new LocalDateSerializer(formatter))
// Date 타입용 JSON 변환 형식
.simpleDateFormat("yyyyMMdd HHmmss")
// ObjectMapper 생성
.build();
/* 3. 생성한 ObjectMapper를 converters에 추가하여 설정한 날짜 형식 적용 */
converters.add(0,
new MappingJackson2HttpMessageConverter(objectMapper));
}
}
JSON 데이터를 java 객체로 변환하는 작업을 역직렬화(deserialize)라고 한다. Spring MVC에서의 역직렬화는 요청 처리 메서드의 파라미터로 사용된 커맨드 객체에 @ResponseBody 어노테이션을 적용하는 방식으로 수행된다.
주요 어노테이션
@ResponseBody
이 어노테이션은 요청 처리 메서드의 파라미터인 커맨드 객체에 사용할 수 있다. POST/PUT 방식으로 전송받은 JSON 데이터를 이 어노테이션이 붙은 커맨드 객체와 동일한 타입의 객체로 변환한다.
RestMemberController
@RestController
public class RestMemberController {
... 코드 생략
@PostMapping("/api/members")
public void newMember2(
/* regReq에 JSON 데이터를 RegisterRequest 타입으로 변환하여 전달 */
@RequestBody RegisterRequest regReq,
HttpServletResponse response) throws IOException {
try {
Long newMemberId = registerService.regist(regReq);
response.setHeader("Location", "/api/members/" + newMemberId);
/* HTTP status 201 (POST 요청 및 응답 성공) */
response.setStatus(HttpServletResponse.SC_CREATED);
} catch (DuplicateMemberException dupEx) {
/* HTTP status 409 (중복되는 값으로 인한 오류) */
response.sendError(HttpServletResponse.SC_CONFLICT);
}
}
... 코드 생략
}
역직렬화에서도 직렬화를 수행할 때와 동일하게 @JsonFormat 어노테이션을 사용하거나, Jackson2ObjectMapperBuilder.deserializerByType() 메서드를 사용하는 방식으로 JSON 데이터를 날짜 형식으로 변환하는 것이 가능하다.
MvcConfig
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
... 코드 생략
/* java 객체를 json으로 변환 시 날짜 데이터에 적용할 설정 */
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
/* 1. 날짜/시간 패턴 설정 */
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
/* 2. JSON 데이터를 java 객체로 변환할 때 사용할 ObjectMapper 생성 */
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
.json()
// UNIX timestamp 비활성화
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
// JSON을 LocalDateTime 타입으로 변환할 시 적용할 형식
.deserializerByType(LocalDateTime.class, new LocalDateDeserializer(formatter))
// JSON을 Date 타입으로 변환할 시 적용할 형식
.simpleDateFormat("yyyyMMdd HHmmss")
// 날짜 형식 설정을 반영한 ObjectMapper 생성
.build();
/* 3. 생성한 ObjectMapper를 converters에 추가 */
converters.add(0,
new MappingJackson2HttpMessageConverter(objectMapper));
}
}
@RequestBody 어노테이션이 적용된, JSON 형식으로 변환한 데이터를 전달받는 객체 역시 @Valid 어노테이션이나 별도의 Validator를 이용하여 검증하는 것이 가능하다.
RestMemberController
@RestController
public class RestMemberController {
private MemberDao memberDao;
private MemberRegisterService registerService;
... 코드 생략
@PostMapping("/api/members")
public void newMember(
/* @Valid를 사용한 커맨드 객체에 관한 유효성 검증 수행 */
@RequestBody @Valid RegisterRequest regReq,
HttpServletResponse response,
Errors errors) throws IOException {
/* regReq에 관한 검증 실패 시 HTTP status 400으로 응답*/
if (errors.hasErrors()) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
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);
}
}
... 코드 생략
}
Spring MVC를 이용한 API 호출에서는 HttpServletResponse 타입을 이용하여 요청에 대해 응답할 경우, 요청한 JSON 결과가 존재하지 않으면 서버가 기본 제공하는 HTML을 응답 결과로 제공한다. 하지만 API 호출 프로그램에서 JSON 응답과 HTML 응답을 모두 처리하기보다는 JSON을 통해 일관된 방법으로 응답을 처리하도록 하는 것이 성능 개선 측면에서 더 좋다.
Spring MVC에서 정상인 경우와 비정상인 경우 모두 JSON 응답을 전송하는 방법으로, 리턴 타입을 ResponseEntity로 지정하는 방법이 있다. 이때 리턴 타입이 ResponseEntity이면 ResponseEntity의 body() 메서드에서 지정한 객체를 사용하여 JSON 변환을 처리한다.
ResponseEntity.status(상태코드).build(객체)
status(상태코드): HttpStatus 열거 타입에 정의된 값을 사용body(객체): 지정한 객체의 데이터를 JSON으로 변환ResponseEntity.status().build()
build()
: JSON으로 변환할 별도의 객체가 없을 때 사용하여 응답 코드를 그대로 JSON으로 변환한다. 해당 메서드 사용 시status()를noContent(),badRequest(),notFound()등으로 대체할 수 있다.
@RestController
public class RestMemberController {
... 코드 생략
/* id값을 이용한 회원 조회 결과를 JSON으로 변환 */
@GetMapping("/api/members/{id}")
public ResponseEntity<Object> member(@PathVariable(value = "id") Long id) {
Member member = memberDao.selectById(id);
if (member == null) {
/* 1. 조회된 데이터가 존재하지 않으면 에러를 JSON으로 변환 */
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("no member"));
}
/* 2. 조회된 데이터가 존재하면 해당 데이터를 JSON으로 변환 */
return ResponseEntity.status(HttpStatus.OK).body(member);
}
... 코드 생략
}
JSON 변환에 관한 메서드에서도 @ExceptionHandler을 적용한 예외 처리 메서드를 통해 예외 응답을 처리하도록 구현함으로써 코드의 중복을 없앨 수 있다. 또한@RestControllerAdvice 어노테이션을 사용하여 에러 처리 코드를 하나의 클래스에 모음으로써 효과적으로 에러 응답을 관리할 수 있다.
RestControllerAdvice("적용할 패키지")
: 이 어노테이션이 사용된 클래스의 메서드는 지정한 패키지와 그 하위 패키지에 속한 컨트롤러 클래스에 대해@ControllerAdvice와 같은 역할을 수행한다. 또한 그 응답 결과를 JSON이나 XML 등의 형식으로 변환하는 작업을 수행한다.
ApiExceptionAdvice
@RestControllerAdvice("controller")
public class ApiExceptionAdvice {
@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNoData() {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("no member"));
}
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleBindException(
MethodArgumentNotValidException ex) {
String errorCodes = ex.getBindingResult().getAllErrors()
.stream()
.map(error -> Objects.requireNonNull(error.getCodes())[0])
.collect(Collectors.joining(","));
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("errorCodes = " + errorCodes));
}
}
@Valid 어노테이션이 붙은 커맨드 객체가 값 검증에 실패하면 400 상태 코드를 응답한다. 만약 @Valid를 이용한 검증에 실패했을 때, HTML 대신 JSON 형식의 응답을 제공하고 싶다면 다음의 방법들이 존재한다.
RestMemberController
@RestController
public class RestMemberController {
... 코드 생략
/* 1. 첫번째 파라미터로는 커맨드 객체, 두번째 파라미터로 Errors 객체 사용 필수 */
public ResponseEntity<Object> newMember(
@RequestBody @Valid RegisterRequest regReq,
Errors errors) {
/* 2. @Valid에 의한 검증 시 에러 검출 */
if (errors.hasErrors()) {
/* 3. 모든 에러 정보 추출 후 각 에러의 코드값을 나열한 문자열 생성 */
String errorCodes = errors.getAllErrors()
.stream()
.map(error -> Objects.requireNonNull(error.getCodes())[0])
.collect(Collectors.joining(","));
/* 4. 응답이 Http status 400 코드일 때 JSON 형식으로 에러 정보 전송 */
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("errorCode = " + errorCodes));
}
/* 검증 시 에러 없이 정상 실행 */
try {
Long newMemberId = registerService.regist(regReq);
URI uri = URI.create("/api/members/" + newMemberId);
return ResponseEntity.created(uri).build();
} catch (DuplicateMemberException dupEx) {
return ResponseEntity.status(HttpStatus.CONFLICT).build();
}
}
... 코드 생략
}
@RestControllerAdvice & @ExceptionHandler 활용@RequestBody와 @Valid를 사용한 객체의 검증에 실패했을 때, 이를 객체로 삼는 요청 처리 메서드에 Errors 타입 파라미터가 존재하지 않으면 MethodArgumentNotValidException이 발생하는 것을 이용한 방법이다. 이 방법에서는 다음과 같이 일련의 코드를 작성한다.
@RestControllerAdvice적용 컨트롤러에@ExceptionHandler적용 예외 처리 메서드 추가
:@ExceptionHandler의 인자는MethodArgumentNotValidException.class로 설정하고,
메서드의 리턴값은ResponseEntity<에러 응답용 클래스>로 지정한다.- 1.의 컨트롤러에 관한 Bean을 Spring MVC 전체 설정 클래스에 추가
: 이 작업까지 수행해야 실제 POST 요청 시 ControllerAdvice가 정상 동작
RestMemberController
@RestController
public class RestMemberController {
... 코드 생략
@PostMapping("/api/members")
public ResponseEntity<Object> newMember(
/* 커맨드 객체인 regReq에 @RequestBody와 @Valid를 모두 사용 */
@RequestBody @Valid RegisterRequest regReq) {
try {
Long newMemberId = registerService.regist(regReq);
URI uri = URI.create("/api/members/" + newMemberId);
return ResponseEntity.created(uri).build();
} catch (DuplicateMemberException dupEx) {
return ResponseEntity.status(HttpStatus.CONFLICT).build();
}
}
... 코드 생략
}
ApiExceptionAdvice
@RestControllerAdvice("controller")
public class ApiExceptionAdvice {
... 코드 생략
/* POST 요청 메서드에서 Errors 타입 파라미터 미사용 */
@ExceptionHandler(MethodArgumentNotValidException.class)
public ResponseEntity<ErrorResponse> handleBindException(
MethodArgumentNotValidException ex) {
/* 발생한 에러를 나열한 문자열 생성 */
String errorCodes = ex.getBindingResult().getAllErrors()
.stream()
.map(error -> Objects.requireNonNull(error.getCodes())[0])
.collect(Collectors.joining(","));
/* 생성한 문자열을 값으로 삼는 에러 응답용 객체를 JSON 형식으로 변환 */
return ResponseEntity
.status(HttpStatus.BAD_REQUEST)
.body(new ErrorResponse("errorCodes = " + errorCodes));
}
}
public class ErrorResponse {
private String message;
public ErrorResponse(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
MvcConfig : @RestControllerAdvice 적용 컨트롤러 관련 설정 추가
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
... 코드 생략
/* @RestControllerAdvice 적용 컨트롤러를 Bean으로 등록해야 사용 가능 */
@Bean
public ApiExceptionAdvice apiExceptionAdvice() {
return new ApiExceptionAdvice();
}
}