SPRINGBOOT로 AWS LAMBDA 이용하기

spring framework 2017.07.07 09:52 posted by dev.bistro


결론적으로 @SpringBootApplication 과 Main method으로 시작되는 startup은 불가능하지만, 그 부분을 생성자 부분으로 변경한 다면 사용 가능하다. 다만, 첫  시작은 dependency lookup 방식으로 bean을 가져와야 한다.
(이건 올해 초 얘기고, 지금은 https://github.com/spring-cloud/spring-cloud-function 을 이용하면 된다)

테스트환경

  • intellij 2016.3
  • gradle
  • spring-boot 1.4.3

intellij Spring Initializr 의 셋팅

%e1%84%89%e1%85%b3%e1%84%8f%e1%85%b3%e1%84%85%e1%85%b5%e1%86%ab%e1%84%89%e1%85%a3%e1%86%ba-2017-01-14-%e1%84%8b%e1%85%a9%e1%84%92%e1%85%ae-6-38-44

이후 build.gradle은 다음과 같이 셋팅하였다. boot를 이용한 gradle의  task package 나  assemble는 main이 없기때문에 사용이 힘들고 maven shade 플러그인이나 gradle shadow를 이용한다.

참고 build.gradle
( plugin : https://github.com/johnrengelman/shadow 는 몇년전에 PR 1줄 했네…)

이후 https://github.com/bistros/test-springboot-lambda 으로 프로젝트를 구동 시켜봤지만 단순히 String을 리턴하는데도 2초씩 걸린다.  (handleRequest에서 스프링 프로젝트를 초기화 하니 당연히 느릴수밖에… source link )

%e1%84%89%e1%85%b3%e1%84%8f%e1%85%b3%e1%84%85%e1%85%b5%e1%86%ab%e1%84%89%e1%85%a3%e1%86%ba-2017-01-14-%e1%84%8b%e1%85%a9%e1%84%92%e1%85%ae-7-14-56

그래서 다음처럼 생성자에서 초기화하도록 수정 (source link) 하였다.

%e1%84%89%e1%85%b3%e1%84%8f%e1%85%b3%e1%84%85%e1%85%b5%e1%86%ab%e1%84%89%e1%85%a3%e1%86%ba-2017-01-14-%e1%84%8b%e1%85%a9%e1%84%92%e1%85%ae-7-15-55


신고


spring 5.0 이후 RouterFunction을 이용하여 EndPoint를 구현 할 수 있다. 이 RouterFunction을 스프링 빈으로 만들면 기존의 @Controller와 동일한 포지션의 역할을 수행하는데 어떻게 스캐닝을 하는걸 까?


Springboot 2.0 은 아직 개발중이지만, 그 안에서 확인 할 수 있다.

링크 를 보면

@ConditionalOnMissingBean(RouterFunction.class)
@ConditionalOnBean(RouterFunction.class)

즉, RouterFunction의 스프링 존재 여부에 따라 분기를 타고 AnnotationConfig, FunctionalConfig 설정을 적용하게 된다. 

즉 현재 기본 설정으로는 RouterFunction Bean, Controller Bean을 동시에 스캐닝 해주지 않는다 ('기본설정'에서')


1. List<RouterFunction<T>> routerFunctions 을 파라미터로 주입받아 하나의 RouterFunction으로 재구성한다. 

2. 오더링 하고, 기본 DefaultHandlerStrategiesBuilder, ViewResolvers를 설정한다.

3. WebHandler를 구성한다. 실제 구현 클래스는 HttpWebHandlerAdapter 이다. (만약 기존의 방식이었다면, 새롭게 WebHandler 인터페이스를 상속받는 DispatcherHandler 가 처리하게 된다)

신고

springframework 5.0 webflux module

spring framework 2017.04.23 11:42 posted by dev.bistro


오늘 날자 기준으로 SpringFramework는  v5.0.0.M5 (링크) 가 나와있고, RC1 버전이 5월 예정되어 있지만 힘들지싶다 (링크)  Spring 5에는 webflux 모듈이 새로 추가되었지만,  사실 Spring5 마일스톤에서 착실히 진행 중이던  spring-web-reactive가 이름이 바뀐 모듈이다. (JIRA)

0. webflux 모듈은 webmvc를 대체하는 가?

우선 spring-mvc 모듈은 여전히 남아 있다. 기존에서 @RestController 을 사용하던 방식을 여전히 사용 할 수 있다.  (즉 webflux따위 몰라도 된다)

그리고 새로운 방법으로도 사용 할 수 있다. 링크 에서 말하는 것처럼 webflux 모듈은 완전히 새로운 방법 (Java8 람다를 이용한 라우팅/핸들링)과 @Controller와 handling을 사용하는 중간 적인 방법도 사용 가능하다.

요즘들어서 통합테스트의 어려움,  몇년간 Scala에서 느끼던 재미를 Java에서는 느끼지 못해 아쉬웠는데, webflux가 좀 해소해주지 않을까 해서 살펴볼 예정이다.

1. 프로젝트의 생성

Intellij2017.1 버전은 New Project 할때 SpringBoot 2.0.0-SNAPSHOT을 지원한다. 그러므로 쉽게 프로젝트를 생성 할 수 있다. 아니라면  링크 의 pom.xml,  Application.java를  참고한다.

2. Hello Controller 만들기

기존의 mvc모듈이 @RestController, @RequestMapping으로 이루어져 있다면, webflux는 RouterFunction, HandlerFunctions 2개 인터페이스를 알 면 된다. 그리고 HttpServletRequest,HttpServletResponse는 잊고, ServerRequest, ServerResponse를 기억 하면 된다.

가장 기본적인 ‘hello world’를 반환하는 /hello를 만들어보도록 하자.

public class HelloRouter {
    RouterFunction router() {
      return route(RequestPredicates.path("/hello"), helloHandler());
    }
 
    HandlerFunction helloHandler() {
        return request -> ServerResponse.ok().body(fromObject("Hello World"));
    }
}


작성하고 Application.java를 springboot로 실행해고 http://localhost:8080/hello 를 호출해도 404 NotFound 이다. 왜일까? 
문서 상으로 Running Server (링크)가 있지만 좀 귀찮은 방식이라 좀 더 찾아보니 링크 처럼 spring-boot-starter-webflux는 RouterFunction beans에 대해서 자동으로 디텍팅을 해준다고 한다. 

그래서 class HelloRouter 위에 @Configuration 와, router() 메소드 위에 @Bean을 붙여준 후 다시 http://localhost:8080/hello 를 실행해본다면 ‘Hello World’ 메세지를 확인 할 수 있을 것이다.

@Configuration
public class HelloRouter {
    @Bean
    RouterFunction router() {
        return route(RequestPredicates.path("/hello"),
                     request -> ServerResponse.ok()
                                              .body(fromObject(new Date())));
    }
}
처럼 좀 더 괜찮은 방식으로 써도 괜찮다. 

3. 여러개의 Router ( Controller ) 만들기 


 위의 예제를 보면 1개의 API는 1개의 Router를 만들고 Bean으로 등록해야 하는 것 같다. 하지만 그렇게 불편해지면 다 안쓰겠지
@Bean
RouterFunction router() {
    return route(GET("/hello"), request -> ServerResponse.ok().body(fromObject("Hello-World")))
        .andRoute(GET("/time"), request -> ServerResponse.ok().body(fromObject(new Date())))
        .andRoute(DELETE("/delete"), request -> {
                      System.out.println("delete");
                      return ServerResponse.ok().body(Mono.empty());});
}


처럼 andRoute를 이용해서 체이닝을 할 수도 있고 많은 static method를 이용해서 URI Mapping도 좀 더 짧게 코딩 할 수 있다. 아래 처럼 메소드 래퍼런스를 이용하여 좀 더 깔끔하게 정리할 수도 있다.

public class HelloHandler {
 
  public Mono helloWorld(ServerRequest request){
    return ok().body(fromObject("Hello-World"));
  }
 
  public Mono time(ServerRequest request){
    return ok().body(fromObject(new Date()));
  }
 
  public Mono printlog(ServerRequest request){
    System.out.println("delete");
    return ServerResponse.ok().body(Mono.empty());
  }
}
 
@Configuration
public class HelloRouter {
  HelloHandler helloHandler = new HelloHandler();
  @Bean
  RouterFunction router() {
    return route(GET("/hello"), helloHandler::helloWorld)
      .andRoute(GET("/time"), helloHandler::time)
      .andRoute(DELETE("/delete"), helloHandler::printlog);
    }
  }

4. Request에서 Parameter 받아오기 


고전적인 방식의 PathVariable이랑, Command pattern인 RequestBody의 구현에 대해서 아래 코드를 참고 하면 된다.
public Mono getName(ServerRequest request){
   String name = request.pathVariable("name");
   return ok().body(fromObject(name));
}
 
//curl -H "Content-Type: application/json" -X POST -d '{"id":"id","name":"name"}' http://localhost:8080/param/body
public Mono getBody(ServerRequest request){
   Mono notFound = ServerResponse.notFound().build();
   Mono user = request.bodyToMono(User.class);
   return user.flatMap(u -> ok().body(u).switchIfEmpty(notFound));
}
springboot가 스타트업 될때 로깅메시지를 보면 ReactiveWebServerApplicationContext와 함께 /hello 매핑 정보가 없다는 것도 볼 수 있다.
2017-04-22 21:07:59.915  INFO 1806 --- [           main] .r.c.ReactiveWebServerApplicationContext : Refreshing org.springframework.boot.web.reactive.context.ReactiveWebServerApplicationContext@1dd92fe2: startup date [Sat Apr 22 21:07:59 KST 2017]; root of context hierarchy
2017-04-22 21:08:00.701  WARN 1806 --- [           main] o.h.v.m.ParameterMessageInterpolator     : HV000184: ParameterMessageInterpolator has been chosen, EL interpolation will not be supported
2017-04-22 21:08:00.909  INFO 1806 --- [           main] o.s.w.r.handler.SimpleUrlHandlerMapping  : Mapped URL path [/webjars/**] onto handler of type [class org.springframework.web.reactive.resource.ResourceWebHandler]
2017-04-22 21:08:00.910  INFO 1806 --- [           main] o.s.w.r.handler.SimpleUrlHandlerMapping  : Mapped URL path [/**] onto handler of type [class org.springframework.web.reactive.resource.ResourceWebHandler]


gitlink : https://github.com/bistros/webflux-sample


신고

VTD-XML 을 이용한 XML 분리.

spring framework 2014.02.14 22:01 posted by dev.bistro


http://bistros.tistory.com/entry/JAXP-DOM4J-JDOM2%EC%9D%98-%EC%84%B1%EB%8A%A5-%EB%B9%84%EA%B5%90 이 글이나 http://vtd-xml.sourceforge.net/userGuide/5.html 이 글을 보면, 딱히 VTD-XML을 이용하는 이유를 설명안해도 될것같다.


1개의 거대한  XML Documet를 특정 XPATH로 분리하여 처리할 이슈가 있었다. 처음에는 STAX을 이용하여 Current Node를 계속 포인팅해가면서 StringBuiler에 기록해나가는 방식이었는데.. 아래처럼 쓰기로 결정하였다. (VTD의 라이브러리를 확실히 파악하지 못해서 개선할 여지는 충분할 것이라 생각한다)


대략적인 기능: 

ROOT NODE - 예) /documents/document

ID NODE  - 예) /documents/document/id

2개의 XPATH정보로부터  <ID, XML Document의 String> 형태로 얻어냄


    @Override
    public HeusoRawDocument getNext() throws VTDException {
        HeusoRawDocument rawDocument=null;
        String content=null, docId=null;

        try {
            int rootTagPoint= rootAutoPilot.evalXPath();
            if(rootTagPoint != -1){
                long elementFragment = docVN.getElementFragment();
                int startPoint = (int)elementFragment;
                int pointLength= (int)(elementFragment >> 32);
                content =  docVN.toRawString(startPoint, pointLength);

                int docIdTagPoint = docIdAutoPilot.evalXPath();
                if(docIdTagPoint != -1){
                    docId = docVN.getXPathStringVal();
                }
            }
        } catch (VTDException vtde){
            logger.error("VTD-XML 파서가 파일에서 1개씩 읽어오는 동안 오류가 발생하였습니다. 대략적인 위치는 인덱스 : {} ", count);
            throw vtde;
        }

        if ( content != null && docId != null ){
            String fixedLengthDocId = StringUtils.leftPad(docId, DOCUMENT_ID_LENGTH);
            rawDocument = new HeusoRawDocument(collId, fixedLengthDocId, content);
            count++;
        }
        return rawDocument; 

}


신고

모 커뮤니티에 스프링 컨트롤러에서 ModelAndView가 아닌 String 으로 반환 할 경우 어떻게 되나 라는 글이 올라왔다.


대충 알고 있지만, 한정 정리를 해볼까한다.


소스코드 : STS케플러 버전에서 Spring MVC Project를 생성하면 아래와 같은 HomeController 코드가 기본으로 생긴다. 보시다시피 한국에서 가장 많이 쓰이는(?)  ModelAndView 반환 형식이 아니다 String이다.

그럼 이 녀석은 어떻게 ModelAndView로 되는걸까?





DispatcherServlet을 통하는것은 동일 하다. 이후 적용되는 ViewNameMethodReturnValueHandler를 참고하면


String 형식일 경우, mavContainer 에 ViewName을 셋팅하는 작업을 하고 있음을 볼 수 있다. 이 코드에서 추가로 볼 수 있는 결국에 이렇게 mavContainer에 셋팅 된 이후




RequestMappingHandlerAdapter에서

ModelMap model = mavContainer.getModel();

ModelAndView mav = new ModelAndView(mavContainer.getViewName(), model);

으로 ModeAndView를 확정한다.


신고

DZONE의 Java&XML에 관련된 글 3개가 마무리된 듯하다.

http://java.dzone.com/articles/java-and-xml-part-1

http://java.dzone.com/articles/java-and-xml-part-2

http://java.dzone.com/articles/java-and-xml-part-3-jaxb


기존의 RDB에 데이터를 저장하는것으로는 한계에 부딪쳐서, Key-Document 형태로  저장한다. 이 Document는 XML이며 여기에서 XPath기반으로 데이터를 추출해야한다. 뭐 몇건안되면 문제가 안되겠지만.. 몇건이 된다는 거다. 
6core * cpu 2개, 메모리16G  2대로 이 키워드 추출 부분을 소화해내야 하는데 하루에 몇억건이므로 성능을 무시할 수는 없다

현재는 StAX을 커스텀 마이징한 XML리더와, XPATH를 사용하기 위한 DOM(Java6)을 사용하고 있다. 여기에서 JDOM2으로 변경을 확인해보고 싶어서. 실제로 사용하는 1.2GB XML파일 아.. 다운받는데도 몇분이야 130MB의 welformed XML을 가지고 테스트 하였다.


XPath 표현식은 1개/ 3개일 경우를 테스트 했고, 가능한 로직은 동일하게 했다


1. DOM

for(int index=0; index<expres.size(); index++){

NodeList nodes = (NodeList) xPath.compile( expres.get(index) ).evaluate(document, XPathConstants.NODESET);

total = total + nodes.getLength(); 

}


2. JDOM2 (2.0.2)

for(int index=0; index<exps.size(); index++){

List<Object> nodes = xpathFactory.compile( exps.get(index) ).evaluate(document);

total = total + nodes .size();

}




 

 TOTAL

  DOM4J

 JDOM2 

 XPATH 1건 

 127286

  약 25 - 30초

  약 5-6초

 XPATH 3건

 636430

  안끝나... 

  약 20초


현재 알파버전이다 JDK 기본 파서를 이용하고 있지만, 고려하고 있던 VTD와, JDOM2등... 적용로직마다 최적의 파서를 선택하는게 중요한 일임을 다시 한번 깨닫는다 (안끝나...) , 물론 이번에 만든 파서도 개량하는것도 중요하고...


ps, 쓰고보니.. 사내 위키에 VTD, JDOM, StAX등의 성능 비교글이 있네... ㅠㅠ; 단 2011년 글이고 내가 정말 필요한 XPath글은 아니라 참고만 하면될듯하다.

신고

'spring framework > spring-batch' 카테고리의 다른 글

JAXP DOM4J, JDOM2의 성능 비교  (0) 2013.11.13
Spring Batch Admin  (0) 2013.01.11
JobLauncher, SimpleJobLauncher  (0) 2012.08.28
JobParametersIncrementer의 사용.  (0) 2012.08.25

angularjs , $resource 와 spring @requestBody

spring framework 2013.11.11 20:23 posted by dev.bistro

현재 만드는 시스템은 Daum통합검색이라는 매우 레거시-_-한 시스템에 붙여야 한다. 그래서 방어적인 설계와, 프레임웍을 선정하였지만... 운영툴은 다르지 않는가!? 그래서 angularjs를 적용중이다. 
운영툴의 난이도는 둘째치고 angularjs 계속 배우면서 진행하려니 매우 더디네.. 오늘은 INSERT부분에서 좀 막혀서 정리한다.


* Spring 설정

HttpMessageConverter 인터페이스의 구현체를 바라보면 Jackson2 와 Jackson이 있다. 



그 중에서 내가 사용할 MappingJackson2HttpMessageConverter 는 @since 3.1.2이고 Jackson 2를 이용하고 있다.  이 녀석은 생성될 때 아래와 같음을 주의하자.

super(new MediaType("application", "json", DEFAULT_CHARSET), new MediaType("application", "*+json", DEFAULT_CHARSET));

이 컨버터를 RequestMappingHandlerAdapter에 messageConverters로 등록한다. 


이후 아래와 같은 컨트롤러를 작성하고

    @RequestMapping(value = "/api/collection", consumes = APPLICATION_JSON_VALUE, method=RequestMethod.POST)

    @ResponseBody

    public Response addCollection(@RequestBody(required = true) Collection collection){

collectionService.addCollection(collection);

/* .... */  

    }


Javascript에서 아래와 같은 코드를 작성

    var collectionResource = $resource("$!{CONTEXT_ROOT}/collection/api/collection");

    /* ... */

    $scope.submit = function(){

        collectionResource.save (   {

            collId : collInfo.collId,

            name : collInfo.name,

            status : "OFF",

            planner : "loginuser"

         } , function(data){

            /* ... */

        });

    }


anguljarjs의 초보의 서비스 적용기는.. 힘들구나.

신고

spring interceptor

spring framework 2013.11.05 00:36 posted by dev.bistro


술먹고... 쓰는 블로그.

오늘 업무시간의 마지막에 spring interceptor 얘기가 나왔다. 안쓰니 계속 까먹는건 나이 떄문이겠지...

Interceptor은 HandlerInterceptor를 implement 한다. 일종의 필터이다. 이 녀석은 결국에는 HandlerExecutionChain 안에서 handler와 List<HandlerInterceptor> interceptorList 으로 존재한다.  (List이다) 

이 녀석은 마침에 doDispatch 에서 불러지고  

HandlerInterceptor[] interceptors = mappedHandler.getInterceptors(); 이후에 for문을 돈다. 

여기에서 상황에 맞게 아니지 순서에 맞게.. interceptor.preHandle, triggerAfterCompletion,,  interceptor.postHandle를 순회하면서 HandlerInterceptor의 3가지 method를 다 실행하는 것이다.

정리하지면

1. DispatcherServlet이 내부적으로 적용하는 Filter와 같은 기능이다.

2. List이다.  즉 order 가 있다 하지만 order를 정의할 수 없는 메소드가 없으므로.. 아마 설정이 읽는 순서가 실행 순서가 되겠다.

단순해서 ... 더 쓸 말이 없다...


DispatcherServlet src : https://github.com/spring-projects/spring-framework/blob/master/spring-webmvc/src/main/java/org/springframework/web/servlet/DispatcherServlet.java

참고 메소드 : applyPreHandle, applyPostHandle, triggerAfterCompletion

신고


현재 개발중인 플랫폼은 다양한 값들이 필요로 하고, 실행환경에서 변경이 필요하다. 그래서 filter나 properties방식이 아닌, KEY-VALUE Table을 만들어 넣고 이 테이블을 Properties로 쓰려고 하였다.


현재 Mybatis의 사용방법은  'Mapper Annotations'방법에 조금 복잡한 쿼리들은 기존의 XML 방법으로 이용하고 있다.  (참고 :  http://mybatis.github.io/mybatis-3/java-api.html )


[ SELECT NAME, VALUE FROM XXX_PLATFORM_ENV ] 라는 아주 단순한 쿼리이기 때문에 가능하면 annotation 으로 처리하고 싶어서 별짓을 다 해봤으나... 내가 원하는 KEY-VALUE의 MAP으로 받을수가 없었다.

가잡 근접하게 나온 결과가

key = {NAME=key, VALUE=....}, key = {NAME=key, VALUE=....} 이런결과였다 

즉, <Key, Key + 필드> 형태로 나오지 <Key, 1개 필드> 형태는 죽어도 못 뽑아내겠더라... 

대충 

@Select("SELECT name, value  FROM INFO_ENVIRONMENT")
@MapKey("key")
@Results({
@Result(column="name", property="key", id=true),
@Result(column="value", property="value", id=false)
})

어차피 mapper라 상위 service에서 List -> Map 으로 변경하는게 가장 속 편할 거 같다....

(XML mapper나, resultHandler를 쓰지 않고 Map<String, String>을 뽑아 낼수는 없을걸까?)



신고

spring integration + redis

spring framework/spring-integration 2013.06.03 10:44 posted by dev.bistro


2년째 쓰고 있지만 맥북에어11 은... 코딩하다 담 오겠다. 주말에 깨작거리다 그냥 월요일 회사 출근해서 샘플 코드를 돌렸다.


spring-integration 과 mongodb / redis /standrd IO를 위해서는 dependency가 추가로 필요로 하다. 긴장하자

<spring.integration.version>2.2.0.RELEASE</spring.integration.version>

<dependency>

<groupId>org.springframework.integration</groupId>

<artifactId>spring-integration-redis</artifactId>

<version>${spring.integration.version}</version>

</dependency>


<dependency>

<groupId>org.springframework.integration</groupId>

<artifactId>spring-integration-mongodb</artifactId>

<version>${spring.integration.version}</version>

</dependency>

<dependency>

<groupId>org.springframework.integration</groupId>

<artifactId>spring-integration-stream</artifactId>

<version>${spring.integration.version}</version>

</dependency>


아 물론 xml 에서 NameSpace도 지정을 잘 해주자... 

공식 http://static.springsource.org/spring-integration/reference/htmlsingle/ 문서에는 해당 내용이 존재 하지 않아서 http://www.springframework.org/schema/integration/ 에서 직접 찾았다...


http://www.springframework.org/schema/integration/redis

http://www.springframework.org/schema/integration/redis/spring-integration-redis-2.2.xsd

http://www.springframework.org/schema/integration/stream

http://www.springframework.org/schema/integration/stream/spring-integration-stream-2.2.xsd

http://www.springframework.org/schema/data/mongo 

http://www.springframework.org/schema/data/mongo/spring-mongo-1.1.xsd



<bean id="redisConnectionFactory"

class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">

<property name="hostName" value="192.168.172.153" />

<property name="port" value="2000" />

</bean>


<int-redis:store-inbound-channel-adapter

id="listAdapter" connection-factory="redisConnectionFactory" key="f"

channel="splittingChannel" collection-type="LIST" >

<int:poller fixed-rate="2000" max-messages-per-poll="1" />

</int-redis:store-inbound-channel-adapter>


<int:splitter input-channel="splittingChannel" output-channel="stdout"  />


<int-stream:stdout-channel-adapter id="stdout" append-newline="true"/>


신고


티스토리 툴바