2016년 7월 28일 목요일

Spring Boot 1.4.0 적용시 변경되는 부분들

[new-and-noteworthy 링크]


  • Hibernate 4 -> 5
  • org.jadira.usertype 4.0.0.GA -> 5.0.0.GA

@ConfigurationProperties deprecated attribute


Deprecated 예정인 항목 중
The locations and merge attributes of the @ConfigurationProperties annotation have been deprecated in favor of directly configuring the Environment.
이 부분이 있다.

@ConfigurationProperties를 통해 yml설정을 사용하던 부분이 앞으로 locations 가 제거되고 나면 문제가 될 듯 하다.
[관련 링크]
@PropertySource는 yml 호출을 아직 지원하지 않고 있다.


MessageSourceAutoConfiguration classpath

그동안 몰랐었는데 Spring Boot MessageSourceAutoConfiguration가 변경된 부분이 있다. [관련 링크]
1.3.2, 1.2.9 이전의 경우
private Resource[] getResources(ClassLoader classLoader, String name) {
    try {
        return new SkipPatternPathMatchingResourcePatternResolver(classLoader).getResources("classpath*:" + name + "*.properties");
    }
    catch (Exception ex) {
        return NO_RESOURCES;
    }
}


1.3.2, 1.2.0 이후
private Resource[] getResources(ClassLoader classLoader, String name) {
    try {
        return new PathMatchingResourcePatternResolver(classLoader).getResources("classpath*:" + name + ".properties");
    }
    catch (Exception ex) {
        return NO_RESOURCES;
    }
}

어차피 i18n 적용을 하면 이 변경은 의미가 없는 것 같고 오히려 한 개라도 일치하는 messageSource properties가 없으면 전체 i18n properties를 다 로드하지 않는 문제가 있다.

Hibernate @GeneratedValue

Hibernate 가 4에서 5로 변경되면서 기존 @GeneratedValue의 사용에 오류가 발생한다.

기본 strategy인 GenerationType.AUTO가 5에서 제대로 동작하지 않기 때문에 명시적으로 선언이 필요하다.

@GeneratedValue(strategy = GenerationType.IDENTITY)


Thymeleaf3

Thymeleaf 3을 쓰는 경우 thymeleaf-extras-java8time 2.1.0.RELEASE -> 3.0.0.RELEASE로 변경해서 사용해야 한다.
템플릿 엔진이 변경되면서 주석처리에 문제가 있던 부분도 해결이 되었다.
따라서 thymeleaf-extras-conditionalcomments 라이브러리는 제거해야 한다.

2016년 7월 20일 수요일

restTemplate 이 xml로 요청을 하는 경우

다음과 같은 경우 restTemplate 요청을 xml로 한다.
  1. jackson-dataformat-xml를 의존성으로 가지고 있다.
  2. new RestTemplate(); 으로 restTemplate을 생성한다.
  3. HttpMessageConvertersAutoConfiguration을 사용해서 messageConverters를 구현하지 않는다.
이 경우 RestTemplate의 생성자에 설정된 messageConverter 추가 규칙대로 converter를 호출하여 사용하게 된다.

public RestTemplate() {
    this.messageConverters.add(new ByteArrayHttpMessageConverter());
    this.messageConverters.add(new StringHttpMessageConverter());
    this.messageConverters.add(new ResourceHttpMessageConverter());
    this.messageConverters.add(new SourceHttpMessageConverter());
    this.messageConverters.add(new AllEncompassingFormHttpMessageConverter());

    if (romePresent) {
        this.messageConverters.add(new AtomFeedHttpMessageConverter());
        this.messageConverters.add(new RssChannelHttpMessageConverter());
    }

    if (jackson2XmlPresent) {
        this.messageConverters.add(new MappingJackson2XmlHttpMessageConverter());
    }
    else if (jaxb2Present) {
        this.messageConverters.add(new Jaxb2RootElementHttpMessageConverter());
    }

    if (jackson2Present) {
        this.messageConverters.add(new MappingJackson2HttpMessageConverter());
    }
    else if (gsonPresent) {
        this.messageConverters.add(new GsonHttpMessageConverter());
    }
}


이 경우 jackson xml messageConverter가 json 보다 우선 추가가 되며 restTemplate 호출 시 HttpMessageConverterExtractor의 extractData 메소드에서 추가된 순서대로 converter를 찾아 canRead(Class clazz, MediaType mediaType) 호출이 true인 messageConverter를 사용하게 된다.

restTemplate을 별다른 설정없이 호출해서 사용하게 되면 mediaType은 null로 없다.

대부분의 messageConverter는 생성자에서 자신이 지원하는 mediaType을 선언하여 mediaType에 대한 제한을 두지만 MappingJackson2XmlHttpMessageConverter는 mediaType이 없는 경우도 canRead에서 true로 응답한다. (다르게 말하자면 제한을 두지 않는다.)

이에 대해 spring boot의 HttpMessageConverters 생성 시 reorderXmlConvertersToEnd 메소드를 통해 AbstractXmlHttpMessageConverter이나 MappingJackson2XmlHttpMessageConverter인 경우는 converters 리스트의 가장 맨뒤로 위치시키는 처리를 하여 가장 나중에 체크 되도록 하였다.

