Cluster 된 개별 WAS들이 모두 정상적으로 가동되고 있는지 모니터링 화면에서 표현해보자.


기준은 각 WAS의 Memory MBean 정보를 활용해서 WAS가 정상적인지도 체크 하고, 현재 Memory 상황은 어떤지 파악도 함께 할 수 있게 해본다.


[각각의 WAS Agent] -> vertx Net-> [모니터링 서버] -> vertx SockJs -> [Client Broswer] 방향으로 Memory 정보를 보낸다.



장애판단 기준은 WAS Agent에서 현재 5초 기준으로 모니터링 서버쪽으로 보내주고 있다.

그에 따라 Client Borswer 부분에서는 해당 정보가 (5초*1.5) 이후에도 정보가 오지 않으면 서버가 내려갔다고 판단한다.


또한 이 정보를 DB화 하여, Memory 증가량을 파악하고, GC 이후에도 Memory 가 부족하지는 않은지 판단해본다.






[MonitorAgent.java]

import java.lang.instrument.Instrumentation;
import java.util.Timer;

import pe.kr.ddakker.monitor.agent.timer.MBeanTimer;

public class MonitorAgent {
	Timer timer = new Timer("ddakker Agent Timer", true);
    Instrumentation instrumentation;
    static MonitorAgent monitorAgent;
    public static boolean isDebug = false;
	
	public static void premain(String args, Instrumentation instrumentation) throws Exception {
		
		//instrumentation.addTransformer(new TomcatTransformer());
		monitorAgent = new MonitorAgent(instrumentation);
		monitorAgent.start();
	}

	public final void start(boolean isRequest, boolean isMBean) {
		MBeanTimer mBeanTimer = new MBeanTimer();
        timer.scheduleAtFixedRate(mBeanTimer, 5000, 5000);
	}
}


[MBeanTimer.java]

import java.lang.management.ManagementFactory;
import java.util.TimerTask;

import javax.management.MBeanServer;
import javax.management.ObjectName;
import javax.management.openmbean.CompositeData;

import pe.kr.ddakker.monitor.agent.send.VertxClient;

public class MBeanTimer extends TimerTask {
	public void run() {
		try {
			ObjectName om = new ObjectName("java.lang:type=Memory");
			
			MBeanServer connection = ManagementFactory.getPlatformMBeanServer();
			
			
			Object attrValue = connection.getAttribute(om, "HeapMemoryUsage");
			
			long max = Long.parseLong(((CompositeData)attrValue).get("max").toString());
			long used = Long.parseLong(((CompositeData)attrValue).get("used").toString());
			long heapUsedPercent = Math.round((used*1.0 / max*1.0) * 100.0);
			
			String msg = "{name: '" + System.getProperty("jvmRoute") + "', heapUsedPercent: '" + heapUsedPercent + "', time: '" + System.currentTimeMillis() + "'}";
			msg = "{result: '0000', grp: 'grp_was', msg: '성공', data: " + msg + "}";
			
			VertxClient.send(msg);
		} catch (Exception e) {
			System.err.println();
		}

	}

}


slf4j + logback 환경에서 이니지스 결재 모듈(INIpay50.jar) 사용 시 이니시스 로깅에 문제가 발생한다.


이니시스측에서도 log4j 를 사용하라고 권고한 상황..


하지만 쓰고 싶다면...


'org.slf4j:log4j-over-slf4j' 부분을 exclude 하면 된다.



※ 'org.slf4j:log4j-over-slf4j' 는 기존 라이브러리가 log4j를 사용하고 있을때, slf4j로 넘겨주는 역할은 하는거란다.



아시는분이 Java Agent에 대한 정보를 주셔서 해당 기능을 Tomcat 요청에 대한 모니터링툴을 만들어보고 있다.


현재 겉모습은 제니퍼의 X-View 와 비슷하게 따라 하려는중이다.ㅎㅎ




1차적으로 요청 URI, 요청 시간, 종료 시간, 처리 시간, HttpStatus 값 정도만 차트에 표현하였다.


구조는 Tomat에 Agent를 띄운 후 Agent에서 모니터링툴 WAS에 WebSocket 로 보낸 후 데이터를 받은 모니터링툴에서 모니터링에 접속한 Client Browser 에 WebSocket로 데이터를 보내주는 방식이다.


모니터링툴에 붙은 Client가 많았을 경우 어떻게 될지 아직 잘 모르겠다.

상황봐서 Queue에 담아서 한다던가 고려해볼 생각이고 현재에는 비슷하게 구현만 하는게 목적이다.




