KSUG(Korea Spring User Group)에 게시된 "VO vs DTO"와 관련한 글타래를 보던 중 한가지 떠오른 것이 있었다.

2004년 경에 ASP.Net 환경의 웹 어플리케이션 개발을 위해 C# 기반의 "AdvDotNet"이란 프레임워크를 개발했었다. 이 프레임워크의 기능 중 하나로, 페이지의 Form으로부터 데이터를 전송받거나 또는 Database에서 조회한 데이터를 페이지의 DataGrid에 바인딩하는 일련의 작업 효율을 높이기 위해 Attribute와 Reflection을 사용하여 어느정도 자동화된 기반 구조를 구현하여 지원했다. BaseEntity라는 클래스와 이들의 Collection을 처리하는 일련의 코드들이었는데, 닷넷 개발자들에게 이러한 구조를 설명하기 위해 정리한 내용 중 "DataTransferObject"라는 타이틀의 문서가 있었다.

...
이러한 n-tier 환경에서 각 계층 간에 데이타를 교환하기 위해서는 데이터 전송용 객체(Data Transfer Object, 이하 DTO)를 사용하기 마련인데, 사실 DTO는 어느 정도의 규모가 되는 시스템이라면 개발자들이 부담을 가질 정도로 수가 많아지고 코딩량이 증가하고 번거로워지기 일수이다.
...
DTO는 VO(Value Object)라는 이름으로 불려지기도 한다. 엄밀한 의미로는 DTO와 VO는 차이가 있지만 일반적으로 동일한 개념으로 받아들인다.  DTO와 달리 VO는 일반적으로 read only 속성을 갖는다.
...

문서에서 나는 DTO를 위와 같이 설명했다(Reflection을 사용함으로써 발생하는 퍼포먼스의 저하는 향상된 코딩 효율과 개발 용이성으로 상쇄될 수 있는 부분임을 강조하기 위해 DTO를 사용할 때 코딩의 번거로움을 먼저 화두를 꺼내고 있다).

내가 프레임워크에 대한 개념을 정립하는데는 2000년 당시에 보았던 javaservice.net의 JDF 관련 문서들이 큰 도움이 되었었는데, 이때에는 현재의 DTO를 "Entity Class"라고 표현했던 기억이 난다. 후에는 상황에 따라 DTO, VO, Model, Entity, JavaBean 등 다양하게 표현하고 또 들을 수 있었다. 여기서 "Model"이란 용어도 다소 헷갈리는 부분인데, 주로 Domain Model로서 DTO를 표현할 때에는 "로직을 갖고 있지 않은 객체로 속성(member)과 그 속성에 접근하기 위한 getter/setter 메서드를 갖는 JavaBean 클래스"를 나타내는 것이었지만, 사실 MVC 패턴의 관점에서 보면 Model은 비즈니스 구현부도 포함하기 때문이다.

사용자 삽입 이미지

※ DTO와 VO 관련 참고 사이트

개발자들의 주변에는 항상 신기술과 함께 IT, 개발 관련 용어들이 범람하고 있다. 사람인지라 모든 것을 이해하기는 불가능하기 때문에 간혹 용어의 의미가 혼동되는 경우 또한 적지 않은데, 오용하지 않기 위해서라도 그때그때 제대로 된 개념을 정립할 필요가 있을 것 같다.

얘기 나온 김에 예전에 세미나 자료로 작성했던 내용인데, Framework와 관련 용어들 중 의미와 경계가 불명확한 개념들에 대해 정리했던 부분을 옮겨본다. 미리 얘기하지만 이것들이 정답은 아니다. ^^


Framework와 관련 용어

혼동되는 개념들과 비교하여 Framework를 정의해본다. 프레임워크와 관련된 주요 개념들이다. 이들은 프레임워크로 실현되기도 하고, 상호 포함관계를 갖기도 하고, 이용관계를 갖기도 한다.

1. Framework vs Library

어플리케이션은 여러 클래스들이 상호작용하면서 그 기능을 수행하는데, 특정 기능을 수행하는 클래스들을 클래스 라이브러리(Class Library) 혹은 툴킷(Toolkit)이라고 한다. 클래스 라이브러리는 범용적으로 사용할 수 있는 기능을 제공하는 재사용할만한 연관된 클래스들의 묶음이다. 클래스 라이브러리와 프레임워크간의 가장 큰 차이는 제어 권한의 위치에 있다.

  • 라이브러리는 사용자 코드에서 활용하는 것이다.
  • 프레임워크는 사용자 코드가 준수해야하는 것이다.

(※ 사용자 코드: 개발자가 직접 비즈니스 로직을 구현한 코드)

구분 Library (Toolkit) Framework
성격 재사용 가능한 하나 이상의 서브루틴(함수)들이 저장된 파일들의 모음 서로 관련이 있는 많은 수의 문제를 풀기 위한 추상적 설계를 구체화한 클래스 집합
사용자 코드의 작성 독립적으로 작성 프레임워크 클래스를 상속하거나 참조하여 코드를 작성
호출 흐름 및 제어 권한 사용자 코드가 라이브러리 코드를 호출하고, 또한 제어하는 구조 프레임워크 코드가 유저 코드를 호출하고, 제어하는 구조(IoC, Inversion of Control)
특징 프로그램(사용자 코드)이 활용하는 대상 프로그램(사용자 코드)이 준수하는 대상

2. Framework vs Component

컴포넌트는 표준으로 정의된 컨테이너 규약 하에서 독립적으로 사용할 수 있는 소프트웨어 모듈이다. 컴포넌트의 기능은 인터페이스로 정의되며 그 내부 구현은 감추어져 있다. 프레임워크가 어플리케이션 기반 구조에 더 초점을 맞춘 개념인 반면, 컴포넌트는 컨테이너라고 하는 기반 구조에서 작동하는 모듈에 초점을 맞춘 개념이라는 점에서 차이가 있다.

컴포넌트와 프레임워크를 혼동시키는 점들

  • 프레임워크와 컴포넌트의 컨테이너는 애플리케이션을 이루는 기반 구조라는 점에서 매우 유사하다.
  • 프레임워크에 등록하는 사용자정의 확장 모듈은 같은 종류의 프레임워크에서 재사용 가능하기 때문에 컴포넌트의 경우와 그 형태가 유사하다. 이런 관계 때문에 컴포넌트와 프레임워크를 혼용하게 되거나 분류가 어려워진다.
  • 컴포넌트는 컨테이너-컴포넌트간의 관계 구조나 컨테이너, 컴포넌트 각각의 내부 구조를 구현하는 데 있어 프레임워크를 사용하기도 한다. 프레임워크는 핫 스팟(Hot Spot)과 콜드 스팟(Cold Spot) 구현 단위나 핫 스팟 인터페이스 설정에 있어 컴포넌트의 개념을 사용하기도 한다.

이런 관계로 일반적으로 프레임워크가 오래 사용되어서 기반 구조가 안정화되고 그 프레임워크를 확장해서 구현한 모듈이 많아지게 되면 그 자체가 바로 컴포넌트와 컨테이너가 된다.

구분 Component Framework
성격 컨테이너라고 하는 기반 구조에서 작동하는 컴포넌트 모듈에 초점 어플리케이션 기반 구조에 초점

3. Framework vs Design Pattern

디자인 패턴과 프레임워크는 이미 성공한 솔루션에서 유래했다는 점과 다른 유사한 사례에서 재사용될 수 있다는 점에서 공통점을 갖는다.

디자인 패턴과 프레임워크의 공통적 특징

  • 어플리케이션의 구조와 디자인을 결정한다.
  • 반복적으로 발견되는 문제를 해결하기 위한 특화된 솔루션이다.

구분 Design Pattern Framework
성격 '추상적인 무엇'으로 일반화 '실제적인 어떤 것'으로 특정 애플리케이션 도메인 영역에 특화
기능 어플리케이션 설계 시 구조적인 가이드 라인을 제공 프레임워크는 하나 이상의 디자인 패턴을 지원
구현부의 제공 여부 구체적으로 구현된 기반 코드가 없다(샘플 코드 정도를 포함). 기반 코드를 제공해서, 자연스럽게 패턴을 유도한다.
예시 MVC(Model-View-Controller) Pattern Spring-MVC 프레임워크, Struts 프레임워크

4. Framework와 Architecture

