잊지 않겠습니다.

Spring @Transactional에 대하여.

DB에 대한 Transaction의 경계를 잡는데에 유용하게 쓰이는 Spring의 @Transaction은 Proxy mode와 AspectJ mode로 동작합니다. 일반적으로 많이 사용하는 것은 Proxy mode를 주로 사용하는데, Proxy mode를 사용하게 되는 경우, 다음과 같은 주의점이 필요합니다.

private method의 @Transaction 미적용 문제

이는 어찌보면 당연한 결과입니다. 우리는 Service 객체를 Proxy를 통해서 얻어오고, 얻어온 객체를 접근할 때 Proxy에 의하여 Transaction이 시작되기 때문에 당연히 이런 상황에서는 Transaction이 적용되지 못합니다.
이 문제는 큰 문제가 될 수 없습니다. 다만, 다음 문제는 조금 생각해봐야지 되는 문제가 발생하게 됩니다.

Transaction이 적용되지 않은 method에서 Transaction이 적용된 method를 호출할때.

소위 말하는 self call의 경우 입니다. 이 경우는 어떻게 될까요? 다음 code를 통해서 확인해보도록 하겠습니다.

먼저, 2개의 public method를 준비합니다.

public interface BookService {
    void doNotTransactionAction();
    void doTransactionAction();
}

그리고, 이에 대한 서비스 구현 코드를 다음과 같이 구성합니다.

    @Override
    public void doNotTransactionAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.isNull(TransactionSynchronizationManager.getCurrentTransactionName());
    }

    @Transactional(readOnly = false)
    @Override
    public void doTransactionAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.notNull(TransactionSynchronizationManager.getCurrentTransactionName());
    }

코드 내용은 매우 단순합니다. method가 호출되면 TransactionSynchronizationManager에서 현 Transaction의 이름을 확인하는 코드입니다. 첫번째 doNotTrancationAction에서는 당연히 Transaction이 없어야지 되고, 두번째 doTransactionAction에서는 당연히 Transaction이 있어야지 될 것으로 생각할 수 있습니다. 이제 이 두 코드를 호출하는 테스트 코드를 작성해서 시험해보면 다음과 같은 결과를 보입니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DomainConfiguration.class)
public class BookServiceImplTest {
    @Autowired
    private BookService bookService;

    @Before
    public void setUp() {
        assertThat(bookService, is(not(nullValue())));
    }

    @Test
    public void doNotTransactionAction() {
        bookService.doNotTransactionAction();
    }

    @Test
    public void doTransactionAction() {
        bookService.doTransactionAction();
    }
}

매우 당연한 결과이지만, 이 결과를 이제 수정해서 다음 코드로 바꾸어보도록 하겠습니다. Transaction이 적용되지 않은 code에서 Transaction이 적용된 method를 호출하는 경우에 어떻게 진행되는지 보도록 하겠습니다.

    @Override
    public void doNotTransactionAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.isNull(TransactionSynchronizationManager.getCurrentTransactionName());
        doTransactionAction();
    }

결과는 다음과 같이 Assert Error가 나타나게 됩니다.

java.lang.IllegalArgumentException: [Assertion failed] - this argument is required; it must not be null
    at org.springframework.util.Assert.notNull(Assert.java:112)
    at org.springframework.util.Assert.notNull(Assert.java:123)
    at me.xyzlast.bookstore.services.BookServiceImpl.doTransactionAction(BookServiceImpl.java:54)
    at me.xyzlast.bookstore.services.BookServiceImpl.doNotTransactionAction(BookServiceImpl.java:47)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

왜 이런 결과를 나타내게 될까요? 이는 Spring Transaction이 Proxy로 동작하기 때문입니다. Public으로 Service interface에 접근할 때, Transaction이 동작하기 때문에 발생하는 에러입니다. 이 문제는 보다 더 복잡한 문제를 야기할 수 있습니다. 다음과 같은 코드를 확인해보도록 하겠습니다.

    @Transactional(readOnly = false)
    @Override
    public void doTransactionAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.notNull(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.isTrue(!TransactionSynchronizationManager.isCurrentTransactionReadOnly());
    }

    @Transactional(readOnly = true)
    @Override
    public void doTransactionReadonlyAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.notNull(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.isTrue(TransactionSynchronizationManager.isCurrentTransactionReadOnly());
        doTransactionAction();
    }

2개의 Transaction action이 있습니다. 하나는 ReadOnly로 동작하고, 나머지 하나는 Not ReadOnly입니다. 그런데 ReadOnly method에서 Not ReadOnly method를 내부에서 호출하게 되면 어떻게 될까요. 결과는 Not ReadOnly method 내부에서도 Transaction은 ReadOnly로 동작하게 됩니다. 이는 매우 심각한 Transaction문제를 가지고 오게 됩니다.

Summary

Spring의 @Transactional annotation은 매우 훌륭합니다. 다만 이를 PROXY mode에서 사용할 때는 다음과 같은 원칙을 알고 사용해야지 됩니다.

  1. private method에서는 동작하지 않음
  2. transaction이 적용되지 않은 public method 내부에서 transaction이 적용된 public method를 호출하는 경우, transaction이 동작하지 않는다.
  3. readonly transaction이 적용된 public method 내부에서 not-readonly transaction이 적용된 public method를 호출하는 경우, 모든 method는 readonly transaction으로 동작하게 된다.
  4. (3)의 경우는 반대로도 적용된다.


Posted by Y2K
,

WebSocket

Spring 4.0에서부터 지원하는 기능중 가장 눈여겨봐야지 될 내용은 WebSocket입니다.

WebSocket은 RFC6455에서 정의한 기능으로 다음과 같은 특징을 가지고 있습니다.

  • Real-time full duplex communication over TCP
  • Uses port 80 / 443
  • URL scheme : ws, wss (SSL)
  • Small overhead for text message (frame base) - 0x00 ~ 0xFF
  • Ping/Pong frames for staying alive

WebSocket을 이용해서 접근하게 되면 다음과 같은 Request와 Server Response가 나타납니다.

  • Request

    GET /echo HTTP/1.1
    Host: localhost:8080
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Sec-WebSocket-Version: 13
    Origin: http://localhost:9000
    Sec-WebSocket-Key: HfYHmSeeKJWzbQv/K5yBVQ==
    Cookie: _ga=GA1.1.18456469.1406047636
    Connection: keep-alive, Upgrade
    Upgrade: websocket
    
  • Response

    HTTP/1.1 101 Switching Protocols
    Server: Apache-Coyote/1.1
    Upgrade: websocket
    Connection: upgrade
    Sec-WebSocket-Accept: i2iJBvVoYvXsVlrnYki5BNeKjew=
    Date: Wed, 23 Jul 2014 17:56:03 GMT
    

WebSocket의 javascript client code는 다음과 같이 구성될 수 있습니다.

  $scope.ws = new WebSocket('ws://localhost:8080/echo');
  $scope.ws.onopen = function() {
    console.log('websocket opened');
  };
  $scope.ws.onmessage = function(message) {
    console.log(message);
    console.log('receive message : ' + message.data);
  };
  $scope.ws.onclose = function(event) {
    console.log(event);
    console.log('websocket closed');
  };

이제 Spring 4.0에서 지원하는 WebSocket을 구성해보도록 하겠습니다. WebSocket을 지원하는 서버를 개발하기 위해서는 다음과 같은 절차로서 구성되어야지 됩니다.

1) MessageHandler의 구성
2) Spring Configuratio에 ConnectionEndPoint 설정

이제 한 단계식 구성을 따라가보도록 하겠습니다.

MessageHandler의 구성

Client에서 전송되는 message를 처리하는 MessageHandler입니다. MessageHandler는 크게 2가지로 나눌 수 있습니다. TextWebSocketHandler와 BinaryWebSocketHandler가 그것입니다. 이 두가지는 이름으로 알 수 있듯이, Text Message/Binary Message를 각각 처리하는데 사용됩니다.

먼저, Input Text Message에 ECHO:를 붙여주는 매우 단순한 TextMessageHandler를 구성해보도록 하겠습니다. TextMessgeHandler의 경우 매우 중요합니다. json data의 경우에도 역시 TextMessageHandler를 통해서 처리가 되는 것이 일반적이기 때문에 큰 Binary를 처리하거나, 아니면 암호화가 된 특정 이진데이터를 처리하는 것이 아니라면 대부분의 WebSocket 서버에서 사용될 내용은 TextMessageHandler를 기반으로 구성이 가능합니다.

단순한 EchoTextMessageHandler는 다음과 같이 구성될 수 있습니다.

@Component
public class EchoHandler extends TextWebSocketHandler {
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        TextMessage echoMessage = new TextMessage("ECHO :" + message.getPayload());
        session.sendMessage(echoMessage);
    }
}

ConnectionEndPoint의 구성

WebSocket을 이용한 client code를 보시면 쉽게 아실 수 있는 것이, connection의 endpoint가 존재합니다. 이 endpoint를 통해서 데이터를 보내고, 받는 작업을 할 수 있습니다. 이런 endpoint를 지정하는 작업은 다음과 같이 진행할 수 있습니다.

ConnectionEndPoint를 구성하기 위해서 Spring은 @EnableWebSocket annotation과 WebSocketConfigurer interface를 제공하고 있습니다. 여기서 중요한 것은 WebSocketConfigurer interface입니다. interface는 다음과 같이 구성됩니다.

public interface WebSocketConfigurer {
    void registerWebSocketHandlers(WebSocketHandlerRegistry registry);
}

registerWebSocketHandlers 라는 method 하나만이 존재하고, 이 method안에 connection endpoint를 다음과 같이 구성할 수 있습니다.

    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(echoHandler, "/echo");
    }

/echo를 connection endpoint로 만들어주고, 연결된 connection에 대한 Handler를 지정해줍니다. DI를 위해 방금 만든 EchoHandler를 Bean으로 등록하고, 등록된 Bean을 이용하는 전체 @Configuration code는 다음과 같습니다.

@Configuration
@EnableWebSocket
@EnableWebMvc
@ComponentScan(basePackages = {
        "me.xyzlast.controllers",
        "me.xyzlast.handlers"
})
public class ControllerConfiguration extends WebMvcConfigurerAdapter implements WebSocketConfigurer {
    @Autowired
    private EchoHandler echoHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(echoHandler, "/echo");
    }
}

위 설정은 WebSocket 서버와 WebMVC를 모두 다 지원하기 위해서 구성한 결과입니다.

이제 WebSocket을 이용한 Echo Server 구성이 모두 마쳐졌습니다. tomcat 7.x 이상이 필요하며, angularJS를 이용한 client page는 다음과 같이 구성할 수 있습니다.

angular.module('websocketClientApp').controller('WsCtrl', function ($scope, $timeout) {
//STARTOFCONTROLLER
$scope.message = '';
$scope.ws;
$scope.echoMessages = [];

$scope.init = function() {
  $scope.ws = new WebSocket('ws://localhost:8080/echo');
  $scope.ws.onopen = function() {
    console.log('websocket opened');
  };
  $scope.ws.onmessage = function(message) {
    console.log(message);
    console.log('receive message : ' + message.data);
    $scope.echoMessages.unshift(message.data);
    $timeout(function() {
      $scope.$apply('echoMessages');
    })
  };
  $scope.ws.onclose = function(event) {
    console.log(event);
    console.log('websocket closed');
  };
};

$scope.send = function() {
  $scope.ws.send($scope.message);
};

$scope.init();
//ENDOFCONTROLLER
});
<h3>WebSocket Test Page</h3>