인터넷의 자료를 통해서 javassist 를 활용하여 Byte Code Instrumentation 시도해보았지만, 일반적인 Java Main Program은 잘 되는데 Tomcat 의 특정 Class를 제어 하려고 했더니 잘 되지 않았다.

많은 삽질 끝에 ClassPool을 얻어 오는 부분을 조금 수정 하니 잘 되었다.

(이 부분때문에 몇일을 삽질 하다.. 포기도 할까 하고, Application Level Servlet Filter를 이용하는 방법으로 선회 할까도 했음...)


검색을 하두 해서 어디서 이 정보를 얻었는지 기억이 안나네요.



[해결 부분]

ClassLoader classLoader = Thread.currentThread().getContextClassLoader();

ClassPool pool = ClassPool.getDefault();

pool.insertClassPath(new LoaderClassPath(classLoader));


[2015. 7. 6.]

WAS Application 요청시 org.apache.catalina.core.StandardEngineValve.invoke(Request request, Response response) 부분을 잡아 처리 하였다.

(만들어 가며 바뀔 수도 있는 부분임..)

이렇게 함으로 인해서 어떠한 문제가 발생할지는 미지수임...

HttpStatus=302 케이스에서 문제 발생


[2015. 8. 10.]

javax.servlet.http.HttpServlet.service(javax.servlet.http.HttpServletRequest,javax.servlet.http.HttpServletResponse  으로 변경

이러면.. JSP Direct 호출이 안 잡히는군.. 쉽지 않구만...


다시 org.apache.catalina.core.StandardEngineValve.invoke(Request request, Response response) 원복!!

Spring Security LOGOUT HttpStatus 302일때문 문제가 발생!!

문제 발생 부분 request.getSession().getId()

이 부분을 Before 일때만 하고, After 일때는 주석


Before, After 요청 정보 맵핑을 Thread.currentThread().getId() 로 처리


우선 잘 된다...


[MonitorAgent.java]

package pe.kr.ddakker.monitor.agent;

import java.lang.instrument.Instrumentation;

import pe.kr.ddakker.monitor.agent.transformer.TomcatTransformer;

public class MonitorAgent {
	
	public static void premain(String args, Instrumentation inst) throws Exception {
		inst.addTransformer(new TomcatTransformer());
	}

	public static void agentmain(String args, Instrumentation inst) throws Exception {
		premain(args, inst);
	}
}


[TomcatTransformer.java]

package pe.kr.ddakker.monitor.agent.transformer;

import java.lang.instrument.ClassFileTransformer;
import java.lang.instrument.IllegalClassFormatException;
import java.security.ProtectionDomain;

import javassist.CannotCompileException;
import javassist.ClassPool;
import javassist.CtBehavior;
import javassist.CtClass;
import javassist.LoaderClassPath;
import javassist.NotFoundException;

/**
 * Tomcat 요청 가로채서 수정!!
 * @author ddakker 2015. 6. 14.
 */
public class TomcatTransformer implements ClassFileTransformer {
	ClassPool pool = null;
	public TomcatTransformer() {
		this.pool = ClassPool.getDefault();
	}
	
	public byte[] transform(ClassLoader loader, String className, Class redefiningClass, ProtectionDomain domain,
			byte[] bytes) throws IllegalClassFormatException {
		if (className.contains("StandardEngineValve")) {
			System.out.println("className: " + className);
			return transformClass(redefiningClass, bytes);
		} else {
			return bytes;
		}
	}

	private byte[] transformClass(Class classToTransform, byte[] b) {
		CtClass cl = null;
		try {
			ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
			
			ClassPool pool = ClassPool.getDefault();
			pool.insertClassPath(new LoaderClassPath(classLoader));
			if (pool != null) {
				cl = pool.makeClass(new java.io.ByteArrayInputStream(b));
				if (cl.isInterface() == false) {
					CtBehavior[] methods = cl.getDeclaredBehaviors();
					for (int i = 0; i < methods.length; i++) {
						System.out.println("methods[" + i + "]: " + methods[i]);
						if (methods[i].isEmpty() == false) {
							doTransform(methods[i]);
						}
					}
				}
				b = cl.toBytecode();
			}
		} catch (Exception e) {
			System.err.println("e111: " + e);
		} finally {
			if (cl != null) {
				cl.detach();
			}
		}
		return b;
	}
	
	private void doTransform(CtBehavior method) throws NotFoundException, CannotCompileException {
		if (method.getName().equals("invoke")) {
			
			System.out.println("0");
			try {
				System.out.println("1");
				method.insertBefore(""
						+ "String jvmRoute = System.getProperty(\"jvmRoute\");"
						+ "String sessionId = $1.getSession().getId();"
						+ "String uri = $1.getRequestURI();"
						+ "pe.kr.ddakker.monitor.websocket.WSClient.send(\"{"
						+ "server: '\" + jvmRoute + \"' "
						+ ", sessionId: '\" + sessionId + \"' "
						+ ", uri: '\" + uri + \"' "
						+ ", stTime: '\" + System.currentTimeMillis() + \"' "
						+ "}\");");
				System.out.println("2");
				method.insertAfter(""
						+ "String jvmRoute = System.getProperty(\"jvmRoute\");"
						+ "String sessionId = $1.getSession().getId();"
						+ "String uri = $1.getRequestURI();"
						+ "pe.kr.ddakker.monitor.websocket.WSClient.send(\"{"
						+ "server: '\" + jvmRoute + \"' "
						+ ", sessionId: '\" + sessionId + \"' "
						+ ", uri: '\" + uri + \"' "
						+ ", edTime: '\" + System.currentTimeMillis() + \"' "
						+ ", status: '\" + $2.getStatus() + \"' "
						+ "}\");");
				System.out.println("3");
			} catch (Exception e) {
				System.err.println("Aa e: " + e);
			}
			System.out.println("4");
		}
		
	}

}



[build.gradle]

jar {
	manifest {
		attributes 'Implementation-Title': 'Gradle Quickstart',
				   'Implementation-Version': version,
				   'Premain-Class': 'pe.kr.ddakker.monitor.agent.MonitorAgent',
				   'Agent-Class': 'pe.kr.ddakker.monitor.agent.MonitorAgent',
				   'Can-Redefine-Classes': true
	}
}


instrument 시 필요한 외부 Library들은 Agent jar에 포함 시켰다.


[실행]

-javaagent:D:\..\tomcat-monitor-agent-1.0.jar


N대의 WAS에서 성능을 위한 Local cache와 Memory효율을 위한 Server Cache 를 사용중이다.

Spring에서 동시에 사용해보자.

Local Cache 는 Clustring 하지않은 Ehcache,
Server Cache 는 Infinispan Server HotRoad 방식을 사용한다.




[context-cache.xml]


<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:util="http://www.springframework.org/schema/util"
	xmlns:cache="http://www.springframework.org/schema/cache"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    					http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd
    					http://www.springframework.org/schema/util http://www.springframework.org/schema/util/spring-util.xsd
    					http://www.springframework.org/schema/cache http://www.springframework.org/schema/cache/spring-cache.xsd">

	<cache:annotation-driven />
	
	<!-- EHCache Local 형 -->
	<bean id="ehCacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager">
        <property name="cacheManager">
            <bean class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean">
                <property name="configLocation" value="classpath:config/cache/ehcache.xml"></property>
            </bean>
        </property>
    </bean>
    
    <!-- Infinispan Server 형 -->
    <util:properties id="hotrod_data" location="classpath:properties/hotrod_data.properties" />
	<bean id="ispnCacheManager" class="org.infinispan.spring.provider.SpringRemoteCacheManagerFactoryBean">
		<property name="configurationProperties" ref="hotrod_data" />
	</bean>

	<bean id="cacheManager" class="org.springframework.cache.support.CompositeCacheManager">
		<property name="cacheManagers">
			<list>
				<ref bean="ehCacheManager" />
				<ref bean="ispnCacheManager" />
			</list>
		</property>
	</bean>
</beans>

[ehcache.xml]

<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    xsi:noNamespaceSchemaLocation="http://ehcache.org/ehcache.xsd" updateCheck="false">
    <diskStore path="java.io.tmpdir" />

    <defaultCache
        maxElementsInMemory="50000"
        eternal="false"
        timeToIdleSeconds="300"
        timeToLiveSeconds="600"
        overflowToDisk="false"
        diskPersistent="false"
        diskExpiryThreadIntervalSeconds="120"
        memoryStoreEvictionPolicy="LRU">
    </defaultCache>

    <cache name="EHCACHE_MENU"
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="0"
        timeToLiveSeconds="600"
        overflowToDisk="false"
        diskPersistent="false"
        diskExpiryThreadIntervalSeconds="120"
        memoryStoreEvictionPolicy="LRU">
    </cache>
</ehcache>

[Service.java]

@Cacheable("ISPN_EVENT")
public Map getEvent(String eventKey) {
	return query...
}
@Cacheable("EHCACHE_MENU")
public Map getMenu(String menuKey) {
	return query...
}

최근 fastjson 이란 알리바바에서 공개한 JSON Parser 을 보게되어 현재 회사에서 사용중인 Library 들을 비교해 보았다.


jackson-databind-2.1.3.jar

json-lib-2.2.1-jdk15.jar

fastjson-1.2.5.jar


[테스트 상황]

Vo 안에 2개의 Vo가 있는 객체 활용하여, to JSON String, to JAVA Object 로 변환 및 역변환을 10,000 실행하였다.




결과는 위에 보여지는 바와 같습니다.
  - to JSON String = fastjson > jackson > json-lib

  - to Object = jackson > fastjson > json-lib

변환 및 역변환 수치 상으로는 알리바바의 fastjson와 jackson 두가지가 근소한 차이의 성능을 보였습니다.
하지만 알리바바의 fastjson의 경우 유효하지 않는 데이터의 경우 표현자체를 하지 않으므로 인해서 JSON String 길이가 작습니다.
따라서 네트워크 전송 비용까지 감안 한다면 알리바바의 fastjson 의 가장 우수 한것으로 판단됩니다.

※ JSON Str Size가 다른 것은 각 Libaray 마다 null 데이터 처리 규칙이 조금 달라서 그렇고, 유효한 데이터에서는 동일합니다.
    - jackjon = null -> 문자열 null 로 표시
    - json-lib = null -> 문자열 공백으로 표시
    - fastjson = null -> key/value 모두 표시하지 않음


서버 모니터링 시스템 구축 중 Tomcat Comet으로 했던걸 WebSocket로 변경 하였다.

단순함!


WebSocket Servlet 에서는 Client Open/Close 만 관리하는 public static Set 객체에 Client 정보를 관리하고, Queue Receive 에서 해당 Set 객체에 접근 하여 각각의 Client 에 데이터를 Push 한다.


Thread 간 통신에 public static 변수를 사용하는게 좋은건지 모르겠으나.. Thread 관련 지식이 부족한 관계로 현재는 우선 패스한다.



import java.util.Set;
import java.util.concurrent.CopyOnWriteArraySet;

import javax.websocket.OnClose;
import javax.websocket.OnError;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.ServerEndpoint;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


@ServerEndpoint(value = "/ws/was")
public class WasWebSocket {

	private static Logger log = LoggerFactory.getLogger(WasWebSocket.class);
	

    public static Set connections = new CopyOnWriteArraySet<>();

    private Session session;
    
    public WasWebSocket() {
        log.info("----------- thread start");
	}

    public Session getSession() {
    	return this.session;
    }

    @OnOpen
    public void onOpen(Session session) {
    	log.info("onOpen this: {}, this.sessson: {}", this.getClass(), session);
        this.session = session;
        synchronized (connections){
        	connections.add(this);
        }
    }


    @OnClose
    public void onClose() {
    	log.info("onClose");
    	synchronized (connections){
    		connections.remove(this);
    	}
    }


    @OnMessage
    public void onMessage(String message) {
    	log.info("@OnMessage");
    	log.info("websocket message: " + message);
    }



    @OnError
    public void onError(Throwable t) throws Throwable {
        log.error("e: ", t.toString());
        synchronized (connections){
    		connections.remove(this);
    	}
    }
}



function wasStatusListen() {
	var oSocket = new WebSocket(WS_URL + "/ws/was");
	 
    oSocket.onmessage = function (e) { 
        var json = eval("(" + e.data + ")");
		if (json.result && json.result == "0000") {
			var wasNode = json.data.name;
			var heapUsedPercent = json.data.heapUsedPercent;
			..
			..
			처리
			
		}
    };
 
    oSocket.onopen = function (e) {
        console.log("open");
    };
 
    oSocket.onclose = function (e) {
        console.log("close");
        wasStatusListen();
    };
    
    window.unload = function() {
		if (oSocket.readyState != 3)
			oSocket.disconnect();
	}
}
wasStatusListen();


Authentication Basic 로 인증되는 서버에서 제공하는 API를 이용 하는 중 Spring RestTemplate 활용 방법


[환경]

Spring-4.1.6

HttpClient-4.3.5


1. 호출 URL 에서 인증 하는 방법

Map<String, Object> resultMap = ezRestTemplate.get("http://admin:1234@localhost:12345/api/overview", HashMap.class);



2. 설정 부분에서 인증 하는 방법

[설정]

import java.security.KeyManagementException;
import java.security.KeyStoreException;
import java.security.NoSuchAlgorithmException;

import javax.net.ssl.SSLContext;

import org.apache.http.auth.AuthScope;
import org.apache.http.auth.UsernamePasswordCredentials;
import org.apache.http.client.HttpClient;
import org.apache.http.conn.ssl.AllowAllHostnameVerifier;
import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
import org.apache.http.conn.ssl.SSLContexts;
import org.apache.http.conn.ssl.TrustSelfSignedStrategy;
import org.apache.http.impl.client.BasicCredentialsProvider;
import org.apache.http.impl.client.HttpClientBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.ImportResource;
import org.springframework.http.client.ClientHttpRequestFactory;
import org.springframework.http.client.HttpComponentsClientHttpRequestFactory;
import org.springframework.web.client.RestTemplate;

import pe.kr.ddakker.framework.support.wrapper.http.EzRestTemplate;

@ImportResource("classpath:config/spring/context-*.xml")
@Configuration
public class ApplicationContext {
	
	@Bean(name = "ezRestTemplate")
	public EzRestTemplate getEzRestTemplate() throws KeyManagementException, NoSuchAlgorithmException, KeyStoreException {
		SSLContext sslContext = SSLContexts.custom().loadTrustMaterial(null, new TrustSelfSignedStrategy()).useTLS().build();
		SSLConnectionSocketFactory connectionFactory = new SSLConnectionSocketFactory(sslContext, new AllowAllHostnameVerifier());
		BasicCredentialsProvider credentialsProvider = new BasicCredentialsProvider();


		credentialsProvider.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials("admin", "1234"));
		 
		HttpClient httpClient = HttpClientBuilder.create()
		                                        .setSSLSocketFactory(connectionFactory)
		                                        .setDefaultCredentialsProvider(credentialsProvider)
		                                        .build();
		 
		ClientHttpRequestFactory requestFactory = new HttpComponentsClientHttpRequestFactory(httpClient);
				
		return new EzRestTemplate(new RestTemplate(requestFactory));
	}
}


