7 분 소요

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

웹 요청에도 쿼리 문자열 대신에 JSON이나 XML을 데이터로 보내기도 한다. GET이나 POST만 사용하지 않고 PUT, DELETE와 같은 다른 방식을 사용한다.

스프링 MVC를 사용하면 이를 위한 웹 컨트롤러를 쉽게 만들 수 있다. 이 장에서는 스프링 MVC에서 JSON 응답과 요청을 처리하는 방법을 살펴보도록 하자.

1. JSON 개요

JSON (JavaScript Object Notation)은 간단한 형식을 갖는 문자열 데이터 교환에 주로 사용한다. 다음은 JSON 형식으로 표현한 데이터의 예이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
    "name": "유관순",
    "birthday": "1902-12-16",
    "age": 17,
    "related": ["남동순", "류예도"],
    "edu":[
        {
            "title": "이화학당보통과",
            "year": 1916
        },
        {
            "title": "이화학당고등과",
            "year": 1916
        }
    ]
}

JSON 규칙은 간단하다. 중괄호를 사용해서 객체를 표현한다. 객체는 (이름, 값) 쌍을 갖는다. 이때 이름과 값은 콜론(:)으로 구분한다. 위 예의 경우 이름이 name인 데이터의 값은 “유관순”이다. 값에는 다음이 올 수 있다.

  • 문자열, 숫자, boolean, null
  • 배열
  • 다른 객체

문자열은 큰 따옴표나 작은 따옴표 사이에 위치한 값이다. 문자열은 \“(큰 따옴표), \n(뉴라인), \r(캐리지 리턴), \t(탭)과 같이 역슬래시를 이용해서 특수 문자를 표시할 수 있다.

숫자는 10진수 표기법(ex. 1.5, 101)이나 지수표기법(ex 1.07e2)을 따른다. boolean 타입 값은 true와 false가 있다.

배열은 대괄호로 표현한다. 대괄호 안에 콤마로 구분한 값 목록을 갖는다. 위 예에서 related 배열은 문자열 값 목록을 갖고 있고 edu 배열은 객체를 값 목록으로 갖고 있다.

2. Jackson 의존 설정

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

1
2
3
4
5
6
7
8
9
10
11
12
<!-- Jackson core와 Jackson Annotation 의존 추가-->
<dependency>
	<groupId>com.fasterxml.jackson.core</groupId>
	<artifactId>jackson-databind</artifactId>
	<version>2.9.4</version>
</dependency>
<!-- java8 data/time 지원 위한 Jackson 모듈 -->
<dependency>
	<groupId>com.fasterxml.jackson.datatype</groupId>
	<artifactId>jackson-datatype-jsr310</artifactId>
	<version>2.9.4</version>
</dependency>

Jackson은 다음과 같이 자바 객체와 JSON 사이의 변환을 처리한다.

image

Jackson은 프로퍼티(get 메서드 또는 설정에 따라 필드)의 이름과 값을 JSON 객체의 (이름, 값) 쌍으로 사용한다. 위 그림에서 Person 객체의 name 프로퍼티 값이 “이름”이라고 할 때 생성되는 JSON 형식 데이터는 이름이 “name”이고 값이 “이름”인 데이터를 갖는다. 프로퍼티 타입이 배열이나 List인 경우 JSON 배열로 변환된다.

3. @RestController로 JSON 형식 응답

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
...
import org.springframework.web.bind.annotation.RestController;
...

@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;
	}
}

기존 컨트롤러 코드와 다른 점은 다음과 같다.

  • 5행 : @Controller 애노테이션 대신 @RestController 애노테이션 사용
  • 11행, 16행 : 요청 매핑 애노테이션 적용 메서드의 리턴 타입으로 일반 객체 사용

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

톰캣을 실행하고 웹 브라우저에서 http ://localhost:8080/sp5-chap16/api/members/ 주소를 입력해서 결과를 확인해 보자

json3

크롬 브라우저에 json-formatter 확장 프로그램을 설치한 뒤에 결과를 보면 다음과 같이 보기 좋게 JSON 데이터를 표시해준다.

@RestController 애노테이션과 @ResponsBody 애노테이션
@RestController 애노테이션이 추가되기 전에는 다음과 같이 @Controller 애노테이션과 @ResponseBody 애노테이션을 사용했다.

1
2
3
4
5
6
7
8
9
10
11
@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();
    }
}

스프링 4버전부터 @RestController 애노테이션이 추가되면서 @Responsebody 애노테이션의 사용 빈도가 줄었다.

3.1 @JsonIgnore를 이용한 제외 처리

위 그림을 보면 응답 결과에 password가 포함되어 있다. 보통 암호와 같이 민감한 데이터는 응답 결과에 포함시키면 안되므로 password 데이터를 응답 결과에서 제외시켜야 한다.
Jackson이 제공하는 @JsonIgnore 애노테이션을 사용하면 이를 간단히 처리할 수 있다. 다음과 같이 JSON 응답에 포함시키지 않을 대상에 @JsonIgnore 애노테이션을 붙인다.

1
2
3
4
5
6
7
8
9
10
11
12
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 결과에서 제외된 것을 알 수 있다.

3.2 날짜 형식 변환 처리 : @JsonFormat 사용

위 그림에서 registerDateTime의 값은 [2023,4,25,20,11,27]이다. Member 클래스의 registerDateTime 속성은 LocalDateTime인데 JSON 값은 배열로 바뀌었다.
만약 registerDateTime 속성이 java.util.Date 타입이면 다음과 같이 유닉스 타임 스탬프로 날짜 값을 표현한다.

