2016년 8월 31일 수요일

sonatype nexus 3.0

nexus 가 드디어 3.0이 나왔다.

설치해서 실행해보면 확 달라진 ui를 맛볼 수 있다.

[Sonatype Releases Nexus Repository 3.0]

가장 크게 달라진 점은 docker registry, nuget, bower, npm등 다양한 registry를 지원하는 점이다.

또한 gui도 변했다.

tree 형태로 열거해서 보던 방식에서 search를 하는 형태로 변경되었는데 초기엔 불편할 수 있지만 성능상 이점이 더 많을 것 같다.

2016년 8월 30일 화요일

spring mvc의 exceptionHandler에서 redirect 할 경우 주의해야할 점

spring 의 exceptionHandler를 사용하는 경우 보통 다음과 같은 형태이다.
@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ModelAndView handleError(SomeException exception) {
        ExceptionMessage exceptionMessage = new ExceptionMessage();
        // exception 결과 표시용 객체 처리
        return new ModelAndView("/error", exceptioMessage);
    }
}

응답 결과를 redirect 하는 경우는 redirect: 를 사용한다.

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler
    @ResponseStatus(HttpStatus.BAD_REQUEST)
    public ModelAndView handleError(SomeException exception) {
        ExceptionMessage exceptionMessage = new ExceptionMessage();
        // exception 결과 표시용 객체 처리
        return new ModelAndView("redirect:/error", exceptioMessage);
    }
}
그런데 이 경우 redirect가 동작하지 않는 문제점이 있다.

이유는 바로 @ResponseStatus 어노테이션 때문이다.

리다이렉트 처리는 302로 응답을 내려보내고 다시 redirect할 주소로 이동을 해야하는데 @ResponseStatus가 명시되어 있으면 해당 status 로 응답이 처리되어 redirect가 동작하지 않는다.

따라서 redirect를 해야할 경우 status는 메소드 내의 view의 status 선언으로 처리를 해야한다.

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler
    //@ResponseStatus(HttpStatus.BAD_REQUEST)
    public ModelAndView handleError(SomeException exception) {
        ExceptionMessage exceptionMessage = new ExceptionMessage();
        // exception 결과 표시용 객체 처리
        ModelAndView modelAndView = new ModelAndView("redirect:/error", exceptioMessage);
        return modelAndView;
    }
}


html, json 요청에 따라 redirect html, json response 처리를 하는 것은 이전 글을 참조하면 된다.
[ spring mvc의 exceptionHandler에서 mediaType 정보 획득하기 ]

만약 동일 Exception에 대해 status 처리를 분기하고 싶은 경우 (또는 위처럼 redirect가 있거나 exception status 처리를 나눠야 하는 경우)는 다음과 같이 처리를 한다.

@ControllerAdvice
public class GlobalExceptionHandler {
    @ExceptionHandler
    //@ResponseStatus(HttpStatus.BAD_REQUEST)
    public ModelAndView handleError(SomeException exception, HttpServletResponse response) {
        ExceptionMessage exceptionMessage = new ExceptionMessage();
        if (리다이렉트의 경우) {
            // exception 결과 표시용 객체 처리
            ModelAndView modelAndView = new ModelAndView("redirect:/error", exceptioMessage);
            return modelAndView;
        }
        // http status코드를 내려야하는 경우
        ModelAndView modelAndView = new ModelAndView(PAGE_ERROR, resultMap);
        response.setStatus(HttpStatus.BAD_REQUEST.value());
        return modelAndView;
    }
}


ModelAndView의 setStatus 메소드를 사용하는 것이 exception 처리에서는 제대로 동작하지 않는 듯 하다.

2016년 8월 18일 목요일

Spring Boot에서 header의 accept language를 이용한 i18n 처리

서버 Locale 설정


스프링에서 web을 사용하면 LocaleContextHolder를 통한 Locale 관리가 이루어진다.

이 LocaleContext에 Locale을 설정하기 위해 spring에서는 LocaleResolver를 제공한다.

spring에서는 LocaleResolver의 구현체를 4가지 제공하는데 아래와 같다.

  • FixedLocaleResolver
  • SessionLocaleResolver
  • CookieLocaleResolver
  • AcceptHeaderLocaleResolver

Spring Boot의 WebMvcProperties는 위 4가지중 Fixed와 AcceptHeader에 대한 설정을 지원하며 기본 설정은 acceptHeader이다.

다만 기본 설정으로 사용하는 경우 AcceptHeaderLocaleResolver의 supportedLocale 설정에 대한 WebMvcProperties의 설정 지원이 없기 때문에 모든 Locale을 받아들이게 된다.