[사용]

// EzRestTemplate 는 RestTemplate 감쌓놓은 Wrapper Class임.
@Resource EzRestTemplate	ezRestTemplate;

Map<String, Object> resultMap = ezRestTemplate.get("http://localhost:12345/api/overview", HashMap.class);
System.out.println("resultMap: " + resultMap);


서버 모니터링 시스템을 만들고 있다.


구조는 서버의 MBean 및 Background 테스팅 도구가 실행한 정보를 각각의 N대의 서버에서 Message Qeue에 전달 하고, 모니터링 WAS에서 Queue 를 Receive 하여 Tomcat Long Pooling 을 활용하여 N대의 Client Browser 에 전송 해 준다.


public static List connections = new ArrayList<>(); 부분이 접속된 Client Browser 정보이므로, Queue Receive Event 시점에 connections Client 갯수만큼 PrintWriter 활용 해서 write 하고, connections 의 CometEvent 객체를 Close 하고, connections에서 Remove() 시킨다.


접속된 Client 정보를 public static 변수로 다른 Thread 에서 제어 하는게 잘 하는건지는 모르겠지만. 잘 된다...


사실 처음에 Tomcat Comet 방식을 이용했지만 지금은 WebSocket 방식으로 동일 하게 활용중이다.

다음 글은 동일한 상황에서 Comet 를 Websocket로 변환.. 거의 동일함..


