태터데스크 관리자

도움말
닫기
적용하기   첫페이지 만들기

태터데스크 메시지

저장하였습니다.

'performance'에 해당되는 글 13건

  1. 2007.09.03 Java 힙 사이즈 이슈 - Initial Size와 Max Size를 어떻게 설정할 것인가?
  2. 2007.08.31 전혀 새로운 책을 꿈꾸며... (1)
  3. 2007.08.26 [ Enterprise Java는 거대한 동기화 머신이다 - Thread ] Enterprise Java & Oracle 성능 분석의 기초 - Part2
  4. 2007.08.24 [시작하면서] Enterprise Java & Oracle 성능 분석의 기초 - Part1 (3)
  5. 2007.08.13 The Power of Byte Code Instrumentation in Java - Part 6 (2)
  6. 2007.08.08 Weblogic 9 - Self Tuing Thread Pool의 위력 - Part1 (2)
  7. 2007.08.05 The Power of Byte Code Instrumentation in Java - Part 4 (1)
  8. 2007.08.05 The Power of Byte Code Instrumenation in Java - Part2 (2)
  9. 2007.07.30 AOP(Aspect Oriented Programming) in Java - Part 5 (1)
  10. 2007.07.27 AOP(Aspect Oriented Programming) in Java - Part 4

Java 힙 사이즈 이슈 - Initial Size와 Max Size를 어떻게 설정할 것인가?

Enterprise Java 2007.09.03 15:59
Java Performance Tuning에 있어서 가장 낮은 과일, 즉 가장 손쉽게 설정하고 성능 효과를 거둘 수 있는 것이 Heap 크기와 관련된 설정이다.

그 중에 Initial Size와 Max Size 관련된 설정이 흔히 성능 개선책으로 많이 언급된다.

Sun HotSpt JVM에서의 일반적인 성능 개선 방안은 이렇다.
  • Initial Size와 Max Size를 동일하게 부여해서 동적인 크기 조정에 따른 부하를 없애라.
이 가이드는 너무나 보편적이어서 누구나 의심없이 항상 고려해야할 성능 개선안으로 받아들인다. 하지만 진짜 그럴까?

아마 이 가정에 대해서 의심을 해보거나 실제로 테스트를 해 본 사람은 거의 없을 것으로 생각된다.

하지만, 큰 Initial Size는 오히려 어플리케이션의 초기 성능을 떨어뜨리는 원인이 될 수 있다. Initial size가 크면 초기 Minor GC와 Major GC에 불필요하게 많은 시간이 소모된다.

반면 Initial Size가 적절히 작으면 초기 Minor GC와 Major GC가 아주 짧게 끝나고 필요할 때 메모리가 확장(Expansion)되므로 오히려 부하가 적다.

아래 테스트 결과를 보자
Case1: java -Xms32M -Xmx512M
Full GC: Pause Time = 5.364361, Count = 16, Average=0.335273
Minor GC: Pause Time = 9.769008, Count = 303, Average=0.032241

Case2: java -Xms512M -Xmx512M
Full GC: Pause Time = 4.609964, Count = 13, Average=0.354613
Minor GC: Pause Time = 10.253730, Count = 259, Average=0.039590

일반적인 성능 개선 추전안과 거꾸로 Initial Size를 작게 한 경우에 오히려 GC에 의한 부담이 작은 것을 확인할 수 있다. 그 이유는 아래와 같이 초기 GC 시간이 작기 때문이다.

Case1:
0.000: [GC 0.000: [DefNew: 2944K->320K(3264K), 0.0061382 secs] 2944K->1298K(32448K), 0.0062603 secs]
0.014: [GC 0.014: [DefNew: 3259K->320K(3264K), 0.0050409 secs] 4237K->2280K(32448K), 0.0051333 secs]
0.023: [GC 0.023: [DefNew: 3264K->320K(3264K), 0.0077398 secs] 5224K->3236K(32448K), 0.0078449 secs]
...
19.840: [GC 19.840: [DefNew: 26176K->2879K(26176K), 0.0233516 secs] 125359K->105130K(258248K), 0.0234460 secs]
19.882: [GC 19.882: [DefNew: 26175K->2879K(26176K), 0.0380722 secs] 128426K->112633K(258248K), 0.0381711 secs]
19.939: [GC 19.939: [DefNew: 26175K->2879K(26176K), 0.0392069 secs] 135929K->120141K(258248K), 0.0392966 secs]

Case2:
0.000: [GC 0.000: [DefNew: 23360K->2880K(26240K), 0.0337995 secs] 23360K->7461K(259264K), 0.0338926 secs]
0.052: [GC 0.052: [DefNew: 26240K->2879K(26240K), 0.0412854 secs] 30821K->14526K(259264K), 0.0413726 secs]
0.113: [GC 0.113: [DefNew: 26239K->2880K(26240K), 0.0406367 secs] 37886K->21559K(259264K), 0.0407367 secs]
...
19.741: [GC 19.741: [DefNew: 23360K->2879K(26240K), 0.0307106 secs] 98231K->82374K(259264K), 0.0307986 secs]
19.789: [GC 19.789: [DefNew: 26239K->2879K(26240K), 0.0393455 secs] 105734K->89908K(259264K), 0.0394374 secs]
19.848: [GC 19.848: [DefNew: 26239K->2879K(26240K), 0.0403141 secs] 113268K->97442K(259264K), 0.0404032 secs]

물론 위의 테스트 시나리오가 너무나 심플해서 복잡한 어플리케이션에서 상황을 재현하지 못한다. 또한 수행시간이 길어지면 초기 구동 성능이 차지하는 비중이 작아지므로 역시 의미가 없다(하지만 이 경우 Initial Size를 크게 주는 것 또한 전혀 의미가 없기는 마찬가지다. 어차피 Heap은 Max Size까지 커질 테니까...)

하지만 엄밀한 테스트와 해석이 없는 교조적인 튜닝 가이드는 무의미하다는 것을 잘 보여준다.

재미있게도 IBM에서 제공하는 JVM 튜닝 가이드에서는 Initial Size와 Max Size를 같이 하지 말고 Initial Size를 작게 지정하라고 권고하고 있다. IBM JVM은 전통적으로 Sun HotSpot JVM과는 전혀 다른 Heap 관리 기법을 사용하기 때문에 이런 가이드가 나온 것으로 이해할 수 있다.(이유는 나중에 기회가 되면 상세하게...)





신고
tags : Heap, java, performance
Trackback 0 : Comment 0

Write a comment


전혀 새로운 책을 꿈꾸며...

Enterprise Java 2007.08.31 19:42
최근 나의 고민 중 하나는 이것이다.

  • Enterprise System을 구성하는 2개의 거대한 축이 있다. Java(WAS)와 Oracle!!
  • 이 두 개의 환경을 관통하고 꿰뚫어 보는 성능 분석 방법론과 튜닝 방법론을 체계화할 순 없을까? 아니, 적어도 두 개의 큰 축을 같은 시각에서 비교 분석할 수는 없을까?
불행하게도 Java 전문가는 Oracle을 모르고, Oracle 전문가는 Java를 모른다.

모두들 외산 언어와 툴을 열심히 익히고 배우기에만 바쁘고, 외산 툴의 기술지원 엔지니어가 최고의 전문가로 인정받는 이 왜곡된 현실에서 누가 선뜻 이런 일을 하겠는가...

다년간 Java와 Oracle 성능 문제를 고민해온 사람들 중 하나로 나 스스로가 이 질문에 답을 던지고자 하는 욕구를 느낀다.

아래 내용 정도면 어떨까?

Enterprise System Performance Tuning: Java 5 and Oracle
- 지금까지 그 누구도 시도하지 못한 책
- "동기화"의 관점에서 Java와 Oracle의 성능 분석 기법을 비교 연구한 책
- Java는 거대한 동기화 머신이다 + Oracle은 거대한 동기화 머신이다

내용:
1. Java Thread vs. Oracle Session

2. Java Heap vs. Oracle Buffer Cache/Shared Pool/Library Cache

3. JDBC vs. Oracle SQL

흩어져서 여기 저기 내 머리 속에 있는 지식과 경험을 어떻게 통합적인 시각에서 풀어낼 것인가!!!

큰 중압감과 함께 굉장히 짜릿한 스릴을 느낀다.

신고
Trackback 0 : Comment 1
  1. jt 2012.08.06 10:27 Modify/Delete Reply

    관리자의 승인을 기다리고 있는 댓글입니다

Write a comment


[ Enterprise Java는 거대한 동기화 머신이다 - Thread ] Enterprise Java & Oracle 성능 분석의 기초 - Part2

Enterprise Java 2007.08.26 00:44
Enterprise Java는 거대한 동기화 머신이다.

작년 5월 쯤 [오라클은 거대한 동기화 머신이다]라는 타이틀로 오라클 성능 분석에 대한 책을 낸 적이 있다.

OWI Advanced Oracle Wait Interface in 10g

조동욱

엑셈 2006.05.08



(책 광고 아님)

이 책에서 이야기하고자 핵심은 다음과 같은 것이다.
  • 오라클에서는 동시에 많은 수의 세션이 동일한 자원(Library Cache/Buffer Cache/Table/Row/...)을 획득하기 위해 경쟁한다.
  • 여러 세션이 동시에 자원을 획득하고 변경하는 것을 방지하기 위해 동기화 메커니즘이 필요하다.
  • 오라클에서는 Lock과 Latch라는 개념을 통해 동기화 메커니즘을 제공한다.
  • Lock과 Latch을 통한 동기화 과정에서 많은 성능 문제가 발생하며 이를 대기이벤트(Wait Event)로 관찰하고 분석할 수 있다.
이 책에서 주장했던 [동기화 메커니즘]은 동시 세션/사용자/쓰레드를 지원하는 시스템에서는 공통적으로 사용된다. Enterprise Java를 대표하는 WAS 환경도 예외가 아니다.

