Java로 소프트웨어를 개발하는 사람이라면 누구나 한번쯤 Out Of Memory Error(이하 OOME)를 만나 보았을 것이다. OOME의 경우 Java의 다른 Error와 다르게 원인과 대응을 하기가 쉽지는 않다.

경험에 비추어 보면 OOME가 발생하는 시점은 대부분 개발이 대부분 완료된 후 사용자 테스트 혹은 인수 테스트단계에서 많이 발생한다. 즉 개발 단계에서 수행하는 단위 테스트의 경우 목적 기능에 대한 검증 위주로 진행 되기 때문에 식별이 어렵고 가동 초기 단계 혹은 이와 유사한 테스트 환경에서 주로 발생하게 되는 것이다. 때문에 OOME가 발생하는 시점에서는 빠르게 대응해야 하는데 경험에 의한 JVM Option을 통한 처리 방법과 Dump 파일의 분석을 통해 대응을 하게 된다

JVM Option
Java의 최대 장점은 한번 만들어진 애플리케이션은 JVM이 존재하는 OS/하드웨어에서도 동작을 한다는 것이다. 그러나 이는 동작을 한다는 것이지 성능을 보장하지는 않는다. 때문에 OS/하드웨어별로 JVM의 설정을 변경하여 성능을 보장하기 한 것이 JVM옵션이다. JVM옵션은 JVM표준인 Standard 옵션과 Non-Standard옵션으로 구분 된다.

  • Standard Option : JVM표준 옵션으로 벤더(JVM을 만드는 회사. Oracle(Sun),IBM,HP,..)와 상관없이 동일한 옵션을 가진다. 32/64Bit,클라이언트/서버 모드 설정과 같이 환경위주의 설정
  • Non-Standard Option : JVM표준이 아니라 Java의 버전,벤더에 따라 추가되거나 없어 지기도 한다. 그러나 성능에 직접적으로 영향을 주는 옵션들이다. 주로 -X,-XX 로 시작된다.

참조 : http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html

Dump 파일 분석
애플리케이션 운영 중 장애 혹은 성능 상 문제가 발생하였을 때 애플리케이션의 상태를 스넵샷 형태로 파일로 저장한 것이 Dump파일이다. 때문에 Dump파일의 분석을 통해 장애 발생 시 애플리케이션이 어떠한 상태 였으며 어떤 점이 문제가 되었는지 확인 할 수 있다.
분석에는 여러 가지 Tool들이 있으며 VisualVM,Eclipse mat 등이 있다. 이를 사용하는 방법은 다음 기회에 설명하도록 하겠다.

아래 내용은 최근 프로젝트를 수행하면서 OOME가 발생하였을 때 오랜만에 다시 공부해본 Java의 메모리 구조에 대해 정리한 내용이다.

Java Virtual Machine의 구조와 JVM Option

OOME는 다양하게 발생하는데 대부분 다음 예외를 많이 접하게 될 것이다.

Exception in thread “main”: java.lang.OutOfMemoryError: Java heap spac
Heap size의 부족으로 Java Object를 Heap에 할당하지 못하는 경우. JVM 옵션 설정을 하지 않은 경우 많이 발생한다.

Exception in thread “main”: java.lang.OutOfMemoryError: PermGen space
Class나 Method 객체를 PermGen space에 할당하지 못하는 경우 발생하며 애플리케이션에서 너무 많은 class를 로드할 때 발생한다. 주로 잘못된 설계/구현에 의해 발생한다.    -XX:PermSize, -XX:MaxPermSize Option을 이용하여 오류를 수정하기도 한다.

Exception in thread “main”: java.lang.OutOfMemoryError: Requested array size exceeds VM limit
사용할 배열의 사이즈가 VM에서 정의될 사이즈를 초과할 때 발생한다.

Exception in thread “main”: java.lang.OutOfMemoryError: request bytes for . Out of swap space?
Java는 런타임시 물리적 메모리를 초과한 경우 가상메모리를 확장해 사용하게 되는데 가용한 가상메모리가 없을 경우 발생한다.

Exception in thread “main”: java.lang.OutOfMemoryError: (Native method)
JVM에 설정된 것 보다 큰 native메모리가 호출 될 때 발생한다.

 

위와 같은 OOME를 해결하기 위해서 먼저 JVM구조를 이해하고 있어야 한다.

JVM-1

그림-1 Java Virtual Machine Model


JVM은 위에서 보는 것과 같이 여러 영역으로 구성이 되어 있으며 JVM Option을 통해 각 영역별로 메모리 할당이 가능하다. 일반적으로 OOME가 발생하면 각 영역별로 메모리 옵션을 늘려 대응을 하게 된다.

아래 그림은 각 영역들과 JVM option을 통해 관리가 가능한 방식을 설명한 것이다.


JVM-2

그림 2 HotSpot JVM 구조와 영역별 Option


하지만 불행하게도 시스템의 물리적 리소스는 한정이 되어 있어 한 영역의 메모리 사이즈를 늘리게 된다면 다른 영역의 메모리는 줄어 들어 또 다른 문제가 발행할 수도 있다. 예를 들어 전체 2G 메모리 사용 중이고 Heap영역이 1.5G    PermGen space 영역이 256MB를  할당하였다. 이때 PermGen space에서 OOME가 발생하여  516MB로 늘리게 되면 Heap영역은 1G로 줄여들게 된다. 때문에 Dump파일의 분석을 통해 정확한 진단을 한 후 원인을 수정하도록 한다.


잘못된 Application과 OOME

위에서 설명한 영역들에서 발생하는 OOME 중 오늘 이야기 하고자 하는 영역은 Permanent Space에서 발생하는 오류 이다. 보통 잘못된 코딩으로 인해 발생하게 되는 OOME의 대부분은 Permanent Space에서 발생하게 되는데 최근 프로젝트에서 발생한 오류도 Permanent Space에서 발행한 오류였다. 친절하게도 java에서는 OOME 가 발생한 영역을 아래와 같이 설명해 준다.


JVM-3

그림 3 Java Out Of Memory Error 예시


Perm Gen space에서 발생한 오류에 대해 대응하기 전에 Perm Generation 영역에 대해서 알아보면 Permanent Generation은 young과 old를 구분하는 Generational Collector 방식인 HotSpot JVM 중 한 영역으로 객체의 생명 주가기 길다고 판단되는 객체들을 이 영역에 할당하여 GC대상에서 제외를 하기 위해서 만들어진 영역이다.  주로 자바의 Class 객체들이나 문자열에 속한 String 객체들이 위치한다.

일반적으로 Class의 로딩은 시스템의 Class path에 의해서 로드된 Class 객체들과 에플리케이션 내 구현으로 다이나믹하게 로드되는 class들이 있는데 주로 문제는 애플리케이션 내 로직으로 다이나믹 하게 생성되는 Class들에 의해서 발생된다. 최근에 많이 사용되는 Spring, MyBatis등과 같은 프레임워크 등이 이와 같은 방식을 취하고 있다. 때문에 위 프레임워크들을 사용할 경우 OOME발생에 주의를 해야 한다.

최근에 발생한 원인은 이들 프레임워크 중 iBatis(구 MyBtis)를 잘못 사용하여서 발생한 것으로 iBatis를 잘못 사용하여 발생한 경우로 볼 수 있다.

프로젝트에서 iBatis를 사용하기 위한 기본 아키텍처 구조는 다음과 같다.

JVM-5

그림 4 iBatis Dao를 이용한 데이터 접근


iBatis의 dao.xml파일 내 사용할 DAO객체 정보를 설정하고 런타임 시 DaoFactory에서 dao.xml을 읽어 DAO객체를 인스턴스화 하는 구조로 다음 같이 구현하였다.

 

이 과정에서 iBatis는 내부적으로 DaoManager 클래스를 다이나믹하게 생성하고 로드하는 과정이 존재한다. 이때 단순하게 이 클래스만 로드되는 것이 아니라 이 클래스에 연관된 다른 클래스들(위 그림에서 보면 색으로 표시된 부분)도 같이 로드되고 위에서 설명한 것처럼 Perm Gen spac에 할당된다

때문에 가급적 메모리의 부하를 적게 하게 위해서 설계 원칙 중 하나로 도메인 별로는 하나의DaoFactory만 생성하고 이를 싱글톤 패턴을 적용하여 관리 하고 있으며 이미 여러 프로젝트를 통해 검증된 방식으로 OOME가 발생할 수 없는 구조로 볼 수 있다.

그러나 Dump파일을 분석한 결과 DAOManager클래스 객체가 Perm Gen space에 매우 많이 쌓여져 있는 것으로 분석되었다.

실제 구현 소스를 살펴 보니 어찌된 이유 인지 같은 DB에 접속하는 동일 도메인 내에서 여러 개의 DaoFactory가 구현되어 있다. 원인을 살펴본 결과 각 영역별 담당자별로 DaoFactory를 만들어 사용하고 있던 것이 문제였다.

JVM-6

그림 5 설계 방식과 실제 구현방식의 비교


이를 원래 설계 원칙 대로 수정한 결과 더 이상 OOME에 대한 오류는 발생하지 않았으며 정상 동작이 되었다.  이와 같이 아무리 잘 설계된 아키텍처라 하더라도 잘못된 구현으로 인해 OOME가 발생할 수 있기 때문에 항상 조심하여야 한다.


마치며

Java라는 언어가 이전에 탄생된 언어에 비해서 메모리 관리에 있어 편리한 장점이 있다고 알려져 있고 많이 개발자들이 애플리케이션을 개발하게 되면서 메모리관리에 대해서는 GC가 해결을 해 줄 것이라고 생각하고 JVM의 구조를 이해하거나 메모리 영역에 대해서는 개발 단계에서 많이 고려하지 않는다. 그러나 최근은 고객의 요구사항은 점점 다양해지고 시스템도 같이 복잡해져 가면서 메모리 문제는 언제 어디서나 마주 칠 수 있다. 비록 과거에 비해 시스템의 사양이 좋아져 대부분이 애플리케이션이 많은 메모리를 이용할 수 있다 하더라도 자원은 늘 한정되어 있는 법이니까.

Java의 Reflaction 기능을 이용하거나 이용하거나 Spring이나 MyBatis와 같은 프레임워크를 사용하는 경우는 메모리에 대한 고려를 하여 설계를 하여야 한다. 또한 JVM의 메모리 구조를 알고 OOME가 발생하였을 때 빠르게 대처 할 수 있는 능력을 가져야 하는 것이 개발자로서 필요한 소양이 아닐까 한다

참조 URL

http://docs.oracle.com/javase/7/docs/webnotes/tsg/TSG-VM/html/preface.html

http://www.oracle.com/technetwork/java/javase/tech/vmoptions-jsp-140102.html#PerformanceTuning

http://www.youtube.com/watch?v=VQ4eZw6eVtQ

http://www.oracle.com/technetwork/java/javase/gc-tuning-6-140523.html



출처 : http://dryang.egloos.com/viewer/4005363

'Language > java' 카테고리의 다른 글

Jakarta Commons Net 에서 FTP  (0) 2014.01.17
URLConnection 에서 cookie 제어  (0) 2013.10.07
JSTL 사용하기  (0) 2012.06.05
[스크랩] JVM 튜닝  (0) 2010.04.21
[스크랩] JVM 튜닝  (0) 2010.04.21

Jakarta Commons Net 에서 FTP 사용시 목록이 안보일 경우

 

 

Jakarta Commons Net 기본 http://www.jakartaproject.com/article/jakarta/1113911351166

Commons net API http://jakarta.apache.org/commons/net/apidocs/index.html

Commons net http://jakarta.apache.org/commons/net/

 

 조회 권한이 없는 경우

   로그인한 유저의 권한이 적당한지 체크해 본다

   또는 텔넷을 통해 해당 유저로 ftp 테스트를 해 본다

 

ⓑ Passive mode를 사용해 본다

   FTPClient ftpClient = new FTPClient();

   ftpClient.connect(server);

   ...

   ftpClient.login(username, password);

   ftpClient.enterLocalPassiveMode();

   ...

 

관련 함수
   enterRemoteActiveMode(InetAddress host, int port)
 
 
ⓒ 날짜 포맷을 변경한다
현재 서버의 날짜가 한글로 나오도록 설정 되었다면 목록이 안나올 수가 있습니다
 
FTPClient ftpClient = new FTPClient();
FTPClientConfig config = new FTPClientConfig(FTPClientConfig.SYST_NT); 
config.setServerLanguageCode("ko");
config.setDefaultDateFormat("d MMM yyyy");
config.setRecentDateFormat("d MMM HH:mm");
ftpClient.configure(config);
ftpClient.connect(server);
...
 
운영체제에 맞게 설정하세요
  FTPClientConfig.SYST_NT
  FTPClientConfig.SYST_MVS
  FTPClientConfig.SYST_OS2
  FTPClientConfig.SYST_OS400
  FTPClientConfig.SYST_UNIX
  FTPClientConfig.SYST_VMS
 
언어설정값은 http://ftp.ics.uci.edu/pub/ietf/http/related/iso639.txt 에서 확인하세요
날짜 포맷은 java.text.SimpleDateFormat 의 형식을 따릅니다
 



'Language > java' 카테고리의 다른 글

Java Out Of Memory 오류와 메모리 구조  (0) 2014.02.21
URLConnection 에서 cookie 제어  (0) 2013.10.07
JSTL 사용하기  (0) 2012.06.05
[스크랩] JVM 튜닝  (0) 2010.04.21
[스크랩] JVM 튜닝  (0) 2010.04.21


cookie 서포트


cookie 서포트 에는 다음의 내용이 포함됩니다.

처음에

cookie 는 , 클라이언트 측에 데이터를 격납하는 1 개의 방법입니다. cookie (은)는 , 개인에게 특화한 포털 사이트 작성 , 유저 설정의 추적 , 및 Web 사이트로의 로그인으로 광범위하게 사용됩니다. Web 사이트에서 cookie 를 사용하고 있는 기업 고객의 경우 ,Java Plug-in 및 Java Web Start 으로 cookie 가 서포트되고 있으면(자) , 클라이언트측의 것 Java 의 배치가 용이하게 됩니다.

cookie 서포트를 이용하면(자) ,Web 서버로부터의 cookie 를 ,Java 어플리케이션으로부터 해당 Web 서버에 돌려줄 수가 있습니다. 이것에 의해 , 클라이언트 상태에 관한 정보를 서버에 제공할 수 있습니다.

Java Plug-in (와)과 Java Web Start 에서의 cookie 서포트

cookie 에는 , 주로세션 cookie 및지속 cookie 의 2 종류가 있습니다.

세션 cookie

세션 cookie 은 애플릿 또는 어플리케이션의 세션의 사이에 메모리에 격납됩니다. 세션 cookie 은 애플릿 또는 어플리케이션이 종료했을 때에 기한 마감이 되어 , 자동적으로 삭제됩니다. 통상 , 이러한 cookie 은 , 유저 개인을 특정하지 않는 세션 ID 을 격납하는 것으로써 , 유저가 페이지로부터 페이지로 이동할 때 , 반복 로그인할 필요가 없어집니다. 세션 cookie 은 , 예를 들면 소비자가 쇼핑이나 에 추가한 상품을 추적하기 위해서 , 상용 Web 사이트에 있어 넓게 사용되고 있습니다.

지속 cookie

지속 cookie 은 지속적인 기억 영역에 보관되어 어플리케이션이 종료해도 삭제되지 않습니다. 지속 cookie 은 기한 마감이 되었을 경우에 삭제됩니다. 지속 cookie 은 특정의 Web 사이트에 대한 유저 설정을 유지하는 것으로 , 장래의 세션으로 그러한 설정을 사용할 수가 있습니다. 지속 cookie 은 , 개별의 유저를 식별하기 위해서 사용되므로 ,Web 사이트에 있어 유저가 , 어떻게 넷 서핑을 하고 있는지를 분석하기 위해서(때문에) 사용됩니다. 이러한 cookie 은 방문자의 인원수 , 특정의 페이지를 열람하기 위해서 소비된 시간의 평균 ,Web 사이트의 전반적인 퍼포먼스등의 정보를 제공하기 위해서도 사용됩니다. 이러한 cookie 은 통상 , 장기간 , 경우에 따라서는 몇 년간 , 유저를 추적하도록(듯이) 설정됩니다.

테크니컬 서포트

Java Plug-in 및 Java Web Start 은 양쪽 모두의 종류의 것 cookie 을 모든 플랫폼상에서 취급할 수 있도록(듯이) 서포트합니다.

Java Plug-in (은)는 , 브라우저 자체의 cookie 스토어를 사용해 지속적인 cookie 서포트를 제공합니다. Internet Explorer 그리고 실행하면(자) ,IE 의 cookie 스토어가 사용되어Mozilla 로 실행하면(자) ,Mozilla 의 cookie 스토어가 사용됩니다. 각각의 브라우저가 다른 cookie 의 폴리시를 가지기 위해서(때문에) ,cookie 를 취급할 때의 동작은 브라우저의 cookie 제어에 의존합니다. 예를 들면 , 브라우저가 써드파티의 것 cookie 을 거부하도록(듯이) 설정되어 있는 경우 , 이 브라우저로 Java Plug-in 를 실행할 경우에는 , 그 폴리시가 적용됩니다.

Java Web Start (은)는 ,Internet Explorer 의 cookie 스토어를 사용하는 것으로써 ,Windows 위에서 지속 cookie 의 서포트를 제공해 ,cookie 를 취급할 때의 동작은 IE 의 cookie 제어에 의해 결정됩니다. Linux/Solaris 위에서는 ,Java Web Start 은 독자적인 cookie 스토어 실장을 사용하는 것으로써 , 지속 cookie 의 서포트를 제공합니다.

프로그램에 의한 cookie 에의 액세스

어플리케이션이 디폴트의 것 cookie 의 처리를 오버라이드(override)하기 위한 방법은 여러종류 있습니다. 다음에 그 중의 4 개를 나타냅니다.

void URLConnection.setRequestProperty(String key, String value)

이 방법에 의해 , 어플리케이션은 URLConnection 에 독자적인 cookie 헤더를 설정하고 나서 접속을 확립할 수가 있습니다.

URL url = new URL( "http://java.sun.com/" );
URLConnection conn = url.openConnection(); 
conn.setRequestProperty("Cookie", "foo=bar"); 
InputStream is = conn.getInputStream(); 
.....

접속이 확립되었을 때에 , 독자적인 cookie 은 HTTP/HTTPS 요구 헤더내에서 , 서버에 송신됩니다.

Map<String, List<String>> URLConnection.getHeaderFields()

접속이 확립된 후 에 , 서버는 HTTP/HTTPS 응답 헤더내에서 cookie 를 돌려줍니다. 다음에 설명하는 방법에 의해 , 어플리케이션은URLConnection 을 개입시켜 cookie 의 값을 취득합니다.

URL url = new URL( "http://java.sun.com/" ); 
URLConnection conn = url.openConnection(); 
conn.connect(); 
..... 
Map<String, List<String>> headers = conn.getHeaderFields(); 
List<String> values = headers.get("Set-Cookie"); 

String cookieValue = null; 
for (Iterator iter = values.iterator(); iter.hasNext(); ) {
     String v = values.next(); 
     if (cookieValue == null)
         cookieValue = v;
     else
         cookieValue = cookieValue + ";" + v;
}

접속이 확립된 후 에 , 서버는 HTTP/HTTPS 응답 헤더내에서 cookie 를 돌려줍니다.

Map<String,List<String>> CookieHandler.get(URI uri, Map<String,List<String>> requestHeaders)

이 방법에 의해 어플리케이션은 , 주어진 URL 에 대한 , 세션 cookie 또는 지속 cookie 을 취득할 수 있습니다.

String retrieveCookie(URL url) 

     String cookieValue = null;

     CookieHandler handler = CookieHandler.getDefault();
     if (handler != null)    {
          Map<String, List<String>> headers = handler.get(url.toURI(), new Map<String,                                                           List<String>>());
          List<String> values = headers.get("Cookie");
          for (Iterator<String> iter=values.iterator(); iter.hasNext();) {
               String v = iter.next(); 

               if (cookieValue == null) 
                    cookieValue = v; 
               else
                    cookieValue = cookieValue + ";" + v; 
          } 
     } 
     return cookieValue; 
}

CookieHandler (은)는 , 브라우저 또는 시스템의 cookie 격납을 추상화 한 것으로 , 실행하기 위해서는 추가의 시큐러티 액세스권이 필요합니다. 상기의 코드는 어플리케이션이 신뢰되고 있는 경우에만 정상적으로 실행할 수 있습니다.

void CookieHandler.put(URI uri, Map<String,List<String>> responseHeaders)

이 방법에 의해 어플리케이션은 , 주어진 URL 에 대한 세션 cookie 또는 지속 cookie 을 설정할 수 있습니다.

void setCookie(URL url, String value) 

     CookieHandler handler = CookieHandler.getDefault(); 
     if (handler != null)    { 
          Map<String, List<String>> headers= new Map<String, List<String>>(); 
          List<String> values = new List<String>(); 
          values.add(value); 
          headers.put("Cookie", values); 

          handler.put(url.toURI(), headers); 
     } 
}

CookieHandler (은)는 , 브라우저 또는 시스템의 cookie 격납을 추상화 한 것으로 , 실행하기 위해서는 추가의 시큐러티 액세스권이 필요합니다. 상기의 코드는 어플리케이션이 신뢰되고 있는 경우에만 정상적으로 실행할 수 있습니다.

상세 정보



'Language > java' 카테고리의 다른 글

Java Out Of Memory 오류와 메모리 구조  (0) 2014.02.21
Jakarta Commons Net 에서 FTP  (0) 2014.01.17
JSTL 사용하기  (0) 2012.06.05
[스크랩] JVM 튜닝  (0) 2010.04.21
[스크랩] JVM 튜닝  (0) 2010.04.21

1. JSTL을 사용하기 위한 라이브러리를 다운

[이곳] 에서 다운받을 수 있습니다. 현재 시점에서는 1.1.2가 최신버전이군요.

2. 라이브러리 추가

다운받은 파일을 열어보면 standard.jar  jstl.jar 두개의 파일이 존재합니다. 둘 모두를 개발중인 프로젝트에 추가합니다.

3. JSP 페이지의 맨 위에 taglib 정의 추가

프로젝트의 맨 위에 다음을 추가해 줍니다.

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>


 

4. JSTL 문법을 사용 (몇가지 예시)

<c:forEach>를 사용한 특정 범위의 숫자값을 순환

<table>
<tr><th>Value</th>
   
<th>Square</th></tr>
<c:forEach var="x" begin="0" end="10" step="2">
 
<tr><td><c:out value="${x}"/></td>
     
<td><c:out value="${x * x}"/></td></tr>
</c:forEach>
</table>


위의 코드는 0부터 10까지 2씩 증가하는 순환문을 뜻합니다. 현재의 증가값은 변수 x에 저장됩니다. 결과는 다음과 같이 출력이 됩니다.

사용자 삽입 이미지



<c:forEach>태그를 이용한 Collection형 배열을 처리
forEach는 Collection, Map, Iterator, Enumeration, Array(Object/Primitive), 쉼표로 구분된 String, SQL쿼리 결과값(javax.servlet.jsp.jstl.sql.Result) 등의 순환을 지원합니다.

<table>

 
<c:forEach items="${entryList}" var="blogEntry">
   
<tr><td align="left" class="blogTitle">
     
<c:out value="${blogEntry.title}" escapeXml="false"/>
   
</td></tr>
   
<tr><td align="left" class="blogText">
     
<c:out value="${blogEntry.text}" escapeXml="false"/>
   
</td></tr>
 
</c:forEach>
</table>


위의 모드는 블로그 글을 순환하며 출력하는 예시입니다. ${entryList}는 title, text를 멤버 변수로 갖는 특정 객체의 집합 배열입니다. 현재 순환중인 객체가 blogEntry에 저장되며 .을 사용하여 title과 text를 출력하는 예시입니다.

여기에 새롭게 현재 순환중의 상태를 확인할 수 있는 varStatus라는 값을 사용할 수 있습니다. 말을 길게 할 필요없이 코드로 보여드리겠습니다.

<table>
 
<c:forEach items=
   
"${entryList}" var="blogEntry" varStatus="status">
   
<tr><td align="left" class="blogTitle">
     
<c:out value="${status.count}"/>.
     
<c:out value="${blogEntry.title}" escapeXml="false"/>
   
</td></tr>
   
<tr><td align="left" class="blogText">
     
<c:out value="${blogEntry.text}" escapeXml="false"/>
   
</td></tr>
 
</c:forEach>
</table>


위에 추가된 ${status.count}에서는 현재 몇번째 순환중인지 값을 확인할 수 있습니다.
사용가능한 변수의 종류는 다음과 같습니다.

current : 현재 순환중인 아이템을 가져옵니다.
index : 현재 순환중인 아이템의 인덱스(0베이스)를 가져옵니다.
count : 현재 순환중인 아이템의 인덱스(1베이스)를 가져옵니다.
first : 현재 순환중인 아이템이 첫번째 아이템인지 여부를 확인합니다. (Boolean)
last : 현재 순환중인 아이템이 마지막 아이템인지 여부를 확인합니다. (Boolean)
begin : forEach에서 지정할 수 있는 begin값을 가져옵니다.
end : forEach에서 지정할 수 있는 end값을 가져옵니다.
step : forEach에서 지정할 수 있는 step값을 가져옵니다.


<c:if>를 사용한 조건문 활용
다음의 코드는 첫번째 아이템이 순환중일 경우 블로그글이 언제 작성되었는지 날짜를 출력하도록 수정된 코드입니다. test안에 Boolean형이 반환될 수 있는 어떤 수식을 사용해도 됩니다.

<table>
 
<c:forEach items=
   
"${entryList}" var="blogEntry" varStatus="status">
   
<c:if test="${status.first}">
     
<tr><td align="left" class="blogDate">
           
<c:out value="${blogEntry.created}"/>
     
</td></tr>
   
</c:if>
   
<tr><td align="left" class="blogTitle">
     
<c:out value="${blogEntry.title}" escapeXml="false"/>
   
</td></tr>
   
<tr><td align="left" class="blogText">
     
<c:out value="${blogEntry.text}" escapeXml="false"/>
   
</td></tr>
 
</c:forEach>
</table>


그런데 정말 희안하게도 위의 <c:if>에는 else가 존재하지 않습니다. 그래서 다음의 방법을 사용하곤 합니다.

<c:choose>를 사용한 다중 조건문 활용
다음의 예제는 pageContext라는 컨텍스트 객체에 접근하여 요청의 스킴을 읽어오는 예제입니다. HTTP로 접속했을때와 HTTPS를 통해 접속했을때 다른 메시지를 출력하는 예시입니다. 추가로 둘다 아닐경우 오류 메시지를 출력하도록 하였습니다.

<c:choose>
 
<c:when test="${pageContext.request.scheme eq 'http'}">
    This is an insecure Web session.
 
</c:when>
 
<c:when test="${pageContext.request.scheme eq 'https'}">
    This is a secure Web session.
 
</c:when>
 
<c:otherwise>
    You are using an unrecognized Web protocol. How did this happen?!
 
</c:otherwise>
</c:choose>



<c:url>를 사용하여 주소 생성
JSTL에서는 <c:url>을 지원하는데요 이 태그는 현재의 서블릿 컨텍스트 이름을 자동으로 앞에 붙여주고 세션관리와 파라미터의 이름과 값의 인코딩을 자동으로 지원합니다. 기본적인 사용법은 다음과 같습니다.

<a href="<c:url value='/content/sitemap.jsp'/>">View sitemap</a>


간단하죠? 여기서 더 나아가 <c:param>을 사용하여 파라미터를 추가할 수 있습니다.

<c:url value="/content/search.jsp">
 
<c:param name="keyword" value="${searchTerm}"/>
 
<c:param name="month" value="02/2003"/>
</c:url>


위의 코드로써 생성되는 URL은 기본적으로 서블릿컨텍스트가 붙게 되며 세션쿠키를 사용중이라면 추가적으로 파라미터들만 추가되며 다음의 모습을 가지게 됩니다.

/blog/content/search.jsp?keyword=foo+bar&month=02%2F2003


만약에 세션 쿠키가 존재하지 않는다면 다음과 같은 결과를 나타내게 됩니다. 마찬가지로 파라미터들은 URL 인코딩되어 출력됩니다.

/blog/content/search.jsp;jsessionid=233379C7CD2D0ED2E9F3963906DB4290
?keyword=foo+bar&month=02%2F2003



<c:import>를 사용하여 페이지 첨부하기
JSP에는 기본적으로 두가지 방법의 페이지 안에 다른 컨텐츠를 추가하는 방법이 존재합니다. include지시자와 <jsp:include> 액션이 있는데요. 하지만 둘 모두 같은 웹 어플리케이션 또는 서블릿 컨텍스트 안에있는 페이지만을 불어들일 수 있습니다. core라이브러리에 있는 <c:import>액션은 좀더 일반적이고 강력한 기능을 가진 <jsp:include>로 볼 수 있습니다. 사용 문법은<c:url>과 매우 배슷하며 심지어 <c:param>도 그대로 사용할 수 있습니다.

<c:import url="ftp://ftp.example.com/package/README"/>


<c:import>에는 var와 scope 두가지 필수적이지 않은 속성이 존재하는데요. var의 경우에는 불러들인 페이지를 곧바로 출력하지 않고 String형 변수로 담아두기 위해 사용됩니다. scope는 이 변수의 스코프를 지정할 수 있습니다. 기본적으로 page로 되어있습니다.

<c:catch>로 예외처리 하기
길게 설명할 필요가 없을것 같네요. <c:import>는 ftp에도 접속이 가능합니다. 다음에 보여드릴 코드의 경우 만약에 해당 위치에 파일이 존재하지 않거나 네트워크의 문제로 페이지를 불러올 수 없는 상황이라면 예외가 발생할 것입니다. 예외가 발생할 경우 var에 예외가 저장됩니다. <c:if>를 통해 예외가 발생했는지 확인하는 예제입니다

<c:catch var="exception">
 
<c:import url="ftp://ftp.example.com/package/README"/>
</c:catch>
<c:if test="${not empty exception}">
  Sorry, the remote content is not currently available.
</c:if>