import java.io.IOException;
import java.io.PrintWriter;
import java.util.ArrayList;
import java.util.List;

import javax.servlet.ServletException;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.apache.catalina.comet.CometEvent;
import org.apache.catalina.comet.CometProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;


@WebServlet("/comet/testCase")
public class TestCaseServlet extends HttpServlet implements CometProcessor {
	private static Logger log = LoggerFactory.getLogger(TestCaseServlet.class);
	
	public static List connections = new ArrayList<>();

	public void init() throws ServletException {
        log.info("----------- thread start");
    }

    public void destroy() {
    	log.info("---------- thread stop");
    	
    	synchronized (connections) {
			connections.clear();
		}
    }

	@Override
	public void event(CometEvent event) throws IOException, ServletException {
		log.info("testCase event: " + event.getEventType());
		HttpServletRequest request = event.getHttpServletRequest();
		HttpServletResponse response = event.getHttpServletResponse();
		response.setCharacterEncoding("UTF-8");
		event.setTimeout(1000*60*60);
		
		if (event.getEventType() == CometEvent.EventType.BEGIN) {
            log.info("Begin for request: {}, response: {}, session: {}", request, response, request.getSession(true).getId());
            synchronized(connections) {
            	connections.add(event);
            }
        } else if (event.getEventType() == CometEvent.EventType.ERROR) {
            log.info("Error for session: {}", request.getSession(true).getId());
            synchronized(connections) {
            	connections.remove(event);
            }
            PrintWriter writer = response.getWriter();
            writer.println("{result: '9999', msg: 'timeout'}");
            event.close();
        } else if (event.getEventType() == CometEvent.EventType.END) {
            log.info("End for session: " + request.getSession(true).getId());
            synchronized(connections) {
            	connections.remove(event);
            }
            PrintWriter writer = response.getWriter();
            writer.println("{result: '0000', msg: 'end'}");
            event.close();
        } else if (event.getEventType() == CometEvent.EventType.READ) {
        	 log.info("read");
            while (true) {
            	;
            }
        }
	}
}