WAS Application에서는 수십 개 ~ 수백 개의 쓰레드가 동일한 자원을 획득하기 위해 경쟁한다. 이 과정에서 필연적으로 동기화 문제가 발생하며 이로 인한 대기 현상(Wait)가 발생하게 된다.

한가지 안타까운 것은 Java 환경에서는 동기화(Synchronization)에 의한 대기 현상과 성능 저하 현상을 관찰하는 체계적인 방법이 없다는 것이다. 오라클에서는 V$SESSION, V$SESSION_WAIT, V$SESSION_EVENT, V$SYSTEM_EVENT, V$LOCK, V$LATCH, ... 등 수많은 뷰를 통해서 Lock과 Latch 등 동기화 객체의 의한 대기 현상을 효과적으로 분석할 수 있다.

반면에 Java 환경에서는 몇가지 툴들이 흩어져서 제공되며, 그나마 오라클이 제공하는 정보에 비하면 상당히 체계가 부족하다. 하지만 오라클은 "Application이고 System인 반면", Java는 "Language이고 Library"이라는 점에서 보면 당연한 결과라고 할 수 있겠다.
(물론 JMX와 같은 표준들이 나오면서 Java 환경도 점점 성능 관리에 대한 개념과 기능들이 추가되고 있다)

Java의 동기화(Synchronization) 방법
(이미 널리 알려진 진부한 사실들이지만 간단히 정리해볼 필요는 있어서...)

Monitor
Java는 기본적으로 Multi Thread 환경을 전제로 설계되었다. 따라서 Multi Thread 환경에서 발생할 수 있는 동기화 문제를 해결하기 위한 기본적인 메커니즘을 제공한다.

이것을 흔히 "Monitor"라고 부른다. Java에서의 모든 Object는 반드시 Monitor를 하나씩 가지고 있다. 특정 Object의 Monitor에는 동시에 하나의 Thread만이 들어갈 수(Enter) 있다. 다른 Thread에 의해 이미 점유된 Monitor에 들어가고자 하는 Thread는 Monitor의 Wait Set에서 대기해야만 한다.

Java에서 Monitory를 점유하는 유일한 방법은 synchronized 키워드를 사용하는 것이다. 가령 다음과 같이 ...