<input type="text" ng-model="message"/>
<button ng-click="send()">SEND</button>

<ul ng-repeat="echo in echoMessages">
  <li>{{echo}}</li>
</ul>

결과는 다음과 같습니다.

SocketJS

WebSocket은 지금까지 사용되고 있던 Ajax에 비하여 매우 훌륭한 방법입니다. 그렇지만, WebSocket이 Spring 4.0에서나 지원이 된 이유는 약간 발전이 너무나도 느렸습니다. WebSocket을 지원하는 Browser의 종류는 다음과 같습니다.

WebSocket의 경우, IE 10이상에서 지원하고 있기 때문에, 현실상 지원하기 힘듭니다. 이 문제를 해결하기 위해서, socket.io와 같은 websocket을 지원하지 않는 browser에서 websocket과 같이 접근하기 위한 방법들이 계속해서 나왔는데, SockJS도 그 방법들중 하나입니다.

SockJS는 기본적으로 WebSocket을 이용하고 있고, interface가 동일합니다. 다만 WebSockeet과는 다음과 같은 차이점을 가지고 있습니다.

  1. schema가 ws가 아닌 http
  2. WebSocket과 같이 Browser에서 제공되는 library가 아닌, 외부 library 사용
  3. IE 6이상부터 지원합니다.
  4. Server 측에서도 websocket이 아닌 SockJS server side library를 사용

websocket과 sockjs의 client code는 다음과 같습니다. 비교해보시면 schema만을 제외하면 완전히 동일한 것을 알 수 있습니다.

WebSocket

var ws = new WebSocket('ws://domain/endpoint');
ws.onopen = function() {
  console.log('open socket');
}
ws.onmessage = function(message) {
  console.log('message', message.data);
}
ws.onclose = function(e) {
  console.log('close');
}

SockJS

var sock = new SockJS('http://domain/endpoint');
sock.onopen = function() {
  console.log('open socket');
}
sock.onmessage = function(message) {
  console.log('message', message.data);
}
sock.onclose = function(e) {
  console.log('close');
}

Spring 4.0은 SockJS 서버를 지원하고 있으며, 지원 방법은 WebSocket 서버와 거의 유사합니다. 아니 동일한 수준입니다.
SockJS 서버의 구축 방법은 WebSocket 서버의 구축 방법과 동일합니다.

1) MessageHandler의 구성
2) Spring Configuratio에 ConnectionEndPoint 설정

MessageHandler의 구성

MessageHandler는 WebSocket과 완전히 동일합니다.

Connection EndPoint의 구성

Connection EndPoint의 경우에도 거의 유사합니다. 다음은 WebSocket의 endpoint와 SockJS의 endpoint 비교입니다.

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(echoHandler, "/echo");
        registry.addHandler(echoHandler, "/echojs").withSockJS();
    }

주의할점은 SockJS와 WebSocket의 endpoint를 동일하게 넣어서는 안된다는 점입니다. 같은 port를 이용하고, schema만이 다르기 때문에 접근되는 endpoint가 동일한 경우, 오동작을 발생시키면서 connection이 종료됩니다.

SockJS client의 구성

SockJS의 경우, bower에서 지원하고 있습니다. 다음 command를 통해서 bower로 install을 할 수 있습니다.

bower install sockjs --save

angularjs + yeoman을 통해서 구성한 sockjs client code는 다음과 같습니다.

angular.module('websocketClientApp').controller('SockCtrl', function ($scope) {
//STARTOFCONTROLLER
$scope.message = '';
$scope.sock;

$scope.init = function() {
  $scope.sock = new SockJS('http://localhost:8080/echojs');
  $scope.sock.onopen = function() {
    console.log('websocket opened');
  };
  $scope.sock.onmessage = function(message) {
    console.log(message);
    console.log('receive message : ' + message.data);
  };
  $scope.sock.onclose = function(event) {
    console.log(event);
    console.log('websocket closed');
  };
};

$scope.send = function() {
  $scope.sock.send($scope.message);
};

$scope.init();
//ENDOFCONTROLLER
});
<script src="bower_components/sockjs/sockjs.js"></script>

<h3>SockJS Test Page</h3>

<input type="text" ng-model="message"/>
<button ng-click="send()">SEND</button>

STOMP

STOMP는 text 지향의 message protocol입니다. websocket을 이용한 message handling을 보다 더 쉽게 만들어줍니다. WebSocket이나 SockJS의 경우 onmessage function에서 받는 메세지를 모두 handling해야지 되는 단점을 가지고 있습니다. 이러한 단점을 구독(subscription)과 사용자 구독(user)를 통해서 처리할 수 있다는 장점을 가지고 있습니다.

STOMP를 이해하기 위해서는 Client code의 흐름을 보는 것이 좀 더 이해가 편합니다.

$scope.client;
$scope.name = '';
$scope.message = '';

$scope.init = function() {
  var socket = new SockJS('http://localhost:8080/endpoint'); //SockJS endpoint를 이용
  $scope.client = Stomp.over(socket); //Stomp client 구성
  $scope.client.connect({}, function(frame) {
    console.log('connected stomp over sockjs');
    // subscribe message
    $scope.client.subscribe('/subscribe/echo', function(message) {
      console.log('receive subscribe');
      console.log(message);
    });
  });
};

// send message
$scope.send = function() {
  var data = {
    name: $scope.name,
    message: $scope.message
  };
  $scope.client.send('/app/echo', {}, JSON.stringify(data));
};

STOMP는 기본적으로 subscribe를 통해서 데이터를 전달받습니다. 따라서, 이 message가 어떠한 method에 의해서 전송을 받았는지를 좀 더 명확히 할 수 있습니다. 기존의 WebSocket이나 SockJS를 이용하게 되는 경우에는 전달받는 message의 범위가 매우 광범위하기 때문에, 전달되는 메세지에 message key값을 넣어서 send/receive에 대한 처리를 따로 해줘야지 되는 것이 일반적입니다. 그렇지만, STOMP의 경우에는 접근하는 방식이 send point와 subscribe point가 각기 다르기 때문에 처리를 좀 더 자유롭게 해줄 수 있습니다.

