iOS7 베타를 깔기위해 오랜만에 iTunes를 켰다가 들뜬맘에 얼떨결에 퀵타임을 설치해버렸다.

이미 설치한거 지우기도 귀찮아서 내버려뒀는데, PC 재부팅후 이클립스를 실행하니

멀쩡하던 프로젝트가 에러를 뿜어내고 있는 것 아닌가?

"QTJava? 뭐 이런 퀵타임스런 이름이 jre 에 있지???"했는데 진짜 퀵타임이 설치한 라이브러리임.. OMG!

근데 에러가 난 이유는 실제론 QTJava.zip 이 저기 나온 경로엔 없고 퀵타임 설치경로에 있기 때문이다.

다만, 퀵타임이 클래스패스까지 손을 대는 바람에... 결국 저런 참사가 벌어진 것이다.

맘대로 클래스패스 수정 할거면 제대로나 하지!


아무튼 구글링 좀 해봤더니...

아니 내가 초짜라니!!(←초짜 맞음)


아무튼 난 동영상 다룰일 없으니 QTJava 를 클래스패스에서 싹 지워버린 후

블로그에 글까지 남기게 한 이 사태에 대한 책임을 물어 퀵타임을 내 PC에서도 삭제 조치 하였다.훗.

리플렉션을 이용하면 다른 클래스의 메서드 불러(invoke) 볼 수 있습니다. iBatis 로 데이터베이스에서 조회한 결과를 객체에 맵핑 할 때, 담을 객체만 정의해줘도 같은 필드명을 가진 속성에 들어가는 것도 이 방법을 이용한 것 입니다.(속도 문제로 현재는 다른 방법을 쓴다네요.) 

일반적인 자바 프로그래밍이 할 수 없는 것을 할 수 있게 해주는 리플렉션이지만 흔히 성능이 나쁘다고 알려져있어 도대체 얼마나 느린지 테스트를 해봤습니다. 추가로 리플렉션 성능을 향상시키기 위해 동적으로 생성한 바이트코드를 사용하는 ReflectASM 와도 비교해 봤습니다.

1. 결과

chartgo (4)

리플렉션 함부로 쓰면 큰일나겠네요! (이미지를 클릭하면 500M 까지의 테스트 결과를 더 볼 수 있습니다.)

 

2. 테스트 코드

자바에서 엑셀파일을 읽는데 POI와 JXL중 어느것이 더 성능이 나을까?

테스트를 위해 Excel 2010에서 sheet 1에 A열부터 Z열까지 각 셀에 다양한 언어로 10~30글자씩 입력하고 65,536행을 모두 같은 내용으로 채워 test.xls 로 저장했다.

테스트코드 및 결과는 다음과 같다.

테스트코드


* POI, JXL 을 java build path 에 추가해야 한다. 구글링하면 다운로드 받을 수 있는 경로가 나오므로 셋팅방법을 별도로 설명하지는 않는다.

결과

 

elapsed(sec)

JXL

3.48

POI(xls)

4.43

POI(xlsx)

28.43

 
* JXL 이 가장 빠른 속도로 내용을 읽어 왔다.
* 같은 문서를 xlsx 형식으로 저장했을때, POI의 읽기 속도는 JXL보다 
8.18배 느리다.
  1. 초보개발자 2012.02.28 12:03

    안녕하세요 질문하나 해도 될까요?
    poi 로 작업 중인데
    20만건이 넘는 엑셀 파일을 읽으려고 하고 있습니다.
    그런데 위에 선언한것 처럼 65536건 밖에 못 읽더군요
    이걸 해결 할 방법이 어떤게 있는지 알수 있을까요?

    • yjacket 2012.03.05 15:29 신고

      안녕하세요?
      xls 포맷으로는 한 시트에 65,536건밖에 입력 못합니다.
      이를 초과 할 데이터를 저장해야 할 때 여러 시트에 분리해 넣는 방법 등을 사용하긴 합니다. (불편 할 수 있지만 속도면에서 유리합니다.)

      반면 POI를 이용하면 xlsx포맷을 이용 할 수 있기 때문에 65,536건 이상의 데이터도 저장 할 수 있습니다. 정확하게 얼마나 많은 건을 저장 할 수 있는지 확인해보진 않았지만 백만건정도는 된다고 하는것 같네요.

자바에서 엑셀파일을 쓰는데 POI와 JXL중 어느것이 더 성능이 나을까?

테스트코드 및 결과는 다음과 같다.

테스트코드


* POI, JXL 을 java build path 에 추가해야 한다. 구글링하면 다운로드 받을 수 있는 경로가 나오므로 셋팅방법을 별도로 설명하지는 않는다.
* StopWatch 클래스는 성능측정을 위해 임의로 작성한 클래스이므로 해당 클래스가 없다면 이 코드는 컴파일 되지 않는다. 해당 클래스를 작성하던지 아니면 해당 구문을 제거하고 컴파일 할 것

결과

 

10,000 rows

65,536 rows

elapsed(sec)

filesize(KB)

elapsed(sec)

filesize(KB)

JXL

                   0.28

                  942

                  1.39

             6,113

POI(xls)

                   0.98

              2,043

               27.07

          13,371

POI(xlsx)

                   6.68

                  276

             257.20

            1,775


* 속도는 JXL 이 가장 빠르고, 파일 크기는 POI(xlsx)가 가장 작다.
* 10,000 행 테스트와 65,536 행 테스트간 처리속도시간는 처리량이 6.55배 증가한 것에 비해 JXL은 4.96배 밖에 증가 하지 않았지만, POI는 xls가 27배, xlsx가 38배나 증가했다.
* POI 는 xls 형식으로 대량의 행을 저장하기에는 속도 및 용량효율측면에서 모두 JXL에 상당히 뒤쳐지지만, xlsx 형식으로 저장시 파일 용량을 JXL로 작성된 xls의 30% 수준으로 줄일 수 있다.
테스트 목적
Java 의 Connection 과 Oracle 의 session 간의 관계 확인