synchronized(dump_test.lock) {
for(int idx=0; idx
}

위의 코드를 실행하는 Thread는 for(...) {} 구문이 실행되는 동안 dump_test.lock 이라는 Object의 Monitor를 점유한다. 따라서 dump_test.lock Object에 대해 Monitor를 점유하려는 모든 Thread는 for(...) {} 구문이 실행되는 동안 대기 상태(Blocked)에 빠지게 된다.

위의 Java 코드에 해당하는 Byte Code를 보면 흥미로운 사실을 알 수 있다.

GETSTATIC dump_test.lock : Ljava/lang/Object;
DUP
ASTORE 1
MONITORENTER
...

ALOAD 1
MONITOREXIT

synchronized { } 블록은 MONITORENTER ... MONITOREXIT Byte Code로 해석된다. 즉 Monitor에 들어간 후 원하는 코드를 실행하고 다시 Monitor를 빠져 나오는 것이 Java가 동기화를 수행하는 방법이다.

동기화 프로그래밍 기법
익히 알려진 대로 Java에서 동기화를 수행하는 방법은 크게 두 가지로 나뉜다.

  1. synchronization: synchronized 블록이나 synchronized 메소드를 통해 동기화를 수행하는 방식
  2. wait/notify: wait와 notify 메소드를 이용해서 동기화를 수행하는 방식. 기본적으로 synchronized 방식의 "응용"이라고 봐도 무방하다.
개인적으로는 두 방법 다 매우 직관적이고 사용하기 편하다고 생각한다. 하지만 많은 사람들이 두 방법에 대해 오해를 하거나 잘 모르는 경우가 많다.

가령 여러 Thread가 동시에 Access할 수 있는 객체는 무조건 sychronized 블록이나 메소드로 보호를 해야 하는가?
No. synchronized를 수행하는 코드와 그렇지 않은 코드의 성능 차이는 대단히 크다. 동기화를 위해 Monitor에 액세스하는 작업에는 그에 상응하는 오버헤드가 따른다. 따라서 반드시 필요한 경우에만 사용해야 한다. 가령 다음과 같은 코드를 보자.

private static instance = null;

public static synchronized getInstance() {
if(instance == null) { instance = new Instance(...); }
return instance;
}

Singleton 방식을 구현하기 위해 getInstance 메소드를 Synchronized로 잘 보호했지만, 성능에 있어서는 불행한 결정이 될 수 있다. instance 변수가 실행 도중에 변경될 가능성이 없다면 위의 코드는 성능 관점에서는 상당히 비효율적이다. 다음과 같이 Sycnrhonized를 사용하지 않게끔 변경하는 것이 바람직하다.

private static instance = new Instance(...);

public static getInstance() { // <-- No Synchronized
return instance;
}

wait/nofiy 메소드를 이용한 Thread 동기화 방법에 대해서는 상당히 개발 경험이 많은 개발자들도 정확하게 이해하지 못하는 경우가 많은 것 같다. 특히 wait 메소드와 sleep 메소드를 정확하게 구분하지 못해 잘못된 코드를 작성하는 경우를 종종 보는데, 이 경우 동기화 문제에 의한 치명적인 성능 저하 현상을 유발할 수 있으므로 특히 주의해야 한다.
(이 블로그에서 API의 정확한 사용법을 일일시 설명하지는 않을 것이며 많은 좋은 자료들이 있으므로, 혹시라도 개념이 명확하지 않은 경우에는 반드시 사용법을 정확하게 이해했으면 한다)

Thread의 상태
Java 5의 Thread.getState() 메소드는 Thread의 현재 상태를 Thread.State Enum으로 리턴한다. Thread.State Enum의 정의는 아래와 같다.

BLOCKED
Thread state for a thread blocked waiting for a monitor lock.
NEW
Thread state for a thread which has not yet started.
RUNNABLE
Thread state for a runnable thread.
TERMINATED
Thread state for a terminated thread.
TIMED_WAITING
Thread state for a waiting thread with a specified waiting time.
WAITING
Thread state for a waiting thread.


BLOCKED 상태는 Sychronized에 의해 점유된 Monitor를 획득하지 못하고 기다리는 상태임을 의미한다. 반면 WAITING이나 TIMED_WAITING 상태는 wait 메소드를 이용해 대기하고 있음을 의미한다. RUNNABLE 상태는 현재 실제로 작업을 할 수 있는 상태이거나 작업을 수행 중인 상태를 의미한다.

이 상태들에 대한 정확한 이해가 Thread들간의 Lock 경합을 이해하는데 필수적이다. 만일 특정 Thread가 특정 Object의 Monitor를 장시간점유하고 있다면, 동일한 Monitor를 필요로 하는 다른 모든 Thread들은 BLOCKED 상태에서 대기하게 된다. 이 현상이 지나치게 되면 Thread 폭주가 발생하고 자칫 System 장애를 유발하게 된다. 이런 현상은 wait 메소드를 이용해 대기를 하는 경우도 마찬가지이다. 특정 Thread가 장시간 notify를 통해 Wait 상태의 Thread들을 깨워주지 않으면 수많은 Thread 들이 WAITING이나 TIMED_WAITING 상태에서 대기하는 현상이 발생하게 된다.

---------------------------------------------------------------------------------------------------------------------------------------------
다음 글에 계속...


신고
Trackback 0 : Comment 0

Write a comment


[시작하면서] Enterprise Java & Oracle 성능 분석의 기초 - Part1

Enterprise Java 2007.08.24 20:29
Enterprise 환경의 성능 분석
Enterprise 환경의 성능 분석에 경험이 있는 사람이라면 시스템의 전체 성능을 정확하게 파악하려면 WAS(Application)과 DB(Persistence)를 통합적으로 보는 통찰력이 필요하다는 것에 공감할 것이다.

WAS와 DB의 성능 문제를 통합적으로 보는 방법론이 있는가?라고 질문하면 "모르겠다"가 답일것이다. 이는 다분히 학술적이고 철학적인 문제일 수 있어서 나의 지식 범위를 벗어날 거 같다.

하지만 다행히!!! 이런 단순한 공식 하나가 있다.

Response Time = Service Time + Wait Time

즉, 사용자가 체감하는 응답 시간은 실제로 일을 하는 시간(Service Time)일을 하지 못하고 대기하는 시간(Wait Time)으로 구분할 수 있다.

만일, 정밀한 성능 분석과 다양한 경험을 통해 Application과 DB단에서의 Service Time과 Wait Time을 측정하고 그 상관 관계를 적절히 추리할 수 있다면 사용자가 겪는 성능 문제를 상당히 직관적이고 정확하게 밝혀낼 수 있다.


응답시간 구간의 구성 요소
아래 그림은 User, WAS(Application), DB(Persistence) 레이어로 이루어진 환경을 단순화한 것으로, User 입장에서의 Response Time이 실제로 어떤 요소로 구성되는지를 표현한 것이다.



위의 그림처럼 [Response Time = Service Time + Wait Time]이라는 사상에 근거하여 보다 구체적으로 응답시간 구간을 나눌 수 있다.
일차적으로 Application단에서의 소요 시간은 다음과 같이 세분화할 수 있다.
  • Network Time: User와의 Network 통신에 소요되는 대기 시간
  • Service Time: Worker Thread가 Active하게 일을 하는 시간
  • DB I/O Time: Database와의 통신(Connection/Execute/Fetch/...)에 소요되는 대기 시간
  • Wait Time: File I/O, Net I/O, Lock waiting(Thread synchronization), GC pause 등, DB I/O를 제외한 기타 대기시간
Application에서의 성능 문제를 정확하게 파악하려면 위의 위의 개별 요소에서의 소요 시간이 적당한지, 적당하지 않다면 왜 그런지를 파악할 수 있어야 한다.

Database 단(Application단 관점에서는 DB I/O Time)에서의 소요 시간은 다음과 같이 세분화할 수 있다.(여기서 Database는 Oracle로 간주한다)
  • Network Time: Application과의 Network 통신에 소요되는 대기 시간
  • Service Time: DB Session/Process가 Active하게 일을 하는 시간
  • Wait Time: Lock Waiting, I/O Wating, Latch free Wating 등 필요 자원을 획득하는데 소요되는 대기 시간
Database에서의 성능 문제 또한 각 성능 요소들이 어느 정도의 비중을 차지하는지, 만일 지나치게 많은 시간을 필요로 한다면 왜 그런지 파악할 수 있어야 세말한 성능 분석을 수행할 수 있다.

성능을 결정하는 각 구성 요소에서 어떤 문제가 발생하고 있는지 정확하게 파악하는 것이 대단히 중요하다. 가령 WAS에서 lock waiting이 어떻게 발생하고 있는지, Garbage Collection에 의한 대기 시간(Pause time)이 얼마나 되는지, 데이터베이스에 작업을 요청하고 대기하는 시간을 얼마인지, 그리고 왜 그 정도의 시간을 대기하는지 등이 정확하게 파악되어야 한다.

DB단에서도 마찬가지이다. Application이 요청한 특정 쿼리가 시간이 얼마나 걸리는지, 왜 그 정도의 시간이 걸리는지, 실제로 일을 하는 것인지 아니면 Lock이나 latch free, I/O 대기 시간은 얼마나 되는지를 정확하게 파악해야 한다.

대기 현상의 중요성
응답 시간 구간의 성능 저하 정도를 파악하는데 가장 직관적인 방법은 "대기"(Wait)가 얼마나 많이 발생하는지를 측정하는 것이다. Service Time은 실제로 일을 하면서 사용한 시간인데 반해, Wait Time은 동기화나 I/O, Garbage Collection등의 이유로 인해 일을 하지 않고 소비한 시간이다.

[Response Time = Service Time + Wait Time]의 공식에서 얻을 수 있는 가장 단순한 진실은 Service Time이나 Wait Time, 혹은 둘다를 줄일 수 있다면 Response Time을 줄일 수 있다는 것이다.

특히 Wait Time의 경우, 불필요한(반드시 그런 것은 아니지만) 시간일 확률이 높다. 따라서 효율적인 진단과 튜닝을 통해 Wait Time을 적절히 줄이는 것이 가능하다. 만일 사용자가 작성한 소스를 수정하지 않고도 Wait Time을 줄일 수 있다면 그 효과는 대단히 긍정적이고 바람직한 것이 된다.

OWI - Oracle Wait Interface
오라클에서는 대기 현상을 분석하는 방법론이 대단히 정교하게 발전되어 왔다. 이 방법론을 흔히 OWI-Oracle Wait Interface-라고 부른다.

오라클 대기 이벤트에 대한 보고는 오라클 커널 개발자들에 의해 디버깅 용도로 추가된 것으로 보이는데, 이후 그 용도가 확대되고 다양한 툴과 API가 추가되면서 이제는 오라클 성능 문제를 파악하는 가장 중요한 방법론으로 발전되었다.

좀 과장해서 말하면 OWI에 대한 지식이 없이 오라클의 성능 문제를 분석한다는 것이 이제는 불가능하다고 할 정도이다.

Java/WAS에서의 대기 현상 분석
애석하게도 Java/WAS 환경에서는 OWI와 같은 체계적인 방법론이 존재하지 않는다. 하지만 Thread Dump, GC Dump와 같은 기본적인 툴들을 통해 대기 현상을 어느 정도 분석할 수 있다. 또한 BCI(Byte Code Instrumentation)이나 JVMPI/JVMTI 등을 통해 자신만의 프로파일을 수행할 수도 있다. 대부분의 Java Profiler 툴들이 이러한 방법들을 사용해서 성능 분석 데이터를 제공한다.
(욱짜의 블로그 http://blog.naver.com/ukja/120040782799 에서 BCI에 대한 간략하게 소개한 바 있다)

Java 5에서 공식적인 표준으로 채택된 JMX의 Platform MXBean은 성능 문제를 분석하는 또 다른 표준을 제공한다. Platform MXBean을 이용하면 JVMPI/JVMTI등의 C Interface를 통해서만 얻을 수 있었던 유용한 정보들을 매우 쉽게 얻을 수 있다. 하지만 성능 문제를 본격적으로 분석하기에는 부족한 면이 많으며 앞으로 API의 개선을 통해 실질적인 성능 정보를 제공해줄 것을 기대해본다.

이런 기본적인 툴들이나 API 이외에 WAS가 제공하는 정보들 또한 중요하다. 대부분의 WAS들이 사용자 Request를 효과적으로 처리하기 위해 Thread Pool, Connection Pool, EJB Pool/Cache와 같은 개념들을 구현하고 있다. 이런 Pool과 Cache들에서 대기 현상(Queuing)이 얼마나 발생하는지를 파악하는 것 또한 중요한 대기 현상 분석 기법이 된다.

(Steven Haines가 지은 Pro Java EE 5 Performance Management and Optimization의 159페이지 ~ 172페이지를 보면 대기현상에 기반한 성능 최적화 튜닝 기법에 대한 실사례가 소개되고 있다. 반드시 읽어보기 바란다)

한가지 고무적인 것은 대부분의 WAS가 이런 류의 성능 정보(Pool/Cache 등의 사용량)를 JMX API를 통해 제공(Expose)하기 때문에 마음만 먹으면 자신만의 성능 Repository를 만들 수 있다는 것이다. (하지만 불행하게도 이런 기능을 제공하는 툴들은 매우 드물다. 적어도 국내 Java 성능 관리 툴들은 JMX에 대해서는 무관심한거같다)

Enteprise Java 성능 분석의 문제점과 필요성
Enterprise Java 환경의 성능 분석에 있어서는 다다익선의 원칙이 적용될 수 있다. 즉, 많이 알 수록 좋다. 되도록이면 많은 종류의 툴을 적용할 수 있고, 많은 프로그래밍 기법을 이해하고, 데이터베이스와의 연동 문제, 그리고 데이터베이스 고유의 문제 등을 이해할 수 있으면 더 좋다.

애석하게도 Java/WAS와 데이터베이스(Oracle) 모두의 성능 문제에 대해 통찰력을 지닌 사람은 국내에서 그리 많지 않은 것 같다. 아니 거의 전무하다고 해도 무방할 것이다. 서로가 서로에 대해서 무지하거나 애써 무시하려고 하는 것이 현실이다.

현실적으로 불가피한 면도 있겠지만, 엔지니어들간의 의사 소통과 지식 공유에 대한 노력이 없었다는 점은 피할 수 없는 비평일 것이다.

이런 의미에서 앞으로 이 블로그를 통해서 Enterprise Java와 Oracle을 아우르는 다양한 성능 문제에 대해 논의할 수 있는 시간이 있었으면 하는 개인적인 바램이다.

하지만 이렇게 심플한가?
앞서 [Response Time = Service Time + Wait Time]이라는 간단한 등식을 소개했다. 이 공식만 보면 모든게 단순하고 명확해보인다. Service Time이나 Wait Time만 줄이면 만사 OK 아닌가??!!

음... 하지만 필자의 경험은 그렇게 단순하지 않다는 것이다.

한 가지 예를 들어보자. Application이 Loop를 돌면서 DML을 수행하는 구조로 구현되어 있다. 그리고 이런 작업을 여러 Thread가 동시에 수행한다. 오라클 튜너가 이를 파악하고 모든 Application을 Batch Execution으로 변환하게끔(즉, PreparedStatement.addBatch, executeBatch를 사용하게끔) 유도했다. 상식적으로 생각해보아도 DB와의 통신이 획기적으로 줄고, DB 작업 자체의 일량도 줄어든다.(그 이유는 오라클의 작동 방식과 관련이 있는데 다음 기회에 소개하기로 한다)

즉, Application 입장에서 보면 Wait Time(DB I/O Time)이 줄어들기 때문에 당연히 사용자의 Response Time은 감소해야 한다.

하지만 결과는? Application에서 극단적인 성능 저하가 발생하고 말았다. 그 이유는 다음 두가지...
  1. Batch Execution은 Application에서 더 많은 메모리를 요구한다. 이로 인해 Garbage Collection이 왕성하게 발생한다.
  2. Batch Execution은 한번의 Operation에 Connection을 보유하는 시간이 좀 더길다. 이런 이유로 이전에 비해 더 많은 Connection이 필요해졌지만, Connection Pool의 설정값이 이전 버전에 최적화되어 있어 Connection Pool이 금방 소진되어 버렸다.
즉 Wait Time을 줄이려는 시도가 다른 Side Effect를 불러 오고 이로 인해 다른 종류의 Wait Time-위의 예에서는 GC Pause Time과 Connection Pool 대기 시간-이 증가하는 현상을 초래하고 만 것이다.

이런 사례는 비일비재하다. 마치 부동산 거품을 이야기할 때마다 거론되는 풍선 효과 같은 것이 실제로 Enterprise Java 환경의 성능 문제에서도 발견된다. 이런 풍선 효과를 최소화하려면 시스템 전체를 아우르는 통찰력과 더불어 정밀한 성능 측정과 분석 능력이 필요하다.


앞으로 볼 것들...
서론이 대단히 거창해져버렸다.
(마치 한국 영화처럼 앞은 거창한데 뒤로 갈 수록 힘을 잃게 되지 않을까 걱정...)

앞으로 시리즈글을 통해 Enterprise Java 환경에서의 성능 분석에 필요한 기본적인 툴들과 API, 데이터베이스와의 연동 문제, 그리고 데이터베이스 본연의 문제 등에 대해 논의하는 시간을 가질려고 한다.

여기에는 Java Thread의 동기화, Heap 관리 및 Garbage Collection, JDBC 연동 문제, 오라클 대기 현상과의 관계 등이 포함될 것이다. 그리고
Enterprise Java 환경의 성능 문제에 대해 입문을 하고자 하는 사람들에게 도움이 될 만한 수준이 될 것이다.

너무 거창하고 폭넓은 주제라...
용두 사미가 되지 않도록 기도할 뿐이다.














신고
Trackback 0 : Comments 3
  1. 멀더엄마 2007.08.24 22:02 신고 Modify/Delete Reply

    우리의 경우, 네트워크/웹서버(트래픽확인)> WAS(메모리/CPU) > DB(메모리/CPU 등...모니터링툴) > Application 대략 이런 순서로 성능 문제가 야기된 원인을 찾아감.
    WAS 프로세스가 죽었을 경우, Thread dump 를 떠서 원인을 찾으려고는 하지만 아직 한번도 정확한 원인을 찾은적이 없는듯.

    내가 본 성능문제 발생 유형 몇가지.

    1. Java App 소스 중 매우 자주 쓰는 메쏘드(XML Doc에서 메뉴를 읽어오는)가 synchronized 로 되어 있었음.
    2. WAS 인스턴스의 GC 옵션이 잘못되어(-client 였나?)있어서 GC하는 동안 쌓인 request가 동시에 DB에 몰리면서 DB CPU가 100% 까지 올라감
    3. WAS 버그로 인한(버그 패치를 제때 안함) memory leak 발생

    그리고 솔루션말고 장비의 HW적인 결함때문에 알수없이 응답이 늦어지기도 하고......

    리플쓰기 매우 조심스러움.(내가 리플달면 블로그 수준이 확 떨어진다그래서리.. )

  2. 멀더엄마 2007.08.24 22:20 신고 Modify/Delete Reply

    위에서 두번째 꺼는 .. 이런 케이스.
    메모리를 매우 많이 쓰는 App => GC를 자주 발생시킴(게다가 GC옵션 및 메모리 할당이 잘못되어있음) => DB에 request가 몰려 정상 응답못함. 근데 계속 DB가 응답 늦다고 DB쪽에서만 문제를 찾고 있었음.

    아 졸려. 집에 가고파. (불쌍한 저급 개발자 나부랭이)

  3. 욱짜 2007.08.24 23:10 신고 Modify/Delete Reply

    와 수준높은 리플 감사. 특히 두번째 케이스는 WAS와 Oracle 간의 상관 관계가 잘 설명되는 좋은 사례!!! Thread dump의 경우 Thread간의 Locking 문제를 찾는데는 유용하지만... 몇몇 명확한 케이스를 제외하고는 이걸 이용해서 WAS가 터무니없이 죽는 현상의 원인을 밝히기는 어려울 듯 함...

Write a comment


The Power of Byte Code Instrumentation in Java - Part 6

Enterprise Java 2007.08.13 11:13

Part 5에 이어...

The Power of Byte Code Instrumentation in Java- Part 6

JDK는 전통적으로 JVM이 구동될 때 이 JVM 안에서 특정 작업을 수행할 에이전트(Agent), 즉 요원을 지정할 수 있는 방식을 제공해왔다. Java 5, 즉 JDK 1.5 이전 버전에서는 JVMPI(JVM Profiler Interface)라고 불렀다. Java 5부터는 JVMTI(JVM Tool Interface)라는 새로운 인터페이스가 제공된다. JVMPI/JVMTI의 특징은 C/C++로 구현 가능한 에이전트의 인터페이스를 제공한다는 것이다.

Java 5가 제공하는 희소식 중 하나는 Java로 구현 가능한 에이전트의 인터페이스를 제공한다는 것이다. 만세~~ 다음 명령어로 확인 가능하다.

Prompt> java

....

-agentlib:[=]
load native agent library , e.g. -agentlib:hprof
see also, -agentlib:jdwp=help and -agentlib:hprof=help
-agentpath:[=]
load native agent library by full pathname
-javaagent:[=]
load Java programming language agent, see java.lang.instrument

위의 옵션 들 중 agentlib/agentpath 옵션은 JVMPI/JVMTI 에이전트를 활성화하는 것이다.
반면 javaagent 옵션은 Java로 구현된 에이전트를 활성화하는 역할을 한다.

위의 설명이 (불)친절하게 안내하고 있는바대로 java.lang.instrument 패키지가 Java 에이전트의 인터페이스 역할을 한다.

java.lang.instrument 패키지에 대한 자세한 설명은 http://java.sun.com/j2se/1.5.0/docs/api/java/lang/instrument/package-summary.html를 참조한다.

JavaAgent BCI의 구조

아래 그림은 java.lang.instrument 패키지가 제공하는 기능을 그림으로 간략하게 표현한 것이다.

위의 과정을 간략하게 설명하면 다음과 같다.

1. Java Agent는 premain이라는 메소드를 구현한다. JVM은 Java Agent의 premain 메소드를 호출해서 Agent를 구동한 후에 Application의 main 메소드를 호출한다.

2. Java Agent는 ClassFileTransformer(클래스파일 변환기) 인터페이스를 구현하고, Instrumentation.addTransformer를 이용해서 클래스파일 변환기를 JVM에 등록한다.

3~4. JVM은 클래스 파일을 로드할 때 등록된 ClassFileTransformer의 transform 메소드를 호출해서 클래스파일의 바이트 코드를 변환하고, 변환된 바이트 코드를 원래 클래스 대신 사용한다. 바이트 코드를 변환하기 위해서 ASM 라이브러리를 사용한다.

5. Agent는 필요한 시점에 Instrumentation.redefineClasses 메소드를 이용해 특정 클래스의 바이트 코드를 런타임에 변경한다.

JavaAgent의 간단한 샘플

아래에 JavaAgent의 아주 간단한 샘플 소스가 있다.(실제 바이트 코드 변환에는 ASM 라이브러리를 사용한다)

public class SimpleProfiler implements ClassFileTransformer {

public SimpleProfiler() {
super();
}

public static void premain(String args, Instrumentation inst) {
try {
// Redefine preloadedclasses. Especially rt.jar boot class

ArrayList defs = new ArrayList();
for( Class c : inst.getAllLoadedClasses()) {
try {
if(c.getName().equals("java.io.File")) { // 여기서는 샘플로 java.io.File 만 변환한다.

System.out.println("Redefining class " + c.getName());
ClassReader reader = new ClassReader(c.getName());
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS+ClassWriter.COMPUTE_FRAMES);

// 여기가 구현부. ASM 라이브러리를 이용한다.

// --> 여기서는 소스를제공하지 않으며, 내부적으로 모든 클래스의 메소드의 시작점과 끝점에

// --> Method Start와 Method End를 알리는 코드를 삽입한다.
ClassAdapter adapter = new SimpleClassAdapter(writer, c.getName));

reader.accept(adapter, ClassReader.SKIP_DEBUG);

byte[] result = writer.toByteArray();
if(result != null) defs.add(new ClassDefinition(c, result));
}
} catch(Exception ex) {
ex.printStackTrace();
}
}

ClassDefinition[] cdef = defs.toArray(new ClassDefinition[defs.size()]);
inst.redefineClasses(cdef);
inst.addTransformer(new SimpleProfiler());

(new Thread(new MonitoringThread())).start();
} catch(Exception ex) {
ex.printStackTrace();
}
}
public byte[] transform(ClassLoader l, String className, Class c,
ProtectionDomain pd, byte[] b) throws IllegalClassFormatException {

if(l != ClassLoader.getSystemClassLoader()) {
return b;
}

if(className.startsWith("instrument/profiler")) {
return b;
}

System.out.println("Classloader = " + l);
System.out.println("Class = " + className);

byte[] result = b;
ClassReader reader = new ClassReader(b);
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS+ClassWriter.COMPUTE_FRAMES);
ClassAdapter adapter = new SimpleClassAdapter(writer, className); // 여기가 구현부...
reader.accept(adapter, ClassReader.SKIP_DEBUG);

result = writer.toByteArray();

return result;

}
}

