JSON 개요
JSON(JavaScript Object otation)은 간단한 형식을 갖는 문자열로 데이터 교환에 주로 시용한다. 다음은 JSON 형식으로 표현한 데이터의 예이다.
{
"name": “유관순",
"birthday": "1902-12-16",
"age": 17,
"related": "남동순" , ” 류예도"]
"edu": [
{
"title:": ”이화학당보통과",
"year": 1916
},
{
"title": ”이화학당고등과",
"year": 1919
}
]
}
Jackson 의존 설정
Jackson은 자바 객체와 JSON 형식 문자열 간 변환을 처리하는 라이브러리이다. 스프링 MVC에서 Jackson 라이브러리를 이용해서 자바 객체를 JSON으로 변환하려면 클래스 패스에 Jackson 라이브러리를 추가하면 된다.
implementation 'com.fasterxml.jackson.core:jackson-databind:2.9.4'
implementation 'com.fasterxml.jackson.datatype:jackson-datatype-jsr310:2.9.4'
@RestController로 JSON 형식 응답
스프링 MVC에서 JSON 형식으로 데이터 를 응답하는 것은 매우 간단하다. @Controller 애노테이션 대신 @RestController 애노테이션을 사용하면 된다.
package controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RestController;
import spring.Member;
import spring.MemberDao;
import spring.MemberRegisterService;
import java.util.List;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RestController
public class RestMemberController {
private MemberDao memberDao;
private MemberRegisterService registerService;
@GetMapping("/api/members")
public List<Member> members() {
return memberDao.selectAll();
}
@GetMapping("/api/members/{ id }")
public Member member(@PathVariable Long id,
HttpServletResponse response) throws IOException {
Member member = memberDao.selectById(id);
if (member == null) {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
return null;
}
return member;
}
public void setMemberDao(MemberDao memberDao) {
this.memberDao = memberDao;
}
public void setRegisterService(MemberRegisterService registerService) {
this.registerService = registerService;
}
}
@RestController 애노테이션을 붙인 경우 스프링 MVC는 요청 매핑 애노테이션을 붙인 메서드가 리턴한 객체를 알맞은 형식으로 변환해서 응답 데이터로 전송한다. 이때 클래스에 Jackson이 존재하면 JSON 형식의 문자열로 변환해서 응답한다.
@RestController 애노테이션이 추가되기 전에는 다음과 같이 @Controller 애노테이션과 @ResponseBody 애노테이션을 사용했다.
@Controller
public class RestMemberController {
private MemberDao memberDao;
private MemberRegisterService registerService;
@RequestMapping(path="/api/members", method = RequestMethod.GET)
@ResponseBody
public List<Member> members() {
return memberDao.selectAll();
}
}
@Jsonlgnore를 이용한 제외 처리
응답 결과에 password가 포함되어 있다. 보통 암호와 같이 민감한 데이터는 응답 결과에 포함시키면 안되므로 password 데이터를 응답 결과에서 제외시켜야 한다.
public class Member {
private Long id;
private String email;
@Jsonlcnore
private String password;
private String name;
private LocalDateTime registerDateTime;
}
날짜 형식 변환 처리: @JsonFormat 사용
registerDateTime의 값은 [2018, 3, 1, 11, 7, 49]이다. Member 클래스의 registerDateTime 속성은 LocalDateTime 타입인데 JSON 값은 배열로 바뀌었다. 만약 registerDateTime 속성이 java.util.Date타입이면 다음과 같이 유닉스 타임 스탬프로 날짜 값을 표현한다.
"registerDateTime": 1519870069000 // 유닉스 타임 스태프는 1970년 1월 1일 이후 흘러간 시간을 말한다.
@JsonFormat(shape= JsonFormat.Shape.STRING)// ISO—8601 형식
private LocalDateTime registerDateTime;
// 출력 값
// "registerDateTime": "2023-02-23T16:36:47"
@JsonFormat(pattern="yyyyMMddHHmmss") // 패턴지정
private LocalDateTime registerDateTime;
// 출력 값
// "registerDateTime": "20230223163647"
날짜 형식 변환 처리 : 기본 적용 설정
날짜 타입에 해당하는 모든 대상에 동일한 변환 규칙을 적용할 수 있어야 한다. @JsonFormat 애노테이션을 사용하지 않고 Jackson의 변환 규칙을 모든 날짜 타입에 적용하려면 스프링 MVC 설정을 변경해야 한다.
스프링 MVC는 자바 객체를 HTTP 응답으로 변환할 때 HttpMessageConverter라는 것을 사용한다. 예를 들어 Jackson을 이용해서 자바 객체를 JSON으로 변환할 때에는 MappingJackson2HttpMessageConverter를 사용하고 Jaxb를 이용해서 XML로 변환할 때에는 Jaxb2RootElementHttpMessageConverter를 사용한다. 따라서 JSON 으로 변환할 때 시용하는 MappingJackson2HttpMessageConverter를 새롭게 등록해서 날짜 형식을 원하는 형식으로 변환하도록 설정하면 모든 날짜 형식에 동일한 변환 규칙을 적용할수있다.
- MVCConfig
@Override
// WebMVCConfigurer 인터페이스에 정의된 메서드
// HttpMessageConverter를 추가로 설정할 때 사용
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
.json() // 유닉스 타임 스탬프로 출력하는 기능 비활성화
// 자동으로 ISO-8601 형식으로 출력한다.
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
//.simpleDateFormat("yyyyMMddHHmmss") Date를 위한 변환 패턴
.build();
// 새로 생성한 컨버터를 가장 먼저 적용시켜주기 위해 0 인덱스에 추가한다.
converters.add(0,
new MappingJackson2HttpMessageConverter(objectMapper));
}
simpleDateFormat()는 LocalDateTime 타입 변환에 사용할 수 없다. 대신 LocalDateTime 타입은 ISO-8601 형식으로 변환한다.
LocalDateTime 타입에 대해 패턴을 지정하고 싶다면, serializerByType()메서드를 이용하면 된다.
@Override
// WebMVCConfigurer 인터페이스에 정의된 메서드
// HttpMessageConverter를 추가로 설정할 때 사용
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
.json()
.serializerByType(LocalDateTime.class,
new LocalDateTimeSerializer(formatter))
.build();
converters.add(0,
new MappingJackson2HttpMessageConverter(objectMapper));
}
RequestBody로 JSON 요청 처리
@PostMapping("/api/members")
public void newMember(
@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);
}
}
@RequestBody 애노테이션을 커맨드 객체에 붙이면 JSON 형식의 문자열을 해당 자바 객체로 변환한다.
JSON 데이터의 날짜 형식 다루기
특정 패턴을 가진 문자열을 LocalDateTime 이나 Date 타입으로 변환하고 싶다면 @JsonFormat 애노테이션의 pattern 속성을 시용해서 패턴을 지정한다.
@JsonFormat(pattern = "yyyyMMddHHmmss")
private LocalDateTime birthDateTime;
@JsonFormat(pattern = "yyyyMMdd HHmmss")
private Date birthDate:
특정 속성 이 아니라 해당 타입을 갖는 모든 속성에 적용하고 싶다면 스프링 MVC 설정을 하면 된다. 다음은 설정의 예이다.
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
.json()
.deserializerByType(LocalDateTime.class,
new LocalDateTimeDeserializer(formatter))
.simpleDateFormat("yyyyMMdd HHmmss")
.build();
converters.add(0,
new MappingJackson2HttpMessageConverter(objectMapper));
}
deserializerByType( )은 JSON 데이터를 localDateTime 타입으로 변환할 때 사용할 패턴을 지정하고 simpleDateFormat( )은 Date 타입으로 변환할 때 시용할 패턴을 지정한다.
요청 객체 검증하기
@PostMapping("/api/members")
public void newMember(
@RequestBody @Valid RegisterRequest regReq,
HttpServletResponse response) throws IOException {
… 생략
}
JSON 형식으로 전송한 데이터를 변환한 객체도 동일한 방식으로 @Valid 애노테이션이 나 별도 Validator 이용해서 검증할 수 있다. 검증에 실패하면 404상태코드를 사용한다.
Validator를 사용할 경우 다음과 같이 직접 상태 코드를 처리 해야 한다.
@PostMapping("/api/members")
public void newMember( @RequestBody RegisterRequest regReq, Errors errors,
HttpServletResponse response) throws IOException {
try {
new RegisterRequestValidatorO.validate(regReq, errors),
if (errors.hasErrors()) {
response.sendError(HttpServletResponse.SC_BAD_REQUEST);
return;
}
....
} catch (DuplicateMemberException dupEx) {
response.sendError(HttpServletResponse.SC_CONFLICT) ;
}
}
ResponseEntity로 객체 리턴하고 응답 코드 지정하기
ResponseEntity를 이용한 응답 데이터 처리
정상인 경우와 비정상인 경우 모두 JSON 응답을 전송하는 방법은 ResponseEntity를 사용하는 것이다.
public class ErrorResponse {
private String message;
public ErrorResponse(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
@GetMapping("/api/members/{id}")
public ResponseEntity<Object> member(@PathVariable Long id) throws IOException {
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);
}
스프링 MVC는 리턴 타입이 ResponseEntity이면 ResponseEntity의 body로 지정한 객체를 사용해서 변환을 처리한다.
//ResponseEntity를 생성하는 기본 방법은 status와 body를 이용해서 상태 코드와 JSON
// 으로 변환할 객체를 지정하는 것이다.
ResponseEntity.status(상태 코드).body(객체)
//200(0K) 응답 코드와 몸체 데이터를 생성할 경우 다음과 같이 ok() 메서드를 이용해서
// 생성할 수도 있다.
ResponseEntity.ok(member)
//만약 몸체 내용이 없다면 다음과 같이 body를 지정하지 않고 build()로 바로 생성한다.
ResponseEntity.status(HttpStatus.NOT_FOUND).build()
//몸체 내용이 없는 경우 status() 메서드 대신에 다음과 같이 관련 메서드를 사용해도 된다.
ResponseEntity.notFound().build()
몸체가 없을 때 status() 대신 사용할 수 있는 메서드는 다음과 같다.
- noContent() : 204
- badRequest(): 400
- notFound(): 404
response.setHeader("location", "/api/members/" + newMemberld);
response.setStatus(HttpServletResponse.SC_CREATED);
위 코드는 아래와 같이 바꿀 수 있다.
@PostMapping("/api/members")
public ResponseEntity<Object> newMember(
@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();
}
}
@ExceptionHandler 적용 메서드에서 ResponseEntity로 응답하기
중복되는 오류가 있다면 @ExceptionHandler 애노테이션을 적용한 메서드에서 처리가 가능하게 만들 수 있다.
@GetMapping("/api/members/{id}")
public ResponseEntity<Object> member(@PathVariable Long id) throws IOException {
Member member = memberDao.selectById(id);
if (member == null) {
throw new MemberNotFoundException();
}
return ResponseEntity.status(HttpStatus.OK).body(member);
}
@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNodata(){
return ResponseEntity.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("no member"));
}
@RestControllerAdvice 애노테이션을 이용해서 에러 처리 코드를 클래스로 분리 할 수도 있다. @RestControlJAdvice 애노테이션은 @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"));
}
}
@Valid 에러 결과를 JSON으로 응답하기
@Valid 애노테이션을 이용한 검증에 실패했을 때 HTML 응답 데이터 대신에 JSON 형식 응답을 제공하고 싶다면 다음과 같이 Errors 타입 파라미터를 추가해서 직접 에러 응답을 생성하면 된다.
public ResponseEntity<Object> newMember(
@RequestBody @Valid RegisterRequest regReq, Errors errors){
if(errors.hasErrors()){
String errorCodes = errors.getAllErrors()
.stream()
.map(error -> error.getCodes()[0])
.collect(Collectors.joining(","));
return ResponseEntity.status(HttpStatus.BAD_REQUEST).body(new ErrorResponse("errorCodes =" + errorCodes));
}
검증 에러가 존재하면 getAllErrors() 메서드로 모든 에러 정보를 구하고(ErrorObject 타입의 객 체 목록), 각 에러의 코드 값을 연결한 문자열을 생성해서 errorCodes 변수에 할당한다.
@Valid 애노테이션을 붙인 객체의 검증에 실패했을 때 Errors 타입 파라미터가 존재하지 않으면 MethodArgumentNotValidException이 발생한다. 따라서 다음과 같이 @ExceptionHandler 애노테이션을 이용해서 검증 실패시 에러 응답을 생성해도 된다.
Reference
초보 웹개발자를 위한 스프링5 프로그래밍 입문
'Spring > 스프링5 프로그래밍 입문' 카테고리의 다른 글
초보 웹 개발자를 위한 스프링5 프로그래밍 입문 REVIEW (1) | 2023.04.17 |
---|---|
Chapter 17. 프로필과 프로퍼티 파일 (0) | 2023.04.14 |
Chapter 15. 간단한 웹 어플리케이션 구조 (0) | 2023.04.12 |
Chapter 14. MVC 4: 날짜 값 변환, @PathVariable, 익셉션 처리 (0) | 2023.04.11 |
Chapter 13. 세션, 인터셉터, 쿠키 (0) | 2023.04.10 |