가정
IBATIS 설정 MaximumActiveConnections 까지 점차적으로 Connection 수를 증가시키면
매 증가시 마다 오라클 session 수가 늘어날 것이다.

실험

1. 연결 유지를 위한 무한루프 프로시저

CREATE OR REPLACE PROCEDURE SP_LOOP
is
begin    
    loop 
        null;
    end loop;
end;



2. SQLMap 

<sqlMap>
    <procedure id="loop">
        {call sp_loop}
    </procedure>
</sqlMap>

 
3. 5초마다 하나씩 커넥션을 늘리고, 연결정보를 출력하는 자바 어플리케이션

import java.sql.SQLException;

import com.ibatis.common.jdbc.SimpleDataSource;
import com.ibatis.sqlmap.client.SqlMapClient;

public class ConnectionTest
{
public static void main(String[] args) throws InterruptedException
{
init();
int i = 0;
while (true)
{
Thread t = new Thread(new Runnable()
{
@Override
public void run()
{
try
{
sqlMapClient.queryForList("loop");
}
catch (SQLException e)
{
e.printStackTrace();
}
}
});
System.out.println("take " + i++);
t.start();

System.out.println(((SimpleDataSource) sqlMapClient.getDataSource()).getStatus());

Thread.sleep(5000);
}
}
private static SqlMapClient sqlMapClient = null;
private static void init()
{
// IBATIS 초기화(생략)
}
}


4. 결과

테스트 어플리케이션이 실행되면 매 5초 마다 새로운 커넥션이 생성되므로
오라클에서 다음 쿼리를 이용해 session 수의 증가를 확인

SELECT status, count(*) 
FROM v$session where username='test' AND program='JDBC Thin Client' 
GROUP BY status

 
그리고 자바 어플리케이션을 실행하면 다음과 같은 로그가 나타난다.


take 0