SimpleProfiler 클래스는 JavaAgent로 다음과 같은 일을 수행한다.

1. premain메소드에서 현재까지Load된클래스들(특히rt.jar에 속하는Class들)을 Redefine한다.(같은 방법으로 어떠한 클래스이든지 원하는 대로 바이트 코드를 변환할 수 있다)

2. transform 메소드에서필요한 클래스들이 로딩되는 시점에 변환한다.

Manifest 파일

JavaAgent 파일은 필요한 클래스 파일들을 Jar 파일로 묶어서 JVM에 제공하게 된다. (여기서는 profiler.jar 라고 부르자)

특히 JVM에게 JavaAgent에 대한 정보를 넘겨주기 위해서 Manifest 파일을 작성해서 Jar 파일에 같이 넣어주어야 한다. Mainfest 파일에 기록해야 할 정보는 다음과 같다.

Premain-Class: Premain 메소드를 담고 있는 Agent Class의 이름. 필수 예) instrument.profiler.SimpleProfiler
Boot-Class-Path: Boot classloader에 의해 로딩될 Jar 파일의 경로. 옵션 예) profiler.jar;asm-3.0.jar

Can-Redefine-Classes: Instrmentation.redefineClasses를 통해 Runtime으로 바이트 코드를 변환하는 것을 허용할지의 여부. 옵션. Default는 False 예) True

아래에 샘플로 사용한 Manifest 파일이 있다.

Premain-Class: instrument.profiler.SimpleProfiler
Can-Redefine-Classes: True
Boot-Class-Path: profiler.jar;asm-3.0.jar

위의 정보를 해석하면

첫째, instrument.profiler.SimpleProfiler가 premain 메소드를 구현하고 있는 JavaAgent이며