프레임워크와 아키텍처는 한마디로 밀접한 관계이다. 최종적으로 완성되는 아키텍처는 사용하는 프레임워크의 종류와 그 사용 전략이 결합되어 결정된다.

아키텍처에 따라 프레임워크의 선택이 제약될 수 있다.

  • 리치 클라이언트(Rich Client) 아키텍처라면 AJAX 프레임워크 또는 X-Internet 도입을 고려
  • 3계층(N-Tier) 기반의 분산형 아키텍처라면 C/S를 위한 프레임워크는 사용할 수 없다.

선택된 프레임워크에 따라 아키텍처가 달라질 수 있다.

  • MVC 기반의 웹 프레임워크를 사용하려고 한다면 그에 맞게 Model2 아키텍처를 사용해야 한다.
  • 프레임워크가 지원하는 패턴에 따라 아키텍처 관점에서 매우 제한적인 프레임워크가 있는 반면에 다양한 아키텍처를 지원하는 유연한 프레임워크도 있다.

구분 Architecture Framework
성격 하나 이상의 프레임워크로 구성 어플리케이션의 구조를 결정

Structure? Architecture? Framework?

  • 스트럭처는 트리(Tree)와 같은 계층적(Hierarchical)인 기반 구조를 말한다.
  • 프레임워크는 다소 수평적인 의미를 갖는 하부 구조(Infra Structure)를 나타낸다.
  • 아키텍처는 더 포괄적인 개념으로 스트럭처와 프레임워크 모두를 포함하는 체계적인 기반 구조를 의미한다.

따라서 프레임워크와 아키텍처는 다음과 같이 표현할 수 있다.

Framework = Design Pattern + Library
Architecture = Structure + Framework



 

"Development Story" 카테고리의 다른 글

2009/10/27 16:04 2009/10/27 16:04
 
Bookmark and Share

현재 웹어플리케이션을 개발하기 위해 한창 프로젝트를 진행하고 있거나, 기존 웹사이트를 유지보수 하고 있는 개발자들에게 Internet Explorer 8(이하 IE8)의 등장은 부담이 아닐 수 없다.

현재 수행중인 프로젝트의 경우에도 초기에 웹 표준 준수를 목표 잡고 Internet Explorer, Firefox 등 다양한 브라우저와 버전들에서 테스트를 병행하여 개발을 진행해왔다. 그럼에도 불구하고 새롭게 등장한 IE8 버전에서 테스트한 결과, 페이지 레이아웃이 다소 깨지거나 동작에 문제가 발생하는 부분들이 발생하였다. 프로젝트 시작하던 올 초 IE8 출시를 예고되던 시점부터 우려하던 내용이 현실이 되고말았다.

지난주 IE8에서 사이트의 레이아웃이 깨지는 것이 크게 이슈화되었고, 이를 처리하기 위한 방법을 고민하던 중 다행히 간단한 코드를 통하여 전체 페이지의 레이아웃을 바로잡을 수 있었다.

다음은 meta.jspf 의 일부인데, 내가 한 작업이라고는 첫번째 메타(META) 태그를 추가한 것이 전부이다.



아래 IE8에서 테스트한 화면을 보면 Poll 설문조사 부분이 완전히 어긋나 있다.

사용자 삽입 이미지

meta 태그를 적용후 다음과 같이 바로잡혔다.

사용자 삽입 이미지

한가지 더 예를 들면, 다음과 같이 테이블 형태가 깨지던 화면이

사용자 삽입 이미지

정상의 모습으로 돌아왔다.

사용자 삽입 이미지

간단하게 meta 태그를 추가함으로써 대부분의 문제는 바로잡혔지만, 전체 문제가 해결된 것은 아니다. 바로 윗 그림을 보면 검색 버튼의 우측에 우리가 의도하지 않았던 파이프 형태의 라인이 들어간 것이 보인다. 소소한 부분들에 대해서는 확인하고 처리할 수 밖에 없을 것 같다.

개발하는 입장에선 부담이 되지만 IE8은 기존의 IE6, IE7에 비해 월등히 향상된 렌더링 성능을 보여주고 있다고 한다. 실제로 웹서핑을 해보면 체감할 수 있을 수준이다. 그리고 한가지, IE8은 자체에 괜찮은 개발자 툴을 제공하고 있다.

사용자 삽입 이미지

웹표준 적용을 위해 기존 웹사이트에 대한 IE8 브라우저에 대한 호환성을 제공하기 위한 방안 중, 가장 권장되는 수준은 웹어플리케이션을 웹 표준에 맞도록 다시 검증하고 HTML과 CSS, Javascript 등을 수정하는 방법이다. 여건에 따라 이것이 용이하지 않다면 위의 예와 같이 <META> 태그를 이용하는 방안을 권장한다. 단, 전체 페이지가 잘 설계되고 구조화되어 있어야 적용이 쉽다는 것과 <META> 태그가 모든 것을 해결해주지는 않는다는 점을 염두에 두어야 한다.

메타(META) 태그를 적용하는 방법은 두가지이다.

  - 페이지 헤더에 메타 태그(X-UA-Compatible)를 추가한다. 이 메타 태그는 여타 태그들 보다 상단에 기술되어야 한다.
  - 서비스단의 자바 코드를 사용해 응답 헤더에 덧씌운다.

메타 태그 외에도 몇가지 방법들이 있는데, 내 위키에 간략히 정리한 것을 다시 붙여본다(내용은 마소의 지난 기사 중 "Internet Explorer 8 스페셜 리포트"에서 참고, 인용했음을 밝힌다).

DTD가 없는 웹 사이트 대응 방법

  • Quirks Mode란 DTD를 표준에 따라 인식하지 못했던 과거의 IE5 브라우저의 렌더링을 그대로 흉내 내는 모드다.

DTD가 있는 웹 사이트 대응 방법

DTD가 있고 IE5에 최적화 된 페이지 대응 방법

DTD가 있고 IE5에 최적화된 웹 페이지는 HTML 소스 코드 <head>…</head> 안쪽에 다음과 같은 코드 한 줄을 포함시키면 된다.

<meta http-equiv="X-UA-Compatible" content="IE=5"/>

IE8은 이 명령을“나는 IE5에 최적화된 페이지 입니다. Quirks Mode로 렌더링 해 주십시오”로 받아들여진다.

DTD가 있고 IE6에 최적화 된 페이지 대응 방법

IE6에 최적화 된 페이지는 IE7에 최적화 작업 후 다음 코드로 대응한다.

<meta http-equiv="X-UA-Compatible" content="IE=7"/>

IE8은 이 명령을“나는 IE7에 최적화된 페이지 입니다. IE7 표준 모드로 렌더링 해 주십시오”라고 받아들인다.

DTD가 혼재되어 있는 웹 사이트 대응법

DTD가 없는 페이지는 IE8이 Quirks Mode로 렌더링 하기 때문에 아무런 대응을 하지 않아도 페이지는 깨지지 않는다. DTD가 있는 페이지에 한하여 IE7에 최적화 시킨 후 다음 코드를 적용 하면 된다.

<meta http-equiv="X-UA-Compatible" content="IE=7"/>

만약 이런 호환 유도 코드를 DTD가 있는 페이지에만 별도로 적용하는 것이 어렵다면 모든 페이지에 호환 유도 코드를 추가하는 방법도 있다. DTD가 있는 페이지만 IE7에 최적화 시킨 후 모든 페이지에 다음 코드를 적용한다.

<meta http-equiv="X-UA-Compatible" content="IE=EmulateIE7"/>

DTD가 없는 페이지는 여전히 Quirks Mode로, DTD가 있는 페이지는 IE7 표준 모드로 렌더링 할 것이다. 따라서 DTD가 있는지 없는지 여부에 관계없이 무조건 IE7 표준 모드로 렌더링 하는‘IE=7’보다 DTD가 있고 없음에 따라 자동으로 렌더링 모드를 전환해 주는‘IE=EmulateIE7’코드를 더욱 권장 한다.

호환 유도 코드를 서버측 응답 헤더에 적용하는 방법

모든 페이지에 일일이 호환 유도 코드를 추가하는 것은 아무리 봐도 효율적이라고 생각할 수 없다. 따라서 서버 사이드 개발자는 서버측 응답 헤더에 다음과 같이 적용할 수 있다. 이 코드는 웹 사이트에 전체적으로 호환 유도 코드를 삽입하는 것과 같은 효과를 거둘 수 있다.

* IIS에 적용할 코드