===============================================================
 jdbcDriver                     oracle.jdbc.OracleDriver
 jdbcUrl                        jdbc:oracle:thin:test/************@127.0.0.1:1521:xe
 jdbcUsername                   test
 jdbcPassword                   ************
 poolMaxActiveConnections       50
 poolMaxIdleConnections         10
 poolMaxCheckoutTime            120000
 poolTimeToWait                 300
 poolPingEnabled                false
 poolPingQuery                  select 1 from dual
 poolPingConnectionsOlderThan   1
 poolPingConnectionsNotUsedFor  1
 --------------------------------------------------------------
 activeConnections              0
 idleConnections                0
 requestCount                   0
 averageRequestTime             0
 averageCheckoutTime            0
 claimedOverdue                 0
 averageOverdueCheckoutTime     0
 hadToWait                      0
 averageWaitTime                0
 badConnectionCount             0
===============================================================
DEBUG [00:08:40.544] {Thread-1} (Log4jImpl.java:26) Created connection 2040402337.
DEBUG [00:08:40.550] {Thread-1} (Log4jImpl.java:26) {conn-100000} Connection
DEBUG [00:08:40.554] {Thread-1} (Log4jImpl.java:26) {conn-100000} Preparing Call: {call sp_loop}
DEBUG [00:08:40.679] {Thread-1} (Log4jImpl.java:26) {pstm-100001} Executing Statement: {call sp_loop}
DEBUG [00:08:40.680] {Thread-1} (Log4jImpl.java:26) {pstm-100001} Parameters: []
DEBUG [00:08:40.680] {Thread-1} (Log4jImpl.java:26) {pstm-100001} Types: []

take 1

===============================================================
 jdbcDriver                     oracle.jdbc.OracleDriver
 jdbcUrl                        jdbc:oracle:thin:test/************@127.0.0.1:1521:xe
 jdbcUsername                   test
 jdbcPassword                   ************
 poolMaxActiveConnections       50
 poolMaxIdleConnections         10
 poolMaxCheckoutTime            120000
 poolTimeToWait                 300
 poolPingEnabled                false
 poolPingQuery                  select 1 from dual
 poolPingConnectionsOlderThan   1
 poolPingConnectionsNotUsedFor  1
 --------------------------------------------------------------
 activeConnections              1
 idleConnections                0
 requestCount                   2
 averageRequestTime             919
 averageCheckoutTime            0
 claimedOverdue                 0
 averageOverdueCheckoutTime     0
 hadToWait                      0
 averageWaitTime                0
 badConnectionCount             0
===============================================================
DEBUG [00:08:45.637] {Thread-2} (Log4jImpl.java:26) Created connection 435456241.
DEBUG [00:08:45.637] {Thread-2} (Log4jImpl.java:26) {conn-100002} Connection
DEBUG [00:08:45.638] {Thread-2} (Log4jImpl.java:26) {conn-100002} Preparing Call: {call sp_loop}
DEBUG [00:08:45.638] {Thread-2} (Log4jImpl.java:26) {pstm-100003} Executing Statement: {call sp_loop}
DEBUG [00:08:45.639] {Thread-2} (Log4jImpl.java:26) {pstm-100003} Parameters: []
DEBUG [00:08:45.639] {Thread-2} (Log4jImpl.java:26) {pstm-100003} Types: []

take 2

===============================================================
 jdbcDriver                     oracle.jdbc.OracleDriver
 jdbcUrl                        jdbc:oracle:thin:test/************@127.0.0.1:1521:xe
 jdbcUsername                   test
 jdbcPassword                   ************
 poolMaxActiveConnections       50
 poolMaxIdleConnections         10
 poolMaxCheckoutTime            120000
 poolTimeToWait                 300
 poolPingEnabled                false
 poolPingQuery                  select 1 from dual
 poolPingConnectionsOlderThan   1
 poolPingConnectionsNotUsedFor  1
 --------------------------------------------------------------
 activeConnections              2
 idleConnections                0
 requestCount                   2
 averageRequestTime             919
 averageCheckoutTime            0
 claimedOverdue                 0
 averageOverdueCheckoutTime     0
 hadToWait                      0
 averageWaitTime                0
 badConnectionCount             0
===============================================================
DEBUG [00:08:50.861] {Thread-3} (Log4jImpl.java:26) Created connection 39288954.
DEBUG [00:08:50.861] {Thread-3} (Log4jImpl.java:26) {conn-100004} Connection
DEBUG [00:08:50.862] {Thread-3} (Log4jImpl.java:26) {conn-100004} Preparing Call: {call sp_loop}
DEBUG [00:08:50.862] {Thread-3} (Log4jImpl.java:26) {pstm-100005} Executing Statement: {call sp_loop}
DEBUG [00:08:50.862] {Thread-3} (Log4jImpl.java:26) {pstm-100005} Parameters: []
DEBUG [00:08:50.863] {Thread-3} (Log4jImpl.java:26) {pstm-100005} Types: []

take 3

===============================================================
 jdbcDriver                     oracle.jdbc.OracleDriver
 jdbcUrl                        jdbc:oracle:thin:test/************@127.0.0.1:1521:xe
 jdbcUsername                   test
 jdbcPassword                   ************
 poolMaxActiveConnections       50
 poolMaxIdleConnections         10
 poolMaxCheckoutTime            120000
 poolTimeToWait                 300
 poolPingEnabled                false
 poolPingQuery                  select 1 from dual
 poolPingConnectionsOlderThan   1
 poolPingConnectionsNotUsedFor  1
 --------------------------------------------------------------
 activeConnections              3
 idleConnections                0
 requestCount                   3
 averageRequestTime             686
 averageCheckoutTime            0
 claimedOverdue                 0
 averageOverdueCheckoutTime     0
 hadToWait                      0
 averageWaitTime                0
 badConnectionCount             0
===============================================================
DEBUG [00:08:57.057] {Thread-4} (Log4jImpl.java:26) Created connection 2008400431.
DEBUG [00:08:57.058] {Thread-4} (Log4jImpl.java:26) {conn-100006} Connection
DEBUG [00:08:57.058] {Thread-4} (Log4jImpl.java:26) {conn-100006} Preparing Call: {call sp_loop}
DEBUG [00:08:57.058] {Thread-4} (Log4jImpl.java:26) {pstm-100007} Executing Statement: {call sp_loop}
DEBUG [00:08:57.059] {Thread-4} (Log4jImpl.java:26) {pstm-100007} Parameters: []
DEBUG [00:08:57.059] {Thread-4} (Log4jImpl.java:26) {pstm-100007} Types: []

.
.
.
(중략)
.
.
.


take 49

===============================================================
 jdbcDriver                     oracle.jdbc.OracleDriver
 jdbcUrl                        jdbc:oracle:thin:test/************@127.0.0.1:1521:xe
 jdbcUsername                   test
 jdbcPassword                   ************
 poolMaxActiveConnections       50
 poolMaxIdleConnections         10
 poolMaxCheckoutTime            120000
 poolTimeToWait                 300
 poolPingEnabled                false
 poolPingQuery                  select 1 from dual
 poolPingConnectionsOlderThan   1
 poolPingConnectionsNotUsedFor  1
 --------------------------------------------------------------
 activeConnections              49
 idleConnections                0
 requestCount                   49
 averageRequestTime             1653
 averageCheckoutTime            0
 claimedOverdue                 0
 averageOverdueCheckoutTime     0
 hadToWait                      0
 averageWaitTime                0
 badConnectionCount             0
===============================================================
DEBUG [00:12:59.662] {Thread-50} (Log4jImpl.java:26) Created connection 630515574.
DEBUG [00:12:59.662] {Thread-50} (Log4jImpl.java:26) {conn-100098} Connection
DEBUG [00:12:59.663] {Thread-50} (Log4jImpl.java:26) {conn-100098} Preparing Call: {call sp_loop}
DEBUG [00:12:59.663] {Thread-50} (Log4jImpl.java:26) {pstm-100099} Executing Statement: {call sp_loop}
DEBUG [00:12:59.664] {Thread-50} (Log4jImpl.java:26) {pstm-100099} Parameters: []
DEBUG [00:12:59.664] {Thread-50} (Log4jImpl.java:26) {pstm-100099} Types: []

take 50

===============================================================
 jdbcDriver                     oracle.jdbc.OracleDriver
 jdbcUrl                        jdbc:oracle:thin:test/************@127.0.0.1:1521:xe
 jdbcUsername                   test
 jdbcPassword                   ************
 poolMaxActiveConnections       50
 poolMaxIdleConnections         10
 poolMaxCheckoutTime            120000
 poolTimeToWait                 300
 poolPingEnabled                false
 poolPingQuery                  select 1 from dual
 poolPingConnectionsOlderThan   1
 poolPingConnectionsNotUsedFor  1
 --------------------------------------------------------------
 activeConnections              50
 idleConnections                0
 requestCount                   50
 averageRequestTime             1687
 averageCheckoutTime            0
 claimedOverdue                 0
 averageOverdueCheckoutTime     0
 hadToWait                      0
 averageWaitTime                0
 badConnectionCount             0
===============================================================
 
take 51
 


activeConnections 는 MaximumActiveConnections의 제한 수치인 50에서 더이상 증가하지 않고 
take 51 에서 멈춘다. 내부적으로 take51 는 다음과 같은 Stack trace 를  나타내고 있다.

Name: Thread-51
State: BLOCKED on oracle.jdbc.driver.T4CConnection@6c63a721 owned by: Thread-1
Total blocked: 1  Total waited: 0

Stack trace: 
 oracle.jdbc.driver.PhysicalConnection.rollback(PhysicalConnection.java:3689)
com.ibatis.common.jdbc.SimpleDataSource.popConnection(SimpleDataSource.java:600)
   - locked java.lang.Object@7d7082d8
com.ibatis.common.jdbc.SimpleDataSource.getConnection(SimpleDataSource.java:222)
...

 
오라클의 세션수는 take n 의 n 수와 일치하며 take 51 시점의 세션수는 이미 한계치에 도달했으므로 더 이상 증가하지 않는다.

STATUS COUNT(*)
--------    ----------
ACTIVE 50



 
  1. 단테 2014.03.12 18:02

    비슷하게 따라 했는데 왜 activeConnections 가 스레드 하나당 하나씩 증가 하지 않을까요???

    ===============================================================
    jdbcDriver com.p6spy.engine.spy.P6DataSource
    jdbcUrl jdbc:oracle:thin:@xxx.xxx.xxx.xxx:1522:myDB
    jdbcUsername myName
    jdbcPassword ************
    poolMaxActiveConnections 80
    poolMaxIdleConnections 40
    poolMaxCheckoutTime 120000
    poolTimeToWait 500
    poolPingEnabled true
    poolPingQuery select 0 from dual
    poolPingConnectionsOlderThan 1
    poolPingConnectionsNotUsedFor 1
    --------------------------------------------------------------
    activeConnections 53
    idleConnections 0
    requestCount 79
    averageRequestTime 50100
    averageCheckoutTime 0
    claimedOverdue 0
    averageOverdueCheckoutTime 0
    hadToWait 0
    averageWaitTime 0
    badConnectionCount 0
    ===============================================================

    ===============================================================
    jdbcDriver com.p6spy.engine.spy.P6DataSource
    jdbcUrl jdbc:oracle:thin:@xxx.xxx.xxx.xxx:1522:myDB
    jdbcUsername myName
    jdbcPassword ************
    poolMaxActiveConnections 80
    poolMaxIdleConnections 40
    poolMaxCheckoutTime 120000
    poolTimeToWait 500
    poolPingEnabled true
    poolPingQuery select 0 from dual
    poolPingConnectionsOlderThan 1
    poolPingConnectionsNotUsedFor 1
    --------------------------------------------------------------
    activeConnections 53
    idleConnections 0
    requestCount 79
    averageRequestTime 50100
    averageCheckoutTime 0
    claimedOverdue 0
    averageOverdueCheckoutTime 0
    hadToWait 0
    averageWaitTime 0
    badConnectionCount 0
    ===============================================================
    [Thread Count] : 880
    [Thread Count] : 881
    [Thread Count] : 882

  2. 단테 2014.03.12 19:23

    현재 현상은 위의 에러와 같이 popConnection() 에서 lock이 걸리는데 재현이 안되네요. 혹시 도움좀 받을 수 있을까 해서요.^^

MaximumIdleConnections 
IBATIS 에서는 MaximumActiveConnections 설정으로 connection pool 의 최대 크기를 설정한다.
그런데 이 설정에 대비되는 설정인 MaximumIdleConnections 을 얼핏(본인이 그랬음-_-;)
connection pool 의 최소 크기 설정이라 생각하기 쉬운데 사실은 그렇지 않다.

Ibatis 는 처음부터 최소치만큼 커넥션을 생성해두지 않고 필요시마다 증가시키므로 (이름부터 Max인) MaximumIdleConnections 은 최소값이 아니고, 단지 ArrayList인 idleConnections 의 최대 길이일 뿐이다.

Connection pooling logic
SimpleDataSource 는 내부적으로 connection pool 을 관리하기 위해 
ArrayList 타입 멤버인 idleConnections 와 activeConnections 에 커넥션을 저장한다.

그리고 처음엔 커넥션을 하나도 안만들고 있다가, 요청이 들어올때 다음 순서로 동작한다.

1. idleConnections 에 대기중인 커넥션이 있으면 이를 반환
2. idleConnections 가 없으면 MaximumActiveConnections 초과하지 않을 경우, 새 커넥션을 만들어 반환
3. MaximumActiveConnections을 초과하면, activeConnections 에서 가장 오래된 커넥션을 가져와 체크아웃타임이 MaximumCheckoutTime을 초과할 경우 이 커넥션을 롤백하고 반환
4. MaximumCheckoutTime을 초과하지 않으면 TimeToWait 만큼 기다렸다가 다시 커넥션 획득 시도


자, 이제 본론이다. 다시 MaximumIdleConnections 의 의미를 돌이켜보자.

MaximumIdleConnections 은 최대 유휴 연결(보관) 수,
즉, idle 상태 연결을 최대 얼마나 보관할지 결정하는 설정인데,
주의해야 할 점은 이 설정을 잘못하면 성능에 심각한 문제가 발생 할 수 있다는 것이다.

다음 상황을 가정해보자

MaximumActiveConnections = 50
MaximumIdleConnections = 0

connection pool size 는 최대 50까지 증가하므로 activeConnections.size() 는 50 이하다. 그런데, activeConnections 에 있던 연결이 다 사용되고 나서 pushConnection() 으로 반환될때 해당 연결은 유휴 상태이므로 idleConnections 로 들어가야하지만, idleConnections 는 위 설정에 의해 길이가 0 이상 될 수 없다. 따라서 유휴 상태로 접어드는 연결은 무조건 소멸되며, idleConnections 에 어떤 연결도 없으므로, ibatis 는 매번 새로운 연결을 만들어내게 되므로 connection pool 을 사용하는 의미가 없게 된다.

이러한 성능 저하는 MaximumActiveConnections, MaximumIdleConnections의 차가 크면 클수록 더 심각하게 되므로, 어플리케이션의 성격에 따라 적절하게 설정 되어야 한다.



참조 : com.ibatis.common.jdbc.SimpleDataSource.popConnection(), pushConnection()

Java 에서 long 은 primitive type, 즉, 원시형 데이터로 클래스가 아니다.
long 은 64bit 정수값을 표현하는 데이터 타입으로 8byte 의 메모리 공간을 사용한다.
Long 은 long 과 마찬가지로 64bit 정수 값을 표현하지만 이것은 클래스다.
그렇다면 Long 은 몇 byte 의 메모리 공간을 사용할까?
테스트를 위해 다음과 같은 코드를 작성해보자.

public class LongTest1
{
public static void main(String[] args)
{
int length = 10000000;
long[] ls = new long[length];
long l = 0;
for (int i = 0; i < length; i++)
ls[i] = l++;
System.out.println(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory());
}
}

위 테스트 클래스는 primitive type인 long을 사용해 배열을 작성하고, 메모리 사용량을 출력한다.

public class LongTest2
{
public static void main(String[] args)
{
int c = 10000000;
Long[] ls = new Long[length];
long l = 0;
for (int i = 0; i < length; i++)
ls[i] = new Long(l++);
System.out.println(Runtime.getRuntime().totalMemory() - Runtime.getRuntime().freeMemory());
}
}


위 테스트 클래스는 Long 클래스의 배열을 작성하고, 메모리 사용량을 출력한다.
 
아래는 heapsize를 최소 100MB, 최대 2GB로 지정한 VM에서 위 두 클래스의 c 값을 바꿔가며 실행한 결과다.


측정한 결과치는 32bit / 64bit VM 에서 각각 다르게 나왔는데, 배열 자체의 사용량을 측정한 것이 아니고 전체 VM의 메모리 사용량을 본 것이므로 long배열의 경우 차이가 없다고 해석 가능하지만, Long배열은 눈에 띄는 차이를 보였다.

아무튼 위 표의 측정치를 통해 long 은 8byte 를 사용하고, Long 은 32bit에선 약 20byte, 64bit 에선 약 28byte 를 사용한다고 볼 수 있다.

측정시 약간의 실수가 있었던것 같아 정정하자면 위 측정 결과는 아주 정확하다고 볼 수 없지만 heapdump 를 확인한 결과 측정결과와 거의 일치하므로 어느정도 신뢰 해도 될 듯 하다. (heapdump 에선 Long 의 메모리 사용량이 64bit 에서 24byte로 나왔다.)
 
  1. conrg 2013.08.19 10:58

    글 잘읽었습니다. 저도 객체 사이즈가 궁금해서 이것저것 찾아봤는데, 위에서 제시한 접근방법이 맞지만, 측정하는 부분이 잘못되었습니다. http://www.javaworld.com/javaworld/javaqa/2003-12/02-qa-1226-sizeof.html?page=1, http://www.javaworld.com/javaworld/javatips/jw-javatip130.html?page=1 두사이트 읽어보세요. 많은 도움이 될것입니다.

JAI (Java Advanced Imaging)
지원 : BMP, GIF (read only), FlashPix (read only), JPEG, PNG, PNM, TIFF, and WBMP
http://java.sun.com/javase/technologies/desktop/media/
http://java.net/projects/jai/
http://java.sun.com/products/java-media/jai/whatis.html
이미지 변환 방법 http://www.javanuri.com/devforum/boardView.jsp?menuId=9&Id=261407&gb=qna
 

JIMI (Java Image Management Interface)
지원 : GIF, JPEG, TIFF, PNG, PICT, Photoshop, BMP, Targa, ICO, CUR, Sunraster, XBM, XPM, and PCX
다운로드 http://java.sun.com/products/jimi/
JAI 와 성능면에서 뒤쳐지는 결과? http://www.hanhuy.com/pfn/java-image-thumbnail-comparison 
난 JIMI가 좋은거 같은데, 왜 모든 사람들은 JAI 만 언급하는가? https://forums.oracle.com/forums/thread.jspa?threadID=1270326
JIMI가 Java3D 로 흡수? 되었다는 얘기 http://stackoverflow.com/questions/967557/java-api-to-convert-jpeg-to-tiff


im4java (imageMagick)

제작사 http://www.imagemagick.org/script/api.php
다운로드 http://im4java.sourceforge.net/
JAI 와 품질,성능에 대한 코멘트 http://stackoverflow.com/questions/2291358/how-do-i-convert-a-tif-to-png-in-java


bmp 를 변환할 일이 있어서, 검색해봤는데 자바 초창기에 JIMI가 많이 쓰이다가 JAI 가 나오고나서는 JAI 를 더 많이 쓰는듯. 하지만  JAI가 너무 크다고 생각하는 사람들은 아직도 JIMI가 좋다고 생각함... 또 어떤사람은 im4java 를 JAI 보다 더 좋고 깔끔하다고 생각하지만 im4java 는 JAI나 JIMI 와 달리 sun(oracle)의 프로젝트가 아니기 때문인지..아니면 실제로 버그가 좀 있는건지 지적당하는 경우도 있음.

JAI는 좀 거대해보이고.. 난 변환만 하면 되니까 jar 크기 젤 작은 JIMI 쓸 생각!

이었으나

JDK 1.4.2 이후 버전에서는 이미지 변환같은 건 imageio로 가능해서 다음과 같이 구현함

File input = new File("D:\image.bmp");
File output = new File("D:\image.png");
BufferedImage bufferedImage = ImageIO.read(input);
ImageIO.write(bufferedImage, "png", output); // jpg, gif 도 가능

자바 어플리케이션에 문제가 있어 아무 말 없이 종료되버리면 개발자 입장에선 난감하기 그지없다.
오류라도 뿜어주면 고맙겠지만, 아무도 오류메시지를 보지 못했다면 어디서 그의 죽음에 관한 정보를 얻을 수 있을것이며, 그 오류는 어떻게 찾아 고쳐야 할까?

다행히 JVM은 자바 어플리케이션에서 심각한 오류나, 예상하지 못한 오류를 발견 했을때는 자동적으로 STDOUT에 dying message 를 남기고, 어플리케이션의 실행위치에 error report 파일을 작성한다는데...

진짜 그런지 한번 보자.
아래는 강제로 에러를 발생시키는 자바 어플리케이션이다.

public class ForceDump {
public static void main( String[] args ) throws Exception
{
java.lang.reflect.Field field = sun.misc.Unsafe.class.getDeclaredField("theUnsafe");
        field.setAccessible(true);
        sun.misc.Unsafe $ = (sun.misc.Unsafe) field.get(null);
        $.putAddress(0, 0);
    }
}


이걸 실행해보면 다음과 같은 결과가 출력된다. 빨간색 글자로 범인은 누구라고 알려준다. 뿐만 아니라 어플리케이션 종료 당시의 해당 어플리케이션의 실행정보를 가지고 있는 에러파일을 저장했다고 한다.

D:\dev\lab\Test\bin>java ForceDump

#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x000000006d9b9565, pid=4568, tid=6248
#
# JRE version: 6.0_25-b06
# Java VM: Java HotSpot(TM) 64-Bit Server VM (20.0-b11 mixed mode windows-amd64 compressed oops)
# Problematic frame:
# V  [jvm.dll+0x1c9565]
#
# An error report file with more information is saved as:
# D:\dev\lab\Test\bin\hs_err_pid4568.log
#
# If you would like to submit a bug report, please visit:
#   http://java.sun.com/webapps/bugreport/crash.jsp
#


자세한 에러 파일 내용을 보기 전에 잠깐,

에러파일이 hs_err_pid<PID>.log 라는 이름으로 java 어플리케이션이 실행된 경로에 생겨있다. 파일이름과 생기는 위치는 이와 같은 형태가 기본값이지만, 이 파일의 이름과 위치를 바꾸려면 다음과 같이 옵션을 주면 된다.
 

D:\dev\lab\Test\bin>java -XX:ErrorFile=../err_file_pid_%p.log ForceDump
#
# A fatal error has been detected by the Java Runtime Environment:
#
#  EXCEPTION_ACCESS_VIOLATION (0xc0000005) at pc=0x000000006d9b9565, pid=3088, tid=1340
#
# JRE version: 6.0_25-b06
# Java VM: Java HotSpot(TM) 64-Bit Server VM (20.0-b11 mixed mode windows-amd64 compressed oops)
# Problematic frame:
# V  [jvm.dll+0x1c9565]
#
# An error report file with more information is saved as:
# ../err_file_pid_3088.log
#
# If you would like to submit a bug report, please visit:
#   http://java.sun.com/webapps/bugreport/crash.jsp
#


-XX:ErrorFile=<filename> 형식이며 filename에 %p 를 쓰면 해당 어플리케이션의 pid 값으로 치환된다.
filename 은 상대 또는 절대경로를 포함해 지정 할 수 있다. (위 예시에서는 상대경로를 지정해보았다.)
만일 유효하지 않은 경로라면 이 옵션을 지정하지 않은 것과 동일하게 동작한다.



아래는 범인에 대한 더 많은 정보를 가지고 있다는 에러파일의 내용이며, 왜 프로그램이 종료 되었는지에 대해 분석하는데 도움이 된다.


JVM 이 java 어플리케이션에서 unexpected error (e.g. out of memory error) 를 발견했을때는 에러파일내용이 아래와 같이 나오기도 한다.(헤더부분만 첨부함)


 
참조
http://www.java.com/en/download/help/error_hotspot.xml 
http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html#DebuggingOptions 

http://forums.oracle.com/forums/thread.jspa?threadID=1237584 (-XX:OnError 옵션을 이용해 에러파일 내용을 메일로 보내는 예제까지)

1. insert 전에는 추가할 레코드의 기본키가 무엇이 될 지 모르는 경우 selectKey 를 이용해 추가된 레코드의 기본키를 반환 받아 확인한다.

iBATIS 2.x 의 insert 메서드는 selectKey 와 함께 쓰일 경우 기본키 객체에 추가된 레코드의 기본키를 넣어 반환한다.

ProductDao.java

public class ProductDao
{
    SqlMapClient sqlMapClient;

    public boolean insertProduct(Product product)
    {
        Integer id = (Integer) sqlMapClient.insert(“insertProduct”, product);
        return id > 0; // product.productId 에도 같은 값이 저장됨
    }
}

Product.xml

<insert id="insertProduct">
    <selectKey keyProperty="productId" resultClass="int" type="pre">
        SELECT S_Products.NEXTVAL FROM DUAL
    </selectKey>

    INSERT INTO Products VALUES (#productId#, #productName#, #productCode#)
</insert>

 

2. insert 전에 추가할 레코드의 기본키를 알고 있을때는?

본키를 이미 알고 있다면, 기본키를 이용한 성공여부 판단이 불가능하기 때문에 insert 구문을 update 메서드로 실행하여 영향 받은 행수를 평가한다.

ProductDao.java

public class ProductDao
{
    SqlMapClient sqlMapClient;

    public boolean insertProduct(Product product)
    { 
        int rows = sqlMapClient.update(“insertProduct”, product);
        return rows > 0;
    }
}

Product.xml

<insert id="insertProduct">
    INSERT INTO Products VALUES (#productId#, #productName#, #productCode#)
</insert>

 

3. 신뢰할 만하다고들 하는 또 다른 방법

예외를 잡는 것 만으로도 상당히 신뢰할만한 성공여부 판단을 할 수 있다고들 한다. 단, 개인적으로는 왠지 찝찝하다. 가급적이면 1, 2 방법으로 판단하고 도저히 방법이 없을 경우에 시도해보도록 하자. 또, 프로시저, 함수를 이용해 DBMS 에게 성공여부를 판단하도록 하는것도 한 방법이 될 수 있다.

A, B 두개의 작업이 있다.

두 작업은 같은 SqlMapClient 객체를 통해 DB에 엑세스 한다.

A 는 10000개의 insert를 일괄처리하고, B 는 1개의 insert를 처리하는데,

만일 A가 트랜잭션중일때 B가 실행되어 예외가 발생한다면 A 작업이 롤백 될까?

 

다음은 테스트코드다.

 

 

이 테스트 코드의 실행결과, 데이터베이스에는 success() 메서드의 결과는 insert 되어 있었지만 error1, error2 메서드의 실행결과는 빠져있었다.

즉, A메서드가 실행중일때 B메서드에서 예외가 발생하여도 A 작업은 문제없이 실행된다.

iBATIS 2.x 는 대량의 insert, update, delete 작업을 효율적으로 처리 할 수 있는 방법으로 startBatch(), executeBatch() 메서드를 제공한다.

개발자 가이드에 의하면 네트워크 트래픽 최소화, JDBC 드라이버의 추가적인 최적화(압축같은) 같은 혜택을 통해 성능향상을 볼 수 있다고 한다.

startBatch(), executeBatch() 메서드는 문서상으로 명시적 transaction 과 함께 사용해야 한다고 나오지는 않았으나
테스트시에는 명시적 transaction 을 사용하지 않았을때는 일반작업과 동일하거나 나쁜 결과를 보였다.

1. 특징

UPDATE 문 1000번 반복 처리 속도 테스트 결과
1) 배치+트랜잭션 : 77ms
2) 트랜잭션 : 1104ms
3) 일반작업 : 4274ms
4) 배치 : 4604ms

다음은 "UPDATE Computers SET ..." 문을 배치로 10번 실행하는 부분의 디버그 로그다.



그리고 다음은 같은 UPDATE 문을 배치하지 않고 트랜잭션만 걸린 상태로 10번 실행한 부분의 디버그 로그다.



배치 적용 유무에 따른 속도 차이는 상황에 따라 많이 다르지만 위 테스트에선 대략 5~7배 정도 차이를 보였다.

위 테스트에서 트랜잭션까지 빼면 디버그 로그는 다음과 같이 나온다.



위 테스트는 트랜잭션만 건 테스트에 비해 3-5배정도 느리게 나왔다.
트랜잭션없이 배치만 걸면 위 테스트와 비슷하거나 더 느린 결과를 보였다.

2. 장점
동일유형의 작업을 빠르게 처리 한다. 테스트시 최대 50배이상 빠르게 실행되는 경우도 있었다.

3. 단점
작업실패시 모든 배치가 롤백된다.
- 롤백하지 않고 실패한건만 무시하고 계속 진행하고 싶은 경우엔 해결 방법이 없다.
문제가 발생했을때 어디서 발생했는지 알기 어렵다.
- executeBatch() 메서드는 성공한 업데이트 수를 반환한다. 단, insert, update, delete 에 한한다.queryForObject 의 수행결과는 알 수 없다
- executeBatchDetail() 메서드는 더욱 상세한 모든 배치작업결과를 반환한다. 하지만 역시 insert, update, delete 에 한한다.
- 또 작업중 예외가 발생하면 배치예외를 던지는데, 어떤 문장(statement)에서 예외가 발생했는지는 보이지만 동일한 문장을 배치작업으로 여러개 수행하는 경우라면 어느 부분에 문제가 발생했는지 알 수 없다.

위와같은 한계로 배치 중 오류발생시 무시하고 다음으로 진행하고 싶을땐 배치할 작업을 저장해두고 있다가 예외를 잡아 일반작업으로 넘겨 처리해줘야 하는 불편이 있다.
※ 배치예외, 배치결과객체 분석이 충분하지 않아 단점으로 지적한 부분에 문제가 있을수도 있다.

*4. 결론*
동일작업을 대량 반복할때 배치, 트랜잭션은 성능을 대폭 향상시킨다.
배치예외가 빈번히 발생하는게 아니라면, 예외시 일반작업 처리를 위한 대비 코드를 사용하는 비용을 치르더라도 배치를 쓰는게 좋다.


결과
ArrayList - 넣기 : 38ms
ArrayList - 빼기 : 36851ms
HashMap - 넣기 : 170ms
HashMap - 빼기 : 17ms
ConcurrentLinkedQueue - 넣기 : 156ms
ConcurrentLinkedQueue - 빼기 : 17ms
LinkedList - 넣기 : 41ms
LinkedList - 빼기 : 9ms
ArrayBlockingQueue - 넣기 : 32ms
ArrayBlockingQueue - 빼기 : 22ms
ArrayDeque - 넣기 : 17ms
ArrayDeque - 빼기 : 5ms
LinkedBlockingQueue - 넣기 : 47ms
LinkedBlockingQueue - 빼기 : 32ms
LinkedBlockingDeque - 넣기 : 100ms
LinkedBlockingDeque - 빼기 : 24ms
PriorityBlockingQueue - 넣기 : 68ms
PriorityBlockingQueue - 빼기 : 188ms
SynchronousQueue - 넣기 : 7ms
SynchronousQueue - 빼기 : 4ms
PriorityQueue - 넣기 : 16ms
PriorityQueue - 빼기 : 164ms

테스트코드

  1. 공학코드 2015.08.14 09:59 신고

    성능 테스트 정보 감사합니다!!!

웹서버를 포함한 솔루션 배포시에는 데이터베이스 연결 설정 파일을 포함시키기 어렵다. 사용자의 DB접속 환경이 어떨지 알 수 없기 때문이다. 그렇다고 서버 셋업시에 사람이 일일이 설정파일을 만지는게 하는것도 불안하다.

보통은 어플리케이션 설치시에 정보를 입력받아 속성파일로 써주거나, 웹사이트가 구동되고 나서 DB설정을 먼저 하도록 한다. 그런데 문제는 Spring 이 dataSource 객체를 생성하는 방식이다.

 

applicationContext.xml

   1: <bean id="propertyConfigurer"
   2:     class="org.springframework.beans.factory.config.PropertyPlaceholderConfigurer">
   3:     <property name="locations" value="WEB-INF/jdbc.properties" />
   4: </bean>
   5:  
   6: <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
   7:     <property name="driverClassName" value="${jdbc.driverClassName}" />
   8:     <property name="url" value="${jdbc.url}" />
   9:     <property name="username" value="${jdbc.username}" />
  10:     <property name="password" value="${jdbc.password}" />
  11: </bean>

Spring 은 이처럼 applicationContext.xml 파일로 properties 파일의 위치를 정의하고 dataSource bean에서 이를 참조하도록 해서 웹서버 시작시에 로딩한다. 그런데 만일 웹서버 시작시에 이 파일이 없거나 속성을 읽어오지 못했다면 서버가 시작조차 되지 않는다.

데이터베이스 연결 속성 파일이 없거나 올바르지 않아도 웹서버가 정상적으로 시작되게 하기 위해서는 PropertyPlaceholderConfigurer 클래스의 ignoreUnresolvablePlaceholders 속성을 true 로 만들어주면 되는데, 이렇게 설정하기 위해서 위 코드의 location 속성 정의 아래 다음 내용을 추가 한다.

   1: <property name="ignoreUnresolvablePlaceholders" value="true" />

Spring – iBATIS 사용시 보통 WEB-INF/jdbc.properties 파일에 데이터베이스에 접속하기 위한 설정을 저장해 둔다.

데이터베이스 접속 환경이 변경되어 이 파일을 수정한후에는 웹서버를 재시작해줘야 변경된 설정이 반영된다만, 웹서버를 재시작하지 않고, 또 jsp 를 쓰지 않는 – flex & blazeds 만으로 구동되는 웹 – 환경에서 변경된 설정을 바로 반영 해줄 수 있는 방법이 필요해졌다.

방법을 요약하면, 스프링으로부터 WebApplicationContext를 받아와 dataSource bean 객체를 받아온 후 값을 변경해주면 된다.

예제는 아래..

applicationContext.xml

   1: <bean id="webConfigService" class="webConfig.service.WebConfigService" autowire="byType"/>

동적로딩을 구현 할 클래스의 bean 설정에 autowire 를 true 로 설정

 

WebConfigService.java

   1: public class WebConfigService
   2: {
   3:     /**
   4:      * 웹 애플리케이션이 실행중인 경로를 가져오기 위해 WebApplicationContext 를 스프링으로부터 가져와 저장한다.  
   5:      */
   6:     private WebApplicationContext webApplicationContext;
   7:     
   8:     /**
   9:      * 스프링 프레임웍에서 이 쓰기 전용 메서드에 WebApplicationContext을 셋팅한다.
  10:      * @param w
  11:      */
  12:     @Autowired
  13:     public void setWebApplicationContext(WebApplicationContext w)
  14:     {
  15:         webApplicationContext = w;
  16:     }
  17:     
  18:     public void reloadDataSource(String driverClassName, String url, String username, String password)
  19:     {
  20:         BasicDataSource bds = (BasicDataSource) webApplicationContext.getBean("dataSource");
  21:         bds.setDriverClassName(driverClassName);
  22:         bds.setUrl(url);
  23:         bds.setUsername(username);
  24:         bds.setPassword(password);
  25:     }
  26: }

