JSON 응답과 요청 처리

HeeSeong·2021년 8월 8일
0
post-thumbnail

JSON


웹 페이지에서 Ajax를 이용해서 서버 API를 호출하는 사이트가 많다. 이들 API는 웹 요청에 대한 응답으로 HTML 대신 JSON이나 XML을 사용한다.

JSON은 간단한 형식을 갖는 문자열로 데이터 교환에 주로 사용한다. JSON의 규칙은 간단하다. 중괄호를 사용해서 객체를 표현한다. 객체는 이름, 값의 쌍을 갖는다. 이름과 값은 콜론(:)으로 구분한다.


{
      "name": "Molecule Man",
      "age": 29,
      "secretIdentity": "Dan Jukes",
      "powers": [
        "Radiation resistance",
        "Turning tiny",
        "Radiation blast"
      ]
}

Jackson 의존 설정


Jackson은 자바 객체와 JSON 형식 문자열 간 변환을 처리하는 라이브러리이다. 스프링 MVC에서 Jackson 라이브러리를 이용해서 자바 객체를 JSON으로 변환하려면 클래스 패스에 Jackson 라이브러리를 추가하면 된다.

Jackson은 프로퍼티의 이름과 값을 JSON 객체의 (이름, 값) 쌍으로 사용한다.


@RestController JSON 응답


스프링 MVC에서 JSON 형식으로 데이터를 응답하는 것은 매우 간단하다. @Controller 애노테이션 대신 @RestController 애노테이션을 사용하면 된다.

@RestController 애노테이션을 붙인 경우 스프링 MVC는 요청 매핑 애노테이션을 붙인 메서드가 리턴한 객체를 알맞은 형식으로 변환해서 응답 데이터로 전송한다. 이때 클래스 패스에 Jackson이 존재하면 JSON 형식의 문자열로 변환해서 응답한다.


@RestController
public class RestMemberController {
	private MemberDao memberDao;
	private MemberRegisterService registerService;

	@GetMapping("/api/members")
	public List<Member> members() {
		return memberDao.selectAll();
	}
}

@JsonIgnore 제외 처리


비밀번호와 같은 데이터를 응답 결과에서 제외시키기 위해 Jackson이 제공하는 @JsonIgnore 애노테이션을 사용하면 처리할 수 있다. JSON 응답에 포함시키지 않을 대상에 @JsonIgnore 애노테이션을 붙인다.


@JsonIgnore
private String password;

@JsonFormat 날짜 형식 변환


@JsonFormat 애노테이션을 사용하지 않고 그냥 LocalDateTime 같은 객체를 @RestController로 반환하면 [2018, 3, 1, 11, 7, 49]와 같이 배열로 반환한다. 만약 Date 타입이면 '1519870069000' 값으로 반환한다.

보통 날짜나 시간은 배열이나 숫자보다는 "2018-03-01 11:07:49" 같이 특정 형식을 갖는 문자열로 표현하는 것을 선호한다. Jackson에서 날짜나 시간 값을 특정한 형식으로 표현하는 가장 쉬운 방법은 @JsonFormat 애노테이션을 사용하는 것이다. 예를 들어 ISO-8601 형식으로 변환하고 싶다면 다음과 같이 shape 속성 값으로 Shape.STRING을 갖는 @JsonFormat 애노테이션을 변환 대상에 적용하면 된다.


public class Member {

	@JsonFormat(shape = Shape.STRING)
	private LocalDateTime registerDateTime;
}

애노테이션을 사용했을 때 출력 형식이다.


{
      "registerDateTime": "2018-03-01T11:07:49"
}

ISO-8601 형식이 아닌 원하는 형식으로 변환해서 출력하고 싶다면 @JsonFormat 애노테이션의 pattern 속성을 사용하면 된다.


public class Member {

	@JsonFormat(pattern = "yyyyMMddHHmmss")
	private LocalDateTime registerDateTime;
}

애노테이션을 사용했을 때 출력 형식이다.


{
      "registerDateTime": "20180301020749"
}

날짜 형식 변환 처리 설정


날짜 형식을 변환할 모든 대상에 @JsonFormat 애노테이션을 붙여야 한다면 상당히 귀찮을 것이다. 그래서 날짜 타입에 해당하는 모든 대상에 동일한 변환 규칙을 적용할 수 있어야 한다. @JsonFormat 애노테이션을 이용하지 않고 Jackson의 변환 규칙을 모든 날짜 타입에 적용하려면 스프링 MVC 설정을 변경해야 한다.

스프링 MVC는 자바 객체를 HTTP 응답으로 변환할 때 HttpMessageConverter라는 것을 사용한다. 즉 JSON으로 변환할 때 사용하는 MappingJackson2HttpMessageConverter를 새롭게 등록해서 날짜 형식을 원하는 형식으로 변환하도록 설정하면 모든 날짜 형식에 동일한 변환 규칙을 적용할 수 있다.

아래는 모든 날짜 타입을 ISO-8601 형식으로 변환하기 위한 설정을 추가한 예제이다.


@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()
				.featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
				//.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(formatter))
				//.deserializerByType(LocalDateTime.class, new LocalDateTimeDeserializer(formatter))
				//.simpleDateFormat("yyyy-MM-dd HH:mm:ss")
				.build();
		converters.add(0, new MappingJackson2HttpMessageConverter(objectMapper));
	}
}