* Apache에 적용할 코드

X-UA-Compatible:IE=EmulateIE7

IE8은 DTD가 없거나 표준 DTD가 아닌 페이지를 만났을 때 Quirks Mode로 렌더링 하고, 표준 DTD를 만났을 때 IE7 표준 모드로 렌더링 할 것이다.

웹 표준 사이트와 낡은 브라우저의 호환성 문제

웹 표준 사이트는 IE8에 별도로 대응할 필요가 없다. IE8이 웹 표준을 잘 지원하고 있기 때문이다. 그러나 웹 표준을 잘 지킨 사이트는 낡은 브라우저에서 깨질 것이다. 낡은 브라우저는 웹 표준을 완전히 지원하지 않기 때문이다. 또한, 낡은 브라우저라고 해서 다 같은 브라우저가 아니다. IE7, IE6, IE5 세 가지 버전의 브라우저 엔진은 지원하는 표준의 범위가 다르기 때문에 렌더링도 각각 다르다. 렌더링 엔진이 제각기 다르기 때문에 각각의 버전에 대응하는 CSS 코드도 달라야 한다. CSS 코드를 적용함에 있어 버전 타깃팅 기법이 필요하다. 다행히도 IE는 조건부 주석이라는 또 다른 호환 유도 코드를 제공하고 있다.



ie7.css 파일은 IE7 버전에만 작용한다. ie6.css 파일은 IE6 버전에만 작용한다. ie5.css 파일은 IE5 버전에만 작용한다. 나머지 브라우저들은 default.css 파일만 파싱하며 ie7.css, ie6.css, ie5.css 파일에 대한 링크를 주석으로 처리한다.

CSS Hack 활용하기

권장하지는 않지만 CSS Hack을 사용하는 방법도 있다. CSS Hack은 브라우저의 버그를 이용하여 문제를 해결하는 방법이다. 버전별로 CSS 파일을 각각 작성하지 않아도 간편하게 낡은 브라우저에 대응할 수 있지만 CSS 문법 규격에 맞지 않는 것이 흠이다. 앞쪽에 선언된 property:value 값은 표준 계열 브라우저(IE8, 파이어폭스, 오페라, 사파리, 크롬)에서 작용하고 뒤에 선언된 property:value 값은 IE 버전에 대응하며 앞에 선언된 속성과 값을 덮어 쓴다.

IE5 ~ IE7 대응‘*’Hack

IE5 ~ IE6 대응‘_’Hack

IE5 대응‘_ & /**/ ’Hack

IE5 대응 Hack의 경우 /**/ 주석 앞에 한 칸의 공백이 있음에 유의한다.


참고로 아직도 여전히,

한국의 IE 버전별 점유율(2009년 2월 Internet Trend 보고서 기준)에 따르면 10년 전에 출시된 IE6 브라우저의 점유율이 59.42%로 가장 높고 IE7이 38.81% 대의 점유율을 보이고 있다. 또한 기타 브라우저들의 점유율도 큰 차이를 보인다. 파이어폭스와 사파리, 오페라, 크롬 브라우저의 지구촌 점유율이 통틀어 32.58%나 되는데 비하여 한국에서는 고작 1.4%에 그치고 있다.

고 한다. 파폭을 쓰는 사람은 개발자들이 다인 것 같다.


"Development Story" 카테고리의 다른 글

2009/06/09 10:55 2009/06/09 10:55
 
Bookmark and Share

싱글턴 패턴(Singleton Pattern) - for Beginner


사용자 삽입 이미지
이 문서는 GoF(Gang of Four) Design Patterns 에 정의된 패턴 목록 중 싱글턴 패턴(Singleton Pattern)을 다시 정리하면서 내용을 요약한 것이다. 개인적으로 자바와 닷넷 양진영에 모두 경험이 있다보니 동일 패턴에 대해서 상호 비교해보는 것이 어떨까 하는 생각이 들었다. 그래서 간략하지만 Java와 C# 양쪽에 걸쳐 내용을 작성하였으며, 소스코드 템플릿 또한 *.java, *.cs로 나누어 예를 제시하였다. 어쩌면 이 코드들 때문에 내용이 조금 더 복잡해 보일지도 모르겠다.


싱글턴 패턴의 개요

GoF의 23가지 디자인 패턴 중 개발자에게 가장 익숙한 패턴의 하나가 바로 '싱글턴 패턴(Singleton Pattern)'일 것이다. 싱글턴 패턴은 해당 클래스의 인스턴스(instance)가 하나만 만들어지고, 어디서든지 그 유일한 인스턴스에 접근할 수 있도록 하기 위한 패턴의로 정의된다.

GoF에 기술된 내용 중 싱글턴 패턴을 활용할 수 있는 상황은 다음과 같다.

  • 클래스의 인스턴스가 오직 하나여야 함을 보장하고, 잘 정의된 접근 방식에 의해 모든 클라이언트가 접근할 수 있도록 해야 할 때.
  • 유일하게 존재하는 인스턴스가 상속에 의해 확장되어야 할 때, 클라인트는 코드의 수정 없이 확장된 서브클래스의 인스턴스를 사용할 수 있어야 할 때.

이를테면 쓰레드 풀, 캐시, 대화상자, 사용자 설정이라든가 레지스트리 설정을 처리하는 객체, 로그 기록용 객체, 프린터나 그래픽 카드 같은 디바이스를 위한 디바이스 드라이버 같은 것들이 좋은 예가 될 것이다.

싱글턴의 기본적인 구조(Structure)는 그림과 같다.

사용자 삽입 이미지

싱글턴 패턴의 구조

그리고 싱글턴 패턴을 구현하는 고전적인 자바 코드의 기본 템플릿은 아래와 같다.

[자바 코드 1]



아래는 동일한 형태의 C# 버전으로 된 코드이다.