둘째, 바이트 코드의 런타임 변환(Instrumentation.redefineClasses)을 허용하며

세째, profiler.jar(JavaAgent 자제) 파일과 asm-3.0.jar(ASM 라이브러리) 파일을 Boot class path에 등록한다.

두번째 옵션은 클래스 파일의 런타임 변환에 사용된다. 만일 위의 예제와 같이 rt.jar에 속한 클래스들, 즉 Boot class path에 속한 클래스들의 바이트 코드를 변환하려면 Profiler 자체가 Boot class path에 속해야 한다. 이것은 ClassLoader에 관한 JVM 고유의 속성에 기인한다.

JavaAgent의 실행

위와 같이 JavaAgent Jar 파일을 생성한 후, 다음과 같은 Java 명령어로 실행하면 된다.

java -javaagent:profiler.jar [Your Target Applicatin]

아래에 간단한 실행 결과가 있다. 모든 클래스의 모든 메소드의 실행 결과를 캡쳐할 수 있다.

---------- Call Table -------------------------------
instrument/target/RunThread.do31179[4694]
instrument/target/RunThread.do2791[5730]
instrument/target/RunThread.do1396[3189]
java.io.File.14[0]
java.io.File.getPath8[0]
java.io.File.exists6[0]
instrument/target/RunThread.5[0]
java.io.File.lastModified4[0]
java.io.File.length2[0]
java.io.File.getPrefixLength2[0]
java.io.File.getCanonicalPath2[0]
instrument/target/ProfilerTarget.main1[0]

JavaAgent와 JVMTI와의 관계. 그리고 JMX

JavaAgent와 JVMTI가 모두 Java 5 (JDK1.5)에서 지원되는 것은 결코 우연이 아니다. JavaAgent와 JVMTI는 몇가지 특징을 공유하고 있다. 실시간 클래스 코드 변환이 대표적인 케이스이고, Java 오브젝트의 크기를 계산한는 함수를 제공하는 것도 같은 맥락이다. (Instrumentation.getObjectSize와 JVMTI의 GetObjectSize)

특히 실시간 클래스 코드 변환은 다른 BCI 라이브러리가 제공하지 못하는 뛰어난 기능으로 이제 BCI는 크게 세가지로 분류가 가능해졌다.

  • Static BCI : 특정 클래스의 바이트 코드를 정적으로 변환하는 방식
  • Load-Time BCI : 특정 클래스가 로딩되는 시점에 변환하는 방식
  • Runt-Time BCI : JVM이 실행되고 있는 시점에 특정 클래스의 바이트 코드를 변환하는 방식

Sun이 Java 5에서 JVMTI와 JavaAgent를 제공하면서 의도한 것은 이 두가지 방법을 상호 보완적으로 사용하라는 것이다. 즉, JVM 자체를 프로파일링하는 것은 여전히 JVMTI와 같은 C 언어 레벨의 API를 사용하되, BCI를 통해 특정 클래스아 액션을 프로파일링하는 것은 JavaAgent와 같은 Java 언어 레벨의 API를 사용하기를 권고한다.

여기에 JMX의 Platform MBean(JVM 정보를 제공하는 MBean) 기능까지 추가되었으니, 이제 Java는 성능 관리에 있어 그 어느때보다 폭넓은 기능을 제공하는 셈이다.

앞으로 시간날 때마다 JVMTI나 JMX같은 추가적인 기능들에 대해서도 논의할 기회를 가질려고 한다.

Epilogue

BCI는 성능 관리를 위해 사용가능한 현존하는 방법들 중 가장 보편적이고 편리한 기법이다.많은 공개 라이브러리가 제공되고 있고, 특히Java 5에서는 JVM 레벨에서 BCI를 지원한다.

만일 WAS와 같은 Java Application시스템 모니터링을 계획하고 있다면, BCI에 대한 검토가 반드시 필요할 것이다.

또는실제로 이 기법을 사용할 기회가 없다고 하더라도, 3rd party의 모니터링을 위해 툴을 도입할 때 이런 기법들이 어떻게 사용되고 있는지 이해해야만 툴의 특징을 파악할 수 있을 것이다.

PS)

아래 예제는 BCI를 이용해 Exception, Call Tree, File I/O, Net I/O, JDBC Request 등을 프로파일링한 결과이다. 상용 모니터링 툴들이 제공하는 것과 거의 동일한 수준의 정보를 BCI를 이용해 손쉽게 수집할 수 있다.

--------------------[Exception Tracking]--------------------
Exception count = 9
[Exception] Exception = java.io.IOException, Message = Something Bad2~~, Thread id = 11, Caller = [Sigature]flowlite.server.ExceptionGenerator.doSomething2, Called from = [Sigature]flowlite.server.ExceptionGenerator.doSomething2, When = Mon Aug 13 11:06:10 KST 2007
flowlite.server.ExceptionGenerator.doSomething2(FlowLiteMBeanServer.java:128)
flowlite.server.ExceptionGenerator.run(FlowLiteMBeanServer.java:113)
java.lang.Thread.run(Unknown Source)

[Exception] Exception = java.io.IOException, Message = Something Bad2~~, Thread id = 11, Caller = [Sigature]flowlite.server.ExceptionGenerator.doSomething2, Called from = [Sigature]flowlite.server.ExceptionGenerator.run, When = Mon Aug 13 11:06:10 KST 2007
flowlite.server.ExceptionGenerator.doSomething2(FlowLiteMBeanServer.java:128)
flowlite.server.ExceptionGenerator.run(FlowLiteMBeanServer.java:113)
java.lang.Thread.run(Unknown Source)

...

--------------------[Active Thread & Call Tree Tracking]--------------------
Thread count = 26
[ActiveThread] Thread id = 62, Thread name = Thread-49, Group name = main
[Call Tree]
[Call][Sigature]flowlite.server.CallTreeGenerator$InnerGenerator.depth1_1, duration = 31[ms]
[Call][Sigature]java.util.Random.nextLong, duration = 0[ms]
[Call][Sigature]java.lang.Math.abs, duration = 0[ms]
[Call][Sigature]java.lang.Thread.sleep, duration = 0[ms]
[Call][Sigature]flowlite.server.CallTreeGenerator$InnerGenerator.depth2_1, duration = 15[ms]
[Call][Sigature]java.util.Random.nextLong, duration = 0[ms]
[Call][Sigature]java.lang.Math.abs, duration = 0[ms]
[Call][Sigature]java.lang.Thread.sleep, duration = 15[ms]

...

--------------------[File I/O Tracking]--------------------
File I/O count = 22
[File Info] Thread id = 22, File name = c:test.txt, Status = 2, File mode = [r+w], read time = Mon Aug 13 11:06:15 KST 2007
[File IO], Thread id = 22, File name =c:test.txt, Bytes read = 1024, Access time = Mon Aug 13 11:06:15 KST 2007
[File IO], Thread id = 22, File name =c:test.txt, Bytes read = 1024, Access time = Mon Aug 13 11:06:15 KST 2007
...

Socket Info] Thread id = 20, Host name =localhost:17904, Status = 1, Access time = Mon Aug 13 11:06:13 KST 2007
[Net IO], Thread id = 20, Bytes read = 1024, Access time = Mon Aug 13 11:06:13 KST 2007
[Net IO], Thread id = 20, Bytes read = 1024, Access time = Mon Aug 13 11:06:13 KST 2007
[Net IO], Thread id = 20, Bytes read = 1024, Access time = Mon Aug 13 11:06:13 KST 2007

...

--------------------[JDBC I/O Tracking]--------------------
Connection count = 1
Statement count = 1
FetchCount count = 100
[Connection] Statement count = 10, DB = Oracle, Oracle Database 10g Enterprise Edition Release 10.2.0.3.0 - 64bit Production
With the Partitioning, OLAP and Data Mining options, Created = 2007-08-13
[Statement] Query = SELECT name FROM t_pstmt_test WHERE id = ?, Execution count = 10, Fetch size = 10, Statement Type = Prepared
[Execution] Column Count = 1, Fetch count = 1, {Parameters} = (1, 4),
[Fetch] Fetched value count = 1
(1, name4),

...

신고
Trackback 0 : Comments 2
  1. KGEE 2010.08.26 10:22 신고 Modify/Delete Reply

    안녕하세요 욱짜님!
    좋은글 보고갑니다. 제가 요즘 졸업준비하느라 JVM CPU/MEMORY Profiling 에 골머리가 아프거든요..ㅎㅎ
    리서치 이후 이렇게 좋은 BCI 포스팅은 첨 보네요.. 극찬!!!
    근데요 오래된 글들이라 그런지 사진들이 다 엑박이네요.. 저만 그런지.. Windpw7 인데..
    암튼 사진이 너무 궁금해서요~~~~! ㅋㅋ
    종종 글보러 오겠습니다.

    • 욱짜 2010.08.27 00:33 신고 Modify/Delete

      네이버 블로그에 있던 글을 옮겼는데 이미지는 안 옮겨가는 바람에 그렇게 되었습니다.

      JVM CPU/Memory Profiling이라니 쉽지 않은 길을 택하셨습니다. 무운을 빕니다.

Write a comment


Weblogic 9 - Self Tuing Thread Pool의 위력 - Part1

Enterprise Java 2007.08.08 16:45

Web Application Server(이하 WAS)에서 가장 설정하기가 까다롭고 그 효과가 예측하기 어려운 것은?

나의 견해로는 Worker Thread의 개수를 지정하는 것이다.

Worker Thread: 클라이언트의 Request를 받아서 처리 작업을 수행하는 Thread를 의미한다. Worker Thread는 클라이언트의 요청을 받아, JSP/Servlet을 구동하고 DB Operaiton을 수행하며 그 결과를 클라이언트에 보내준다.