STOMP에서는 subscribe와 user/** 두개로 subscribe의 target을 나눌수 있는데, 각각의 차이는 전자는 전역이고, 후자는 호출된 사용자에게만 던져진다는 차이를 가지고 있습니다.

이제 Spring을 이용한 STOMP 서버 구축에 들어가보도록 하겠습니다.

Dependency 설정

STOMP를 지원하기 위해서 Spring은 spring-message에 대한 dependency를 필요로 합니다.

compile "org.springframework:spring-messaging:${rootProject.ext.springVersion}"

@Configuration 설정

STOMP를 지원하기 위해서는 @EnableWebSocketMessageBroker과 @Configuration에서는 AbstractWebSocketMessageBrokerConfigurer를 상속받거나, WebSocketMessageBrokerConfigurer 인터페이스를 구현해야지 됩니다.

@Configuration에서 할 일은 2가지입니다.

1) EndPoint의 설정
2) (option) STOMP URL에 대한 전역 prefix 설정
3) (option) send에 대한 subscribe url의 prefix 설정
4) @EnableWebSocketMessageBroker 설정

EndPoint 설정

EndPoint의 경우, SockJS를 사용할지, WebSocket을 사용할지에 따라 다른 설정을 해주게 됩니다. STOMP를 이용하는 것은 대부분 SockJS를 이용해서 Browser의 적합성을 높이는 경우가 많기 때문에, SockJS를 이용하는 것이 일반적입니다. WebSocketMessageBrokerConfigurer 인터페이스의 registerStompEndpoints method를 다음과 같이 정의합니다.

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/endpoint").withSockJS();
    }
prefix 설정

Send Url에 대한 전역 prefix와 subscribe에 대한 전역 prefix를 설정할 수 있습니다. 이는 WebSocketMessageBrokerConfigurer 인터페이스의 configureMessageBroker method 에서 수정하면 됩니다. (아래는 모든 send prefix를 app으로 지정하고, subscribe의 기본 prefix를 subscribe로 지정했습니다.)

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/subscribe");
        registry.setApplicationDestinationPrefixes("/app");
    }
@EnableWebSocketMessageBroker 설정

모든 설정이 완료된 @Configuration code는 다음과 같습니다.

@Configuration
@EnableWebSocketMessageBroker
@ComponentScan(basePackages = {
        "me.xyzlast.controllers"
})
public class StompConfigurer extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/endpoint").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/subscribe");
        registry.setApplicationDestinationPrefixes("/app");
    }
}

@Controller 구성

STOMP의 @Controller는 Spring MVC의 @Controller와 같은 @annotation을 사용하고, STOMP를 제공하기 위한 다음 @annotation들을 제공합니다.

@annotationdescription
@MessageMappingSTOMP client의 send에 대한 target url입니다.
@SendToSTOMP client의 subscribe에 대한 target url을 지정합니다. (지정되지 않은 경우, @MessageMapping에 지정된 URL + @Configuration에서 설정된 prefix URL을 이용합니다. )
@SubscribeEventclient에서 subscribe 할 수 있는 url을 지정합니다. 특정 message가 발생하거나 event가 발생했을 때, Client에 값을 전송하는데 사용합니다.

annotation이 적용된 @Controller code는 다음과 같이 구성될 수 있습니다.

@Controller
public class EchoController {
    @MessageMapping("/echo")
    @SendTo("/subscribe/echo")
    public Hello sendEcho(Hello hello) throws Exception {
        System.out.println("receive message : " + hello.toString());
        Hello echoHello = new Hello();
        echoHello.setMessage(hello.getName());
        echoHello.setName(hello.getMessage());
        return echoHello;
    }
}

위 @Controller의 sendEcho methos는 /echo url을 통해 데이터를 전달받고, /subscribe/echo를 통해 데이터를 전달하게 됩니다.

기존의 SockJS와는 다르게 MessageHandler를 따로 구현하지 않습니다. 기본적으로 @Controller를 이용해서 처리하기 때문에 기존의 WebMvc와 거의 유사한 형태의 개발을 진행할 수 있습니다.

User Destination

STOMP에서는 특정 사용자들에게 메세지를 보낼 수 있습니다. 기본값으로 설정되어 있는 /user로 시작되는 subscribe의 경우에는 각 사용자들이 받을 수 있는 url을 제공합니다. 이는 사용자들의 특화된 message를 보내거나 error handling시에 주로 사용됩니다.

client.subscribe('/user/queue/private-message') = function(messaage) {

}

이러한 User subscribe는 Spring에서 @SendToUser annotation으로 기능이 따로 제공되고 있습니다.

    @MessageMapping("/message")
    @SendToUser
    public String sendMessage(String message) {
        return message.toUpperCase();
    }

    @MessageExceptionHandler
    @SendToUser("/queue/errors")
    public String handlingException(IllegalStateException ex) {
        return ex.getMessage();
    }


Posted by Y2K
,

Spring Security를 이용한 REST API를 만들때, 일반적인 Spring Security를 이용하는 경우에는 다Login 성공 또는 실패시에 모두 302번 Redirect가 발생하게 됩니다. 또한 인증되지 않은 사용자가 API에 접근한 경우에도 마찬가지로 302번 Redirect로 Login Page로 이동하게 되는데, 이는 REST API 개발시에는 원하지 않는 결과입니다.

먼저, 개발할 API 서버는 Form 인증을 지원하는 것을 전제로 합니다. Basic/Digest 인증의 경우에는 다른 방법으로 개발이 되어야지 됩니다.

일반적인 Form 인증을 이용하는 경우에 Spring Security의 기본 Form인증은 다음 문제를 해결해야지 됩니다.

  1. 인증되지 않은 Request가 들어오는 경우, 인증 Page로 Redirect
  2. Login 성공시, 200 Response가 되지 않고, Success Page로 Redirect
  3. Login 실패시, 200 Response가 되지 않고, Failure Page로 Redirect

결국은 모두 Redirect문제가 발생하게 됩니다. 이에 조금 추가를 해서, 일반적으로는 Spring Security는 application이 시작할 때, 각각의 URL에 대한 ROLE을 설정하게 됩니다. 이러한 ROLE의 설정 없이, Request가 발생하였을때 인증된 사용자인 경우에 ROLE을 확인하는 기능을 추가해보도록 하겠습니다.

인증되지 않는 Request에 대한 문제해결

Spring Security는 ErrorHandling에 인증되지 않았을 때의 동작을 AuthenticationEntryPoint를 설정하여 제어할 수 있습니다. 기본적으로 Form인증을 사용하는 경우에는 org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint가 설정이 됩니다. Spring code를 보면 다음과 같이 구성되어 있습니다.

public class LoginUrlAuthenticationEntryPoint implements AuthenticationEntryPoint, InitializingBean {
    public LoginUrlAuthenticationEntryPoint(String loginFormUrl) {
        this.loginFormUrl = loginFormUrl;
    }


    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException)
            throws IOException, ServletException {

        String redirectUrl = null;

        if (useForward) {
            if (forceHttps && "http".equals(request.getScheme())) {
                // First redirect the current request to HTTPS.
                // When that request is received, the forward to the login page will be used.
                redirectUrl = buildHttpsRedirectUrlForRequest(request);
            }

            if (redirectUrl == null) {
                String loginForm = determineUrlToUseForThisRequest(request, response, authException);

                if (logger.isDebugEnabled()) {
                    logger.debug("Server side forward to: " + loginForm);
                }

                RequestDispatcher dispatcher = request.getRequestDispatcher(loginForm);

                dispatcher.forward(request, response);

                return;
            }
        } else {
            // redirect to login page. Use https if forceHttps true

            redirectUrl = buildRedirectUrlToLoginPage(request, response, authException);

        }

        redirectStrategy.sendRedirect(request, response, redirectUrl);
    }
}

보시면 최종적으로는 sendRedirect method를 통해서 login url로 redirect를 시켜주고 있는 것을 볼 수 있습니다. 이 부분에 대한 제어만을 변경시켜준다면 우리가 원하는 조건을 만들어줄 수 있습니다.

새롭게 만들어지는 RestAuthenticationHandler는 매우 단순한 코드를 갖습니다. 인증이 되지 않은 사용자들에게 401 status를 던질뿐이니까요.

@Component
public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint {
    @Override
    public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

이제 이 부분을 Spring Security Configuration에 설정을 해줍니다.

@Configuration
@EnableWebSecurity
@ComponentScan(basePackages = "com.daesung.fms.api.securities")
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .exceptionHandling()
                .authenticationEntryPoint(restAuthenticationEntryPoint())
            .formLogin()
                .loginProcessingUrl("/auth/login")
                .usernameParameter("username")
                .passwordParameter("password")
                .and()
            .logout()
                .logoutUrl("/auth/logout");
    }
}

exceptionHandling()에 구성된 RestAuthenticationEntryPoint 객체만을 설정해주는 것으로 이제 인증되지 않은 request에는 401 status를 보내줄 수 있게 되었습니다.

인증 성공시, 200 status 반환

Spring Security를 이용한 Form 인증을 성공하는 경우, 일반적으로 SuccessfulUrl로 Redirect가 됩니다. 그런데 REST API의 경우에는 이와 같은 구성이 문제가 됩니다. 우리는 인증을 정상적으로 했는지에 대한 확인이 필요한 것이지, 인증을 한 후에 특정 Page로의 Redirect를 원하는 것이 아니기 때문입니다.

Form 인증에서, 인증이 성공한 경우에는 Spring Security 는 기본적으로 org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler에서 동작을 담당합니다. code를 보면 다음과 같습니다.

public class SavedRequestAwareAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
            Authentication authentication) throws ServletException, IOException {
        SavedRequest savedRequest = requestCache.getRequest(request, response);

        if (savedRequest == null) {
            super.onAuthenticationSuccess(request, response, authentication);

            return;
        }
        String targetUrlParameter = getTargetUrlParameter();
        if (isAlwaysUseDefaultTargetUrl() || (targetUrlParameter != null && StringUtils.hasText(request.getParameter(targetUrlParameter)))) {
            requestCache.removeRequest(request, response);
            super.onAuthenticationSuccess(request, response, authentication);

            return;
        }

        clearAuthenticationAttributes(request);

        // Use the DefaultSavedRequest URL
        String targetUrl = savedRequest.getRedirectUrl();
        logger.debug("Redirecting to DefaultSavedRequest Url: " + targetUrl);
        getRedirectStrategy().sendRedirect(request, response, targetUrl);
    }

    public void setRequestCache(RequestCache requestCache) {
        this.requestCache = requestCache;
    }
}

매우 단순한 code입니다. 여기서 중요한 것은 onAuthenticationSuccess method의 끝부분에 위치한 getRedirectStraegy()입니다. 이 부분만을 제거하고, 200 status만을 반환할 수 있다면 우리가 원하는 결과를 얻어낼 수 있습니다. 인증이 성공한 다음에 실행되기 때문에 매우 단순하게 만들 수 있습니다.

public class RestLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

    private RequestCache requestCache = new HttpSessionRequestCache();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        handle(request, response, authentication);
        clearAuthenticationAttributes(request);
    }

    protected void handle(HttpServletRequest request, HttpServletResponse response, Authentication authentication)
            throws IOException, ServletException {
        SavedRequest savedRequest = requestCache.getRequest(request, response);

        if (savedRequest == null) {
            clearAuthenticationAttributes(request);
            return;
        }
        String targetUrlParam = getTargetUrlParameter();
        if (isAlwaysUseDefaultTargetUrl() ||
                (targetUrlParam != null &&
                        StringUtils.hasText(request.getParameter(targetUrlParam)))) {
            requestCache.removeRequest(request, response);
            clearAuthenticationAttributes(request);
            return;
        }
        clearAuthenticationAttributes(request);
    }
}

기본적으로 매우 단순하게 clearAuthenticationAttribute만 해주고, 다른 처리는 해줄 필요가 없습니다. 아무런 Exception이 발생하지 않고, response에 특별히 status를 설정하지 않는 경우에는 200 status를 반환하게 되니까요.

이제 작성된 AuthenticationSuccessHandler를 설정하면 Spring Security는 다음과 같은 코드가 됩니다.

@Configuration
@EnableWebSecurity
@ComponentScan(basePackages = "com.daesung.fms.api.securities")
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .exceptionHandling()
                .authenticationEntryPoint(restAuthenticationEntryPoint())
            .formLogin()
                .loginProcessingUrl("/auth/login")
                .usernameParameter("username")
                .passwordParameter("password")
                .successHandler(loginSuccessHandler()) //추가된 부분
                .and()
            .logout()
                .logoutUrl("/auth/logout")
            .authorizeRequests().anyRequest().authenticated();
    }
}

인증 실패 및 로그 아웃시 동작 설정

인증 실패시의 동작과 로그 아웃시의 동작 역시 인증 성공시와 동일하게 처리할 수 있습니다. 조금 차이가 있다면 인증 실패시에는 401번 UNAUTHENTICATION status를 보내야지 되고, 로그 아웃시에는 항시 200 status를 보내야지 되는 차이만이 있을 뿐입니다.

인증실패시의 Handler는 다음과 같이 구성될 수 있습니다.

/**
 * Created by ykyoon on 14. 4. 22.
 * when login failed, return http status code (401).
 */
public class RestLoginFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request,
                                        HttpServletResponse response,
                                        AuthenticationException exception) throws IOException, ServletException {
        response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Unauthorized");
    }
}

그리고, 로그아웃시의 Handler 역시 다음과 같은 code로 구성가능합니다.

/**
 * Created by ykyoon on 14. 4. 21.
 * after Logouted, send http status ok code (200).
 */
public class RestLogoutSuccessHandler extends SimpleUrlLogoutSuccessHandler {
    @Override
    public void onLogoutSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        String targetUrl = determineTargetUrl(request, response);
        if (response.isCommitted()) {
            logger.debug("Response has already been committed. Unable to redirect to " + targetUrl);
        }
    }
}

이제 구성된 두개의 Handler를 추가하면 Spring Security는 다음 code와 같이 변경됩니다.

@Configuration
@EnableWebSecurity
@ComponentScan(basePackages = "com.daesung.fms.api.securities")
public class SecurityConfiguration extends WebSecurityConfigurerAdapter {
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .exceptionHandling()
                .authenticationEntryPoint(restAuthenticationEntryPoint())
            .formLogin()
                .loginProcessingUrl("/auth/login")
                .usernameParameter("username")
                .passwordParameter("password")
                .successHandler(loginSuccessHandler()) 
                .failureHandler(loginFailureHandler()) //추가2
                .and()
            .logout()
                .logoutUrl("/auth/logout")
                .logoutSuccessHandler(logoutSuccessHandler()) //추가2
            .authorizeRequests().anyRequest().authenticated();
    }
}

이렇게 구성하면 이제 Spring Security로 구성되는 REST API를 구성할 수 있습니다.

API URL별 권한 설정

REST API를 개발하면, 각 URL에 따른 사용 권한을 넣어주는 것이 일반적입니다. 또는 URL 단위로 권한을 묶어서 관리하게 되는데, 그럼 API가 하나하나 추가 될때마다 security의 설정에 매번 URL을 hard coding으로 넣어주게 된다면 이는 매우 큰 비용으로 발생되게 됩니다. 사용자에게 권한만 넘겨주고, 그 권한과 URL을 Request마다 체크를 해서 권한을 확인하면 Security에 대한 권한 코드를 좀더 동적으로 사용할 수 있습니다. (DB 값만을 변경시켜주면 URL에 따른 권한을 변경할 수 있습니다.)

먼저, Spring Security는 각각의 URL과 ROLE간의 Matching을 어떻게 하고 있는지에 대한 구조에 대한 이해가 필요합니다. Spring Security의 URL-ROLE간의 matching은 다음과 같이 진행됩니다.

  1. FilterInvocationSecurityMetadataSource을 통해 URL/METHOD를 이용한 접근권한이 어떻게 되는지 확인합니다. 이 과정이 매우 중요합니다. 우리가 Spring Security에 각각의 URL별 권한을 hard coding으로 넣어주는 경우에는, 이 MetadataSource가 hard coding된 것과 같은 역활을 하게 됩니다.
  2. FilterInvocationSecurityMetadataSource에서 얻어온 ROLE들과 사용자가 갖은 ROLE을 이용해서 AccessVote를 진행합니다.