<c:redirect>를 이용한 페이지 리다이렉트 하기
이 액션은 <jsp:forward> 액션과도 매우 흡사합니다. 하지만 이 기능의 경우에는 서버사이드에서 구현된 요청형태만을 포워딩 합니다. 이게 무슨 말이냐면 포워딩의 경우에는 사용자 입장에서 보면 페이지의 이동 없이 다른 페이지를 띄워줄 수 있지만 리다이렉트의 경우에는 브라우저에 의해 페이지의 이동이 일어나게 됩니다. 하지만 <c:redirect>액션이 좀 더 유연합니다.<jsp:forward>의 경우에는 현재 같은 서블릿 컨텍스트 내의 다른 페이지로만 이동 할 수 있기 때문입니다.

참고 : http://www.ibm.com/developerworks/java/library/j-jstl0318/

 

출처 : http://theeye.pe.kr/entry/simple-example-of-using-jstl-core-tag-library

'Language > java' 카테고리의 다른 글

Jakarta Commons Net 에서 FTP  (0) 2014.01.17
URLConnection 에서 cookie 제어  (0) 2013.10.07
[스크랩] JVM 튜닝  (0) 2010.04.21
[스크랩] JVM 튜닝  (0) 2010.04.21
java & jsp 코딩 가이드  (0) 2009.01.29

JVM GC와 메모리 튜닝

자바스터디 네트워크 [www.javastudy.co.kr]

조대협 [bcho_N_O_SPAM@j2eestudy.co.kr]

모든 Java Application은 JVM(Java Virtual Machine)위에서 동작한다.
이 JVM이 동작하는데 있어서, 메모리의 구조와 특히 GC는 Application의 응답시간과 성능에 밀접한 관계를 미친다. 이번 강좌에서는 JVM 의 메모리 구조와 GC 알고리즘 (JDK 1.4.X에 포함된 새로운 알고리즘 포함) 그리고, JVM의 메모리 튜닝을 통한 Application의 성능향상방법에 대해서 알아보도록 하자.

1.GC란 무엇인가?

GC는 Garbage Collection의 약자로 Java 언어의 중요한 특징중의 하나이다.
GC는 Java Application에서 사용하지 않는 메모리를 자동으로 수거하는 기능을 말한다.
예전의 전통적인 언어 C등의 경우 malloc, free등을 이용해서 메모리를 할당하고, 일일이 그 메모리를 수거해줘야했다. 그러나 Java 언어에서는 GC 기술을 사용함에 따라서 개발자로 하여금 메모리 관리에서 부터 좀더 자유롭게 해주었다.

2.GC의 동작 방법은 어떻게 되는가?

1) JVM 메모리 영역

GC의 동작 방법을 이해하기 위해서는 Java의 메모리 구조를 먼저 이해할 필요가 있다.
일반적으로 Application에서 사용되는 객체는 오래 유지 되는 객체보다, 생성되고 얼마안있어서 사용되지 않는 경우가 많다. <그림 1 참조>

<그림 1. 메모리 foot print>

그래서 Java에서는 크게 두가지 영역으로 메모리를 나누는데 Young 영역과 Old 영역이 그것이다.
Young 영역은 생긴지 얼마 안된 객체들을 저장하는 장소이고, Old영역은 생성된지 오래된 객체를 저장하는 장소이다. 각 영역의 성격이 다른 만큼 GC의 방법도 다르다.
먼저 Java의 메모리 구조를 살펴보자.

<그림 2. Java 메모리 구조>

Java의 메모리 영역은 앞에서 이야기한 두 영역 (Young 영역,Old 영역)과 Perm 영역 이렇게 3가지로 영역으로 구성된다.

<표 1. Java 메모리 영역>

2) GC 알고리즘

그러면 이 메모리 영역을 JVM이 어떻게 관리하는지에 대해서 알아보자.
JVM은 New/Young 영역과, Old영역 이 두영역에 대해서만 GC를 수행한다. Perm영역은 앞에서 설명했듯이 Code가 올라가는 부분이기 때문에, GC가 일어날 필요가 없다. Perm영역은 Code가 모두 Load되고 나면 거의 일정한 수치를 유지한다.

○ Minor GC
먼저 New/Young영역의 GC방법을 살펴보자 New/Young 영역의 GC를 Minor GC라고 부르는데, New/Young영역은 Eden과 Survivor라는 두가지 영역으로 또 나뉘어 진다. Eden영역은 Java 객체가 생성되자 마자 저장이 되는곳이다. 이렇게 생성된 객체는 Minor GC가 발생할때 Survivor 영역으로 이동된다.

Survivor 영역은 Survivor 1과 Suvivor2 영역 두 영역으로 나뉘어 지는데, Minor GC가 발생하면 Eden과 Survivor1에 Alive되어 있는 객체를 Suvivor2로 복사한다. 그리고 Alive되어 있지 않는 객체는 자연히 Suvivor1에 남아있게 되고, Survivor1과 Eden영역을 Clear한다. (결과적으로 Alive된 객체만 Survivor2로 이동한것이다.)
다음번 Minor GC가 발생하면 같은 원리로 Eden과 Survivor2영역에서 Alive되어 있는 객체를 Survivor1에 복사한다. 계속 이런 방법을 반복적으로 수행하면서 Minor GC를 수행한다.

이렇게 Minor GC를 수행하다가, Survivor영역에서 오래된 객체는 Old영역으로 옮기게 된다.

이런 방식의 GC 알고리즘을 Copy & Scavenge라고 한다. 이 방법은 매우 속도가 빠르며 작은 크기의 메모리를 Collecting하는데 매우 효과적이다. Minor GC의 경우에는 자주 일어나기 때문에, GC에 소요되는 시간이 짧은 알고리즘이 적합하다.

이 내용을 그림을 보면서 살펴보도록 하자.

<그림 3-1. 1st Minor GC>

Eden에서 Alive된 객체를 Suvivor1으로 이동한다. Eden 영역을 Clear한다.

<그림 3-2. 2nd Minor GC>

Eden영역에 Alive된 객체와 Suvivor1영역에 Alive된 객체를 Survivor 2에 copy한다.
Eden영역과 Suvivor2영역을 clear한다.

<그림 3-3. 3rd Minor GC>

객체가 생성된 시간이 오래지나면 Eden과 Suvivor영역에 있는 오래된 객체들을 Old 영역으로 이동한다.

○ Full GC

Old 영역의 Garbage Collection을 Full GC라고 부르며, Full GC에 사용되는 알고리즘은 Mark & Compact라는 알고리즘을 이용한다. Mark & Compact 알고리즘은 전체 객체들의 reference를 쭉 따라가다면서 reference가 연결되지 않는 객체를 Mark한다. 이 작업이 끝나면 사용되지 않는 객체를 모두 Mark가 되고, 이 mark된 객체를 삭제한다.<그림 4 참고> (실제로는 compact라고 해서, mark된 객체로 생기는 부분을 unmark된 즉 사용하는 객체로 메꾸어 버리는 방법이다.)

Full GC는 매우 속도가 느리며, Full GC가 일어나는 도중에는 순간적으로 Java Application이 멈춰 버리기 때문에, Full GC가 일어나는 정도와 Full GC에 소요되는 시간은 Application의 성능과 안정성에 아주 큰 영향을 준다.

<그림 4. Full GC>

3. GC가 왜 중요한가?

Garbage Collection중에서 Minor GC의 경우 보통 0.5초 이내에 끝나기 때문에 큰문제가 되지 않는다. 그러나 Full GC의 경우 보통 수초가 소요가 되고, Full GC동안에는 Java Application이 멈춰버리기 때문에 문제가 될 수 있다.
예를 들어 게임 서버와 같은 Real Time Server를 구현을 했을때, Full GC가 일어나서 5초동안 시스템이 멈춘다고 생각해보자.
또 일반 WAS에서도 5~10초동안 멈추면, 멈추는동안의 사용자의 Request가 Queue에 저장되었다가 Full GC가 끝난후에 그 요청이 한꺼번에 들어오게 되면 과부하에 의한 여러 장애를 만들 수 있다..
그래서 원할한 서비스를 위해서는 GC를 어떻게 일어나게 하느냐가 시스템의 안정성과 성능에 큰 변수로 작용할 수 있다.

4. 다양한 GC 알고리즘

앞에서 설명한 기본적인 GC방법 (Scavenge 와 Mark and compact)이외에 JVM에서는 좀더 다양한 GC 방법을 제공하고 그 동작방법이나 사용방법도 틀리다. 이번에는 다양한 GC 알고리즘에 대해서 알아보자. 현재 (JDK 1.4)까지 나와 있는 JVM의 GC방법은 크게 아래 4가지를 지원하고 있다.

- Default Collector
- Parallel GC for young generation (from JDK 1.4 )
- Concurrent GC for old generation (from JDK 1.4)
- Incremental GC (Train GC)

1) Default Collector
이 GC 방법은 앞에서 설명한 전통적인 GC방법으로 Minor GC에 Scavenge를, Full GC에 Mark & compact 알고리즘을 사용하는 방법이다. 이 알고리즘에는 이미 앞에서 설명했기 때문에 별도의 설명을 하지는 않는다.

JDK 1.4에서부터 새로 적용되는 GC방법은 Parallel GC와 Concurrent GC 두가지 방법이 있다. Parallel GC는 Minor GC를 좀더 빨리하게 하는 방법이고 (Throughput 위주) Concurrent GC는 Full GC시에 시스템의 멈춤(Pause)현상을 최소화하는 GC방법이다.

2) Parallel GC
JDK1.3까지 GC는 하나의 Thread에서 이루어진다. Java가 Multi Thread환경을 지원함에도 불구하고, 1 CPU에서는 동시에 하나의 Thread만을 수행할 수 밖에 없기때문에, 예전에는 하나의 CPU에서만 GC를 수행했지만, 근래에 들어서 하나의 CPU에서 동시에 여러개의 Thread를 실행할 수 있는 Hyper Threading기술이나, 여러개의 CPU를 동시에 장착한 HW의 보급으로 하나의 HW Box에서 동시에 여러개의 Thread를 수행할 수 있게 되었다.

JDK 1.4부터 지원되는 Parallel GC는 Minor GC를 동시에 여러개의 Thread를 이용해서 GC를 수행하는 방법으로 하나의 Thread를 이용하는것보다 훨씬 빨리 GC를 수행할 수 있다.

<그림 7. Parallel GC 개념도>

<그림 7> 을 보자 왼쪽의 Default GC방법은 GC가 일어날때 Thread들이 작업을 멈추고, GC를 수행하는 thread만 gc를 수행한다. (그림에서 파란영역), Parallel GC에서는 여러 thread들이 gc를 수행이 가능하기 때문에, gc에 소요되는 시간이 낮아진다.

Parallel GC가 언제나 유익한것은 아니다. 앞에서도 말했듯이 1CPU에서는 동시에 여러개의 thread를 실행할 수 없기 때문에 오히혀 Parallel GC가 Default GC에 비해서 느리다. 2 CPU에서도 Multi thread에 대한 지원이나 계산등을 위해서 CPU Power가 사용되기 때문에, 최소한 4CPU의 256M 정도의 메모리를 가지고 있는 HW에서 Parallel GC가 유용하게 사용된다.

Parallel GC는 크게 두가지 종류의 옵션을 가지고 있는데,Low-pause 방식과 Throughput 방식의 GC방식이 있다.

Solaris 기준에서 Low-pause Parallel GC는 ?XX:+UseParNewGC 옵션을 사용한다. 이 모델은 Old 영역을 GC할때 다음에 설명할 Concurrent GC방법과 함께 사용할 수 있다. 이 방법은 GC가 일어날때 빨리 GC하는것이 아니라 GC가 발생할때 Application이 멈춰지는 현상(pause)를 최소화하는데 역점을 뒀다.

Throughput 방식의 Parallel GC는 ?XX:+UseParallelGC (Solaris 기준) 옵션을 이용하며 Old 영역을 GC할때는 Default GC (Mark and compact)방법만을 사용하도록 되어 있다.Minor GC가 발생했을때, 되도록이면 빨리 수행하도록 throughput에 역점을 두었다.

그외에도 ParallelGC를 수행할때 동시에 몇개의 Thread를 이용하여 Minor영역을 Parallel GC할지를 결정할 수 있는데, -XX:ParallelGCThreads= 옵션을 이용하여 Parallel GC에 사용되는 Thread의 수를 지정할 수 있다.

3) Concurrent GC

앞에서도 설명했듯이, Full GC즉 Old 영역을 GC하는 경우에는 그 시간이 길고 Application이 순간적으로 멈춰버리기 때문에, 시스템 운용에 문제가 된다.

그래서 JDK1.4부터 제공하는 Concurrent GC는 기존의 이런 Full GC의 단점을 보완하기 위해서 Full GC에 의해서 Application이 멈추어 지는 현상을 최소화 하기 위한 GC방법이다.
Full GC에 소요되는 작업을 Application을 멈추고 진행하는것이 아니라, 일부는 Application이 돌아가는 단계에서 수행하고, 최소한의 작업만을 Application이 멈췄을때 수행하는 방법으로 Application이 멈추는 시간을 최소화한다.

<그림 8. Concurrent GC 개념도>

그림 8에서와 같이 Application이 수행중일때(붉은 라인) Full GC를 위한 작업을 수행한다. (Sweep,mark) Application을 멈추고 수행하는 작업은 일부분 (initial-mark, remark 작업)만을 수행하기 때문에, 기존 Default GC의 Mark & Sweep Collector에 비해서 Application이 멈추는 시간이 현저하게 줄어든다.

Solaris JVM에서는 -XX:+UseConcMarkSweepGC Parameter를 이용해 세팅한다.

4) Incremental GC (Train GC)

Incremental GC또는 Train GC라고도 불리는 GC방법은 JDK 1.3에서부터 지원된 GC방법이다. 앞에서 설명한 Concurrent GC와 비슷하게, 의도 자체는 Full GC에 의해서 Application이 멈추는 시간을 줄이고자 하는데 있다.

Incremental GC의 작동방법은 간단하다. Minor GC가 일어날때 마다 Old영역을 조금씩 GC를 해서 Full GC가 발생하는 횟수나 시간을 줄이는 방법이다.

<그림 9. Incremental GC 개념도>

그림 9에서 보듯이. 왼쪽의 Default GC는 FullGC가 일어난후에나 Old 영역이 Clear된다. 그러나, 오른쪽의 Incremental GC를 보면 Minor GC가 일어난후에, Old 영역이 일부 Collect된것을 볼 수 있다.

Incremental GC를 사용하는 방법은 JVM 옵션에 ?Xinc 옵션을 사용하면 된다.
Incremental GC는 많은 자원을 소모하고, Minor GC를 자주일으키고, 그리고 Incremental GC를 사용한다고 Full GC가 없어지거나 그 횟수가 획기적으로 줄어드는 것은 아니다. 오히려 느려지는 경우가 많다. 필히 테스트 후에 사용하도록 하자.

※ Default GC이외의 알고리즘은 Application의 형태나 HW Spec(CPU수, Hyper threading 지원 여부), 그리고 JVM 버전(JDK 1.4.1이냐 1.4.2냐)에 따라서 차이가 매우 크다. 이론상으로는 실제로 성능이 좋아보일 수 있으나, 운영환경에서는 여러 요인으로 인해서 기대했던것만큼의 성능이 안나올 수 있기 때문에, 실환경에서 미리 충분한 테스트를 거쳐서 검증한후에 사용해야 한다.

5. GC 로그는 어떻게 수집과 분석

JVM에서는 GC 상황에 대한 로그를 남기기 위해서 옵션을 제공하고 있다.
Java 옵션에 ?verbosegc 라는 옵션을 주면되고 HP Unix의 경우 ?verbosegc ?Xverbosegc 옵션을 주면 좀더 자세한 GC정보를 얻을 수 있다. GC 정보는 stdout으로 출력이 되기 때문에 “>” redirection등을 이용해서 file에 저장해놓고 분석할 수 있다.

Example ) java ?verbosegc MyApplication

그럼 실제로 나온 GC로그를 어떻게 보는지를 알아보자.

<그림 5. 일반적인 GC 로그, Windows, Solaris>

<그림 5>는 GC로그 결과를 모아논 내용이다. (실제로는 Application의 stdout으로 출력되는 내용과 섞여서 출력된다.)
Minor GC는 ”[GC “로 표기되고, Full GC는 “[Full GC”로 표기된다.
그 다음값은 Heap size before GC인데,GC 전에 Heap 사용량 ( New/Young 영역 + Old 영역 + Perm 영역)의 크기를 나타낸다.

Heap size after GC는 GC가 발생한후에 Heap의 사용량이다. Minor GC가 발생했을때는 Eden과 Survivor 영역으 GC가 됨으로 Heap size after GC는 Old영역의 용량과 유사하다.(Minor GC에서 GC되지 않은 하나의 Survivor영역내의 Object들의 크기도 포함해야한다.)

Total Heap Size는 현재 JVM이 사용하는 Heap Memory양이다. 이 크기는 Java에서 ?ms와 ?mx 옵션으로 조정이 가능한데. 예를 들어 ?ms512m ?mx1024m로 해놓으면 Java Heap은 메모리 사용량에 따라서 512~1024m사이의 크기에서 적절하게 늘었다 줄었다한다. (이 늘어나는 기준과 줄어드는 기준은 (-XX:MaxHeapFreeRatio와 ?XX:MinHeapFreeRation를 이용해서 조정할 수 있으나 JVM vendor에 따라서 차이가 나기때문에 각 vendor별 JVM 메뉴얼을 참고하기 바란다.) Parameter에 대한 이야기는 추후에 좀더 자세히하도록 하자.

그 다음값은 GC에 소요된 시간이다.

<그림 5>의 GC로그를 보면 Minor GC가 일어날때마다 약 20,000K 정도의 Collection이 일어난다. Minor GC는 Eden과 Suvivor영역 하나를 GC하는 것이기 때문에 New/Young 영역을 20,000Kbyte 정도로 생각할 수 있다.

Full GC때를 보면 약44,000Kbyte에서 1,749Kbyte로 GC가 되었음을 볼 수 있다. Old영역에 큰 데이타가 많지 않은 경우이다. Data를 많이 사용하는 Application의 경우 전체 Heap이 512이라고 가정할때, Full GC후에도 480M정도로 유지되는 경우가 있다. 이런 경우에는 실제로 Application에서 Memory를 많이 사용하고 있다고 판단할 수 있기 때문에 전체 Heap Size를 늘려줄 필요가 있다.

이렇게 수집된 GC로그는 다소 보기가 어렵기 때문에, 좀더 쉽게 분석할 수 있게 하기 위해서 GC로그를 awk 스크립트를 이용해서 정제하면 분석이 용이하다.

<표 2. gc.awk 스크립트>

이 스크립트를 작성한후에 Unix의 awk 명령을 이용해서

% awk ?f gc.awk GC로그파일명

을 쳐주면 아래<표 3>와 같이 정리된 형태로 GC 로그만 추출하여 보여준다.

<표 3. gc.awk 스크립트에 의해서 정재된 로그>

Minor와 Major는 각각 Minor GC와 Full GC가 일어날때 소요된 시간을 나타내며, Alive는 GC후에 남아있는 메모리양, 그리고 Freed는 GC에 의해서 collect된 메모리 양이다.

이 로그파일은 excel등을 이용하여 그래프등으로 변환해서 보면 좀더 다각적인 분석이 가능해진다.

※ JDK 1.4에서부터는 ?XX:+PrintGCDetails 옵션이 추가되어서 좀더 자세한 GC정보를 수집할 수 있다.

※ HP JVM의 GC Log 수집

HP JVM은 전체 heap 뿐 아니라 ?Xverbosegc 옵션을 통해서 Perm,Eden,Old등의 모든 영역에 대한 GC정보를 좀더 정확하게 수집할 수 있다.

Example ) java ?verbosegc ?Xverbosegc MyApplication ß (HP JVM Only)

HP JVM의 GC정보는 18개의 필드를 제공하는데 그 내용을 정리해보면 <표 4.>와 같다.

<GC : %1 %2 %3 %4 %5 %6 %7 %8 %9 %10 %11 %12 %13 %14 %15 %16 %17 %18>

<표 4. HP JVM GC 로그 필드별 의미>

이 로그를 직접 보면서 분석하기는 쉽지가 않다. 그래서, HP에서는 좀더 Visual한 환경에서 분석이 가능하도록 HPJtune이라는 툴을 제공한다. 다음 URL에서 다운로드 받을 수 있다.

http://www.hp.com/products1/unix/java/java2/hpjtune/index.html

<그림 6. HP Jtune을 이용해서 GC후 Old영역의 변화 추이를 모니터링하는 화면>

6. GC 관련 Parameter

GC관련 설정값을 보기전에 앞서서 ?X와 ?XX 옵션에 대해서 먼저 언급하자. 이 옵션들은 표준 옵션이 아니라, 벤더별 JVM에서 따로 제공하는 옵션이기 때문에, 예고 없이 변경되거나 없어질 수 있기 때문에, 사용전에 미리 JVM 벤더 홈페이지를 통해서 검증한다음에 사용해야한다.

1) 전체 Heap Size 조정 옵션

전체 Heap size는 ?ms와 ?mx로 Heap 사이즈의 영역을 조정할 수 있다. 예를 들어 ?ms512m ?mx 1024m로 설정하면 JVM은 전체 Heap size를 application의 상황에 따라서 512m~1024m byte 사이에서 사용하게 된다. 그림2의 Total heap size

메모리가 모자를때는 heap을 늘리고, 남을때는 heap을 줄이는 heap growing과 shirinking 작업을 수행하는데, 메모리 변화량이 큰 애플리케이션이 아니라면 이 min heap size와 max heap size는 동일하게 설정하는 것이 좋다. 일반적으로 1GB까지의 Heap을 설정하는데에는 문제가 없으나, 1GB가 넘는 대용량 메모리를 설정하고자 할 경우에는 별도의 JVM 옵션이 필요한 경우가 있기때문에 미리 자료를 참고할 필요가 있다.

※ IBM AIX JVM의 경우
%export LDR_CNTRL=MAXDATA=0x10000000
%java -Xms1500m -Xmx1500m MyApplication

2) Perm size 조정 옵션

Perm Size는 앞에서도 설명했듯이, Java Application 자체(Java class etc..)가 로딩되는 영역이다. J2EE application의 경우에는 application 자체의 크기가 큰 편에 속하기 때문에, Default로 설정된 Perm Size로는 application class가 loading되기에 모자른 경우가 대부분이기 때문에, WAS start초기나, 가동 초기에 Out Of Memory 에러를 유발하는 경우가 많다.

PermSize는 -XX:MaxPermSize=128m 식으로 지정할 수 있다.
일반적으로 WAS에서 PermSize는 64~256m 사이가 적절하다.

3) New 영역과 Old 영역의 조정New 영역은 ?XX:NewRatio=2 에 의해서 조정이 된다.
NewRatio Old/New Size의 값이다. 전체 Heap Size가 768일때, NewRatio=2이면 New영역이 256m, Old 영역이 512m 로 설정이 된다.
JVM 1.4.X에서는 ?XX:NewSize=128m 옵션을 이용해서 직접 New 영역의 크기를 지정하는 것이 가능하다.

4) Survivor 영역 조정 옵션
-XX:SurvivorRatio=64 (eden/survivor 의 비율) :64이면 eden 이 128m일때, survivor영역은 2m가 된다.

5) -server와 ?client 옵션
JVM에는 일반적으로 server와 client 두가지 옵션을 제공한다.
결론만 말하면 server 옵션은 WAS와 같은 Server환경에 최적화된 옵션이고, client옵션은 워드프로세서와 같은 client application에 최적화된 옵션이다. 그냥 언뜻 보기에는 단순한 옵션 하나로보일 수 있지만, 내부에서 돌아가는 hotspot compiler에 대한 최적화 방법과 메모리 구조자체가 아예 틀리다.

○ -server 옵션

server용 application에 최적화된 옵션이다. Server application은 boot up 시간 보다는 user에 대한 response time이 중요하고, 많은 사용자가 동시에 사용하기 때문에 session등의 user data를 다루는게 일반적이다. 그래서 server 옵션으로 제공되는 hotspot compiler는 java application을 최적화 해서 빠른 response time을 내는데 집중되어 있다.

또한 메모리 모델 역시, 서버의 경우에는 특정 사용자가 서버 운영시간동안 계속 서버를 사용하는게 아니기 때문에 (Login하고, 사용한 후에는 Logout되기 때문에..) 사용자에 관련된 객체들이 오래 지속되는 경우가 드물다. 그래서 상대적으로 Old영역이 작고 New 영역이 크게 배정된다. <그림 7. 참조 >

○ -client 옵션

client application은 워드프로세서 처럼 혼자 사용하는 application이다. 그래서 client application은 response time보다는 빨리 기동되는데에 최적화가 되어 있다. 또한대부분의 client application을 구성하는 object는GUI Component와 같이 application이 종료될때까지 남아있는 object의 비중이 높기 때문에 상대적으로 Old 영역의 비율이 높다.

<그림 7. ?server와 ?client 옵션에 따른 JVM Old와 New영역>

이 두옵션은 가장 간단한 옵션이지만, JVM의 최적화에 아주 큰부분을 차지하고 있는 옵션이기 때문에, 반드시 Application의 성격에 맞춰서 적용하기 바란다.
(※ 참고로, SUN JVM은 default가 client, HPJVM는 default가 server로 세팅되어 있다.)

○ GC 방식에 대한 옵션

GC 방식에 대한 옵션은 앞에서도 설명했지만, 일반적인 GC방식이외에, Concurrent GC,Parallel GC,Inceremental GC와 같이 추가적인 GC Algorithm이 존재한다. 옵션과 내용은 앞장에서 설명한 “다양한 GC알고리즘” 을 참고하기 바란다.

7.JVM GC 튜닝

그러면 이제부터 지금까지 설명한 내용을 기반으로 실제로 JVM 튜닝을 어떻게 하는지 알아보도록 하자.

STEP 1. Application의 종류와 튜닝목표값을 결정한다.

JVM 튜닝을 하기위해서 가장 중요한것은 JVM 튜닝의 목표를 설정하는것이다. 메모리를 적게 쓰는것이 목표인지, GC 횟수를 줄이는것이 목표인지, GC에 소요되는시간이 목표인지, Application의 성능(Throughput or response time) 향상인지를 먼저 정의한후에. 그 목표치에 근접하도록 JVM Parameter를 조정하는것이 필요하다.

STEP 2. Heap size와 Perm size를 설정한다.

-ms와 ?mx 옵션을 이용해서 Heap Size를 정한다. 일반적으로 server application인 경우에는 ms와 mx 사이즈를 같게 하는것이 Memory의 growing과 shrinking에 의한 불필요한 로드를 막을 수 있어서 권장할만하다.

ms와mx사이즈를 다르게 하는 경우는 Application의 시간대별 memory 사용량이 급격하게 변화가 있는 Application에 효과적이다.
PermSize는 JVM vendor에 따라 다소 차이가 있으나 일반적으로 16m정도이다. Client application의 경우에는 문제가 없을 수 있지만, J2EE Server Application의 경우 64~128m 사이로 사용이 된다.

Heap Size와 Perm Size는 아래 과정을 통해서 적정 수치를 얻어가야한다.

STEP 3. 테스트 & 로그 분석.

JVM Option에 GC 로그를 수집하기 위한 ?verbosegc 옵션을 적용한다. (HP의 경우 ?Xverbosegc 옵션을 적용한다.)

LoadRunner나 MS Strest(무료로 MS社의 홈페이지에서 다운로드 받을 수 있다.)와 같은 Strest Test툴을 통해서 Application에 Strest를 줘서. 그 log를 수집한다. 튜닝에서 있어서 가장 중요한것은 목표산정이지만, 그만큼이나 중요한것은 실제 Tuning한 Parameter가 Application에 어떤 영향을 주는지를 테스트하는 방법이 매우 중요하다. 그런 의미에서 적절한 Strest Tool의 선정과, Strest Test 시나리오는 정확한 Tuning을 위해서 매우 중요한 요인이다.

○ Perm size 조정
아래 그림8.은 HP JVM에서 ?Xverbosegc 옵션으로 수집한 GC log를 HP Jtune을 통해서 graph로 나타낸 그래프이다. 그림을 보면 Application이 startup되었을때 Perm 영역이 40m에서. 시간이 지난후에도 50m 이하로 유지되는것을 볼 수 있다. 특별하게 동적 classloading등이 수십m byte가 일어나지 않는등의 큰 변화요인이 없을때, 이 application의 적정 Perm 영역은 64m로 판단할 수 있다.

<그림 8. GC 결과중 Perm 영역 그래프>

○ GC Time 수행 시간 분석

다음은 GC에 걸린 시간을 분석해보자. 앞에 강좌 내용에서도 설명햇듯이. GC Tuning에서 중요한 부분중 하나가 GC에 소요되는 시간 특히 Full GC 시간이다.

지금부터 볼 Log는 모社의 물류 시스템의 WAS 시스템 GC Log이다. HP JVM을 사용하며, -server ?ms512m ?mx512m 옵션으로 기동되는 시스템이다.

그림 9를 보면 Peak 시간 (첫번째 동그라미) 14시간동안에 Full GC(동그란점)가 7번일어난것을 볼 수 있다. 각각에 걸린 시간은2.5~6sec 사이이다.
여기서 STEP 1.에서 설정한 AP Tuning의 목표치를 참고해야하는데.

Full GC가 길게 일어나서 Full GC에 수행되는 시간을 줄이고자 한다면 Old 영역을 줄이면 Full GC가 일어나는 횟수는 늘어나고, 반대로 Full GC가 일어나는 시간을 줄어들것이다.

반대로 Full GC가 일어나는 횟수가 많다면, Old 영역을 늘려주면 Full GC가 일어나는 횟수는 상대적으로 줄어들것이고 반대로 Full GC 수행시간이 늘어날 것이다.

특히 Server Application의 경우Full GC가 일어날때는 JVM자체가 멈춰버리기 때문에, 그림 9의 instance는 14시간동안 총 7번 시스템이 멈추고, 그때마다 2.5~6sec가량 시스템이 response를 못하는 상태가 된것이다. 그래서 멈춘 시간이 고객이 납득할만한 시간인지를 판단해야 하고, 거기에 적절한 Tuning을 해야한다.

Server Application에서 Full GC를 적게일어나게하고, Full GC 시간을 양쪽다 줄이기 위해서는 Old영역을 적게한후에, 여러개의 Instance를 동시에 뛰어서 Load Balancing을 해주면, Load가 분산되기 때문에 Full GC가 일어나는 횟수가 줄어들테고, Old 영역을 줄였기 때문에, Full GC에 드는 시간도 줄어들것이다. 또한 각각의 FullGC가 일어나는동안 하나의 서버 instance가 멈춰져 있어도, Load Balancing이 되는 다른 서버가 response를 하고 있기때문에, Full GC로 인한 Application이 멈추는것에 의한 영향을 최소화할 수 있다.