거의 대부분의 WAS가 Thread Pool을 사용하고 있으며, Thread Pool이 지닐 수 있는 Thread의 개수를 사용자가 직접 지정하는 방법을 사용한다. 즉 내가 Thread Pool의 Thread 개수를 최대 15개로 지정하면 비록 동시에 100개의 Request가 들어온다고 해도 최대 15개만의 Thread가 Request를 처리할 수 있다.

Weblogic의 경우에는 Default Thread Pool의 Thread 개수가 15개이다. WAS 종류마다 각기 다른 종류의 알고리즘과 다른 종류의 디폴트 크기를 사용한다.

Weblogic 8의 Thread Pool 관리

아래 그림은 Weblogic 8의 Thread Pool 관리 기법을 표현한 것이다.

Weblogic 8은 [weblogic.kernel.Default]라는하나의 디폴트 Execute Queue를 가지고 있으며, 하나의 Execute Queue에하나의 Thread Pool이 매달리는 구조이다. 하나의 Thread Pool의 디폴트 Thread 개수가 15개이므로 Weblogic 8은 기본적으로 하나의 Execute Queue와 하나의 Thread Pool(Thread 개수는 15개)을 제공하는 셈이다.

아래 그림은 Weblogic8 Server의 콘솔을 통해Default Execute Queue와Thread Pool을 조회하는 화면이다.

Default Execute Queue의 이름은 항상 [weblogic.kernel.Default]라는 사실을 기억하자.

비록 거의 사용되지 않지만,Default Execute Queue외에 새로운 Execute Queue를 생성할 수 있으며 특정 Request들은 특정 Execute Queue를 사용할 수 있도록 지정할 수 있다. 가령 [회원]관련 업무는 [Default Execute Queue]를 사용하고, [주문]관련 업무는 [Execute Queue 2]를 사용할 수 있다. 이렇게 되면 두 업무간에 Worker Thread를 공유하지 않기 때문에 Thread 경쟁과 Context Switching이 줄어드는 효과를 기대할 수 있다.

사용자가 직접 Thread의 개수를 지정해줘야 하기 때문에 이러한 방식의 Worker Thread 관리 방식을 Manual 모드라고 부른다.

Manual 모드의 Thread Pool 관리가 가지는 문제점은 내 시스템에서 최적의 Thread 개수를 예측하는 것이 사실상 불가능하다는 것이다.

가령 50개의 동시 Request가 발생하는 WAS 시스템을 고려해보자.이 WAS 시스템의 CPU 개수는 2개이다. Worker Thread의 개수는 몇개가 적당하겠는가?

대답은 아마 사람들마다 다를 것이다. CPU 개수가 2개이므로 Worker Thread의 개수도 2~3개가 적당하다? 아니면 동시 Request 수가 50개이므로 Thread 개수도 50개? 아니면 그 중간 어디쯤인 15개 정도?

아래에 간단한 테스트 결과가 있다. Worker Thread수가 5개인 경우와 15개(디폴트 값이라는 것을 명심)인 경우 응답 속도를 비교해보면...

재밌게도 Worker Thread 개수가 5개인 경우 평균 응답 속도가 833ms로, 15개의 1915ms에 비해 2배 이상 빠른 것을 확인할 수 있다.

즉, Worker Thread의 개수가 지나치게(CPU 개수에 비교해) 많으면 Thread간의 경쟁과 Context Switching에 의해 오히려 성능이 저하되는 것을 확인할 수 있다.

아래 그림은 [Weblogic Tuning Guide]에서 발췌한 것으로 Thread Count를 CPU 개수와 비례해서 적절히 설정할 것을 권장하고 있다.

또 하나의 큰 문제는 사용자의 동시 Request 수가 고정적이지 않다는 것이다. 만일 하루 중 동시 Request수가 1~100 사이에서 반복적으로 변한다면 동시 Request 수를 어느 기준으로 맞추어야 할 것인가...

이런 이유때문에 Bea는 Weblogic 9에서 [Self Tuning Thread Pool]의 개념을 도입했다.

Part2에 계속 ....

신고
Trackback 0 : Comments 2
  1. 멀더엄마 2007.08.10 11:06 신고 Modify/Delete Reply

    Thread들끼리... CPU랑 메모리 등등... 자원 경쟁 발생해서 그런겨?

  2. 욱짜 2007.08.10 16:18 신고 Modify/Delete Reply

    Thread끼리 CPU를 경쟁했다는 표현이 더 맞을거 같은데...우선 Thread 자체가 리소스를 차지하기 때문에 Overhead가 있고, Thread가 CPU를 할당받을 때 Context Switching이 발생하니까 더 심해지고...

Write a comment


The Power of Byte Code Instrumentation in Java - Part 4

Enterprise Java 2007.08.05 23:50

Part3에 이어...

One Simple but Powerful Example of BCI

Part3에서 언급한 봐와 같이 java.lang.Exception 클래스의 바이트 코드를 직접 수정함으로써 모든 Exception의 발생을 캡쳐할 수 있다. Exception 클래스의 생성자들에서 다음과 같은 코드를 수행하게끔 바이트 코드를 수정하면 된다.

public Exception(String s)
{
super(s);
ExceptionCallBack.exceptionOccurred(this);
}

ASM이 제공하는 Library를 이용하면 이 작업을 매우 손쉽게 수행할 수 있다.

ASM 라이브러리의 사용법을 상세히 설명하는 것은 이 블로그의 범위를 벗어나며 http://asm.objectweb.org 에서 제공하는 매뉴얼과 문서에 이미 상세하게 설명이 되어 있다.

아래 소스 코드는 java.lang.Exception 클래스를 ASM Library를 이용해 읽고(Read),그대로(변형없이) Write하는 예제이다.

package flowlite.exception2;

import java.io.FileOutputStream;

import org.objectweb.asm.*;
import org.objectweb.asm.commons.*;

public class ExceptionTransformer implements Opcodes {

// Convert class
public void transform(String newClassName) throws Exception {

System.out.println("Starting transformation of java.lang.Exception...");

// Reader
ClassReader reader = new ClassReader("java.lang.Exception");
// Writer

ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);

// Class Adapter
ClassAdapter adapter = new ExceptionClassAdapter(writer);


reader.accept(adapter, ClassReader.SKIP_FRAMES);

byte[] b = writer.toByteArray();
FileOutputStream fos = new FileOutputStream(newClassName + ".class");
fos.write(b);
fos.flush();

}

public static void main(String[] args) {
try {
String newClassName = "Exception";
if(args.length >= 1) newClassName = args[0];
ExceptionTransformer fit = new ExceptionTransformer();
fit.transform(newClassName);
} catch(Exception ex) {
ex.printStackTrace();
}

}
}


class ExceptionClassAdapter extends ClassAdapter implements Opcodes {

public ExceptionClassAdapter(ClassVisitor visitor) {
super(visitor);

}

}

위의 소스 코드를 보면 바이트 코드 변환은 다음과 같은 과정을 통해 이루어진다는 것을 알 수 있다.

  • ClassReader 객체를 이용해 원래 클래스의 바이트 코드를 읽어들인다.
  • ClassAdapter 객체를 이용해 바이트 코드를 변경한다.
  • ClassWriter 객체를 이용해 변경된 바이트 코드를 얻는다.
  • 변경된 바이트 코드는 파일로 저장하거나 ClassLoader에게 넘겨 준다.

위의 코드를 실행하면 Exception.class 파일이 아래 그림과 같이 파일시스템에 저장된다.

-Xbootclasspath 옵션 사용하기

문제는 어떻게 하면 원래 JVM의 rt.jar 에서 제공하는 Exception 클래스 파일이 아닌 내가 생성한 Exception 클래스 파일을 쓰게 하느냐이다. rt.jar 파일을 직접 변경시키는 것은 매우 위험하고, 법적으로도(이건 농담이 아님) 문제가 될 수 있기 때문에 권장되지 않는다.

답은 -Xbootclasspath 옵션을 이용하는 것이다. "java -X" 명령을 수행하면 다음과 같은 결과를 확인할 수 있다.

즉, -Xbootclasspath/p:<내가 작성한 Exception Class의 패스>를 지정하면 JVM은 rt.jar보다 먼저(prepend) 내가 작성한 Exception Class 파일을 읽어 들인다. 이렇게 함으로써 rt.jar 파일에 대한 수정을 가하지 않아도 된다.

우선 아래와 같은 Test Class를 작성한 후

public class ExceptionTest2 {

public static void doException(boolean bException) throws RuntimeException {
if(bException)
throw new RuntimeException("test");

}

public static void main(String[] args) {

try {
ExceptionTest2.doException(true);<-- 여기서 Exception발생
} catch(Exception ex) {}

try {
ExceptionTest2.doException(false);
} catch(Exception ex) {}
}
}

아래와 같이 -Xbootclasspath 옵션을 이용해서 수행한다.

(./converted_classes/java/lang 디렉토리에 내가 만든 Exception Class가 있다...)

java -Xbootclasspath/p:./converted_classes ExcetpionTest2

java.lang.Exception에 내가 필요한 바이트 코드 삽입하기

우리가 java.lang.Exception 클래스의 생성자에 삽입하고자 하는 코드는 다음과 같다.

ExceptionCallBack.exceptionOccurred(this);

따라서 가장 먼저 알아야 할 것은 이 코드에 해당하는 바이트 코드가 무엇이냐이다. ASM Bytecode Outline Plugin을 이용하면 아래와 같이 손쉽게 알아낼 수 있다.

위의 그림에서 빨간 색으로 밑줄이 그어진 부분이 우리가 필요로 하는 부분이다. 간략하게 설명하면...

  • mv.visitVarInsn(ALOAD, 0) : 0번째 변수는 항상 나 자신(this)를 가리킨다.
  • mv.visitMethodInsns(INVOKESTATIC, [className], [methodName], [methodDescription]) : 메소드를 호출한다.
    • INVOKESTATIC은 Static Method를 실행하겠다는 의미이다. methodDescription은 메소드의 파라미터와 리턴값을 의미한다. (Ljava/lang/Exception;) 은 java.lang.Exception 클래스가 호출된 메소드의 파라미터 타입이라는 것을 의미한다. V는 리턴타입이 void라는 것을 의미한다.