따라서 MappingJackson2XmlHttpMessageConverter에 의존성이 있으며 boot 의 autoConfiguration을 통한 생성을 하지 않은 new RestTemplate을 사용할 경우엔 converter에서 MappingJackson2XmlHttpMessageConverter를 제거하거나 아니면 HttpMessageConvertersAutoConfiguration을 통해 생성된 HttpMessageConverters를 constructor를 통해 넘겨주면 된다.


@Autowired
private HttpMessageConverters messageConverters;

new RestTemplate(messageConverters.getConverters());


spring boot를 사용하지 않고 restTemplate을 쓰는 경우 xmlConveter를 맨 뒤로 보내는 HttpMessageConverters의 reorderXmlConvertersToEnd method를 참고하면 된다.
private void reorderXmlConvertersToEnd(List> converters) {
    List> xml = new ArrayList>();
    for (Iterator> iterator = converters.iterator(); iterator.hasNext();) {
        HttpMessageConverter converter = iterator.next();
        if ((converter instanceof AbstractXmlHttpMessageConverter) || (converter instanceof MappingJackson2XmlHttpMessageConverter)) {
            xml.add(converter);
            iterator.remove();
        }
    }
    converters.addAll(xml);
}


restTemplate에서 xmlConverter를 안 쓴다면 restTemplate에서 해당 converter를 제거하는 방법도 있다.
RestTemplate restTemplate = new RestTemplate();
restTemplate.getMessageConverters().removeIf(messageConverter -> messageConverter instanceof MappingJackson2XmlHttpMessageConverter);


MessageConverter를 사용한다면 되도록 boot에서 bean으로 등록해준 messageConverter를 사용하는게 좋다

2016년 7월 15일 금요일

restTemplate의 Collection 응답 처리 방법

restTemplate으로 Collection 을 요청하는 경우 주의해야 할 점이 있다.


리턴 값 자체를 List, Set, Map으로 하는 경우 빈 값이 전달되면 restTemplate의 리턴 값은 null 값으로 처리된다.

Collection을 사용할 땐 null 보단 빈 Collection으로 리턴되는 것이 좋다.

따라서 아래와 같이 쓰는 게 좋다.

객체[] objs = getRestTemplate().getForObject("호출주소", 객체[].class);
List<객체> objList = Arrays.asList(objs);


이렇게 사용하는 경우 objList는 unmodifiable 객체가 되어 add나 remove가 불가능하다. 만약 add, remove를 원한다면 다음과 같이 선언한다.
List<객체> objList = new ArrayList<>(Arrays.asList(objs));


2016년 7월 13일 수요일

Spring MVC에서 요청 처리 시 @RequestBody는 어떻게 쓰는게 좋은가

@RequestBody의 동작

Spring MVC Controller에서 요청을 object로 binding할 때 @RequestBody 라는 어노테이션을 제공한다.
이 어노테이션의 기능은 다음과 같다.

  • @RequestBody를 사용하지 않은 경우 : query parameter, form data를 object에 맵핑한다.
  • @RequestBody를 사용하는 경우 : body에 있는 data를 HttpMessageConverter를 이용해 선언한 object에 맵핑한다.


post form data 전송은 다음과 같은 모양이다.
[head 영역]
-------
키1=데이터1&키2=데이터2


@RequestBody를 사용하는 경우는 body영역의 모양을 json과 같이 converter가 변환할 수 있는 형태로 전달해야하며 다음과 같은 모양이다.
[head 영역]
-------
{ "키1" : "데이터1", "키2" : "데이터2" }

@RequestBody를 사용하는 경우 두번째 형태의 body 데이터를 처리할 수 있다.

jquery 요청시

jquery로 요청하는 경우 POST나 PUT 요청은 기본이 form data 요청이다.
json을 변수로 선언해도 jquery는 form data 형태로 변경하여 요청을 한다.

var parameter = {};
parameter.key1 = "value1";
parameter.key2 = "value2";
$.ajax({
    url : "요청url",
    type : "POST",
    data : parameter
});


만약 json body로 요청을 하고 싶은 경우 다음과 같이 요청한다.
var parameter = {};
parameter.key1 = "value1";
parameter.key2 = "value2";
$.ajax({
    url : "요청url",
    type : "POST",
    contentType: "application/json; charset=utf-8",
    data : JSON.stringify(parameter)
});


restTemplate 요청 시


전통적인 방식의 form submit은 Controller에서 @RequestBody를 사용하지 않아야 올바르게 맵핑이 된다.
post form data submit은 다음과 같은 모양으로 요청을 한다.

위 형태로 요청하는 것을 restTemplate으로 구현할 땐 MultiValueMap으로 해당 data를 선언하여 전달한다.

MultiValueMap map = new LinkedMultiValueMap();
map.add("키1", "데이터1");
map.add("키2", "데이터2");
String result = restTemplate.postForObject("호출 주소", map, String.class);

하지만 중간에 map에 데이터를 선언하는 처리가 들어가면 불편한 점이 많다.