BindResult 삽질기!!!


회사 동료가 Spring Validation 기능을 활용해보자고 해서 해보는데 엄청난 시간 소모가...

결론은 BindResult 파라미터 선언을 @Valid 바로 뒤에 해야 한다는...


[TestWeb.java]

import javax.validation.Valid;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.validation.BindingResult;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

@Controller
@RequestMapping("test")
public class TestWeb {
	private static Logger log = LoggerFactory.getLogger(TestWeb.class);

	@RequestMapping(value="case1", method=RequestMethod.GET)
	public String test(@Valid TestVo testVo, BindingResult bindResult, Model model) {
		log.debug("bindResult.hasErrors(): {}", bindResult.hasErrors());
		if (bindResult.hasErrors()) {
			throw new RuntimeException("파라미터 설정이 잘 못 되었습니다..");
			//return "coupon/mainError";	// 공통 Exception 처리 Throws 하거나 에러 뷰 return
		}
		return "test/case1";
	}
	
	
	
}

[TestVo.java]

import javax.validation.constraints.NotNull;
import javax.validation.constraints.Size;

import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class TestVo {
	@Size(min=0, max=5)
	private String test1;
	
	@NotNull
	private String test2;
}

[build.gradle]

dependencies {
	compile 'spring.... 4.1.x...'
	compile 'org.hibernate:hibernate-validator:5.1.3.Final'
}