이제 처리해야할 마지막 작업은 위와 같은 형태의 코드를 java.lang.Exception 클래스의 생성자마다 삽입해주는 것이다. 아래에 샘플 코드가 있다.

package flowlite.exception2;

import java.io.FileOutputStream;

import org.objectweb.asm.*;
import org.objectweb.asm.commons.*;

public class ExceptionTransformer implements Opcodes {

// Convert class
public void transform(String newClassName) throws Exception {

System.out.println("Starting transformation of java.lang.Exception...");

ClassReader reader = new ClassReader("java.lang.Exception");
ClassWriter writer = new ClassWriter(ClassWriter.COMPUTE_MAXS);
ClassAdapter adapter = new ExceptionClassAdapter(writer);


reader.accept(adapter, ClassReader.SKIP_FRAMES);

byte[] b = writer.toByteArray();
FileOutputStream fos = new FileOutputStream(newClassName + ".class");
fos.write(b);
fos.flush();

}


public static void main(String[] args) {
try {
String newClassName = "Exception";
if(args.length >= 1) newClassName = args[0];
ExceptionTransformer fit = new ExceptionTransformer();
fit.transform(newClassName);
} catch(Exception ex) {
ex.printStackTrace();
}

}
}


class ExceptionClassAdapter extends ClassAdapter implements Opcodes {

public ExceptionClassAdapter(ClassVisitor visitor) {
super(visitor);

}

public MethodVisitor visitMethod(int access, String name, String desc, String sig, String[] exes) {
MethodVisitor mv = super.visitMethod(access, name, desc, sig, exes);

if(name.equals("")) { // Constructor
System.out.println("Redefine Constructor...");
ExceptionConstructorAdviceAdapter ecaa =

new ExceptionConstructorAdviceAdapter(access, name, desc, mv);
return ecaa;
}

return mv;

}
}


class ExceptionConstructorAdviceAdapter extends AdviceAdapter {

public ExceptionConstructorAdviceAdapter(int access, String name, String desc, MethodVisitor mv) {
super(mv, access, name, desc);
}

protected void onMethodEnter() {

}

protected void onMethodExit(int opcode) {
if(opcode == RETURN) {
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESTATIC,

"flowlite/exception2/ExceptionCallBack",

"exceptionOccurred", "(Ljava/lang/Exception;)V");
mv.visitEnd();
}
}

}

위의 소스 코드에 대한 설명은 다음과 같다.

  • ClassReader를 이용해 rt.jar의 원본 java.lang.Exception 클래스를 읽는다.
  • ExceptionClassAdapter(extends ClassAdapter)객체를 이용해 원본 바이트 코드에 대한 변경을 시도한다.
    • ExceptionClassAdpater는 visitMethod를 이용해 Exception 클래스의 생성자에 대한 변경을 시도한다.
    • ClassAdapter.visitMethod는 바이트 코드에서 특정 메소드에 대한 정의가 시작될 때 호출된다. 따라서 visitMethod를 재정의함으로써 원하는 메소드를 우리 입맞에 맞게 수정할 수 있다.
    • 생성자(Constructor)의 메소드 이름은 항상 이다.
  • ExceptionClassAdapter.visitMethod 메소드는 만일 메소드가 생성자()이면 ExceptionConstructorAdviceAdapter(extends AdviceAdapter) 객체를 이용해 생성자에 대한 재정의를 시도한다.
    • AdviceAdpater 객체는 onMethodEnter, onMethodExit(opcode)라는 두 메소드를 제공한다.
    • onMethodEnter는 메소드 시작 시점에, onMethodExit는 메소드 리턴 직전 시점에 수행될 코드를 정의할 공간을 마려해준다.
    • 즉, ExceptionConstructorAdviceAdapter 객체는 이 두 메소드를 재정의함으로써 메소드의 시작/끝 시점에 자신이 원하는 코드를 삽일할수 있다.

위의 설명과 같이 하나의 ClassAdapter와 AdviceAdpater를 이용해서 우리가 원하는 작업을 수행할 수 있다.

이 예제에서는 onMethodExit를 이용해서 생성자가 종료되기 직전에 우리가 원하는 바이트 코드를 삽입한다. ExceptionConstructorAdviceAdapter.onMethodExit 메소드를 좀 더 자세히 살펴보자.

protected void onMethodExit(int opcode) {
if(opcode == RETURN) {
mv.visitVarInsn(ALOAD, 0);
mv.visitMethodInsn(INVOKESTATIC,

"flowlite/exception2/ExceptionCallBack",

"exceptionOccurred", "(Ljava/lang/Exception;)V");
mv.visitEnd();
}
}

위의 코느는 생성자가 정상적으로 리턴될 때(opcode == RETURN) "ExceptionCallBack.exceptionOccurred(this)" 코드를 수정하게끔 바이트 코드를 변경한다.

ExceptionTransformer를 수행한 후 생성된 Exception Class의 바이트 코드를 보면 아래와 같이 원하는 결과를 얻었음을 알 수 있다.

// class version 49.0 (49)
// access flags 33
public class Exception extends Throwable {


// access flags 24
final static long serialVersionUID = -3387516993124229948

// access flags 1
public () : void
ALOAD 0
INVOKESPECIAL Throwable.() : void
ALOAD 0
INVOKESTATIC ExceptionCallBack.exceptionOccurred(Exception) : void
RETURN
MAXSTACK = 1
MAXLOCALS = 1

// access flags 1
public (String) : void
ALOAD 0
ALOAD 1
INVOKESPECIAL Throwable.(String) : void
ALOAD 0
INVOKESTATIC ExceptionCallBack.exceptionOccurred(Exception) : void
RETURN
MAXSTACK = 2
MAXLOCALS = 2

// access flags 1
public (String,Throwable) : void
ALOAD 0
ALOAD 1
ALOAD 2
INVOKESPECIAL Throwable.(String,Throwable) : void
ALOAD 0
INVOKESTATIC ExceptionCallBack.exceptionOccurred(Exception) : void
RETURN
MAXSTACK = 3
MAXLOCALS = 3

// access flags 1
public (Throwable) : void
ALOAD 0
ALOAD 1
INVOKESPECIAL Throwable.(Throwable) : void
ALOAD 0
INVOKESTATIC ExceptionCallBack.exceptionOccurred(Exception) : void
RETURN
MAXSTACK = 2
MAXLOCALS = 2
}

이제 마지막 단계로 ExceptionCallBack.exceptionOccurred의 클래스와 메소드를 다음과 같이 정의한다.

package flowlite.exception2;

public class ExceptionCallBack {


public static void exceptionOccurred(Exception ex) {
System.out.println("Oops~ " + ex + " has occurred...");
ex.printStackTrace();
}
}

이 작업을 마무리하고, 다음과 같이 -Xbootclasspath 옵션을 이용해서 Exception을 발생시키는 ExceptionTest2를 수행해보자.(새로 생성된 Exception.class 파일을 converted_classes/java/lang 디렉토리로 카피하는 것은 기본!!!)

java -Xbootclasspath/p:./converted_classes ExcetpionTest2

이제 Exception이 발생할 때마다 ExceptionCallBack.exceptionOccurred가 수행되므로 다음과 같은 수행 결과가 나타난다.

놀랍지 않은가?

바이트 코드에 대한 아주 간단한 조작만으로도 모든 Exception 발생에 대한 처리 로직을 추가할 수 있다.

이번 예제를 통해 ASM을 이용한 BCI의 개념이 이해되었기를 바라며, 좀 더 복잡한 예제를 통해 더 의미있는 논의를 해보기로 한다.

신고
Trackback 0 : Comment 1
  1. 마늘소스 2011.03.31 18:55 Modify/Delete Reply

    관리자의 승인을 기다리고 있는 댓글입니다

Write a comment


The Power of Byte Code Instrumenation in Java - Part2

Enterprise Java 2007.08.05 11:42
Part1에 이어...
Byte Code Instrumentation(이하 BCI)의 예제를 논의하기 전에 우선 이 시리즈에서 사용할 BCI 라이브러리인 ASM의 사용법에 대해 간략하게 알아보자
ASM
ASM은 http://asm.objectweb.org/에서 설치에 필요한 라이브러리와 개발 도구(Eclipse Plugin - AMS Bytecode Outline Plugin)들을 다운받을 수 있다. Eclipse Plugin을 이용하면 ASM을 활용하는데필요한 학습 시간을 극적으로 줄일 수 있다.
아래 그림은 Eclipse ASM Bytecode Outline Plugin을 이용해서 특정 자바 소스 파일의 Bytecode를 확인한 결과이다.
Bytecode를 직접 본 적이 없다면 아마 상당히 생소할 것이다. 하지만 앞으로 몇 가지 예제를 거치다 보면 자연스럽게 Bytecode에 익숙해지게 된다.
ASM Bytecode Outline Plugin의 가장 강력한 기능은 ASM화된코드를 손쉽게 확인할 수 있다는 것이다. 아래와 같이 [ASM]라는 이름의 아이콘을 선택하면 흉물스런 Bytecode가세련된 Java Code로 변환되는 것을 확인할 수 있다.
ASM화된 코드(ASMified Code)란 [ASM 라이브러리가 제공하는 API를 이용해서 Bytecode를 생성하는 코드]를 말한다. 이 ASM화된코드를 적절히Copy/Paste/Edit해서 필요한 거의 모든 Bytecode를 직접 생성하고 조작할 수 있다.
굉장하지 않은가?
나의느낌은전율 그 자체였다. 이 원리를 이용하면 어떠한 Java어플리케이션이라도 내가원하는 코드를 어플리케이션의 코드 변경없이 삽입할 수있다. 응용처는 그야말로 무궁무진하다.
이런 강력함때문에 Java Monitor/Profiler/Analyzer 등 성능과 관련된 대부분의 툴들이 어떤 식으로든 BCI를 이용하고 있다. 이런 기능의 예를 앞으로 다양하게 살펴볼 것이다.
ASM 라이브러리에 대한 자세한 설명은 http://asm.objectweb.org/에서 제공하는 매뉴얼과 Java Doc을 통해 얻을 수 있다. 반드시 매뉴얼을 읽어볼 것을 권장한다. ASM은 Visitor Pattern을 이용해 구현되어 있어서 직관적인 프로그래밍이 가능하다는 장점이 있다.
신고
Trackback 0 : Comments 2
  1. 멀더엄마 2007.08.05 15:36 신고 Modify/Delete Reply

    폰트가 별루야. 읽기 불편해. (역시 난 멀다빠의 안티.... )

  2. 욱짜 2007.08.05 16:10 신고 Modify/Delete Reply

    내가 맑은 고딕을 고정으로 사용하고 있어서 다른 컴에서 어떻게 보이는지 전혀 몰랐네.ㅠㅠ

