m2 eclipse plugin이 1.0 버전 업하면서 생긴 단순한 설정은 아래와 같이 처리한다.
http://knight76.tistory.com/entry/이클립스-STS-maven-builder-변경


컴파일을 잘 되는데, pom.xml 파일에 maven parent를 사용하는 부분에서 에러가 있다고 나온다. build life cycle에서 무엇인가 이슈가 있다.

 

원인을 찾아보니, 이클립스 싸이트의 m2e 플러그인에 대한 내용이 있다. 간단히 요약해보면 다음과 같다. 빌드시 명시적으로 문제의 소지가 있는 플러그인에 대해서 에러로 처리할테니, 무시하든지 추가하든지 하는 설정을 넣으라는 얘기이다. (m2eclipse 플러그인 개발자도 maven 때문에 고생한다…. )

http://wiki.eclipse.org/M2E_plugin_execution_not_covered

https://issues.sonatype.org/browse/MNGECLIPSE-823에 내용에 있는 것처럼 리소스들과 컴파일이 안되는 문제가 발생되기도 했다. 0.12 버전 이하에서는 이클립스 빌드시 maven을 사용해서 컴파일을 했었다. 사실 이클립스 플러그인 개발입장에서는 그저 maven에 위임하는데, 어떤 “ Plug excution”이 동작하여 예상치 못하게 파일(리소스)가 날아가(missing) 버리거나 JVM, OS 리소스 부족으로 문제를 일으키는 경우가 있었다.

이 문제를 해결해가 위해서 1.0 부터는 빌드 lifecycle에 명확한 방법(explicit instructions)을 제공하였다. 이것을 “project build lifecycle mapping” (lifecycle mapping)이라고  한다. pom.xml 설정에 maven 빌드와 별도로플러그 실행(execution)에 대한 정보를 넣을 수 있게 하였다.

예를 들어 project build lifecycle mapping 정보가 없는 경우에 대해서는 다음과 같이 m2 이클립스 플러그인에서 에러로 처리한다고 한다.

Plugin execution not covered by lifecycle configuration:
org.apache.maven.plugins:maven-antrun-plugin:1.3:run
    (execution: generate-sources-input, phase: generate-sources)
이 문제를 해결하기 위해서 ignore 또는 execute goal을 넣어야 한다. 

 

아래 에러 로그에 있는 내용이 바로 위에 작성한 위키의 내용과 일치한다.

Multiple annotations found at this line:
    - Plugin execution not covered by lifecycle configuration: org.codehaus.mojo:xml-maven-plugin:1.0:transform
     (execution: default, phase: process-resources)
    - Plugin execution not covered by lifecycle configuration: org.apache.maven.plugins:maven-antrun-plugin:1.3:run
     (execution: process-resources, phase: process-resources)
    - maven-dependency-plugin (goals "copy-dependencies", "unpack") is not supported by m2e.
    - Plugin execution not covered by lifecycle configuration: org.apache.maven.plugins:maven-antrun-plugin:1.3:run
     (execution: copy-base-resource, phase: generate-sources)

 

이 방식을 이용해서 아래와 같이 pom.xml 파일에 정리하였더니 더 이상 이클립에서 에러라고 처리되지 않는다.

<build>
        <pluginManagement>
            <plugins>
                <!--This plugin's configuration is used to store Eclipse m2e settings
                    only. It has no influence on the Maven build itself. -->
                <plugin>
                    <groupId>org.eclipse.m2e</groupId>
                    <artifactId>lifecycle-mapping</artifactId>
                    <version>1.0.0</version>
                    <configuration>
                        <lifecycleMappingMetadata>
                            <pluginExecutions>
                                <pluginExecution>
                                    <pluginExecutionFilter>
                                        <groupId>
                                            org.apache.maven.plugins
                                        </groupId>
                                        <artifactId>
                                            maven-antrun-plugin
                                        </artifactId>
                                        <versionRange>
                                            [1.3,)
                                        </versionRange>
                                        <goals>
                                            <goal>run</goal>
                                        </goals>
                                    </pluginExecutionFilter>
                                    <action>
                                        <ignore></ignore>
                                    </action>
                                </pluginExecution>
                                <pluginExecution>
                                    <pluginExecutionFilter>
                                        <groupId>
                                            org.codehaus.mojo
                                        </groupId>
                                        <artifactId>
                                            xml-maven-plugin
                                        </artifactId>
                                        <versionRange>
                                            [1.0,)
                                        </versionRange>
                                        <goals>
                                            <goal>transform</goal>
                                        </goals>
                                    </pluginExecutionFilter>
                                    <action>
                                        <ignore></ignore>
                                    </action>
                                </pluginExecution>
                                <pluginExecution>
                                    <pluginExecutionFilter>
                                        <groupId>org.apache.maven.plugins</groupId>
                                        <artifactId>maven-dependency-plugin</artifactId>
                                        <versionRange>[1.0.0,)</versionRange>
                                        <goals>
                                            <goal>copy-dependencies</goal>
                                        </goals>
                                    </pluginExecutionFilter>
                                    <action>
                                        <ignore />
                                    </action>
                                </pluginExecution>
                            </pluginExecutions>
                        </lifecycleMappingMetadata>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>
    </build>

Posted by '김용환'
,

 

최신 이클립스/ STS 에 포함되었던 maven plugin m2 eclipse가 버전업을 하면서 패키지가 변경되었다. (0.1 –> 1.0) maven 빌드는 되는데, 이클립스 에서의 소스상으로는 에러가 나오는 것처럼 볼 수 있다.

 

해결을 하려면, 다음과 같이 수정하면 컴파일이 된다.


<?xml version="1.0" encoding="UTF-8"?>
<projectDescription>
    <name>xxx-parent</name>
    <comment></comment>
    <projects>
    </projects>
    <buildSpec>
        <buildCommand>
            <name>org.eclipse.jdt.core.javabuilder</name>
            <arguments>
            </arguments>
        </buildCommand>
        <buildCommand>
           <name>org.maven.ide.eclipse.maven2Builder</name>
            <name>org.eclipse.m2e.core.maven2Builder</name>
            <arguments>
            </arguments>
        </buildCommand>
    </buildSpec>
    <natures>
        <nature>org.maven.ide.eclipse.maven2Nature</nature>
        <nature>org.eclipse.jdt.core.javanature</nature>
       <nature>org.eclipse.m2e.core.maven2Nature</nature>
        
    </natures>
</projectDescription>

 

 

* 참고 사항

구버전 (2.5.2.RELEASE)

 

신버전 (2.8.1.RELEASE)






그 외 .classpath  파일에 수정사항이 있다.

<?xml version="1.0" encoding="UTF-8"?>
<classpath>
<classpathentry kind="con" path="org.maven.ide.eclipse.MAVEN2_CLASSPATH_CONTAINER"/>
<classpathentry kind="con" path="org.eclipse.m2e.MAVEN2_CLASSPATH_CONTAINER"/>
</classpath>

이렇게 두개를 바꿔야 한다.

Posted by '김용환'
,

 

최신 이클립스, STS에서의 maven은 3.X 대이다.  따라서 maven2에서 동작하던 일부 코드가 컴파일이 안 되는 경우가 있다. 설정을 일부 바꾸어 컴파일하면 괜찮아진다.


maven의 ant plugin 때문에 아래와 같이 exception이 발생했다.