최근 인터넷 곳곳에서 ORM 관련 내용들이 많이 쏟아져나오고 있다.


예전에도 Spring 과 Hibernate를 이용해서 내부 프로젝트를 진행 한적이 있었는데, 그때는 단순한것은 Criteria를 사용하고, 조금이라도 복잡한것은 HQL을 사용했던 기억인데 개념적으로 잘 이해되지 않아 초반에 힘들었던거 같은데...


이번에도 역시 많은 삽질을 해야 했다.. 개념 이해 뿐만 아니라 이래저래 많은 변화가 있었던것 같다.

Hibernate에서 JPA, Spring Data JPA, QueryDSL, Spring Data JPA & QueryDSL 등 표준이니 뭐니...


단순 셈플링이다.

아직 알지도 못하는 많은 방법들이 존재해서 뭐가 어떻게 되는건지 심도 있는 학습이 필요할 것 같다.

작은 내부 프로젝트를 진행하려고 하는데 이번 기회에 사용해봐야겠다.


[build.gradle]


apply plugin: 'java'
apply plugin: 'idea'

ext {
    javaVversion            = '1.7'
    servletVersion          = '3.0.1'
    springframeworkVersion  = '4.1.6.RELEASE'
    hibernateJpaVersion        = '1.0.1.Final'
    hibernateEntitymanagerVersion = '4.1.9.Final'
    hibernateCommonsAnnotationsVersion = '4.0.1.Final'
    hibernateValidatorVersion = '4.3.1.Final'
    aspectjVersion = '1.6.8'
    queryDslVersion = '3.2.0'
    springDataJpaVersion = '1.8.0.RELEASE'
}

List loggerSlf4jAndLogback = [
        "ch.qos.logback:logback-classic:1.0.13",
        "org.slf4j:jcl-over-slf4j:1.7.5"
]

sourceCompatibility = javaVversion
version = '1.0'

task wrapper(type: Wrapper) {
  gradleVersion = '2.3'
  distributionUrl = 'http://services.gradle.org/distributions/gradle-2.3-bin.zip'
}

repositories {
    mavenCentral()
}