스프링에 의해 WebConfigService 클래스 setWebApplicationContext 메서드가 호출되며 WebApplicationContext 가 셋팅된다.

dataSource 의 값을 변경해주고 싶으면 reloadDataSource 메서드를 쓰면 된다. 설정내용은 즉시 반영 되는듯.

  1. 신용호 2010.08.09 16:24

    너무너무 감사합니다.
    이글 덕분에 해결했습니다.
    Tomcat의 경우 글처럼 하면 되는데
    이상하게 Jeus에선 bds.close()를 먼저 해야 되네요.

    • yjacket 2010.08.17 18:09 신고

      도움되셨다니 다행입니다! JEUS 는 안써봐서 모르겠지만. 톰캣도 bds.close() 먼저 해주는게 좋겠네요~~ ^^

자바 프로그램을 작성중 웹으로부터 전달받은 임시파일을 데몬이 읽어 처리 하도록 해야 할 일이 생겼다. 임시파일의 적당한 위치를 찾기 위해 자바 어플리케이션상에서의 현재 경로가 어떻게 되는지 궁금해 다음 코드를 짜서 확인해보았다.

   1: // Java Application
   2: // d:\workspace\daemon\src\test.java
   3: File file = new File(“.”);
   4: System.out.println(file.getCanonicalPath()); // d:\workspace\daemon\
   5:  
   6: // Dynamic Web Project
   7: // d:\workspace\web\webContent\test.jsp
   8: // d:\apache-tomcat-6.0.14
   9: <%
  10: java.io.File file = new java.io.File(".");
  11: %>
  12: <%=file.getCanonicalPath()%> // d:\apache-tomcat-6.0.14\bin\
  13:  
  14:  
  15:  