[C# 코드 1]



이 코드에서 Singleton 클래스는 private 변수와 생성자를 갖고 있으며 클라이언트에서 인스턴스를 요청할 때까지 Singleton 객체의 생성을 지연(lazy instantiation)하고 있다.

그런데 위의 코드 형태는 주석에도 달려있듯이 멀티(다중)쓰레딩 환경에서의 잠재적 문제를 안고 있기 때문에 실전에 절대 사용하면 안된다. 두개 이상의 쓰레드가 인스턴스를 획득하기 위해 getInstance() 메서드(C#의 경우 Instance 속성(Property))에 진입하여 경합을 벌이는 과정에서 서로 다른 두개의 Singleton 인스턴스가 만들어지는 좋지 않은 상황이 발생할 여지가 있다.

멀티쓰레드 환경에서의 싱글턴(Multithreaded Singleton)

위에서 제기한 문제를 해결하기 위해서는 다음 세가지의 해법을 사용할 수 있다.

  1. 인스턴스를 필요할 때 생성하지 않고, 처음부터 인스턴스를 만들어 버린다. 다시 말해서 lazy instantiation을 포기하고 static 멤버필드를 사용항여 언과 동시에 초기화하는 것이다.  단, 인스턴스를 미리 만들어 버리게 되면, 특히 해당 인스턴스가 자원을 많이 차지하는 컴포넌트일 경우에는 시스템 리소스가 쓸데없이 낭비될 가능성이 있다.
  2. getInstance() 메서드(C#의 경우 Instance 속성)를 동기화시킨다. 단, 동기화시키고자할 때는 getInstance()의 속도가 그렇게 중요하지 않다고 판단될 경우이며 동기화로 인한 오버헤드를 감수해야 한다. - 메서드를 동기화 시키면 일반적으로 성능이 100배 정도는 저하된다고 한다.
  3. DCL(Double-checked Locking) 기법을 사용한다. 단, 자바의 경우 DCL은 자바 5 버전 이상의 JVM 환경에서 인스턴스 변수에 volatile 키워드를 사용해야만 한다. voatile 키워드는 멀티쓰레드 환경에서도 uniqueInstance 변수가 원자성을 유지하도록 하여 올바른 싱글턴 인스턴스의 초기화가 진행되도록 한다(The volatile keyword in Java를 참고하라). 하지만 자바 1.4 및 그 이전에 나온 JVM에서는 메모리 모델의 문제로 제대로 동작하지 않는다는 것에 주의해야 한다(자세한 내용은 The "Double-Checked Locking is Broken" Declaration 참고하라).

설명보다는 코드를 보고 이해하는 것이 빠를 것 같다. 각 해법을 적용하여 멀티쓰레드 환경에서 제대로 동작(thread-safe)하는 싱글턴 구현의 예제 코드들이 아래에 있다.

1. 처음부터 인스턴스를 생성하는 예제 코드

[자바 코드 2]



[C# 코드 2]



2. 동기화 예제 코드

[자바 코드 3]



[C# 코드 3]



3. DCL(Double-checked Locking) 예제 코드

[자바 코드 4]



[C# 코드 4]



아래 C# 코드는 위 코드와 동일하게 DCL을 사용하지만 volatile을 사용하는 다른 버전의 예제이다.

[C# 코드 5]


싱글턴 레지스트리(Singleton Registry)

서두에서 "유일하게 존재하는 인스턴스가 상속에 의해 확장되어야 할 때, 클라인트는 코드의 수정 없이 확장된 서브클래스의 인스턴스를 사용할 수 있어야 할 때" 싱글턴을 활용한다고 하였다. 이때에는 서브클래스를 만드는 것이 중요한 게 아니라, 이 새로운 서브클래스의 유일한 인스턴스를 만들어 클라이언트가 이를 사용할 수 있도록 하는 것이 관건이다.

싱글턴의 서브클래스를 만들 때 가장 유연한 방법은 싱글턴에 대한 레지스트리를 사용하는 것이다. 아래 자바 예제 코드는 레지스트리를 갖고 있는 싱글턴으로 특정 클래스 객체의 인스턴스를 생성하기 위해서 리플렉션을 사용하고 있다. 'classname'은 Singleton의 서브클래스 이름이다. 이렇게 하면 서브클래스의 선택에 있어서 런타임에 싱글톤을 결정하는 유연성을 가질 수 있다(자세한 내용은 Simply Singleton을 참고하라).

[자바 코드 5]


결론

이상으로 멀티쓰레딩 환경에서의 싱글턴 패턴 구현 코드를 들여다 보았다. 그렇다면 이 세가지 중 어떤 코드 템플릿을 사용하는 것이 좋을까?

자바에서는 Double-checked locking과 Singleton 패턴 등 (조금 오래되긴 했지만) DCL과 관련한 문서들을 참고해보면 멀티쓰레드 환경에서 제대로 동작하는 싱글턴을 만들기 위한 최상의 솔루션은 동기화를 수락하거나 static 멤버필드를 사용하는 것을 권장하고 있다. 닷넷의 경우 The Correct Double Checked-Lock Pattern Implementation를 보면 [C# 코드 2]와 같은 형태의 코드를 사용할 것을 권장하고 있다.

싱글턴 구현에 있어서 반드시 DCL을 사용해야 하는 특별한 경우가 아니라면 대부분의 상황에서는 static 변수를 사용하거나 동기화 블럭을 사용하는 것으로도 충분할 것 같다. 성능의 저하는 다소 존재하겠지만 다양한 java  및 .net 버전과 메모리 모델에 종속적이지 않은 싱글턴을 구현하는 잇점도 있다고 생각한다. DCL을 적용해야한다면 특히 자바의 경우 volatile 키워드와 함께 반드시 자바 5 버전 이상을 사용해야 한다는 것을 잊지 말아야 한다.

마지막으로 참고가 될만한 두가지 사항을 덧붙이며 싱글턴 패턴에 대한 요약을 마무리한다.

싱글턴 패턴 사용 시 주의할 점(Java 기준)

  • 중복되는 얘기지만 DCL을 사용하려면 자바 5(1.5) 이후 버전을 사용해야 한다.
  • 클래스 로더가 여러개 있으면 싱글턴이 제대로 작동하지 않고, 여러 개의 인스턴스가 생길 수 있다. 이 경우 클래스 로더를 직접 지정해서 사용해야한다.
  • 개인적으로 최근 프로젝트 환경을 보면 슬슬 자바 5 버전으로 많이 갈아타고 있는 듯 하다. 정말 오래된 시스템을 유지 보수하는 경우가 아니라면 자바 1.2 이전 버전을 사용할 일은 없겠지만, 혹시라도 자바 1.2 이전 버전의 환경에서 작업한다면 JVM의 가비지 컬렉터 관련 버그 때문에 싱글턴 레지스트리를 사용해야할 수도 있다.

아래 코드는 클래스 로더를 직접 지정하는 예제이다. 이 코드는 Class.forName() 메서드를 대체할 수 있다(자세한 내용은 Simply Singleton을 참고하라).

[자바 코드 6]

정적 클래스 변수(메서드) vs. 싱글턴 패턴

굳이 싱글턴 패턴을 사용할 필요없이 전역 클래스 변수(static 멤버필드)를 사용하면 되지 않을까 하는 의문이 들 수도 있다. lazy instantiation을 구현하는 싱글턴 패턴에 비해서 전역 변수를 사용하는 경우 다음과 같은 단점들이 있을 수 있다.

  • 싱글턴 패턴은 static 인스턴스를 미리 생성해놓는 경우를 제외하고는 객체가 필요한 상황이 되었을 때에 비로소 인스턴스를 생성한다. 반면 전역 변수를 사용하면 대부분의 경우는 어플리케이션을 시작할 때 미리 객체가 생성한다. 그런데 그 객체가 자원을 많이 차지하고, 실제로 어플리케이션을 종료할 때까지 한번도 쓰지 않게된다면 괜한 자원만 낭비하는 꼴이 되고만다(이러한 상황은 시스템 플랫폼에 따라 달라질 수도 있다. 어떤 JVM은 객체를 나중에 필요할 때 생성하기도 한다고 한다).
  • 전역 변수를 사용하다 보면 간단한 객체에 대한 전역 레퍼런스를 자꾸 만들게 되면서 네임스페이스를 지저분하게 만드는 경향이 생긴다. 물론 싱글턴도 남용될 수 있지만, 네임스페이스가 지저분해지게 되는 것을 부추길 정도는 아니다.

참고 자료

아래는 이 포스트를 작성하기 위해 참고한 도서와 관련 사이트의 목록이다. 영어가 짧고 스크롤의 압박이 심하다보니 사이트의 글들을 죄다 꼼꼼하게 읽어보지는 못했다. ^^; 하지만 정독해보면 분명 도움이 될 내용들이라고 장담한다. ^^

Books

  • GOF의 디자인 패턴, 피어슨에듀케이션 코리아
  • Head First Design Patterns, 한빛미디어
  • 예제로 배우는 C# 디자인 패턴, 정보문화사 (비추. 자바와 닷넷 관련한 더 좋은 패턴 책들이 많이 있다.)
  • J2EE 패턴 (GoF & J2EE), SUN SL-500
  • The Design Patterns Java Companion

Terms

Articles


P.S. 내용 중 잘못된 부분이 있다면 지적 바랍니다. ^^


"Development Story" 카테고리의 다른 글

2008/11/27 10:09 2008/11/27 10:09
 
Bookmark and Share

Apache iBATIS Abator (iBATOR)

2008/07/22 17:21
사용자 삽입 이미지
지난 프로젝트에서 iBatis를 사용하면 어베이터(Abator 1.0.0-238)라는 코드 생성기를 함께 활용했었다(abator는 얼마전에 공식 명칭이 iBATOR로 변경되었다).

iBatis용 코드 생성기는 abator 말고도 몇가지가 더 있는데, 이왕이면 iBatis에서 권장하는 툴을 사용하는 것이 좋지 않겠나 하는 생각이 있었고 abator를 제외한 툴들은 더이상 업데이트가 안되고 있었는 것도 abator를 선택한 이유였다.

관련 코드를 자동으로 생성해주는 것 까진 좋았는데, 필드 및 메서드 마다 붙어버리는 abator 관련 주석들이 코드를 지저분하게 만들고 가독성을 떨어뜨리는 단점이 있다. 그리고 기본적인 CRUD를 제외한 쿼리들과 Stored Procedure의 경우는 어쩔 수 없이 수동으로 편집을 해야했기 때문에, 프로젝트 말미엔 abator의 활용도가 많이 떨어지지 않았었나 싶다.

누가 abator를 사용함으로써 개발 효율이 높아졌느냐고 물어본다면, 직접 써보고 판단하라고 얘기하고 싶다. 아무튼 득실을 따진다면 사견이지만, 잃을 건 없다라는 것.

아래는 개발자들을 위해서 작성했던 간단한 소개 자료를 첨삭한 내용이다.


Abator

Abator는 iBATIS를 위한 코드 생성기이다. Abator는 데이터베이스의 테이블에 접근하기 위한 iBATIS 연관 코드들인 Object와 환경 설정 파일(configuration file)을 생성하며, 기본적인 CRUD(Create, Retrieve, Update, Delete) 쿼리를 포함한다.

Abator 설치를 위한 특별한 의존성은 없지만 다음 조건을 만족해야 한다.

  • JRE 1.4 이상.
  • DatabaseMetaData 인터페이스를 구현하는 JDBC 드라이버가 필요하다.

Abator가 생성하는 코드(파일) 목록은 다음과 같다.

  • 테이블 구조에 매치되는 자바 POJOs
  • iBATIS 호환 SQL Map XML 파일들
  • DAO 인터페이스와 구현 클래스들


Abator 사용

1. 설정 파일 작성 - abatorConfig.xml

적절한 설정 파일을 생성한다. 설정은 적어도 다음 요소들을 포함하여야 한다(Abator 사이트 참고).

  • A <jdbcConnection> element to specify how to connect to the target database
  • A <javaModelGenerator> element to specify target package and target project for generated Java model objects
  • A <sqlMapGenerator> element to specify target package and target project for generated SQL map files
  • (Optionally) A <daoGenerator> element to specify target package and target project for generated DAO interfaces and classes (you can omit the <daoGenerator> element if you don't wish to generate DAOs)
  • At least one database <table> element

다음은 abatorConfig.xml의 예제이다.

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE abatorConfiguration
  PUBLIC "-//Apache Software Foundation//DTD Abator for iBATIS Configuration 1.0//EN"
  "http://ibatis.apache.org/dtd/abator-config_1_0.dtd">
 
<!-- ===================================================================== -->
<!-- Abator Config File                                                    -->
<!-- XXX Project, Kyungseo.Park@gmail.com, 2007.10                         -->
<!-- ===================================================================== -->
 
<abatorConfiguration>
  <abatorContext id="SybaseTables" generatorSet="Java5">
 
    <!-- ================================================================= -->
    <!-- jdbcConnection                                                    -->
    <!-- ================================================================= -->
 
   
<jdbcConnection driverClass="com.sybase.jdbc3.jdbc.SybDriver"
        connectionURL="jdbc:sybase:Tds:192.168.1.5:3000/sybdb"
        userId="sybdb"
        password="sybdb">

      <classPathEntry location="jconn3.jar" />
    </jdbcConnection>
 
   
<!--
    <jdbcConnection driverClass="oracle.jdbc.driver.OracleDriver"
        connectionURL="jdbc:oracle:thin:@129.110.30.30:1521:oradb"
        userId="oradb"
        password="oradb">

      <classPathEntry location="classes12.jar" />
    </jdbcConnection>
    -->
 
    <!-- ================================================================= -->
    <!-- javaTypeResolver                                                  -->
    <!-- ================================================================= -->
 
    <javaTypeResolver >
      <property name="forceBigDecimals" value="false" />
    </javaTypeResolver>
 
    <!-- ================================================================= -->
    <!-- javaModelGenerator                                                -->
    <!-- ================================================================= -->
 
   
<javaModelGenerator targetPackage="xxx.business.sys.domain"
targetProject="XxxProject\src">

      <property name="enableSubPackages" value="true" />
      <property name="trimStrings" value="true" />
      <property name="rootClass" value="xxx.framework.base.BaseObject" />
    </javaModelGenerator>
 
    <!-- ================================================================= -->
    <!-- sqlMapGenerator                                                   -->
    <!-- ================================================================= -->
 
   
<sqlMapGenerator targetPackage="xxx.resources.sql.sys"
targetProject="XxxProject\src">

      <property name="enableSubPackages" value="true" />
    </sqlMapGenerator>
 
    <!-- ================================================================= -->
    <!-- daoGenerator                                                      -->
    <!-- ================================================================= -->
 
   
<daoGenerator type="IBATIS" targetPackage="xxx.business.sys.dao"
targetProject="XxxProject\src">

      <property name="enableSubPackages" value="true" />
      <property name="rootInterface" value="xxx.framework.base.BaseDao" />
    </daoGenerator>
 
    <!-- ================================================================= -->
    <!-- table                                                             -->
    <!-- ================================================================= -->
 
    <table tableName="TM_XXX_ROLE" domainObjectName="Role" >
      <property name="useActualColumnNames" value="true"/>
    </table>
 
  </abatorContext>
</abatorConfiguration>


2. Abator 실행(Run)

커맨드 라인을 열고 다음과 같이 입력하여 Abator를 실행할 수 있다. 단, 설정 파일에 정의된 targetProject의 대상 디렉토리는 반드시 미리 존재해야 한다.

java -jar abator.jar abatorConfig.xml false
java -jar abator.jar abatorConfig.xml true
java -cp abator.jar org.apache.ibatis.abator.api.AbatorRunner abatorConfig.xml false
java -cp abator.jar org.apache.ibatis.abator.api.AbatorRunner abatorConfig.xml true

여기서 파라미터의 의미는 다음과 같다.

  • 첫번째 파라미터: 환경 파일의 경로
  • 두번째 파라미터: 파일이 이미 존재할 경우 overwrite 여부

abator 실행을 위한 간단한 배치 파일을 만들어 놓으면 편하다.  예를 들어 “abator.bat” 파일을 만들어 다음과 같이 편집한다.

java -jar abator.jar abatorConfig.xml true

3. Abator 실행 후 작업

다음과 같은 마무리 작업이 필요하다.

  • SqlMapConfig.xml 파일을 생성하거나 편집한다.
  • iBATIS DAO Framework를 사용할 경우, dao.xml 파일을 생성하거나 편집한다.


Abator로 생성된 코드 예제

1. Java Model Classes

Domain Class

package xxx.business.sys.domain;
 
import xxx.framework.base.BaseObject;
import java.util.Date;
 
public class Role extends BaseObject {
   
/**
     * This field was generated by Abator for iBATIS.
     * This field corresponds to the database column TM_XXX_ROLE.role_nmbr
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007.
     */

    private
Integer role_nmbr;
 
   
/**
     * This field was generated by Abator for iBATIS.
     * This field corresponds to the database column TM_XXX_ROLE.role_nm
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007.
     */

    private
String role_nm;
 
    ...
    // member field 들에 대한 setter 및 getter
    ...
 
}


Domain Example Classes

package xxx.business.sys.domain;
 
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
 
public class RoleExample {
   
/**
     * This field was generated by Abator for iBATIS.
     * This field corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007.
     */

    private
String orderByClause;
 
   
/**
     * This field was generated by Abator for iBATIS.
     * This field corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007.
     */

    private List<Criteria> oredCriteria = new ArrayList<Criteria>();
 
   
/**
     * This method was generated by Abator for iBATIS.
     * This method corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007.
     */

    public void setOrderByClause(
String orderByClause) {
        this.orderByClause = orderByClause;
    }
 
   
/**
     * This method was generated by Abator for iBATIS.
     * This method corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007.
     */

    public
String getOrderByClause() {
        return orderByClause;
    }
 
   
/**
     * This method was generated by Abator for iBATIS.
     * This method corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007.
     */

    public List<Criteria> getOredCriteria() {
        return oredCriteria;
    }
 
    ...
 
}


2. SQL Map Files

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN" "http://ibatis.apache.org/dtd/sql-map-2.dtd" >
<sqlMap namespace="TM_XXX_ROLE" >
  <resultMap id="abatorgenerated_RoleResult" class="xxx.business.sys.domain.Role" >
   
<!--
      WARNING - This element is automatically generated by Abator for iBATIS, do not modify.
      This element was generated on Wed Nov 28 17:31:32 KST 2007.
    -->

    <result column="role_nmbr" property="role_nmbr" jdbcType="INTEGER" />
    <result column="role_nm" property="role_nm" jdbcType="VARCHAR" />
    <result column="rmrk" property="rmrk" jdbcType="VARCHAR" />
    <result column="sys_mngm_ysno" property="sys_mngm_ysno" jdbcType="CHAR" />
    <result column="rgsr" property="rgsr" jdbcType="CHAR" />
    <result column="rgsr_dttm" property="rgsr_dttm" jdbcType="TIMESTAMP" />
    <result column="amnd" property="amnd" jdbcType="CHAR" />
    <result column="amnd_dttm" property="amnd_dttm" jdbcType="TIMESTAMP" />
  </resultMap>
  <sql id="abatorgenerated_Example_Where_Clause" >
   
<!--
      WARNING - This element is automatically generated by Abator for iBATIS, do not modify.
      This element was generated on Wed Nov 28 17:31:32 KST 2007.
    -->

    <iterate property="oredCriteria" conjunction="or" prepend="where" removeFirstPrepend="iterate" >
      (
      <iterate prepend="and" property="oredCriteria[].criteriaWithoutValue" conjunction="and" >
        $oredCriteria[].criteriaWithoutValue[]$
      </iterate>
      <iterate prepend="and" property="oredCriteria[].criteriaWithSingleValue" conjunction="and" >
        $oredCriteria[].criteriaWithSingleValue[].condition$
          #oredCriteria[].criteriaWithSingleValue[].value#
      </iterate>
      <iterate prepend="and" property="oredCriteria[].criteriaWithListValue" conjunction="and" >
        $oredCriteria[].criteriaWithListValue[].condition$
        <iterate property="oredCriteria[].criteriaWithListValue[].values" open="(" close=")" conjunction="," >
          #oredCriteria[].criteriaWithListValue[].values[]#
        </iterate>
      </iterate>
      <iterate prepend="and" property="oredCriteria[].criteriaWithBetweenValue" conjunction="and" >
        $oredCriteria[].criteriaWithBetweenValue[].condition$
        #oredCriteria[].criteriaWithBetweenValue[].values[0]# and
        #oredCriteria[].criteriaWithBetweenValue[].values[1]#
      </iterate>
      )
    </iterate>
  </sql>
  <select id="abatorgenerated_selectByPrimaryKey" resultMap="abatorgenerated_RoleResult" parameterClass="xxx.business.sys.domain.Role" >
   
<!--
      WARNING - This element is automatically generated by Abator for iBATIS, do not modify.
      This element was generated on Wed Nov 28 17:31:32 KST 2007.
    -->

    select role_nmbr, role_nm, rmrk, sys_mngm_ysno, rgsr, rgsr_dttm, amnd, amnd_dttm
    from TM_XXX_ROLE
    where role_nmbr = #role_nmbr:INTEGER#
  </select>
  <select id="abatorgenerated_selectByExample" resultMap="abatorgenerated_RoleResult" parameterClass="xxx.business.sys.domain.RoleExample" >
   
<!--
      WARNING - This element is automatically generated by Abator for iBATIS, do not modify.
      This element was generated on Wed Nov 28 17:31:32 KST 2007.
    -->

    select role_nmbr, role_nm, rmrk, sys_mngm_ysno, rgsr, rgsr_dttm, amnd, amnd_dttm
    from TM_XXX_ROLE
    <isParameterPresent >
      <include refid="TM_XXX_ROLE.abatorgenerated_Example_Where_Clause" />
      <isNotNull property="orderByClause" >
        order by $orderByClause$
      </isNotNull>
    </isParameterPresent>
  </select>
  <delete id="abatorgenerated_deleteByPrimaryKey" parameterClass="xxx.business.sys.domain.Role" >
   
<!--
      WARNING - This element is automatically generated by Abator for iBATIS, do not modify.
      This element was generated on Wed Nov 28 17:31:32 KST 2007.
    -->

    delete from TM_XXX_ROLE
    where role_nmbr = #role_nmbr:INTEGER#
  </delete>
  <delete id="abatorgenerated_deleteByExample" parameterClass="xxx.business.sys.domain.RoleExample" >
   
<!--
      WARNING - This element is automatically generated by Abator for iBATIS, do not modify.
      This element was generated on Wed Nov 28 17:31:32 KST 2007.
    -->

    delete from TM_XXX_ROLE
    <include refid="TM_XXX_ROLE.abatorgenerated_Example_Where_Clause" />
  </delete>
  <insert id="abatorgenerated_insert" parameterClass="xxx.business.sys.domain.Role" >
   
<!--
      WARNING - This element is automatically generated by Abator for iBATIS, do not modify.
      This element was generated Wed Nov 28 17:31:32 KST 2007.
    -->

    insert into TM_XXX_ROLE (role_nmbr, role_nm, rmrk, sys_mngm_ysno, rgsr, rgsr_dttm, amnd,
      amnd_dttm)
    values (#role_nmbr:INTEGER#, #role_nm:VARCHAR#, #rmrk:VARCHAR#, #sys_mngm_ysno:CHAR#,
      #rgsr:CHAR#, #rgsr_dttm:TIMESTAMP#, #amnd:CHAR#, #amnd_dttm:TIMESTAMP#)
  </insert>
  <update id="abatorgenerated_updateByPrimaryKey" parameterClass="xxx.business.sys.domain.Role" >
   
<!--
      WARNING - This element is automatically generated by Abator for iBATIS, do not modify.
      This element was generated on Wed Nov 28 17:31:32 KST 2007.
    -->

    update TM_XXX_ROLE
    set role_nm = #role_nm:VARCHAR#,
      rmrk = #rmrk:VARCHAR#,
      sys_mngm_ysno = #sys_mngm_ysno:CHAR#,
      rgsr = #rgsr:CHAR#,
      rgsr_dttm = #rgsr_dttm:TIMESTAMP#,
      amnd = #amnd:CHAR#,
      amnd_dttm = #amnd_dttm:TIMESTAMP#
    where role_nmbr = #role_nmbr:INTEGER#
  </update>
  <update id="abatorgenerated_updateByPrimaryKeySelective" parameterClass="xxx.business.sys.domain.Role" >
   
<!--
      WARNING - This element is automatically generated by Abator for iBATIS, do not modify.
      This element was generated on Wed Nov 28 17:31:32 KST 2007.
    -->

    update TM_XXX_ROLE
    <dynamic prepend="set" >
      <isNotNull prepend="," property="role_nm" >
        role_nm = #role_nm:VARCHAR#
      </isNotNull>
      <isNotNull prepend="," property="rmrk" >
        rmrk = #rmrk:VARCHAR#
      </isNotNull>
      <isNotNull prepend="," property="sys_mngm_ysno" >
        sys_mngm_ysno = #sys_mngm_ysno:CHAR#
      </isNotNull>
      <isNotNull prepend="," property="rgsr" >
        rgsr = #rgsr:CHAR#
      </isNotNull>
      <isNotNull prepend="," property="rgsr_dttm" >
        rgsr_dttm = #rgsr_dttm:TIMESTAMP#
      </isNotNull>
      <isNotNull prepend="," property="amnd" >
        amnd = #amnd:CHAR#
      </isNotNull>
      <isNotNull prepend="," property="amnd_dttm" >
        amnd_dttm = #amnd_dttm:TIMESTAMP#
      </isNotNull>
    </dynamic>
    where role_nmbr = #role_nmbr#
  </update>
</sqlMap>


3. Java DAO Classes(optional)

DAO Interface

package xxx.business.sys.dao;
 
import xxx.business.sys.domain.Role;
import xxx.business.sys.domain.RoleExample;
import xxx.framework.base.BaseDao;
import java.util.List;
 
public interface RoleDAO extends BaseDao {
   
/**
     * This method was generated by Abator for iBATIS.
     * This method corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007
     */

    void insert(Role record);
 
   
/**
     * This method was generated by Abator for iBATIS.
     * This method corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007
     */

    int updateByPrimaryKey(Role record);
 
   
/**
     * This method was generated by Abator for iBATIS.
     * This method corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Thu Dec 13 16:43:17 KST 2007
     */

    int updateByPrimaryKeySelective(Role record);
 
    ...
 
}


DAO Implement

package xxx.business.sys.dao;
 
import xxx.business.sys.domain.Role;
import xxx.business.sys.domain.RoleExample;
import com.ibatis.dao.client.DaoManager;
import com.ibatis.dao.client.template.SqlMapDaoTemplate;
import java.util.List;
 
public class RoleDAOImpl extends SqlMapDaoTemplate implements RoleDAO {
 
   
/**
     * This method was generated by Abator for iBATIS.
     * This method corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007
     */

    public RoleDAOImpl(DaoManager daoManager) {
        super(daoManager);
    }
 
   
/**
     * This method was generated by Abator for iBATIS.
     * This method corresponds to the database table TM_XXX_ROLE
     *
     * @abatorgenerated Wed Nov 28 17:31:32 KST 2007
     */

    public void insert(Role record) {
        insert("TM_XXX_ROLE.abatorgenerated_insert", record);
    }
 
    ...
 
}



참고 사이트

"Development Story" 카테고리의 다른 글

2008/07/22 17:21 2008/07/22 17:21
 
Bookmark and Share

문제의 발생 - Memory Leak

사용자 삽입 이미지
지난 프로젝트 말미에 발생한 이슈에 대해서 정리했던 내용을 잠시 언급하고자 한다. 위키에 정리했던것을 다소 첨삭하였다.

시스템의 계층화된 아키턱처의 구조로 볼 때, Persistence Layer (또는 Data Access Layer)의 경우 "iBatis + iBatis DAO"를 사용하여 구현되었는데, 어느 시점에서 간간이 예의 OOM(OutOfMemory) 에러가 발생했다. 런타임이 지속될수록 어플리케이션의 응답속도가 현저하게 떨어지는 증상이 나타났는데, 어딘가에  Memory Leak(메모리 누수)가 있음이 분명했다.

참고로 iBatis의 정확한 버전은 2.1.6 build# 592 이다. 개발할 당시 최신 버전은 2.3.0 이었는데, 좀더 검증되고 레퍼런스가 많은 버전을 선택했었다.

우선 문제가 발생한 시점을 전후로 변경된 코드 내역들에 대해서 검토하고, 각 계층(특히 Service Layer와 Persistence Layer)에 있어서 base code(class)의 기본적인 프로세스(Life Cycle)를 다시 리뷰하면서 문제 코드에 대한 범위를 좁혀갔다.

다행히 손쉽게 심증이 가는 부분을 찾을 수 있었지만 정확한 원인(물증 :)을 파악할 필요가 있었기 때문에, 기본적으로 GC 로그를 모니터링하고 APM(Application Performance Management)Profiler 툴을 활용하여 문제 영역에 접근해보기로 했다. 사실, 문제 해결보다는 제니퍼를 사용해본 경험이 전무하여 툴을 활용해보고 싶은 마음이 더 강했는지도 모르겠다. ^^; 아무튼 JenniferJProfiler 평가판을 다운로드하였다. 제니퍼는 설치 및 사용에 대한 문서가 간략하고 잘 정리되어 있어 쉽게 설치할 수 있었다.

  • APM: Jennifer 3.2 (2주 평가판)
  • Profiler: JProfiler 5.1 (10일 평가판)


Test 및 결과 요약

테스트 프로그램들을 작성하고 언급한 툴들을 활용하여 결과를 모니터링하였다. 프로그램은 쓰레드로 작성되었으며, 시작 버튼을 누르면 종료 버튼을 누르기 전까지 무한히 서비스를 호출하도록 하였다. 테스트는 개발 환경과 동일한 구성을 갖고 있는 1G 메모리(RAM)의 내 극빈한 M1 노트북에서 실행하였다.

자세한 상황과 과정을 모두 설명하기는 힘들지만, 대강 결론부터 얘기하자면 BaseService 에서 해당 작업을 종료할 때 호출하는 logging 메서드(작업에 대한 사용자 행위 추적을 구현) 부분에서부터 문제가 시작되었다.
 
BaseService에 대해 부연하자면, 모든 business logic을 구현하는 서비스들은 BaseServcie 클래스를 상속하도록 되어 있다. Controller와 유사한 역할을 한다고 봐도 무방할 듯 하다. UI(JNLP 클라이언트)에서 사용자의 액션 및 이벤트 등에 의해 발생하는 모든 서버 단 호출(Service Call)의 경우 호출 종료와 동시에 BaseServcie에 의해 logging 메서드가 호출된다. 하나의 이벤트에 의해 호출되는 서비스가 하나 이상이 될 수 있으므로 이 메서드는 호출 빈도수가 매우 높은 편에 속한다.

테스트 프로그램을 시작하면 BaseServcie 를 상속하는 서비스를 호출하게 되고 동시에 계속해서 logging 메서드가 호출되게 된다.

logging 메서드 호출부를 포함한 상태에서 테스트 프로그램을 실행하고 제니퍼로 모니터 하였다. 아래 그림은 문제 상황에서 제니퍼를 사용하여 메모리의 추이를 지켜본 것이다.

사용자 삽입 이미지

heap 사이즈가 지속적으로 증가하고 있다. 우측에서 갑자기 떨어지는 부분은 종료 버튼을 눌러 테스트 프로그램의 쓰레드를 중단하던 순간인데 이것은 무시해도 좋을 것 같다. 아무튼 상황이 심각해서 쓰레드를 종료하지 않으면 계속 프로그램을 실행하면 나중에는 노트북이 먹통(CPU 100%)이 될 정도록 버벅거렸다.

참고로 로컬에서 돌아가는 WAS(JEUS)의 VM 옵션은 다음과 같다.

<command-option>
 -Xms512m -Xmx512m -XX:NewSize=128m -XX:MaxNewSize=128m -XX:PermSize=128m -XX:MaxPermSize=128m
 -verbose:gc -XX:+PrintGCDetails -XX:+PrintGCTimeStamps -Xloggc:D:\gc.log
 -server
</command-option>

다음 그림은 위 그림과 동일한 시점에서의 GC로그 콘솔을 캡처한 화면이다.

사용자 삽입 이미지

minor GC가 꾸준히 발생하다가 Full GC가 지속적으로 발생하였다. 화면의 경우 Full GC가 발생한 직후 캡처한 것인데, 최초 Full GC의 경우 14초 이상 소요되는 것을 확인할 수 있다.

Full GC의 경우 속도가 느리고 GC가 일어나는 과정에서 thread가 중지되어 어플리케이션이 멈춰 버리기 때문에 성능과 안정성에 큰 영향을 준다. 위와 같이 Full GC가 지속적으로 발생되는 것만으로도 결국 그만큼 프로그램의 응답 속도가 현저하게 느려지는 결과를 초래하였다. 이것이 시간이 지날 수록 어플리케이션이 느려지는 주요한 원인이 되었다.

다음으로 아래 그림은 테스트 프로그램에서 logging 메서드 호출부를 뺀 상태에서 테스트한 것이다.

사용자 삽입 이미지
 
다음은 동일한 조건과 시점에서의 GC 로그이다.

사용자 삽입 이미지

다소 안정되어 보인다.


원인 분석과 소스 수정


어플리케이션 실행 중 heap 상에 존재하는 객체들(실제 메모리의 Collection 및 Object들)을 대상으로 메모리 누수와 연관된 객체를 파악할 수 있다면 보다 확실하게 문제 코드에 접근할 수 있을텐데, 제니퍼 세팅이 잘못 되었는지 유일하게 Collelction에 대한 실시간 모니터링만 되지 않았다. 대신 JProfiler를 사용해서 확인하였는데, 아래 그림이 해당 화면을 캡처한 것이다.

사용자 삽입 이미지
 
그림으로는 잘 보이지않지만 java.lang.String 타입의 instance count가 1,496,688이고 char[] 배열 타입은 1,423,882개로 나타난니다. 이 둘(문자열)을 합친 메모리만 200M를 웃돌고 있다.

여기서 다시 메소드를 조사해보았다.

사용자 삽입 이미지
 
선택된 부분을 확대해서 보면 다음과 같다.

사용자 삽입 이미지
 
트리에서 선택된 노드는 LoggingService2.logging 메서드이며, 이것은 BaseService의 logging에 의해 호출된다. LoggingService2.logging의 두 단계 아래 노드를 보면 "DaoManagerBuilder.buildDaoManager" 메서드를 확인할 수 있다.

LoggingService2에는 DB에 로그 데이터를 기록하기 위해 DaoCofig로부터 DaoManager를 가져오는 부분을 필수로 포함하고 있다.

DaoManager daoManager = DaoConfig.newDaoManager(DaoResource.JobResName);

상단의 코드에 기술된 DaoConfignewDaoManager 등의 코드는 iBatis의 예제 코드인 JPetStore의 소스 중 동일한 이름의 클래스를 참고하였는데, 이 코드의 전체는 하단과 같다. 프로젝트에서 사용한 newDaoManager 메서드의 코드 블럭은 이 예제와 거의 동일하였다.

package com.ibatis.jpetstore.persistence;
 
import com.ibatis.common.jdbc.ScriptRunner;
import com.ibatis.common.resources.Resources;
import com.ibatis.dao.client.DaoManager;
import com.ibatis.dao.client.DaoManagerBuilder;
 
import java.io.Reader;
import java.sql.Connection;
import java.sql.DriverManager;
import java.util.Properties;
 
public class DaoConfig {
 
  private static final
String resource = "com/ibatis/jpetstore/persistence/dao.xml";
  private static final DaoManager daoManager;
 
  static {
    try {
      daoManager = newDaoManager(null);
     
Properties props = Resources.getResourceAsProperties("properties/database.properties");
     
String url = props.getProperty("url");
     
String driver = props.getProperty("driver");
     
String username = props.getProperty("username");
     
String password = props.getProperty("password");
      if (url.equals("jdbc:hsqldb:mem:jpetstore")) {
        Class.forName(driver).newInstance();
       
Connection conn = DriverManager.getConnection(url, username, password);
        try {
          ScriptRunner runner = new ScriptRunner(conn, false, false);
          runner.setErrorLogWriter(null);
          runner.setLogWriter(null);
          runner.runScript(Resources.getResourceAsReader("ddl/hsql/jpetstore-hsqldb-schema.sql"));
          runner.runScript(Resources.getResourceAsReader("ddl/hsql/jpetstore-hsqldb-dataload.sql"));
        } finally {
          conn.close();
        }
      }
    } catch (
Exception e) {
      throw new
RuntimeException("Description.  Cause: " + e, e);
    }
 
  }
 
  public static DaoManager getDaoManager() {
    return daoManager;
  }
 
  public static DaoManager newDaoManager(
Properties props) {
    try {
     
Reader reader = Resources.getResourceAsReader(resource);
      return DaoManagerBuilder.buildDaoManager(reader, props);
    } catch (
Exception e) {
      throw new
RuntimeException("Could not initialize DaoConfig.  Cause: " + e, e);
    }
  }
 
}


JPetStore 예제에서는 getDaoManager()를 사용하여 정적으로 초기화된 단 하나의 DaoManager를 사용했지만, 프로젝트에서는 서브 시스템별로 dao.xml을 정의하고 각 시스템별로 달리 DaoManager를 사용하였으므로 모든 서비스 코드에서는 위에 기술한 바와 같이 DaoConfig의 newDaoManager() 메서드를 통하여 daoManager 인스턴스를 초기화하여 사용하였다.

public class SomeService extends BaseService {
 private DaoManager daoManager;
 private SomeDAO someDao;

 public SomeService() {
  daoManager = DaoConfig.newDaoManager(DaoResource.JobResName);
  this.someDao = (SomeDAO) daoManager.getDao(SomeDAO.class);
 }
 ...
}

참고로 BaseService 유형의 클래스들은 Servlet이 Container에 의해 관리되듯이 단 하나의 인스턴스로 존재한다.

여기서 잠시 iBatis DAO와 관련된 내용에 대해 언급하자면, iBatis DAO 프레임워크를 초기화하기 위해서는 dao.xml 파일 경로를 파라미터로 넘겨주어 DaoManagerBuilder.buildDaoManager()를 호출해야한다. DAO 프레임워크는 이 메서드에서 해당 dao.xml 파일을 읽고 상응하는 DaoManager를 생성하게된다. client는 이 DaoManger를 사용하여 insert 등과 같은 제반 작업을 수행하게 된다. 아래 그림을 참고하면 된다.

사용자 삽입 이미지

DAO sequence diagram (그림 참고)

앞서 설명했듯이, 테스트 프로그램은 문제 상황을 빨리 발생시키기 위해서 무한하게 트랜잭션을 발생시키는 형태를 취했지만 logging 메서드 자체가 매우 빈번하게 호출되는 구조임을 전제로 하고 생각해 볼 때,

일반적으로 특정 메서드의 블럭을 벗어나게 되면 메서드 내에서 지역변수로 사용한 객체는 Garbage Collection의 대상이 대상이 된다. 하지만, 설명한 바와 같이 DaoManagerBuilder.buildDaoManager()에서 dao.xml 파일에 대한 I/O가 일어나고 읽어들인 파일을 parsing하고 DaoManager의 인스턴스를 생성하는 작업을 수행하게 되는데, 결과로 미루어 볼때 어느 정도 시간 간격을 둔 상태에서는 별 무리 없이 수행되었지만, logging과 같이 짧은 시간에 많은 수의 서비스를 수행과 동시에 호출되는 경우 GC가 제대로 되지 못하고 결국 감당을 하지 못하게 되는 듯 하다. 추측컨데, JProfiler에서 보여진 수많은 문자열 관련 객체들은 반복되는 I/O와 parsing 작업의 부산물이라 여겨졌다.

이것이 원인이 맞다면, DaoManager를 초기화 하기 위해 매번 DaoConfig의 newDaoManager를 호출함으로써 빈번한 buildDaoManager 작업을 발생시킬 수 있는 개연성을 갖는 것을 미연에 방지하기 위해 DaoConfig 자체를 수정하는 것이 좋을 듯 했다.

따라서 근본적인 원인을 제공한 DaoConfig의 소스를 아래 코드와 같이 수정한 후 다시 프로그램을 실행하였다. DaoConfig는 싱글톤(singleton)으로 변경하고 각 업무단의 DaoManager를 Hashtable을 사용하여 cache 하도록 수정하였다. 캐슁으로 인한 부작용(?!)은 없을까 잠시 생각해보았는데 dao.xml을 리로드하기 위해서는 어차피 WAS를 재시작할 필요가 있었다. SQL Map 파일의 경우 절대 경로를 사용하여 자동으로 다시 로드 되게끔 되어 있다(개발서버의 경우만 해당).

다음이 대강의 코드이다.

public class DaoConfig {
 
  private static DaoConfig instance = null;
  private static Hashtable<String, DaoManager> daoManagers = null;
 
  private DaoConfig() {
    daoManagers = new Hashtable<String, DaoManager>();
  }
 
  public static DaoConfig getInstance() {
    if (instance == null) {
      instance = new DaoConfig();
    }
    return instance;
  }
 
  public DaoManager getDaoManager(
String resource, Properties props) {
    DaoManager daoManager = null;
    if (daoManagers.containsKey(resource)) {
      daoManager = (DaoManager) daoManagers.get(resource);
    }
    else {
      try {
        Reader reader = Resources.getResourceAsReader(resource);
        daoManager = DaoManagerBuilder.buildDaoManager(reader, props);
        daoManagers.put(resource, daoManager);
      } catch (
Exception e) {
        throw new
RuntimeException("DaoConfig를 초기화할 수 없습니다. 원인: " + e, e);
      }
    }
 
    return daoManager;
  }
 
  /* 호환성을 위해 남겨둔 method들 */
  public static DaoManager newDaoManager(
String resource) {
    return newDaoManager(resource, null);
  }
 
  public static DaoManager newDaoManager(
String resource, Properties props) {
    DaoConfig config = DaoConfig.getInstance();
    return config.getDaoManager(resource, props);
  }
 
}

다시 프로그램을 실행하고 결과를 보여주는 제니퍼 화면을 캡처 하였다. 비교를 위해 이전의 화면들을 다시 붙였다. 수정된 코드가 반영된 결과 화면은 마지막 화면이다.

* 최초의 문제가 있던 상태

사용자 삽입 이미지

* logging 메서드를 호출하지 않은 상태

사용자 삽입 이미지

* DaoConfig를 수정한 이후 상태

사용자 삽입 이미지
 
훨씬 안정적인 상태로 변했다.
이 상태에서의 GC 로그는 다음과 같다.

사용자 삽입 이미지
 
결론적으로 DaoManager를 각각의 서비스 코드에서 매번 초기화하기 위해서 DaoConfig의 newDaoManager 메서드를 호출할 때, 빈번한 DaoManagerBuilder.buildDaoManager()가 호출되는 구조는 문제를 일으킬 여지가 있는 것 같다.

대강 정리를 마치고 내용을 한번 읽어보았는데, 정리가 제대로 된 것 같지는 않다. ^^;
아무튼 요는, 항상 기본에 충실하고 미리 생각하는 개발자가 되자! ^^


P.S. 잘못된 내용이 있다면 지적 바랍니다. ^^



참고

관련 API



 

"Development Story" 카테고리의 다른 글

2008/07/07 11:59 2008/07/07 11:59
 
Bookmark and Share