[ERROR] Failed to execute goal org.apache.maven.plugins:maven-antrun-plugin:1.3:run (copy-base-resource) on project batch_board: An API incompatibility was encountered during configuration of mojo org.apache.maven.plugins:maven-antrun-plugin:1.3:run: java.lang.NoSuchMethodError: org.apache.tools.ant.util.FileUtils.close(Ljava/io/InputStream;)V

 

최신 STS 2.8.1의 경우는 maven install 환경을 보면 3.0.2를 사용중이다.

image

 

미리 설치한 maven 2.2.1로 바꾸어둔다.

image

 

package explorerr 에서 pom.xml 파일에 대한 Run Configruation을 고쳐 2.2가 동작하게 하니 컴파일이 잘 되었다.

image

 

maven3가 maven2와의 backward compatibilty를 완벽히 지원하지 않는다.  없어서 일부 플러그인에서는 약간 문제가 있어서 테스트가 잘할 필요가 있다.

Posted by '김용환'
,

 

시작하며..

리눅스 RAM 디스크에서 자바 컴파일(ant/ivy)하면 얼마나 성능을 향상시킬 수 있을까 고민을 해봤다.
빌드할 때 file io 를 줄이고, jvm 구동시간을 줄이면 어떨까 하는 생각으로 시도해보았다.

램디스크를 쓰면, 일반 하드디스크를 쓰는 것에 비해서 성능이 20배이상 좋다는 블로그 (http://forums.bukkit.org/threads/harddrive-vs-ramdisk-vs-tmpfs-benchmark.2710/) 와  10분 걸렸던 빌드가 5분이면 되었다는 블로그(어딘지 못찾고 있음)을 보았다.

10초 내 외의  빌드 시간이 짧은 프로젝트들이 수백 개가 있는 상황에서 램디스크를 활용하면 속도가 향상될지 테스트해보았따.

 

본론

리눅스에서 RAM 디스크를 사용하는 방법이 두가지이다. ram 디스크과 tmpfs 이다.

 

1. 램디스크 이용하기 (ramdisk)

/dev/ram* 을 이용해서  9M짜리 램디스크 잡아본다. (9216 = 9*1024M)

]# mkfs -t ext3 -q /dev/ram1 9216
]# mkdir -p /ramdisk
]# mount /dev/ram1 /ramdisk
]# df -H | grep ramdisk

]# df -H
Filesystem             Size   Used  Avail Use% Mounted on
/dev/sda1               11G   3.5G   7.0G  34% /
/dev/sda3              278G   2.7G   261G   1% /home
tmpfs                  6.3G      0   6.3G   0% /dev/shm
/dev/ram1              8.2M   1.2M   6.7M  15% /ramdisk

영구히 쓰려면, 커널을 수정 하거나 grub 설정을 변경해야 한다. 많이 귀찮은 작업이 있다. 잘못하면 OS 부팅이 안되고, 함부로 리눅스 서버를 리부팅할 수 없는 상황이라 간단하게 mount 방식을 이용했다.

]# mkdir –p /ramfs

]# mount -t ramfs -o size=1G,mode=777 ramfs /ramfs

 

 

2. tmpfs 를 이용

tmpfs는 임시적으로 파일을 ram을 이용하는 방식이다. 내가 사용하는 centos 5.3 64비트에는 다행히 tmpfs를 지원한다.

]# grep tmpfs /proc/filesystems
nodev   tmpfs

]# mkdir /tmpfs

]# mount -t tmpfs -o size=1G,mode=777 tmpfs /tmpfs

]# df -H
Filesystem             Size   Used  Avail Use% Mounted on
/dev/sda1               11G   3.5G   7.0G  34% /
/dev/sda3              278G   2.7G   261G   1% /home
tmpfs                  6.3G      0   6.3G   0% /dev/shm
/tmpfs                 1.1G      0   1.1G   0% /tmpfs

 

 

3. 비교

빌드에 대한 객관성을 유지하기 위해서 캐쉬가 된 부분을 모두 포함한 것이다. ant와 java는 모두 하나의 파일시스템안에 넣었다. 테스트 결과는 다음과 같다.

  디스크 tmpfs
ram disk (mount방식)
xeon 5148  2.33GHz 4개, ram : 8G A 소스 : 14 s 
B 소스 : 11s
A 소스 : 18 s 
B 소스 : 11s
X
xeon L5640 2.27Ghz] 12개, ram :12G A 소스 : 14 s 
B 소스 : 9s
A 소스 : 14 s 
B 소스 : 9s
A 소스 : 14 s 
B 소스 : 9s

* 특이한 것은 램에서 돌리는 것이 가장 빠른 것은 아닌 것 같았다. 18초가 나오는 경우도 있었다. 
정확한 이유는 알 수 없지만… (어디선가 바틀렉이 있는것 같다.. 잘 찾아봐야지.)

 

마치며..

시간을 체크해보 전체 빌드 시간 중 java 컴파일에 해당되는 시간은 상당히 적었다. 연동이나 ivy 연동부분이나 javascript 작업등으로 인해서 길었던 부분이 있었다. 따라서 file io 이슈나 java 시작 시간은 큰 문제가 아니다. 오히려 컴파일 이슈가 큰 녀석이나 해당되는 것 같았다.

확실히 몇 분 단위의 빌드시간이 되어야 RAM 디스크를 이용 하는게 나을 것 같다. 빌드시간이 10초 내외의 수백개의 프로젝트를 빌드할 때는 병렬처리 및 클러스터링 빌드 방식이 더 나은 것 같다. 

Posted by '김용환'
,

 

시작하며..

0.6.4 버전의 카산드라(Cassandra)를 패치한 0.6.5의 큰 변화는 리눅스 운영체제에 동작하는 java 프로세스가 mmap을 사용하면서 swap 메모리가 증가되는 부분을 막기 위함이다.

아래 내용은 이 글을 이해하기 위한 작은 정보이다.

http://knight76.tistory.com/entry/Swap-메모리
http://knight76.tistory.com/entry/자바-RandomAccessFile-클래스의-map-메서드-내부-구조-분석

 

본론..

<이슈와 결과 내용>

0.6.4버전의 카산드라에서 read 성능이 너무 낮아졌고, swap 메모리가 많아지면서 성능이 떨어졌다고 한다.