17행의 extendMessageConverters() 메서드는 WebMvcConfigurer 인터페이스에 정의된 메서드로서 HttpMessageConverter를 추가로 설정할 때 사용한다. @EnableWebMvc 애노테이션을 사용하면 스프링 MVC는 여러 형식으로 변환할 수 있는 HttpMessageConverter를 미리 등록한다. extendMessageConverters()는 등록된 HttpMessageConverter 목록을 파라미터로 받는다.

미리 등록된 HttpMessageConverter에는 Jackson을 이용하는 것도 포함되어 있기 떄문에 새로 생성한 HttpMessageConverter는 목록의 제일 앞에 위치시켜야 한다. 가장 먼저 적용되야 하기 때문이다. (0번 인덱스에 추가)

JSON으로 변환할 때 사용할 ObjectMapper를 생성한다. Jackson2ObjectMapperBuilder는 ObjectMapper를 보다 쉽게 생성할 수 있도록 스프링이 제공하는 클래스이다. featuresToDisable는 Jackson이 날짜 형식을 출력할 때 유닉스 타임 스탬프로 출력하는 기능을 비활성화한다. 이 기능을 비활성화 하면 ObjectMapper는 날짜 타입의 값을 ISO-8601 형식으로 출력한다.

이렇게 새로 생성한 ObjectMapper를 사용하는 MappingJackson2HttpMessageConverter 객체를 converters의 첫번째 항목으로 등록하면 설정이 끝난다.

모든 Date 타입의 값을 원하는 형식으로 출력하도록 설정하고 싶다면 simpleDateFormat() 메서드를 이용해서 패턴을 지정한다. Date 타입을 변환할 때 사용할 패턴을 지정해도 LocalDateTime 타입의 변환에는 해당 패턴을 사용하지 않는다. 대신 LocalDateTime 타입은 ISO-8601 형식으로 변환한다.

모든 LocalDateTime 타입에 대해 ISO-8601 형식 대신 원하는 패턴을 설정하고 싶다면serializerByType() 메서드를 이용해서 LocalDateTime 타입에 대한 JsonSerializer를 직접 설정하면 된다.

MappingJackson2HttpMessageConverter가 사용할 ObjectMapper 자체에 시간 타입을 위한 변환 설정 추가는 개별 속성에 적용한 @JsonFormat 애노테이션 설정보다 후순위다.


@RequestBody JSON 요청 처리


JSON 형식으로 전송된 요청 데이터를 커맨드 객체로 전달받는 방법은 매우 간단하다. 커맨드 객체에 @RequestBody 애노테이션을 붙이기만 하면 된다.


@RestController
public class RestMemberController {

	@PostMapping("/api/members2")
	public void newMember2(
			@RequestBody RegisterRequest regReq, 
			Errors errors, 
			HttpServletResponse response) throws IOException {
		try {
			new RegisterRequestValidator().validate(regReq, errors);
			if (errors.hasErrors()) {
				response.sendError(HttpServletResponse.SC_BAD_REQUEST);
				return;
			}
			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 형식의 문자열을 해당 자바 객체로 변환한다.
스프링 MVC가 JSON 형식으로 전송된 데이터를 처리하려면 요청 컨텐츠 타입이 'application/json'이어야 한다. 보통 POST 방식의 폼 데이터는 쿼리 문자열인 "p1=v1&p2=v2"로 전송되는데 이때 컨텐츠 타입은 'application/x-www-form-unlencoded'이다.


JSON 데이터의 날짜 형식


JSON 형식의 데이터를 날짜 형식으로 변환할 때, 별도 설정을 하지 않으면 'yyyy-MM-ddTHH:mm:ss' 패턴의 문자열을 LocalDateTime과 Date로 변환한다.

특정 패턴을 가진 문자열을 LocalDateTime이나 Date 타입으로 변환하고 싶다면 @JsonFormat 애노테이션의 pattern 속성을 사용해서 패턴을 지정한다.


@JsonFormat(pattern = "yyyyMMddHHmmss")
private LocalDateTime birthDateTime;

특정 속성이 아니라 해당 타입을 갖는 모든 속성에 적용하고 싶다면 설정을 추가하면 된다.
deserializerByType()은 JSON 데이터를 LocalDateTime 타입으로 변환할 때 사용할 패턴을 지정하고 simpleDateFormat()은 Date 타입으로 변환할 때 사용할 패턴을 지정한다. 단 simpleDateFormat()은 Date 타입을 JSON 데이터로 변환할 때에도 사용된다는 점을 주의하자.


@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer {

	@Override
	public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
		DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMdd HHmmss");
		ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
				.json()
				.featuresToDisable(SerializationFeature.IDENT_OUTPUT)
                		.deserializerByType(LocalDateTime.class,
                		new LocalDateTimeDeserializer(formatter))
                		.simpleDateFormat("yyyyMMdd HHmmss")
				.build();
	}
}

ResponseEntity 객체 응답 코드


@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)
					.body(new ErrorResponse("no member"));
			// return ResponseEntity.notFound().build();
		}
		return ResponseEntity.ok(member);
	}
}

상태 코드를 지정하기 위해 HttpServletResponse()의 setStatus(), sendError() 메서드를 사용했지만 HttpServletResponse를 이용해서 응답을 하면 JSON 형식이 아닌 서버가 기본으로 제공하는 HTML을 응답 결과로 제공한다. 404, 500 같이 처리에 실패한 경우 HTML 응답 데이터 대신에 JSON 형식의 응답 데이터를 전송해야 API 호출 프로그램이 일관된 방법으로 응답을 처리할 수 있다.

정상, 비정상 모두 JSON 응답을 전송하기 위해서 ResponseEntity를 사용한다.

profile
끊임없이 성장하고 싶은 개발자

0개의 댓글