1
2
3
4
5
6
{
    "id": 1,
    "email": "glove@naver.com",
    "name": "윤건우",
    "registerDateTime": 1519870069000
}

유닉스 타입 스탬프는 1970년 1월 1일 이후 흘러간 시간을 말한다. 보통 초 단위로 표현하나 Jackson은 별도 설정이 없으면 밀리초 단위로 값을 변환한다. System.currentTimeMillis() 메서드가 리턴하는 정수도 유닉스 타임 스탬프 값이다.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonFormat.Shape;

public class Member {
	private Long id;
	private String email;
	@JsonIgnore
	private String password;
	private String name;
	@JsonFormat(shape=Shape.STRING)
	private LocalDateTime registerDateTime;
    ...
}

다음 그림은 위 애노테이션을 사용했을 때 출력 형식을 보여준다.

ISO-8601 형식이 아닌 원하는 형식으로 변환해서 출력하고 싶다면 @JsonFormat 애노테이션의 pattern 속성을 사용한다. 다음 코드는 pattern 속성의 사용 예를 보여준다.

1
2
3
4
5
6
7
8
9
10
11
12
import com.fasterxml.jackson.annotation.JsonFormat;

public class Member {
	private Long id;
	private String email;
	@JsonIgnore
	private String password;
	private String name;
	@JsonFormat(pattern="yyyyMMddHHmmss")
	private LocalDateTime registerDateTime;
    ...
}

pattern 속성은 java.time.format.DateTimeFormatter 클래스나 java.text.SimpleDateFormat 클래스의 API 문서에 정의된 패턴을 따른다.

3.3 날짜 형식 변환 처리 : 기본 적용 설정

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

스프링 MVC는 자바 객체를 HTTP 응답으로 변환할 때 HttpMessageConverter라는 것을 사용한다.

  • Jackson을 이용해서 자바 객체를 JSON으로 변환할 때에는 MappingJackson2HttpMessageConverter를 사용한다.
  • Jaxb를 이용해서 XML로 변환할 때에는 Jaxb2RootElementHttpMessageConverter를 사용한다.

따라서 JSON으로 변환할 때 사용하는 MappingJackson2HttpMessageConverter를 새롭게 등록해서 날짜 형식을 원하는 형식으로 변환하도록 설정하면 모든 날짜 형식에 동일한 변환 규칙을 적용할 수 있다.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
...

import org.springframework.http.converter.json.Jackson2ObjectMapperBuilder;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;

import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
...

@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer{
	...

	@Override
	public void extendMessageConverters(
			List<HttpMessageConverter<?>> converters) {
		ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
				.json()
				.featuresToDisable(
						SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
				.build();
		converters.add(0, new MappingJackson2HttpMessageConverter(objectMapper));
	}
}
  • 17행의 extendMessageConverters() 메서드는 WebMvcConfigurer 인터페이스에 정의된 메서드로서 HttpMessageConverter를 추가로 설정할 때 사용한다.

  • @EnableWebMvc 애노테이션을 사용하면 스프링 MVC는 여러 형식으로 변환할 수 있는 HttpMessageConverter를 미리 등록한다. extendMessageConverters()는 등록된 HttpMessageConverter 목록을 파라미터로 받는다.

  • 미리 등록된 HttpMessageConverter에는 Jackson을 이용하는 것도 포함되어 있기 때문에 새로 생성한 HttpMessageConverter는 목록의 제일 앞에 위치시켜야 한다. 그래야 가장 먼저 적용된다. 이를 위해 24행에서 새로운 HttpMessageConverter를 0번 인덱스에 추가했다.

  • 설정 코드에서 주의깊게 볼 점은 19~23행이다. 이 코드는 JSON으러 변환할 때 사용할 ObjectMapper를 생성한다.
  • 참고로 19행의 Jackson2ObjectMapperBuilder는 ObjectMapper를 보다 쉽게 생성할 수 있도록 스프링이 제공하는 클래스이다.
  • 21~22행은 Jackson이 날짜 형식을 출력할 때 유닉스 타임 스탬프로 출력하는 기능을 비활성화한다. 이 기능을 비활성화하면 ObjectMapper는 날짜 타입의 값을 ISO-8601 형식으로 출력한다.

모든 java.util.Date 타입의 값을 원하는 형식으로 출력하도록 설정하고 싶다면 Jackson2ObjectMapperBuilder.simpleDateFormat() 메서드를 이용해서 패턴을 지정한다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
@Configuration
@EnableWebMvc
public class MvcConfig implements WebMvcConfigurer{
	...

	@Override
	public void extendMessageConverters(
			List<HttpMessageConverter<?>> converters) {
		ObjectMapper objectMapper = Jackson2ObjectMapperBuilder
				.json()
				.simpleDateFormat("yyyyMMddHHmmss") // Date를 위한 변환 패턴
				.build();
		converters.add(0, new MappingJackson2HttpMessageConverter(objectMapper));
	}
}

Jackson2ObejctMapperBuilder.simpleDateFormat()으로 Date 타입을 변환할 때 사용할 패턴을 지정해도 LocalDateTime 타입 변환에는 해당 패턴을 사용하지 않는다. 대신 LocalDateTime 타입은 ISO-8601 형식으로 변환한다.

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

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;

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()
				.serializerByType(LocalDateTime.class, new LocalDateTimeSerializer(formatter))
				.build();
		converters.add(0,
				new MappingJackson2HttpMessageConverter(objectMapper));
	}
}

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

Ref.

  • 최범균, 스프링프로그래밍입문5, 가메출판사.

카테고리:

업데이트: