JAXB 잘 사용하기

general java 2011. 12. 21. 16:01

2010년 초에 정리한 줄 알았는데, 과거 메일을 참조로 정리한다.
이 내용은 현재 JAXB 버전과 다를 수 있다.


JAXB에 대해서..
java XML 파서중에 Xstream, JAXB 가 있다. 취향에 따라서 다양하게 쓰기는 하는데..
아직 까지는 잘만 쓴다면 JAXB가 성능이 조금 좋은 것 같았다. JAXB 에 대한 이슈와 오해를 정리하고 어떻게 해야 잘 쓸 수 있는 것인지 확인한다.


JAXB의 초기화
XML 파서인 JAXB는 DOM을 사용해서 상당히 느렸는데, 최근에는 SAX와 STAX를 사용하여 성능이 좋아지고 있으나, 주의해야 하는 것은 JAXBContext를 매번 생성하지 않고 한번 생성된 것을 재사용하는 구조로 써야 한다. QName의 String.intern을 많이 호출하는 성능 상 이슈와 GC상 perm 영역에서 minor GC가 많이 일어난다. 만약 Perm gen 영역을 작게 했을 때는 OOME가 perm gen에서 일어날 수도 있다.

많은 개발자들이 JAXB를 쓸 때 항상 조심해야 하는 것으로 한번만 JAXBContext 객체를 하라는 내용이 많으나 그 중의 hudson 개발자인 가와구치도 Hudson commitor들에게 얘기한 내용이 있다.

http://markmail.org/message/hy2tmioxev7skqkl#query:+page:1+mid:hy2tmioxev7skqkl+state:results

TIP 1. Never create the same JAXBContext more than once

-------------------------------------------------------
Always, always assign JAXBContext to a static final field. A JAXBContext object can be shared safely by multiple threads. This is a very expensive to create, so create it once, and share it.





JAXB의 Thread Safe

JAXBContext 가 Thread safe하지 않다고 알고 있는 사람이 있기도 하다. Thread safe 하다.다만..
마셜러와 언마셜러와 밸리데이터는 thread safe하지 않기 때문에 잘 써야 한다.  JAXBContext 를 단독으로 쓸 때 잘 유의할 필요는 있다. 그러나 Spring과 연계되어서 쓸 때는 내부적으로 잘 thread safe하게 해서 편하게 쓸 수 있다.

http://jaxb.java.net/guide/Performance_and_thread_safety.html


8.1. Performance and thread-safety
The JAXBContext class is thread safe, but the Marshaller, Unmarshaller, and Validator classes are not thread safe.

For example, suppose you have a multi-thread server application that processes incoming XML documents by JAXB. In this case, for the best performance you should have just one instance of JAXBContext in your whole application like this:

Singleton JAXBContext
class MyServlet extends HttpServlet {   
    static final JAXBContext context = initContext();   
    private static JAXBContext initContext() {       
        return JAXBContext.newInstance(Foo.class,Bar.class);   
    }
}

And each time you need to unmarshal/marshal/validate a document. Just create a new Unmarshaller/Marshaller/Validator from this context, like this:

Thread local Unmarshaller
    public void doGet( HttpServletRequest req, HttpServletResponse ) {       
         Unmarshaller u = context.createUnmarshaller();       
          u.unmarshal(...);   
    }

This is the simplest safe way to use the JAXB RI from multi-threaded applications.

If you really care about the performance, and/or your application is going to read a lot of small documents, then creating Unmarshaller could be relatively an expensive operation. In that case, consider pooling Unmarshaller objects. Different threads may reuse one Unmarshaller instance, as long as you don't use one instance from two threads at the same time.


 




JAXB의 Accessor
jaxb-api, jaxb-impl 2.0 library을 기준으로 사용했고, 다음의 2가지 기준이 맞아야 Permgen영역에서 OOME 가 발생한다.,

JAXB의 plain object는 다음과 같이 사용할 수 있다.




@XmlRootElement(name = "message")
@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlType(propOrder = {"to", "from", "body"})
public class Message2 {
 private String to;

 private String from;

 private String body;
 