Write a comment


AOP(Aspect Oriented Programming) in Java - Part 5

Enterprise Java 2007.07.30 17:06

Part 4에 이어...

AspectJ의 Load Time Weaving

ApsectJ 1.5는 AspectWerkz라는 신흥 AOP 컴포넌트를 흡수하면서 Load Time Weaving 기능을 크게 향상시켰다. Load Time Weaving이란 말 그대로 클래스가 로드되는 시점에 Weaving 작업을 수행하는 것을 의미한다.

전통적으로 AspectJ에서는 ajc(AspectJ Compiler)라는 컴파일러를 사용해서 사용자가 작성한 Class 파일이나 Jar 파일을 컴파일 시간에 Weaving하는 방식을 지원했다. 비록 이 방법이 아직까지도 가장 보편적이고 또 편리한 방법이긴 하지만, 컴파일시 Weaving은 역시 불편한 방법이다. 하지만!!! 성능 면에서는 가장 유리한 방법이라는 것은 다시 한번 염두에 두자

AspectJ에서 Load Time Weaving이 적용되는 방식은 아래 그림과 같다.

aop.xml

aop.xml 파일은 LTW의 룰을 지정하는 역할을 한다. 즉 어떤 Aspect를 어떻게 Weaving 할 것인지 지정할 수 있다.

아래에 aop.xml의 간단한 예제가 있다.

위의 aop.xml 파일은[aop.ltw.SimpleLTWAspect]라는 이름의 Aspect를 사용하며, 이 Aspect를 이용해서 Weaving을 수행할 것을 지정한다.

AspectJ의 LTW 모듈은 [클래스패스(Classpath)/META-INF]에 있는 모든 aop.xml 파일을 불러와서 Weaving 작업을 수행한다.

aop.xml이 제공하는 문법은 매우 다양하고 강력하다. 어떤 Aspect를 어떤 타겟(비지니스 로직)에 대해 어떤 조건(pointcut)으로 사용할지를 자유롭게 지정할 수 있다. 예를 들어 Abstract Aspect를 만든 후 aop.xml에서pointcut을 정의할 수도 있다. aop.xml을 사용하는 상세한 방법은 AspectJ Manual을 참조한다.

LTW의 간단한 예제

아래에 간단한 Aspect가 있다.

이 Aspect의 역할은 Method의 시작과 끝을 잡아서 수행 시간을 측정하는 것이다. 어플리케이션 성능 측정을 위한 가장 기본적인 기능을 구현할 것이라고 볼 수 있다.

이 Aspect를 다음과 같이 ajc를 이용해서 컴파일한다.

c:aspectj1.5binajc -1.5 -cp ../..;c:aspectj1.5libaspectjrt.jar SimpleLTWAspect.aj

컴파일에 성공하면 SimpleLTWApsect.class 파일이 생긴다. 이 Aspect 파일과 위에서 샘플로 사용한 aop.xml 파일을 이용해서 LTW을 수행하는 명령어는 다음과 같다.

(SimpleLTW 객체는 몇 개의 메소드를 반복적으로 호출하는 단순한 객체이다)

java -javaagent:aspectjweaver.jar -cp ../.. aop.ltw.SimpleLTW

아래 결과를 보면 우리가 원하는 대로 각 메소드를 실행하는데 걸린 시간이 계산되어 나오는 것을 알 수 있다.

위에서 본 간단한예제만으로도 AspectJ에서 제공하는 LTW의 유연함과 강력함을 느낄 수 있으리라 믿는다.

PS)

Java 5 (JDK 1.5)부터는 java.lang.instrument 패키지를 이용해서 Byte Code Instrumentation(BCI)을 직접적으로 지원한다. 더 이상 BCI가 어둠의 자식이 아니라는 것을 의미한다.오히려 BCI가 Sun에서도 인정하는 보편적인 방법론임을 의미한다.

자연스럽게, AspectJ 1.5의 LTW도 이 기능을 이용한다. 위의 예에서 "-javaagent:aspectjweaver.jar" JVM 옵션이 java.lang.instrument 패키지를 이용한다는 것을 의미한다. 즉, aspectjweaver.jar 내에 클래스 로드 타임시 실시간으로 클래스를 Weaving하는 기능을 제공하는 Class Transformer가 존재한다.

JDK 1.4에서는 VM 레벨에서 BCI가 지원되지 않는다.JDK 1.4라면 아래와 같은 형식으로 사용가능하다.

java -classpath[aspectjweaver.jar]

-Djava.system.class.loader=org.aspectj.weaver.loadtime.WeavingURLClassLoader

-Daj.class.path=. -Daj.aspect.path=.[YourClass]

또는 JRockit에서는 다음과 같은 JVM옵션을 사용할 수 있다.

-Xmanagement:class=org.aspectj.weaver.loadtime.JRockitAgent

신고
tags : aop, java, performance
Trackback 0 : Comment 1
  1. 2013.02.20 09:25 Modify/Delete Reply

    비밀댓글입니다

Write a comment


AOP(Aspect Oriented Programming) in Java - Part 4

Enterprise Java 2007.07.27 18:39

Part 3에 이어...

AspectJ에서 Concern 구현 하기

아래에 심플한(?) 비지니스 로직을 구현하는 객체가 있다. 우리의 걱정 거리는 비지니스 로직에서 Exception이 발생할 때마다 상세한 발생 상황을 기록하는 것이다.

우리의 비지니스 로직은 다음과 같다.

이 비지니스 로직에 대한 우리의 Concern을 처리해야 하는 상황은 다음과 같다.

  • ExceptionGenerator의 로직을 수행하는 과정에서 Exception이 발생하면 이것을 캡쳐해서 기록하고 싶다.
  • 이 때 어떤 메소드를 호출하다가 Exception이 발생했는지, Exception의 종류는 무엇인지 등의 정보가 종합적으로 기록하고 싶다.

이 상황을 AOP 없이 처리하려면 제 아무리 자바의 고수라고 하더라도 다음과 같은 방식으로 일일이 소스를 변경해야 한다.

try { doSomething1() } catch(Exception ex) {

logger.log("Error " + ex + " occurred when executing ExceptionGenerator.doSomething1()...");

}

비록 Java Logging API나 Log4j 같은 라이브러리들이 이러한 작업을 처리하는데 상당히 도움이 되지만, 핵심 로직안에 우리의 Concern을 처리하는 로직을 넣어야 한다는 기본적인 사실에는 전혀 변화가 없다.

하지만, AspectJ를 사용하면... ? 핵심 로직에는 Exception Handling에 관련된 소스를 전혀 추가할 필요없이 다음과 같은 형태의 Aspect만을 만들어주면 된다.

Exception Aspect Version 1

매우 심플한 Aspect지만 두 가지의 핵심적인 정보를 담고 있는 완전한 형태의 Aspect이다.

  • pointcut : 모든객체의 메소드콜을 횡단으로 매치시키는 call (* *.*(..)) 이라는 pointcut이 callpoint라는 이름으로 정의되어 있다.
  • after advice : callpoint pointcut에서 Exceptoin이 발생한 이후(after + throwing) 수행할 advice가 정의되어 있다.

AspectJ에서는 대부분의 Concern이 pointcut과 advice의 조합으로 이루어진다. 즉 어떤 지점(pointcut)에서 어떤 일(advice)를 수행할 지가 바로 AspectJ가 구현하는 Concern에 대한 해결책이 된다.

위의 ExceptionAspect와 ExceptionGenerator를 Weaving해서 수행하면 다음과 같은 결과가 나온다.

결과1

이제 ExceptionAspect를 좀 더 다듬어서 보다 완전한 형태의 정보를 얻을 수 있도록 해보자.

Exception Aspect Version 1

더욱 세련된 모양의 Aspect가 구현되었음을 확인할 수 있다. Version 1에 비해 다음과 같은 특징들이 추가되었다.

  • After advice에서 Exception발생시 Exception 객체를 받는다. 이렇게 받은 객체를 이용해서 필요한 정보를 추출한다.
  • thisJoinPointStaticPart (또는 thisJoinPoint.getStaticPart())를 이용해 어떤 지점에서 발생한 Exception인지를 알아낸다.

위의 Aspect를 보고 "아... 정말 내가 원하던 방법론이다"라고 감탄을 했다면 이미 일류 프로그래머이거나 일류 프로그래머가 될 잠재력을 가지고 있는 사람일 것이다.

AspectJ 혹은 AOP를 현재 프로젝트에 사용하고 싶은 욕구가 이는가...!!!

신고
tags : aop, java, performance
Trackback 0 : Comment 0

Write a comment

티스토리 툴바