<그림 9. GC 소요시간>

데이타에 따라서 GC Tuning을 진행한후에는 다시 Strest Test를 진행해서 응답시간과 TPS(Throughput Per Second)를 체크해서 어떤 변화를 주었는지를 반드시 체크해봐야한다.

<그림 10. GC후의 Old 영역>

그림 10은 GC후에 Old 영역의 메모리 변화량을 나타낸다.

금요일 업무시간에 메모리 사용량이 올라가다가. 주말에가서 완만한 곡선을 그리는것을 볼 수 있다. 월요일 근무시간에 메모리 사용량이 매우 많고, 화요일에도 어느정도 메모리 사용량이 있는것을 볼 수 있다. 월요일에 메모리 사용량이 많은것을 볼때, 이 시스템의 사용자들이 월요일에 시스템 사용량이 많을 수 있다고 생각할 수 있고, 또는 다른 주의 로그를 분석해봤을때 이 주만 월요일 사용량이 많았다면, 특별한 요인이나 Application 변경등이 있었는지를 고려해봐야할것이다.

이 그래프만을 봤을때 Full GC가 일어난후에도 월요일 근무시간을 보면 Old 영역이 180M를 유지하고 있는것을 볼 수 있다. 이 시스템의 Full GC후의 Old영역은 80M~180M를 유지하는것을 볼 수 있다. 그래서 이 시스템은 최소 180M이상의 Old 영역을 필요로하는것으로 판단할 수 있다.

STEP 4. Parameter 변경.
STEP 3에서 구한 각 영역의 허용 범위를 기준으로 Old영역과 New 영역을 적절하게 조절한다.
PermSize와 New영역의 배분 (Eden,Survivor)영역등을 조정한다.
PermSize는 대부분 Log에서 명확하게 나타나기 때문에, 크게 조정이 필요가 없고 New영역내의 Eden과 Survivor는 거의 조정하지 않는다. 가장 중요한것은 Old영역과 New 영역의 비율을 어떻게 조정하는가가 관건이다.

이 비율을 결정하면서, STEP1에서 세운 튜닝 목표에 따라서 JVM의 GC Algorithm을 적용한다. GC Algorithm을 결정하는 기본적인 판단 내용은 아래와 같다.

이렇게 Parameter를 변경하면서 테스트를 진행하고, 다시 변경하고 테스트를 진행하는 과정을 거쳐서 최적의 Parameter와 GC Algorithm을 찾아내는것이 JVM의 메모리 튜닝의 이상적인 절차이다.

지금까지 JVM의 메모리 구조와 GC 모델 그리고 GC 튜닝에 대해서 알아보았다.

정리하자면 GC 튜닝은 Application의 구조나 성격 그리고, 사용자의 이용 Pattern에 따라서 크게 좌우 되기때문에, 얼마만큼의 Parameter를 많이 아느냐 보다는 얼마만큼의 테스트와 로그를 통해서 목표 값에 접근하느냐가 가장 중요하다

참조 : 자바스터디 네트워크 [www.javastudy.co.kr]조대협 [bcho_N_O_SPAM@j2eestudy.co.kr]

출처 : 바람이 될래!
글쓴이 : 황진이 원글보기
메모 :

'Language > java' 카테고리의 다른 글

URLConnection 에서 cookie 제어  (0) 2013.10.07
JSTL 사용하기  (0) 2012.06.05
[스크랩] JVM 튜닝  (0) 2010.04.21
java & jsp 코딩 가이드  (0) 2009.01.29
[세련된 자바 웹 프로그래머 되기] ① 기본기 갈고닦기  (0) 2009.01.21

JVM GC와 메모리 튜닝

자바스터디 네트워크 [www.javastudy.co.kr]

조대협 [bcho_N_O_SPAM@j2eestudy.co.kr]

모든 Java Application은 JVM(Java Virtual Machine)위에서 동작한다.
이 JVM이 동작하는데 있어서, 메모리의 구조와 특히 GC는 Application의 응답시간과 성능에 밀접한 관계를 미친다. 이번 강좌에서는 JVM 의 메모리 구조와 GC 알고리즘 (JDK 1.4.X에 포함된 새로운 알고리즘 포함) 그리고, JVM의 메모리 튜닝을 통한 Application의 성능향상방법에 대해서 알아보도록 하자.

1.GC란 무엇인가?

GC는 Garbage Collection의 약자로 Java 언어의 중요한 특징중의 하나이다.
GC는 Java Application에서 사용하지 않는 메모리를 자동으로 수거하는 기능을 말한다.
예전의 전통적인 언어 C등의 경우 malloc, free등을 이용해서 메모리를 할당하고, 일일이 그 메모리를 수거해줘야했다. 그러나 Java 언어에서는 GC 기술을 사용함에 따라서 개발자로 하여금 메모리 관리에서 부터 좀더 자유롭게 해주었다.

2.GC의 동작 방법은 어떻게 되는가?

1) JVM 메모리 영역

GC의 동작 방법을 이해하기 위해서는 Java의 메모리 구조를 먼저 이해할 필요가 있다.
일반적으로 Application에서 사용되는 객체는 오래 유지 되는 객체보다, 생성되고 얼마안있어서 사용되지 않는 경우가 많다. <그림 1 참조>

 

 

 

<그림 1. 메모리 foot print>

그래서 Java에서는 크게 두가지 영역으로 메모리를 나누는데 Young 영역과 Old 영역이 그것이다.
Young 영역은 생긴지 얼마 안된 객체들을 저장하는 장소이고, Old영역은 생성된지 오래된 객체를 저장하는 장소이다. 각 영역의 성격이 다른 만큼 GC의 방법도 다르다.
먼저 Java의 메모리 구조를 살펴보자.

 

 

 

<그림 2. Java 메모리 구조>

Java의 메모리 영역은 앞에서 이야기한 두 영역 (Young 영역,Old 영역)과 Perm 영역 이렇게 3가지로 영역으로 구성된다.

 

 

<표 1. Java 메모리 영역>

2) GC 알고리즘

그러면 이 메모리 영역을 JVM이 어떻게 관리하는지에 대해서 알아보자.
JVM은 New/Young 영역과, Old영역 이 두영역에 대해서만 GC를 수행한다. Perm영역은 앞에서 설명했듯이 Code가 올라가는 부분이기 때문에, GC가 일어날 필요가 없다. Perm영역은 Code가 모두 Load되고 나면 거의 일정한 수치를 유지한다.

○ Minor GC
먼저 New/Young영역의 GC방법을 살펴보자 New/Young 영역의 GC를 Minor GC라고 부르는데, New/Young영역은 Eden과 Survivor라는 두가지 영역으로 또 나뉘어 진다. Eden영역은 Java 객체가 생성되자 마자 저장이 되는곳이다. 이렇게 생성된 객체는 Minor GC가 발생할때 Survivor 영역으로 이동된다.

Survivor 영역은 Survivor 1과 Suvivor2 영역 두 영역으로 나뉘어 지는데, Minor GC가 발생하면 Eden과 Survivor1에 Alive되어 있는 객체를 Suvivor2로 복사한다. 그리고 Alive되어 있지 않는 객체는 자연히 Suvivor1에 남아있게 되고, Survivor1과 Eden영역을 Clear한다. (결과적으로 Alive된 객체만 Survivor2로 이동한것이다.)
다음번 Minor GC가 발생하면 같은 원리로 Eden과 Survivor2영역에서 Alive되어 있는 객체를 Survivor1에 복사한다. 계속 이런 방법을 반복적으로 수행하면서 Minor GC를 수행한다.

이렇게 Minor GC를 수행하다가, Survivor영역에서 오래된 객체는 Old영역으로 옮기게 된다.

이런 방식의 GC 알고리즘을 Copy & Scavenge라고 한다. 이 방법은 매우 속도가 빠르며 작은 크기의 메모리를 Collecting하는데 매우 효과적이다. Minor GC의 경우에는 자주 일어나기 때문에, GC에 소요되는 시간이 짧은 알고리즘이 적합하다.

이 내용을 그림을 보면서 살펴보도록 하자.

 

 

<그림 3-1. 1st Minor GC>

Eden에서 Alive된 객체를 Suvivor1으로 이동한다. Eden 영역을 Clear한다.

 

 

<그림 3-2. 2nd Minor GC>

Eden영역에 Alive된 객체와 Suvivor1영역에 Alive된 객체를 Survivor 2에 copy한다.
Eden영역과 Suvivor2영역을 clear한다.

 

 

<그림 3-3. 3rd Minor GC>

객체가 생성된 시간이 오래지나면 Eden과 Suvivor영역에 있는 오래된 객체들을 Old 영역으로 이동한다.

○ Full GC

Old 영역의 Garbage Collection을 Full GC라고 부르며, Full GC에 사용되는 알고리즘은 Mark & Compact라는 알고리즘을 이용한다. Mark & Compact 알고리즘은 전체 객체들의 reference를 쭉 따라가다면서 reference가 연결되지 않는 객체를 Mark한다. 이 작업이 끝나면 사용되지 않는 객체를 모두 Mark가 되고, 이 mark된 객체를 삭제한다.<그림 4 참고> (실제로는 compact라고 해서, mark된 객체로 생기는 부분을 unmark된 즉 사용하는 객체로 메꾸어 버리는 방법이다.)

Full GC는 매우 속도가 느리며, Full GC가 일어나는 도중에는 순간적으로 Java Application이 멈춰 버리기 때문에, Full GC가 일어나는 정도와 Full GC에 소요되는 시간은 Application의 성능과 안정성에 아주 큰 영향을 준다.

 

 

<그림 4. Full GC>

3. GC가 왜 중요한가?

Garbage Collection중에서 Minor GC의 경우 보통 0.5초 이내에 끝나기 때문에 큰문제가 되지 않는다. 그러나 Full GC의 경우 보통 수초가 소요가 되고, Full GC동안에는 Java Application이 멈춰버리기 때문에 문제가 될 수 있다.
예를 들어 게임 서버와 같은 Real Time Server를 구현을 했을때, Full GC가 일어나서 5초동안 시스템이 멈춘다고 생각해보자.
또 일반 WAS에서도 5~10초동안 멈추면, 멈추는동안의 사용자의 Request가 Queue에 저장되었다가 Full GC가 끝난후에 그 요청이 한꺼번에 들어오게 되면 과부하에 의한 여러 장애를 만들 수 있다..
그래서 원할한 서비스를 위해서는 GC를 어떻게 일어나게 하느냐가 시스템의 안정성과 성능에 큰 변수로 작용할 수 있다.

4. 다양한 GC 알고리즘

앞에서 설명한 기본적인 GC방법 (Scavenge 와 Mark and compact)이외에 JVM에서는 좀더 다양한 GC 방법을 제공하고 그 동작방법이나 사용방법도 틀리다. 이번에는 다양한 GC 알고리즘에 대해서 알아보자. 현재 (JDK 1.4)까지 나와 있는 JVM의 GC방법은 크게 아래 4가지를 지원하고 있다.

- Default Collector
- Parallel GC for young generation (from JDK 1.4 )
- Concurrent GC for old generation (from JDK 1.4)
- Incremental GC (Train GC)

1) Default Collector
이 GC 방법은 앞에서 설명한 전통적인 GC방법으로 Minor GC에 Scavenge를, Full GC에 Mark & compact 알고리즘을 사용하는 방법이다. 이 알고리즘에는 이미 앞에서 설명했기 때문에 별도의 설명을 하지는 않는다.

JDK 1.4에서부터 새로 적용되는 GC방법은 Parallel GC와 Concurrent GC 두가지 방법이 있다. Parallel GC는 Minor GC를 좀더 빨리하게 하는 방법이고 (Throughput 위주) Concurrent GC는 Full GC시에 시스템의 멈춤(Pause)현상을 최소화하는 GC방법이다.

2) Parallel GC
JDK1.3까지 GC는 하나의 Thread에서 이루어진다. Java가 Multi Thread환경을 지원함에도 불구하고, 1 CPU에서는 동시에 하나의 Thread만을 수행할 수 밖에 없기때문에, 예전에는 하나의 CPU에서만 GC를 수행했지만, 근래에 들어서 하나의 CPU에서 동시에 여러개의 Thread를 실행할 수 있는 Hyper Threading기술이나, 여러개의 CPU를 동시에 장착한 HW의 보급으로 하나의 HW Box에서 동시에 여러개의 Thread를 수행할 수 있게 되었다.

JDK 1.4부터 지원되는 Parallel GC는 Minor GC를 동시에 여러개의 Thread를 이용해서 GC를 수행하는 방법으로 하나의 Thread를 이용하는것보다 훨씬 빨리 GC를 수행할 수 있다.

 

 

<그림 7. Parallel GC 개념도>

<그림 7> 을 보자 왼쪽의 Default GC방법은 GC가 일어날때 Thread들이 작업을 멈추고, GC를 수행하는 thread만 gc를 수행한다. (그림에서 파란영역), Parallel GC에서는 여러 thread들이 gc를 수행이 가능하기 때문에, gc에 소요되는 시간이 낮아진다.

Parallel GC가 언제나 유익한것은 아니다. 앞에서도 말했듯이 1CPU에서는 동시에 여러개의 thread를 실행할 수 없기 때문에 오히혀 Parallel GC가 Default GC에 비해서 느리다. 2 CPU에서도 Multi thread에 대한 지원이나 계산등을 위해서 CPU Power가 사용되기 때문에, 최소한 4CPU의 256M 정도의 메모리를 가지고 있는 HW에서 Parallel GC가 유용하게 사용된다.

Parallel GC는 크게 두가지 종류의 옵션을 가지고 있는데,Low-pause 방식과 Throughput 방식의 GC방식이 있다.

Solaris 기준에서 Low-pause Parallel GC는 ?XX:+UseParNewGC 옵션을 사용한다. 이 모델은 Old 영역을 GC할때 다음에 설명할 Concurrent GC방법과 함께 사용할 수 있다. 이 방법은 GC가 일어날때 빨리 GC하는것이 아니라 GC가 발생할때 Application이 멈춰지는 현상(pause)를 최소화하는데 역점을 뒀다.

Throughput 방식의 Parallel GC는 ?XX:+UseParallelGC (Solaris 기준) 옵션을 이용하며 Old 영역을 GC할때는 Default GC (Mark and compact)방법만을 사용하도록 되어 있다.Minor GC가 발생했을때, 되도록이면 빨리 수행하도록 throughput에 역점을 두었다.

그외에도 ParallelGC를 수행할때 동시에 몇개의 Thread를 이용하여 Minor영역을 Parallel GC할지를 결정할 수 있는데, -XX:ParallelGCThreads= 옵션을 이용하여 Parallel GC에 사용되는 Thread의 수를 지정할 수 있다.

3) Concurrent GC

앞에서도 설명했듯이, Full GC즉 Old 영역을 GC하는 경우에는 그 시간이 길고 Application이 순간적으로 멈춰버리기 때문에, 시스템 운용에 문제가 된다.

그래서 JDK1.4부터 제공하는 Concurrent GC는 기존의 이런 Full GC의 단점을 보완하기 위해서 Full GC에 의해서 Application이 멈추어 지는 현상을 최소화 하기 위한 GC방법이다.
Full GC에 소요되는 작업을 Application을 멈추고 진행하는것이 아니라, 일부는 Application이 돌아가는 단계에서 수행하고, 최소한의 작업만을 Application이 멈췄을때 수행하는 방법으로 Application이 멈추는 시간을 최소화한다.

 

 

<그림 8. Concurrent GC 개념도>

그림 8에서와 같이 Application이 수행중일때(붉은 라인) Full GC를 위한 작업을 수행한다. (Sweep,mark) Application을 멈추고 수행하는 작업은 일부분 (initial-mark, remark 작업)만을 수행하기 때문에, 기존 Default GC의 Mark & Sweep Collector에 비해서 Application이 멈추는 시간이 현저하게 줄어든다.

Solaris JVM에서는 -XX:+UseConcMarkSweepGC Parameter를 이용해 세팅한다.

4) Incremental GC (Train GC)

Incremental GC또는 Train GC라고도 불리는 GC방법은 JDK 1.3에서부터 지원된 GC방법이다. 앞에서 설명한 Concurrent GC와 비슷하게, 의도 자체는 Full GC에 의해서 Application이 멈추는 시간을 줄이고자 하는데 있다.

Incremental GC의 작동방법은 간단하다. Minor GC가 일어날때 마다 Old영역을 조금씩 GC를 해서 Full GC가 발생하는 횟수나 시간을 줄이는 방법이다.

 

 

<그림 9. Incremental GC 개념도>

그림 9에서 보듯이. 왼쪽의 Default GC는 FullGC가 일어난후에나 Old 영역이 Clear된다. 그러나, 오른쪽의 Incremental GC를 보면 Minor GC가 일어난후에, Old 영역이 일부 Collect된것을 볼 수 있다.

Incremental GC를 사용하는 방법은 JVM 옵션에 ?Xinc 옵션을 사용하면 된다.
Incremental GC는 많은 자원을 소모하고, Minor GC를 자주일으키고, 그리고 Incremental GC를 사용한다고 Full GC가 없어지거나 그 횟수가 획기적으로 줄어드는 것은 아니다. 오히려 느려지는 경우가 많다. 필히 테스트 후에 사용하도록 하자.

※ Default GC이외의 알고리즘은 Application의 형태나 HW Spec(CPU수, Hyper threading 지원 여부), 그리고 JVM 버전(JDK 1.4.1이냐 1.4.2냐)에 따라서 차이가 매우 크다. 이론상으로는 실제로 성능이 좋아보일 수 있으나, 운영환경에서는 여러 요인으로 인해서 기대했던것만큼의 성능이 안나올 수 있기 때문에, 실환경에서 미리 충분한 테스트를 거쳐서 검증한후에 사용해야 한다.

5. GC 로그는 어떻게 수집과 분석

JVM에서는 GC 상황에 대한 로그를 남기기 위해서 옵션을 제공하고 있다.
Java 옵션에 ?verbosegc 라는 옵션을 주면되고 HP Unix의 경우 ?verbosegc ?Xverbosegc 옵션을 주면 좀더 자세한 GC정보를 얻을 수 있다. GC 정보는 stdout으로 출력이 되기 때문에 “>” redirection등을 이용해서 file에 저장해놓고 분석할 수 있다.

Example ) java ?verbosegc MyApplication

그럼 실제로 나온 GC로그를 어떻게 보는지를 알아보자.

 

 

<그림 5. 일반적인 GC 로그, Windows, Solaris>

<그림 5>는 GC로그 결과를 모아논 내용이다. (실제로는 Application의 stdout으로 출력되는 내용과 섞여서 출력된다.)
Minor GC는 ”[GC “로 표기되고, Full GC는 “[Full GC”로 표기된다.
그 다음값은 Heap size before GC인데,GC 전에 Heap 사용량 ( New/Young 영역 + Old 영역 + Perm 영역)의 크기를 나타낸다.

Heap size after GC는 GC가 발생한후에 Heap의 사용량이다. Minor GC가 발생했을때는 Eden과 Survivor 영역으 GC가 됨으로 Heap size after GC는 Old영역의 용량과 유사하다.(Minor GC에서 GC되지 않은 하나의 Survivor영역내의 Object들의 크기도 포함해야한다.)

Total Heap Size는 현재 JVM이 사용하는 Heap Memory양이다. 이 크기는 Java에서 ?ms와 ?mx 옵션으로 조정이 가능한데. 예를 들어 ?ms512m ?mx1024m로 해놓으면 Java Heap은 메모리 사용량에 따라서 512~1024m사이의 크기에서 적절하게 늘었다 줄었다한다. (이 늘어나는 기준과 줄어드는 기준은 (-XX:MaxHeapFreeRatio와 ?XX:MinHeapFreeRation를 이용해서 조정할 수 있으나 JVM vendor에 따라서 차이가 나기때문에 각 vendor별 JVM 메뉴얼을 참고하기 바란다.) Parameter에 대한 이야기는 추후에 좀더 자세히하도록 하자.

그 다음값은 GC에 소요된 시간이다.

<그림 5>의 GC로그를 보면 Minor GC가 일어날때마다 약 20,000K 정도의 Collection이 일어난다. Minor GC는 Eden과 Suvivor영역 하나를 GC하는 것이기 때문에 New/Young 영역을 20,000Kbyte 정도로 생각할 수 있다.

Full GC때를 보면 약44,000Kbyte에서 1,749Kbyte로 GC가 되었음을 볼 수 있다. Old영역에 큰 데이타가 많지 않은 경우이다. Data를 많이 사용하는 Application의 경우 전체 Heap이 512이라고 가정할때, Full GC후에도 480M정도로 유지되는 경우가 있다. 이런 경우에는 실제로 Application에서 Memory를 많이 사용하고 있다고 판단할 수 있기 때문에 전체 Heap Size를 늘려줄 필요가 있다.

이렇게 수집된 GC로그는 다소 보기가 어렵기 때문에, 좀더 쉽게 분석할 수 있게 하기 위해서 GC로그를 awk 스크립트를 이용해서 정제하면 분석이 용이하다.

 

 

<표 2. gc.awk 스크립트>

이 스크립트를 작성한후에 Unix의 awk 명령을 이용해서

% awk ?f gc.awk GC로그파일명

을 쳐주면 아래<표 3>와 같이 정리된 형태로 GC 로그만 추출하여 보여준다.

 

 

 

 

 

<표 3. gc.awk 스크립트에 의해서 정재된 로그>

Minor와 Major는 각각 Minor GC와 Full GC가 일어날때 소요된 시간을 나타내며, Alive는 GC후에 남아있는 메모리양, 그리고 Freed는 GC에 의해서 collect된 메모리 양이다.

이 로그파일은 excel등을 이용하여 그래프등으로 변환해서 보면 좀더 다각적인 분석이 가능해진다.

※ JDK 1.4에서부터는 ?XX:+PrintGCDetails 옵션이 추가되어서 좀더 자세한 GC정보를 수집할 수 있다.

※ HP JVM의 GC Log 수집

HP JVM은 전체 heap 뿐 아니라 ?Xverbosegc 옵션을 통해서 Perm,Eden,Old등의 모든 영역에 대한 GC정보를 좀더 정확하게 수집할 수 있다.

Example ) java ?verbosegc ?Xverbosegc MyApplication ß (HP JVM Only)

HP JVM의 GC정보는 18개의 필드를 제공하는데 그 내용을 정리해보면 <표 4.>와 같다.

<GC : %1 %2 %3 %4 %5 %6 %7 %8 %9 %10 %11 %12 %13 %14 %15 %16 %17 %18>

 

 

<표 4. HP JVM GC 로그 필드별 의미>

이 로그를 직접 보면서 분석하기는 쉽지가 않다. 그래서, HP에서는 좀더 Visual한 환경에서 분석이 가능하도록 HPJtune이라는 툴을 제공한다. 다음 URL에서 다운로드 받을 수 있다.

http://www.hp.com/products1/unix/java/java2/hpjtune/index.html

 

 

<그림 6. HP Jtune을 이용해서 GC후 Old영역의 변화 추이를 모니터링하는 화면>

6. GC 관련 Parameter

GC관련 설정값을 보기전에 앞서서 ?X와 ?XX 옵션에 대해서 먼저 언급하자. 이 옵션들은 표준 옵션이 아니라, 벤더별 JVM에서 따로 제공하는 옵션이기 때문에, 예고 없이 변경되거나 없어질 수 있기 때문에, 사용전에 미리 JVM 벤더 홈페이지를 통해서 검증한다음에 사용해야한다.

1) 전체 Heap Size 조정 옵션

전체 Heap size는 ?ms와 ?mx로 Heap 사이즈의 영역을 조정할 수 있다. 예를 들어 ?ms512m ?mx 1024m로 설정하면 JVM은 전체 Heap size를 application의 상황에 따라서 512m~1024m byte 사이에서 사용하게 된다. 그림2의 Total heap size

메모리가 모자를때는 heap을 늘리고, 남을때는 heap을 줄이는 heap growing과 shirinking 작업을 수행하는데, 메모리 변화량이 큰 애플리케이션이 아니라면 이 min heap size와 max heap size는 동일하게 설정하는 것이 좋다. 일반적으로 1GB까지의 Heap을 설정하는데에는 문제가 없으나, 1GB가 넘는 대용량 메모리를 설정하고자 할 경우에는 별도의 JVM 옵션이 필요한 경우가 있기때문에 미리 자료를 참고할 필요가 있다.

※ IBM AIX JVM의 경우
%export LDR_CNTRL=MAXDATA=0x10000000
%java -Xms1500m -Xmx1500m MyApplication

2) Perm size 조정 옵션

Perm Size는 앞에서도 설명했듯이, Java Application 자체(Java class etc..)가 로딩되는 영역이다. J2EE application의 경우에는 application 자체의 크기가 큰 편에 속하기 때문에, Default로 설정된 Perm Size로는 application class가 loading되기에 모자른 경우가 대부분이기 때문에, WAS start초기나, 가동 초기에 Out Of Memory 에러를 유발하는 경우가 많다.

PermSize는 -XX:MaxPermSize=128m 식으로 지정할 수 있다.
일반적으로 WAS에서 PermSize는 64~256m 사이가 적절하다.

3) New 영역과 Old 영역의 조정New 영역은 ?XX:NewRatio=2 에 의해서 조정이 된다.
NewRatio Old/New Size의 값이다. 전체 Heap Size가 768일때, NewRatio=2이면 New영역이 256m, Old 영역이 512m 로 설정이 된다.
JVM 1.4.X에서는 ?XX:NewSize=128m 옵션을 이용해서 직접 New 영역의 크기를 지정하는 것이 가능하다.

4) Survivor 영역 조정 옵션
-XX:SurvivorRatio=64 (eden/survivor 의 비율) :64이면 eden 이 128m일때, survivor영역은 2m가 된다.

5) -server와 ?client 옵션
JVM에는 일반적으로 server와 client 두가지 옵션을 제공한다.
결론만 말하면 server 옵션은 WAS와 같은 Server환경에 최적화된 옵션이고, client옵션은 워드프로세서와 같은 client application에 최적화된 옵션이다. 그냥 언뜻 보기에는 단순한 옵션 하나로보일 수 있지만, 내부에서 돌아가는 hotspot compiler에 대한 최적화 방법과 메모리 구조자체가 아예 틀리다.

○ -server 옵션

server용 application에 최적화된 옵션이다. Server application은 boot up 시간 보다는 user에 대한 response time이 중요하고, 많은 사용자가 동시에 사용하기 때문에 session등의 user data를 다루는게 일반적이다. 그래서 server 옵션으로 제공되는 hotspot compiler는 java application을 최적화 해서 빠른 response time을 내는데 집중되어 있다.

또한 메모리 모델 역시, 서버의 경우에는 특정 사용자가 서버 운영시간동안 계속 서버를 사용하는게 아니기 때문에 (Login하고, 사용한 후에는 Logout되기 때문에..) 사용자에 관련된 객체들이 오래 지속되는 경우가 드물다. 그래서 상대적으로 Old영역이 작고 New 영역이 크게 배정된다. <그림 7. 참조 >

○ -client 옵션

client application은 워드프로세서 처럼 혼자 사용하는 application이다. 그래서 client application은 response time보다는 빨리 기동되는데에 최적화가 되어 있다. 또한대부분의 client application을 구성하는 object는GUI Component와 같이 application이 종료될때까지 남아있는 object의 비중이 높기 때문에 상대적으로 Old 영역의 비율이 높다.

 

 

<그림 7. ?server와 ?client 옵션에 따른 JVM Old와 New영역>

이 두옵션은 가장 간단한 옵션이지만, JVM의 최적화에 아주 큰부분을 차지하고 있는 옵션이기 때문에, 반드시 Application의 성격에 맞춰서 적용하기 바란다.
(※ 참고로, SUN JVM은 default가 client, HPJVM는 default가 server로 세팅되어 있다.)

○ GC 방식에 대한 옵션

GC 방식에 대한 옵션은 앞에서도 설명했지만, 일반적인 GC방식이외에, Concurrent GC,Parallel GC,Inceremental GC와 같이 추가적인 GC Algorithm이 존재한다. 옵션과 내용은 앞장에서 설명한 “다양한 GC알고리즘” 을 참고하기 바란다.

7.JVM GC 튜닝

그러면 이제부터 지금까지 설명한 내용을 기반으로 실제로 JVM 튜닝을 어떻게 하는지 알아보도록 하자.

STEP 1. Application의 종류와 튜닝목표값을 결정한다.

JVM 튜닝을 하기위해서 가장 중요한것은 JVM 튜닝의 목표를 설정하는것이다. 메모리를 적게 쓰는것이 목표인지, GC 횟수를 줄이는것이 목표인지, GC에 소요되는시간이 목표인지, Application의 성능(Throughput or response time) 향상인지를 먼저 정의한후에. 그 목표치에 근접하도록 JVM Parameter를 조정하는것이 필요하다.

STEP 2. Heap size와 Perm size를 설정한다.

-ms와 ?mx 옵션을 이용해서 Heap Size를 정한다. 일반적으로 server application인 경우에는 ms와 mx 사이즈를 같게 하는것이 Memory의 growing과 shrinking에 의한 불필요한 로드를 막을 수 있어서 권장할만하다.

ms와mx사이즈를 다르게 하는 경우는 Application의 시간대별 memory 사용량이 급격하게 변화가 있는 Application에 효과적이다.
PermSize는 JVM vendor에 따라 다소 차이가 있으나 일반적으로 16m정도이다. Client application의 경우에는 문제가 없을 수 있지만, J2EE Server Application의 경우 64~128m 사이로 사용이 된다.

Heap Size와 Perm Size는 아래 과정을 통해서 적정 수치를 얻어가야한다.

STEP 3. 테스트 & 로그 분석.

JVM Option에 GC 로그를 수집하기 위한 ?verbosegc 옵션을 적용한다. (HP의 경우 ?Xverbosegc 옵션을 적용한다.)

LoadRunner나 MS Strest(무료로 MS社의 홈페이지에서 다운로드 받을 수 있다.)와 같은 Strest Test툴을 통해서 Application에 Strest를 줘서. 그 log를 수집한다. 튜닝에서 있어서 가장 중요한것은 목표산정이지만, 그만큼이나 중요한것은 실제 Tuning한 Parameter가 Application에 어떤 영향을 주는지를 테스트하는 방법이 매우 중요하다. 그런 의미에서 적절한 Strest Tool의 선정과, Strest Test 시나리오는 정확한 Tuning을 위해서 매우 중요한 요인이다.