 @XmlElement(name = "TO")
 public String getTo() {
  return to;
 }
 public void setTo(String to) {
  this.to = to;
 }
 
 @XmlElement(name = "from")
 public String getFrom() {
  return from;
 }
 
 public void setFrom(String from) {
  this.from = from;
 }

 
 @XmlElement(name = "body")
 public String getBody() {
  return body;
 }
 
 public void setBody(String body) {
  this.body = body;
 }

}


 





원인을 잘 살펴본다.



Accessor의 GetterSetterReflection 인스턴스의 optimize가 호출되면, property에 대해서 getter와 setter 가 있는지 확인합니다.


         @Override
        public Accessor<BeanT,ValueT> optimize(JAXBContextImpl context) {
            if(getter==null || setter==null)
                // if we aren't complete, OptimizedAccessor won't always work
                return this;
            if(context!=null && context.fastBoot)
                // let's not waste time on doing this for the sake of faster boot.
                return this;

            Accessor<BeanT,ValueT> acc = OptimizedAccessorFactory.get(getter,setter);
            if(acc!=null)
                return acc;
            else
                return this;
        }
 


 

만약 getter/setter 메서드 중 하나라도 존재하지 않으면, 그냥 JAXB 내부의 Optimize 작업을 하지 않는다. 클래스 로딩 작업이 없게 된다. 만약 setter, getter가 존재하면, 내부 Optimize작업을 하는데,  Optimize 작업이 바로 내부 클래스를 직접 생성한다.

 OptimizedAccessorFactory 클래스 내부 소스를 보면, 선언된 클래스이름에 JaxbAccessorM_getter이름_setter이름_property타입의 클래스를 생성한다.

 String newClassName = toVMClassName(getter.getDeclaringClass())+"$JaxbAccessorM_"+getter.getName()+'_'+setter.getName()+'_'+typeName;

 

* 참고 : Accessor에 대한 개념 파악에 도움되는 글

Improving field get and set performance with ASM or Javassist

http://stackoverflow.com/questions/3022741/improving-field-get-and-set-performance-with-asm-or-javassist


public class AccessorGenerator { 
 
   
private final ClassPool pool; 
 
   
public PropertyGenerator() { 
        pool
= new ClassPool(); 
        pool
.appendSystemPath(); 
   
} 
 
   
public Map<String, PropertyAccessor> createAccessors(Class<?> klazz) throws Exception { 
       
Field[] fields = klazz.getDeclaredFields(); 
 
       
Map<String, PropertyAccessor> temp = new HashMap<String, PropertyAccessor>(); 
       
for (Field field : fields) { 
           
PropertyAccessor accessor = createAccessor(klazz, field); 
            temp
.put(field.getName(), accessor); 
       
} 
 
       
return Collections.unmodifiableMap(temp); 
   
} 
 
   
private PropertyAccessor createAccessor(Class<?> klazz, Field field) throws Exception { 
       
final String classTemplate = "%s_%s_accessor"; 
       
final String getTemplate = "public Object get(Object source) { return ((%s)source).%s; }"; 
       
final String setTemplate = "public void set(Object dest, Object value) { return ((%s)dest).%s = (%s) value; }"; 
 
       
final String getMethod = String.format(getTemplate,  
                                               klazz
.getName(), 
                                               field
.getName()); 
       
final String setMethod = String.format(setTemplate,  
                                               klazz
.getName(),  
                                               field
.getName(),  
                                               field
.getType().getName()); 
 
       
final String className = String.format(classTemplate, klazz.getName(), field.getName()); 
 
       
CtClass ctClass = pool.makeClass(className); 
        ctClass
.addMethod(CtNewMethod.make(getMethod, ctClass)); 
        ctClass
.addMethod(CtNewMethod.make(setMethod, ctClass)); 
        ctClass
.setInterfaces(new CtClass[] { pool.get(PropertyAccessor.class.getName()) }); 
       
Class<?> generated = ctClass.toClass(); 
       
return (PropertyAccessor) generated.newInstance(); 
   
} 
 
   
public static void main(String[] args) throws Exception { 
       
AccessorGenerator generator = new AccessorGenerator(); 
 
       
Map<String, PropertyAccessor> accessorsByName = generator.createAccessors(PurchaseOrder.class); 
 
       
PurchaseOrder purchaseOrder = new PurchaseOrder("foo", new Customer()); 
 
        accessorsByName
.get("name").set(purchaseOrder, "bar"); 
       
String name = (String) accessorsByName.get("name").get(purchaseOrder); 
       
System.out.println(name); 
   
} 
} 