만약 특정 Locale만 처리가 가능하도록 원한다면 별도로 bean을 선언해주는 것이 좋다.
@Bean
public LocaleResolver localeResolver() {
    AcceptHeaderLocaleResolver acceptHeaderLocaleResolver = new AcceptHeaderLocaleResolver();
    acceptHeaderLocaleResolver.setDefaultLocale(Locale.getDefault());
    List localeList = new ArrayList<>();
    localeList.add(Locale.KOREAN);
    localeList.add(Locale.CHINESE);
    localeList.add(Locale.ENGLISH);
    acceptHeaderLocaleResolver.setSupportedLocales(localeList);
    return acceptHeaderLocaleResolver;
}


restTemplate의 Locale 설정

restTemplate에서 요청하는경우 accept-language header에 locale 정보를 담아 보내면 된다.

accept-language 를 header에 담기 위해 쓸 수 있는 HttpHeaderInterceptor가 있다
이 interceptor를 restTemplate에 선언하면 client에서 오는 header정보를 그대로 담아 전달한다.

다만 이렇게 쓰는 경우 localeResolver를 통해 변경한 locale은 담을 수 없다.

localeResolver를 통해 변경된 locale은 response header의 content-language 정보이기 때문에 request header의 accept-language를 전달하는 HttpHeaderInterceptor가 아닌 별도의 Interceptor를 구현해야한다.


/**
 * Locale별 응답 에러 메세지처리를 위한 interceptor
  *
 */
public class AcceptLanguageClientHttpRequestInterceptor implements ClientHttpRequestInterceptor {
    @Override
    public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
        request.getHeaders().set(HttpHeaders.ACCEPT_LANGUAGE, LocaleContextHolder.getLocale().toLanguageTag());
        return execution.execute(request, body);
    }
}
주의 해야할 점은 header에 보낼 locale정보는 toLanguageTag 메소드로 보내야 한다는 점이다.

java에서 쓰이는 Locale은 ko_KR과 같은 형태이지만 request header의 accept-langauge의 형태는 ko-kr 과 같은 형태이기 때문에 header에서 쓰이는 형태로 전송하지 않으면 정상적으로 language를 전달받지 못한다.


2016년 8월 10일 수요일

restTemplate에서 get 요청시 필요한 query parameter 를 domain Object에서 추출하기

restTemplate 사용시 post나 put은 request body에 object를 json으로 전달하여 중간에 변수 설정하는 단계를 생략하고 사용하기가 편하다.

하지만 get이나 delete는 body가 아닌 query parameter로 값을 전송하는데 일일이 명시해야 하는 번거로움이 있다.
restTemplate.getForObject("호출도메인/user/{id}", User.class, id)

간단하게 id를 기준으로 호출하는 형태면 별 문제가 없지만 검색 처리와 같이 많은 query parameter를 사용하는 형태의 경우 일일이 명기해서 쓰면 추후 parameter가 변경될 때마다 변경해야할 번거로움이 크다.

그렇다고 post나 put처럼 request body로 전송하는 것도 올바른 처리는 아닌 것 같고 단순히 object를 query parameter로 쉽게 바꿀 수 있는 방법이 없을까 싶어 처리를 해보았다.

우선 검색을 위한 파라메터를 모아둔 object를 만든다.

@Data
public class Search {
 
 @Min(value = 1, groups = Search.class)
 private int page;
 
 @Range(min = 1, max = 200, groups = Search.class)
 private int pageSize;
 
 @NotNull(groups = Search.class)
 private String searchKeyword;
 
 @NotNull(groups = Search.class)
 private String sortColumn;
 
 @NotNull(groups = Search.class)
 private String sortOrder;
 
 public interface Search {};
}


해당 object를 이용해 검색을 위한 query parameter를 만들고 restTemplate을 호출한다.
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl("호출도메인/board/search/findBySearch");
MultiValueMap params = new LinkedMultiValueMap<>();
params.setAll(objectMapper.convertValue(search, Map.class));
builder.queryParams(params);
Article[] articles = restTemplate.getForObject(builder.build().toUri(), Article[].class);

구현은 다음과 같다.

  • UriCompomentsBuilder를 통해 queryParameter까지 생성을 한다.
  • Object mapper를 통해 object를 map으로 변환한 후 해당 map의 내용을 MultiValueMap에 담은 후 uriCompomentBuilder에 전달한다.

이렇게 사용하면 get요청에 parameter가 많은 경우 쉽게 사용할 수 있다.
하지만 단일 domain object만 정상적으로 사용이 가능하고 sub domain을 가지고 있는 경우는 query parameter가 의도된 대로 생성되지 않는다.

간단히 사용할 케이스에만 사용하면 좋지 않을까 싶다.