○ Perm size 조정
아래 그림8.은 HP JVM에서 ?Xverbosegc 옵션으로 수집한 GC log를 HP Jtune을 통해서 graph로 나타낸 그래프이다. 그림을 보면 Application이 startup되었을때 Perm 영역이 40m에서. 시간이 지난후에도 50m 이하로 유지되는것을 볼 수 있다. 특별하게 동적 classloading등이 수십m byte가 일어나지 않는등의 큰 변화요인이 없을때, 이 application의 적정 Perm 영역은 64m로 판단할 수 있다.

 

 

<그림 8. GC 결과중 Perm 영역 그래프>

○ GC Time 수행 시간 분석

다음은 GC에 걸린 시간을 분석해보자. 앞에 강좌 내용에서도 설명햇듯이. GC Tuning에서 중요한 부분중 하나가 GC에 소요되는 시간 특히 Full GC 시간이다.

지금부터 볼 Log는 모社의 물류 시스템의 WAS 시스템 GC Log이다. HP JVM을 사용하며, -server ?ms512m ?mx512m 옵션으로 기동되는 시스템이다.

그림 9를 보면 Peak 시간 (첫번째 동그라미) 14시간동안에 Full GC(동그란점)가 7번일어난것을 볼 수 있다. 각각에 걸린 시간은2.5~6sec 사이이다.
여기서 STEP 1.에서 설정한 AP Tuning의 목표치를 참고해야하는데.

Full GC가 길게 일어나서 Full GC에 수행되는 시간을 줄이고자 한다면 Old 영역을 줄이면 Full GC가 일어나는 횟수는 늘어나고, 반대로 Full GC가 일어나는 시간을 줄어들것이다.

반대로 Full GC가 일어나는 횟수가 많다면, Old 영역을 늘려주면 Full GC가 일어나는 횟수는 상대적으로 줄어들것이고 반대로 Full GC 수행시간이 늘어날 것이다.

특히 Server Application의 경우Full GC가 일어날때는 JVM자체가 멈춰버리기 때문에, 그림 9의 instance는 14시간동안 총 7번 시스템이 멈추고, 그때마다 2.5~6sec가량 시스템이 response를 못하는 상태가 된것이다. 그래서 멈춘 시간이 고객이 납득할만한 시간인지를 판단해야 하고, 거기에 적절한 Tuning을 해야한다.

Server Application에서 Full GC를 적게일어나게하고, Full GC 시간을 양쪽다 줄이기 위해서는 Old영역을 적게한후에, 여러개의 Instance를 동시에 뛰어서 Load Balancing을 해주면, Load가 분산되기 때문에 Full GC가 일어나는 횟수가 줄어들테고, Old 영역을 줄였기 때문에, Full GC에 드는 시간도 줄어들것이다. 또한 각각의 FullGC가 일어나는동안 하나의 서버 instance가 멈춰져 있어도, Load Balancing이 되는 다른 서버가 response를 하고 있기때문에, Full GC로 인한 Application이 멈추는것에 의한 영향을 최소화할 수 있다.

 

 

<그림 9. GC 소요시간>

데이타에 따라서 GC Tuning을 진행한후에는 다시 Strest Test를 진행해서 응답시간과 TPS(Throughput Per Second)를 체크해서 어떤 변화를 주었는지를 반드시 체크해봐야한다.

 

 

<그림 10. GC후의 Old 영역>

그림 10은 GC후에 Old 영역의 메모리 변화량을 나타낸다.

금요일 업무시간에 메모리 사용량이 올라가다가. 주말에가서 완만한 곡선을 그리는것을 볼 수 있다. 월요일 근무시간에 메모리 사용량이 매우 많고, 화요일에도 어느정도 메모리 사용량이 있는것을 볼 수 있다. 월요일에 메모리 사용량이 많은것을 볼때, 이 시스템의 사용자들이 월요일에 시스템 사용량이 많을 수 있다고 생각할 수 있고, 또는 다른 주의 로그를 분석해봤을때 이 주만 월요일 사용량이 많았다면, 특별한 요인이나 Application 변경등이 있었는지를 고려해봐야할것이다.

이 그래프만을 봤을때 Full GC가 일어난후에도 월요일 근무시간을 보면 Old 영역이 180M를 유지하고 있는것을 볼 수 있다. 이 시스템의 Full GC후의 Old영역은 80M~180M를 유지하는것을 볼 수 있다. 그래서 이 시스템은 최소 180M이상의 Old 영역을 필요로하는것으로 판단할 수 있다.

STEP 4. Parameter 변경.
STEP 3에서 구한 각 영역의 허용 범위를 기준으로 Old영역과 New 영역을 적절하게 조절한다.
PermSize와 New영역의 배분 (Eden,Survivor)영역등을 조정한다.
PermSize는 대부분 Log에서 명확하게 나타나기 때문에, 크게 조정이 필요가 없고 New영역내의 Eden과 Survivor는 거의 조정하지 않는다. 가장 중요한것은 Old영역과 New 영역의 비율을 어떻게 조정하는가가 관건이다.

이 비율을 결정하면서, STEP1에서 세운 튜닝 목표에 따라서 JVM의 GC Algorithm을 적용한다. GC Algorithm을 결정하는 기본적인 판단 내용은 아래와 같다.

 

 

이렇게 Parameter를 변경하면서 테스트를 진행하고, 다시 변경하고 테스트를 진행하는 과정을 거쳐서 최적의 Parameter와 GC Algorithm을 찾아내는것이 JVM의 메모리 튜닝의 이상적인 절차이다.

지금까지 JVM의 메모리 구조와 GC 모델 그리고 GC 튜닝에 대해서 알아보았다.

정리하자면 GC 튜닝은 Application의 구조나 성격 그리고, 사용자의 이용 Pattern에 따라서 크게 좌우 되기때문에, 얼마만큼의 Parameter를 많이 아느냐 보다는 얼마만큼의 테스트와 로그를 통해서 목표 값에 접근하느냐가 가장 중요하다

참조 : 자바스터디 네트워크 [www.javastudy.co.kr]조대협 [bcho_N_O_SPAM@j2eestudy.co.kr]

출처 : 바람이 될래!
글쓴이 : 황진이 원글보기
메모 :

목차


  1. Introduction
    1. Objectives
    2. Target Audience
    3. 참고 문서
  2. Basic rules
    1. 포맷 규칙(Formatting rule)
      1. 들여쓰기 및 탭 문자 사용
      2. 블록의 시작 및 끝
      3. 라인의 작성 기준
      4. 줄바꿈 및 여백 활용 기준
    2. 클래스 이름 부여 규칙
      1. 클래스 명칭의 접두 문자
      2. 클래스 명칭의 첫번째 단어
      3. 클래스의 접미 단어
    3. 변수 이름 부여 규칙
      1. 변수명의 접두사
      2. 변수명의 첫번째 어절
      3. 변수명의 두번째 어절
    4. 함수 이름 부여 규칙
      1. 일반 함수명의 첫번째 어절 및 나머지 어절
      2. 멤버 함수인 경우
    5. 상수명 부여 규칙
      1. 대문자 사용 원칙
    6. 소스 파일 작성 규칙
      1. 소스 파일 설명
      2. 헤더 파일 설명
    7. Java 코딩 규칙
      1. Description
    8. 주석
      1. 항목 뒤에 붙이는 주석
      2. 항목 위에 붙이는 주석
    9. 영역 (area)
      1. 패키지 선언
      2. 임포트(import) 선언
      3. 클래스 선언
      4. 상수 선언
      5. 변수 선언
      6. 생성자(constructor)  선언
      7. property 선언
      8. public 메소드 선언
      9. protected, private 메소드 선언
  3. JSP 프로그램 코딩 규칙
    1. 머리말(header)
    2. 주석(comment)
      1. 영역에 대한 주석
      2. 한 줄에 대한 주석
      3. 여러 줄에 대한 주석
      4. 항목 뒤에 붙이는 주석
    3. 블록 설정 시 코딩 형식
      1. 명령문이 한줄이 경우
      2. 명령문이 한줄이고 else가 있을 경우
      3. 명령문이 2줄이상이고 else가 있을 경우
    4. 영역 (area)
      1. 설명문(description) 서술
      2. 지시어(directive) 서술
      3. 각종 빈(bean) 선언
      4. 빈(bean)의 속성(property) 설정
      5. 선택적 JSP 선언문
      6. 각종 상태 점검 및 초기화 코드 작성
      7. HTML 태그 서술
      8. <HEAD> </HEAD> 블록 서술
      9. <BODY> </BODY> 블록 기술
  4. 참고 자료

 

Introduction#

Objectives#

프로그램 소스의  표준화를  통해서 코딩의 규격화와 효율성 및  가독성을  증대시키고, 유지 보수를  원
활히 하는데 목적이 있다.
소프트웨어  제품 개발에 사용되는 언어가 다양하므로 특정  언어  및  환경에 의존적이지 않는  일반적
인  표준  코드  서술  규범을  먼저  기술하고  특정  언어  및  환경을  위한  서술  규범은  별도로  기술한다.
또한,  이 문서에 기술되지 않았거나 문서의  기준을 적용하기  곤란한 부분에  대해서는 헝가리안 표기
법 및 관습적인 코딩 규약을 사용할 것을 권장한다.

 

Target Audience#

웹 개발자 및 ActiveX 개발자

 

참고 문서#

Sun  microsystem에서  권장하는  자바  코딩  관습  문서는  “http://java.sun.com/docs/codeconv/”에서  찾아볼 수 있다.

 

Basic rules#

포맷 규칙(Formatting rule)#

들여쓰기 및 탭 문자 사용#

들여쓰기는  코드에서  블록의  시작과  끝을  쉽게  알아볼  수  있게  해준다.  들여쓰기  되었을  때,  더욱
읽기  좋고,  더  많은  단계의  블록이  다른  블록  내부에  포함되어  있을  때,  들여쓰기의  필요성은  더욱
강조된다. 들여쓰기  및  탭  크기는  기본적으로  4  글자로 한다.  탭  문자는  사용하지 않는  것을  원칙으
로 한다. 다음은 들여쓰기 예이다.

 

  1. <myTagLib:forEach var="client" items="${clients}">
        <myTagLib:mail value="${client}" />
    </myTagLib:forEach>

 

연속 들여쓰기는  이전  줄의 적당한  지점에서  시작한다. 연속  들여쓰기의 공백은  기본 들여쓰기 공백
의 배수이다. (4개의 공백문자의 배수):

 

  1. <%@ page    attribute1="value1"
                attribute2="value2"
                ...
                attributeN="valueN"
    %>

 

블록의 시작 및 끝#

중괄호”{“  와  중괄호”}”  는  항상  수직선상에  위치하도록  한다  또한  항상  블록의  시작에서는 4칸  들
여쓰기를  한다.  자바  진영의  주된  경향(흐름)은  시작하는  괄호를  코드의  끝에  위치ㅅ키는  것이지만,
시각적 효과를 위해 수직 정렬을 표준으로 채택하였다.

  1.     main() {
            for( ; ; ) {
            }
        }

라인의 작성 기준#

1024*768 화면 기준으로 각 라인은  한 화면에  가급적 전부 보이도록 100 컬럼이내로  짧게  작성하며,
코드의 길이가 긴 라인은 두 줄에 걸쳐서 작성한다. 한 줄에 꼭 하나의 명령문만을 기술하여야 한다.
복잡한 수식을 가진 긴 라인이라면 여러 개의 작은 수식으로 분할하여 작성한다.

double length = Math.sqrt( Math.pow(Math.random(),2.0) + Math.pow(Math.randown(),2,0) ) ; 은 아래와 같이
분리한다.

 

  1.     double xSquared = Math.pow(Math.random(), 2.0 ) ;
        double ySquared = Math.pow(Math.random(), 2, 0 ) ;
        double length    = Math.sqrt(xSquared + ySquared) ;

줄바꿈 및 여백 활용 기준#

accept.png 각 코드 블록(code block)간의 여백은 한 줄을 띄우는 것을 원칙으로 하며, 함수 혹은 각 영역(area)간의 간격은 두 줄을 원칙으로 한다.
accept.png  각각의  생성자  그리고  각각의  메소드와  같은  클래스의  부분들(parts)  사이에  적어도  하나  이상의  빈줄을삽입하라.
accept.png  메소드 내부에서 지역 변수의 선언 이후에, 그리고 코드의 다른 영역들 사이에 빈 줄을 삽입하라.
accept.png  연산자( =, /, * 등)의 양 옆에 공백을 삽입하라. 그러나 증가(++), 감소(--), 부정(!) 같은 복합연산자에는 공백을 이용해서는 안된다
accept.png  for  구문의  부분들을  공백으로  구분하여라.  초기화,  검사,  증가  부분을  구분하는  세미콜론(;)  다음에  공백을 준다.
accept.png  메소드의 인자(parameter)  목록의 쉼표(comma)와 뒤의 공백은 각각의 인자의 시작과 끝을 명확하게 만들어준다.

클래스 이름 부여 규칙#

클래스 명칭의 접두 문자#

클래스 명칭의 첫번째 문자는 클래스를  뜻하는  대문자  ‘C’로 시작하거나,  알파벳  대문자로 사용한다.

 

클래스 명칭의 첫번째 단어#

클래스  명칭의  접두  문자  이후의  첫번째  단어는  클래스의  성격을  잘  나타낼  수  있는  명사  혹은  형용사를 사용하며, 단어의 첫 글자는 대문자로 시작하며, 나머지는 소문자로 기술하여야 한다.

 

클래스의 접미 단어#

클래스의 종류를 나타내는 접미사를 붙여주는 것을 권장한다.

  → 다이얼로그  : Dlg
  → 윈도우      : Wnd
  → 문서        : Doc
  → 자바 빈     : Bean
  → 커스텀 태그 : Tag

 

변수 이름 부여 규칙#

 

변수명의 접두사#

클래스의 멤버 변수일 때는 m을 접두사로서 붙여준다.
함수의 전역 변수인 경우에는 g를 접두사로 붙이고, static 변수인 경우에는 s를 붙이도록 한다.
변수명의 접두사는 항상 소문자를 쓰도록 한다.

 

변수명의 첫번째 어절#

변수명의  접두사  이후에 위치하는  첫번째 어절은 변수의 타입을 쓴다.

 

[ Table 1 ]  변수 유형에 따른 표기 방법

변수타입 변수타입

정수 (integer)            n

문자 (char)               c

NULL 종료 문자열      sz

배열 (array)              arr

실수 (float)          f

문자열 (String)     str

논리형 (Boolean)  b or is

더블 (double)       d

변수명의 두번째 어절#

변수명의  두번째  어절은  첫번째  문자만  대문자로  사용하고  나머지는  소문자로  표시하며,  변수의  의
미 및 용도를 잘 나타낼 수 있는 명사를 선택한다.

 

함수 이름 부여 규칙#

일반 함수명의 첫번째 어절 및 나머지 어절#

함수명을 구성하는 각 어절의 첫 문자는 대문자로 시작하고, 나머지 문자는 소문자로 기술한다.
어절의 첫번째 문자를 대문자로 기술하는 것은 단어를 시각적으로 구별하기 쉽게하는 효과가 있다.

 

멤버 함수인 경우#

클래스의  멤버  함수의  경우에는  첫번째  어절의  첫  문자를  소문자로  기술해야  하며,  개체의  속성
(property)을  나타내는  멤버  함수일  경우에는  get  이라는  접두사를  붙이고,  속성을  지정하는  함수일
경우 set이라는 접두사를 붙인다. (단, MS  계열 개발 언어에서는 메소드의 명칭이 대문자로 시작하는
관습이 통용되고 있다.)

상수명 부여 규칙#

대문자 사용 원칙#

상수 이름은  항상 대문자를 사용하되 각 단어 혹은 어절의  사이에는 밑줄(_)로 연결한다. 상수에  대문자를 사용하는 것은 다른 종류의 명칭들과 구별하기 위함이다.

 

소스 파일 작성 규칙#

소스 파일 설명#

.html,  .js, .java  등  소스  파일은  파일의  최상위 부분에 그 파일에  대한 정보를  표시해주어야 하며  다음과 같은 주석을 작성한다.

  1.   /*
       * 작성일자 : 2001-05-25
       * 작 성 자 : 홍길동
       * 파일이름 : file.cpp
       * 수정이력 : 2001-05-25 최초 작성
       * 작성목적 : 프로젝트명, 프로그램명, 소스 용도, 공통 시스템 혹은 단위 시스템 여부.
       */

 

헤더 파일 설명#

.h 등 헤더 파일에는 다음과 같은 주석을 작성한다.

  1.   /*
       * 작성한날 : 2001-05-25
       * 작 성 자 : 최정현
       * 파일이름 : file.h
       * 수정이력 : 2001-05-25 최소 작성
       * 작성목적 : 프로젝트명, 프로그램명, 소스 용도, 라이브러리 여부
       */

 

Java 코딩 규칙#

 

Description#

머리말은 모든  소스 파일의 처음  부분에  기술하는 소스에 대한 설명이다. Java  프로그램의 머리말은 다음과 같은 형식으로 작성한다.

  1. /**
     * <PRE>
     * 프 로 그 램 명 : UserRegisterBean.java
     * 작    성    자 : 곽중선
     * 작    성    일 : 2001/09/24
     * 설          명 : 사용자 관리를 위한 register bean
     * 수  정  이  력 : 2004/02/22
                         - 각종 메소드를 가상함수로 전환, SimpleUserRegisterBean 추가
     *                 2004/05/08 - 가상함수 제거, 일반 클래스로 전환
     * ver            : 1.1
     * 검  토  사  항 : - 사용자 역할 변경 시 관련 문서함 자동 생성
     * </PRE>
     */

 

주석#

항목 뒤에 붙이는 주석#

소스  한  라인에  대한  주석문  혹은  변수의  선언  후에  변수에  대한  설명을  짧게  적을  경우에  문자의 오른편에 기술한다.

  1. private PageContext pageContext;  // JSP페이지의 모든 객체정보를 가지고 있다.
    private long          startTime;    // 시스템의 현재의 시간을 가져온다.

 

항목 위에 붙이는 주석#

주석을  항목 위에  붙이는  경우는  두  라인 이상의 명령문에  대한  해석이나 메소드에  대한 설명을 붙이는 경우이다. 주석 내용의 위 아래에 단일선으로 시작과 끝을 표시하도록 한다.

  1. /**
     * 디버그 빈이 실행된 시간을 가져온다.
     */
    public DebugBean()
    {
       startTime = System.currentTimeMillis();
    }

 

영역 (area)#

영역이란 소스 파일을 기술하는 내용의 성격에 따라서 분류하는 것을 의미하며 다음과 같은 순서에 따라 서술하도록 한다.

 

패키지 선언#

자바 클래스가 위치한 패키지 명을 가장 우선 선언한다.

  1. package gw;

 

임포트(import) 선언#

자바 표준 API 패키지를 먼저 기술하고, 커스텀 패키지를 나중에 기술한다.

  1.  import java.util.*;
     import javax.servlet.*;
     import javax.servlet.http.*;
     import javax.servlet.jsp.*;

 

클래스 선언#

클래스의 명칭과 공개 여부 등을 선언한다.

  1.     public class DebugBean

 

상수 선언#
  1.     static private final boolean   TRACE_FLAG = true;
        static private final String     LINE_FEED =
    System.getProperty("line.separator");

 

변수 선언#

public, protected, pritvae 순으로 변수를 선언한다.

 

  1.  private PageContext     pageContext;  // JSP페이지의 모든 객체를 가지고 있다.
     private long              startTime;    // 시스템의 현재의 시간을 가져온다.
     private String           debugType;
     private ServletContext context;

 

생성자(constructor)  선언#

클래스의 생성자 메소드를 가장 우선적으로 기술한다.

  1.     /**
         * [생성자]
         *  기본 디버그 방식은 로그 파일 저장으로 설정한다.
         **/
        public DebugBean()
        {
            m_strDebugType = "log";
        }

 

property 선언#

클래스의 외부에서 사용할 수 있는 모든 property는 생성자 및 파괴자 다음에 기술해야 한다.

  1.  /**
       * [set debugType property]
       *   m_strDebugType값을 설정한다.
       */
        public void setDebugType(String strDebugType)
        {
            m_strDebugType = strDebugType;
        }

 

public 메소드 선언#

모든 public 메소드는  private 메소드 보다  우선적으로  기술하여야 하며, 다른  메소드를 호출  혹은 이용하는 메소드를 앞서서 기술해야 한다.

  1. /**
     * 디버그 빈이 실행된 시간을 가져온다.
     */
     public DebugBean()
     {
         startTime = System.currentTimeMillis();
     }

 

protected, private 메소드 선언#
  1.     /**
          * 디버그 메시지를 로그에 남기거나 또는 콘솔로 출력을 한다.
          */
        private void log(String propName, Hashtable values)
        {
            log( propName, toTabbedTable(values) );
        }

 

JSP 프로그램 코딩 규칙#

구조가 잘 갖춰진 소스 코드 파일은 읽기 쉽고, 파일 사이에 정보를 더 빨리 놓아둘 수 있다.
JSP 파일은 다음의 섹션들이 나열한 순서대로 구성된다:

accept.png  도입 주석
accept.png  JSP 페이지 지시자(directive)
accept.png  선택적 태그 라이브러리 지시자
accept.png  선택적 JSP 선언문
accept.png  HTML 과 JSP 코드

 

머리말(header)#

JSP의 머리말은 다음과 같은 형식으로 작성한다. 이 주석은 서버측에서만 볼 수 있다. JSP 변환 과정
에서 제거되기  때문이다.  여기에는  웹 개발자를  위한 JSP에  대한 작성자,  일자,  개정판의 저작권,  증
명과 설명이 포함된다.

 

  1. <%--
        단위 시스템 명 : admin
        서브 시스템 명 : 없음
        프 로 그 램 명 : page.jsp
        작    성    자 : 곽중선
        작    성    일 : 2001/10/18
        설          명 : 이 프로그램은 Page를 출력하기 위한 프로그램입니다.
        ver            : 1.0
    --%>

 

주석(comment)#

JSP에서의 주석은 다음과 같은 세가지 유형을 사용할 수 있다.

 

영역에 대한 주석#
  1.  <%------------------------------------------------------------------
       comment area
       ------------------------------------------------------------------%>

 

한 줄에 대한 주석#
  1.     <% //   comment area   %>

 

여러 줄에 대한 주석#
  1.     <% /*
         comment area
        */ %>

 

항목 뒤에 붙이는 주석#

변수 선언에는 반드시 주석을 서술한다.

  1.     String  str_count;          // 전체 행수를 취득
        String  str_jsql;           // 행수 취득 sql
        int i_boardPage    = 1;  //  선택 page
        int i_MAX_list     = 3;  //  모든 열수
        int i_total_row     = 0;  // 모든 행수
        int i_dspTotalPage = 4;  // 보여 줄 전체 page

 

블록 설정 시 코딩 형식#

명령문이 한줄이 경우#
  1. if (str_Page != null )
        i_boradPage = Integer.parseInt(str_Page);

 

명령문이 한줄이고 else가 있을 경우#
  1.  if ( (I_total_row % i_MAX_list ) > 0 )
         i_totalPage = i_tempPage + 1;
     else
         i_totalPage = i_tempPage + 1;

 

명령문이 2줄이상이고 else가 있을 경우#
  1.  if ( (i_total_row % i_MAX_list ) > 0 )
     {
         i_totalPage = i_tempPage + 1;
         ……
     }
     else
     {
         i_totalPage = i_tempPage + 1;
          ……
     }

 

영역 (area)#

영역이란 소스 파일을 기술하는 내용의 성격에 따라서 분류하는 것을 의미한다.
다음과 같은 5가지 영역으로 분할하여 순서대로 서술하도록 한다.

 

설명문(description) 서술#

소스의  관련 시스템 명,  작성자,  작성  이력,  기능에  대한 설명 등을  서술한다.  JSP  머리말 작성  방법을 참고하라.

 

지시어(directive) 서술#

JSP  페이지 지시자는  JSP  변환  시점에  관련된 속성을 정의한다. JSP 스펙은  같은 페이지에  JSP  페이지  지시자의  수량을 제한하지 않는다.  페이지에  사용할  스크립트  언어, 세션 트래킹의  사용  여부, 그리고 오류를 기록할 페이지 정보, JAVA import 경로, file include,  커스텀 태그 라이브러리 등을 선언한다.

  1. ① <%@ page language="java" contentType="text/html" %>
    ② <%@ page errorPage="/public/script/errorPage.jsp" %>
    ③ <%@ taglib uri="/WEB-INF/ufTagLib.tld" prefix="uftag" %>
       <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="jstlc" %>
       <%@ taglib uri="http://java.sun.com/jsp/jstl/functions" prefix="jstlfn" %>
    ④ <%@ page import="java.util.List" %>
       <%@ page import="jsp.sql.DataSourceManager" %>
       <%@ page import="jsp.sql.ConnectionWrapper" %>
       <%@ page import="jsp.beans.security.*" %>

 

① JSP에서 사용하는 언어, 컨텐츠 타입을 정의한다.
② 페이지 내부에서 예외가 발생할 경우, 오류를 처리할 페이지 주소를 나타낸다.
③ 태그 라이브러리 지시자는 JSP 에서 사용하는 커스텀 태그 라이브러리를 선언한다. 짧은 지시자는 하나의 줄에 선언할 수 있다. 여러 개의 태그 라이브러리 지시자들은 한 곳에 같이 모아둔다:
④ import하는 외부 클래스들을 선언한다. 가급적 ".*" 를 이용해 패키지를 import하지 말고, 클래스를 일일이 선언한다.

 

각종 빈(bean) 선언#

JSP 페이지 내에서 참조하거나 생성하는 모든 bean에 대한 선언을 기술한다.

  1. <%-- 디버깅 빈 적재 --%>
    <jsp:useBean id="debug" class="jsp.beans.util.DebugBean" >
        <jsp:setProperty name="debug" property="pageContext" value="<%=pageContext %>" />
        <jsp:setProperty name="debug" property="debugType" value="log" />
    </jsp:useBean>
  2.  
  3. <%-- 코드 관리 빈(bean) 적재 --%>
    <jsp:useBean id="codeRegistry" scope="session" class="jsp.beans.registry.CodeRegistryBean">
        <jsp:setProperty name="codeRegistry" property="dataSource" value="admDB" />
    </jsp:useBean>

 

빈(bean)의 속성(property) 설정#

앞서 선언한 빈(bean)의 속성을 설정하는 코드를 기술한다.

  1. <jsp:setProperty name="codeRegistry" property="pageContext" value="<%=pageContext %>" />
    <jsp:setProperty name="codeRegistry" property="codeId" />
    <jsp:setProperty name="codeRegistry" property="codeName" />
    <jsp:setProperty name="codeRegistry" property="codeExplain" />
    <jsp:setProperty name="codeRegistry" property="codeDate" />

    <jsp:setProperty name="debug" property="debugLog" value='<%=request.getParameter("operation") %>' />

 

선택적 JSP 선언문#

JSP  선언문은  JSP에서  소속된  메소드와  변수를  선언한다.  이들  메소드와  변수는  자바  프로그램에서의 선언문과 다를바 없다. 선언문은 <%! ... %> JSP 선언문 블록 하나에 모두 몰아서 한 곳에 집중해 놓는 것이 좋다.

 

  1. <%!
         private int hitCount;
         private Date today;

         public int getHitCount() {
             return hitCount;
         }
    %>

 

각종 상태 점검 및 초기화 코드 작성#

사용자 로그인 상태 체크, 페이지 파라미터 점검 등의 작업을 수행하는 코드를 기술한다.

  1. <%-- 사용자 로그인 확인 --%>
    <pubtag:validateSession name="userBean" />

 

HTML 태그 서술#

HTML  소스의 시작을 표시하는  <HTML>  태그를  기술한다.  비즈니스  로직(business  logic)만을  처리하며, 화면에 내용을 표시하지 않는 페이지일 경우에는 생략한다.

 

<HEAD> </HEAD> 블록 서술#

HTML 페이지에서 머리말(header)에 해당하는 블록이며, 이 부분에는 다음과 같은 내용을 서술한다.

HTML 페이지의 간단한 제목, <TITLE> 태그
CSS(Cascading Stytle Sheet) 기술
CSS를 사용해서 헤딩, 테이블 등의 공통적인 특성을 중앙에서 제어하도록 한다. 이 방법은 사용자들에게  프리젠테이션의  일관성을  향상시키고,  유지보수  노력과  JSP  페이지의  코드  사이즈를  줄여준다.
가급적 아래의 예제 중에서 앞선 방식을 권장한다.

 

  1.  <LINK REL=StyleSheet HREF="../css/common.css">

또는,

  1.  <STYLE TYPE="text/css">
     .ic {
         font-family: webdings;
         color: #999900;
     }
     </STYLE>

 

클라이언트 자바스크립트(Client-side Javascript) 함수 기술

자바 스크립트가  제대로  돌아가기  위해서는  자바스크립트는  브라우저  타입의 특성과 관계없어야  된다.  또한  동작을  한다면,  JSP에서  자바스크립트  코드를  독립적인  파일로  분리하고,  다음과  같이  JSP에서 자바스크립트를 불러쓰는 방식을 권장한다.
이렇게  하면  자바스크립트의  재사용성도  향상되고,  여러  JSP  사이에  일관된  습성의  자바스크립트를 유지할 수 있고, JSP 페이지의 복잡성을 줄여준다.

 

  1. <SCRIPT LANGUAGE="JavaScript" SRC="./Lib/jsc/gwMenu.jsc"></SCRIPT>
    <SCRIPT LANGUAGE="JavaScript">
       function goSubMenu( url )
       {
           var thisForm = document.forms[0];
           thisForm.action = url;
           thisForm.submit();
       }
    </SCRIPT>

 

<BODY> </BODY> 블록 기술#

HTML 코드 작성시 HTML 태그 혹은 예약어는 모두 대문자로 서술하는 것을 원칙으로 한다.

 

JSP 스크립틀릿 JSP Scriptlets

