CGLIB를 이용하여 고성능의 프록시 객체를 만드는 방법에 대해서 살펴본다.
CGLIB 소개 및 설치
CGLIB는 코드 생성 라이브러리로서(Code Generator Library) 런타임에 동적으로 자바 클래스의 프록시를 생성해주는 기능을 제공한다. CGLIB를 사용하면 매우 쉽게 프록시 객체를 생성할 수 있으며, 성능 또한 우수하다. 더불어, 인터페이스가 아닌 클래스에 대해서 동적 프록시를 생성할 수 있기 때문에 다양한 프로젝트에서 널리 사용되고 있다. 예를 들어, Hibernate는 자바빈 객체에 대한 프록시를 생성할 때 CGLIB를 사용하며, Spring은 프록시 기반의 AOP를 구현할 때 CGLIB를 사용하고 있다.
CGLIB의 주요 구성 요소
CGLIB는 프록시 생성과 관련된 모듈은 아래 그림과 같이 Enhancer 클래스, Callback 인터페이스 그리고 CallbackFilter 인터페이스이다. 이 세가지만 있으면 아주 손쉽게 원하는 프록시 객체를 생성할 수 있게 된다.
Callback 인터페이스를 구현한 인터페이스를 두가지(MethodInterceptor, NoOp)만 표시했는데 이외에도 몇가지 인터페이스가 더 존재한다. 하지만, MethodInterceptor 인터페이스가 주로 사용되며, 본 글에서도 MethodInterceptor를 사용한 프록시 기능에 대해서만 살펴볼 것이다.
CGLIB 설치하기
CGLIB의 설치 방법은 매우 간단하다. 필요한 jar 파일을 클래스패스로 설정해주기만 하면 된다. CGLIB 모듈은 CGLIB 프로젝트 사이트인 http://cglib.sourceforge.net/ 에서 다운로드 받을 수 있다. 현재 버전은 2.1.3 버전으로서 다운로드 사이트에서 다음의 두가지 파일 중 하나를 받으면 된다.
이제 남은 작업은 cglib-nodep-2.1_3.jar 파일을 클래스패스에 추가해주는 것 뿐이다. 그럼, CGLIB를 사용할 준비가 끝난다.
Enhancer를 사용한 프록시 객체 생성 및 MethodInterceptor 사용하기
CGLIB를 사용하여 프록시를 생성할 때에는 크게 크게 두가지 작업을 필요로 한다.
본 글에서는 아래의 클래스를 프록시 대상으로 사용할 것이다. 테스트 결과를 눈으로 확인할 수 있도록 하기 위해 System.out.println()을 사용해서 알맞은 문자를 출력하도록 했다.
Enhancer 클래스를 사용한 프록시 객체 생성 하기
이제 net.sf.cglib.proxy.Enhancer 클래스를 사용하여 MemberServiceImpl 클래스의 프록시 객체를 생성해보자. 생성하는 코드는 다음과 같이 매우 간단한다.
단 4줄의 코드로 프록시 객체를 생성할 수 있다. 위 코드는 다음과 같은 구조의 프록시 객체를 생성하게 된다.
즉, enhancer.create()로 생성한 객체는 프록시 객체가 되고, 이 객체의 메소드를 호출하게 되면 프록시 객체를 거쳐서 실제 객체의 메소드가 호출된다. 실제로 위 코드를 실행하면 다음과 같은 결과가 출력된다.
MethodInterceptor 사용하여 프록시 객체 다루기
앞서 실행 결과를 보면 직접 MemberServiceImpl 객체를 생성해서 실행하는 것과 별반 차이가 없어보인다. 이는 프록시 객체가 단순히 원본 객체의 메소드를 직접적으로 호출하기 때문이다. 하지만, 대부분의 프록시 객체는 원본 객체에 접근하기 전에 별도의 작업을 수행하며, CGLIB는 Callback을 사용해서 별도 작업을 수행할 수 있도록 하고 있다.
CGLIB가 제공하는 여러가지 Callback 중 앞서 코드에서도 나왔던 net.sf.cglib.proxy.NoOp 는 아무 작업도 수행하지 않고 곧바로 원본 객체를 호출하는 Callback 이다.
CGLIB가 제공하는 Callback 중 가장 많이 사용되는 것은 net.sf.cglib.proxy.MethodInterceptor 이다. MethodInterceptor는 다음과 같이 프록시와 원본 객체 사이에 위치하여 메소드 호출을 조작할 수 있도록 해 준다.
프록시 객체에 대한 모든 호출이 MethodInterceptor를 거친뒤에 원본 객체에 전달된다. 따라서, MethodInterceptor를 사용하면 원본 객체 대신 다른 객체의 메소드를 호출할 수 있도록 할 수 있으며, 심지어 원본 객체에 전달될 인자의 값을 변경할 수도 있다.
MethodInterceptor 인터페이스는 다음과 같 1개의 메소드를 선언하고 있다.
이 두가지 방법중에서 어떤 것을 사용해도 무방하지만, 리플렉션 방식보다 MethodProxy 방식이 빠르다고 하니 MethodProxy를 사용할 것을 추천한다.
MethodInterceptor를 사용하면 원본 객체의 메소드를 호출하기 전에 여러가지 작업을 수행할 수 있게 된다. 예를 들어, MemberServiceImpl에 대한 모든 메소드 호출 기록을 로그로 남기고 싶다면 다음과 같이 MethodInterceptor 구현 클래스를 작성하면 된다.
Enhancer.setCallback() 메소드를 통해서 사용할 MethodInterceptor 객체를 지정해주기만 하면 MethodInterceptor가 사용된다. 앞서 작성한 MethodCallLogInterceptor을 MemberServiceImpl 프록시에 적용하려면 다음과 같은 코드를 사용하면 된다.
위와 같이 Enhancer.setCallback()을 통해서 MethodInterceptor 를 지정하면, 프록시의 메소드를 호출할 때 마다 MethodInterceptor.intercept() 메소드가 호출된다. 다음은 위 코드의 실행 결과인데, 원본 객체의 메소드가 호출되기 전에 MethodCallLogInterceptor.intercept() 메소드가 실행된 것을 확인할 수 있다.
before MethodCallLogInterceptor.intercept() -> MethodCallLogInterceptor
MemberServiceImpl.regist -> 원본 객체 메소드 실제 호출
after MethodCallLogInterceptor.intercept() -> MethodCallLogInterceptor
before MethodCallLogInterceptor.intercept()
MemberServiceImpl.getMember:madvirus -> 원본 객체 메소드 실제 호출
after MethodCallLogInterceptor.intercept()
MethodCallLogInterceptor 는 원본 객체의 메소드를 호출하는 앞뒤로 간단한 문장을 출력하는 정도로 끝났지만, 다음과 같이 다양한 형태로 MethodInterceptor 를 구현할 수 있을 것이다. 예를 들어, 다음과 같이 조건에 따라서 원본 객체가 아닌 다른 객체의 메소드를 호출할 수도 있을 것이다.
CallbackFilter를 사용하여 알맞은 Callback 실행하기
여러개의 Callback 중에서 상황에 따라서 원하는 Callback을 사용하고 싶은 경우도 있을 것이다. 예를 들어, MemberServiceImpl.regist() 메소드와 MemberServiceImpl.getMember() 메소드에 대해서 서로 다른 Callback을 적용하고 싶은 경우가 있을 것이다. 이럴 땐 net.sf.cglib.proxy.CallbackFilter 인터페이스를 사용하면 된다.
CallbackFilter 인터페이스는 다음과 같은 메소드를 정의하고 있다.
이때 CallbackFilter 인터페이스를 구현한 MemberServiceCallbackFilter 클래스는 다음과 같이 구현할 수 있다.
RegistMemberInterceptor와 GetMemberInterceptor는 intercept() 메소드에서 단순히 클래스 이름을 출력한 뒤 원본 객체의 메소드를 호출하도록 구현하였으며,
위 코드를 실행할 경우 다음과 같이 메소드 이름에 따라서 알맞은 Callback이 실행되는 것을 확인할 수 있다.
맺음말
CGLIB는 강력하면서도 고성능의 코드 생성 라이브러리로서, 인터페이스를 필요로 하는 JDK의 다이나믹 프록시 대신 사용될 수 있다. 더불어, CGLIB는 바이트 코드를 조작하는 프레임워크인 ASM을 사용함으로써 리플렉션을 사용하는 JDK 다이나믹 프록시보다 빠르다. 이런 이유로 Hibernate, Spring, dynaop와 같은 다양한 프레임워크에서 CGLIB를 사용하고 있는 것이다. 여러분도 고성능의 프록시 객체가 필요할 경우 CGLIB를 사용해보기 바란다.
관련링크:
CGLIB 소개 및 설치
CGLIB는 코드 생성 라이브러리로서(Code Generator Library) 런타임에 동적으로 자바 클래스의 프록시를 생성해주는 기능을 제공한다. CGLIB를 사용하면 매우 쉽게 프록시 객체를 생성할 수 있으며, 성능 또한 우수하다. 더불어, 인터페이스가 아닌 클래스에 대해서 동적 프록시를 생성할 수 있기 때문에 다양한 프로젝트에서 널리 사용되고 있다. 예를 들어, Hibernate는 자바빈 객체에 대한 프록시를 생성할 때 CGLIB를 사용하며, Spring은 프록시 기반의 AOP를 구현할 때 CGLIB를 사용하고 있다.
CGLIB의 주요 구성 요소
CGLIB는 프록시 생성과 관련된 모듈은 아래 그림과 같이 Enhancer 클래스, Callback 인터페이스 그리고 CallbackFilter 인터페이스이다. 이 세가지만 있으면 아주 손쉽게 원하는 프록시 객체를 생성할 수 있게 된다.
Callback 인터페이스를 구현한 인터페이스를 두가지(MethodInterceptor, NoOp)만 표시했는데 이외에도 몇가지 인터페이스가 더 존재한다. 하지만, MethodInterceptor 인터페이스가 주로 사용되며, 본 글에서도 MethodInterceptor를 사용한 프록시 기능에 대해서만 살펴볼 것이다.
CGLIB 설치하기
CGLIB의 설치 방법은 매우 간단하다. 필요한 jar 파일을 클래스패스로 설정해주기만 하면 된다. CGLIB 모듈은 CGLIB 프로젝트 사이트인 http://cglib.sourceforge.net/ 에서 다운로드 받을 수 있다. 현재 버전은 2.1.3 버전으로서 다운로드 사이트에서 다음의 두가지 파일 중 하나를 받으면 된다.
- cglib-2.1_3.jar - ASM 모듈을 필요로 하는 버전
- cglib-nodep-2.1_3.jar - ASM 모듈이 포함되어 있는 버전
이제 남은 작업은 cglib-nodep-2.1_3.jar 파일을 클래스패스에 추가해주는 것 뿐이다. 그럼, CGLIB를 사용할 준비가 끝난다.
Enhancer를 사용한 프록시 객체 생성 및 MethodInterceptor 사용하기
CGLIB를 사용하여 프록시를 생성할 때에는 크게 크게 두가지 작업을 필요로 한다.
- net.sf.cglib.proxy.Enhancer 클래스를 사용하여 원하는 프록시 객체 만들기
- net.sf.cglib.proxy.Callback을 사용하여 프록시 객체 조작하기
본 글에서는 아래의 클래스를 프록시 대상으로 사용할 것이다. 테스트 결과를 눈으로 확인할 수 있도록 하기 위해 System.out.println()을 사용해서 알맞은 문자를 출력하도록 했다.
package test;
public class MemberServiceImpl implements MemberService {
public MemberServiceImpl() {
System.out.println("create MemberServiceImpl");
}
public void regist(Member member) {
System.out.println("MemberServiceImpl.regist");
}
public Member getMember(String id) {
System.out.println("MemberServiceImpl.getMember:"+id);
return new Member();
}
}
public class MemberServiceImpl implements MemberService {
public MemberServiceImpl() {
System.out.println("create MemberServiceImpl");
}
public void regist(Member member) {
System.out.println("MemberServiceImpl.regist");
}
public Member getMember(String id) {
System.out.println("MemberServiceImpl.getMember:"+id);
return new Member();
}
}
Enhancer 클래스를 사용한 프록시 객체 생성 하기
이제 net.sf.cglib.proxy.Enhancer 클래스를 사용하여 MemberServiceImpl 클래스의 프록시 객체를 생성해보자. 생성하는 코드는 다음과 같이 매우 간단한다.
// 1. Enhancer 객체를 생성
Enhancer enhancer = new Enhancer();
// 2. setSuperclass() 메소드에 프록시할 클래스 지정
enhancer.setSuperclass(MemberServiceImpl.class);
enhancer.setCallback(NoOp.INSTANCE);
// 3. enhancer.create()로 프록시 생성
Object obj = enhancer.create();
// 4. 프록시를 통해서 간접 접근
MemberServiceImpl memberService = (MemberServiceImpl)obj;
memberService.regist(new Member());
memberService.getMember("madvirus");
Enhancer enhancer = new Enhancer();
// 2. setSuperclass() 메소드에 프록시할 클래스 지정
enhancer.setSuperclass(MemberServiceImpl.class);
enhancer.setCallback(NoOp.INSTANCE);
// 3. enhancer.create()로 프록시 생성
Object obj = enhancer.create();
// 4. 프록시를 통해서 간접 접근
MemberServiceImpl memberService = (MemberServiceImpl)obj;
memberService.regist(new Member());
memberService.getMember("madvirus");
단 4줄의 코드로 프록시 객체를 생성할 수 있다. 위 코드는 다음과 같은 구조의 프록시 객체를 생성하게 된다.
즉, enhancer.create()로 생성한 객체는 프록시 객체가 되고, 이 객체의 메소드를 호출하게 되면 프록시 객체를 거쳐서 실제 객체의 메소드가 호출된다. 실제로 위 코드를 실행하면 다음과 같은 결과가 출력된다.
create MemberServiceImpl
MemberServiceImpl.regist
MemberServiceImpl.getMember:madvirus
MemberServiceImpl.regist
MemberServiceImpl.getMember:madvirus
MethodInterceptor 사용하여 프록시 객체 다루기
앞서 실행 결과를 보면 직접 MemberServiceImpl 객체를 생성해서 실행하는 것과 별반 차이가 없어보인다. 이는 프록시 객체가 단순히 원본 객체의 메소드를 직접적으로 호출하기 때문이다. 하지만, 대부분의 프록시 객체는 원본 객체에 접근하기 전에 별도의 작업을 수행하며, CGLIB는 Callback을 사용해서 별도 작업을 수행할 수 있도록 하고 있다.
CGLIB가 제공하는 여러가지 Callback 중 앞서 코드에서도 나왔던 net.sf.cglib.proxy.NoOp 는 아무 작업도 수행하지 않고 곧바로 원본 객체를 호출하는 Callback 이다.
CGLIB가 제공하는 Callback 중 가장 많이 사용되는 것은 net.sf.cglib.proxy.MethodInterceptor 이다. MethodInterceptor는 다음과 같이 프록시와 원본 객체 사이에 위치하여 메소드 호출을 조작할 수 있도록 해 준다.
프록시 객체에 대한 모든 호출이 MethodInterceptor를 거친뒤에 원본 객체에 전달된다. 따라서, MethodInterceptor를 사용하면 원본 객체 대신 다른 객체의 메소드를 호출할 수 있도록 할 수 있으며, 심지어 원본 객체에 전달될 인자의 값을 변경할 수도 있다.
MethodInterceptor 인터페이스는 다음과 같 1개의 메소드를 선언하고 있다.
- public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable
- object: 원본 객체
- method: 원본 객체의 호출될 메소드를 나타내는 Method 객체
- args: 원본 객체에 전달될 파라미터
- methodProxy: CGLIB가 제공하는 원본 객체의 메소드 프록시.
// 방법1: 자바의 리플렉션 사용
Object returnValue = method.invoke(object, args);
// 방법2: CGLIB의 MethodProxy 사용
Object returnValue = methodProxy.invokeSuper(object, args);
Object returnValue = method.invoke(object, args);
// 방법2: CGLIB의 MethodProxy 사용
Object returnValue = methodProxy.invokeSuper(object, args);
이 두가지 방법중에서 어떤 것을 사용해도 무방하지만, 리플렉션 방식보다 MethodProxy 방식이 빠르다고 하니 MethodProxy를 사용할 것을 추천한다.
MethodInterceptor를 사용하면 원본 객체의 메소드를 호출하기 전에 여러가지 작업을 수행할 수 있게 된다. 예를 들어, MemberServiceImpl에 대한 모든 메소드 호출 기록을 로그로 남기고 싶다면 다음과 같이 MethodInterceptor 구현 클래스를 작성하면 된다.
package test;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class MethodCallLogInterceptor implements MethodInterceptor {
public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy)
throws Throwable {
System.out.println("before MemberServiceLogger.intercept()");
Object returnValue = methodProxy.invokeSuper(object, args);
System.out.println("after MemberServiceLogger.intercept()");
return returnValue;
}
}
import java.lang.reflect.Method;
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
public class MethodCallLogInterceptor implements MethodInterceptor {
public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy)
throws Throwable {
System.out.println("before MemberServiceLogger.intercept()");
Object returnValue = methodProxy.invokeSuper(object, args);
System.out.println("after MemberServiceLogger.intercept()");
return returnValue;
}
}
Enhancer.setCallback() 메소드를 통해서 사용할 MethodInterceptor 객체를 지정해주기만 하면 MethodInterceptor가 사용된다. 앞서 작성한 MethodCallLogInterceptor을 MemberServiceImpl 프록시에 적용하려면 다음과 같은 코드를 사용하면 된다.
Enhancer enhancer = new Enhancer();
enhancer.setSuperclass(MemberServiceImpl.class);
enhancer.setCallback(new MethodCallLogInterceptor());
Object obj = enhancer.create();
MemberServiceImpl memberService = (MemberServiceImpl)obj;
memberService.regist(new Member());
enhancer.setSuperclass(MemberServiceImpl.class);
enhancer.setCallback(new MethodCallLogInterceptor());
Object obj = enhancer.create();
MemberServiceImpl memberService = (MemberServiceImpl)obj;
memberService.regist(new Member());
위와 같이 Enhancer.setCallback()을 통해서 MethodInterceptor 를 지정하면, 프록시의 메소드를 호출할 때 마다 MethodInterceptor.intercept() 메소드가 호출된다. 다음은 위 코드의 실행 결과인데, 원본 객체의 메소드가 호출되기 전에 MethodCallLogInterceptor.intercept() 메소드가 실행된 것을 확인할 수 있다.
before MethodCallLogInterceptor.intercept() -> MethodCallLogInterceptor
MemberServiceImpl.regist -> 원본 객체 메소드 실제 호출
after MethodCallLogInterceptor.intercept() -> MethodCallLogInterceptor
before MethodCallLogInterceptor.intercept()
MemberServiceImpl.getMember:madvirus -> 원본 객체 메소드 실제 호출
after MethodCallLogInterceptor.intercept()
MethodCallLogInterceptor 는 원본 객체의 메소드를 호출하는 앞뒤로 간단한 문장을 출력하는 정도로 끝났지만, 다음과 같이 다양한 형태로 MethodInterceptor 를 구현할 수 있을 것이다. 예를 들어, 다음과 같이 조건에 따라서 원본 객체가 아닌 다른 객체의 메소드를 호출할 수도 있을 것이다.
public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy)
throws Throwable {
Object returnValue = null;
if (어떤 조건) {
returnValue = otherService.getMember((String)args[0]);
} else {
returnValue = methodProxy.invokeSuper(object, args);
}
return returnValue;
}
throws Throwable {
Object returnValue = null;
if (어떤 조건) {
returnValue = otherService.getMember((String)args[0]);
} else {
returnValue = methodProxy.invokeSuper(object, args);
}
return returnValue;
}
CallbackFilter를 사용하여 알맞은 Callback 실행하기
여러개의 Callback 중에서 상황에 따라서 원하는 Callback을 사용하고 싶은 경우도 있을 것이다. 예를 들어, MemberServiceImpl.regist() 메소드와 MemberServiceImpl.getMember() 메소드에 대해서 서로 다른 Callback을 적용하고 싶은 경우가 있을 것이다. 이럴 땐 net.sf.cglib.proxy.CallbackFilter 인터페이스를 사용하면 된다.
CallbackFilter 인터페이스는 다음과 같은 메소드를 정의하고 있다.
- public int accept(Method method) - 메소드에 적용될 Callback의 인덱스를 리턴
...
enhancer.setSuperclass(MemberServiceImpl.class);
Callback[] callbacks = new Callback[] {
new RegistMemberInterceptor(), // 인덱스 0
new GetMemberInterceptor() // 인덱스 1
};
enhancer.setCallbacks(callbacks); // 배열로 Callback 목록 지정
enhancer.setCallbackFilter(new MemberServiceCallbackFilter());
enhancer.setSuperclass(MemberServiceImpl.class);
Callback[] callbacks = new Callback[] {
new RegistMemberInterceptor(), // 인덱스 0
new GetMemberInterceptor() // 인덱스 1
};
enhancer.setCallbacks(callbacks); // 배열로 Callback 목록 지정
enhancer.setCallbackFilter(new MemberServiceCallbackFilter());
이때 CallbackFilter 인터페이스를 구현한 MemberServiceCallbackFilter 클래스는 다음과 같이 구현할 수 있다.
package test;
import java.lang.reflect.Method;
import net.sf.cglib.proxy.CallbackFilter;
public class MemberServiceCallbackFilter implements CallbackFilter {
public int accept(Method method) {
if (method.getName().equals("regist")) {
return 0; // RegistMemberInterceptor를 선택
} else {
return 1; // GetMemberInterceptor를 선택
}
}
}
import java.lang.reflect.Method;
import net.sf.cglib.proxy.CallbackFilter;
public class MemberServiceCallbackFilter implements CallbackFilter {
public int accept(Method method) {
if (method.getName().equals("regist")) {
return 0; // RegistMemberInterceptor를 선택
} else {
return 1; // GetMemberInterceptor를 선택
}
}
}
RegistMemberInterceptor와 GetMemberInterceptor는 intercept() 메소드에서 단순히 클래스 이름을 출력한 뒤 원본 객체의 메소드를 호출하도록 구현하였으며,
enhancer.setSuperclass(MemberServiceImpl.class);
Callback[] callbacks = new Callback[] {
new RegistMemberInterceptor(),
new GetMemberInterceptor()
};
enhancer.setCallbacks(callbacks);
enhancer.setCallbackFilter(new MemberServiceCallbackFilter());
Object obj = enhancer.create();
MemberServiceImpl memberService = (MemberServiceImpl)obj;
memberService.regist(new Member());
memberService.getMember("madvirus");
Callback[] callbacks = new Callback[] {
new RegistMemberInterceptor(),
new GetMemberInterceptor()
};
enhancer.setCallbacks(callbacks);
enhancer.setCallbackFilter(new MemberServiceCallbackFilter());
Object obj = enhancer.create();
MemberServiceImpl memberService = (MemberServiceImpl)obj;
memberService.regist(new Member());
memberService.getMember("madvirus");
위 코드를 실행할 경우 다음과 같이 메소드 이름에 따라서 알맞은 Callback이 실행되는 것을 확인할 수 있다.
RegistMemberInterceptor.intercept() -> regist 메소드 호출시 콜백
MemberServiceImpl.regist
GetMemberInterceptor.intercept() -> getMember 메소드 호출시 콜백
MemberServiceImpl.getMember:madvirus
MemberServiceImpl.regist
GetMemberInterceptor.intercept() -> getMember 메소드 호출시 콜백
MemberServiceImpl.getMember:madvirus
맺음말
CGLIB는 강력하면서도 고성능의 코드 생성 라이브러리로서, 인터페이스를 필요로 하는 JDK의 다이나믹 프록시 대신 사용될 수 있다. 더불어, CGLIB는 바이트 코드를 조작하는 프레임워크인 ASM을 사용함으로써 리플렉션을 사용하는 JDK 다이나믹 프록시보다 빠르다. 이런 이유로 Hibernate, Spring, dynaop와 같은 다양한 프레임워크에서 CGLIB를 사용하고 있는 것이다. 여러분도 고성능의 프록시 객체가 필요할 경우 CGLIB를 사용해보기 바란다.
관련링크:
출처 - http://javacan.tistory.com/entry/114
'Development > Java' 카테고리의 다른 글
java.lang.NoSuchMethodError (0) | 2012.11.22 |
---|---|
java - Executor (0) | 2012.11.16 |
java - jar (0) | 2012.11.09 |
java 로드맵 (0) | 2012.11.07 |
java - enum (0) | 2012.11.05 |
Posted by linuxism