SpringOne Platform 2016 SlideShare

http://www.slideshare.net/SpringCentral/presentations

SpringOne Platform 2016 SlideShare 가 공개되었다.

2016년 8월 5일 금요일

SonarQube 6.0 Release

SonarQube 6.0 이 2016년 8월 4일 공개되었다.

[Release Note]

눈에 띄는 변경점은 아래와 같다.

  • Elasticsearch가 1.7.5에서 2.3.x로 변경
  • Scanner 관련 수행 시간 표시
  • Quality Profiles,  Rules, Permissions 설정 개선
  • Project Creator 기능 추가 (프로젝트 만들때 반복적인 설정 template 화 하는 기능인 듯.)


8/5일 현재 docker registry에는 아직 6.0은 올라와 있지 않다.

SonarQube Scanner 사용하기

jenkins에서 sonarqube를 사용하는 방법이 sonarqube 4.x 이후 scanner가 나오면서 변경되었다. (중간에 sonar-runner 라는 이름이었다가 scanner로 변경됨)

sonarqube 3.x 버전에서는maven의 sonar:sonar 명령어를 통해 실행하였는데 maven을 통한 실행이라 모듈형 프로젝트의 실행은 편한 장점은 있지만 상당히 느렸다.

scanner는 단독 실행하기 때문에 maven을 경유하는 비용이 없어졌다.

대신 scanner에서 실행할 수 있는 기준을 명시한 sonar-project.properties를 프로젝트에 추가해야한다.

기본적인 사용방법은 해당 사이트의 document에서 확인할 수 있다.
[SonarQube Scanner document]

multi module 형태의 maven 프로젝트의 경우 각 모듈에 대한 선언을 해주어야 한다.
이 때 pom 타입의 packaging은 대상에서 제외하여야 한다.
대략 아래와 같이 선언하면 된다.

sonar.projectKey=bluesky-project
sonar.projectName=bluesky-project
sonar.projectVersion=0.0.1-SHAPSHOT

sonar.sources=.
sonar.java.source=1.8
sonar.sourceEncoding=UTF-8
sonar.exclusions=**/generated-sources/*,**/test/*
sonar.test.exclusions=**/test/*

sonar.modules=bluesky-core,bluesky-test,\
bluesky-opensource-jdbc,\
bluesky-opensource-data-jpa,\
bluesky-opensource-data-mongodb,\
bluesky-opensource-security,\
bluesky-opensource-boot-autoconfigure,\
bluesky-app-user,\
bluesky-app-blog,\
bluesky-app-bookkeeping,\
bluesky-app-domain,\
bluesky-app-api-battlenet,\
bluesky-web-default
#bluesky-parent,\
#bluesky-opensource,\
#bluesky-app,\
#bluesky-web,\

bluesky-core.sonar.projectBaseDir=bluesky-parent/bluesky-core
bluesky-test.sonar.projectBaseDir=bluesky-parent/bluesky-test
#bluesky-opensource.sonar.projectBaseDir=bluesky-parent/bluesky-opensource
bluesky-opensource-jdbc.sonar.projectBaseDir=bluesky-parent/bluesky-opensource/bluesky-opensource-jdbc
bluesky-opensource-data-jpa.sonar.projectBaseDir=bluesky-parent/bluesky-opensource/bluesky-opensource-data-jpa
bluesky-opensource-data-mongodb.sonar.projectBaseDir=bluesky-parent/bluesky-opensource/bluesky-opensource-data-mongodb
bluesky-opensource-security.sonar.projectBaseDir=bluesky-parent/bluesky-opensource/bluesky-opensource-security
bluesky-opensource-boot-autoconfigure.sonar.projectBaseDir=bluesky-parent/bluesky-opensource/bluesky-opensource-boot-autoconfigure
#bluesky-app.sonar.projectBaseDir=bluesky-parent/bluesky-app
bluesky-app-user.sonar.projectBaseDir=bluesky-parent/bluesky-app/bluesky-app-user
bluesky-app-blog.sonar.projectBaseDir=bluesky-parent/bluesky-app/bluesky-app-blog
bluesky-app-bookkeeping.sonar.projectBaseDir=bluesky-parent/bluesky-app/bluesky-app-bookkeeping
bluesky-app-domain.sonar.projectBaseDir=bluesky-parent/bluesky-app/bluesky-app-domain
bluesky-app-api-battlenet.sonar.projectBaseDir=bluesky-parent/bluesky-app/bluesky-app-api-battlenet
#bluesky-web.sonar.projectBaseDir=bluesky-parent/bluesky-web
bluesky-web-default.sonar.projectBaseDir=bluesky-parent/bluesky-web/bluesky-web-default