이러한 두과정을 거치는 Filter가 존재를 하는데, 이 필터가 아래 그림의 최종적인 Filter인 FilterSecurityInterceptor입니다.

이러한 Filter Chain에서 새로운 Filter를 만들어서 Spring Security에 추가를 할 예정입니다. Spring Security의 FilterSecurityInterceptor에는 모든 인증된 Request만이 접근할 수 있도록 설정을 하고, 그 뒤에 우리가 새롭게 만든 Filter를 통해서 인증을 처리할 계획입니다.

먼저, 새로운 FilterInvocationSecurityMetaSource가 필요합니다. 이는 FilterInvocationSecurityMetadataSource interface를 상속하면 매우 간단하게 구성 가능합니다.

/**
 * Created by ykyoon on 14. 4. 21.
 * DB 기반의 인증 관리 시스템.
 */
public class FmsFilterInvocationSecurityMetadataSource implements FilterInvocationSecurityMetadataSource {

    public FmsFilterInvocationSecurityMetadataSource(MenuRoleService menuRoleService) {
        this.menuRoleService = menuRoleService;
        parser = new FmsUrlParser();
        permissions = new Hashtable<>();
    }
    private final MenuRoleService menuRoleService;
    private final FmsUrlParser parser;
    private final Map<String, Permission> permissions;

    @Override
    public Collection<ConfigAttribute> getAttributes(Object object) throws IllegalArgumentException {
        final HttpServletRequest request = ((FilterInvocation) object).getRequest();
        final String httpMethod = request.getMethod().toUpperCase();
        final String url = parser.parse(request.getRequestURI());
        final String key = String.format("%s %s", httpMethod, url);

        final Permission permission;
        if(permissions.containsKey(key)) {
            permission = permissions.get(key);
        } else {
            permission = menuRoleService.findByMethodAndUrl(httpMethod, url);
            if(permission != null) {
                permissions.put(key, permission);
            }
        }

        String[] roles;
        if(permission == null) {
            roles = new String[] { "ROLE_ADMIN" };
        } else {
            roles = new String[] { "ROLE_ADMIN", permission.getName() };
        }
        return SecurityConfig.createList(roles);
    }

    @Override
    public Collection<ConfigAttribute> getAllConfigAttributes() {
        return null;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return FilterInvocation.class.isAssignableFrom(clazz);
    }

    /**
     * url - permission hashmap clear
     */
    public void clear() {
        permissions.clear();
    }
}

작성된 code의 menuRoleService는 request URL과 http method를 이용해서 접근 가능한 ROLE을 반환합니다. 약간의 tip으로, DB를 계속해서 읽는 것보다는 Hashtable을 이용해서 한번 읽어들인 ROLE은 또다시 DB 조회를 하지 않도록 MAP을 가지고 있도록 구성하였습니다.

이제 AccessVote를 구현할 차례입니다. AccessVote는 FilterInvocationSecurityMetadataSource에서 넘겨준 ROLE과 사용자의 ROLE간의 비교를 해주게 됩니다.

/**
 * Created by ykyoon on 14. 4. 22.
 * Authorized Name 기반의 인증 Vote class
 */
@Slf4j
public class FmsAccessDecisionVoter implements AccessDecisionVoter<FilterInvocation> {

    @Override
    public boolean supports(ConfigAttribute attribute) {
        return attribute instanceof SecurityConfig;
    }

    @Override
    public boolean supports(Class<?> clazz) {
        return clazz != null && clazz.isAssignableFrom(FilterInvocation.class);
    }

    @Override
    public int vote(Authentication authentication, FilterInvocation fi, Collection<ConfigAttribute> attributes) {
        assert authentication != null;
        assert fi != null;
        assert attributes != null;

        SecurityConfig securityConfig = null;

        boolean containAuthority = false;
        for(final ConfigAttribute configAttribute : attributes) {
            if(configAttribute instanceof SecurityConfig) {
                securityConfig = (SecurityConfig) configAttribute;
                for(GrantedAuthority grantedAuthority : authentication.getAuthorities()) {
                    containAuthority = securityConfig.getAttribute().equals(grantedAuthority.getAuthority());
                    if(containAuthority) {
                        break;
                    }
                }
                if(containAuthority) {
                    break;
                }
            }
        }
        return containAuthority ? ACCESS_GRANTED : ACCESS_DENIED;
    }
}

AccessDecisionVoter는 하는 일이 매우 단순합니다. Authentication이 넘어왔고, 이에 대한 규칙을 이용해서 ROLE을 matching이 되는 것이 있는지 없는지를 확인하는 과정을 거치게 됩니다. 이 코드를 조금 더 수정을 한다면 권한 1, 권한 2, 권한 3 모두를 갖는 사용자만이 접근할 수 있는 규칙 역시 만드는 것이 가능합니다. 이건 Security에 대한 운영원칙과 BL에 따라 달라질 것입니다.

이제 구성된 FilterInvocationSecurityMetadataSource와 AccessDecisionVoter를 Spring Security에 추가해보도록 하겠습니다.

먼저 Bean들을 등록시켜줍니다.

    /**
     * FMS API 권한 Filter.
     * @return securityMetadataSource() 가 적용된 데이터.
     * @throws Exception
     */
    @Bean
    public FilterSecurityInterceptor filterSecurityInterceptor() throws Exception {
        FilterSecurityInterceptor filterSecurityInterceptor = new FilterSecurityInterceptor();
        filterSecurityInterceptor.setSecurityMetadataSource(securityMetadataSource());
        filterSecurityInterceptor.setAuthenticationManager(authenticationManager());
        filterSecurityInterceptor.setAccessDecisionManager(affirmativeBased());
        return filterSecurityInterceptor;
    }

    //여러 Voter를 추가하는 것이 가능. List로 index가 작은 Voter가 먼저 투표. 투표 포기(ACCESS_ABSTAIN)
    //가 되는 경우, 다음 Voter에게 넘어감.
    @Bean
    public AffirmativeBased affirmativeBased() {
        FmsAccessDecisionVoter voter = new FmsAccessDecisionVoter();
        List<AccessDecisionVoter> voters = new ArrayList<>();
        voters.add(voter);
        return new AffirmativeBased(voters);
    }

    @Bean
    public FilterInvocationSecurityMetadataSource securityMetadataSource() {
        return new FmsFilterInvocationSecurityMetadataSource(menuRoleService);
    }

그리고, FilterSecurityInterceptor 뒤에 우리가 만들어준 Filter를 등록시켜주면 됩니다.

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http
            .csrf().disable()
            .exceptionHandling()
                .authenticationEntryPoint(restAuthenticationEntryPoint())
                .and()
            .formLogin()
                .loginProcessingUrl("/auth/login")
                .usernameParameter("username")
                .passwordParameter("password")
                .successHandler(loginSuccessHandler())
                .failureHandler(loginFailureHandler())
                .and()
            .logout()
                .logoutUrl("/auth/logout")
                .logoutSuccessHandler(logoutSuccessHandler())
                .and()
            .authorizeRequests().anyRequest().authenticated()
                .and()
            .addFilterAfter(filterSecurityInterceptor(), FilterSecurityInterceptor.class);
    }

Summary

REST API 서비스 구현시에, Spring Security를 이용한 인증처리를 만들었습니다. 기본적인 Form 인증과 구성된 Security 다음 동작이 다릅니다.

동작기본 Spring Security FormREST Security Form
인증성공시설정된 인증 성공 URL로 302 Redirect200 status return
인증실패시설정된 인증 실패 URL로 302 Redirect401 unauthentication status return
로그아웃시설정된 로그아웃 성공 URL로 302 Redirect200 status return

또한, API의 추가 또는 권한변경시 코드의 변경 없이, DB값을 조회해와서 동적인 ROLE을 설정하는 것이 가능합니다.

인증은 매우 중요하고, 인증은 회사의 운영원칙에 많은 변경이 오게 되는 것이 사실입니다. 특히 REST API를 구성한다면 이 인증에 대한 많은 고민들이 필요하실 것이라고 생각하며 이만 글을 줄입니다. 모두들 Happy Coding~

Posted by Y2K
,

기본적으로 Spring MVC를 이용하는 경우, multipart encrypt를 이용해서 form submit으로 file을 upload하는 것이 일반적입니다. 한번 javascript만을 이용한 file upload를 해보도록 하겠습니다.

먼저, MultipartResolver를 사용하기 위해서 apache common fileupload를 추가해야지 됩니다.

compile 'commons-fileupload:commons-fileupload:1.3.1'

Spring Configuration에 MultipartResolver를 추가합니다.

    @Bean(name = "multipartResolver")
    public MultipartResolver multipartResolver() {
        return new CommonsMultipartResolver();
    }

그리고, 간단한 fileupload html을 꾸밉니다.

<form id="uploadForm" enctype="multipart/form-data">
  <input type="file" id="fileId" name="file-data"/>
</form>
<button id="btn-upload">file upload</button>

그리고, 이에 대한 controller code는 다음과 같습니다.

    @RequestMapping(value = "/file/upload", method = RequestMethod.POST)
    @ResponseBody
    public Object uploadFile(MultipartHttpServletRequest request) {
        Iterator<String> itr =  request.getFileNames();
        if(itr.hasNext()) {
            MultipartFile mpf = request.getFile(itr.next());
            System.out.println(mpf.getOriginalFilename() +" uploaded!");
            try {
                //just temporary save file info into ufile
                System.out.println("file length : " + mpf.getBytes().length);
                System.out.println("file name : " + mpf.getOriginalFilename());
            } catch (IOException e) {
                System.out.println(e.getMessage());
                e.printStackTrace();
            }
            return true;
        } else {
            return false;
        }
    }

일단, 간단히 파일을 실제적으로 저장은 시키지 않았습니다. byte data를 얻어냈기 때문에, byte data만 저장하면 되어서 일단 위의 코드에서는 제거 되어있습니다.

이제 ajax로 file을 올리기 위해서 button에 click event를 binding시켜줍니다.

  $('#btn-upload').on('click', function () {
    console.log('btn-upload');
    var form = new FormData(document.getElementById('uploadForm'));
    $.ajax({
      url: "upload",
      data: form,
      dataType: 'text',
      processData: false,
      contentType: false,
      type: 'POST',
      success: function (response) {
        console.log('success');
        console.log(response);
      },
      error: function (jqXHR) {
        console.log('error');
      }
    });
  });

ajax로 파일이 정상적으로 upload 되는 것을 확인해볼 수 있습니다.

file length : 65893871
file name : 어떤 과학의 초전자포 08.zip
어떤 과학의 초전자포 08.zip uploaded!


Posted by Y2K
,

최신 Gradle 2.0으로 업데이트 한 내용이 존재합니다.


전에 정리한 적이 있는데, 조금 글을 다듬고 정리할 필요성이 있어서 다시 옮깁니다.
먼저, 개발되는 Project는 다음과 같은 구조를 갖습니다.