dependencies {
    compile loggerSlf4jAndLogback

    compile "org.springframework:spring-webmvc:" + springframeworkVersion
    compile "org.springframework:spring-orm:" + springframeworkVersion
    compile "org.springframework:spring-aspects:" + springframeworkVersion

    compile "org.springframework.data:spring-data-jpa:" + springDataJpaVersion
    compile 'com.h2database:h2:+'
    //compile 'mysql:mysql-connector-java:5.1.23'

    compile "org.hibernate.javax.persistence:hibernate-jpa-2.0-api:" + hibernateJpaVersion
    compile "org.hibernate:hibernate-entitymanager:" + hibernateEntitymanagerVersion
    compile "org.hibernate.common:hibernate-commons-annotations:" + hibernateCommonsAnnotationsVersion
    compile "org.hibernate:hibernate-validator:" + hibernateValidatorVersion

    compile "com.mysema.querydsl:querydsl-core:" + queryDslVersion
    compile "com.mysema.querydsl:querydsl-apt:" + queryDslVersion
    compile "com.mysema.querydsl:querydsl-jpa:" + queryDslVersion
    compile "com.mysema.querydsl:querydsl-sql:" + queryDslVersion


    compile "org.aspectj:aspectjrt:" + aspectjVersion
    compile "org.aspectj:aspectjweaver:" + aspectjVersion


    compile 'javax:javaee-api:7.0'

    //providedCompile 'javax.servlet:javax.servlet-api:3.1.0'

    testCompile group: 'junit', name: 'junit', version: '4.11'
    testCompile "org.springframework:spring-test:" + springframeworkVersion
}

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

task generateQueryDSL(type: JavaCompile, group: 'build') {
    source = sourceSets.main.java
    classpath = configurations.compile
    options.compilerArgs = [
            "-proc:only",
            "-processor", "com.mysema.query.apt.jpa.JPAAnnotationProcessor"
    ]
    destinationDir = sourceSets.generated.java.srcDirs.iterator().next()
}

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

clean {
    delete sourceSets.generated.java.srcDirs
}

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

[Config AppConfig.java]

package pe.kr.ddakker.jpa;

import org.h2.tools.Server;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.context.annotation.aspectj.EnableSpringConfigured;
import org.springframework.core.env.Environment;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseBuilder;
import org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType;
import org.springframework.orm.jpa.JpaTransactionManager;
import org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean;
import org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter;
import org.springframework.transaction.annotation.EnableTransactionManagement;

import javax.sql.DataSource;
import java.beans.PropertyVetoException;
import java.sql.SQLException;
import java.util.Properties;

/**
 * Created by ddakker on 2015-04-09.
 */
@Configuration
@ComponentScan("pe.kr.ddakker.jpa")
@EnableTransactionManagement
@EnableSpringConfigured
@EnableJpaRepositories("pe.kr.ddakker.jpa")
public class AppConfig {

    @Autowired
    Environment env;

    @Bean
    public DataSource testDataSource() {
        return new EmbeddedDatabaseBuilder().setType(EmbeddedDatabaseType.H2).build();
    }

    @Bean
    public HibernateJpaVendorAdapter jpaVendorAdapter() {
        return new HibernateJpaVendorAdapter();
    }

    @Bean
    public LocalContainerEntityManagerFactoryBean entityManagerFactory() throws SQLException, PropertyVetoException{
        LocalContainerEntityManagerFactoryBean emf =
                new LocalContainerEntityManagerFactoryBean();
        emf.setDataSource(testDataSource());
        emf.setJpaVendorAdapter(jpaVendorAdapter());
        emf.setPersistenceUnitName("rss");
        emf.setPackagesToScan("pe.kr.ddakker.jpa.domain");
        Properties properties = new Properties();
        properties.put("hibernate.dialect", "org.hibernate.dialect.H2Dialect");
        properties.put("hibernate.hbm2ddl.auto", "create-drop");
//        properties.put("hibernate.show_sql","true");
//        properties.put("hibernate.format_sql", "true");
        emf.setJpaProperties(properties);
        return  emf;
    }

    @Bean
    public JpaTransactionManager transactionManager() throws PropertyVetoException, SQLException{
        JpaTransactionManager trans = new JpaTransactionManager();
        trans.setEntityManagerFactory(entityManagerFactory().getObject());
        return trans;
    }

}

[Domain User.java]

package pe.kr.ddakker.jpa.domain;

import javax.persistence.*;
import java.io.Serializable;
import java.util.Date;




@Entity
public class User implements Serializable {
    @Id
    @Column(name = "USER_ID")
    private Long id;

    @Column(name = "USER_NM")
    private String name;

    @Column(name = "USER_AGE")
    private Integer age;

    @Temporal(TemporalType.DATE)
    @Column(name = "REG_DT")
    private Date regDt;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public Integer getAge() {
        return age;
    }

    public void setAge(Integer age) {
        this.age = age;
    }

    public Date getRegDt() {
        return regDt;
    }

    public void setRegDt(Date regDt) {
        this.regDt = regDt;
    }
}

[Repository UserRepository.java]

package pe.kr.ddakker.jpa.repository;

import org.springframework.data.querydsl.QueryDslPredicateExecutor;
import org.springframework.data.repository.CrudRepository;
import pe.kr.ddakker.jpa.domain.User;

import java.util.List;