가급적,  태그라이브러리로  동일한  기능을  구현할  수  있으면,  JSP  스클립틀릿은  피하기  바란다.  이렇
게  하면 페이지  가독성도 높이고, 유지보수도 쉬워진다. 비지니스  로직을  프리진테이션  로직에서 분
리하는데 일조하며, JSP 2.0 스타일의 페이지로 쉽게 옮겨갈 수 있다. (JSP 2.0도 스크립틀릿을 지원하
지만  덜 강조한다). 다음  예제는  customers  표시  방법이 데이터  형에  따라서  다른  스크립틀릿을  사용
하는 것을 보여준다:

customers 는 Customers의 배열

  1. <table>
        <%  for ( int i=0; i<customers.length; i++ ) { %>
            <tr>
                <td><%= customers[i].getLastName() %></td>
                <td><%= customers[i].getFirstName() %></td>
            </tr>
        <%  } %>
    </table>


customers 는 Enumeration 형

  1. <table>
        <%  for ( Enumeration e = customers.elements();
                e.hasMoreElements(); ) {
                Customer customer = (Customer)e.nextElement();
        %>
            <tr>
                <td><%= customer.getLastName() %></td>
                <td><%= customer.getFirstName() %></td>
            </tr>
        <%  } %>
    </table>

 

그러나,  공통  태그라이브러리를  사용하면,  customers  의  데이터  형이  달라도  보다  높은  유연성이  생긴다. 예를 들어서,  JSTL  에서는customers 를 나타내기  위해서 배열과  Enumeration  양쪽 모두  지원하는 다음과 같은 코드로 표시할 수 있다:

 

  1. <table>
        <c:forEach var="customer" items="${customers}">
            <tr>
                <td><c:out value="${customer.lastName}"/></td>
                <td><c:out value="${customer.firstName}"/></td>
            </tr>
        </c:forEach>
    </table>

 

비지니스  로직과  프리젠테이션 층과의 결합도을  줄이기  위한  모델-뷰-컨트롤러(MVC)  디자인  패턴을 적용하려고  한다면,  JSP  스크립틀릿은  비지니스  로직을  구현하기  위해서  사용되어서는  안된다.  오히려,  JSP  스크립틀릿은  클라이언트의  요청을  처리한  뒤에  나오는  데이터("가치  객체  value  objects"로
불리는)를 적절한 클라이언트의  준비된  형식에  전송하기 위해  필요하다면  사용된다.  심지어  그럴 경우에도, 프론트 컨트롤러 서블릿이나 커스텀 태그로 하는 게 더 낫다.

 

JSP 표현식 JSP expressions

JSP 표현식은 JSP scriptlet처럼 가능하면 삼가는 것이 좋다. 동일한 작업을 수행하는 다음 3가지 예제가 잘 설명해준다.  가급적 나중에 기술될 코드 스타일을 권장한다.

  1. Example 1 (자바 코드 사용):
        <%= myBean.getName() %>

    Example 2 (JSP 태그 사용):
        <jsp:getProperty name="myBean" property="name" />

    Example 3 (JSTL 태그 사용):
        <c:out value="${myBean.name}" />

 

프로그래밍 습관 Programming Practices

일반적으로,  다음과  같은  이유로  JSP  페이지에  자바  코드(선언문,  스크립틀릿,  표현식)  쓰기를  피한다:

accept.png JSP  페이지의  자바 코드 문법  에러는 페이지가 배치되기 전까지 발견되지 않는다. 반면에, 태그 라이브러리와 서블릿에 있는 문법 에러는 배치 이전에 발견된다.
accept.png  JSP 페이지 내의 자바 코드는 디버깅하기 힘들다.
accept.png  JSP 페이지 내의 자바 코드는 유지보수하기 힘들다.
accept.png  보통 복잡한 비즈니스  로직과  프리젠테이션 로직을 섞어놓지  않는 것이  납득이  가는 습관이다. JSP는 우선적으로 프리젠테이션 로직을 목적으로 한다.
accept.png  자바 코드가 포함된 코드와 HTML과 다른 스크립트 명령어가 섞여있다면 읽기 어렵다.
accept.png  JSP  2.0  은  더  단순한  표현  언어(EL)  덕분에  scriptlet의  중요도를  낮추었다.  JSP  에서JSP 2.0 스타일로 쉽게 옮기기 위해서는 자바 코드를 사용하지 않는 것이 좋다.

JSP 내장 객체 JSP Implicit Objects

API  호출을  통해서  객체를  참조하기  보다는  JSP  내장  객체를  직접  사용하는  것이  좋다.  그래서 ServletContext  인스턴스에  의해  제공되는  초기  파라미터에  접근하기  위해  다음을  쓰는  대신에  다음의 코딩 스타일 중 가급적 나중 방식을 권장한다.

 

  1. getServletConfig().getServletContext().getInitParameter("param")

    application.getInitParameter("param")

    <c:out value="${initParam['param']}" />

 

인용부호 (Quoting)

일정한 인용부호의 사용을 채택한다. 인용부호는 작은따옴표 ' 대신 큰따옴표 " 를 사용한다.

[ Table 2 ]  인용 부호 표기법

불규칙한 인용부호 좋은 인용부호
<%@ page import='javabeans.*'%> <%@ page import="javabeans.*" %>

예외적인 경우는  작은따옴표가  필요할 때이다.  예를  들어서 스크립트 언어에서 큰따옴표를  사용하는
경우를 들 수 있다:

  1. <jsp:include page='<%= getFoodMenuBar("Monday") %>' />

 

커스텀 태그 사용하기 Using Custom Tags

만일 커스텀태그가 몸체가  없다면,  컨텐트는 (생략해서  "JSP"  기본값으로 하지 말고)  empty라고 명확
하게 선언되어야 한다. 태그 라이브러리 설명서에서 다음과 같이 정해준다:

  1. <tag>
        <name>hello</name>
        <tag-class>com.mycorp.util.taglib.HelloTagSupport</tag-class>
        <body-content>empty</body-content>
        ...
    </tag>

이렇게  하면  JSP 컨테이너는 태그  몸체가 무조건 비어야 되며 파싱할 어떤 JSP  문법도  포함하지  않아야 된다고  인식한다. 효과는 빈  몸체를 파싱하기 위해서 불필요하게  자원의 할당이  이뤄지는 것을 제거한다.
빈  태그는,  가독성향상을  위해  열고  닫는  XML  엘리먼트  쌍으로  표시하기보다는,  짧은  XML  엘리먼트로 표시한다. 그래서,

 

  1. <myTag:hello />

 

라고 표기하는 것이 아래보다 낫다.

 

  1. <myTag:hello></myTag:hello>

 

스크립트 엘리먼트의 들여쓰기 Indentation of Scripting Elements

JSP  스크립트  엘리먼트(선언문,  스크립틀릿,  표현식과  같은)가  한  줄에  맞지  않는  경우,  스크립트  언
어의 들여쓰기 작성요령이  엘리먼트  내에  적용된다. 몸체는  엘리먼트의  여는  기호  <%=와 같은  줄에
서  시작한다.  다음줄의  기준도  여는  기호  <%=가  된다.  몸체의  끝은  다른  줄에  있는  엘리먼트의  닫
는 기호(%>)로 마무리된다. 예제이다:

  1. <%= (Calendar.getInstance().get(Calendar.DAY_OF_WEEK)
       = Calendar.SUNDAY) ?
         "Sleep in" :
         "Go to work"
    %>

첫줄과  마지막  줄을  제외한  몸체의  가운데  줄들은  단위  들여쓰기  공백(앞  예제에서   로  보인다)이 앞에 있기 때문에 나머지 JSP 보다 확연히 드러나게 된다.

 

JSP, HTML, JAVA의 복합 들여쓰기 (Compound Indentation with JSP, HTML and Java)

자바의  스크립트  코드와  템플릿  텍스트(HTML)가  섞여있는  JSP  엘리먼트를  위해서  복합  들여쓰기가 필요한  이유는  JSP  소스를  이해하기  위해  드는  노력을  덜어주기  위해서이다.  기존의  들여쓰기  요령은 JSP 소스파일을 더 알아보기 어렵게 할 수 있기 때문이다.
보편적인  규칙에  따라서,  다른  소스  사이에  끼어  있을  경우마다  보통  들여쓰기에  특별한  단위를  할당한다.  클라이언트에  보여지는  들여쓰기에  영향을 준다. 추가적인  들여쓰기는 (브라우저에  의해) 보통  무시되고,  브라우저에  나타나는  결과는  차이가  없다.  예를  들어,  <TABLE>  태그  앞에  공백이  몇개  더  추가된다고  해서,  브라우저에서  보여지는  테이블의  위치가  달라지는  것은  아니다.  그래서  이러한 작성요령을 사용하는 것이 더 좋아 보인다:

  1. <table>
        <%  if { tableHeaderRequired ) { %>
            <tr>
                <th>Last Name</th>
                <th>First Name</th>
            </tr>
        <%  } %>
        <c:forEach var="customer" items="${customers}">
            <tr>
                <td><c:out value="${customer.lastName}"/></td>
                <td><c:out value="${customer.firstName}"/></td>
            </tr>
        </c:forEach>
    </table>

 

위 방식보다는 아래 방식이 낫다:

 

  1. <table>
        <%  if { tableHeaderRequired ) { %>
        <tr>
            <th>Last Name</th>
            <th>First Name</th>
        </tr>
        <% } %>
        <c:forEach var="customer" items="${customers}">
        <tr>
            <td><c:out value="${customer.lastName}"/></td>
            <td><c:out value="${customer.firstName}"/></td>
        </tr>
        </c:forEach>
    </table>

 

참고 자료#

JavaCodeConventions.pdf   한글

java_CodingStyle.pdf   영문

program_coding_rule.pdf   한글 참조

Code_Conventions.mht   한글

 

원문 : http://ebizdocs.springnote.com/pages/1110314#toc_15

프로그래밍 초보자가 능히 한 사람 몫을 할 정도, 혹은 혼자 코딩하도록 내버려둬도 다른 사람들이 불안에 떨지 않을 만큼 성장하는 가장 빠른 방법은 무엇일까요? 디자인 패턴을 공부하고 최신 기술을 익히고 실전 프로그래밍을 많이 해보는 것?

물론 중요합니다. 그러나 이보다 훨씬 더 중요한 것은 기초를 다지는 것입니다. 슬램덩크에서 강백호는 농구부 입단 후 2주일 간 드리블 연습만 했고 이것이 그가 빠른 시간 안에 한 사람 몫을 해내는 데 밑거름이 됐지요. 복잡한 이론, 어려운 신기술은 잠시 접어두고 프로그래머로서의 기본을 재점검해보겠습니다.

4년 전 학교에서 어느 벤처 경영인의 강연을 들은 적이 있습니다. 미국에서 벤처를 시작해 어느 정도 성공을 거둔 기업가였는데, 그는 강연 내내 기본을 강조했습니다. 미국과 한국의 기업 문화의 차이를 비교하면서 미국의 벤처들은 대체로 경영인으로서의 기본적으로 지켜야 할 것들을 잘 지키는 반면 한국의 벤처는 기본적인 것들을 제대로 지키지 못하고 그로 인해 실패하는 경우가 많다고 하더군요.

그는 모든 것을 기본이란 말 하나로 설명했습니다. 기본이 물론 성공의 충분조건은 아닙니다. 그러나 기본을 지키지 않고는 성공할 수 없습니다. 어떤 분야든 이것은 예외가 없을 것입니다.

그렇다면 프로그래머, 그 중에서도 자바 웹 프로그래머의 기본은 무엇일까요? 당연히 자바 언어에 대해 잘 아는 것입니다. 웹 프로그래밍이라는 것도 결국 사용하는 API가 다른 것 뿐, 좋은 자바 웹 프로그래머가 되려면 먼저 좋은 자바 프로그래머가 되어야 합니다.

너무도 당연한 말 같지만 현실은 그렇지 않습니다. 여러 자바 커뮤니티에 가보면 자바에 대한 기본적인 질문이 수없이 올라오며, 현업 프로그래머 중에도 기초가 부족한 사람이 너무나도 많습니다. 자바 프로그래머라면 자바에 관한 기본서 하나 정도는 마스터하고 시작하도록 합시다.

자바 기본서들은 대체로 내용이 충실하므로 아무거나 사도 나쁜 선택은 아닐 것입니다. 그래도 추천이 필요하다면 『Thinking in Java』를 추천합니다. 프로그래밍에 처음 입문한다면 예제들을 직접 따라해 보는 것도 좋을 것입니다.

자바에 익숙해졌다면 다음 단계는 웹 기술입니다. 웹 프로그래밍의 기본은 웹과 관련된 스펙(specification)에 대한 지식, 구체적으로 서블릿/JSP 스펙, HTTP 스펙(RFC 2068), HTML W3C 스펙 등입니다. 이 스펙들에 대해 상세히 다 알 필요는 없지만 웹 프로그래밍에서 사용하는 API들이 어떤 스펙에 기반을 두고 있는지, 자세히 알고 싶으면 무엇을 찾아야 하는지는 알아야 합니다.

공대생이 공학 수학의 내용을 전부 알고 있을 필요는 없지만 미분방정식을 풀고 싶으면 어느 페이지를 찾아봐야 하는지는 알고 있어야 하는 것처럼 어떤 요구사항이 발생했을 때 그 요구사항을 구현하려면 어떤 스펙을 찾아봐야 하는지 정도는 알고 있어야 하는 것이죠. 그리고 의외로 많은 웹 프로그래머들이 HTML, CSS에 익숙지 않은데, 이 때문에 웹사이트의 브라우저 호환성이 떨어질 뿐만 아니라 지저분한 코드를 양산하게 됩니다.

HTML 코드 역시 유지보수 대상이 되는 코드이며 자바 코드 못지않게 깔끔하게 유지할 수 있어야 함을 기억해야 합니다. 이를 위해서는 HTML과 CSS에 대해 상세히 알아둘 필요가 있습니다. XML은 이제 프로그래머의 기본이니 언급할 필요도 없을 것입니다. XML 파일을 이용하는 것이 편하게 느껴질 정도가 되면 코드의 유연성을 높일 좋은 방법을 많이 생각해낼 수 있을 것입니다.

스펙을 실제로 활용하는 것은 API를 통해서입니다. 서블릿/JSP API는 스펙과는 달리 실제로 API를 통해 무엇을 할 수 있는지를 상세하게 알고 있어야 합니다. 이것은 비단 서블릿/JSP API뿐 아니라 자바 기본 API, 각종 라이브러리의 API도 마찬가지입니다. 필자가 이제껏 자바에 관해 받아본 질문 중 대부분은 API 문서만 잘 들여다 보면 해결되는 것이었습니다.

API 문서를 자주 찾아보는 습관을 들입시다. 리눅서들은 매뉴얼을 읽지 않고 질문하는 사람에게 RTFM(Read The Fucking Manual)이라는 대답을 해줍니다. 자바 역시 RTFM이 필요합니다. J2EE 기본서를 하나 사서 보는 것도 좋을 것입니다. J2EE 기본서에는 웹 관련 스펙 중 중요한 부분들, 서블릿/JSP 스펙 및 API들이 잘 정리되어 있습니다. 『Java Server Programming, J2EE Edition』 정도면 훌륭한 참고서가 될 것입니다.

이제부터 이런 기본적인 지식 중에 중요하지만 간과하기 쉬운 것들, 간단하지만 알면 도움이 되는 정보, 자주 부딪히게 되는 고민 등 몇 가지 작은 문제들을 짚어볼 것입니다. 모두 기본 학습 과정을 잘 거쳤다면 자연스럽게 알 수 있는 내용입니다. 이런 하나하나의 지식을 통해 자신에게 부족한 점을 되짚어볼 수 있는 계기를 마련할 수 있기를 바랍니다.

web.xml
배치 서술자(deployment descriptor)라고 부르는 web.xml은 웹 프로젝트를 구성하는 데 있어 필수적이면서 웹 애플리케이션의 동작을 여러 가지로 조정하는 기능을 합니다. 스트러츠를 사용하는 경우도 스트러츠를 사용하기 위한 설정은 web.xml에 하게 되는데 그 설정들이 무슨 의미를 가지고 있는지 정도는 상식으로 알아두는 것이 좋을 것입니다. <리스트 1>의 실제 스트러츠 설정 예제를 봅시다.

 <리스트 1> web.xml 서블릿 맵핑

PHP, ASP 등의 다른 서버 사이드 스크립트나 JSP 페이지는 페이지를 호출하는 경로에 실제 스크립트 파일이 존재해야 하지만 서블릿은 이와 달리 web.xml의 설정을 이용해 URL을 특정 서블릿으로 맵핑할 수 있습니다. <리스트 1>의 설정은 호출된 URL을 스트러츠의 Action으로 맵핑하기 위한 설정입니다.

servlet 설정에서 action이라는 이름의 서블릿을 org.apache.struts.action.ActionServlet 클래스로 등록하고 다음 servlet-mapping 설정에서 *.do라는 URL로 호출된 페이지들을 action이라는 이름의 서블릿으로 맵핑합니다. url-pattern 값을 *.nhn으로 바꾼다면 *.nhn으로 호출된 요청들이 ActionServlet으로 맵핑될 것입니다. 스트러츠는 이 ActionServlet에서 요청을 각 Action으로 분기시켜 줍니다. init-param은 서블릿을 초기화할 때 사용할 파라미터 값이며 getInitParameter 메쏘드를 통해 읽어올 수 있습니다. load-on-startup은 서블릿 엔진이 시작될 때 로드될 우선순위를 지정하는 값입니다.

인덱스 페이지를 지정하는 것도 web.xml에서 할 수 있습니다. 많은 웹 사이트들이 구체적인 경로 지정 없이 도메인명까지만 써줘도 페이지를 표시합니다. 이를테면 http://www.hangame.com으로 호출할 경우 다음과 같이 설정해두면 www.hangame.com의 /index.jsp를 호출하게 만들 수 있습니다.

 <리스트 2> web.xml 인덱스 페이지 설정

태그명에서 짐작할 수 있듯이 인덱스 페이지는 여러 개를 둬서 순서대로 검색하게 할 수 있습니다. 예를 들어 index.html과 index.jsp가 순서대로 지정된다면 서블릿 엔진은 index.html이 있으면 index.html을 보여주고 없으면 index.jsp를 호출합니다. 이것도 없으면 404 에러가 나거나 디렉토리 목록이 보이게 됩니다. 이 인덱스 페이지는 모든 경로에 대해 동작합니다.

이와 같은 설정의 경우 http://www.hangame.com/login/을 호출한다면 http://www.hangame.com/login/index.jsp를 찾게 되는 것입니다. 이 설정은 사실 아파치 등의 웹서버에서도 해줄 수 있으나 보통 웹 서버에서는 인덱스 페이지가 실제 파일로 존재해야 보여줄 수 있는데, 서블릿 엔진에서는 실제 파일로 존재하지 않고 서블릿 맵핑으로 지정만 되어 있어도 보여줄 수 있다는 장점이 있습니다.

접근 권한도 설정할 수 있습니다. 권한 체계가 간단한 웹 애플리케이션이라면 web.xml만으로도 충분한 권한 설정을 해줄 수 있습니다.

 <리스트 3> web.xml 접근 제한 설정