rootProject
-- domainSubProject (Spring Data JPA + queryDSL)
-- webAppSubProject (Spring Web MVC)

위와 같은 Project는 개발의 편의를 위해서 다음 두 조건을 만족해야지 됩니다.

  1. queryDSL을 사용하기 위한 Q-Entity를 생성이 compileJava task전에 수행되어야지 됩니다.
  2. web application 개발을 위해, tomcat을 실행시킬 수 있어야합니다.
  3. 개발된 web application을 test 환경 또는 product 환경에 배포가 가능해야지 됩니다.

root project

root project는 sub project들의 공통 설정이 필요합니다.

  1. build시에 필요한 plugin의 repository를 설정합니다.
  2. sub project들에서 사용될 공통 dependency들을 설정합니다. (sub project들의 build.gradle 이 너무 길어지는 것을 막을 수 있습니다.)
공통 plugin 추가
  • maven의 POM과 동일한 provided과 optional의 사용을 위해 spring prop plugin을 추가합니다.
  • subprojects들에 필요한 plugin들을 모두 추가합니다. (저는 java와 groovy, idea, eclipse 등을 추가했습니다.)
  • source java version과 target java version을 정해줍니다.
  • source code의 Encoding을 정해줍니다.
  • SubProject에서 기본적으로 사용될 dependency들을 모두 적어줍니다.

build.gradle (root project)

apply plugin: 'base'

// spring prop plugin 추가
buildscript {
    repositories {
        maven { url 'http://repo.springsource.org/plugins-release' }
    }
    dependencies {
        classpath 'org.springframework.build.gradle:propdeps-plugin:0.0.5'
    }
}

subprojects {
    // Plugin 설정, 만약에 code에 대한 static analysis가 필요한 경우에 이곳에 설정.
    apply plugin: 'java'
    apply plugin: 'eclipse'
    apply plugin: 'eclipse-wtp'
    apply plugin: 'idea'
    apply plugin: 'groovy'
    apply plugin: 'propdeps'
    apply plugin: 'propdeps-maven'
    apply plugin: 'propdeps-idea'
    apply plugin: 'propdeps-eclipse'

    // 기본적으로 사용할 repository들을 정의
    repositories {
        mavenCentral()
        maven { url "http://oss.sonatype.org/content/repositories/snapshots/" }
        maven { url "http://192.168.13.209:8080/nexus/content/repositories/releases" }
    }

    dependencies {
        def springVersion = "4.0.1.RELEASE"

        compile 'org.slf4j:slf4j-api:1.7.5'
        compile "org.springframework:spring-context:${springVersion}"
        compile "org.springframework:spring-aspects:${springVersion}"
        compile "org.springframework:spring-jdbc:${springVersion}"
        compile "org.springframework:spring-context-support:${springVersion}"

        compile 'mysql:mysql-connector-java:5.1.27'
        compile 'com.jolbox:bonecp:0.8.0.RELEASE'
        compile 'com.google.guava:guava:15.0'
        compile 'org.aspectj:aspectjrt:1.7.4'
        compile 'org.aspectj:aspectjtools:1.7.4'
        compile 'org.aspectj:aspectjweaver:1.7.4'

        testCompile 'org.springframework:spring-test:4.0.0.RELEASE'
        testCompile "junit:junit:4.11"

        groovy "org.codehaus.groovy:groovy-all:2.1.6"
        testCompile "org.spockframework:spock-core:1.0-groovy-2.0-SNAPSHOT"
        testCompile "org.spockframework:spock-spring:1.0-groovy-2.0-SNAPSHOT"
    }

    // source, target compiler version 결정
    sourceCompatibility = 1.8
    targetCompatibility = 1.8

    // source code encoding
    [compileJava, compileTestJava]*.options*.encoding = 'UTF-8'
}
domain project
  • domain project는 queryDsl의 Q-Entity들의 생성이 필요합니다.
  • compileJava에 대한 Q-Entity 생성 Task의 Depend가 잡혀있으면 사용하기 편합니다.

build.gradle (domain project)

dependencies {
    def springVersion = "4.0.1.RELEASE"

    compile 'org.hibernate:hibernate-core:4.3.1.Final'
    compile 'org.hibernate:hibernate-entitymanager:4.3.1.Final'
    compile "org.springframework:spring-orm:${springVersion}"

    def queryDSL = '3.2.4'
    compile("com.mysema.querydsl:querydsl-core:$queryDSL")
    compile("com.mysema.querydsl:querydsl-jpa:$queryDSL")
    compile("com.mysema.querydsl:querydsl-sql:$queryDSL")
    provided("com.mysema.querydsl:querydsl-apt:$queryDSL") {
        exclude group: 'com.google.guava'
    }
    compile 'org.springframework.data:spring-data-jpa:1.5.0.RELEASE'
}

sourceSets {
    generated {
        java {
            srcDirs = ['src/main/generated']
        }
    }
}

// QEntity 생성 task
task generateQueryDSL(type: JavaCompile, group: 'build', description: 'Generates the QueryDSL query types') {
    source = sourceSets.main.java
    classpath = configurations.compile + configurations.provided
    options.compilerArgs = [
            "-proc:only",
            "-processor", "com.mysema.query.apt.jpa.JPAAnnotationProcessor"
    ]
    destinationDir = sourceSets.generated.java.srcDirs.iterator().next()
}

// compileJava task에 dependency를 걸어줍니다.
compileJava {
    dependsOn generateQueryDSL
    // compile target에 generated된 QClass들의 위치를 추가.
    source sourceSets.generated.java.srcDirs.iterator().next()
}

compileGeneratedJava {
    dependsOn generateQueryDSL
    options.warnings = false
    classpath += sourceSets.main.runtimeClasspath
}

clean {
    delete sourceSets.generated.java.srcDirs
}

idea {
    module {
        sourceDirs += file('src/main/generated')
    }
}
webapplication project

마지막으로 web application project입니다. 이는 조건이 조금 더 많습니다.

  • tomcat을 실행시켜 local 개발 환경에서 사용할 수 있어야지 됩니다.
  • 외부 테스트 또는 운영환경의 tomcat에 배포가 가능해야지 됩니다.

위 조건을 만족하기 위해서 gradle의 tomcat plugin과 cargo plugin을 이용합니다.

plugin에 대한 자료들은 다음 url에서 보다 많은 자료를 볼 수 있습니다.

tomcat plugin > https://github.com/bmuschko/gradle-tomcat-plugin
cargo plugin > https://github.com/bmuschko/gradle-cargo-plugin

build.gradle (webapp project)

apply plugin: 'war'
apply plugin: 'tomcat'
apply plugin: 'cargo'

// tomcat과 cargo plugin에 대한 repository 설정입니다.
buildscript {
    repositories {
        mavenCentral()
        jcenter()
    }

    dependencies {
        classpath 'org.gradle.api.plugins:gradle-tomcat-plugin:1.0'
        classpath 'org.gradle.api.plugins:gradle-cargo-plugin:1.4'
    }
}


dependencies {
    // tomcat plugin 설정입니다.
    String tomcatVersion = '7.0.47'
    tomcat "org.apache.tomcat.embed:tomcat-embed-core:${tomcatVersion}"
    tomcat "org.apache.tomcat.embed:tomcat-embed-logging-juli:${tomcatVersion}"
    tomcat("org.apache.tomcat.embed:tomcat-embed-jasper:${tomcatVersion}") {
        exclude group: 'org.eclipse.jdt.core.compiler', module: 'ecj'
    }
    providedCompile 'javax.servlet:javax.servlet-api:3.1.0'
    providedCompile 'javax.websocket:javax.websocket-api:1.0'
    providedCompile 'javax.servlet:jsp-api:2.0'
    providedCompile "org.apache.tomcat:tomcat-servlet-api:${tomcatVersion}"

    // cargo에 대한 설정입니다.
    def cargoVersion = '1.4.5'
    cargo "org.codehaus.cargo:cargo-core-uberjar:$cargoVersion",
            "org.codehaus.cargo:cargo-ant:$cargoVersion"


    def springVersion = "4.0.1.RELEASE"
    compile "org.springframework:spring-webmvc:${springVersion}"
    compile 'jstl:jstl:1.2'
    compile 'org.apache.tiles:tiles-jsp:3.0.3'

    compile 'org.slf4j:slf4j-api:1.7.6'
    compile 'org.slf4j:jcl-over-slf4j:1.7.6'

    compile 'ch.qos.logback:logback-classic:1.0.13'
    compile 'ch.qos.logback:logback-core:1.0.13'

    compile 'org.apache.velocity:velocity:1.7'
    compile 'org.freemarker:freemarker:2.3.20'
    compile 'com.ctlok:spring-webmvc-rythm:1.4.4'
    compile project(':bookstoreHibernate')
}

// tomcarRun을 실행시키기 위해서 war에 대한 dependency를 주입합니다.
tomcatRun {
    contextPath = ""
    URIEncoding = 'UTF-8'
    dependsOn war
}

tomcatRunWar {
    dependsOn war
}

// cargo를 이용한 배포를 위해서 war에 대한 dependency를 주입합니다.
cargoRedeployRemote {
    dependsOn war
}

cargoDeployRemote {
    dependsOn war
}

cargo {
    containerId = 'tomcat7x'
    port = 8080

    deployable {
        context = "${project.name}"
    }

    // remoteDeploy 되는 target의 tomcat 정보
    remote {
        hostname = '192.168.13.209'
        username = 'ykyoon'
        password = 'qwer12#$'
    }
}

bower를 이용한 javascript dependency

web application에서의 외부 javascript dependency를 사용하는 방법입니다. bower를 이용하는 경우, 외부에서 javascript에 대한 source code를 모두 다운받고 compile된 javascript를 dist에 저장하게 됩니다.

그런데, 우리의 web application은 dist에 저장된 특정 파일만을 사용하게 됩니다. 그럼 이 dist에 있는 file을 최종적으로 배포할 webapp folder에 넣어줘야지 됩니다. 이를 위해서 개발된 것이 bower-installer 입니다. 그런데 bower-installer의 경우에는 윈도우즈에서 동작이 정상적이지 않습니다. 아니 실행이 되지 않습니다.; 그래서 bower-installer와 동일한 동작을 하는 task를 만들어봤습니다.

먼저, bower-installer는 bower.json의 install property에 설정합니다. jquery와 bootstrap에 대한 dependency를 설정한 bower.json 입니다.

bower.json

{
    "name" : "bookstore-web",
    "version" : "0.0.0.1",
    "dependencies" : {
        "jquery" : "1.11.0",
        "bootstrap" : "3.1.1"
    },
    "install" : {
        "path" : {
            "css" : "src/main/webapp/lib/css",
            "js" : "src/main/webapp/lib/js",
            "eot" : "src/main/webapp/lib/fonts",
            "svg" : "src/main/webapp/lib/fonts",
            "ttf" : "src/main/webapp/lib/fonts",
            "woff" : "src/main/webapp/lib/fonts",
            "map" : "src/main/webapp/lib/js"
        },
        "sources" : {
            "jquery" : [
                    "bower_components/jquery/dist/jquery.min.js",
                    "bower_components/jquery/dist/jquery.min.map"
                ],
            "bootstrap" : [
                    "bower_components/bootstrap/dist/css/bootstrap.min.css",
                    "bower_components/bootstrap/dist/css/bootstrap-theme.min.css",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.svg",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff",
                    "bower_components/bootstrap/dist/js/bootstrap.min.js"
                ]
        }
    }
}