요약하면,

자바 어플리케이션은 프로젝트 홈이 현재 경로가 된다.
다이나믹 웹 프로젝트는 톰캣 실행파일 경로가 현재 경로가 된다.(단, 이건 직접 아파치 톰캣을 실행했을떄 얘기고, 이클립스에서 실행 했을 경우는 이클립스 홈이 현재 경로가 된다.)

But, 위 실험 결과와는 상관없이 업로드 경로는 System.getProperty(“java.io.tmpdir”) 로 해 볼 예정. 근데, 윈도에서는 System.getProperty(“java.io.tmpdir”) 뒤에 \ 가 붙는데, 리눅스에선 안붙어서 검사하는 코드도 추가해야되겠다…쩝… 크로스플랫폼 지원하기가 쉽지 않네.

프로그래밍을 하다보면 어떤 로직을 실행하는데 실행시간이 얼마나 걸리는지 궁금 할 때가 있는데 다음 코드로 간단히 doSomething() 함수를 실행하는데 걸리는 시간을 화면에 출력 할 수 있다. 같은 기능을 하지만 서로 다르게 구현 한 각 로직의 성능을 시험해보고 싶을때 활용해보자. 실행하는 동안은 시스템을 건드리지 말아야 정확한 측정 결과를 구할 수 있다.

   1: long l = System.currentTimeMillis();   
   2: for (int i = 0; i < 10000; i++)  
   3: {  
   4:     doSomething();  
   5: }  
   6: System.out.println(System.currentTimeMillis() - l);  

+ Recent posts