<리스트 3>의 예는 서블릿 스펙 문서에 있는 예입니다. 이것의 의미는 GET이나 POST로 /retail/*과 같은 요청은 CONTRACTOR와 HOMEOWNER라는 role을 가진 사용자에게만 허락하겠다는 뜻입니다. 이외의 사용자는 권한이 없다는 401 에러 페이지를 보게 됩니다. 이런 접근 제한 뿐 아니라 로그인 처리도 login-config 설정을 이용하면 가능합니다. 실제 톰캣의 admin과 manager 애플리케이션은 이 설정을 이용해서 인증과 권한 처리를 합니다.

자세한 스펙은 서블릿 스펙 문서에 정의되어 있으나 실제 활용하기엔 다소 부족한 감이 있고, 톰캣의 실제 활용 예를 보는 것이 도움이 될 것입니다. 이외에도 서블릿 필터 설정, 세션 설정, 리소스 설정 등 여러 가지 유용한 설정을 해줄 수 있고 공통적인 에외 처리를 위한 에러 페이지 설정도 가능합니다. 에러 페이지 설정 부분은 이후 예외 처리에서 자세히 다룰 것입니다.

예외 처리
자바의 강점 중 하나가 편리한 예외 처리 방식입니다. C 등 예외 처리 문법이 없는 언어를 먼저 접한 프로그래머에게는 생소한 개념일 수 있겠지만 알면 알수록 편리한 것이 자바의 예외 처리입니다. 하지만 의외로 많은 자바 프로그래머들이 예외 처리를 어려워하고 예외 처리를 제대로 하지 않아 여러 가지 문제를 발생시킵니다.

기본이라고 할 수도 있는 부분이긴 하나 사실 이것은 자바의 예외 처리 문법만 배운다고 되는 문제는 아니며 예외 처리에 대한 많은 고민이 필요합니다. 특히 웹 애플리케이션의 예외 처리는 프로그래머를 위한 부분과 웹 사이트 방문객을 위한 부분 두 가지를 모두 고려해야 합니다.

먼저 프로그래머의 입장을 살펴봅시다. 예외가 발생하면 어디까지는 그냥 던지고 어디서 캐치하는 것이 좋을까요? 자바의 예외는 자바 코드의 모든 영역에서 발생할 수 있습니다. 이 모든 영역에 다 try-catch를 걸고 예외를 잡을 수는 없는 일입니다. 대부분의 예외는 일단 그냥 던지는 것이 좋습니다. 자바의 예외가 좋은 것은 꼭 예외가 발생한 그 지점에서 처리를 하지 않아도 된다는 것 때문입니다. 예외를 던짐으로써 예외를 처리하기에 적절한 위치에서 처리하게 만들 수 있습니다. 어떻게 처리해야 할지 잘 모르겠다면 그냥 그대로 던지도록 하는 것이 좋습니다.

예외를 잡아서 처리해야 하는 곳은 일반적으로 사용자에게 화면을 보여주기 직전이며 이것은 웹 애플리케이션이 MVC(Model-View-Controller) 패턴으로 작성되어 있다면 컨트롤러에서 이 일을 하게 됩니다. 컨트롤러에서 예외를 보고 판단해 사용자에게 보여줄 화면을 결정하는 것입니다.

쇼핑몰에서 마일리지 적립액으로 상품을 구매하는 과정을 예로 들어보겠습니다. 만약 고객이 자신의 마일리지보다 더 많은 금액의 상품을 구매하려 한다면 구매를 수행하는 모델 객체에서 예외가 발생할 것입니다. 그러면 이 모델 클래스에서 예외를 바로 잡지 말고 던져서 구매 프로세스의 컨트롤러 객체에서 이를 잡아서 예외 페이지로 포워드시켜 예외 메시지를 보여주는 식으로 코딩하면 됩니다.

웹사이트 방문객을 위해 중요한 것은 자바 예외가 발생했을 때 이해할 수 없는 시스템 에러 메시지나 스택 정보 등의 황당한 화면이 아닌 친절한 에러 메시지를 표시해 주는 것입니다. 이를 위해서는 컨트롤러에서도 처리하지 못하고 던져진, 정말 예상 밖의 예외를 모두 끌어 모아 처리하는 부분이 필요합니다.

서블릿/JSP에서는 이런 부분의 처리를 위한 기능을 여러 가지로 제공하고 있고 스트러츠 등의 프레임워크에서도 다양한 방법을 제공합니다. JSP의 에러 페이지 설정이 그 한 예입니다. 그러나 JSP의 에러 페이지 설정 방식은 모든 JSP 페이지에 설정해야 작동한다는 단점이 있습니다. 만약 에러 페이지 지정을 빠뜨린 페이지에서 예외가 발생한다면 서블릿 엔진의 에러 메시지가 그대로 웹사이트 방문객에게 전달되고 맙니다.

이런 부분을 쉽게 처리하기 위한 방법이 있습니다. 이것은 앞에서 설명했던 web.xml의 에러 페이지 설정을 이용하는 것입니다. 우선 다음의 예를 볼까요.

 <리스트 4> web.xml 에러 페이지 설정

이렇게 설정해 두면 웹 애플리케이션 전반에서 발생하는 예외 중 java.lang.Exception을 상속한 예외는 모두 잡혀서 /common/error.jsp 페이지에서 처리하게 됩니다. 예외가 발생하면 request 객체에 예외 상황에 대한 정보가 attribute로 저장된 후 /common/error.jsp로 포워드되어 이 곳에서 request에 담긴 정보를 바탕으로 예외 처리를 해줄 수 있습니다. 이 곳에서는 일반적인 에러 메시지를 사용자에게 보여주면 됩니다. 자바 예외 뿐 아니라 HTTP 에러 코드도 잡아낼 수 있습니다.

이를테면 없는 페이지를 호출해서 404 에러가 나는 경우 이를 잡아서 페이지가 없다는 에러 메시지를 좀더 친절한 메시지로 보여줄 수 있습니다. 덧붙여, 이 에러 처리 페이지는 가급적 순수한 서블릿으로 만드는 것이 좋습니다. 스트러츠의 Action으로 에러 페이지를 구성해본 적이 있었는데 설정 상의 문제로 스트러츠의 ActionServlet 로딩이 실패할 경우 예외를 제대로 표시하지 못합니다. JSP로 만드는 것도 나쁘진 않으나 복잡한 로직이 들어갈수록 서블릿이 코딩하기 더 편할 수 있습니다. 만약 이 에러 페이지 자체에서 또다시 예외가 발생하면 찾기 힘든 경우가 많기 때문에 주의를 많이 기울여야 합니다.

로깅
에러 페이지에서 해야 할 또 하나 중요한 일은 예외 상황에 대한 로그를 남기는 것입니다. 에러 페이지까지 왔다는 것은 이미 개발자의 예상을 벗어난 동작을 하고 있다는 것이므로 이 사실은 개발자에게 빨리 전달되어야 합니다. 이 때문에 로그를 제대로 남겨서 조회하기 편한 시스템을 구축해야 합니다. 로깅 API는 여러 가지가 있고 JDK 자체에도 포함되어 있지만 log4j가 가장 널리 사용되고 성능, 기능, 안정성 등 여러 가지 면에서 다른 것들보다 낫습니다.

여러가지 로깅 API를 바꿔가면서 사용할 수 있게 해주는 자카르타의 commons-logging 프로젝트도 쓸만합니다. 로거 객체는 일반적으로 클래스 당 하나를 클래스의 전체 이름으로 생성해 사용합니다. <리스트 5>는 commons-logging을 사용하는 예입니다.

 <리스트 5> 로그 사용법

이러면 로그 객체는 Avatar 클래스의 전체 이름, com.hangame.avatar.Avatar로 생깁니다. 만약 여기에 log4j를 붙여서 사용한다면 <리스트 6>과 같은 log4j 설정을 사용할 수 있습니다.

 <리스트 6> log4j.xml 예제

이 설정은 com.hangame와 org.apache라는 이름의 로거를 두 개 생성하고 있습니다. 로거의 특성은 이름으로 상속됩니다. com.hangame.avatar.Avatar라는 이름의 로거는 com.hangame의 속성을 모두 상속 받게 됩니다. 그러면 com.hangame이 normal과 memory라는 두 개의 appender를 갖고 있기 때문에 com.hangame.avatar.Avatar 로거가 찍은 로그는 표준 출력으로도 나가고 메모리에도 남게 됩니다. log4j의 이런 특성을 이용하면 다양한 방식으로 로그를 남길 수 있고 로그를 선택적으로 켜고 끌 수 있습니다. 이런 기능들을 잘 활용하면 로그를 조회하기 쉽게 구성할 수 있습니다.

앞에서 예를 든 것처럼 메모리에 최근 로그를 남겨두고 이를 조회할 수 있는 페이지를 만든다거나 데이터베이스에 로그를 쌓을 수도 있습니다. 그리고 주기적으로 이런 로그 조회 페이지를 모니터하면서 로그 리포트를 개발자에게 메일 등으로 자동 발송하는 시스템도 구상해 볼 수 있을 것입니다.

예외 추적
예외 처리 시스템을 구축하고 예외 로그를 남겼으면 다음은 이 정보를 바탕으로 문제점을 찾아들어가는 것입니다. 예외 추적의 출발점은 당연히 예외 스택 정보입니다. 대부분의 문제는 예외 스택 정보만 가지고도 찾아낼 수 있습니다.

하지만 의외로 많은 프로그래머들이 예외가 발생했을 때 스택 정보를 보지 않고 자신의 경험에 의지해 문제점을 예측하려 하곤 합니다. 이런 실제 상황에 기반을 두지 않은 예측은 운 좋게 문제를 바로 짚어내는 경우도 있겠지만 대개의 경우 시간만 낭비하게 됩니다. 예외가 발생하면 반드시 스택 정보에 찍힌 소스의 라인부터 살펴보는 습관을 기르는 것이 좋습니다.

스택 정보는 가끔 수백 라인에 이를 정도로 길어지는 경우도 간혹 있습니다. 이 모든 정보를 다 찾아볼 필요는 없습니다. 스택 정보는 메쏘드가 호출된 역순으로 찍히므로 위에 있는 정보가 예외가 발생한 위치와 가까운 정보입니다.

그렇다고 늘 제일 위의 정보를 봐야 하는 것은 아닙니다. 웹 애플리케이션의 경우 스택 정보는 자신이 작성한 클래스뿐 아니라 서블릿 엔진을 포함한 여러 가지 클래스의 정보가 같이 담겨 있습니다. 이런 정보는 보통 볼 필요가 없고 스택 정보에서 자신이 작성한 클래스 중 제일 위에 있는 것, 이것이 예외가 발생한 지점이며 이 곳을 찾아보면 대부분의 문제점은 정확하게 추적할 수 있습니다.

또 한 가지 자바 초보자를 괴롭히는 문제는 NullPointerException입니다. 사실 이것은 초보자에게는 아주 까다로운 문제지만 조금만 알면 가장 찾기 쉬운 문제 중 하나가 NullPointerException입니다. NullPointerException은 객체의 멤버 변수나 메쏘드를 이용하려고 할 때 그 객체가 null인 경우에 발생합니다. 따라서 NullPointerException이 발생하면 앞의 방법대로 예외가 발생한 라인을 찾아들어간 다음 그 라인에서 멤버 지정 연산자(.) 앞에 있는 객체를 보면 됩니다. 이 사실만 알고 있어도 NullPointerException이 발생했을 때 어떤 객체가 null인지를 쉽게 찾아낼 수 있을 것입니다.

간혹 NullPointerException이 싫어서 다음과 같은 코드를 작성하는 경우가 있습니다.

if ("Y".equals(param)) doSomthing();
else doOther();

이런 코드는 조심해서 써야 합니다. param의 null 체크가 귀찮아서 이런 식의 코드를 쓰곤 하는데 만약 param의 값이 Y인 경우는 doSomething()을 실행하고, N이나 null이면 doOther()를 실행해야 하는 경우라면 이 코드는 문제가 없습니다. 그러나 만약 param은 null이면 안 되는 상황이라면 어떻게 될까요? 다른 부분의 버그로 param에 null이 들어와도 프로그래머는 이것을 알아차리지 못하고 넘어가게 됩니다. 즉, 버그를 은폐하는 코드가 됩니다.

당장의 문제를 발생하지 않더라도 이런 코드는 나중에 찾기 힘든 문제를 유발할 수 있습니다. 이런 경우는 그냥 NullPointerException이 발생하도록 내버려 두면 param에 null 값이 들어왔을 때 다른 부분에 버그가 있기 때문이라는 사실을 감지할 수 있습니다. 상황에 따라 위와 같은 코드를 써도 되는지를 신중히 검토한 후 사용해야 합니다. 예외 발생이 두려워서 버그를 은폐할 수 있는 코드를 만들지 맙시다.

한글 문제
웹 프로그래머들을 괴롭게 하는 문제를 꼽을 때 빠지지 않는 것이 한글 문제입니다. 한글 문제가 지금처럼 골치 아프게 된 데는 역사적으로 복잡한 원인들이 얽혀 있는데 이런 문제는 접어두고 자바 웹 프로그래머로서 한글 문제를 해결하기 위해 알아야 하는 것들을 살펴보겠습니다.

자바는 문자열과 바이트 스트림을 다르게 취급합니다. 자바의 스트링은 유니코드의 문자셋을 사용하며 문자열을 파일에 쓰거나 네트워크로 전송하는 등 실제 입출력이 일어날 때는 문자열을 바이트 스트림으로 변환하게 됩니다. 이 때 바이트 스트림으로 변환하는 규칙이 인코딩입니다. 따라서 바이트 스트림으로 전달된 것을 문자열로 바꾸거나 문자열을 바이트 스트림으로 전달할 때는 반드시 인코딩을 지정해야 합니다.

이런 인코딩 중 한글을 표현할 수 있는 인코딩은 자바에서 사용하는 이름을 기준으로 하면 EUC-KR, MS949, UTF-8, UTF-16 정도가 있습니다. EUC-KR은 KSC5601-1987 기반 인코딩으로 한글의 모든 문자를 다 표현할 수 없습니다. MS949는 EUC-KR을 확장해 모든 한글을 표현할 수 있지만 비표준이고 코드 자체에 기술적인 결함이 많습니다. UTF-8과 UTF-16은 유니코드의 인코딩들이며 모든 한글을 표현할 수 있고 표준이며 한글 이외의 다른 언어와 함께 표현이 가능합니다.

보통 많이 쓰이는 EUC-KR은 RFC 표준 인코딩이긴 하나 한글의 확장 문자들을 제대로 표시하지 못합니다. 그래서 자바 웹 프로그래밍에서는 MS949를 많이 쓰게 됩니다. 자바에서 스트링 객체를 생성할 때는 이 중에 하나로 인코딩을 줘서 생성해야 한글을 표현할 수 있게 인코딩됩니다.

웹서버로 전달되는 요청은 클라이언트의 웹브라우저가 문자열을 바이트 스트림으로 인코딩하는데 이 때 사용하는 인코딩은 일반적으로 한글 윈도우의 기본 인코딩인 MS949입니다. 그런데 서블릿 엔진에서 요청을 처리하는 데 사용하는 기본 인코딩이 ISO-8859-1이기 때문에 아무 것도 지정하지 않으면 MS949로 인코딩된 바이트들을 ISO-8859-1 인코딩의 스트링 객체로 만들기 때문에 한글이 깨져보이게 됩니다. 따라서 기본 인코딩을 MS949로 지정하면 인코딩이 보존된 상태로 한글이 깨지지 않게 됩니다. HttpServletRequest.setCharacterEncoding() 메쏘드에서 이것을 지정할 수 있습니다.

그러나 이것에도 약간 문제가 있습니다. 서블릿 스펙상 이 메쏘드는 POST 요청에만 적용됩니다. 즉, POST 요청의 파라미터는 setCharacterEncdoing에서 지정한 인코딩으로 스트링 객체가 생성되기 때문에 한글을 보존할 수 있으나 GET 요청은 setCharacterEncoding의 적용을 받지 않기 때문에 GET으로 받은 파라미터는 인코딩 변환을 다시 해주어야 합니다. 다만, 이것은 서블릿 엔진에 따라 다릅니다. 톰캣의 경우도 4.1 버전과 5.0 버전이 다르게 동작하니 주의가 필요합니다.

웹서버에서 다시 클라이언트로 응답할 때는 반대의 과정입니다. 자바의 스트링 객체가 바이트 스트림으로 변환되며 이 때 역시 인코딩을 지정해야 합니다. 이 인코딩은 JSP 페이지에서 페이지 지시자의 pageEncoding 속성을 통해 지정할 수 있고, 서블릿 2.4 스펙에서는 HttpServletResponse.setCharacterEncoding을 사용할 수 있습니다. HTTP 요청을 읽는 과정과 역순이라고 생각하면 됩니다.

그리고 웹 서버에서 요청을 읽을 때 MS949를 지정해 주듯이 클라이언트의 웹 브라우저도 웹 서버에서 생성한 응답을 정확하게 읽으려면 어떤 인코딩을 사용해야 하는지 알아야 합니다. 이것을 지정해주는 것이 HTML의 Content-Type입니다. 다음과 같이 지정할 수 있습니다.

<meta http-equiv="Content-Type" content="text/html;charset=euc-kr" />

여기서 지정하는 charset은 원칙적으로는 당연히 웹 서버에서 응답 객체를 생성할 때 지정한 인코딩 값과 같아야 한글로 제대로 읽을 수 있습니다. 그러나 여기 지정하는 charset이 RFC 표준 문자셋이 아닐 경우 브라우저에 따라 인식하지 못할 수도 있습니다. 따라서 MS949로 인코딩했다면 MS949를 지정해야 정상이지만 MS949가 RFC 표준이 아니기 때문에 문제가 생길 수 있습니다.

그렇다고 응답의 인코딩을 EUC-KR로 지정하게 되면 확장 한글을 표시할 수 없기 때문에 문제가 됩니다. 그래서 페이지 인코딩은 MS949로 하지만 Content-Type에는 euc-kr을 지정해 주게 되는 것입니다. 물론 이렇게 되면 경우에 따라 확장 한글이 깨질 수 있지만 다행스럽게도 대부분의 브라우저에서 이렇게 지정하면 잘 동작합니다.

사실 이 부분은 응답 스트림에 적용되는 인코딩과 HTML Content-Type에 지정하는 인코딩이 같기만 하면 되기 때문에 굳이 MS949를 사용할 필요는 없고 UTF-8 등의 인코딩을 사용해도 무방합니다. 따라서 응답 스트림의 인코딩도 UTF-8로 하고 Content-Type도 UTF-8로 지정하는 것이 가장 확실한 방법일 수 있습니다. 또한, HTML의 Content-Type에 UTF-8이 지정되어 있으면 이 페이지에서 폼을 전송할 경우에도 UTF-8로 인코딩되어 요청을 파싱하는 쪽에서도 UTF-8을 사용할 수 있습니다.

유니코드의 인코딩들인 UTF-8, UTF-16은 한 인코딩으로 다국어를 처리할 수 있기 때문에 다국어 지원이 필요한 웹 애플리케이션은 실제로 UTF-8로 작성된 것이 많습니다. 다국어 지원이 필요 없다고 해도 UTF-8을 사용하는 것이 오히려 한글 문제를 더 쉽게 해결하는 방법이 될 수 있습니다.

웹 뿐 아니라 데이터베이스나 파일에 입출력을 할 때도 마찬가지의 원리가 적용됩니다. 사용하는 인코딩이 다르면 변환 과정을 거쳐야 합니다. 이것은 리눅스나 유닉스에서 문제가 될 수 있습니다. 리눅스는 MS949를 지원하지 않고 EUC-KR만 지원하기 때문입니다. 따라서 윈도우에서 개발하고 리눅스에서 돌리는 경우 문제가 되는 경우가 간혹 있습니다.

MS949가 또 하나 문제가 되는 영역은 XML 파서입니다. 현재 가장 널리 사용되는 XML 파서는 Xerces인데 이 파서는 RFC 표준 문자셋 외에는 지원하지 않기 때문에 MS949 인코딩은 파싱 에러가 납니다. 그런 반면 JDK 1.4에 포함된 파서인 Crimson은 네임스페이스 파싱에 버그가 있습니다. MS949를 XML 인코딩으로 쓸 경우 XML 파서 선택이 문제가 될 수 있는 것입니다.

다행스럽게도 JDK 5.0에 포함된 파서는 Xerces를 썬에서 패치한 것인데 이것은 아무 문제가 없습니다. 하지만 여전히 많은 오픈소스 라이브러리들이 Xerces를 사용하고 있기 때문에 문제가 되는 경우는 계속 나타날 수 있을 것입니다. 이것 때문에라도 UTF-8을 사용할 필요가 있습니다.

자바에서의 한글 문제는 문자열과 바이트 스트림의 변환에 인코딩이 주어져야 한다는 사실만 생각하면 다 쉽게 해결할 수 있습니다. 역시 기본이 잘 갖춰져 있으면 한글 문제도 쉽게 해결할 수 있는 것입니다.

URL 인코드
URL 인코딩이 필요한 것은 URL에 사용 가능한 문자가 제한되어 있기 때문입니다. URL 스펙(RFC 1738)에 정의된 바로는 URL에 사용할 수 있는 문자는 알파벳, 숫자와 몇 가지의 특수문자뿐입니다. 따라서 다양한 문자를 URL로 전달하려면 URL에서 허용하는 문자로 변환해 전달해야 합니다. 이것은 GET 요청의 파라미터로 값을 전달하려할 때 문제가 됩니다.

예를 들어 http://website.com/process.jsp에 로그인되지 않은 상태에서 접근하면 자동으로 로그인 페이지인 http://website.com/login.jsp로 리다이렉트된 후 로그인을 하면 원래 요청했던 페이지로 다시 리다이렉트되도록 해야 한다고 합시다.

그러면 /process.jsp에서는 로그인 페이지로 리다이렉트시키면서 파라미터로 현재 요청한 URL, 즉 /process.jsp를 넘겨주고 login.jsp에서는 로그인 처리가 끝난 후 이 URL로 다시 리다이렉트시키면 됩니다. 여기서 /process.jsp에서는 http://website.com/login.jsp?redirect=http://website.com/process.jsp와 같은 형식으로 리다이렉트를 해주면 될 것입니다.

여기서 문제는 redirect 파라미터의 값이 URL이기 때문에 URL 안에 URL이 들어간 형태가 되어 제대로 파싱되지 않는다는 것입니다. 그래서 파라미터로 넘겨야 하는 URL 부분을 URLEncoder로 인코딩해 http://website.com/login.jsp?redirect=http%3A%2F%2Fwebsite.com%2Fprocess.jsp와 같은 형태로 넘겨야 합니다. 이 값을 받는 부분에서는 다시 디코딩을 해줄 필요가 없습니다.

URL은 자동으로 웹 서버에서 파싱할 때 디코딩을 해주기 때문입니다. URL을 통해 GET 요청의 파라미터로 보내야 하는 값은 반드시 URL 인코딩을 거쳐야 한다는 사실만 기억하세요. 참고로 자바스크립트에서도 escape, unescape 함수를 통해 URL 인코딩, 디코딩과 유사한 작업을 수행할 수 있습니다.

클래스패스의 리소스 사용법
웹 애플리케이션은 보통 애플리케이션의 설정을 담고 있는 파일이 필요합니다. web.xml, struts-config.xml 등의 설정 파일들은 보통 웹 애플리케이션의 /WEB-INF/에 위치하는데 그 외에 애플리케이션에서 사용하는 파일들은 어디에 놓고 사용하는 것이 편리할까요? 가장 관리하기 쉽고 부가적인 작업이 적은 방법은 클래스 패스에 두는 것입니다. /WEB-INF/classes에 두면 자바의 클래스 로더를 이용해 이런 파일들에 접근할 수 있습니다. log4j 등 많은 라이브러리들이 자신의 설정 파일을 클래스 패스에서 가장 먼저 찾게 됩니다. <리스트 7>의 예제를 볼까요.

 <리스트 7> 클래스패스의 파일 읽기

이 코드는 클래스 패스에서 config.xml을 읽습니다. 웹 애플리케이션의 기본 클래스 패스는 /WEB-INF/classes이므로 기본적으로 여기서 찾게 됩니다. 이것으로 jar 파일 안의 내용도 읽을 수 있습니다. 이 경우는 ClassLoader.getResourceAsStream을 통해 스트림으로 파일 내용을 읽을 수 있습니다.

대부분의 IDE나 maven 등의 빌드 툴에서는 소스 경로에 있는 파일 중 자바 소스가 아닌 파일들을 자동으로 클래스 패스로 복사해주므로 이용하기도 편리합니다. 자카르타의 commons-discovery 프로젝트는 이런 기능들을 모아서 편리하게 이용할 수 있게 제공하고 있습니다.

서블릿/액션 멤버 변수 공유 문제
JSP가 보급되기 시작하던 초기에 많이 발생하던 문제로 웹 사이트 이용자가 접속했을 때 자신의 정보가 아닌 다른 사람의 정보가 나타나면서 엉키는 경우가 있었습니다. 이것의 원인은 서블릿에 대한 이해가 부족해서 발생한 것이었습니다. <리스트 8>을 봅시다.

 <리스트 8> 멤버 변수가 공유되는 서블릿


얼핏 별 문제가 없어 보이지만 이 코드는 심각한 문제가 있습니다. 서블릿은 보통 서블릿 엔진에서 하나만 생성되고 한 번 생성된 서블릿 객체가 계속 재활용됩니다. 때문에 A와 B라는 두 사용자가 동시에 이 서블릿을 호출하게 되면 A의 호출을 수행하는 중에 B의 호출이 userInfo의 값을 바꿔버릴 수 있습니다. 그러면 A는 B의 정보를 보거나 그 반대의 경우가 생길 수 있는 것입니다.

혼자서 테스트할 때는 한 번에 한 쓰레드만 service 메쏘드를 호출하기 때문에 이런 문제가 잘 드러나지 않기 때문에 별 문제 없는 줄 알고 있다가 서비스를 오픈하고 나면 문제가 되는 경우가 있으므로 조심해야 합니다. JSP에서 <%! %>를 통해 선언하는 내용도 마찬가지 문제가 발생하므로 주의해야 합니다. 이런 내용 역시 자바 클래스와 멤버 변수의 기본 개념을 이해하고 서블릿 스펙만 한 번 읽어본다면 금방 알 수 있는 내용입니다.

생각하기
이 내용을 읽으면서 모르는 내용이 하나도 없었다면 자바 웹 프로그래머로서 어느 정도 기본은 되어 있다고 할 수 있습니다. 이런 내용은 그 하나하나에 대한 지식을 쌓는 것도 중요하지만 더 중요한 것은 이런 내용을 알아야 한다는 사실을 아는 것입니다.

무엇을 알아야 하는가를 가르쳐주는 것은 스펙입니다. 스펙 문서들은 대부분 영어이고 그다지 친절하게 되어 있진 않지만 해당 분야에 대해 가장 정확한 정보를 담고 있습니다. 자세한 내용을 다 알진 못하더라도 스펙에 어떤 내용이 있는지 알아야 그 내용 중 자신에게 필요한 내용을 찾아서 공부할 수 있는 것입니다. 이런 정보를 어디서 찾을 수 있는가를 알고 있는 것도 중요합니다. 기본적으로 www.ietf.org, jcp.org,java.sun.com, www.w3.org 정도의 사이트에는 익숙해지는 게 좋을 것입니다.

많은 프로그래머들이 실제로 자기 손으로 프로그래밍해보는 게 실력이 느는 제일 좋은 방법이라고 말하지만 필자는 여기에 동의하지 않습니다. 물론, 실제 경험을 쌓는 것이 필수적인 과정이긴 합니다. 그러나 기본 지식을 등한시한 상태에서 코딩만 해보는 것으로는 실력이 잘 늘지 않습니다.

코딩 기술은 늘 수 있겠지만 정말 실제 서비스를 해야 하는 프로그래밍에서 중대한 실수를 저지르게 되거나 남들이 쉽게 하는 일들을 어렵게 빙 둘러가면서 하게 될 수 있습니다. 그래서 기본기를 갖추는 것이 중요한 것입니다.

거듭해서 기본의 중요성을 강조했는데 한 가지 덧붙이고 싶은 말은 이런 기본 지식 뿐 아니라 기본을 활용하는 능력을 키우는 것도 잊지 말아야 한다는 것입니다. 앞서 언급한 예외 처리 같은 내용은 기본이긴 하나 자바 문법만 잘 안다고 알 수 있는 내용은 아니며 기본을 바탕으로 좋은 판단을 내릴 수 있는 능력이 있어야 합니다.

결국 좋은 프로그래머가 되려면 먼저 좋은 사고 능력을 가지고 있어야 하는 것입니다. 글짓기를 잘하는 방법으로 흔히 다독(多讀), 다작(多作), 다상량(多商量)을 이야기합니다. 많이 읽고 많이 쓰고 많이 생각하라는 것입니다. 프로그래밍도 이와 비슷합니다. 각종 스펙과 좋은 코드를 많이 읽어보고 직접 코딩도 많이 해보면 분명 실력이 늘지만 이것으로는 충분치 않습니다.

프로그래밍을 하면서 끊임없이 생각해야 합니다. 지금 작성한 코드는 좋은 코드인가, 이렇게 코딩하면 불편한데 개선할 방법은 없을까, 이 API들로 무엇을 할 수 있을까, 개발 속도를 향상시키려면 어떻게 해야 할까 등 생각을 많이 해야 진짜 발전을 이룰 수 있습니다.

만일 손가락이 아플 정도로 하루 종일 키보드를 두드리고 있다면 좋은 프로그래머라고 할 수 없습니다. 생각하는 데 좀 더 많은 시간을 써야 합니다. 모니터를 구부정하게 들여다보면서 키보드를 두드리는 것은 것보다는 의자에 편안히 기대서 생각하는 시간을 늘려보세요. 복잡한 문제가 있으면 바깥 공기를 쐬면서 산책을 하면서 생각을 하는 것도 좋습니다. 굳이 건강을 생각하지 않더라도 걷는 것은 두뇌를 활성화하기 때문에 해결책을 더 빨리 찾을 수 있게 합니다.

남들이 보기에는 게을러 보일 수 있지만 놀고 있는 게 아니라는 것은 결과로 충분히 보여줄 수 있습니다. 물론 이런 생각을 잘 이어나가기 위해서는 생각의 재료가 되는 기본에 충실해야 함은 물론입니다. 어둠침침한 구석에 앉아 키보드만 두드리는 긱(geek)이 아닌 보다 인간다운 프로그래머가 됩시다.

 

출처 : http://www.zdnet.co.kr/ArticleView.asp?artice_id=00000039134853

지난 글에서는 자바 웹 프로그래머로서 한 사람 몫을 해내기 위한 기초에 대해 다루었습니다. 다음 단계는 한 사람 몫을 넘어서 팀 전체에 영향을 미치는 일을 할 수 있는 능력을 기르는 것이겠죠. 팀에서 어떠한 역할을 맡아도 잘해낼 수 있으려면 프로그래밍도 잘해야 하지만 개발 환경을 구성하는 방법도 잘 알고 있어야 합니다.

이 글에서는 개발 환경이 잘 갖춰진 팀의 프로그래머는 이미 구축된 환경에 대해 좀 더 잘 이해할 수 있도록, 그렇지 못한 경우는 스스로 개발 환경을 구축해나갈 수 있도록 여러 가지 개발에 필요한 것들을 살펴보겠습니다.

레이싱 경기장에는 피트(pit)라고 부르는 장소가 있습니다. 경주차의 수리나 조정, 타이어 교환, 연료 보급 등을 하는 곳이죠. 이런 일을 담당하는 사람을 미캐닉(mechanic)이라고 부릅니다. 레이싱 경기가 얼핏 보면 차의 성능과 드라이버의 기량만이 승부를 결정짓는 것 같지만, 장거리 레이스가 될수록 미캐닉의 역할이 승부를 가르는 경우도 적지 않습니다. 미캐닉이 잘한다고 레이싱에서 승리하는 것은 아니지만 미캐닉이 못하면 십중팔구 패배하게 됩니다.

웹 프로젝트도 이런 면에서 레이싱과 비슷한 점이 있습니다. 웹 프로젝트의 성공에 있어 가장 중요한 것은 요구사항의 구현이고, 기술적인 성공을 좌우하는 것은 프로그래밍이지만 프로그래밍 외의 기술적인 잡무들은 웹 프로젝트의 실패를 결정지을 수 있는 조건입니다. 소스 관리를 비롯해서 웹의 특성상 자주 발생하는 서버 설치, 프로젝트의 배치(deploy), 서버 리스타트 등이 그런 일들이죠. 이런 일들은 대체로 단순 반복 작업이면서도 실수할 가능성이 있고 또 실수를 하면 그 파급 효과가 클 수 있습니다.

그래서 이런 개발 주변 환경을 제대로 갖춰놓지 않으면 이것이 프로젝트를 실패로 이끌기도 합니다. 고로, 레이싱에서 승리하기 위해서는 좋은 미캐닉과 좋은 장비들을 확보해서 피트에서의 작업을 효율적으로 해내야 하는 것처럼 웹 프로젝트를 성공으로 이끌기 위해서는 개발 환경을 잘 갖춰서 프로그래머들의 잡무 부담을 줄이는 것이 중요합니다.

경우에 따라서 미캐닉처럼 이런 잡무를 전담하는 사람을 따로 두는 경우도 있습니다. 혹은 일부러 전담자를 두지 않더라도 한두 사람만이 수행 방법을 숙지하게 되어 결국 이 사람들에게 잡무 요청이 몰려 실질적인 전담자가 되버리는 경향이 있습니다. XP(eXtreme Programming)의 지속적인 통합(Continuous Integration)도 이런 문제를 이야기합니다. 통합 작업을 자동화해서 누구나 쉽게 할 수 있게 만들지 않으면 한두 사람만이 통합 작업을 할 수 있게 되고 통합을 자주 수행할 수도 없게 됩니다.

그러면 각자 개발한 모듈들을 통합할 때 많은 문제가 발생할 수 있고, 그런 경우 통합이 잘못된 것인지 아니면 각자 개발한 소스가 잘못된 것인지 찾기 힘들게 되죠. 이런 상황을 ‘Integration Hell’이라고 부르는데, 이런 상황을 피하기 위해서는 지속적인 통합을 통해 문제를 조기에 발견하여 문제가 쌓이지 않게 해야 합니다. 이 글에서는 지속적인 통합을 확장해 프로그래머들을 잡무에서 해방시켜 좀 더 창조적인 업무에 전념할 수 있도록 개발 환경을 구축하는 방법들을 이야기할 것입니다.

‘무엇을 해야 하는가’에 대한 고민
좋은 소프트웨어를 만드는 첫걸음은 어떻게 만드는가를 아는 것이 아니라 무엇을 만들어야 하는가를 아는 것입니다. 아무리 고급 코딩 기술과 어려운 알고리즘을 자유자재로 구사한다고 해도 사용자가 원하지 않는 소프트웨어를 만들어 냈다면 실패한 것입니다. 개발 환경 구축도 각종 도구들의 사용법을 익히는 것도 중요하지만 그 전에 자신이 속한 조직에서 어떤 일들이 있고 이런 일들을 잘하려면 무엇을 갖추어야 하는가를 고민하는 것이 첫 번째입니다.

이런 요구사항은 팀의 상황에 따라 많이 달라질 수도 있고 범위를 어떻게 잡느냐에 따라 간단한 빌드 작업으로 한정될 수도 있고 종합적인 프로젝트 관리를 포함하는 광범위한 내용이 될 수도 있습니다. 그럼 웹 프로젝트에는 어떤 요구사항이 있고 이런 요구사항을 충족하기 위해서 어떤 개발 환경이 필요한지를 살펴보겠습니다.

의사소통 관리
비단 웹 프로젝트 뿐 아니라 대부분의 프로젝트들에서 가장 중요한 성공 요인은 프로젝트의 성공을 향한 팀원들의 의지와 팀원간의 효율적인 의사소통(communication)입니다. 얼핏 둘 다 비기술적인 이슈라서 프로그래머의 영역 밖이라고 생각할 수도 있겠지만 의사소통은 이미 개발 방법론의 영역에 들어와 있습니다. 의사소통은 흔히 구성원들의 적극성의 문제, 즉 사람의 문제로 치부되는 경향이 있고 이것이 어느 정도는 사실입니다.

하지만 최근 유행하는 방법론들에서는 환경적 요인이 의사소통에 미치는 영향이 크다고 보고 의사소통을 효율적으로 하기 위한 장치들을 많이 제시하고 있습니다. 일반적인 프로그래밍 작업에서의 의견 교환, 의사 결정을 위한 회의, 문서화, 업무 요청 관리, 고객의 요구사항 관리 등이 모두 의사소통이며 개발 환경을 통해 이런 부분들을 향상시킬 수 있습니다.

XP에서는 보통 의사소통 비용은 물리적인 거리의 제곱에 비례해서 늘어난다고 합니다. 그래서 의사소통을 효율적으로 하기 위한 방법들로 물리적인 요소를 많이 제시합니다. 프로그래밍을 두 사람이 붙어 앉아서 같이 하는 짝 프로그래밍(Pair Programming), 회의가 소모적으로 흐르는 것을 방지하기 위한 기립 회의(Stand up Meeting), 자연스러운 정보 확산과 의견 교환을 위한 정보 방열기(Information Radiator) 등이 그런 것들이죠. 이런 프랙티스(practice)들을 잘 실천하려면 짝 프로그래밍을 할 수 있는 자리 배치, 팀원들이 쉽게 모여서 이야기할 수 있는 공간, 화이트보드 등 물리적 환경을 갖추어 놓아야 합니다.

소프트웨어적으로 지원할 수 있는 부분도 많습니다. 업무 요청 관리는 보통 사내 그룹웨어 등으로 소화하기 마련인데 이 부분이 잘 되어 있지 않으면 일의 진행에 병목현상이 발생하고 때로는 팀원간 마찰의 원인이 되기도 합니다. 단순히 메일을 이용하는 것보다는 업무 요청을 보내는 것, 진행 상황, 결과 등을 종합적으로 관리할 수 있는 소프트웨어가 있는 것이 좋습니다. 이슈 트래커(issue tracker)도 어느 정도 이런 부분을 소화할 수 있으나 팀의 상황에 맞게 개발해두는 것도 좋을 것입니다.

문서화 역시 시스템으로 지원해야 할 부분입니다. 문서화 시스템에서 가장 중요한 것은 문서를 빠르게 작성하고 작성한 문서를 즉시 공유할 수 있는 것입니다. 그리고 버전 관리도 되어야 하죠. 이런 요구사항을 가장 잘 만족하는 것은 위키(wiki)입니다. 대규모 CMS(Content Management System)도 많지만 오히려 단순한 위키가 더 높은 유연성을 발휘하는 경우가 많습니다.

소프트웨어 형상 관리
소프트웨어 개발 환경에서 기술적으로 가장 중요한 것은 소스 버전 관리입니다. 개발팀에서 작성하는 모든 소스는 버전 관리가 되어야 합니다. 프로그램 소스는 물론이고 스크립트, SQL, 각종 문서, 설정 파일들까지 포함합니다. 소스 버전 관리의 목적은 작업 기록의 보존을 통해 문제가 발생했을 때 원인을 추적하거나 이전 상태로 되돌리기 위한 것이죠.

보통 소스 버전 관리를 위해서 CVS(Concurrent Versions System)를 많이 사용합니다. CVS를 좀 더 개선한 Subversion도 있고 여러 가지 상용 툴도 있지만 여전히 CVS가 오픈소스 공동체에서 가장 많이 쓰이며 IDE와도 잘 통합되어 있습니다. CVS에 대한 내용 설명은 『실용주의 프로그래머를 위한 버전 관리 using CVS』라는 책을 추천하는 것으로 대신하겠습니다.

소스 관리는 사실 SCM(Software Configuration Management)의 일부분이기도 합니다. SCM은 소스에 대한 버전 관리 뿐 아니라 소프트웨어의 기능성 수준에 대한 변동 기록, 버그 수정 내역, 요구사항의 변화 등을 종합적으로 관리하는 것을 말합니다. 소프트웨어의 새 버전이 발표되면 보통 릴리스 노트(Release Notes)가 같이 배포되는데 이런 것이 SCM의 대표적인 산출물이죠.

상용 SCM 툴도 많지만 보통은 소스 관리에 CVS를 쓰고 이외에는 앞에서 언급한 이슈 트래커를 쓰는 것으로 SCM의 대부분의 영역이 커버됩니다. 소스 외의 형상 관리는 사실 업무 요청 관리와 기능적으로 아주 비슷하기 때문이죠. 오픈소스와 친한 사람이라면 버그질라(bugzilla)를 접해본 적이 있을 것입니다. 버그를 보고하고 버그가 수정되는 과정을 기록으로 남기고 조회할 수 있게 해주는 시스템이죠.

이런 것을 버그 트래커(bug tracker)라고 하는데 이를 확장한 것이 위에서 언급한 이슈 트래커입니다. 오픈소스 소프트웨어 중에도 Mantis나 TUTOS 등이 있고 위키와 이슈트래커를 합친 Trac이 있습니다. 이런 툴 하나 정도는 갖춰 놓아야할 것입니다.

빌드 자동화
지속적인 통합에서는 빌드 자동화를 가장 중요하게 다룹니다. 빌드는 소스코드를 컴파일하고 여러 가지 변환 작업을 거쳐 프로그램이 동작할 수 있게 구성하는 작업을 말합니다. 일반적인 자바 애플리케이션은 간단한 컴파일만으로 빌드가 완료되지만 웹 프로젝트는 좀 더 할 일이 많습니다. 컴파일한 클래스들이 웹 프로젝트 구조에 맞게 위치해야 하고 각종 라이브러리, TLD 파일들도 적절히 위치시켜 웹 컨테이너(서블릿 엔진)에 배치시켰을 때 정상적으로 동작하도록 해야 합니다.

프로젝트 구조가 복잡하면 그만큼 빌드 자동화도 더 복잡해집니다. 그래서 빌드 자동화 작업을 하기 전에 우선 프로젝트 구조를 잘 구성해 놓아야 합니다. <표 1>은 빌드 자동화 툴인 Ant에서 제시하는 프로젝트 구조에 대한 권고안입니다.

<표 1> Ant에서 제시하는 프로젝트 구조에 대한 권고안


여기서 눈여겨봐야 할 부분은 src와 web입니다. 자바 소스는 src에, 웹 자원은 web 아래에 컨텍스트 구조대로 배치시킵니다. 여러 IDE(Integrated Development Environment)들에서 웹 프로젝트를 구성할 때도 src와 web의 이름은 달라지기도 하지만 기본적으로 src와 web을 나누는 기준은 같습니다. 경우에 따라서 자바 소스는 src 아래에서 다시 한번 분류를 하기도 합니다. src/java, src/test로 소스와 테스트를 나누기도 하고 src/resources 등의 폴더를 만들어서 xml이나 properties를 보관하기도 합니다. 이 구조를 기준으로 보면 웹 프로젝트 빌드 작업은 다음과 같은 단계로 진행됩니다.


[1] 웹 컨테이너에서 웹 애플리케이션의 루트 역할을 하는 CONTEXT_ROOT 디렉토리를 만든다. 앞의 구조에서 web 디렉토리를 그대로 사용하기도 하고 dist/web, dist/<컨텍스트명>과 같이 만들기도 한다.
[2] 자바 클래스를 컴파일하고 CONTEXT_ROOT/WEB-INF/classes로 복사한다.
[3] 클래스패스 리소스를 CONTEXT_ROOT/WEB-INF/classes로 복사한다.
[4] 필요한 라이브러리를 CONTEXT_ROOT/WEB-INF/lib로 복사한다.
[5] JSP, HTML, CSS, JS 등의 웹 자원들을 CONTEXT_ROOT로 복사한다.
[6] tld 파일을 CONTEXT_ROOT/WEB-INF/tld나 CONTEXT_ROOT/WEB-INF/lib로 복사한다.


여기서 CONTEXT_ROOT를 web으로 그대로 사용할 수도 있는데, 그렇다면 4~6번 과정은 필요 없거나 한번만 해도 되는 작업이 됩니다. 대신 경우에 따라 프로젝트에서 빌드한 결과물을 지우고 다시 빌드하고 싶을 때 일이 복잡해질 수 있죠. 보통 IDE에서는 web 디렉토리를 그대로 CONTEXT_ROOT로 사용합니다.

만약 이런 일련의 빌드 과정을 수동으로 한다면 그 비효율은 말할 나위가 없겠죠? 그래서 IDE에서는 간단한 설정만 해두면 자동으로 빌드해줍니다. 하지만 IDE 없이 빌드해야 할 때도 있습니다. 개발할 때는 당연히 IDE로 개발을 하고 빌드도 할 수 있겠지만 IDE에서 지원하는 것 이외의 부가적인 작업을 해야 한다거나 개발자의 PC가 아닌 서버에서 직접 빌드하고 배치해야 할 때도 있습니다.

이럴 때의 빌드를 자동화하기 위해 여러 가지 빌드 자동화 도구를 사용할 수 있습니다. 유닉스/리눅스 환경에서는 오래 전부터 이런 목적을 위해 사용해 온 make라는 도구가 있습니다. Makefile이라는 파일에 빌드를 위한 스크립트들을 make 문법에 맞게 나열해두고 make 명령을 실행하면 빌드가 실행되죠.

하지만 make에서 지원하는 기능이 너무 빈약해서 빌드가 복잡할 경우 Makefile에 모든 빌드 과정을 기술해야 하기 때문에 Makefile이 복잡해져서 유지보수가 힘들다는 문제가 있습니다. 그래서 자바 진영에서는 make를 넘어서는 툴을 만들고자 했고 그래서 Ant가 등장했습니다. 그러나 Ant 역시 한계가 있었기 때문에 Ant를 한 단계 더 발전시킨 Maven이 등장했죠. 현재 대부분의 오픈소스 자바 프로젝트는 Ant나 Maven 둘 중의 하나를 빌드 도구로 사용하고 있습니다. 여기서는 구체적인 도구의 사용법을 설명하기보다 어떠한 도구들이 있는지를 간단히 살펴보고 각 도구의 장단점을 비교해 보도록 하겠습니다.

Ant
Ant는 구조적으로는 make를 그대로 이어 받았습니다. build.xml에 빌드 설정을 해두고 ant 명령을 실행하면 빌드 작업이 수행됩니다. Ant는 자주 하는 작업들을 미리 자바 클래스로 코딩해서 태스크로 만들어 두었고 클래스패스 설정이나 파일, 디렉토리 선택 등을 쉽게 할 수 있는 문법을 갖추어 놓았기 때문에 make보다 훨씬 간단하게 빌드 설정을 할 수 있죠. <리스트 1>의 build.xml의 예를 봅시다.

 <리스트 1> build.xml의 예

이것은 자바 컴파일을 하고 리소스를 복사하는 빌드 파일입니다. target 태그가 작업의 단위를 정의하는 역할을 하며 다음과 같이 target을 지정하여 실행할 수 있습니다.


ant <target 이름>


앞의 build.xml에서는 build, compile, resource라는 세 개의 target을 정의하고 있고 build라는 target은 compile, resource에 의존하고 있기 때문에 build target을 실행하면 자동으로 compile과 resource가 먼저 실행됩니다. 그리고 project 태그에서 default로 build를 정의하고 있기 때문에 target을 지정하지 않고 ant를 실행하면 자동으로 build가 실행되죠. compile target에서는 javac 태스크를 이용해서 컴파일을 하고 resource 태스크에서는 copy 태스크를 이용해서 복사를 합니다. 이것이 Ant의 기본적인 구조이고 다른 부가적인 기능들도 있지만 대체로 이런 식으로 build.xml을 구성하게 됩니다.

사실 Ant 개발자들은 Ant가 make보다 훨씬 선언적으로 빌드를 정의할 수 있다는 점을 장점으로 내세우지만 이건 사실 javac, copy와 같은 태스크를 미리 자바 클래스로 코딩해 놓았기 때문에 그런 것일 뿐 실제적으로는 Make의 메커니즘과 큰 차이가 없습니다. 그런 반면 build.xml을 선언적으로 작성할 수 있게 하기 위해 build.xml의 문법에 스크립트적인 요소를 최소한으로 줄였고 또 XML 자체가 프로그래밍이 필요한 부분을 기술하기에는 적합하지 않기 때문에 Ant의 파워는 오히려 make보다 낮아졌습니다.

그리고 실제로 자바 클래스 컴파일에 파일 몇 개 복사하는 정도라면 아주 간단하게 빌드 파일을 작성할 수 있지만 복잡해지기 시작하면 build.xml은 점점 이해하기 힘든 코드가 되어갑니다. 그래서 Ant에서는 build.xml에 주석을 충분히 달아놓을 것을 권고하고 있습니다.

하지만 주석을 많이 달아야한다는 것은 주석을 달지 않으면 안될 만큼 지저분한 코드를 만들게 된다는 뜻이기도 합니다. 사실 빌드 스크립트야 한번 만들어두면 계속 쓰니까 이해하건 말건 무슨 상관이냐고 할 수도 있겠지만 프로젝트 규모가 커지고 연관 프로젝트가 많아질수록 빌드 요구사항도 계속 변합니다.

그래서 자주 수정할 수 있도록 빌드 스크립트를 명료하게 유지할 필요가 있죠. 결국 Ant는 make보다는 조금 사정이 나아졌지만 본질적으로는 make와 동일한 문제를 갖고 있습니다.

주석과 나쁜 냄새  
학교에서는 꽤 오랫동안 소스코드에는 주석을 많이 달아야 다른 사람이 유지보수하기 쉽다고 가르쳐 왔습니다. 하지만 이것은 사실이 아닙니다. 오히려 많은 주석은 리팩토링에서 말하는 나쁜 냄새 중 하나입니다.

마틴 파울러가 쓴 『리팩토링』에서는 나쁜 코드를 식별하는 방법으로 냄새라는 표현을 사용합니다. 코드에 좋지 않은 부분이 있으면 ‘냄새 나는 코드’라는 거죠. 마틴 파울러는 주석은 그 자체가 나쁜 냄새를 풍기는 것은 아니지만 보통 코드의 나쁜 냄새를 감추기 위한 탈취제로 사용되기 때문에 주석을 써야 할 필요성을 느낀다면 주석을 쓰는 대신 코드를 리팩토링해서 코드에서 냄새가 나지 않도록 하라고 말합니다. 좋은 코드는 주석을 많이 달아놓은 코드가 아니라 코드 자체만으로도 쉽게 이해할 수 있는 코드라는 뜻이죠.

썬에서 제시하는 자바 코드 컨벤션(Java Code Convention)에서도 구현에 대해 설명하는 주석은 되도록 쓰지 말라고 권고하고 있습니다. 빌드 스크립트라고 예외가 되어선 안됩니다. 개발팀에서 개발하고 유지하는 모든 소스코드는 냄새가 나지 않는 ‘깨끗한 코드’가 되어야 합니다.

Maven
Maven은 이런 점들을 해결하고 나아가 종합적인 프로젝트 관리까지 소화하기 위해 만들어졌습니다. 그래서 기본 구조 자체도 make, Ant와는 상당히 다릅니다. 우선 Ant의 문제점이라고 지적되던 부분, 빌드를 위한 프로세스와 데이터가 섞여서 build.xml에 기술된다는 것을 해결하기 위해 데이터와 프로세스를 완전히 분리했습니다. Maven에서는 make의 Makefile, Ant의 build.xml에 대응되는 것으로 project.xml을 작성하는데 이 project.xml에는 프로세스가 전혀 들어가지 않고 오로지 프로젝트에 대한 설명만이 들어갑니다. <리스트 2>를 봅시다.

 <리스트 2> project.xml의 예

이것이 project.xml의 예입니다. 보다시피 어떻게 빌드를 할 것인가에 대한 내용은 전혀 없습니다. 프로젝트의 구조에 대한 설명들만 있죠. 어떻게 빌드할 것인가는 Maven의 플러그인에 있습니다. 플러그인은 Jelly 스크립트라는 XML 기반의 스크립트 언어와 자바 클래스를 결합해서 만들게 되며 goal이라는 것을 정의하는데 이것은 Ant의 target과 비슷하며 다음과 같이 실행할 수 있습니다.


maven <goal 이름>


Maven 플러그인의 Jelly 스크립트는 Ant의 build.xml 보다 훨씬 더 프로그래밍 요소를 많이 포함하고 있기 때문에 더 강력합니다. 게다가 데이터와 프로세스가 완전히 분리되어 있어 사용하는 입장에서는 데이터만 잘 정의해도 됩니다. 따라서 Ant보다 더 선언적이고 작성하기도 쉽습니다.

그리고 Ant는 라이브러리 의존성, 프로젝트간 의존, 상속 관계 등에 대한 지원이 전혀 없었는데 Maven에서는 Repository라는 개념을 도입하여 이런 문제를 해결하고 있습니다. 프로젝트가 어떤 라이브러리를 사용하는지, 어떤 프로젝트의 특성을 상속받는지 등을 project.xml에 설정만 해주면 필요한 jar 파일을 자동으로 Maven Repository에서 다운받아서 빌드를 해주는 것이죠. 데비안 GNU/리눅스를 사용해 봤다면 dselect를 써봤을 것입니다.

dselect에서는 어떤 소프트웨어를 설치하려고 선택하면 그 소프트웨어가 필요로 하는 라이브러리나 다른 소프트웨어를 자동으로 다운로드해서 설치해줍니다. Maven이 제공하는 의존성 관리도 이와 같은 개념입니다.

또 Maven은 프로젝트에 대한 각종 문서가 집약된 사이트를 생성하도록 해줍니다. 오픈소스 프로젝트의 홈페이지를 돌아다니다 보면 ‘built by maven’이라는 딱지가 붙은 사이트가 많습니다. 이런 사이트에는 공통적으로 Project Info와 Project Reports가 있고 이 안에 소스 저장소 연결, Javadoc, 테스트 리포트, checkstyle 리포트 등의 문서들이 있습니다.

이것은 Maven의 site 플러그인이 자동으로 생성한 사이트입니다. 여러 가지 유용한 정보들을 자동으로 생성해주기 때문에 프로젝트에 대한 정보를 팀내에서 쉽게 공유할 수 있죠. 스케쥴러를 이용해서 매일 특정 시간에 빌드하고 사이트를 생성하도록 하면 매일 매일 개발 진척 상황을 살펴볼 수 있습니다.

고민할 필요 없이 Maven을 쓰면 되겠구나 싶지만 아직은 아닙니다. Maven은 지원하는 기능이 많다보니 maven을 제대로 배우는 데는 적지 않은 시간이 걸립니다. 게다가 플러그인들은 여러 가지 구조적인 부분에서 많은 ‘가정’을 포함한 상태로 개발되어 있습니다. 이를테면 Maven으로 빌드하는 jar 파일은 항상 버전 넘버가 뒤에 붙어야 한다는 식이죠.

이런 암묵적인 룰들이 많기 때문에 Maven 플러그인을 확장하고 싶을 때 자신의 생각했던 것과 다르게 동작하는 경우가 많습니다. 게다가 Maven에서 사용하는 스크립트 언어인 Jelly 스크립트는 Ant보다는 낫지만 여전히 복잡한 요구사항을 수용하기에는 ‘진짜 프로그래밍 언어’에 비해 부족한 점이 많습니다. 결국 Jelly 스크립트로 작성한 코드는 Ant의 build.xml 못지 않게 어지러운 코드가 되곤 하죠. 게다가 Maven은 속도가 Ant에 비해 아주 느린데 이 점이 의외로 개발하면서 자주 빌드해야 할 때는 치명적인 문제가 될 수 있습니다.

선택의 문제
이런 점들 때문에 Maven을 무작정 권고하기는 어렵습니다. 그래서 또 다른 대안을 찾는 사람들도 있습니다. Groovy+Ant라는 것도 있습니다. Groovy라는 자바 기반의 스크립트 언어에서 Ant의 태스크들을 쉽게 사용할 수 있게 구성해 놓은 것이죠. 나름대로 괜찮은 대안이지만 아직 Maven의 다양한 기능들을 소화하고 있진 못합니다. 결국 선택은 개발자의 몫입니다. Ant는 한계가 있지만 쉽고 가벼운 툴이고, Maven은 기능이 다양하지만 어렵고 약간의 문제가 있습니다.

일단은 Ant로 구성해 놓고 Maven을 개선한 새로운 툴이 나오기를 기다리는 것도 나쁘지 않을 것입니다. 그리고 사실 빌드 도구 만드는 것이 어려운 일은 아니므로 스스로 자신의 팀에 맞는 빌드 도구를 만들어보는 것도 좋습니다. 목적은 위에서 나열한 웹 프로젝트의 빌드 과정을 효과적으로 해내는 것이지 어떤 도구를 사용하느냐가 중요한 것은 아닙니다.

관리의 자동화
자동화의 대상은 프로젝트의 빌드만이 아닙니다. 그 외에 개발에서 발생하는 수많은 잡무들도 모두 자동화 대상입니다. 다음이 그런 작업들입니다.


[1] 서버 설치 및 서버 환경 구성
[2] 빌드한 프로젝트를 웹 컨테이너에 배치하기
[3] 웹 컨테이너를 스탑/스타트/리스타트 시키기
[4] 주기적으로 프로젝트를 테스트하고 결과를 리포팅하기
[5] 상황에 따라 서버의 셋팅 변경하기


Ant와 Maven은 이런 일에도 상당 부분 도움을 줍니다. 이미 이런 작업들이 Ant 태스크나 Maven 플러그인으로 많이 만들어져 있기도 하고 또한 빌드 자동화와 일관된 방식으로 문제를 해결할 수 있다는 것도 장점입니다. 하지만 사실 이런 부분들은 빌드 툴보다는 bash 같은 셸 스크립트가 더 간편한 경우가 많습니다. 서버에 접속해야 하는 일들도 많고 Ant나 Maven의 태스크보다 리눅스/유닉스의 툴들이 더 유용한 경우가 많습니다. 리눅스/유닉스 세계에서는 이미 오래 전부터 이런 일들을 쉘 스크립트로 해왔습니다.

하지만 여기에도 문제가 있는 것이 셸 스크립트 역시 Ant나 Maven의 Jelly 스크립트처럼 완전한 프로그래밍 언어가 아니기 때문에 파워도 부족하고 문법도 좀 덜 친숙합니다. 그래서 이런 경우는 펄(Perl)이나 파이썬 등의 스크립트 언어를 사용하기도 합니다. 스크립트 언어는 셸 명령을 셸 스크립트에 비해 큰 불편 없이 실행시킬 수 있고 프로그래밍 언어로서의 파워도 갖고 있다는 장점이 있는 반면 배워야 할 언어가 하나 더 늘고 이런 류의 작업을 위해 이미 만들어진 것들이 많지 않다는 문제가 있습니다. 결국 또다시 선택의 문제로 귀결됩니다.

참고로 필자의 선택은 간단한 경우는 셸 스크립트, 좀 복잡해지면 파이썬입니다. 중요한 것은 잡무를 모두 자동화해서 프로그래머가 개발한 것을 쉽게 테스트하고 또 실제 서비스에 쉽게 반영할 수 있게 하는 것입니다. 필요하다면 어떤 도구라도 사용할 수 있다는 유연한 사고방식이 필요합니다.

게으를 수 있는 권리
“인간은 누구나 게으를 권리가 있다.” 폴 라파르크의 『게으를 수 있는 권리』에 나오는 말입니다. 이 책에서는 일의 노예가 되어가는 현대인들에게 삶의 목적에 대해 다시 한 번 생각해볼 것을 요구하며 또한 무작정 부지런하기만 한 것이 효율적이지도 않다고 말합니다.

프로그래머에게도 이 말은 적용됩니다. 주어진 일은 무조건 열심히 하고 보자는 생각에 낡은 방식대로 묵묵히 지겨운 일을 해내는 것은 개인에도 조직에도 바람직하지 않습니다. 이건 조직의 입장에서도 생산성 향상을 할 수 있는 기회를 놓치기 하기 때문에 또다른 의미의 태업입니다. 프로그래머는 귀찮은 일에 대해 게을러져야 더 나은 방법을 찾아낼 수 있습니다. 지퍼의 발명도 구두끈을 매기 귀찮아하는 사람이 해낸 것이고 많은 발명들이 게으름의 소산입니다.

프로그래머에게 이런 게으름은 이미 권리를 넘어서 의무입니다.

물론 게으름만으로는 충분하지 않습니다. 마냥 게을러지기만 해서는 귀찮은 일을 하지 않게 될 뿐이고 더 나은 방식으로 하게 하진 못합니다. 결국 할 일을 하면서 좀 더 게을러지기 위해서는 귀찮음을 해결하는 과정에서의 부지런함은 필요합니다. 구두끈을 매기 귀찮아 하는 사람이 지퍼를 발명하는 수고를 해야 했듯이 말이죠. 그리고 기존의 방식을 개선해야 하기 때문에 어느 정도의 창의성도 있어야 합니다.

‘우리는 이제까지 잘해왔다’는 논리에 빠지는 것 또한 경계 대상입니다. 한번 성공을 거둔 경험이 있는 조직은 자신들의 방식이 성공을 가져왔기 때문에 쉽게 매너리즘에 빠집니다. 하지만 IT 세계는 빠르게 발전하고 있고 예전에는 효율적이었던 방식이 더 효율적인 기술들이 나옴에 따라 오히려 발목을 붙잡고 있는 경우도 많다는 사실을 기억해야 합니다.

그럼 이런 자동화로 절약한 시간들은 어떻게 써야 할까요? 물론 더 게을러지기 위해서 한 일들이지만 그렇다고 정말로 게을러져서는 곤란합니다. 프로그래머의 게을러질 권리는 단순한 반복 작업에만 해당되는 것입니다. 좋은 개발 환경으로 인해 절약한 시간들은 좀 더 창조적인 일을 하는데 재투자해야 합니다. 프로그래밍 자체에 집중하는 것도 좋고 좀 더 높은 수준의 자동화에 도전해보는 것도 좋습니다. 개발 환경 구축 과정에서 경험하게 된 것들을 공유해보는 것도 여러 사람에게 유익한 결과가 될 것입니다.

학창 시절 배웠던 러다이트 운동(Luddite Movement)을 기억할 것입니다. 산업혁명 이후 기계가 인간이 할 일을 대체하기 시작하면서 많은 사람들이 일자리를 잃고 임금도 급락했습니다. 그래서 노동자들이 들고 일어나서 기계를 파괴하고 다니는 비밀 조직을 결성한 것이 바로 러다이트 운동입니다.

프로그래머에게도 이런 일이 닥치지 말란 법이 없습니다. 기계가 할 수 있는 일은 기계에 맡기고 좀 더 창조적인 일을 할 줄 알아야 기계에게 일자리를 빼앗기지 않겠죠. Ant라는 이름에도 이런 생각이 담겨 있습니다. 모 광고 카피처럼 단순 반복 작업은 부지런한 개미에게 맡기고 당신은 프로그래밍을 즐기라는 뜻인 거죠.

사실 자신의 잡무를 스스로 자동화할 수 있는 사람은 프로그래머 밖에 없습니다. 이것이 프로그래머라는 직업의 매력이기도 하죠. 하루 일과에서 가장 많은 시간을 일하면서 보내는데 이 일이 즐거워야 하지 않겠습니까. 즐거운 프로그래밍을 하는 첫걸음은 귀찮고 일을 제거하는 것입니다. 귀찮은 것들일랑 모두 컴퓨터에 맡기고 우리는 프로그래밍을 즐겨 봅시다.

 

출처 : http://www.zdnet.co.kr/ArticleView.asp?artice_id=00000039135663

이제 기초를 익히고 스스로 개발 환경을 갖출 줄도 알게 되었으니 어디 가서 프로그래머라고 말할 수 있게 되었습니다. 다음은 사람들을 이끌고 나갈 수 있는 능력을 갖추는 것입니다.

패턴과 프레임워크, 다소 어려워 보일 수 있는 주제이지만 프로그래머로써 한 단계 거듭나기 위해 거쳐야 할 과제입니다. 이제까지 기본기로만 싸워왔다면 이제부터는 좀 고난이도의 기술과 새로운 무기들을 써 봅시다. 지금부터가 정말 즐거운 프로그래밍의 세계로 들어가는 길입니다.

탈무드에는 배고픈 자식에게 고기를 잡아주기보다 고기를 잡는 방법을 가르쳐주라는 말이 있습니다. 좋은 프로그램을 고기라고 본다면 좋은 프로그램을 만들어주는 것보다 좋은 프로그램을 만드는 방법을 가르쳐야 한다는 말이 되겠죠. 하지만 그러기엔 마소 주니어의 지면은 넉넉지 않습니다. 사실 초급자는 잡지에서 새로운 것을 익히려 하기보다 입문서에서 배워야 합니다.

그렇다면 이 한정된 지면에서 할 수 있는 최선은 자세한 내용을 기술하는 것이 아니라 무엇을 공부해야 하는지, 어디서 정보를 얻을 수 있는지, 요즘 추세는 어떤지를 알려주는 것이겠지요. 말하자면 좋은 프로그램이 아니라 그것을 만드는 방법을 고기로 보고 이것을 잡는 방법을 제시하겠다는 뜻입니다.

필자의 짧은 세 번의 연재에는 모두 이런 생각이 담겨 있고 그래서 기술적인 상세 내용 설명보다는 무엇을 공부해야 하는가에 대한 내용에 치중했습니다. 이번 글 역시 디자인 패턴, 프레임워크, XP 세 가지를 다루고 있지만 각각의 상세한 설명보다는 이런 개념들을 어떻게 이해하고 활용해야 하는가에 초점을 맞출 것입니다.

디자인 패턴과 프레임워크는 먼저 용어에 대한 설명부터 시작합니다. 그리고 그것들의 목적과 가치에 대해 살펴보고 몇 가지 예를 살펴볼 것입니다. XP는 제목에 포함시키긴 했지만 이것은 디자인 패턴과 프레임워크를 XP의 시각에서 바라본다는 의미이고 개별적인 주제로는 다루지 않습니다.

용어로서의 디자인 패턴
디자인 패턴이란 프로그래밍에서 발생하는 여러 가지 문제 영역에 대한 해결 방법들을 모아서 정리해놓은 것입니다. GoF(Gang of Four)가 여러 가지 패턴들을 모아 『디자인 패턴』이라는 책을 내면서부터 디자인 패턴이라는 용어가 일반화되기 시작했습니다. GoF의 디자인 패턴 중 하나를 예로 들면 데코레이터(Decorator)라는 패턴이 있습니다. 랩퍼(Wrapper)라는 말로도 쓰이는데 이것은 어떤 객체에 기본적인 동작은 유지하면서 부가적인 기능을 넣고 싶을 때 사용합니다.

이 패턴의 간단한 예로 JDBC에서 수행한 쿼리를 로그로 남기고 싶을 때 로깅을 위한 데코레이터를 사용할 수 있습니다. 보통 JDBC에서는 Connection 객체를 이용해서 Statement 객체를 생성하고 이것을 이용해서 쿼리를 수행하게 되는데 이 때 Statement 대신 LoggableStatement를 사용하는 것입니다.

LoggableStatement는 Statement를 상속하며 생성자에서 Connection 객체가 만든 Statement 객체를 받아서 멤버로 갖고 있습니다. 그리고 쿼리 요청이 오면 먼저 로깅을 하고 실제 쿼리는 생성자에서 받은 Statement 객체에 위임합니다. 이런 방식으로 Statement의 동작은 그대로 유지하면서 로그를 남기는 기능을 추가할 수 있습니다.

이런 것이 디자인 패턴입니다. 수학 공식처럼 같은 종류의 문제들에 대해 이미 정리된 해결책을 이용함으로써 매번 같은 문제를 해결하는데 중복으로 투자되는 노력을 줄이려는 것이죠.

하지만 디자인 패턴을 그대로 사용하는 것이 좋은 것만은 아닙니다. 오용의 위험성이 크기 때문입니다. 사실 어떤 문제 상황에 어떤 패턴을 적용하면 되는가를 판단하기란 쉬운 일이 아니기 때문에 필요하지도 않은 복잡한 패턴을 도입하는 경우가 많습니다. 그 때문에 간단하게 해결될 문제가 더 복잡해지기도 하죠. 그래서 안티 패턴 같은 이야기도 나오고 있습니다. XP(eXtreme Programming)에서도 디자인 패턴을 의식하지 말라고 합니다.

그보다 원하는대로 동작할 수 있는 가장 간단한 방법으로 코딩을 하다가 중복이 생기거나 코드가 지저분해지면 리팩토링을 합니다. 그러면서 자연스럽게 코드가 디자인 패턴의 모습을 갖추어 나가는 것이 바람직합니다.

이처럼 디자인 패턴의 원래 목적은 문제 해결 시간을 단축시키는 것이었지만 문제 영역과 패턴을 정확히 매칭시키기 어렵고 Over-Engineering이 나오는 경우가 많기 때문에 실제 개발에서는 디자인 패턴을 직접적으로 사용하지 않는 것이 좋습니다.

디자인 패턴의 가치는 그보다도 의사소통 비용을 줄여줄 수 있다는 데 있습니다. 서로가 패턴에 대해 잘 알고 있다면 복잡한 설명 없이도 아주 간단하게 의사소통이 이루어질 수 있죠. 이를테면 이 예를 설명하기 위해 복잡하게 LoggableStatement의 구조를 설명하는 것 대신에 ‘이 LoggableStatement 클래스는 Statement의 데코레이터로 로깅을 추가로 하게 되어 있어’라고 말할 수 있다는 것입니다. 즉 패턴은 해결책으로서보다 의사소통을 위한 용어로서 활용하는 거라고 생각하는 것이 좋습니다.

자바빈즈의 해악
패턴 오용의 한 예로 자바빈즈를 들 수 있습니다. ‘해악’이라는 표현까지 써가면서 자바빈즈를 언급한 것에 대해 놀라는 사람도 많을 것입니다. 자바를 배우면서 보통 자바빈즈의 장점에 대한 이야기를 많이 듣게 되니까요. 하지만 점점 자바빈즈를 해로운 것으로 인식하는 사람들이 많아지고 있습니다. 먼저 자바빈즈가 무엇인지부터 다시 살펴봅시다.

자바빈즈는 원래 컴포넌트 아키텍처를 자바에서 구현하기 위해 도입된 기술입니다. 객체의 프로퍼티의 조작 메커니즘을 제공하여 재사용성이 높고 유연한 컴포넌트를 만드는 것이 목적이죠. 자바빈즈의 특징으로 언급되는 것이 여러 가지가 있지만 다른 자바 객체와 구별되는 특징은 프로퍼티 관리 방식입니다. 프로퍼티로 private 필드를 두고 그 필드에 대한 public getter/setter를 만들어서 이를 이용해서 프로퍼티에 접근을 합니다.

사실 이것은 일반적인 필드 인캡슐레이션(field encapsulation)과 크게 다르지 않습니다. 다른 점은 이 프로퍼티의 이름과 getter/setter의 이름에 네이밍 룰을 부여해서 빈즈 API를 통해서 프로퍼티의 이름으로 이 프로퍼티를 읽거나 쓸 수 있다는 점입니다.

예를 들어 name이라는 프로퍼티가 있다면 getName, setName이라는 이름으로 getter/setter를 만들고 빈즈 API에서는 name이라는 이름으로 프로퍼티에 접근할 수 있습니다. 즉, 일반적인 필드 인캡슐레이션을 하면 프로퍼티에 접근하기 위해 메쏘드명을 코드에 직접 써야 하지만 빈즈 규약에 맞게 getter/setter를 만들고 빈즈 API를 이용하면 String으로 주어진 프로퍼티에 접근하는 것이 가능해진다는 것입니다.

이 점을 이용하면 복잡한 애플리케이션에서 자바빈즈 객체의 프로퍼티를 동적으로 쉽게 조작할 수 있습니다. 그래서 정보 은폐(information hiding, data hiding)와 동적인 프로퍼티 접근의 두 마리 토끼를 동시에 잡는 것이 자바빈즈의 목적입니다.

그러면 무엇이 문제일까요? 우선 첫 번째 목적인 정보 은폐가 제대로 되지 않습니다. 사실 필드 인캡슐레이션은 정보 은폐를 위한 것이라고 하지만 이것은 허구에 불과합니다. 실질적으로 프로퍼티에 대한 접근성은 public field와 private field, public getter/setter가 완전히 동일합니다. 똑같이 읽고 쓸 수 있는데 뭐가 다르겠습니까. 단지 좀 더 조심스럽게 쓰지 않을까라고 추측하는 것뿐입니다. 그래서 자바빈즈를 사용할 때 꼭 필요한 경우가 아니라면 setter는 만들지 않기를 권하기도 합니다.

하지만 일반적인 필드 인캡슐레이션이라면 setter를 만들지 말라는 조언을 할 수 있겠지만 자바빈즈는 그 목적이 프로퍼티를 동적으로 조작하는 것이므로 컴포넌트로서의 역할을 제대로 하려면 setter도 당연히 필요합니다. 결국 필드 인캡슐레이션으로 인해 코드는 길어졌지만 정보 은폐는 달성하지 못했으므로 실질적인 이득이 없습니다.

또 다른 문제는 자바빈즈의 한계에 관한 것입니다. 동적으로 프로퍼티의 내용에 접근할 수는 있지만 프로퍼티를 추가하거나 없애는 정도의 유연성은 소화할 수 없습니다. 많은 경우 자바빈즈를 데이터 전송 객체(Data Transfer Object)로 사용하는데 이런 경우는 프로퍼티의 내용 뿐 아니라 프로퍼티 종류 자체가 자주 변합니다. 그런데 자바빈즈로 만들게 되면 프로퍼티가 늘어날 때마다 필드와 메쏘드를 추가해야 합니다. 그리고 데이터의 종류가 달라질 때마다 새로운 클래스를 만들어야 하구요. 자바 컬렉션에 Map이 있는데 이런 비효율을 감당해야 할 이유는 별로 없습니다.

Map에서는 프로퍼티에 동적으로 접근하는 것은 물론이고 프로퍼티가 자유롭게 늘어나거나 줄어들 수 있습니다. 게다가 빈즈 API는 String으로 주어진 프로퍼티명에 대한 getter/setter를 호출하기 위해 내부적으로 Introspection이라는 기술을 사용하는데 이것은 자바의 Reflection API를 이용합니다. 때문에 성능상에서도 해싱(Hashing)을 이용하는 HashMap에 비해 나은 것이 없습니다.

그래서 자바빈즈의 단점에 대해 많은 사람들이 공감하기 시작했고 최근에 등장하는 각종 프레임워크들도 이런 경향을 반영하고 있습니다. 초기의 프레임워크들은 자바빈즈를 활용하는 기능들을 제공했으나 최신 프레임워크들은 자바의 컬렉션을 적극적으로 활용하고 있습니다.

일반적으로 데이터 전송 객체의 경우는 Map을 그냥 사용하는 것이 좋고 그 외에 복잡한 로직을 수행하는 객체인데 프로퍼티를 많이 사용해야 한다면 Map을 데코레이터로 사용하면 편리합니다. 그리고 일반적으로 클래스를 설계할 때 setter는 되도록 만들지 않는 것이 좋습니다.

프레임워크의 가치
프레임워크(framework)란 단어는 여러 가지 의미로 사용되지만 보통은 어떤 영역의 API들을 사용하기 편리한 형태로 포장해놓은 것을 말합니다. 이를테면 자바 컬렉션 프레임워크는 자바에서 각종 컬렉션을 사용하기 쉽게 모아놓은 것이고 자카르타 프로젝트의 BSF(Bean Scripting Framework)는 자바 애플리케이션에서 자바빈즈나 스크립팅을 하기 쉽게 만든 API의 집합입니다. 그리고 지금 논의하려는 웹 애플리케이션 프레임워크는 웹에서 서블릿 API들을 좀 더 편리하고 객체지향적으로 사용할 수 있게 해주는 것입니다.

현재 웹 애플리케이션 프레임워크의 주류는 단연 스트럿츠(struts)입니다. 전 세계 자바 웹 개발자의 60%가 스트럿츠를 사용하고 있다고 하죠. 하지만 스트럿츠가 있음에도 계속해서 새로운 프레임워크가 등장하고 있습니다. 현재까지 공개된 프레임워크의 숫자는 이미 60개를 넘습니다. 스트럿츠에는 어떤 부족함이 있길래, 그리고 다른 프레임워크들은 뭐가 부족하길래 개발자들이 새로운 프레임워크를 계속 만들까요.

프레임워크의 정의  
스트럭처(structure), 아키텍처(architecture), 프레임워크(framework)란 용어는 기술과 시대가 변하면서 조금씩 그 의미를 달리해 가는 것 같습니다. 스트럭처라는 용어가 Tree와 같은 계층적인 탄탄한 기반 구조를 말하는 반면, 프레임워크는 다소 수평적인 의미를 가지는 하부 구조를 나타냅니다. 또한 아키텍처라는 것은 이 두 부분을 모두 포함하는 더 포괄적인 개념 체계적인 기반 구조를 나타냅니다.

그러나 프레임워크이란 용어는 스트럭처나 아키텍처보다 더 낮은 레벨의 의미를 지닙니다. 즉, 프레임워크의 실체는 때론 API의 집합으로 나타나기도 한다는 것이지요. 썬 마이크로시스템즈의 Java Activation Framework가 그러하고 Java Media Framework가 그러합니다. 그러나 최근에 와서 IBM의 샌프란시스코 프레임워크(San Francisco Framework)라는 용어가 등장하면서 ‘반제품’의 의미를 강하게 띄는 것 같습니다.

알다시피 샌프란시스코 프레임워크는 정형화된 업무를 위한 비즈니스 컴포넌트를 미리 만들어 두고, 이를 조립함으로써 생산성을 극대화시키자는 것이 요지입니다. 어쨌거나 현재의 프레임워크란 것은 ‘기반 틀 구조’라는 모호한 추상적인 개념이기보다는 물리적인 실체이면서 반제품 성격의 구체적이고 체계화된 API를 제공하는 개념이라고 봐야 할 것입니다.
- javaservice.net 이원영님의 글

프레임워크의 가치는 기술적인 영역에서 소모적이고 반복적인 코딩을 프레임워크가 소화하여 개발자가 비즈니스 로직에만 전념할 수 있도록 해주는 데 있습니다. 이를테면 웹 프로그래밍을 한다면 HTTP 프로토콜도 어느 정도 알아야 하고 서블릿 API를 익히고 쿠키도 쓸 줄 알아야 합니다.

하지만 JSF(Java Server Faces) 같은 프레임워크를 이용하면 일반 자바 GUI 프로그래밍을 하듯이 프로그래밍할 수 있습니다. 서블릿 API를 몰라도 웹 애플리케이션을 잘 만들 수 있는 것이죠. 로깅(logging) 프레임워크가 없다면 개발자가 로그를 남기고 싶을 때 직접 파일을 열어서 파일에 내용을 쓰는 등의 복잡한 코드를 써나가야 하지만 log4j와 같은 프레임워크를 쓰면 단순히 Logger 객체에 로그 메시지를 넘겨주는 것으로 로깅을 할 수 있습니다.

EJB도 이런 프레임워크의 일종입니다. EJB가 없다면 분산 트랜잭션이 생길 때마다 개발자가 직접 분산 트랜잭션 관리를 해야 하지만 EJB 엔진이 이를 대신해주면 개발자는 비즈니스 로직의 구현에만 집중할 수 있죠. 이처럼 기술적인 영역이 달라질 때마다 개발자는 새로운 API를 익히고 또 소모적이고 반복적인 코드를 써야 하는데 해당 분야에 좋은 프레임워크가 있으면 중복 코드도 쉽게 제거할 수 있고 기술적인 세부사항에 신경쓰지 않아도 됩니다.

결국 프레임워크의 가치는 개발자의 할 일을 줄여주는 데 있는 것입니다.

하지만 간혹 프레임워크의 가치를 유지보수의 용이함에서 찾는 경우가 있습니다. 프레임워크가 꽉 짜놓은 틀 안에서만 코딩을 하면 개발자들이 모두 일관된 방식으로 개발하기 때문에 다른 사람이 작성한 코드를 파악하기 쉬워서 유지보수하기도 쉽다는 것이죠.

그러나 이렇게 일관성을 필요 이상으로 강조하면 때때로 문제가 될 수 있습니다. PEAA(Patterns of Enterprise Application Architecture)에서는 기업용 애플리케이션(Enterprise Application)에는 모두 각각의 어려움을 가지고 있는데 이 어려움은 모두 종류가 다르기 때문에 이 모든 어려움을 해결하는 하나의 아키텍처는 불가능하다고 말합니다. 다른 문제 영역에는 다른 해결책을 써야한다는 것이죠. 그런데 프레임워크를 통해서 지나치게 틀을 고정시켜 놓게 되면 그 틀에서 수용할 수 없는 문제를 만났을 때 아주 비효율적인 코딩을 하게 됩니다. 물론 이것은 프레임워크 자체가 가져오는 직접적인 문제는 아닙니다.

그보다도 프레임워크의 장점을 ‘일관성’에서 찾는 사람들이 일반적으로 범하는 오류입니다. 일관성의 가치를 지나치게 높게 평가한 나머지 코딩 패턴에 너무 많은 구속을 하게 되고 그 결과로 문제 영역에 맞는 패턴을 사용하는 것이 아니라 패턴에 코드를 끼워 맞추는 코드를 만들게 됩니다. 그래서 결과적으로 프레임워크를 유연하게 활용하지 못하고 오히려 코드가 더 길고 복잡해지는 경우가 많습니다. 앞서 언급한 디자인 패턴의 오용과 같은 것이죠.

사실 일관성의 문제는 프레임워크보다도 컨벤션을 통해 해결해야 하는 것입니다. XP에서는 코드에 대한 소유권을 개발자 한 명이 아니라 모두가 갖게 되는 코드 공동 소유(collective code ownership)를 권장합니다. 그래서 다른 사람의 코드를 읽고 수정하는 일은 아주 일상적인 일입니다. 이런 상황에서 개발자 각각의 개성이 강해서 서로의 코드를 읽기 어렵다면 문제가 크겠죠.

그럼에도 불구하고 XP는 프레임워크에 대해 약간의 부정적인 입장을 취하고 있습니다. 이것은 프레임워크가 디자인 패턴과 같은 문제를 발생시킬 수 있다고 보기 때문이기도 하지만 고정적인 패턴에 따라 코딩된 코드보다도 코드 컨벤션이 통일되고 리팩토링이 잘된 코드가 더 읽기 쉽다고 보기 때문입니다. 실질적으로 코딩 패턴의 강제를 통해 얻을 수 있는 것은 그리 많지 않습니다. 어차피 이해하기 가장 어려운 부분은 비즈니스 로직인데 이 부분까지 패턴이나 프레임워크로 강제할 수는 없기 때문입니다.

결국 아무리 다양한 기능을 제공한다 해도 결과적으로 개발자가 작성해야 하는 코드를 줄여주지 못한다면 좋은 프레임워크라고 할 수 없습니다. 스트럿츠에 대한 대안이 계속 쏟아져 나오고 있는 것도 실제적으로 스트럿츠가 개발자의 일을 많이 줄여주지 못하기 때문입니다.

스트럿츠의 강점으로 컨트롤러(controller)를 꼽는데 사실 스트럿츠의 컨트롤러는 HttpServlet의 역할을 Action으로, web.xml의 역할을 struts-config.xml로 옮겨온 것 밖에 없습니다. 사실 스트럿츠의 컨트롤러 기능 자체는 서블릿 API의 래퍼 몇 개로 완전히 대체할 수 있습니다. 이런 점을 깨달은 개발자들이 정말로 개발자의 일을 줄여주는 것을 목표로 다양한 프레임워크를 쏟아내고 있는 것입니다.

프레임워크의 구성 요소
실제로 어떤 프레임워크를 사용하든 그대로 사용하는 경우는 그다지 많지 않습니다. 다른 프레임워크와 결합해서 쓰기도 하고 상속을 통해서 확장하거나 혹은 직접 소스코드를 고쳐서 사용하기도 합니다. 어차피 모든 영역에 맞는 프레임워크는 없기 때문에 각자의 환경에 맞게 커스터마이징하는 것은 바람직한 일입니다. 그래서 이번에는 프레임워크를 구성하는 요소들에 대해 살펴보겠습니다.

제어 구조의 반전
요즘 나오는 프레임워크들은 공통적으로 제어 구조의 반전(IoC, Inversion of Control)를 내세우고 있습니다. 제어 구조의 반전이라는 말의 의미는 개발자가 제어하던 것을 프레임워크로 옮겨서 프레임워크에서 제어한다는 것입니다.

예를 들면 GUI로 입력을 받는 프로그램을 개발자가 직접 모든 부분을 만든다면 입력 항목을 출력하고 사용자의 입력을 기다린 다음 사용자의 입력을 판단해서 로직을 실행하는 코드를 모두 순차적으로 쓰게 됩니다. 그런데 이걸 GUI 프레임워크를 이용하면 화면과 입력에 관한 부분은 프레임워크가 처리하고 사용자는 단지 사용자가 입력했을 때 반응하는 이벤트 핸들러만 작성합니다. 즉 입력 제어가 개발자에서 프레임워크로 이동하는 것입니다.

윈도우 프로그래밍에서 콜백(callback)이라고 부르는 것도 비슷한 개념입니다. 사실 서블릿 자체도 일종의 프레임워크이고 이미 IoC가 일어나고 있습니다. HTTP 요청을 대기하고 받아들여서 서블릿을 실행하는 코드를 개발자가 작성하는 것이 아니라 서블릿 엔진이 제공하고 개발자는 단지 서블릿만을 작성하게 되죠. 결국 이런 컨트롤의 반전을 통해서 중복 코드를 효과적으로 제거할 수 있습니다. 프레임워크의 목적이 소모적이고 반복적인 코드를 제거하는 것인 만큼 대부분의 프레임워크에는 프레임워크 개발자가 의도했든 아니든 IoC가 사용되고 있습니다.

하지만 IoC 역시 주의해서 사용해야 합니다. 일상적인 프로그램과는 달리 제어 구조가 거꾸로 되기 때문에 디버깅하기도 어렵고 컨트롤이 반전된 부분에 대한 선행 지식이 없으면 이해하기 어려운 코드가 됩니다. 그래서 IoC는 꼭 필요한 경우가 아니면 사용하지 않는 것이 좋습니다.

MVC 패턴
요즘 프레임워크가 IoC를 강조한다면 스트럿츠처럼 좀 오래된 프레임워크들은 MVC(Model-View-Controller)를 내세웠습니다. UI가 포함된 프레임워크에는 거의 필수적으로 사용되는 패턴입니다. 원래 MVC는 웹보다도 일반 GUI 애플리케이션을 개발할 때 UI(User Interface)와 비즈니스 로직을 효과적으로 분리하기 위해서 고안되었습니다. 현재는 웹에서도 그 효과가 입증되었기 때문에 널리 쓰이고 있습니다.

MVC의 구조는 MFC 프로그램에서 등장했던 Document-View 구조를 한 차원 더 발전시킨 것으로 프로그램의 구성 요소를 모델, 뷰, 컨트롤러로 나누어서 각각 다른 역할을 맡게 하는 것입니다. 모델은 비즈니스 로직을 담는 객체를 말하며 이 모델은 PEAA에서 말하는 도메인 모델이 됩니다.

<그림 1>MVC 패턴의 구조


데이터베이스에 접근하는 것도 모델 객체의 몫입니다. 모델 객체들 자체로 UML 클래스 다이어그램을 그린다면 그 자체로 비즈니스 로직의 표현이 될 수 있습니다. 뷰는 화면 UI를 구성하는 요소이며 일반적으로 웹에서는 JSP가 뷰의 역할을 맡습니다.

그리고 컨트롤러는 사용자의 요청을 받아서 모델 객체를 실행하고 그 결과를 뷰로 전달하는 역할을 맡게 되는데 일반적으로 이 부분은 프레임워크에서 담당하며 개발자는 어떤 모델을 실행하고 어떤 뷰를 선택할 것인지를 컨트롤러에 알려주기만 하면 됩니다. 이런 구조가 이상적인 MVC 패턴의 구조입니다.

MVC 패턴의 가장 큰 장점은 모델과 뷰의 분리를 통해 화면 UI를 위한 코드와 비즈니스 로직을 위한 코드가 섞이지 않는다는 것입니다. 그래서 때때로 화면 UI 개발자와 비즈니스 로직 개발자를 따로 두는 것도 가능합니다. 그리고 이 패턴을 통해 프로그램 디자인의 기본 원칙인 low coupling, high cohesion을 자연스럽게 달성할 수 있죠.

무언가 변경하고 싶을 때 여러 컴포넌트들을 같이 변경해야 하는 coupling은 줄이면서 컴포넌트간의 협력(cohesion)은 컨트롤러를 통해 자유롭게 할 수 있는 것입니다. 또한 모델과 뷰가 분리되면 한 모델에 여러 가지 다양한 뷰를 붙이는 것도 가능하고 그 반대의 경우도 가능합니다. 그래서 요구사항의 복잡도는 높지만 규격이 잘 정해진 애플리케이션을 만들 때는 개발자의 일을 획기적으로 줄일 수 있습니다. MVC 프레임워크란 결국 이런 모델과 뷰의 분리를 효과적으로 할 수 있는 컨트롤러를 제공한다는 데에 그 가치가 있습니다.

XML 설정 파일의 역할
프레임워크에서 또 하나 중요한 것은 설정 파일의 활용입니다. 유행처럼 번져나가기 시작한 XML 설정 파일은 요즘 거의 프레임워크의 필수사항처럼 되어가고 있습니다. 초기에 설정 파일이 등장하기 시작한 이유는 소스의 내용 중 상수에 해당하는 값들을 설정 파일로 빼 놓으면 컴파일을 다시 하지 않아도 동작을 변경할 수 있기 때문입니다. C++ 등의 언어에서는 컴파일 후에 링크까지 해야 하기 때문에 이 차이는 적지 않습니다.

하지만 자바에서는 필요한 부분만 간단하게 컴파일하면 바로 동작하게 할 수 있고 XP의 지속적인 통합(Continuous Integration)이 일반화되면서 소스를 변경하는 비용이 크게 줄어들었죠. 그래서 사실 지금은 XML 설정 파일에 상수를 넣어두는 것이 자바 코드에 넣는 것보다 개발시의 변경 비용이 적다고 하기 힘듭니다.

하지만 여전히 애플리케이션 사용자의 입장에서는 XML이 훨씬 변경하기 쉽습니다. 클라이언트로 배포된 프로그램 혹은 서버에 배치된 프로그램의 설정을 바꾸기 위해 다시 빌드를 할 수는 없는 일이니까요.

하지만 이런 비용적인 측면 외에도 XML 설정 파일은 자바 코드보다 좀 더 정보를 서술적(descriptive)으로 담아놓을 수 있다는 장점이 있습니다.

XML은 구조적인 정보와 메타 정보를 포함한 모든 정보를 한 눈에 알아보기 쉽게 정리할 수 있습니다. 또한 자바 코드에서 여러 클래스로 분산될 수 있는 설정 내용을 개발자가 관리하기 편한 단위로 손쉽게 통합해서 관리할 수 있죠.

자바의 프로퍼티도 설정 파일 용도로 많이 사용되어 왔습니다만 구조적인 정보를 제대로 정의하기 어렵기 때문에 설정 파일로는 부적합합니다. 게다가 프로퍼티는 한글 문제도 있기 때문에 앞으로 프로퍼티는 쓰지 않는 것이 좋다고 봅니다.

뭐든지 잘못 쓰면 해가 된다는 점에서 XML 역시 예외가 아닙니다. XML 설정 파일을 오용하는 대표적인 사례로 Jelly 스크립트가 있습니다. 자카르타 commons 프로젝트의 하나인데 XML로 스크립팅을 할 수 있게 해주는 엔진입니다. Jelly 스크립트의 파워는 여전히 진짜 프로그래밍 언어에 미치지 못하면서 Jelly로 스크립트를 짜면 가독성도 떨어지고 리팩토링하기 어렵기 때문에 점점 지저분한 코드가 되고 맙니다. Ant도 이와 비슷한 문제점이 있습니다. 그래서 복잡한 제어 흐름이 필요한 부분에서는 자바 코드를 직접 이용하고 이것이 무겁다면 좀더 가벼운 스크립트 언어를 자바에 임베딩해서 사용하는 것이 좋습니다.

단, SQL의 경우는 언어 자체가 결과물에 대한 정의를 표현하는 것이기 때문에 복잡한 제어 구조가 필요하지 않아 XML로 별 무리 없이 소화가 가능합니다. 실제로 XML과 JDBC를 결합시킨 iBatis에서 XML로 SQL들을 관리해보면 실제 자바 코드보다 훨씬 쉽고 보기 좋게 코딩할 수 있습니다.

또 한 가지 주의해야 할 것은 XML 설정 파일을 이용하게 만들더라도 XML 없이도 프레임워크가 동작하게 만들어야 한다는 것입니다. 앞서의 iBatis는 구현된 기능들이 정말 편리하고 좋지만 각 컴포넌트들이 지나치게 강하게 결합되어 있고 XML 설정 파일 없이는 아무 것도 하지 못합니다. 그래서 추가 기능을 넣고 싶다거나 개선하고 싶은 부분이 있을 때 상속 등의 방법으로는 해결하기 어렵고 거의 대부분 소스를 수정해야 해결할 수 있죠. 그래서 프레임워크를 만들더라도 일단 자바 코드로 모두 만든 다음 마지막에 XML로 설정 내용들을 빼는 작업을 하는 게 좋습니다.

살펴볼만한 프레임워크
그림도 코드도 없는 지루한 텍스트를 읽어 내려오느라 지루했을 독자들을 위해 구체적인 프레임워크들을 몇 가지 소개할까 합니다. 웹 애플리케이션 프레임워크 뿐 아니라 여러 영역에서 유용하다고 생각되는 것들을 뽑았습니다. 단지 필자의 경험 속에서 유용했던 것들이므로 한 번 살펴볼 가치가 있다는 정도로 받아들이면 될 것입니다.

스트럿츠와 JSF
웹 애플리케이션 프레임워크로는 스트럿츠가 현재의 주류이며 JSF가 일부에서 미래의 주류로 꼽힙니다. 모두 한 사람이 주도한 프레임워크입니다.

필자의 판단을 덧붙이면, 스트럿츠는 실제로 개발자의 일을 거의 줄여주지 못하며 오히려 늘이는 경우조차 간혹 있습니다. 스트럿츠를 쓸 바에는 직접 스트럿츠와 유사한 형태로 서블릿을 직접 사용하도록 재구성하는 것이 좋으리라 생각됩니다. 물론 유효성 검사 기능과 Tiles는 좋은 기능이지만 이들은 스트럿츠와 분리해서도 사용 가능합니다.

JSF는 미래의 주류라고 하기엔 부족함이 너무 많습니다. 썬에서 Java Studio Creator를 통해 멋진 가능성을 내보이긴 했지만 그런 IDE를 사용하지 않을 때의 JSF 코딩은 너무 번잡하고 복잡도도 큽니다. 이는 구조적으로 아직 개선의 여지가 많다는 것이죠. JSF 스펙 역시 꾸준히 발전하고 있으니 미래는 어떨지 모르겠지만 지금 JSF를 사용하는 것은 시기상조라 생각됩니다. 오히려 JSF와 비슷한 구조인 Tapestry가 더 완성도가 높다는 평가를 받고 있습니다.

Turbine
Turbine도 꽤 널리 쓰이는 프레임워크 중 하나입니다. 기본 틀은 스트럿츠와 비슷하나 훨씬 많은 부분들을 커버하고 있기 때문에 코딩량은 상당히 줄어듭니다. 개발자의 편의를 위한 클래스들도 많으며 Turbine 개발 과정에서 만들어진 컴포넌트들이 상당수 독립해서 별도의 프로젝트로 진행되고 있습니다. 전에는 유연성이 다소 떨어진다는 것이 단점이었는데 점점 나아지고 있습니다.

Spring
Spring은 요즘 주목 받는 프레임워크로 AOP(Aspect Oriented Programming)를 내세우고 있으며 low coupling, high cohesion이라는 디자인 원칙이 잘 지켜진 프레임워크입니다. 딱히 웹을 염두에 두었다기보다 종합적인 애플리케이션 프레임워크이기 때문에 부분별로 사용할 수도 있고 다른 프레임워크에 붙여서 사용하기도 합니다. 데이터베이스와 관련해서 풍부한 API를 제공하며, Mock Object를 프레임워크 자체에서 제공하고 있다는 것도 특이한 점입니다. 현재 가장 추천할 만한 프레임워크입니다.

iBatis
데이터베이스와 관련해서는 앞서의 Spring으로도 충분할 수 있지만 XML로 SQL을 관리하고자 한다면 iBatis가 최선의 선택일 것입니다. 내부적인 코드의 품질은 다소 떨어지지만 기능상으로는 별다른 부족함이 없습니다. Spring과 iBatis가 JDBC에 대한 단순 래퍼 수준의 API를 제공한다면 Hibernate는 OR(Object-Relation) 맵핑을 통해서 개발자는 객체만을 바라보고 데이터베이스를 간접적으로 이용할 수 있는 프레임워크입니다. 기본적으로 구조가 깔끔하고 편리하다는 평가를 받고 있으며 비즈니스 로직을 도메인 모델로 직접 다룰 수 있다는 점에서 높이 평가할만 합니다.

OSCache, Quartz, JUnit
이외에 웹에서 캐싱 기법으로 성능을 향상시키기 위한 OSCache, 스케쥴러를 만들기 위한 API인 Quartz도 아주 유용합니다. 물론 테스팅 프레임워크로 JUnit은 언급할 필요가 없을 것입니다. 이런 프레임워크들을 꼭 사용하지 않더라도 한 번 살펴보면 배울 만한 점들을 발견할 수 있고 이런 점들이 나중에 도움이 되는 경우가 적지 않을 것입니다.

많은 논란거리들
패턴이나 프레임워크는 특정 영역에 대해 어느 정도 검증된 해결책입니다. 그래서 때때로 이에 대한 맹신이 나타나기도 합니다. 검증된 방법이라는 논리로 너무 쉽게 문제 영역에 패턴을 적용하거나 프레임워크를 사용하곤 하죠. 하지만 그 문제 영역이 패턴이나 프레임워크를 활용할 수 있는 바로 그 영역이라는 점은 검증하기 힘든 것입니다. 때문에 패턴 중심적인 사고 방식이나 프레임워크에 끼워 맞추는 코딩을 지양하고 문제 영역의 해결에 충실하면서 패턴을 생각하는 것이 더 좋습니다.

바둑 격언 중에 ‘정석은 배우고, 잊어버려라’라는 말이 있습니다. 정석을 익혀서 알고는 있되 실전에서는 정석에 집착하지 말고 상황에 따라 유연하게 대처해야 좋은 바둑을 둘 수 있다는 뜻입니다. 패턴은 프로그래밍의 정석과도 같습니다. 상황에 대한 검증된 해결책이지만 그 상황은 계속 변하고 그 변화에 빠르게 대처해야 합니다.

패턴이나 프레임워크에 대한 맹신으로 빠져드는 것을 경계하기 위해 어떤 사람들은 무엇이든지 장단점이 있으니 너무 집착하지 말라고 말합니다. 이것은 옳은 말이지만 사실 큰 의미가 있는 말은 아닙니다. 무엇이든 장단이 있다는 사실은 누구도 부인할 수 없지만 또한 누구도 부인할 수 없는 자명한 사실이기에 의미 없는 말이기도 합니다. 실제 상황에 맞게 잘 쓰려면 구체적으로 장점이 무엇인지, 단점이 무엇인지를 파악하는 것이 중요합니다. 앞서 패턴과 프레임워크를 다루면서 각각의 단점들을 구구절절이 따지고 들어간 것도 그런 이유입니다.

나날이 신기술이 쏟아져 나오고 있지만 그에 못지않게 패러다임도 많이 변화하고 있습니다. 유연성 높은 컴포넌트 기술로 주목받던 자바빈즈가 해로운 것이라는 평가를 받기 시작하고, 많이 써야 좋은 것이라고 하던 주석을 이제는 적게 쓸수록 좋은 것이라고 합니다.

신기술을 따라가는 것도 필요하지만 이러한 패러다임의 변화를 읽는 것이 더 중요합니다. 현재의 방식이 지금은 가장 효율적인 방식일 수 있겠지만 지금보다 훨씬 더 효율적인 방식이 나오는데 모르고 있으면 도태될 수밖에 없습니다. 흔히 프로그래머는 워낙 기술이 빨리 발전하기 때문에 후배들을 당할 수가 없다는 말을 하곤 합니다.

하지만 현실은 결코 그렇지 않습니다. 기술의 변화는 단지 API가 변하는 것 뿐 그렇게 중요한 것은 아닙니다. 프로그래밍의 주요한 패러다임은 그렇게 빠른 속도로 변하지 않으며 또한 그런 패러다임의 변화를 주도하는 것은 반짝이는 아이디어가 아니라 경험 속에서 축적된 고민들입니다. 끊임없이 고민하면서 프로그래밍을 해왔다면 이런 변화를 따라잡는 것은 어려운 일이 아닙니다.

좋은 프로그래머가 되기 위해서 폭넓은 지식과 빠른 학습 능력은 물론 필수적인 것입니다만 그보다 더 중요한 것은 늘 고민하는 자세와 깊이 있는 사고 능력입니다. 변화를 따라잡는 힘은 빠른 학습 능력에서 나오는 것이 아니라 변화의 중요성을 인식하고 발전을 위해 고뇌하는데서 나오는 것입니다.

이번 연재는 아마 지난 두 번보다 훨씬 더 많은 논란거리들을 담고 있을 것입니다. 패러다임은 한 사람의 고민에 의해서도 발전하지만 많은 사람들의 머리를 맞댄 토론에서도 발전합니다. 필자의 글에 반론이 있거나 더 깊은 논의를 원하는 독자들은 youngrok.com이나 javaservice.net, 혹은 c2.com에 제시해 주세요. 서로를 발전시킬 수 있는 좋은 기회가 있길 바랍니다.

 

출처 : http://www.zdnet.co.kr/ArticleView.asp?artice_id=00000039136776

+ Recent posts