/**
 * Created by ddakker on 2015-04-09.
 */
public interface UserRepository extends CrudRepository, QueryDslPredicateExecutor {

    public List findByName(String name);
}

[Test UserRepositoryTest.java]

package pe.kr.ddakker.jpa.repository;

import com.mysema.query.jpa.impl.JPAQuery;
import org.junit.Before;
import org.junit.FixMethodOrder;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.MethodSorters;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import org.springframework.transaction.annotation.Transactional;
import pe.kr.ddakker.jpa.AppConfig;
import pe.kr.ddakker.jpa.domain.QUser;
import pe.kr.ddakker.jpa.domain.User;

import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.Date;
import java.util.List;

import static org.junit.Assert.assertEquals;

/**
 * Created by ddakker on 2015-04-09.
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {AppConfig.class})
@Transactional
@FixMethodOrder(MethodSorters.NAME_ASCENDING)
public class UserRepositoryTest {
    @Autowired private UserRepository userRepository;

    @PersistenceContext private EntityManager entityManager;

    /**
     * Spring JPA CRUD 기능을 이용한다면
     */
    @Before
    public void create() {
        User inputUser = new User();
        inputUser.setId(System.currentTimeMillis());
        inputUser.setName("ddakker");
        inputUser.setAge(33);
        inputUser.setRegDt(new Date());
        userRepository.save(inputUser);

        inputUser = new User();
        inputUser.setId(System.currentTimeMillis());
        inputUser.setName("petitjjang");
        inputUser.setAge(33);
        inputUser.setRegDt(new Date());
        userRepository.save(inputUser);

        inputUser = new User();
        inputUser.setId(System.currentTimeMillis());
        inputUser.setName("sisigi");
        inputUser.setAge(1);
        inputUser.setRegDt(new Date());
        userRepository.save(inputUser);

        assertEquals("전체 사이즈", 3, userRepository.count());
    }

    /**
     * Method Name Query를 사용한다면
     */
    @Test
    public void testRepository_methodNameQuery() {
        List userList = userRepository.findByName("ddakker");
        assertEquals("갯수는", 1, userList.size());
    }
    /**
     * Spring Data JPA & QueryDSL Predicate 사용한다면
     */
    @Test
    public void testRepository_Predicate() {
        String name = "dda%";
        int age = 33;

        QUser user = QUser.user;
        Page page =  userRepository.findAll(user.name.like(name).and(user.age.eq(age)), new PageRequest(0,10));
        assertEquals("검색 결과", 1, page.getNumberOfElements());

        Iterable users = userRepository.findAll(user.age.eq(age));
        for (User u : users) {
            System.out.println("iterable user: " + u.getId() + ", " + u.getName() + ", " + u.getAge() + "," + u.getRegDt());
        }
    }


    /**
     * Spring Data JPA & QueryDSL 확장 기능을 이용한다면
     * @throws Exception
     */
    @Test
    public void testRepository_support() throws Exception {
        UserRepositorySupport userRepositorySupport = new UserRepositorySupport(User.class);
        userRepositorySupport.setEntityManager(entityManager);
        assertEquals("큰 나이", 33, userRepositorySupport.getMaxAge());
        assertEquals("작은 나이", 1, userRepositorySupport.getMinAge());

    }

    /**
     * 직접 QueryDSL을 써본다면..
     *      - 이것을 어느 영역에 둬야 할까...
     */
    @Test
    public void test_Dsl() {
        JPAQuery query = new JPAQuery(entityManager);
        QUser qUser = QUser.user;
        List userList = query.from(qUser).where(qUser.name.eq("ddakker")).list(qUser);

        for (User u : userList) {
            System.out.println("list user: " + u.getId() + ", " + u.getName() + ", " + u.getAge() + "," + u.getRegDt());
        }
    }
}

[Repository UserRepositorySupport.java]

package pe.kr.ddakker.jpa.repository;

import org.springframework.data.jpa.repository.support.QueryDslRepositorySupport;
import pe.kr.ddakker.jpa.domain.QUser;
import pe.kr.ddakker.jpa.domain.User;

/**
 * Created by ddakker on 2015-04-09.
 */
public class UserRepositorySupport extends QueryDslRepositorySupport {
    public UserRepositorySupport(Class user) {
        super(user);
    }

    public int getMinAge() {
        QUser qUser = QUser.user;
        return from(qUser).uniqueResult(qUser.age.min());
    }

    public int getMaxAge() {
        QUser qUser = QUser.user;
        return from(qUser).uniqueResult(qUser.age.max());
    }
}

[참고] https://www.youtube.com/watch?v=ho0fQt8v_HA


+ Recent posts