(https://issues.apache.org/jira/browse/CASSANDRA-1214)

아래와 같이 성능이 어느 정도 나오지 못하고 들쑥날쑥 하고 있다.

JVM의 full gc 시간이 항상 일어나지 않는다. native heap 의 영역의 메모리가 많아져서 swap memory로 갔다가 gc time 때 swap으로부터 메모리를 읽은 후 gc 데이터를 모두 날리는 작업을 하면서 성능이 떨어지는 것이다.

gc 시점 때 all stop + swap in/out이 일어나면서 계속 성능이 저하되는 현상이 생긴 것이다.
(마치 gc 그래프와 흡사한 성능 그래프가 증명하고 있다.)

0.6.5 버전에서는 0.6.5 버전의 패치를 통해서 13%의 성능 향상이 일어났다.

 

<자세한 내용>

카산드라 0.6.4 버전에서는 SSTable read 성능을 높이기 위해서 NIO를 사용했다. 내부 데이터를 MappedByteBuffer 클래스로 읽는다. 이 때 mmap을 사용한다.

storage-conf.xml 설정 파일의 DisAccessMode 속성의 값은 auto이다.

<DiskAccessMode>auto</DiskAccessMode>

 

DatabaseDescritor 클래스의 static 블럭에서 파일을 읽고 있으며 64비트 머신인 경우에는 mmap이 디폴트값으로 정해진다.

String modeRaw = xmlUtils.getNodeValue("/Storage/DiskAccessMode");
diskAccessMode = DiskAccessMode.valueOf(modeRaw);
if (diskAccessMode == DiskAccessMode.auto) {
          diskAccessMode = System.getProperty("os.arch").contains("64") ?
          DiskAccessMode.mmap
: DiskAccessMode.standard;
          indexAccessMode = diskAccessMode;
}

 

disk access mode 의 값이 standard인 경우는 index buffer 자체가 없기 때문에 큰 성능을 기대할 수는 없지만, mmap 모드인 경우에는 index buffer를 메모리에 사용할 수 있다.

SSTableReader 클래스 생성자 에서는 mmap disk access의 경우 mmap을 사용할 수 있도록 되어 있다.

    long indexLength = new File(indexFilename()).length();
    int bufferCount = 1 + (int) (indexLength / BUFFER_SIZE);
    indexBuffers = new MappedByteBuffer[bufferCount];
    long remaining = indexLength;
    for (int i = 0; i < bufferCount; i++)
    {
        indexBuffers[i] = mmap(indexFilename(), i * BUFFER_SIZE,
                                 (int) Math.min(remaining, BUFFER_SIZE));
        remaining -= BUFFER_SIZE;
    }

 

mmap 메서드는 다음과 같이 정의되어 있다.

private static MappedByteBuffer mmap(String filename, long start, int size) throws IOException
{
    RandomAccessFile raf;
    try
    {
        raf = new RandomAccessFile(filename, "r");
    }
    catch (FileNotFoundException e)
    {
        throw new IOError(e);
    }

    try
    {
        return raf.getChannel().map(FileChannel.MapMode.READ_ONLY, start, size);
    }
    finally
    {
        raf.close();
    }
}


RandromAccessFile 클래스의 map 메서드는 내부적으로 clib의 공유할 수 있는 mmap64 시스템콜을 호출하여 native heap 영역의 메모리를 공유한다. (참고 : http://knight76.tistory.com/entry/자바-RandomAccessFile-클래스의-map-메서드-내부-구조-분석)

 

<버그질라 이슈에 대한 설명>

리눅스 커널의 메모리 관리자는 swap 메모리로 언제든지 데이터를 옮길 수 있다.
리눅스 커널의 vm.swappiness 파라미터가 0 이상이면 메모리가 어느 정도 부족하다고 판단하면 swap 메모리로 옮겨놨다가 필요할 때 램에 적재하게 한다.

mmap을 쓰다보니 native heap 영역에 있는 데이터들의 gc 가 되는 시점이 중요해진다.  java의 heap 영역을 청소하는 full gc 가 호출 될 때 unmap이 호출이 되는 구조로 되어 있다. (참고 : http://knight76.tistory.com/entry/자바-RandomAccessFile-클래스의-map-메서드-내부-구조-분석). unmap이 되는 클래스가 PhantomReference를 상속하였는데, 이 의미는 정말 사용 중이지 않을 때 gc 대상이 된다. (전문 용어로 reachable 하지 않거나 referent 객체로부터 로부터 mark되지 않은 상태). soft reference나 weak reference 보다도 최대한 끝까지 살아남는 놈들이다.  (자바 스펙에는 모호하게 적혀 있고, java.lang.ref api와 소스에서 그 의미를 찾아볼 수 있다.)

full gc가 일어나는 시점에서 본다면, native heap 영역이 가득차거나 java heap 영역이 가득찰 때 일어난다. (사실 정확하게 찬다는 의미보다는 gc가 적당한 비율로 메모리를 찼다고 판단하면 gc한다.) 결국은 많은 메모리를 사용 중에 일어날 수 있다.

리눅스 커널은 mmap으로 할당받은 데이터인 SSTable의 정보를 swap 메모리로 이동한다. 무식한 이 데이터는 full gc에도 차곡차곡 쌓이다가 정말 확실히 gc의 대상이 될 때, gc가 된다.
이 때, gc 시점에 정리를 하는데, swap 영역에 있는 녀석이 gc가 될 수 없다. 즉 swap in(ram으로 불러들임)을 한후, gc가 된다. 만약 swap 영역의 메모리가 ram 메모리의 크기보다 많이 크다면 성능은 전혀 기대할 수 없는 수준으로 내려갈 수 밖에 없다.

따라서, 만약 적당한 크기의 swap 영역을 지정할 필요가 있다.  이 영역을 제대로 잡는 것은 계속적인 테스트를 통해서만 할 수 있다.

이런 부분 때문에 카산드라에서는 swaping 셋팅을 전혀 안하는 것이 최고의 선택(best value)라고 하는 배경이다.

http://wiki.apache.org/cassandra/MemtableThresholds

Virtual Memory and Swap

On a dedicated cassandra machine, the best value for your swap settings is no swap at all -- it's better to have the OS kill the java process (taking the node down but leaving your monitoring, etc. up) than to have the system go into swap death (and become entirely unreachable).

Linux users should understand fully and then consider adjusting the system values for swappiness, overcommit_memory and overcommit_ratio.

 

http://wiki.apache.org/cassandra/FAQ#mmap

Why does top report that Cassandra is using a lot more memory than the Java heap max?

Cassandra uses mmap to do zero-copy reads. That is, we use the operating system's virtual memory system to map the sstable data files into the Cassandra process' address space. This will "use" virtual memory; i.e. address space, and will be reported by tools like top accordingly, but on 64 bit systems virtual address space is effectively unlimited so you should not worry about that.

What matters from the perspective of "memory use" in the sense as it is normally meant, is the amount of data allocated on brk() or mmap'd /dev/zero, which represent real memory used. The key issue is that for a mmap'd file, there is never a need to retain the data resident in physical memory. Thus, whatever you do keep resident in physical memory is essentially just there as a cache, in the same way as normal I/O will cause the kernel page cache to retain data that you read/write.

The difference between normal I/O and mmap() is that in the mmap() case the memory is actually mapped to the process, thus affecting the virtual size as reported by top. The main argument for using mmap() instead of standard I/O is the fact that reading entails just touching memory - in the case of the memory being resident, you just read it - you don't even take a page fault (so no overhead in entering the kernel and doing a semi-context switch). This is covered in more detail here.

 

그럼에도 불구하고, 최근에 릴리즈된 1.0.2소스를 보니 DiskAccessMode의 디폴트값이 auto로 되어 있다. 이는니눅스  64 비트 운영체제를 사용하는 신규 개발자에게는 재앙이 될 수 있다는 얘기도 된다.

1.0.2에서는 약간 구조를 바꾸어서 쓰고 있으며, Table .getDataFileLocation() 호출시 적당한 시점에 System.gc()도 및 unmap를 통해서 메모리가 적당히 있을 수 있도록 수정되었다.  (정확히 말하면, 언제부터 바뀐지는 잘 모르지만, 많이 노력하고 있다는 느낌이다.

 

<mlockall의 사용>

0.6.5에서 바뀐 부분은 mlockall 메서드를 사용할 수 있도록 한 부분이다. 이를 위해서 JNA를 사용하였다. (참조 : http://knight76.tistory.com/entry/jni-vs-jna)

ivy.xml 설정 파일에 jna library가 추가되었다.

<dependency org="net.java.dev.jna" name="jna" rev="3.2.7"/>

ivysettings 설정 파일이 추가되었다.

<ivysettings>
  <settings defaultResolver="ibiblio"/>
  <resolvers>
    <chain name="chain" dual="true">
      <ibiblio name="java.net2" root="http://download.java.net/maven/2/" m2compatible="true"/>
      <ibiblio name="ibiblio" m2compatible="true" />
    </chain>
  </resolvers>
  <modules>
    <module organisation="net.java.dev.jna" name="jna" resolver="chain" />
  </modules>
</ivysettings>

 

CassandraDaemon 클래스의 setup 메서드안에서  FBUtilities.tryMlockall(); 을 호출한다. tryMlockall() 에서는 clib의 mlockAll() 시스템 콜을 호출한다. 즉 처음부터 nio 로 만든 객체들을 swap 메모리로 가지 않게 하고, ram에서만 사용할 수 있도록 바뀐 것이다.


public static void tryMlockall()
{
    int errno = Integer.MIN_VALUE;
    try
    {
       int result = CLibrary.mlockall(CLibrary.MCL_CURRENT);
        if (result != 0)
            errno = Native.getLastError();
    }
    catch (UnsatisfiedLinkError e)
    {
        // this will have already been logged by CLibrary, no need to repeat it
        return;
    }
    catch (Exception e)
    {
        logger_.debug("Unable to mlockall", e);
        // skipping mlockall doesn't seem to be a Big Deal except on Linux.  See CASSANDRA-1214
        if (System.getProperty("os.name").toLowerCase().contains("linux"))
        {
            logger_.warn("Unable to lock JVM memory (" + e.getMessage() + ")."
                         + " This can result in part of the JVM being swapped out, especially with mmapped I/O enabled.");
        }
        else if (!System.getProperty("os.name").toLowerCase().contains("windows"))
        {
            logger_.info("Unable to lock JVM memory: " + e.getMessage());
        }
        return;
    }

    if (errno != Integer.MIN_VALUE)
    {
        if (errno == CLibrary.ENOMEM && System.getProperty("os.name").toLowerCase().contains("linux"))
        {
            logger_.warn("Unable to lock JVM memory (ENOMEM)."
                         + " This can result in part of the JVM being swapped out, especially with mmapped I/O enabled."
                         + " Increase RLIMIT_MEMLOCK or run Cassandra as root.");
        }
        else if (!System.getProperty("os.name").toLowerCase().contains("mac"))
        {
            // OS X allows mlockall to be called, but always returns an error
            logger_.warn("Unknown mlockall error " + errno);
        }
    }
}

 

이런 용도로 CLibrary 클래스가 추가되었다.

public final class CLibrary
{
    private static Logger logger = LoggerFactory.getLogger(CLibrary.class);

    public static final int MCL_CURRENT = 1;
    public static final int MCL_FUTURE = 2;
   
    public static final int ENOMEM = 12;

    static
    {
        try
        {
            Native.register("c");
        }
        catch (NoClassDefFoundError e)
        {
            logger.info("JNA not found. Native methods will be disabled.");
        }
        catch (UnsatisfiedLinkError e)
        {
            logger.info("Unable to link C library. Native methods will be disabled.");
        }
    }

    public static native int mlockall(int flags);

    public static native int munlockall();

    private CLibrary() {}
}

 

참고로 munlockall() 메서드도 native로 바인딩되었지만 1.0.2에서도 실제 사용하지 않고 있다.

 

 

마치며..

2일 동안 카산드라의 파일 시스템쪽이 어떻게 되어 있는지 잘 몰랐는데. 이번 기회에 카산드라의 mmap 이슈와 그 처리방법에 대해서 잘 파악할 수 있었다. 시간이 되면 카산드라 분석해볼까나…

 

 

 

* 참고 내용 : mlock, mlockall (man 페이지를 이용한 정보 처리)


mlock, mlockall, munlock, munlockall 시스템 콜 함수에 대한 정리
"man mlock" 명령어를 사용하면 mlock 에 대한 정보를 확인할 수 있다.

       #include <sys/mman.h>

       int mlock(const void *addr, size_t len);

       int munlock(const void *addr, size_t len);

       int mlockall(int flags);

       int munlockall(void);

man mlock 에 대한 결과값을 확인할 수 있다.

mlock() 또는 mlockall()은 swap 영역으로 메모리 이동이 되지 않도록 RAM 안에 사용된 virtual memory(가상 주소)를 lock 한다.
munlock() 또는 munlockall()은 리눅스 커널 메모리 관리자에 의해서 swap out될 수 있도록 unlock해주는 함수이다.

mlock() 함수는 주소와 길이에 대한 영역을 lock을 개런티한다.
mlockall() 함수는 프로세스에 대한 주소번지 모두를 lock 한다.
코드/데이터/스택/동적라이르리러,사용자 커널데이터/공유메모리/메모리맵 데이터까지모두 lock한다.

mlockall() 의 아규먼트는 int형 타입의 flag 값을 지정하면 된다. 아래 타입 중에 하나만 또는 OR로 사용가능하다.

- MCL_CURRENT (1): 프로세스의 주소 영역으로 매핑된 모든 페이지
- MCL_FUTURE (2) : 앞으로 프로세스의 주소 영역에 매필될 모든 페이지.
               이 옵션을 사용하면 메모리 할당과 같은 시스템 콜(mmap, sbrk, malloc)을 쓰다 RAM 영역을 넘어설때 할당을 해주지 못하게 된다.
               만약 Stack에서 그 일이 발생하면, SIGSEGV signal이 발생하고 stack 영역은 확장을 할 수 없다.

 

Memory locking은 두 개의 주요 어플리케이션에 사용할 수 있다. real time 알고리즘과 보안을 높이 필요로 하는 데이터 처리쪽에 사용할 수 있다.
특히 real time 알고리즘이 필요한 어플리케이션에서는 locking 함으로서, page fault를 원천적으로 봉쇄해서
time-critical section(시간 동기 문제)가 일어나지 않게 할 수 있다.

 

Cassandra 0.6.5~1.0.2 (최신) 까지 mlockall(MCL_CURRENT)을 주어 사용하고 있음. 따라서 언제든지 차곡차곡 쌓이는 일부 메모리는 swap 메모리로 늘어날 수 있음..

Posted by '김용환'
,

 

시작하며..

자바언어에서는 RandomAccessFile 을 이용해서, native heap 영역의 메모리를 얻어올 수 있다. jvm 내의 heap 영역이 아니기 때문에 따로 잡은 메모리 공간을 이용한다. 이를 통해서 자바는 속도를 향상시켜 read/write를 빨리 할 수 있다. java 1.6 update 22 내부 소스를 바탕으로 어떤 구조로 되어 있는지 살펴본다.

 

본론

간단하게 RandomAccessFile 클래스를 이용해서 Channel을 얻어온 후 READ only용으로 메모리 mapping 하는 메서드를 이용해서 MappedByteBuffer 클래스 타입의 인스턴스를 리턴하는 메서드이다.


 

private static MappedByteBuffer mmap(String filename, long start, int size) throws IOException
{
    RandomAccessFile raf;
    try
    {
        raf = new RandomAccessFile(filename, "r");
    }
    catch (FileNotFoundException e)
    {
        throw new IOError(e);
    }

    try
    {
        return raf.getChannel().map(FileChannel.MapMode.READ_ONLY, start, size);
    }
    finally
    {
        raf.close();
    }
}

 


RandomAccess의 getChannel() 메서드는 sun.nio.ch.FileChannelImpl 클래스를 리턴한다.  FileChannelImpl 클래스의 원형은 다음과 같다.

class FileChannelImpl  {

public MappedByteBuffer map(MapMode mode, long position, long size)
    throws IOException {


addr = map0(imode, mapPosition, mapSize);

Unmapper um = new Unmapper(addr, size + pagePosition); // 큰 의미없는 data structure
return Util.newMappedByteBufferr(isize, addr + pagePosition, um);

}

}


 

FileChannelImpl 클래스의 map 메서드에서는 map0() 메서드를 호출하여 주소 번지를 얻어오고, ByteBuffer로 만들어 리턴한다.

map0() 메서드의 원형은 native 로 연결되어 있다.

// Creates a new mapping
private native long map0(int prot, long position, long length)
    throws IOException;

 

이 메서드는 jni 코드인 FileChannelImpl.c 파일로 연결되어 있다. 역시 mmap64 메서드를 사용하고 그 주소값을 리턴하고 있다. 디폴트는 MAP_SHARED이다.

 


JNIEXPORT jlong JNICALL
Java_sun_nio_ch_FileChannelImpl_map0(JNIEnv *env, jobject this,
                     jint prot, jlong off, jlong len)
{
    void *mapAddress = 0;
    jobject fdo = (*env)->GetObjectField(env, this, chan_fd);
    jint fd = fdval(env, fdo);
    int protections = 0;
    int flags = 0;

    if (prot == sun_nio_ch_FileChannelImpl_MAP_RO) {
        protections = PROT_READ;
        flags = MAP_SHARED;
    } else if (prot == sun_nio_ch_FileChannelImpl_MAP_RW) {
        protections = PROT_WRITE | PROT_READ;
        flags = MAP_SHARED;
    } else if (prot == sun_nio_ch_FileChannelImpl_MAP_PV) {
        protections =  PROT_WRITE | PROT_READ;
        flags = MAP_PRIVATE;
    }

    mapAddress = mmap64(
        0,                    /* Let OS decide location */
        len,                  /* Number of bytes to map */
        protections,          /* File permissions */
        flags,                /* Changes are shared */
        fd,                   /* File descriptor of mapped file */
        off);                 /* Offset into file */

    if (mapAddress == MAP_FAILED) {
    if (errno == ENOMEM) {
        JNU_ThrowOutOfMemoryError(env, "Map failed");
        return IOS_THROWN;
    }
    return handle(env, -1, "Map failed");
    }

    return ((jlong) (unsigned long) mapAddress); 
}

 


 

참고로..

다른 함수를 살펴보면, postion0 () 메서드에서는 lseek64를, size0() 메서드에서는 fstat64를, close0() 메서드에서는 close를, release에서는 fcntl를, forces0 메서드에서는 fsync를, unmap0 메서드는 munmap를 쓰는등 파일 관련된 시스템 콜을 사용하고 있다. 

특별히 transfer0은 sendfile 또는 sendfile64 (동적 라이브러리를 이용한 function pointer의 형태)를 사용한다. sendfile64 구현을 안한 리눅스가 있나 보다…

 

주소번지를 받고 나서 메모리 매핑을 하는Util.newMappedByteBufferr(isize, addr + pagePosition, um) 메서드를 살펴본다. Reflection의 Constructor인 directByteBufferConstructor를 이용해서 MappedByteBuffer 클래스를 생성한다.

static MappedByteBuffer newMappedByteBuffer(int size, long addr, Runnable unmapper)


MappedByteBuffer dbb;
if (directByteBufferConstructor == null)
     initDBBConstructor();

dbb = (MappedByteBuffer)directByteBufferConstructor.newInstance(
new Object[] { new Integer(size), new Long(addr), unmapper });

}

private static void initDBBRConstructor() {

Class th = Class.forName("java.nio.DirectByteBufferR"); // java api에는 나오지 않는 녀석
directByteBufferRConstructor = th.getDeclaredConstructor(
                    new Class[] { int.class, long.class,
                              Runnable.class });
directByteBufferRConstructor.setAccessible(true);

}

 



설명도 잘 나와 있다.

class DirectByteBuffer extends DirectByteBuffer implements DirectBuffer  {
..

// For memory-mapped buffers -- invoked by FileChannelImpl via reflection
    //
    protected DirectByteBufferR(int cap, long addr, FileDescriptor fd, Runnable unmapper)   {
        super(cap, addr, fd, unmapper);
   }


// For memory-mapped buffers -- invoked by FileChannelImpl via reflection
//
protected DirectByteBuffer(int cap, long addr, FileDescriptor fd, Runnable unmapper){
    super(-1, 0, cap, cap, fd);
    address = addr;
    viewedBuffer = null;
    cleaner = Cleaner.create(this, unmapper);

}
..
}


 

DirectByteBufferR 클래스는 래퍼류로서 DirectByteBuffer를 상속받았기 때문에 생성자 레벨에서 편하게 가공할 수 있다. DirectByteBuffer클래스는 MappedByteBuffer를 상속받은 클래스이기 때문에 MappedByteBuffer Casting 이 가능하다.

 

참고로.. Cleaner 클래스는 PhantomReference 를 상속해서 쓰고 있다. GC를 편리하게 하기 위함으로 추가할 때마다 내부적으로 linked list로 객체를 만들어 정리될 때 Runnable로 들어온 객체의 run() 메서드를 호출한다.

public class sun.misc.Cleaner extends java.lang.ref.PhantomReference ..

 

private static class Unmapper
    implements Runnable
{

    private long address;
    private long size;

    private Unmapper(long address, long size) {
        assert (address != 0);
        this.address = address;
        this.size = size;
    }

    public void run() { // 메모리가 부족할 때 종료되게 함.. PhantomReference 임.
        if (address == 0)
            return;
        unmap0(address, size);
        address = 0;
    }

}



unmap0 메서드를 호출하여 항상 gc가 되게 한다.

 

 

마치며..

자바에서 nio map 사용시 리눅스 mmap 시스템 콜에 의해서 매핑된 데이터를 처리 하는 로직을 살펴보았다. 자연스럽게 생명주기도 살펴보았다.

다음에는 Cassandra의 Linux swap 이슈에 대해서 살펴봐야지..  mmap이슈가 있었던 내용을 쓰면서 정리하고자 한다.

Posted by '김용환'
,

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 '김용환'
,

최근에 제가 간단한 프로젝트를 하고 있던 중에. UI 개발이 필요한 부분이 있었습니다.

 

웹 UI 로 하기에는 귀찮고 빠른 개발을 필요로 했기 때문에 때문에 java와 eclipse 기반의 UI builder 종결자가 필요했었습니다. 직접 사용해보니. 확실히 20-30% 정도 생산성이 늘어난 것 같습니다.

 

StackOverflow에 보니 WindowBuilder Pro 이클립스 플러그인이 java UI Builder 라고 추천이 되어 있어서 사용하게 되었습니다. 혹시나 저처럼 빠름 개발을 위해서 또는 데모를 위해서 UI 개발이 필요한 사람에게 도움이 될까 해서 내용을 작성해 봅니다.

 

WindowBuilder Pro는 구글이 내놓은 구글 자바 개발 툴 4개 중의 하나입니다.

(나머지 3개는 WindowTester Pro, CodePro AnalyiX, Goolge web toolkit 입니다.)

 

저는 기존의 Swing/AWT를 개발 경험이 있었던 지라, 학습은 2-3시간 정도만 했더니 쉽게 이해할 수 있었습니다.

UI 개념을 이해한 상태라면, 빨리 익숙해질 수 있는 UI 를 가지고 있습니다.

 

단순히 Swing/AWT 뿐 아니라, GWT, Swing, SWT UI를 제공하고 있습니다. 따라서 이클립스 플러그인의 UI 화면을 설계할 수 있지요. 구글이 정리해주니까 좋긴 하네요. 전에는 이런 툴을 10불 주고 샀었습니다. ^^;;'

 

 

 

 

큰 기능을 먼저 말씀드리면, UI와 소스 뷰를 동시에 제공하기 때문에 design된 화면과 소스를 동시에 전환하면서 개발할 수 있습니다. UI component를 붙이면 자동으로 소스가 생성하고, 그 생성된 소스를 수정하면 바로 desing UI에 반영하는 형태를 가지고 있습니다.

 

또한자바의 ResourceBundle 클래스를 이용해서   i18n(internationlization)을 제공합니다.

 

 

 

 

코드에 팩토리 패턴을 추가할 수 있습니다.

 

 

 

그리고, 제가 좋아했던 이벤트 핸드러를 쉽게 지정할 수 있습니다.

button에 더블 클릭하면 ActionListener를 상속받은 anonymous class가 생성되고 바로 이벤트 처리를 할 수 있는 코드가 생성됩니다.

 

마우스 오른쪽 UI를 통해서면다양한  event를 쉽게 넣을 수 있습니다. 기존에는 이런 api들을 일일이 알고 습득하고 시행착오를 겪으면서 사용했어야 할 api들이 바로 개발툴을 통해서 쉽게 개발이 가능하게 됩니다.

 

 

 

 

메뉴 추가도 그리 어렵지 않습니다. drop&drag 형태로 추가를 할 수 있습니다.

 

 

 

깜짝 놀랜 점은 Layout이 잘 지원하고 있다는 것입니다. 이 부분은 SWT, Swing, GWT 의 Layout을 잘 알고 있을 때에만 쉽게 쓸 수 있다는 한계가 있긴 하지만. 상당히 도움이 되고 있습니다.

Swing의 경우는 FlowLayout이 디폴트 layout이지만.. 빠른 개발을 위해서 Null Layout(Absolute Layout)을 사용하면 진짜 빨리 개발이 가능합니다. 제가 이렇게 해서 좀 덕을 봤습니다.

 

 

 

 

 

 

 

 

Google Web ToolKit과 연동이 쉽습니다.

GWT Application의 테스트 케이스, 어플 실행, 이미지 번들, 리모트 서비스등 다양한 연동이 가능합니다.

 

 

 

UI  적인 요소 외에 Data Binding을 지원합니다. Model-View를 bind해주는 기능을 Swing, JFace, SWT, EMF에 지원하여 좀더 멋진 코디가 가능하게 됩니다.

 

 

 

이런 Design 툴의 큰 약점은 바로 소스 generation시 상당히 지저분 코드로 나온다는 점입니다.

Visaul cafe라던가 Borland JBuilder를 써보신 분은 아마도 치를 떨었던 부분이지요. 특히나 JBuilder는 custom java component가 추가되면서 상당히 불편한 점이 있었습니다.

 

이런 부분에 대해서 고민했는지. WindowBuilder Pro 에서는 Preference를 제공합니다.

 

 

 

코드가 생성될 때, 어떤 코드로 만들어질지를 선택할 수 있습니다.  디플트가 아마도 block 모드라서 처음에는 지저분할 수 있는데, flat모드로 바꾸면 됩니다.  그렇다고 해서 아주 깨끗한 코드는 만들어지지는 않습니다.

다만 주관적인 특성상, 기존 망했던 제품보다는 훨씬 나아보입니다.

 

 

flat 모드

 

block 모드

 

 

flat mode

 

block mode

 

 

마지막으로 이 제품은 튜토리얼이나 시작 문서가 있으니. 대충 써보면 감을 잡고 쉽게 사용가능합니다.

http://code.google.com/intl/ko-KR/javadevtools/wbpro/quick_start.html

http://code.google.com/intl/ko-KR/javadevtools/wbpro/tutorials/index.html

Posted by '김용환'
,

oscache 를 사용할 때 자주 실수하는 점은 총 3가지이다. 



1. cache 크기로 인한 OOME 발생

os cache는 ehcache와 달리 개수로만 cache한다. 만약 cache에 들어갈 entity의 크기는 10,000개인데, 이 entity의 크기가 확 커질 수 있다.  ibatis 설정의 oscache 또는 cron과 함께 쓰는 oscache쪽을 잘 살펴봐야 한다.

* Heap 메모리 크기 변화



프로파일링 하면서 oscache 문제 확인, 크기를 줄이니 OOME 문제는 사라졌다.




2.  모든 쓰레드 Wait 현상 (마치 deadlock처럼 느껴짐)

아래와 같은 코드는 oscache는 모든 쓰레드의 blocking을 일어나게 하고, 웹 서비스가 중단되게 할 수 있다.

       try{

              cacheData = admin.getFromCache(key, cacheTime);

       } catch (NeedsRefreshException e) {

              throw e;

       } 


oscache getXXX 메소드를 호출하면 wait 메소드를 호출하고 데이터가 들어올 때까지 기다린다. 데이터가 들어와서  putXX 메소드가 호출되면, notify 메소드가 호출되고 데이터를 전달하는 구조이기 때문에

이는 수많은 리퀘스트가 동시에 요청하는 웹에 대해서는 문제를 야기할 수 있다. 

응답을 처리하는 것보다 요청하는 것이 급격하게 많은 경우에 쓰레드가 모두 소진되어 쓸 수 없는 상황까지 갈 수 있다또한 따라서, cancelupdate 메소드를 호출하면서 wait된 thread를 빨리 notify해야 한다.

       try{

             cacheData = admin.getFromCache(key, cacheTime);

catch (NeedsRefreshException e) {
admin.cancelUpdate(key);

throw e;

}


그러나, 소스를 변경한다 해도 원론적인 문제를 해결할 수 없을 수 있음. 만약 1000개의 리퀘스트가 한번에 들어온다고 가정했을때.. deadlock은 일어날 수 있다.

따라서 해결 방법은 다른 cache를 써야 한다. ehcache를 사용하는 것이 유일한 해법인 것 같다.


3. 원하지 않는 데이터 얻어옴

   try{

             cacheData = admin.getFromCache(key, cacheTime);

catch (NeedsRefreshException e) {
bo.getdata(); // throw RuntimeException
admin.cancelUpdate(key);

throw e;

}
 

2번과 연관이 있는 코드로.. BO 비지니스가 cancelUpdate 보다 먼저 있다. 만약  bo.getdata() 호출시 RuntimeException이 발생되었다. 그렇다면, cancelUpdate() 메소드는 전혀 호출되지 않으며 원하지 않는 결과가 일어날 수 있다. 

따라서, 다음과 같은 코드로 변경되어야 한다.

        try{

             cacheData = admin.getFromCache(key, cacheTime);

catch (NeedsRefreshException e) {
try {
  bo.getdata(); // throw RuntimeException
} catch (RuntimeException re) {
   log.error(re);
}
admin.cancelUpdate(key);

throw e;

}



또는 finally를 이용한 처리도 괜찮다.

       try{

             cacheData = admin.getFromCache(key, cacheTime);

catch (NeedsRefreshException e) {
bo.getdata(); // throw RuntimeException


} finally {
admin.cancelUpdate(key);

             }



결론은 oscache보다는 ehcache이다.  

1. API 구조가 안좋다. wait/notify 구조로 인해서 오해할 수 있는 api들이 존재. ehcache는 api 깔끔
2. oscache는 간단히 쓸 수 있지만, 크기만 지정가능하다. ehcache는 크기, 알고리즘, 용량, 살아있을 수 있는 기간까지 넣을 수 있다.




 
Posted by '김용환'
,








[예제 프로젝트(maven)]


(데몬 실행)
maven clean compile jetty:run





[PPT 설명]

특징
- REST를 쓰기 위해서 Spring MVC 모델을 그대로 차용, Annotation 이용(Controller)
- JSR 311을 따르지는 않지만, 대부분의 기능 구현 (기존의 코드를 최대한 재사용)
- 하나의 리소스는 여러 개의 Represenation을 가질 수 있도록 함 (JSON/XML/ATOM/RSS)
- 브라우저에서 지원하지 않는 PUT & POST 요청을 처리할 수 있음
- Custom parser 이용 가능
- UTIL(converter..) 클래스 지원
=> REST 관련 Conception만 조금 공부하면 되고, 나머지는 기존 MVC만 알면 되기 까닭에 재사용성이 큼




Annotation 지원 
@Controller : MVC
@RequestMapping : HTTP 메소드, URI, 헤더 처리@RequestMapping(method=RequestMethod.GET, value="/members", 
@PathVariable : 메소드 안에서 파라미터와 매핑하기 위해서 사용public ModelAndView getEmployee(@PathVariable String id) { … } 
@RequestParam : URL 매개변수 이용
@RequestHeader
@RequestBody
@HttpEntity<T>
@ResponseEntity<T> : 정의한대로 response 리턴
@ResponseStatus 
@ExceptionHandler 

public @ResponseBody Employee getEmployeeBy(@RequestParam("name") String name, @RequestHeader("Accept") String accept, @RequestBody String body) {…} 
public ResponseEntity<String> method(HttpEntity<String> entity) {…} 







- @Controller

@Controller
@RequestMapping("/restful")
public class RestfulController {

@RequestMapping(value = "/message/{name}", method = RequestMethod.GET)
public String getMessage(@PathVariable String name, ModelMap model) {
model.addAttribute("message", name);
return "list";
}
}




- Request Parameter Type 
Strong type 형태로 파라미터를 받는다. 

@Controller
@RequestMapping("/restful")
public class RestfulController {

@RequestMapping(value = "/message/{name}", method = RequestMethod.GET)
public String getMessage(@PathVariable String name, ModelMap model) {
model.addAttribute("message", name);
return "list";
}
}





- @PathVariable 
 path를 받는 Annotation

@Controller
@RequestMapping("/restful")
public class RestfulController {

@RequestMapping(value = "/message/{name}", method = RequestMethod.GET)
public String getMessage(@PathVariable String name, ModelMap model) {
model.addAttribute("message", name);
return "list";
}
}




@ResponseBody
Response를 xml형태로 보여줄 수 있다.

@Controller
@RequestMapping("/restful")
public class RestfulController {

@RequestMapping(value = "/messagebody/{message}", method = RequestMethod.GET)
@ResponseBody
public Body getMessageBody(@PathVariable String message, ModelMap model) {
Body body = new Body();
body.setMessage(message);
return body;
}
}




@XmlRootElement(name = "body")
public class Body {
@XmlElement
private String msg;

public String getMessage() {
   return msg;
}

public void setMessage(String message) {
    this.msg = message;
}
}




@ResponseStatus
만약 에러가 나면 특정 Http Status 정보를 response로 보낼 수 있다.


@RequestMapping(value = "/exception", method = RequestMethod.GET)
public String throwException() {
    throw new ResourceNotFoundException();
}


@ExceptionHandler(ResourceNotFoundException.class)
@ResponseStatus(value = HttpStatus.BAD_GATEWAY) 
public void handleNotFoundException(ResourceNotFoundException ex, HttpServletRequest request) {
     System.out.println("handleNotFoundException:" + ex);
}

…..

class ResourceNotFoundException extends RuntimeException { 


<실행결과>


$ curl -i  localhost:8080/restful/exception
HTTP/1.1 502 Bad Gateway
Content-Length: 0
Server: Jetty(6.1.26)




- Rest 방식과 기존 파라미터 요청 방식 을 같이 사용 가능?? 
사용가능하다. 

@RequestMapping(value = "/test/{name}/id/{id}", method = RequestMethod.GET)
public String getString(@PathVariable String name, @PathVariable int id,                                String email, ModelMap model) {
    model.addAttribute("message", name);
    model.addAttribute("id", id);
    model.addAttribute("email", email);
    return "test";
}




// test.jsp
<html>
<head></head>
<body>
<h1>Test</h1>
<h3>message : ${message}</h3>
<h3>email : ${email}</h3>
</body>
</html>


실행결과



$ curl http://localhost:8080/restful/test/jaja/id/11?email=aaa@google.com
<html>
<head></head>
<body>
        <h1>Test</h1>
        <h3>message : jaja</h3>
        <h3>email : aaa@google.com</h3>
</body>
</html>



- json이나 xml payload 요청하기

Spring MVC가  알아서 deserialization을 해줌!!

@Controller

@RequestMapping("/comm")

@ResponseStatus(value = HttpStatus.ACCEPTED)

public class JsonRequestController {

    @RequestMapping(method = RequestMethod.GET) 

    @ResponseStatus(value=HttpStatus.FOUND)

    public void sendFruitMessage(@RequestBody Message name) {

        System.out.println("name : " + name.getName());

        return ;

    }

}


 


@XmlRootElement(name = "message")

public class Message {

       @XmlElement

private String name;

public String getName() {

return name;

}

      public void setName(String name) {

this.name = name;

     }

}



<테스트>

XML Payload



<클라이언트>

$ curl -i -H "Content-Type: application/xml" -X get -d '<message><name>Kim Yong Hwan</name></message>' localhost:8080/comm

HTTP/1.1 302 Found

Content-Length: 0

Server: Jetty(6.1.26)


<서버>

name : Kim Yong Hwan


 
JSon Payload


<클라이언트>

$ curl  -i -H "Content-Type: application/json" -X get -d '{"name":"Kim Yong Hwan"}' localhost:8080/comm

HTTP/1.1 302 Found

Content-Length: 0

Server: Jetty(6.1.26)


<서버>

name : Kim Yong Hwan





* 실제 테스트 코드

web.xml

<web-app id="WebApp_ID" version="2.4"

xmlns="http://java.sun.com/xml/ns/j2ee" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee 

http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd">


<display-name>Spring Web MVC Rest Demo Application</display-name>


<servlet>

    <servlet-name>mvc-dispatcher</servlet-name>

    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>

    <load-on-startup>1</load-on-startup>

</servlet>


<servlet-mapping>

    <servlet-name>mvc-dispatcher</servlet-name>

    <url-pattern>/</url-pattern>

</servlet-mapping>


<context-param>

    <param-name>contextConfigLocation</param-name>

    <param-value>/WEB-INF/mvc-dispatcher-servlet.xml</param-value>

</context-param>


<listener>

    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>

</listener>


</web-app>





mvc-dispatcher-servlet.xml

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:context="http://www.springframework.org/schema/context"

xmlns:mvc="http://www.springframework.org/schema/mvc" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

xsi:schemaLocation="

        http://www.springframework.org/schema/beans     

        http://www.springframework.org/schema/beans/spring-beans-3.0.xsd

        http://www.springframework.org/schema/context 

        http://www.springframework.org/schema/context/spring-context-3.0.xsd

        http://www.springframework.org/schema/mvc

        http://www.springframework.org/schema/mvc/spring-mvc-3.0.xsd">


<context:component-scan base-package="com.google.controller" />


<mvc:annotation-driven />


<bean

class="org.springframework.web.servlet.view.InternalResourceViewResolver">

    <property name="prefix" value="/WEB-INF/pages/" />

    <property name="suffix" value=".jsp" />

</bean>


</beans>





RestfulController.java

@Controller

@RequestMapping("/restful")

public class RestfulController {


@RequestMapping(value = "/message/{name}", method = RequestMethod.GET)

public String getMessage(@PathVariable String name, ModelMap model) {

model.addAttribute("message", name);

return "list";

}


@RequestMapping(value = "/command/{id}/content/{content}", method = RequestMethod.GET)

public String getCommand(@PathVariable("id") String id, @PathVariable("content") long content, ModelMap model) {

model.addAttribute("id", id);

model.addAttribute("content", content);

return "command";

}


@RequestMapping(value = "/link/{id}", method = RequestMethod.DELETE)

public String deleteLink(@PathVariable("id") String id, ModelMap model) {

model.addAttribute("id", id);

return "delete";

}

}




// WEB-INF/pages/list.jsp

<html>

<body>

<h1>Spring MVC Restful Test</h1>

<h3>message : ${message}</h3>

</body>

</html>




// WEB-INF/pages/command.jsp

<html>

<body>

<h1>Spring MVC Restful Test</h1>

    <h3>command id : ${id}</h3>

<h3>content : ${content}</h3>

</body>

</html>




// WEB-INF/pages/delete.jsp

<html>

<body>

<h1>Spring MVC Restful Test</h1>

<h3>deleted id : ${id}</h3>

</body>

</html>




테스트결과
자세한 것은 PPT 또는 프로젝트로 확인


웹콜 테스트

http://localhost:8080/restful/message/reset
http://localhost:8080/restful/command/aa/content/111

시그윈 또는 linux 테스트
curl -X DELETE http://localhost:8080/restful/link/1
curl -X DELETE http://localhost:8080/restful/message/3









Content Negotiation
- HTTP 1.1 스펙에서 정의
- 의미
   media type, 언어, 문자집합, 인코딩 등에 대해 브라우저가 제공한 선호도에 따라 자원의 가장 적합한 표현을 선택. 불완전한 협상 정보를 보내는 브라우저의 요청을 지능적으로 처리하는 기능
- 일반적으로 다른 프로토콜을 쓰려면, request의 “type” 파라미터를 확인하고, 이에 맞는 marshalling 정보를 전달해야 한다.
- 트위터 API가 이렇게 사용되고 있음
- Spring MVC에서는 한번에 해결해주는 클래스 사용 ContentNegotiatingViewResolver
- 요청 데이터 포맷에 맞춰 다양한 MIME(미디어 타입) 이나 content type으로 전달 가능
ATOM, RSS, XML, JSON
예)
http://localhost:8080/fruit/banana.xml
http://localhost:8080/fruit/banana.rss
http://localhost:8080/fruit/banana.html

Accept : application/xml
Accept : application/json
Accept : application/html


- 코드

FruitController.java

@Controller

@RequestMapping("/fruit")

public class FruitController {


    @RequestMapping(value="{fruitName}", method = RequestMethod.GET)

    public String getFruit(@PathVariable String fruitName, ModelMap model) {

    Fruit fruit = new Fruit(fruitName, 1000);

    model.addAttribute("model", fruit);

    return "listfruit";

    }

}




 
Fruit.java


@XmlRootElement(name = "fruit")
public class Fruit {
String name;
int quality;

public Fruit() {} 
  
public Fruit(String name, int quality) {
    this.name = name;
    this.quality = quality;
}
 
@XmlElement
public void setName(String name) {
this.name = name;
}
  
@XmlElement
public void setQuality(int quality) {
this.quality = quality;
}
}



mvc-dispatcher-servlet.xml


<bean class="org.springframework.web.servlet.view.ContentNegotiatingViewResolver">
  <property name="order" value="1" />
  <property name="mediaTypes">
<map>
   <entry key="json" value="application/json" />
   <entry key="xml" value="application/xml" />
   <entry key="rss" value="application/rss+xml" />
</map>
  </property>
 
  <property name="defaultViews">
<list>
  <!-- JSON View -->
  <bean
class="org.springframework.web.servlet.view.json.MappingJacksonJsonView">
  </bean>
 
  <!-- RSS View -->
  <bean class="com.google.rss.RssFeedView" />
 
  <!-- JAXB XML View -->
  <bean class="org.springframework.web.servlet.view.xml.MarshallingView">
<constructor-arg>
<bean class="org.springframework.oxm.jaxb.Jaxb2Marshaller">
   <property name="classesToBeBound">
<list>
   <value>com.google.bean.Fruit</value>
</list>
   </property>
</bean>
</constructor-arg>
  </bean>
 </list>
  </property>
  <property name="ignoreAcceptHeader" value="true" />
 
</bean>
 
<bean
class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<property name="order" value="2" />
<property name="prefix" value="/WEB-INF/pages/" />
<property name="suffix" value=".jsp" />
</bean>
 



결과

$ curl -H 'Accept: application/xml' localhost:8080/fruit/banana.xml

<?xml version="1.0" encoding="UTF-8" standalone="yes"?><fruit><name>banana</name

><quality>1000</quality></fruit>



 

$ curl -H 'Accept: application/rss' localhost:8080/fruit/banana.rss

<?xml version="1.0" encoding="UTF-8"?>

<rss xmlns:content="http://purl.org/rss/1.0/modules/content/" version="2.0">

  <channel>

    <title>Sample Title</title>

    <link>http://google.com</link>

    <description>Sample Description</description>

    <item>

      <link>http://www.google.com</link>

      <content:encoded>banana1000</content:encoded>

      <author>Test</author>

    </item>

  </channel>

</rss>




$ curl -H 'Accept: application/json' localhost:8080/fruit/banana.json

{"model":{"quality":1000,"name":"banana"}}










웹브라우져에서는 PUT/DELETE를 쓸 수 없다. 

- HTML version 4 과 XHTML 1에서는 HTML  form안의 HTTP 요청은 GET과 POST 방식만 허용. 그동안 put/delete 메소드를 사용하려면, XMLHttpRequest를 이용하였음
- HTTP상에서는 ok! 
- HTML5에서 put/delete는 사용하지 못한다고 나와 있음
    http://www.w3.org/TR/html5-diff/
     Using PUT and DELETE as HTTP methods for the form element is no longer supported.

Spring MVC는 이 것을 가능하게 함

=> 자세한 것은 PPT 참조





클라이언트 API - RestTemplate
기존의 JDBC Template과 비슷.

Map<String, String> vars = new HashMap<String, String>();

vars.put("id", "111");

vars.put("content", "222");

RestTemplate restTemplate = new RestTemplate();

String result = restTemplate.getForObject("http://localhost:8080/restful/command/{id}/content/{content}", String.class, vars);

System.out.println("result : " + result);



결과
 

result : <html>

<body>

<h1>Spring MVC Restful Test</h1>

    <h3>command id : 111</h3>

<h3>content : 222</h3>

</body>

</html>






좋은 자료(레퍼런스)
http://static.springsource.org/spring/docs/3.0.x/spring-framework-reference/html/mvc.html
http://dev.anyframejava.org/docs/anyframe/plugin/restweb/1.0.1/reference/html/index.html
http://www.mkyong.com/spring-mvc/spring-3-mvc-contentnegotiatingviewresolver-example/
http://www.ibm.com/developerworks/web/library/wa-spring3webserv/index.html
http://blog.springsource.com/2010/01/25/ajax-simplifications-in-spring-3-0/












Posted by '김용환'
,