위의 install property에 지정된 js와 css들을 옮기는 task는 다음과 같이 설정할 수 있습니다. war task에 dependency를 주입해서 위의 tomcatRun이나 cargoRedeployRemote 등에서도 사용할 수 있습니다.

import org.apache.tools.ant.taskdefs.condition.Os
import groovy.json.JsonSlurper
task bowerInstall(type:Exec, description : 'copy js files dependencies that is defined in Bower.js') {

    if(Os.isFamily(Os.FAMILY_WINDOWS)) {
        commandLine 'cmd', 'bower', 'install'
    } else {
        commandLine 'bower', 'install'
    }

    def jsonHandler = new JsonSlurper()
    def jsonFile = file("bower.json")
    def conf = jsonHandler.parseText(jsonFile.getText("UTF-8"))
    def pathMap = [:]

    conf.install.path.each {
        pathMap.put(it.key, it.value)
    }

    conf.install.sources.each {
        it.value.each { f ->
            def sourceFile = file(f)
            String sourceName = sourceFile.name
            int dotPos = sourceName.lastIndexOf(".")
            String ext = sourceName.substring(dotPos + 1, sourceName.length())
            if(pathMap.containsKey(ext)) {
                copy {
                    from f
                    into pathMap.get(ext)
                }
            }
        }
    }
}

war {
    dependsOn bowerInstall
}


위 정리된 내용을 github에 공유합니다. 아래 주소에서 git clone 하시면 됩니다. ^^

 

 https://github.com/xyzlast/study-spring-bookstore.git



Posted by Y2K
,

Java VM - YoungGen space의 동작

