자바 객체
와 JSON
형식 문자열 간 변환
을 처리하는 라이브러리로 다음과 같이 pom.xml에 의존을 추가
<!-- pom.xml -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.4</version>
</dependency>
<!-- java8 date/time -->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
<version>2.9.4</version>
</dependency>
Spring MVC에서 JSON
형식으로 데이터를 응답하는 방법은 @Controller 대신 @RestController
를 사용
// RestMemberController.java
// 기존의 @Controller 대신 새로운 어노테이션 사용
@RestController
public class RestMemberController {
private MemberDao memberDao;
private MemberRegisterService registerService;
/*
* 다음 두 매서드에서 기존의 String 형태의 뷰 이름을 리턴하는 것이 아니라,
* 일반 객체를 리턴함
*/
@GetMapping("/api/members")
public List<Member> members() {
return memberDao.selectAll();
}
@GetMapping("/api/members2/{id}")
public Member member2(@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;
}
...
@RestController
어노테이션을 붙인 경우 스프링 MVC는 요청 매핑 어노테이션을 붙인 객체가 리턴한 객체를알맞은 형식
으로 변환해서 응답 데이터로 전송Jackson
이 존재하면 JSON
형식의 문자열로 변환해서 응답
스프링 4버전 이전에는 @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(); } }
JSON
에는 비밀번호 같은 민감한 정보가 표기되므로 이를 제외
해야 함@JasonIgnore
어노테이션을 이용하여 이를 처리
public class Member {
private Long id;
private String email;
@JsonIgnore
private String password;
private String name;
private LocalDateTime registerDateTime;
...
유닉스 타임 스태프
로 날짜를 표기
@JasonFormat
어노테이션을 이용
public class Member {
private Long id;
private String email;
@JsonIgnore
private String password;
private String name;
@JsonFormat(shape = Shape.STRING) // ISO-8601 형식으로 변환
private LocalDateTime registerDateTime;
...
원하는 형식
일 경우 다음과 같이 @JsonFormat의 pattern
속성을 이용
@JsonFormat(pattern = "yyyyMMddHHmmss")
private LocalDateTime registerDateTime;
비효율적
Spring MVC의 설정을 변경함으로 해결 가능
// MvcConfig.java
public class MvcConfig implements WebMvcConfigurer {
...
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder // 스프링이 제공하는 클래스
.json()
// 다음 매서드는 유닉스 타임스태프로 출력하는 기능을 비활성화(ISO-8601 사용)
.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
.build();
/*
* 미리 등록된 HttpMessageConverter에는 Jackson을 사용하는 것도 포함되어 있으므로,
* 새로 생성한 HttpMessageConverter는 다음과 같이 인덱스 0에 위치(맨 앞)함
*/
converters.add(0, new MappingJackson2HttpMessageConverter(objectMapper));
}
}
extendMessageConverters()
매서드는 WebMvcConfigurer에 정의된 인터페이스로,HttpMessageConverter
를 추가로 설정할 때 사용ObjectMapper
를 사용하는 객체를 converters
의 첫 번째
항목으로 등록하면 설정 완료JSON 형식의 요청 데이터를 다음과 같이 커맨드 객체에 @RequestBody
어노테이션을 붙여 자바 객체로 변환
// RestMemberController.java
@RestController
public class RestMemberController {
...
@PostMapping("/api/members")
public ResponseEntity<Object> newMember(
// 다음 어노테이션을 붙임으로, JSON 형식의 문자열을 해당 자바 객체로 변환
@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();
}
}
...
}
yyyy-MM-ddTHH:mm:ss
@JsonFormat
어노테이션의 pattern
속성을 사용해 지정 가능
@JsonFormat(pattern = "yyyyMMddHHmmss")
private LocalDateTime birthDateTime;
모든
속성에 적용하려면 다음과 같이 스프링 MVC 설정을 변경
// MvcConfig.java
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {
...
@Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
.json()
.featuresToEnable(SerializationFeature.INDENT_OUTPUTS)
// 다음 두 줄에 걸쳐 스프링 MVC 속성을 설정
.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(formatter))
.simpleDateFormat("yyyy-MM-dd HH:mm:ss")
.build();
converters.add(0, new MappingJackson2HttpMessageConverter(objectMapper));
}
}
지금까지는 상태 코드를 지정하기 위해 다음과 같이 HttpSevletResponse
의 setStatus()
, sendError()
매서드를 이용
@GetMapping("/api/members2/{id}")
public Member member2(@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;
}
하지만 위와 같이 404 응답을 하면 JSON 형식이 아닌 서버가 기존으로 제공하는 HTML
을 응답 결과로 제공
ReponseEntity
를 이용하여 정상/비정상인 두 경우 모두
처리 가능 // ErrorReponse.java
public class ErrorResponse {
private String message;
public ErrorResponse(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
}
// RestMemberController.java
@RestController
public class RestMemberController {
...
@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)
.body(new ErrorResponse("no member"));
}
return ResponseEntity.ok(member);
}
...
}
body
로 지정한 객체를 이용해 변환을 처리status
로 지정한 값을 응답 상태 코드로 사용
ResponseEntity.status(상태코드).body(객체)
결국, 위의 코드에서 member를 찾지 못한 에러가 발생시 다음과 같은JSON
형식의 데이터를 생성
실행화면
{
“member” : “no member”
}
코드 중복
이 발생@ExceptionHandler
어노테이션을 적용한 매서드에서 에러처리를 하도록 구현해 해결
// RestMemberController.java
@GetMapping("/api/members3/{id}")
public Member member3(@PathVariable Long id) {
Member member = memberDao.selectById(id);
if (member == null) {
throw new MemberNotFoundException();
}
return member;
}
// 위 매서드에서 발생하는 에러는 다음 매서드가 JSON 형식으로 처리
@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNoData() {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("no member"));
}
@RestControllerAdvice
어노테이션을 이용해 다음처럼 에러 처리 코드를 별도 클래스로 분리
가능
// ApiExceptionAdvice.java
@RestControllerAdvice("controller")
public class ApiExceptionAdvice {
@ExceptionHandler(MemberNotFoundException.class)
public ResponseEntity<ErrorResponse> handleNoData() {
return ResponseEntity
.status(HttpStatus.NOT_FOUND)
.body(new ErrorResponse("no member"));
}
...
}
@Valid
어노테이션을 붙인 커맨드 객체가 값 검증에 실패하면 400코드를 HTML
응답으로 전송Errors
타입 파라미터를 추가해, 직접
에러 응답을 생성
@PostMapping("/api/members")
public ResponseEntity<Object> newMember(
@RequestBody @Valid RegisterRequest regReq,
Errors errors) {
// hasErrors()를 호출하여 검증 에러 존재를 판별
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));
}
...
MethodArgumentNotValidException
이 발생하므로@ExceptionHandler
어노테이션을 이용해 분리
가능
// ApiExceptionAdvice.java
@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));
}
}