만약 @RequestBody로 요청을 받는 경우 단순히 아래와 같이 간단하게 사용이 가능하다.

String result = restTemplate.postForObject("호출 주소", 대상object, String.class);

언제 써야할까?

GET 이나 DELETE 요청은 body에 데이터를 담지 않는다.
따라서 @RequestBody를 사용하지 않는 것이 좋다.
@RequestBody를 사용하지 않는 경우에도 Spring MVC는 기본적으로 GET과 DELETE 요청시 form data처리를 하지 않는다.
간혹 요청 시 필요한 파라메터가 많은 경우 object에 바인딩을 하고 사용하기 위해 @RequestBody로 요청으로 사용하는 경우가 있지만 개인적으로 원래 사용방식을 따르는 형태가 좋지 않을까 싶다.

angular.js나 backbone.js같은 mv? javascript 라이브러리를 사용하는 경우 기본으로 json 포맷의 requestBody를  전송한다.
이런 경우는 @RequestBody로 요청을 처리하는게 좋다.

restTemplate 요청은 아무래도 form data 전송보다 json body형태가 훨씬 간결하고 유지보수하기 편하다.

대략 정리하면 다음과 같은 전반적인 규칙으로 사용을 하면 좋지 않을까 싶다.


Case @RequestBody 사용 여부 description
GET 요청 (brower) X query parameter 사용
POST 요청 (brower) X form data 사용
PUT 요청 (brower) X form data 사용
DELETE 요청 (brower) X query parameter 사용
GET 요청 (javascsript mv? library) X query parameter 사용
POST 요청 (javascsript mv? library) O
PUT 요청 (javascsript mv? library) O
DELETE 요청 (javascsript mv? library) X query parameter 사용
GET 요청 (restTemplate) X query parameter 사용
POST 요청 (restTemplate) O
PUT 요청 (restTemplate) O
DELETE 요청 (restTemplate) X query parameter 사용

2016년 7월 8일 금요일

spring boot mvc에서 java 8 date 객체 json 처리하기

spring mvc를 사용하면 date 객체를 주고받는 형태에 대해 고민을 하게 된다.

특히 java 8의 date 객체들을 쓰게 되면 더더욱 그렇다.


응답 처리


java 8의 LocalDateTime이나 LocalDate를 사용하면 응답하는 MappingJackson2JsonView의 기본 json 결과는 다음과 같다.
{ "localDateSample" : [2016,7,8], "localDateTimeSample" : [2016,7,1,0,0] }


serialized as numeric timestamps 형태로 년월일 / 년월일시분 값을 넘겨주는데 이런 형태로 응답하면 client의 javascript 객체에서 변환해서 쓸 때 불편한 점이 많다.

date format string 을 넘겨받아 사용하는게 더 사용하기 편하다고 생각을 해서 다음과 같이 boot properties 설정을 한다.
# response datetime 처리 설정
spring.jackson.serialization.write-dates-as-timestamps=false


위와 같이 설정을 하면 응답처리는 다음과 같이 된다.
{ "localDateSample" : "2016-07-08", "localDateTimeSample" : "2016-07-31T00:00" }

이렇게 사용하면 javascript에서 사용하기가 편해지고 timezone 설정이 들어간 경우도 moment.js 와 같은 라이브러리를 사용하면 쉽게 사용할 수 있다.


요청 처리


위와 같이 설정하는 경우 응답에 대해서 처리는 되었지만 요청이 온 경우에 대해서도 처리를 해주어야 한다.
요청에 대해 date format string 을 넘겨받는 설정은 WebMvcConfigurerAdapter에 다음과 같이 선언을 해준다.
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    ...

    // request date conversion 처리
    @Override
    public void addFormatters(FormatterRegistry registry) {
        DateTimeFormatterRegistrar registrar = new DateTimeFormatterRegistrar();
        registrar.setUseIsoFormat(true);
        registrar.registerFormatters(registry);
    }
}


여기까지 설정을 하면 @ResponseBody로 리턴되는 produces = MediaType.APPLICATION_JSON_VALUE 응답처리에 올바르게 적용된다.
하지만 html, json에 대한 contentNegotiation 처리에도 올바른 응답을 처리하기 위해선 WebMvcConfigurerAdapter에 다음과 같이 선언해야한다.
@Configuration
public class WebMvcConfig extends WebMvcConfigurerAdapter {
    ...

    @Autowired
    private ObjectMapper objectMapper;

    @Override
    public void configureViewResolvers(ViewResolverRegistry registry) {
        registry.viewResolver(thymeleafViewResolver);  // thymeleaf를 쓰는 경우
        registry.enableContentNegotiation(new MappingJackson2JsonView(objectMapper));
    }
}


위에서 중요한 핵심은 objectMapper를 spring boot에서 생성한 것을 써야한다는 점이다. 만약 new MappingJackson2JsonView() 로 생성한다면 spring boot 설정으로 선언한 objectMapper가 아닌 spring web의 Jackson2ObjectMapperBuilder로 생성한 objectMapper를 사용하기 때문에 jackson 설정이 동작하지 않는다.