번역(http://javaeesupportpatterns.blogspot.ie/2013/11/java-vm-beware-of-younggen-space.html) 입니다. 약간의 의역과 오역이 들어가있을수 있습니다. ^^;

정상적인 JVM 상태는 application의 성능 및 안정성에 매우 중요한 목표라고 할 수 있다. 이러한 health 평가는 GC의 major collection이나 memory leak에 촛점이 맞춰져 있다. Young Generation space 또는 shot lived object의 경우에는 크기나 저장 공간이 어떻게 될까?

이 article은 우리의 client의 최신 장애 경험에 의거한 이야기이다. 샘플 application을 통해, 이 현상을 보여줄 것이며, 메모리 저장소와 minor collection이 Old Generation collection 또는 tenured space와 비슷한 정도의 성능저하를 나타낼 수 있음을 보여줄 것이다.

JVM Health diagnostic

만약에 당신이 JVM tunning의 신참이라면, 모든 application에 공통으로 사용할 수 있는 일반적인 솔루션은 존재하지 않는다는 것은 곧 알 수 있을 것이다. Web을 통한 여러 소스의 경로를 통해, 여러분들은 JVM GC pauses가 매우 섬세하며 이것을 이해하기 위해서는 매우 많은 노력을 해야지 된다는 것을 알 수 있다. (어떤 application은 JVM pause time을 1% 이하로 줄이길 요구한다.)

성능과 load testing을 같이 하는 Java profiling (메모리 leak detection을 포함)은 JVM runtime health와 당신의 application의 memory 저장공간에 대한 데이터와 시실을 모두 수집하기 위한 좋은 방법이 된다.

그럼, “Healthy” JVM 이란 무엇을 의미하는가? 이 질문에 대해서 당신의 최고의 지식을 가지고 다음 질문들을 대답해보길 바란다.

만약에 당신이 NO라고 대답한다면, 90%이상의 확신을 가진 대답을 해주면 된다. 또는 I DON’T Know 라고 해도 된다.

  • Application에서 Java heap 또는 OldGen space leaking over time이 발생하는가? (major collection이 발생한 경우)
  • Application에서 현재 크거나 잦은 JVM GC Collection이 발생하고 있는지?
  • JVM overall pause time이 5%보다 넘거나 이상적인 baseline보다 높은지?
  • Application의 response time이 JVM GC 행위에 영향을 받고 있는지?
  • 3개월 내로, java.lang.OutOfMemoryError error를 경험해본적이 있는지?
  • 3개월 내로, JVM의 crush 현상을 격어본적이 있는지?
  • 당신의 JVM 상태가 현제 불안정하거나 인위적인 개입을 받아야지 되는 상태에 있는지? (JVM reboot etc.. )

만약에 당신이 YES 또는 I don’t know라고 대답을 했다면, 당신의 production 성능을 높이기 위해서 tuning team이 현 JVM GC policy를 살펴서 해야지 될 일들이 남아있다는 것이다.

만약에 당신이 모든 질문에 NO라고 대답할 수 있다면, 당신은 매우 견고한 application과 JVM 안정성을 가지고 있는 것이다. 축하한다. 여전히 나는 당신에게 major release와 load forecasts에 대한 상황을 평가하는 것을 제안한다.

Young Generation : Stop-The-World

우리가 JVM Health 평가를 진행할 때, JVM overall pause time은 자주 언급되는 문제이다. JVM overall pause time은 JVM이 stop the world event가 발생할 때, 얼마나 많은 시간이 걸리는지를 측정하는 것이다. Event가 발생중에, application thead는 모두 중지되고, 어떠한 일도 동작하지 않으며, application의 응답시간은 증가되게 된다. 이 수치는 JVM의 불안정성 이나 예측 불가능한 응답시간에 대한 매우 중요한 지표로 사용된다.

최근 몇년을 거쳐 사람들이 많이 알고 있으나, 잘못된 정보는 YoungGen 또는 minor collection은 매우 명료하며, application response time에 영향을 주지 않는다.라는 선입관이다. 이 문장은 대부분의 경우에 참이지만, Java heap size가 작고 (YG space < 1GB), 생명주기가 매우 짧은 경우만을 한정해야지 된다. 위 시나리오는 minor collection 실행이 매우 빠르며 (< 20msec), 자주 일어나지 않으며 (every 30 seconds++), overall JVM pause 시간중 YoungGen space가 매우 작게 남아있는 경우이다(<< 1%). 그러나, YG memory 할당 빈도수는 매우 빠르게 증가되어 이러한 상황은 매우 쉽게 변화될 수 있다(traffic의 증가로 인하여 사용자가 증가하는 경우, YG의 Size 및 할당 빈도수는 매우 쉽게 증가한다).

다음 article에서 제안한 내용을 중심으로 YoungGen space와 concurrent collectors에 대한 정보를 좀 더 얻기를 바란다.

Oracle HotSpot mostly concurrent collectors: CMS vs. G1

http://www.oracle.com/webfolder/technetwork/tutorials/obe/java/G1GettingStarted/index.html

Oracle HotSpot minor collections exhaustive coverage

http://www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html
http://blog.griddynamics.com/2011/06/understanding-gc-pauses-in-jvm-hotspots_02.html

이미 사용하고 있는 HotSpot GC 규정에 상관없이, 모든 collector들 CMS, G1, YoungGen space collection 모두는 “stop the world”를 발생시킬 수 있다. 우리의 지식대로, Azul Zing C4만이 실시간 compacting collectior인 것으로 알려지고 있다. 우리는 현 시점에서 이 collector를 사용할 수 있는 경험을 갖지 못했다. C4 tuning 경험이나 연구 결과가 있다면 많은 공유를 부탁드린다.

지금 우리는 약간의 이론을 소개하도록 한다. sample application을 통해, 성능 검사 결과는 다양한 YoungGen의 저장공간과 할당 빈도를 보여줄 것이다.

Sample application specification

YG 할당빈도수에 따른 JVM pause time %를 측정하기 위해 우리는 다음 sample application을 작성하였다.

// jvm uri를 갖는 REST WebService (JAX-RS)
@GET
@Path("/jvm")
@Produces(MediaType.APPLICATION_JSON)
public Integer jvm() {}

jvm method의 실행은 다음 로직을 따르게 된다.

이미 결정된 크기의 short-lived object를 할당한다. (fast Young GC의 요건에 적합하다.)

추가적으로, 1 GB의 CMS collector의 동작의 약간의 noise를 가할 목적의 long-lived objects의 저장소를 만들어준다(GC의 대상이 아니다.).

private final static int LONG_LIVED_OBJ_FOOTPRINT = (1024 * 1024 * 1024);
private final static int SHORT_LIVED_OBJ_FOOTPRINT = (100 * 1024 * 1024);

// 1 GB static memory footprint
private final static byte byteArrayLongLivedObj[] = new byte[LONG_LIVED_OBJ_FOOTPRINT];

// 100 MB memory allocation (waste) created per execution
public void generateShortLivedObj(String objId) {          
  byte byteArrayShortLivedObj[] = new byte[SHORT_LIVED_OBJ_FOOTPRINT];
}

마지막으로, 테스팅하는 환경 변수는 다음과 같다.

OS: Windows 7 @64-bit
Java EE container: WildFly 8 Beta1
JVM: Oracle HotSpot 1.7 @64-bit with a Java heap size of 5 GB (YG space: 1152 MB XX:NewRatio=3). GC collector: CMS
IDE: JBoss Developer Studio 7.0.0.GA
JVM monitoring: JVisualVM
JVM memory leak analyzer: Plumbr 3.0
JVM verbose:gc log analyzer: GCMV
Performance & load testing: Apache JMeter 2.9

Performance testing results and observations

Simulated된 성능 테스팅은 높은 JVM pause time을 가지고 있었으며, peak load하에서 잦은 성능저하를 보여줬다.

Baseline

  • 10개의 concurrent thread
  • JVM process당 100 MB의 short lived object.

short lived object memory 저장 공간은 매우 극적이지만, 처음에는 잘 운영되었다.

Result

Average response time: 140 ms
Throughput: 68 req / sec
JVM overall pause time: 25.8%
YG collection frequency: 7 collections per second
Rate of GC: 308,909 MB per minute

JvirtualVM에 따르면, 이 JVM은 정상적이다(memory leak이 없으며, 안정적이며, OldGen collect 횟수가 낮다). 그러나, verbose:gc log를 좀 더 깊게 바라보면, JVM pause time이 전체 runtime 시간의 25.8%가 되는 것을 알 수 있고, 모든 지연시간은 YG Collection에 의해서 발생됨을 알 수 있다. 이것은 명확하게 verbose:gc log vs JVM에 대한 검증된 기준과의 차이에 대한 적절한 분석이 필요함을 보이고 있다.

Test & tuning #1.

  • 10 concurrent threads
  • 50 MB of short lived objects / JVM process

(먼저번의 테스트와의 차이점은 short lived object의 크기가 다르다는 점이다.)

위 조건에서 application의 메모리 할당 용량은 100MB에서 50MB 사이에서 동작하고 있었다. 우리는 명확히 개선된 것을 모든 지표에서 볼 수 있었으며 request 당 할당되는 메모리 양의 감소가 처리양에 관련이 있는 것을 볼 수 있다.

Result

Average response time: 119 ms  -21
Throughput: 79 req / sec  +11
JVM overall pause time: 15.59%  -10.21
YG collection frequency: 3-4 collections per second  -3
Rate of GC: 164 950 MB per minute  -143 959

Test & tuning #2

  • 10 concurrent threads
  • 5 MB of short lived objects created per execution per JVM process

(Test 1에 비하여 request 당 생성되는 short lived object의 크기가 5M로 반으로 줄었다.)

Result

Average response time: 107 ms  -33
Throughput: 90 req / sec  +22
JVM overall pause time: 1.9%  -23.9
YG collection frequency: 1 collection every 2-3 seconds * significant reduction
Rate of GC: 15 841 MB per minute  -293 068

보는것과 같이, application footprint와 memory 할당에 대한 최종 개선은 의미있는 매우 적절한 1.9%정도인 JVM pause time을 보여주고 있다.3번의 테스트를 통해서 매우 중요한 점을 알 수 있는데, OldGen footprint와 CMS 동작은 JVM pause time에 전혀 영향을 주지 않고 있으며, 성능 문제는 과도한 동작과 YG Collection에 연결된 매우 큰 크기의 stop the world event와 연결되어 있음을 알 수 있다.

Solution & recommandations

이 문제 케이스에서 우리는 JVM pause time을 줄이기 위해서는 연관된 과도한 YG collection 활동에 대한 tuning과 application request에 의한 memory footprint를 제거시켜주는것이 중요함을 알 수 있다. 따라서, application rate와 YG GC frequency를 줄이는 것이 중요하다.

그러나, 이러한 tuning 전략은 단기간에 가능하지 않는데, 다른 솔루션들을 분석하는 것이 더욱더 가치가 있기 때문이다. 이와 유사한 결과는 어쩌면 다음과 같은 허용량 개선 전략으로도 성취할 수도 있다.

  • Horizontal & Vertical scaling : traffic을 증가된 숫자의 JVM process로 나누면 메모리 할당 빈도와 YG Collection 횟수를 줄일 수 있다. 이는 근본적인 원인을 hardware로 넘기는 방법이다. 나의 추천사항은 언제나 당신의 application memory footprint를 먼저 tunning하고, 다른 scaling option을 찾는 것이다.
  • Java heap size & YG ratio tuning : YG 영역의 크기의 증가는 틀림없이 YG Collections에 의한 frequency를 줄일 수 있다. 그러나 매우 주의해야지 된다. 절대로 OldGen space를 굶주리게 만들어서는 안된다 그렇지 않으면 당신은 매우 간단히 문제를 JVM thrashing과 OOM events쪽으로 옮겨버리게 된다.

Final words

JVM YG collections의 성능 영향에 대해서 보다 나은 이해가 있기를 바란다. 나는 이 article을 읽은 후에 다음과 같은 실습을 해보기를 권합니다.

  • 당신이 맡고 있는 가장 바쁘게 돌아가는 application을 선택합니다.
  • verbose:gc log를 살펴보고, JVM pause time과 GCMV를 결정해보세요.
  • YG collection에 의한 영향을 판단하고, tunning 기회를 알아보시길 바랍니다.

당신의 comment와 JVM tuning 경험 기회를 공유할 수 있기를 바랍니다.


Posted by Y2K
,

(원문 : http://javaeesupportpatterns.blogspot.ie/2013/02/java-8-from-permgen-to-metaspace.html)

Java8에서 가장 큰 변화라고 개인적으로 생각하고 있는 Metaspace에 대한 post를 한번 번역해서 소개합니다.

JDK7까지 사용된 Permanent General (PermGen) space가 Oracle의 주도하에 제거된 것이 큰 특징중 하나입니다. 예를 들어, 내부 문자열의 경우, JDK7에서 이미 제거되어있었으나, JDK8에서 최종적으로 폐기되게 되었습니다.

이 문서는 PermGen의 후계자라고 할 수 있는 Metaspace에 대한 내용입니다. 또한 우리는 HotSpot 1.7 vs HotSpot 1.8 간의 leaking class meta object의 동작에 대해서 알아보도록 하겠습니다.

Metaspace : 새로운 메모리 저장소의 탄생

JDK8 HostSpot VM의 경우, 재사용성을 위한 객체에 대한 metadata를 Metaspace라고 불리우는 native memory에 저장하게 됩니다. 마치 Oracle JRockit 또는 IBM JVM과 같은 동작으로 구성되게 됩니다.

좋은 뉴스는 이제는 더이상 java.lang.OutOfMemoryError : PermGen space와 같은 문제를 더이상 야기하지 않는다는 것과 더이상 tunning과 monitoring을 통해서 이러한 memory space를 조절할 필요가 없다는 것입니다. 그렇지만, 이러한 변화는 기본적으로 안보이게 설정이 되기 때문에 우리는 이 새로운 meta memory에 대한 족적을 여전히 살펴볼 필요성은 존재하게 됩니다. 이러한 변화가 마법과 같이 class의 메모리 공간을 줄여주거나 class loader의 memory leak을 줄여주지는 못하는 것을 인지해야지 됩니다. 우리는 새로운 naming convention하에서 다른 시각으로 이러한 문제를 바라보는 것이 필요합니다.

Summary

PermGen space situation

  • memory space는 완벽하게 제거됩니다.
  • PermSize와 MaxPermSize JVM argument는 무시되며, 시작될때 경고를 표시합니다.

Metaspace memory allocation model

  • class의 metadata는 거의 대부분 native memory에 저장됩니다.
  • class의 metadata를 사용하기 위한 klasses들은 모두 제거됩니다.

Metaspace capacity

  • 기본적으로 metaspace는 native memory를 사용하기 때문에, OS에서 사용 가능한 memory 영역 모두를 사용 가능합니다.
  • 새로운 Flag(MaxMetaSpaceSize)가 가능합니다. 이는 class metadata를 저장하기 위한 최대한의 memory size를 정하는 것이 가능합니다. 만약에 Flag를 설정하지 않는다면, Metaspace는 application의 동작에 따라 가변적으로 동작하게 됩니다.

Metaspace garbage collection

  • dead class와 classloader의 GC는 MaxMetaspaceSize에 도달하면 발생합니다.
  • Metaspace의 tuning과 monitoring은 GC의 횟수와 지역을 제한하기 위해 필요한 작업입니다. 과도한 Metaspace GC는 classloader의 memory leak 또는 적합하지 않은 Application의 memory size를 초래합니다.

Java heap space impact

  • 여러 다양한 데이터들은 Java Heap space로 저장 영역이 이동되었습니다. 결국 기존 JDK7에서 JDK8로 업그레이드를 하는 경우, Java Heap memory를 증가시킬 필요가 있습니다.

Metaspace monitoring

  • metaspace 사용량은 GC log output을 통해서 확인 가능합니다.
  • Jstat * JVirtualVM 은 아직 metaspace를 monitoring 하지 못합니다. 아직 예전 PermGen space reference를 확인하도록 되어 있습니다.

PremGen vs. Metaspace runtime comparison

새로운 Metaspace memory space의 동작을 이해하기 위해서, 우리는 metadata leaking java program을 준비했습니다.

주어진 시나리오에 따라 다음과 같이 테스트를 진행했습니다.

  • JDK 1.7에서 PermGen memory space를 128M로 설정 후, PermGen memory의 감소를 관찰합니다
  • JDK 1.8에서 Metaspace memory space의 GC와 dynamic increase를 monitoring 합니다.
  • JDK 1.8에서 MaxMetaspaceSize를 128M로 설정 후, Memory의 감소를 관찰합니다.

JDK 17 - PermGen depletion

  • Java program with 50K configured iterations
  • Java heap space는 1024 MB
  • Java PermGen space 는 128MB

JVirtualVM의 경우, PermGen depletion은 약 30K+개의 class가 class loader에 로딩되었을 때, 발생합니다. GC output을 통해 다음 log를 확인할 수 있습니다.

Class metadata leak simulator
Author: Pierre-Hugues Charbonneau
http://javaeesupportpatterns.blogspot.com
ERROR: java.lang.OutOfMemoryError: PermGen space

JDK 1.8 - Metaspace dynamic re-size

  • Java program with 50K configured iterations
  • Java heap space는 1024 MB
  • Java metaspace space : 지정하지 않음 (default)


GC output에서 볼 수 있듯이, JVM Metaspace는 20MB에서 320MB로 dynamic resizing이 이루어진 것을 볼 수 있습니다. 또한 JVM의 GC 시도를 GC output을 통해서 확인 할 수 있습니다. 우리는 memory leak이 발생하는 application을 구성하고 있기 때문에, JVM은 dynamic expand 밖에 할 수 없습니다.

이 프로그램은 50K 정도의 OOM event와 50K+ 정도의 classes들을 load 시킬 수 있습니다.

JDK 1.8 - Metaspace depletion

  • Java program with 50K configured iterations
  • Java heap space는 1024 MB
  • Java metaspace space : 128M (-XX : MaxMetasapceSize=128M)

JVirtualVM에서 보듯이, metaspace 감소는 약 30K+ 개의 class들이 감소한 후 부터 발생하고 있습니다. 마치 JDK 1.7과 동작이 비슷합니다. 이러한 현상을 GC output을 통해서 확인할 수 있습니다. 또다른 흥미로운 현상은 native memory footprint를 보면 알 수 있듯이, native memory는 MaxMetaspaceSize의 2배를 할당하고 있다는 점입니다. 이것은 native memory 낭비를 줄일, Metaspace의 재할당 정책에 대한 좋은 tunning point입니다.

이제 java program output을 통해 Exception을 확인해보도록 하겠습니다.

Class metadata leak simulator
Author: Pierre-Hugues Charbonneau
http://javaeesupportpatterns.blogspot.com
ERROR: java.lang.OutOfMemoryError: Metadata space
Done!

예상과 같이, Metaspace가 128MB가 되었을 때, 마치 JDK 1.7과 거의 유사한 base line을 갖는 50 K 정도의 객체를 생성할 때 Exception이 발생합니다. 새로운 OOM(Out Of Memory) error는 JVM에 의해서 발생되게 됩니다. OOM event는 memory allocation failuire에 의해 발생되게 됩니다.

metaspace.cpp 

Final words

새로운 Java 8 Metaspace에 대해서, 빠른 분석과 설명을 알아보길 바랍니다. 현 분석대로라면 마지막 실험에서 보셨던 것과 같이, Metaspace GC 또는 OOM 조건이 발생하는 것에 대한 monitoring과 tuning이 필요한 것을 보여줍니다. 다음 article에서는 새로운 기능에 연관된 performance 향샹에 대한 비교를 포함할 수 있었으면 좋겠습니다.

Posted by Y2K
,

java8이 나오고 빠르게 한번 환경을 변경시켜보고 난 후, 문제점 상황을 정리해봤습니다.

jacoco 버젼 문제

jacoco를 최신 버젼으로 해줄 필요가 있습니다. jacoco는 최신 버젼이 0.7.0.201403182114 입니다. 버젼뒤의 날짜를 보시면 아시겠지만, java8 발표 일자와 완전히 동일합니다. 기존 jacoco에서는 byte code exception이 발생하기 때문에 반드시 업그레이드 할 필요가 있습니다.

tomcat 8

gradle tomcat plugin이 아직 tomcat8을 지원하지 못합니다. gradle을 이용한 build의 경우에는 java 8을 사용하시는 것을 고민해주시는 것이 좋습니다. 물론 java 8에서 tomcat7을 사용하는 것은 아무런 문제가 발생되지 않습니다.

SonarQube

sonarQube에서 아직 java 8을 지원하지 못하고 있습니다. 3월말에 최신 업데이트가 될 것이라는 이야기가 아래 링크에 있는데…… java8은 작년 여름에 release 될 예정이였지 않나요. 이걸 믿어야지 되는지 고민하고 있습니다.

http://sonarqube.15.x6.nabble.com/Sonar-Support-for-JDK-8-td5019488.html

무엇보다 위 링크에 이야기되고 있는 JIRA Issue는 이미 resolved된 상태입니다. 일단 stackoverflower에 질문을 올려두긴 했는데.. 메일링 리스트에서 물어보세요. 라는 답변을 받아서 상처받고 메일링 리스트에 가입해서 다시 물어봤습니다. ㅠ-ㅠ 답변을 기다려야지요.

FindBugs

sonarQube에서 지금 지원을 못하는 가장 결정적 이유가 바로 FindBug가 Java8을 지원하지 못하는 이슈가 있기 때문입니다. StringSequence class를 비롯해서 몇몇 Class에 대한 오류를 발생시키고 있습니다. Java8에서 가장 큰 이슈중 하나네요.

Java8

개인적으로는 이번 java8을 매우 기다리고 있습니다. PermGemSize에 대한 GC의 개선과 더불어 lamda expression으로 대표되는 java 언어의 확장성. 그리고 지겹고 지겨운 Date 객체 추가.

개인적으로는 이 3가지때문에 java8을 기다리고 있는데. 다른 분들은 어떠실지 모르겠네요.

Posted by Y2K
,

매번 코드를 작성할 때마다 까먹어서 정리합니다. 기억력이 감퇴가 된것 같네요.

  1. MessageSource를 선언. refresh를 위해 ReloadableResourceBundleMessageSource를 사용하는 것이 정신건강상 좋다.
  2. LocaleChangeInterceptor를 선언
  3. addInterceptors method를 override 시켜, interceptor를 추가
  4. AcceptHeaderLocaleResolver, CookieLocaleResolver, SessionLocaleResolver 중 하나를 선택하여 LocaleResolver를 구현

AcceptHandlerLocaleResolver

기본적인 Http Header인 Accept-Language를 이용하는 방법입니다. 기본값이 AcceptHeaderLocaleResolver로 되어 있습니다. 가장 구현이 간단하나, parameter를 통해서 동적으로 Locale을 변경하지 못합니다. UnsupportedOperationException이 발생합니다. 이를 테스트 하기 위해서는 Browser의 설정중에 언어 셋을 변경시켜야지 됩니다.

CokieLocaleResolver

Cookie를 이용하는 방식으로, Cookie name, expired time 등을 설정 가능합니다.

SessionLocaleResolver

Session값을 이용하는 방법으로 서버의 부하측면이 적다면 가장 좋은 방법인것 같습니다.

@Configuration code는 다음과 같습니다.

@Configuration
@EnableWebMvc
@ComponentScan(basePackages = {
        "me.xyzlast.web.controllers"
})
public class ControllerConfiguration extends WebMvcConfigurerAdapter {
    @Override
    public void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/lib/**").addResourceLocations("/lib/**");
        super.addResourceHandlers(registry);
    }

    @Bean
    public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() throws IOException {
        return new PropertySourcesPlaceholderConfigurer();
    }

    @Bean
    public LocaleChangeInterceptor localeChangeInterceptor() {
        LocaleChangeInterceptor localeChangeInterceptor = new LocaleChangeInterceptor();
        localeChangeInterceptor.setParamName("lang");
        return localeChangeInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(localeChangeInterceptor());
        super.addInterceptors(registry);
    }

    @Bean
    public AcceptHeaderLocaleResolver acceptHeaderLocaleResolver() {
        return new AcceptHeaderLocaleResolver();
    }

    @Bean
    public ReloadableResourceBundleMessageSource messageSource() {
        ReloadableResourceBundleMessageSource messageSource = new ReloadableResourceBundleMessageSource();
        messageSource.setDefaultEncoding("UTF-8");
        messageSource.setBasename("classpath:messages");
        messageSource.setFallbackToSystemLocale(false);
        return messageSource;
    }
}


Posted by Y2K
,

Bower는 maven, gradle을 이용한 java에서의 open source library의 버젼관리를 javascript client library에 옮겨온 개념입니다.

bower 소개

Bower는 maven, gradle과 비슷하게 bower.json과 .bowerrc 두개의 파일로 설정이 관리가 됩니다.

  • .bowerrc : Bower에서 source와 dist를 다운받을 위치를 지정합니다.
  • bower.json : Bower에서 다운받을 외부 component의 버젼 및 Project의 name, version을 지정합니다. 이는 maven, gradle에서 project와 비슷한 값을 지정하게 됩니다.

.bowerrc

{
  "directory": "components"
}

bower.json

{
  "name": "my-project",
  "version": "0.0.1",
  "dependencies": {
    "modernizr": "~2.6.2",
    "bootstrap": "~2.2.2",
    "angular-strap": "~0.7.0",
    "angular-ui" : "0.4.0",
    "ngInfiniteScroll" : "1.0.0",
    "angular-underscore" : "",
    "underscore" : "",
    "angular-bootstrap" : "~0.2.0",
    "font-awesome" : "3.0.2",
    "emoji" : "0.1.2"
  }
}

bower의 설치

bower는 node.js를 이용해서 작성되어있습니다. 먼저 node.js를 설치하는 것이 필요합니다. node.js의 설치방법은 너무나 많은 곳들에서 소개가 되고 있기 때문에 넘어가기로 하겠습니다.
Bower는 전역으로 사용되는 application이기 때문에, npm install -g를 이용해서 설치를 해야지 됩니다. 최종적으로 사용하기 위해서는 bower-installer 역시 같이 설치해주는 것이 좋습니다.

sudo npm install -g bower
sudo npm install -g bower-installer

bower의 활용

기본적으로 mvnrepository와 같이 사용할 library를 http://bower.io/search/ 에서 검색해서 찾는 것이 가능합니다. bower를 이용해서 library를 설치하기 위해서는 다음 command를 실행하면 됩니다.

bower install jquery

위는 jquery를 지정해서 설치하게 됩니다. 버젼이 따로 적혀있지 않는 경우, 가장 최신의 버젼을 다운받아 사용하게 됩니다. 특정하게 버젼을 지정하기 위해서는 다음과 같이 작성할 수 있습니다.

bower install jquery#1.11.0

설치와 bower.json 파일 기록을 동시에 할 수 있습니다.

bower install jquery#1.11.0 --save
bower install jquery#1.11.0 -s

bower-installer

bower를 통해서 component들을 설치하게 되면 source 코드와 min file 모두가 다운받게 됩니다. 그렇지만, 사용하게 될 component 들은 대다수 min file들만이 사용되게 됩니다. 이를 위해 설치한 component들의 min file 들만을 사용할 위치로 copy 해주는 tool이 bower-installer 입니다. bower-install는 bower.json에서 설정하며, 다음과 같이 설정할 수 있습니다.

    "install" : {
        "path" : {
            //"파일 확장자" : "copy될 위치"
            "css" : "src/main/webapp/lib/css",
            "js" : "src/main/webapp/lib/js",
            "woff" : "src/main/webapp/lib/fonts"
        },
        "sources" : {
            "component 이름" : [
                "copy 할 file 이름"
            ]
        }
    }

bower.json을 수정한 후, bower-installer를 실행하면 위치에 맞게 file이 모두 copy되는 것을 볼 수 있습니다.

bower와 gradle java war project의 연결

gradle을 이용한 war project의 경우, src/main/webapp에 사용되는 모든 code들과 javascript가 위치하게 됩니다. 여기 위치에 bower_component를 모두 다 다운받아서 처리하게 되면 url이 지저분해지는 경향이 있습니다. 이를 개발 방향에 맞게 정리해주는 것이 필요합니다. 이 때, bower-installer를 사용하면 원하는 directory 구조를 만들 수 있습니다.

제가 개인적으로 생각하는 좋은 구조는 다음과 같습니다.

project
- src
    - main
        - java
        - resources
        - webapp
    - test
        - java
        - resources
 .bowerrc
 .bower.json
 build.gradle
 setting.gradle

.bowerrc

{
    "directory" : "bower_components"
}

bower.json (bootstrap과 jquery를 설치한 상태입니다.)

{
    "name" : "bookstore-web",
    "version" : "0.0.0.1",
    "dependencies" : {
        "jquery" : "1.11.0",
        "bootstrap" : "3.1.1"
    },
    "install" : {
        "path" : {
            "css" : "src/main/webapp/lib/css",
            "js" : "src/main/webapp/lib/js",
            "eot" : "src/main/webapp/lib/fonts",
            "svg" : "src/main/webapp/lib/fonts",
            "ttf" : "src/main/webapp/lib/fonts",
            "woff" : "src/main/webapp/lib/fonts"
        },
        "sources" : {
            "jquery" : [
                    "bower_components/jquery/dist/jquery.min.js"
                ],
            "bootstrap" : [
                    "bower_components/bootstrap/dist/css/bootstrap.min.css",
                    "bower_components/bootstrap/dist/css/bootstrap-theme.min.css",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.eot",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.svg",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.ttf",
                    "bower_components/bootstrap/dist/fonts/glyphicons-halflings-regular.woff",
                    "bower_components/bootstrap/dist/js/bootstrap.min.js"
                ]
        }
    }
}

위와 같이 설정해주면 bower-installer를 실행하면 src/main/webapp/lib 안에 모든 javascript component를 위치할 수 있게 됩니다. 그런데, 이 과정 역시 gradle에 추가하는 것이 보다 더 사용하기 편합니다. 다음과 같이 gradle task를 추가합니다.

import org.apache.tools.ant.taskdefs.condition.Os
task bowerInstaller(type:Exec) {
    if(Os.isFamily(Os.FAMILY_WINDOWS)) {
        commandLine 'cmd', 'bower-installer'
    } else {
        commandLine 'bower-installer'
    }
}

이제 gradle bowerInstaller commmand를 통해 bower-installer를 실행할 수 있습니다. bowerInstaller를 따로 실행시켜줘도 좋지만, war로 배포될 때 자동으로 포함될 수 있도록 war task에 dependency를 추가하도록 합니다.

war {
    dependsOn bowerInstaller
}

이제 gradle war를 통해서도 모든 component들을 같이 관리할 수 있습니다. gradle의 놀라운 확장성에 대해서 다시 한번 감탄하게 됩니다. ^^

마치면서

bower는 java에서의 maven, gradle과 같이 dependencies에 대한 관리 툴입니다. 따라서 기존 관행처럼 모든 javascript를 SCM에 올릴 필요가 더이상 없어집니다. 버젼 관리와 같이 개발되는 application에서 사용되고 있는 component에 대한 관리와 같이 maven, gradle을 사용할 때와 같은 매우 멋진 작업들이 가능하게 됩니다. 모두 bower를 한번 써봅시다. ^^

Posted by Y2K
,