jaxb가 reflection을 사용하면 속도가 너무 느리기 때문에 Accessor 클래스를 만들어서 속도를 향상시키고 private 멤버도 쉽게 접근하는 구조를 가지고 있다. 따라서 메소드를 확인하는 작업이 줄어들어서 속도를 향상시키는 패턴을 가지고 있다.로그를 보면 구체적으로 알 수 있다.


Plain Object인  Message2 를 만들고 JAXBContext로 클래스를 매번 생성하게 하였다.

@XmlRootElement(name = "message")
@XmlAccessorType(XmlAccessType.PROPERTY)
@XmlType(propOrder = {"to", "from", "body"})
public class Message2 {
 private String to;

 private String from;

 private String body;
 
 @XmlElement(name = "x")

 public String getTo() {
  return to;
 }
 public void setTo(String to) {
  this.to = to;
 }

 




jvm 출력 로그는 다음과 같다. AccessroM_ 클래스가 생성되었음을 확인할 수 있다.

[Loaded com.google.spring.bean.Message2$JaxbAccessorM_getBody_setBody_java_lang_String from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorM_getBody_setBody_java_lang_String from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorM_getFrom_setFrom_java_lang_String from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorM_getFrom_setFrom_java_lang_String from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorM_getTo_setTo_java_lang_String from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorM_getTo_setTo_java_lang_String from __JVM_DefineClass__]


 

만약 getter/setter 메서드를 없앤 클래스를 테스트해보았다.

@XmlRootElement(name = "message")
@XmlAccessorType(XmlAccessType.FIELD)
public class Message2 {
 public String to;

 public String from;

 public String body;
}




출력되는 jvm 로그는 다음처럼 Accessor가 생기는 것을 볼 수 있다.

Loaded com.google.spring.bean.Message2$JaxbAccessorF_to from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorF_to from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorF_from from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorF_from from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorF_body from __JVM_DefineClass__]
[Loaded com.google.spring.bean.Message2$JaxbAccessorF_body from __JVM_DefineClass__]

 
JAXB OOME 테스트

매번 JAXBContext를 생성하면 과연 OOME가 일어날까?
jvm 의 perm gen 영역과 heap 영역을 작게 했다.

-XX:PermSize=2500k
-XX:MaxPermSize=2500k
-XX:+PrintGCDetails
-XX:+PrintGCTimeStamps
-XX:+TraceClassUnloading
-XX:+TraceClassLoading
-Xms10m
-Xmx10m





모니터링시
생성된 자바 loaded class 가 계속 상승하여 jvm에 부하를 주지만, heap, perm gen 영역은 그리 메모리가 크게 늘어나지 않았다.



아마도 OOME가 나는 이유는 web에서 session 단위로 hashmap으로 jsp로 전달되는 과정에서 (서블릿 스펙상 session은 최소 1초로 사용) 일어난 것으로 생각된다. 일반 Application에서는 큰 영향을 없을 수 있다.


마치며..
JAXB는 성능이 좋아 사람들에게 많이 쓰이고 있다. 매번 클래스를 Reflection 해서 정보를 얻어오기 보다  Accessor클래스를 생성하고 있다. 만약 JAXBContext 를 매번 호출하면 Accessor 클래스 로딩이 호출시마다 일어나 jvm, gc에 부하를 줄 수 있다. JAXBContext는 한번만 초기화하고 객체를 재사용할 필요가 있다.


Posted by 김용환 '김용환'
TAG ,

댓글을 달아 주세요

  1. 2017.12.22 10:10  댓글주소  수정/삭제  댓글쓰기

    비밀댓글입니다