한동안 포스팅이 좀 뜸했었죠? 회사 솔루션에 대한 라이센스 정책을 만들고, 그에 대한 구현을 하느라, 그리고 외산 솔루션 하나 분석하느라 정신이 없네요. 가끔은 정말 제 자신이 둘 정도 있었으면 하는 바람입니다. ... 


그래서 말인데 오늘은 JDK7.0부터 새롭게 제안된 Fork/Join 방식에 대해 소개할까 합니다. 소개라기보다는 그냥 링크인데요, 그리 길지 않아 번역을 할까 하다가 갑작스레 귀차니즘이 발동하여.... 그렇게 됐습니다. 나중에 시간적 정신적 여유가 생기면 따로 번역해 보기로 하죠. 


Fork와 Join 태스크들간의 협력 관계랍니다.


구글에서 Fork Join Java로 검색을 하면 몇 가지 읽을만한 아티클들을 찾아볼 수 있습니다. 그 중 제가 봤을때 가장 내용이 충실했던 것은 바로, http://www.oracle.com/technetwork/articles/java/fork-join-422606.html 입니다. Fork/Join이 없던 시절의 프로그래밍 기법과 비교도 해 주고, 실제 소스 코드도 제공해주니 도움이 될 것입니다. 더욱 더 좋은 점은, 피보나치 수열과 같은 너무나도 간단해서 그닥 쓸모 없는 예제가 아니라 그럭저럭 실제로도 쓸만한 예제를 다루고 있다는 점이랄까요.


아 근데 제 Java Concurrent Programming 책은 어디 간 걸까요...


신고
Posted by Layered 트랙백 0 : 댓글 0

댓글을 달아 주세요

이번 포스트에서는 초보 프로그래머들이 다서 어려워하는 개념인 클로저(Closure)에 대한 내용을 다루도록 하겠습니다. 

사실 '자바스크립트 클로저'라고 검색을 하면 수많은 Q&A와 포스트들을 찾을 수 있는데요, 그 많은 글들을 읽어보아도 완벽하게 손에 쥘 듯 이해되지는 않는 듯 합니다. 저도 마찬가지였구요. 그 이유는 크게 두 가지인 것으로 생각됩니다. 하나는, 클로저와 객체의 스코프(영역, Scope)를 통합하여 이해하지 못하기 때문이고, 다른 하나는 클로저라는 것이 왜 클로저라고 불리는지 이해하지 못하기 때문입니다. 실제로 클로저를 능숙하게 사용한다는 자바스크립트 프로그래머들에게 클로저가 왜 클로저라고 불리느냐 물어보면 제대로 대답하지 못하는 경우가 많더군요. 

어쨌든, 이 글을 통해 클로저와 스코프에 대해 명확히 이해할 수 있기를 기대하며 시작해보도록 하겠습니다. 이 글의 원문 주소는  http://robertnyman.com/2008/10/09/explaining-javascript-scope-and-closures/ 입니다.



이 글에서는 많은 사람들이 어려워하는 개념에 대해 다룰까 합니다. 바로 자바스크립트의 유효 범위(scope)와 클로저(closure)에 대한 내용입니다.

 

1. 배경 (Background)

이미 매우 많은 기사나 블로그에서 자바스크립트의 유효범위와 클로저에 대해 다루었지만, 필자가 생각하기에 그들 대부분 명백하고 확실했던 것 같지는 않았습니다. 게다가 그 글들 중 상당수는 사람들이 적어도 15개 정도의 다른 프로그래밍 언어를 경험한 것으로 간주하는 것 같았습니다. 하지만 제 경험상 자바스크립트를 개발하는 대부분의 사람들은 CJava 같은 언어가 아니라 HTMLCSS를 먼저 접하고 개발을 시작했었습니다.

하여, 제 최종 목표는 유효범위와 클로저가 무엇인지, 어떻게 동작하는지, 그리고 어떤 점이 좋은지에 대해 모든 사람들이 명확하게 이해할 수 있도록 돕는 것입니다. 물론, 이 글을 읽기 전에 변수(variables)나 함수(functions)의 개념에 대한 기본적인 이해는 필요할 것입니다.

 

2. 유효범위 (Scope)

유효범위라 함은, 변수나 함수가 접근 가능한 영역, 혹은 어떤 맥락(context)이 실행되는 곳을 의미합니다.  기본적으로 변수나 함수는 전역(global scope)이나 지역(local scope)적으로 선언될 수 있습니다. 변수들은 소위 함수영역(function scope)라고 불리는 유효범위를 갖고 있고, 함수들도 변수로서 동일한 유효범위를 갖고 있습니다.


- 전역 범위(Global Scope)

 무엇이든 전역적이다 라고 말한다면, 그것이 코드의 어느 곳에서든 접근 가능하다는 것을 의미합니다. 다음 예제를 확인해 보시기 바랍니다.


var monkey = "Gorilla";

function greetVisitor () {
	return alert("Hello dear blog reader!");
}

 

이 코드가 브라우저 내에서 실행된다면, 이 함수의 유효범위는 window 객체가 될 것입니다. 그러므로 해당 브라우저에서 실행되는 모든 코드에서 접근 가능할 것입니다.


- 지역 범위(Local Scope)

 전역 범위와는 다르게 지역 범위라고 하면, 함수와 같이 코드의 어떤 부분에서만 접근이 가능하게 정의되었다는 것을 의미합니다. 예를 들어 봅시다;


function talkDirty () {
	var saying = "Oh, you little VB lover, you";
	return alert(saying);
}
alert(saying); // Throws an error


위 코드에서, saying이라는 변수는 talkDirty라는 함수 내에서만 접근할 수 있습니다. 이 함수의 외부에서는 정의된 적이 없죠. 주의할 점은, 위의 2 라인에서 sayingvar 지시자 없이 선언하면 이 변수는 자동적으로 전역변수가 된다는 것입니다.

이것이 의미하는 바는, 함수 내에 중첩된 함수가 자신을 포함하고 있는 함수의 변수와 함수에 접근할 수도 있다는 것입니다:


function saveName (firstName) {
	function capitalizeName () {
		return firstName.toUpperCase();
	}
	var capitalized = capitalizeName();
	return capitalized; 
}
alert(saveName("Robert")); // Returns "ROBERT"


위 코드에서 볼 수 있듯, capitalizeName 함수에는 어떠한 입력 파라미터도 없습니다만, 그를 포함하고 있는 함수인 saveName의 파라미터-firstName에 접근할 수 있습니다. 좀 더 명확히 알기 위해 예제를 하나 더 보기로 하겠습니다.


function siblings () {
	var siblings = ["John", "Liza", "Peter"];
	function siblingCount () {
		var siblingsLength = siblings.length;
		return siblingsLength;
	}
	function joinSiblingNames () {
		return "I have " + siblingCount() + " siblings:\n\n" + siblings.join("\n");
	}
	return joinSiblingNames(); 
}
alert(siblings()); // Outputs "I have 3 siblings: John Liza Peter"


여기서 알 수 있듯이, 두 개의 내부 함수 모두 siblings라는 지역 변수에 접근할 수 있고, 또 각각의 내부 함수들은 서로 접근할 수 있습니다.(여기서는 joinSiblingNames 함수가 siblingCount 함수에 접근하고 있습니다.) 하지만 siblingLength라는 변수는 siblingCount라는 함수 내에서만 접근할 수 있는데, 이것이 이 변수의 유효범위인 것입니다.

 

3. 클로저 (Closure)

실행범위에 대해 좀 더 잘 이해했을테니, 이제 거기에 클로저에 대한 개념을 섞어봅시다. 클로저라는 것은 (보통은 함수인) 표현식인데, 어떤 실행 맥락 내의 변수 집합과 연동할 수 있는 것을 의미합니다. 혹은, 좀 더 쉽게 설명한다면, 외부 함수의 지역 변수를 참조하는 내부 함수들은 클로저를 만들게 됩니다. 예를 들면 다음과 같습니다.


function add (x) {
	return function (y) {
		return x + y;
	};
}
var add5 = add(5);
var no8 = add5(3);
alert(no8); // Returns 8


와우, 도대체 무슨 일이 일어난 걸까요? 한 번 자세히 살펴봅시다.

1. add 함수가 호출되었을 때함수가 하나 리턴됩니다.

2. 이 함수는 현재의 실행 맥락(context)을 폐쇄(close)하고 그것이 실행될 때의 파라미터 x의 값을 기억합니다. (위 코드에서는 5입니다.)

3. add함수의 실행 결과가 add5 변수에 할당되었을 때이 변수도 최초 초기화 되었을 때의 값을 알고 있게 됩니다.

4. add5 변수는 항상 입력된 값에 5를 더하는 함수를 참조하게 된 것입니다.

5. , add5에 입력 파라미터로 3을 이용하면, 5 3을 더하여 8을 리턴하게 됩니다.


, 자바스크립트 세계에서 add5 함수는 다음과 같이 동작하게 될 것입니다.


function add5 (y) {
	return 5 + y;
}


- 악명높은 반복문(Loop) 문제

i라는 값을 원하는 방식 대로 할당하기 위해 얼마나 많이 반복문을 만들어 봤습니까? , 엘리먼트들에 입력된 i들이 모두 마지막 값만 리턴하는 것을 본 적이 있습니까?


+ 잘못된 참조

먼저 잘못된 코드를 살펴보기로 합시다. 이 코드에서는 다섯 개의 a 엘리먼트를 생성하고 거기에 각각의 i 에 해당하는 값을 텍스트(‘Link’+i)로 추가하고 alert로 그 i값을 출력하는 onclick 이벤트를 추가합니다. 아마도 다음과 같은 모양이 될 것 같습니다.


function addLinks () {
	for (var i=0, link; i<5; i++) {
		link = document.createElement("a");
		link.innerHTML = "Link " + i;
		link.onclick = function () {
			alert(i);
		};
		document.body.appendChild(link);
	}
}
window.onload = addLinks;


각각의 엘리먼트들은 원하는 대로 “Link0”, “Link1”등의 텍스트를 포함하게 됩니다. 하지만 각각을 클릭했을 때에는 모든 엘리먼트들이 5를 출력하는 것을 볼 수 있을 것입니다. 도대체 무슨 일일까요? 그 이유는, i라는 변수가 반복문 내에서 매번 1씩 증가하는데, onclick 이벤트가 호출되지 않았으므로, 매번 더해진 값이 각각의 엘리먼트에 적용되는 것입니다.

그러므로 반복문은 i 5가 될 때 까지 반복될 것인데, 그 값은 addLink라는 함수가 종료되기 직전의 마지막 값입니다. 그래서, onclick 이벤트가 호출된다 하더라도 i의 마지막 값인 5가 출력됩니다.


+ 실행 참조

이를 위해 클로저를 생성하여 i 값을 설정할 때, 설정하는 순간의 i 값이 onclick 이벤트에 적용되도록 해야 합니다. 다음과 같이 말이죠.


function addLinks () {
	for (var i=0, link; i<5; i++) {
		link = document.createElement("a");
		link.innerHTML = "Link " + i;
		link.onclick = function (num) {
			return function () {
				alert(num);
			};
		}(i);
		document.body.appendChild(link);
	}
}
window.onload = addLinks;


이 코드를 실행하면, 첫 번째 엘리먼트는 0, 두 번째는 1우리가 정확히 원했던 방식대로 출력됩니다. 여기서 사용한 해결책은 onclick 이벤트에 적용되는 내부 함수가 num이라는 파라미터를 입력받는 클로저를 생성하는 것입니다. 그리고 여기에 그 시점의 i가 이용됩니다.

그 뒤 이 함수는 i 값을 안전하게 간수한 뒤 폐쇄(close)되며 호출될 때 최초 입력되었던 값을 그대로 리턴할 수 있게 됩니다.

 

- 자기 실행 함수 (Self-Invoking Function)

자기 실행 함수는 즉시 실행되는 함수를 말하는데, 그를 위해 자기 자신이 클로저를 생성합니다. 다음을 살펴봅시다.


(function () {
	var dog = "German Shepherd";
	alert(dog);
})();
alert(dog); // Returns undefined


알겠지만, dog이라는 변수는 자신의 맥락 내에서만 사용이 가능하도록 숨겨져 있습니다. 숨겨진 개(dog)라니놀랍군요. 하지만 정말 놀라운 부분은 이제부터 시작인데요, 이 방식으로 위에서 다뤘던 반복문 문제를 해결했다는 점입니다. 그리고 이것은 Yahoo Javascript Module Pattern의 기반이기도 합니다.


+ Yahoo Javascript Module Pattern

이 패턴의 요지는 클로저를 생성하는 자기 호출 함수를 이용한다는 것인데, 그 결과 private이나 public 속성/메소드를 정의할 수 있게 됩니다. 다음의 간단한 예를 살펴보죠:


var person = function () {
	// Private
	var name = "Robert";
	return {
		getName : function () {
			return name;
		},
		setName : function (newName) {
			name = newName;
		}
	};
}();
alert(person.name); // Undefined
alert(person.getName()); // "Robert"
person.setName("Robert Nyman");
alert(person.getName()); // "Robert Nyman"


  이 코드의 아름다운 점은 이제 우리가 객체에 대해 어떤 것은 public으로 접근하게 하고, 어떤 것은 private으로 만들어 외부에서 함부러 바꾸지 못하게 할 수 있다는 것입니다. 위에 정의되어 있는 name이라는 변수는 이 함수의 외부에서는 접근이 불가능하지만, name 변수에 대한 참조를 갖고 있는 클로저를 생성하기 때문에 getName 함수와 setName 함수를 이용하여 값을 얻거나 변경할 수 있게 됩니다.

 

4. 결론

필자의 순수한 희망은, 초급이든 숙련된 개발자이든 간에, 자바스크립트 내에서 유효범위와 클로저가 어떻게 동작하는지 명확하게 이해하는 것입니다. 질문과 피드백은 언제든 환영합니다. 여러분들이 제공해 주신 어떤 내용이든 중요하다고 생각되면, 이 글에 지속적으로 반영하도록 하겠습니다.



이해가 잘 되셨나요? 원문에 쓰여 있는 대로, 질문과 피드백은 언제든 환영합니다. 즐거운 코딩 되시길~

신고

'일하기 > JAVASCRIPT' 카테고리의 다른 글

[번역] Explaining Javascript Scope and Closure  (0) 2013.08.28
Posted by Layered 트랙백 0 : 댓글 0

댓글을 달아 주세요


  • 간일 : 2013년 5월
  • 규격 : 320페이지 / 486g / 153x244mm


휴가 기간동안 뚝딱 읽기 딱 좋은 책입니다. 고작 320페이지에 글자 크기도 작지 않고, 여백도 시원시원합니다. 거기다가 들고 다니기에 부담스럽지 않은 486g이니, 수영장 카바나에서나 해변의 파라솔 아래서 슬며시 지적 허영심을 충족시키기에 충분한 책이랄까요. (실제로 제가 그랬습니다.)



내용인 즉슨, 사이코패스(psychopath)라는 존재들이 우리가 일반적으로 생각하는 대로 '잔혹한 범죄를 일으키고 인간성이라고는 눈꼽만큼도 없는 무시무시한 존재'인 것만은 아니라는 것입니다. 상당수의 사이코패스들의 경우, 성격을 구성하는 요소들 중 일부가 극단적으로 발현된 것이며, 그들의 성격적 특성이나 그들이 보이는 행동들이 오히려 현대 사회를 이끌어가는 힘이자 도피처가 될 수 있다고 얘기합니다. 사회와 공동체에 극단적인 피해를 주는 몇 가지 요인들을 제거할 수 있다면 말이죠. 



한국어판 제목은 [천재의 두 얼굴, 사이코패스]라고 되어 있기는 하지만, 원서의 제목은 [the Wisdom of Psychopaths]로 직역하면 "사이코패스들의 지혜" 정도가 될 것입니다. 그리고 솔직히 말하면, 한국어판 제목은 책의 내용을 제대로 요약할 의지가 없는, 말 그대로 "독자를 끌기 위한 낚시" 제목이라고 할 수 있습니다. 천재가 두 얼굴을 가진 사이코패스다라는 개념보다는, 사이코패스들 중 일부는 몇몇 영역에서 천재성을 보인다, 혹은 몇몇 영역에서 매우 유능하다, 혹은 사이코패스의 성격적 특성들이 천재(혹은 유능한 리더)의 특징들인 것 처럼 보일 수 있다 정도로 이해하는 것이 훨씬 이 책의 논조에 부합됩니다. 즉, 이 책은 천재와 천재성에 대해 적극적으로 설명하지도, 정의내리지도 않습니다. 모든 초점은 사이코패스와 그들의 유능성에 맞춰져 있죠. 이런 면에서 볼 때, 원서의 제목이 훨씬 더 적절하다고 할 수 있겠네요.



한국어판 제목이 어쨌든 간에, 이 책이 사고를 넓혀주는 데 도움을 준다는 사실은 분명합니다.(적어도 저의 경우에는 그랬습니다.) 


가장 먼저, "사이코패스"라고 하는 존재의 지평을 넓혀줍니다. 사이코패스는 "잔혹한 도끼 살인마"도 아니고 "감정이 100% 없는 살인 기계"인 것도 아니라는 것이죠. 좀 위험한 발언일 수 있습니다만, '호모 사피엔스보다 진화된 새로운 종족'이라는 말까지 나옵니다. 


'차가운 공감'이라는 개념도 눈길을 끕니다. 공감이라고 하면 일반적으로 "따뜻한, 마음을 주고받는" 이라는 뜻을 내포하지만, 이 책에서는 "완벽하게 상대방의 감정 상태를 이해하고 그것의 근원을 파악해내는"것도 공감이며, 이것이 사이코패스들의 근원적인 힘이라고 말합니다.


또, '기능성 사이코패스'라는 개념을 제시하여, 어떤 사람이 특정 상황에서 보이는 '유능함'이 사이코패스적 특징에 의한 것일 수 있음을 설명합니다.

 

단순화하긴 하였지만, 성공하는 사람들의 일반적인 특징을 정의하고 그것들이 사이코패스적 특질들과 연관이 있는지도 알려줍니다. 일반적으로 사이코패스적 특질이라 알려진 무자비함/매력/집중력/강인한 정신/겂 없음/현실 직시/실행력이 사실은 일반인들이 우리의 리더에게 요구하는 특성임을 상기시켜주면서 말이죠.



약간 무리수인 부분들도 보이긴 합니다. 특히 마지막에 나오는 티벳 승려에 대한 내용이라던가, 명상이나 동양 철학의 공(空) 개념을 사이코패스적 특징에 연결한다던가 하는 내용들이 그러합니다. 사이코패스적 특징들이 현대 사회를 살아가는 데 도움을 줄 수 있다는 얘기를 하면서 너무 멀리 가버린 것이 아닌가 싶습니다. 하지만, 책 전체에 흐르는 일관적인 논조를 해칠 정도는 아니라고 할 수 있을 듯 합니다. 그냥 읽으면서 '아 좀 무리수...' 정도라고 생각된다고 할까요.



어쨌든, 이 책을 읽은 독자가 "사이코패스들의 지혜"를 받아들이건 받아들이지 않건, 한 때의 지적 호기심과 허영심을 채우기에 충분한 책이라 생각됩니다. 마지막으로 이 책에 나오는 사이코패스가 한 말을 인용하며 장황했던 서평(?)을 마칠까 합니다.


"나는 어린 시절부터 사물을 보는 시각이 남달랐습니다. 하지만 이런 남다른 시각은 오히려 살면서 내게 도움이 됐죠. 이게 옳은 표현인지는 모르겠지만, 굳이 말하자면 사이코패스 성향은 현대사회의 치료제와도 같습니다. 이 치료제를 적당히 복용하면 매우 유익하죠. 왜냐하면 부서지기 쉬운 현대인들의 심리 상태를 보호해주고, 삶에서 느끼는 불안감을 덜어 주기 때문입니다. 하지만 과다 복용을 할 경우에는 다른 약과 마찬가지로 부작용이 생길 수 있습니다. "



신고

'읽기' 카테고리의 다른 글

천재의 두 얼굴, 사이코패스  (0) 2013.08.27
Posted by Layered 트랙백 0 : 댓글 0

댓글을 달아 주세요

이번 포스트에서는 오픈소스 JTA 구현체인 Atomikos를 스프링 웹 어플리케이션 및 Tomcat과 연동하여 사용하는 방법에 대해 다뤄 보겠습니다. 기본적인 내용들은 Atomikos 홈페이지에서 다루고 있기는 하지만, 그대로 따라해보니 몇 가지 부족한 부분이 있더군요. 이 포스트에서는 원문을 보지 않고도 최대한 간단히, 그리고 빠르게 적용할 수 있도록 설명해 보도록 하겠습니다.

Atomikos 홈페이지에서 소개하고 있는 Tomcat Integration 정보는 [여기]에서 확인하실 수 있습니다. 이 페이지에서 자신이 사용하는 버전에 맞는 항목을 참고하시면 됩니다. 이 포스트에서는 Tomcat 7.x와 연동하는 방법에 대해 살펴보도록 하겠습니다.


이 포스트의 내용을 진행하기 위한 사전 준비사항은 다음과 같습니다.

  1. 설치 및 설정 완료된 Apache Tomcat 7.x
  2. 사용하는 DB에 대한 JDBC Driver JAR 파일
  3. Tomcat에서 실행할 스프링 웹 어플리케이션
  4. Atomikos에서 제공하는 JAR 파일 
    1. atomikos-integration-extension-3.7.1-20120529.jar


아래 내용에서 사용되는 환경변수들은 다음과 같습니다.

  • $CATALINA_HOME : 톰캣 설치 디렉토리.
  • $CATALINA_LIB : $CATALINA_HOME/lib - 톰캣이 사용할 라이브러리가 위치하는 디렉토리
  • $CATALINA_CONF : $CATALINA_HOME/conf - 톰캣에 대한 환경설정 파일 이 위치한 디렉토리
  • $WEB_CONTEXT_ROOT : 웹 어플리케이션이 설치된 디렉토리


  1. 먼저 Tomcat에 대한 설정을 진행합니다. (2~7)

  2. 사용하는 DB에 대한 JDBC 드라이버 JAR 파일을 $CATALINA_LIB 디렉토리에 복사해 넣습니다.
    (저의 경우에는 Oracle10g를 위한 ojdbc6.jar 파일을 넣었습니다.)

  3. atomikos-integration-extension-3.7.1-20120529.jar 파일을 $CATALINA_LIB 디렉토리에 복사해 넣습니다.

  4. 그 외 JTA와 Spring 연동을 위한 갖가지 라이브러리들을 $CATALINA_LIB 디렉토리에 복사해 넣습니다.
    (atomikos-integration-extension-3.7.1-20120529.jar 파일은 2번에서 이미 넣은 것이니 여기서는 생략해도 됩니다.)


    이 파일들은 [JAR Finder]에서 검색하여 다운로드 받으실 수 있습니다. (이 파일들이 없으면 Tomcat 기동 시 수많은 오류를 보게 될 것입니다.)

  5. $CATALINA_CONF/server.xml 파일에 다음 내용을 입력합니다.
    <Listener className="com.atomikos.tomcat.AtomikosLifecycleListener" />
    


  6. $CATALINA_CONF/context.xml 파일에 다음 내용을 입력합니다. 이 정보는 context.xml 파일에 등록되어 있는 <WatchedResource> 태그 이후에 입력합니다.
        <Transaction factory="com.atomikos.icatch.jta.UserTransactionFactory" />
    	<!-- Also register Atomikos TransactionManager as java:comp/env/TransactionManager -->
        <Resource name="TransactionManager"
                auth="Container"
                type="com.atomikos.icatch.jta.UserTransactionManager"
                factory="org.apache.naming.factory.BeanFactory" />
    
        <!-- Spring LoadTimeWeaver Support for the Tomcat server. -->
        <Loader loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader"
              useSystemClassLoaderAsParent="false"/>
        <Resource
    			name="defaultDatasource"
    			auth="Container"
    			type="com.atomikos.jdbc.AtomikosDataSourceBean"
    			factory="com.atomikos.tomcat.EnhancedTomcatAtomikosBeanFactory"
    			uniqueResourceName="defaultDatasource"
    			xaDataSourceClassName="oracle.jdbc.xa.client.OracleXADataSource"
    			
    			xaProperties.serverName="xxx.xxx.xxx.xxx" 
    			xaProperties.portNumber="xxxx"  
    			xaProperties.user="xxxx"
    			xaProperties.password="xxxx"
    			xaProperties.databaseName="xxxx"
    			xaProperties.URL="jdbc:oracle:thin:@xxx.xxx.xxx.xxx:xxxx:xxxx"
    			
    			maxPoolSize="10"
    			minPoolSize="1"
    	/>      
    


  7. 여기까지 하면 일단 Tomcat에 대한 설정은 마무리된 것입니다. 이제 웹 어플리케이션에 대한 설정을 진행합니다.

  8. $WEB_CONTEXT_ROOT/WEB-INF/web.xml 파일에 다음과 같이 resource-ref 정보를 입력합니다. 8에서 입력했던 Datasource 정보를 이용합니다.
    	<resource-ref>
    		<res-ref-name>defaultDatasource</res-ref-name>
    		<res-type>com.atomikos.jdbc.AtomikosDataSourceBean</res-type>
    		<res-auth>Container</res-auth>
    		<res-sharing-scope>Unshareable</res-sharing-scope>
    	</resource-ref>
    


  9. 스프링 웹 어플리케이션에서 사용할 데이터소스 정보와 트랜잭션 매니저 정보를 다음과 같이 스프링 컨텍스트 파일에 입력합니다. 이 예제에서는 MyBatis를 같이 이용하고 있습니다.
    <?xml version="1.0" encoding="UTF-8"?>
    <beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
    	xmlns:aop="http://www.springframework.org/schema/aop" xmlns:context="http://www.springframework.org/schema/context"
    	xmlns:tx="http://www.springframework.org/schema/tx" xmlns:jee="http://www.springframework.org/schema/jee"
    	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
    		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-3.0.xsd
    		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.0.xsd
    		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-3.0.xsd
    		http://www.springframework.org/schema/jee http://www.springframework.org/schema/jee/spring-jee-3.0.xsd">
    
    	<tx:annotation-driven transaction-manager="transactionManager" />
    
    	<jee:jndi-lookup id="defaultDataSource" jndi-name="defaultDatasource" />
    
    	<!-- Construct Atomikos UserTransactionManager, needed to configure Spring -->
    	<bean id="AtomikosTransactionManager" class="com.atomikos.icatch.jta.UserTransactionManager" init-method="init"
    		destroy-method="close">
    
    		<!-- when close is called, should we force transactions to terminate or not? -->
    		<property name="forceShutdown" value="false" />
    	</bean>
    
    	<!-- Also use Atomikos UserTransactionImp, needed to configure Spring -->
    	<bean id="AtomikosUserTransaction" class="com.atomikos.icatch.jta.UserTransactionImp">
    
    		<property name="transactionTimeout" value="300" />
    	</bean>
    
    	<!-- Configure the Spring framework to use JTA transactions from Atomikos -->
    	<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
    		<property name="transactionManager" ref="AtomikosTransactionManager" />
    		<property name="userTransaction" ref="AtomikosUserTransaction" />
    	</bean>
    
    
    	<bean id="oracleSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
    		<property name="dataSource" ref="defaultDataSource" />
    		<property name="configLocation" value="classpath:mybatis/oracle-configuration.xml" />
    	</bean>
    
    	<bean id="oracleSessionTemplate" class="org.mybatis.spring.SqlSessionTemplate">
    		<constructor-arg ref="oracleSessionFactory" />
    	</bean>
    
    	<bean id="myBatisMapperScanner" class="org.mybatis.spring.mapper.MapperScannerConfigurer">
    		<property name="basePackage" value="com.bory.app.dao" />
    		<property name="annotationClass" value="org.springframework.stereotype.Repository" />
    	</bean>
    
    </beans>
    
    


  10. $WEB_CONTEXT_ROOT/WEB-INF/classes 디렉토리에 jta.properties 파일을 생성하고 그 내용을 적절히 수정합니다. jta.properties 파일에 대한 내용은 [여기]에서 확인하실 수 있습니다.


이제 모든 작업이 완료되었습니다. Tomcat을 재기동하면 Atomikos에 의해 트랜잭션 처리가 수행되는 웹 어플리케이션을 이용할 수 있게 됩니다.

신고
Posted by Layered 트랙백 0 : 댓글 0

댓글을 달아 주세요

거의 일 년 만의 휴가. 싱가포르에 다녀왔습니다. 우리나라가 워낙 더웠어서 그랬는지, 싱가포르가 시원하게 느껴지더군요.


싱가포르 여행은 이번이 다섯 번째 인데, 처음으로 묵었던 만다린 오리엔탈 호텔은 지금까지 지내봤던 호텔 중에 최고였습니다. 마리나 베이 샌즈/플러튼/플러튼 베이/인터컨티넨탈/하드락 등등 다섯 번의 여행 동안 여러 호텔들에 묵어봤지만 이런 서비스를 받은 건 처음인 듯 하네요.. 시설이 최고인 건 아니었지만 서비스는 최고였달까요.. 싱가포르에 여행가시는 분은 꼭 한 번 묵어보시길 바랍니다. 다만 조식 부페는 소문과는 달리 먹을 게 좀 없었다능... 종류는 많은데 막상 먹을 건 별로.. ㅋ


아무튼 휴가를 다녀왔으니, 이제 다시 열심히 살아야 할 듯 하네요. 



신고

'여행하기' 카테고리의 다른 글

휴가 복귀 했습니다.  (0) 2013.08.26
Posted by Layered 트랙백 0 : 댓글 0

댓글을 달아 주세요

현재 우리나라 자바 엔터프라이즈 개발 시장에서 가장 빈번히 사용되고 있고, 그에 걸맞게 강력한 기능을 제공하는 개발 프레임워크는 단연 스프링 프레임워크일 것입니다. (여담입니다만, 혹자는 "소프트웨어 프레임워크는 그저 유행일 뿐이다" 라고 얘기하기도 하지만, 저 개인적으로는 분명 그 발전에는 방향성이 있다고 생각합니다. 소프트웨어 공학, 혹은 공학을 넘은 철학이 그 방향성을 잡아주고 있다고 할까요.) 사실상의(de facto) 표준이라 불리고 있으며, 이 프레임워크의 핵심 기능 중 하나인 의존성 주입(Dependency Injection) 개념은 이미 JavaEE6에 정식으로 채택되어 사용되고 있을 정도입니다. 

(또 여담입니다만, 프레임워크를 개발하는 엔지니어 입장에서, 너무 강력한 프레임워크가 나오니 허탈해졌다라고 할 정도랄까요. 그런데 어쩌겠습니까... 좋으면 제대로 알고 잘 쓰고, 이를 확장해 나갈 줄 알아야겠죠. ㅋ)


그런데, 스프링 프레임워크를 이용하여 이클립스에서 웹 개발을 해 보신 분들이라면 어플리케이션이 여러번 재배포 되었을 때 어느 순간 java.lang.OutOfMemoryError:PermGen space라는 오류가 발생하면서 WAS(혹은 서블릿 컨테이너)가 정상적으로 동작하지 못하게 되는 것을 본 적이 있을 것입니다. 그리고 그럴 때마다 WAS를 재기동하셨을 것입니다. 아, 물론 그것이 스프링 프레임워크의 잘못이라는 것은 아닙니다. 하지만 대부분의 여러분들은 "스프링 프레임워크를 쓰지 않던 시절보다 더 빈번히 이 오류를 보게" 되었을 것이라는 말입니다. 


그럼 도대체 이 에러는 왜 발생하는 것일까요? 어떻게 하면 제거할 수 있을까요? 이번에 번역한 글은 이에 대한 내용을 다루고 있습니다. 이 글을 통해 JVM이 인스턴스를 만들 때, 어떤 메커니즘이 내부적으로 수행되는지, 클래스에 대한 정보는 어디에 저장되는지, 그리고 그것들이 제대로 처리되지 않을 때 어떤 문제가 발생하는지 알게 될 것입니다. 아울러, 이 글에서는 다루고 있지 않지만, 마지막에 JVM 실행 옵션을 이용하여 어플리케이션 재배포 시에 발생하는 PermGen space 에러가 발생하지 않도록 하는 방법에 대해서도 다뤄 보겠습니다. 


서론이 길었습니다. 이제 시작하도록 하죠. 이 번역문의 원문은 http://plumbr.eu/blog/what-is-a-permgen-leak에서 찾아볼 수 있습니다.




What is a PermGen leak?


이 글에서 다룰 내용은 자바 어플리케이션 내에서 발생할 수 있는 특정 메모리 문제에 대한 실제적인 소개이다. , Stack trace 상에서 가끔씩 보는 java.lang.OutOfMemoryError: PermGen space의 원인에 대해 분석할 것이다.

그에 앞서 이 주제를 제대로 이해할 수 있도록, 이와 관련된 핵심 개념을 다뤄보기로 하겠다. 객체는 무엇인지, 클래스와 클래스로더(ClassLoader)는 무엇인지, 그리고 JVM 메모리 모델은 무엇인지도 같이 다룰 것이다. 이 개념들이 익숙하다면 바로 이 다음 장으로 넘어가도 된다. 그 다음, PermGen 메모리 누수의 전형적인 두 가지 경우를 서술하고 그에 대한 힌트나 그것을 해결하기 위한 제안을 다룰 것이다.


Objects, Classes and ClassLoaders

뭐 완전 기초부터 다룰 생각은 없다. 이 글을 읽는 사람이라면 자바 내의 모든 것들이 객체(Object)라는 개념에 익숙할 것이다. 그리고 모든 객체들은 클래스에 의해 정의된다는 것도, , 모든 객체는 그 객체에 대한 클래스의 구조(Structure)를 기술하는 java.lang.Class 객체의 참조(reference)를 갖고 있는 것도 알 것이다.

하지만 코드 내에서 새로운 객체를 생성할 때, 내부적으로는 도대체 어떤 일이 일어나는 것일까? 예를 들어, 다음과 같이 정말로 복잡한 코드를 작성했다고 했다면 말이다.


Person boss = new Person();


자바 가상 머신(Java Virtual Machine, JVM)이 객체를 생성하려면 객체의 구조를 이해해야 한다. 그리고 그러기 위해, JVMPerson이라는 클래스를 찾는다. 그리고 이 어플리케이션이 실행되는 과정에서 이 Person이라는 클래스에 최초로 접근하였다면, JVM은 이 클래스를, 정상적인 경우, Person.class라는 파일로부터 로드한다. 드라이브(디스크 드라이브)로부터 Person.class 파일을 검색하고, 그것을 메모리에 적재하고 그것의 구조를 분석(parsing)하는 것을 클래스 로딩이라고 한다. 그리고 클래스를 제대로 로딩하는 것을 보장하는 역할은 클래스로더(ClassLoader)가 맡고 있다. 클래스로더들은 java.lang.ClassLoader 클래스의 인스턴스(instance)인데, 자바 프로그램 내의 모든 개별 클래스들은 동일한 클래스로더에 의해 로드 되어야 한다. 어쨌든 여기까지 다음 그림과 같은 관계를 얻게 된다.



, 다음 그림에서 볼 수 있듯, 모든 클래스로더는 자신이 로드한 모든 클래스에 대한 참조를 보유하고 있다. 이 그림은 우리가 다루고자 하는 내용과 아주 밀접하니 주의 깊게 살펴보기 바란다.




다시 한 번 말하지만, 이 그림은 다음에 또 필요하게 될 것이다.


Permanent Generation

현존하는 거의 모든 JVM, 자바 클래스들에 대한 내부적인 표상(internal representation)을 위해, Permanent Generation(줄여서 PermGen)이라고 하는 구분된 메모리 영역을 사용한다. PermGen은 그 외에 추가 정보를 저장하기 위해서도 사용되지만, -관심이 있다면 그에 대한 상세한 내용은 포스트를 참고하기 바란다. 이 글에서는 클래스에 대한 정의만 저장된다고 가정하기로 하자. 필자가 사용하고 있는 Java1.6이 실행되고 있는 장비에서PermGen 영역에 대한 기본 크기는 그리 대단할 것 없는 82MB이다.


 이전 포스트에서 다뤘듯이, 메모리 누수(Memory Leak)라고 하는 것은, 어떤 객체들이 더 이상 사용되지 않고 있지만 가비지컬렉터(Garbage Collector, GC)가 그 클래스들이 사용되고 있지 않다는 것을 인지하지 못하는 것이다. 이것이 반복되어, 사용되지 않는 객체들이, 어플리케이션의 다음 메모리 할당 요청이 처리될 수 없을 만큼, heap 메모리를 점유하게 되면 OutOfMemoryError가 발생하게 된다.

java.lang.OutOfMemoryError: Permgen space의 원인도 이와 완벽하게 동일하다: JVM이 새로운 클래스 정의를 로드해야 하는데 이를 수행하기 위한 PermGen 공간이 충분하지 않기 때문이다. , 이미 너무 많은 클래스들이 로딩되어 있다는 말이다. 이것이 발생하는 것은, 어플리케이션이나 서버가 현재 크기의 PermGen이 처리할 수 없을 만큼 많은 클래스들을 사용하고 있기 때문일 수도 있다. 그것이 아니라면, 메모리 누수 때문일 수 있다.


Permanent Generation Leak

그런데, 도대체 어째서 PermGen 영역에서 메모리 누수가 발생하는 것일까? PermGen 영역에는 클래스에 대한 정의가 등록되어 있을 것이고, 이것들이 사용되지 않게 되는 경우는 없을 건데 말이다. 혹시 그런 경우가 있는 것일까?

실제로, 그런 경우가 있다. 어플리케이션 서버에 배포되어 있는 자바 웹 어플리케이션이 서버로부터 제거(undeployed)되면 그 어플리케이션과 관계된 EAR/WAR내의 클래스들은 모두 쓸모 없어진다. 어플리케이션 서버가 여전히 사용 중에 있으므로 JVM은 동작을 계속할 것인데, 이 수많은 클래스에 대한 정의들이 더 이상 사용할 필요가 없어진 것이다. 이들은 PermGen에서 제거되어야 하고, 만약 그렇지 않다면 PermGen 영역에서 Memory Leak이 발생한 것이다.

Tomcat의 개발자들은 Tomcat 6.0.24와 그 이후 버전에서 발생한 다양한 메모리 누수 상황들과 그것을 해결하는 방법을 다룬 위키 페이지를 작성해 두었다. 참고하기 바란다.


Leaking Threads

클래스로더 누수가 발생하는 다른 시나리오는 스레드들의 실행이 너무 길어질 때 이다. 이것은 당신이 작성한 어플리케이션이나, (내 경험으로는 다음 경우가 더 많았는데) 당신의 어플리케이션이 사용한 서드파티 라이브러리가, 과도하게 오랫동안 실행되는 스레드들을 사용할 때 발생할 수 있다. 이것의 한 예로, 특정 코드를 주기적으로 수행하는 타이머 스레드 같은 것이 있을 수 있다.

스레드의 수명이 지정되지 않았다면, 바로 문제가 될 소지가 있다. 어플리케이션의 어떤 부분이 스레드를 하나라도 시작했다면, 그 스레드가 어플리케이션보다 더 오래 살아남지 않도록 제어해야 한다. 개발자들이 이것을 책임져야 한다는 것을 아예 모르거나, 이것을 처리하는 클린업 코드를 작성하는 것을 깜빡하는 것은 흔한 일이다.

이 처리를 제대로 하지 않았다면, 스레드들은 웹 어플리케이션이 제거(undeployed)된 이후에도 계속 동작하게 될 것이고, 그 스레드는 웹 어플리케이션이 시작할 때 사용된 Context ClassLoader라 불리는 클래스로더에 대한 참조를 보유하게 될 것이다. 이 말은 즉, 제거된 어플리케이션의 모든 클래스들이 메모리 내에 지속적으로 남아있게 될 것이라는 거다. 해결 방법은? 어플리케이션이 스레드를 시작했다면, 반드시 어플리케이션 제거 과정에서 Servlet Context Listener를 이용하여 그 스레드들을 멈추고 제거해야 한다. 만약 서드파티 라이브러리를 사용했다면, 그것이 실행한 스레드를 중지하기 위한 셧다운 훅(shutdown hook)을 찾아보아야 한다. 만약 그런 것이 없다면 버그 리포팅을 해야 한다.


Leaking Drivers

메모리 누수의 또 다른 전형적인 경우는 데이터베이스 드라이버에 의해 발생된다. 우리는 Plumbr에 탑재한 우리의 데모 어플리케이션에서 이 문제가 발생하는 것을 본 적이 있었다. 이 데모 어플리케이션은 스프링 프레임워크에서 제공하는 Pet Clinic 어플리케이션을 아주 조금 수정한 것이다. 이 어플리케이션을 어플리케이션 서버에 배포하는 과정에서 발생했던 일을 간략히 기술해 보겠다.

l  서버는 새로운 java.lang.ClassLoader 인스턴스를 생성하고 그것을 사용하여 어플리케이션의 클래스들을 로딩하기 시작한다.

l  PetClinic 어플리케이션은 HSQL 데이터페이스를 이용하므로, 그것을 사용하기 위해 그에 상응하는 JDBC 드라이버, org.hsqldb.jdbcDriver를 로딩한다.

l  이 클래스는, 제대로 만들어진 JDBC 드라이버이니, JDBC 규격에서 정의된 대로 초기화 과정에서 java.sql.DriverManager에 등록(register)한다. 이 등록 과정에서 드라이버매니저의 정적 필드(static field) org.hsqldb.jdbcDriver의 인스턴스에 대한 참조를 저장하게 된다.

이제 어플리케이션이 어플리케이션 서버에서 제거되어도, java.sql.DriverManager는 여전히 그 참조를 보유하고 있게 될 것이다. HSQLDB나 스프링 프레임워크나 어플리케이션 내의 어떤 코드에서도 그 참조를 제거하지 않으니 말이다. 위에서 설명했듯이, JDBC 드라이버 객체는 여전히 org.hsqldb.jdbcDriver에 대한 참조를 유지하게 되는데, 이것은 또 어플리케이션을 로드하는 데 사용된 클래스로더에 대한 참조를 포함하고 있을 것이다. 그리고 이 클래스로더가 이 어플리케이션의 모든 클래스에 대한 참조를 포함하고 있다. 우리의 데모 어플리케이션의 경우에서, 어플리케이션이 기동될 때 거의 2000개 정도의 클래스들이 로딩되는데, PermGen 영역에서 대략 10MB 정도를 사용한다. 이 말은 어플리케이션을 5~10회 가량 재배포하면 PermGen의 기본 크기를 모두 차지하게 되고 그로 인해 java.lang.OutOfMemoryError: PermGen space 에러가 발생하게 될 것이라는 말이다.

해결하는 방법은? 한 가지 방법은 Servlet Context Listener에 어플리케이션이 셧다운 되는 동안 드라이버매니저에서 HSQLDB 드라이버를 등록 해제하는 코드를 작성하는 것이다. 이는 아주 간단하게 처리할 수 있다. 하지만 이 드라이버를 사용하는 모든 어플리케이션에 코드를 작성해야 할 것이다.

최신 버전의 Plumbr와 데모 어플리케이션을 다운로드하여 메모리 누수가 발생하는지 확인하고, Plumbr가 그것을 어떻게 찾아내고 우리가 그것을 어떤 방식으로 설명하는지 확인해보기 바란다.


Conclusion

어플리케이션에서 java.lang.OutOfMemoryError: PermGen space 가 발생하는 데에는 수많은 이유가 있다. 가장 주된 원인은 어플리케이션이 셧다운 된 이후에도 어플리케이션의 클래스 로더에 의해 로딩된 클래스나 클래스에 대한 참조들이 남아있기 때문이다. 혹은 클래스로더에 대한 직접적인 링크 때문일 수도 있다. 어쨌든 이것을 해결하는 방법은 대부분의 경우에 유사하다고 할 수 있다. 먼저 여전히 참조가 유지되고 있는 곳을 찾아내고, 그 다음, 어플리케이션이 제거되거나 셧다운 될 때 이들을 해제할 수 있는 셧다운 훅을 어플리케이션에 추가한다. 이것은 Servlet Context Listener나 서드파티 라이브러리가 제공하는 API를 이용하여 처리할 수 있을 것이다.




이 글은 Permgen Space나 클래스로더, 그리고 심지어 OutOfMemory에 대해 지나치게 단순화하여 설명하는 경향이 있습니다. 대신, java.lang.OutOfMemory : PermGen space 오류가 왜 발생하는지에 대해서는 깔끔하게 잘 설명하고 있죠. 목표로 하는 내용 외의 기타 등등의 세부사항을 적절히 배제한 것이 이해를 돕는데는 큰 도움이 되네요.

참고로, 자바에서 OutOfMemory는 메모리 누수에 의해서만 발생하는 것은 아닙니다. 그 외 여러 가지 경우에서도 발생할 수 있는데, 추후에 이에 대해 별도의 포스트로 언급해 보도록 하겠습니다.

또, 클래스로더 역시 이 글에서 다룬 것과 같이 아주 단순하지만은 않습니다. 이에 대해서도 따로 다뤄보도록 하겠습니다.


아참, 그런데... 이 원글은 스프링 프레임워크에 대해서는 전혀 다루지 않고 있는데, 저는 왜 스프링 프레임워크를 굳이 언급한 것일까요? 스프링 프레임워크의 어떤 특징 때문에 이 문제가 좀 더 빈번하게 발생한다고 생각하고 있는 것일까요? 혹시 그것이 아니라면, 사용자들이 스프링 프레임워크를 뭔가 잘 못 사용하기 때문에 이 문제가 빈번하게 발생하는 것은 아닐까요? 이 부분은 여러분 스스로 생각해 보시기 바랍니다. (최근에 어떤 책을 읽었는데, 가장 중요한 부분은 스스로 생각하라고 하더군요... 하지만 그 책의 문제는, 가장 중요한 것은 그렇다 쳐도 그 밖에 중요하다고 할 만한 것은 아무것도 다루고 있지 않는다는 것이었습니다.)


마지막으로, 자바 실행 옵션 중, PermGen 영역에 로딩되어 있는 클래스들을 메모리에서 해제하고 다시 로딩할 수 있도록 하는 것이 있습니다. 위에서 언급한 메모리 누수가 아닌, 단순 어플리케이션 재로딩시에 발생하는 Permgen space에러를 해결하는 데에 다음 실행 옵션이 도움이 될 것입니다.

-XX:+CMSClassUnloadingEnabled -XX:+CMSPermGenSweepingEnabled


그 외 자바 실행 옵션에 대한 상세한 내용은 [여기]에서 확인해 보시기 바랍니다.

신고
Posted by Layered 트랙백 0 : 댓글 0

댓글을 달아 주세요

바로 이전 포스트에서 SVNANT를 이용하여 Ant로 SVN을 관리하는 방법에 대해 다루었습니다. 그런데, SVNANT 같은 것은 어떻게 만드는 것일까요? 사실 SVNANT 뿐만 아니라 ant-contrib와 같은 모든 Ant의 서드파티 플러그인들은 Ant가 제공하는 태스크(Task) 확장 기능에 기반하고 있습니다. 오늘은 이 방법에 대해 다뤄볼까 합니다.

본 번역은 Apache Ant에서 제공하는 공식 문서 중 하나를 번역한 것입니다. 사용자 정의 태스크를 만드는 방법, 태스크의 생명주기(Life Cycle), Ant와의 연동 등등 자세하게 다루고 있어 이를 통해 내가 원하는 Ant 태스크를 손쉽게 만들 수 있습니다. 원문의 주소는 http://ant.apache.org/manual/develop.html 입니다.

자 그럼 이제 시작해볼까요.



Apache Ant를 이용한 개발


사용자 태스크 작성하기(Writing your own Task)

 

사용자 태스크를 만드는 방법은 아주 간단합니다.


  1. org.apache.tools.ant.Task 클래스나, 태스크들이 확장할 수 있도록 만들어 놓은 그 외의 클래스들을 상속하여 자바 클래스를 만듭니다.
  2. 각각의 속성들을 위한 setter 메소드들을 만듭니다. setter 메소드들은 모두 하나의 입력값(argument)만을 받는 public void 메소드여야 합니다. 메소드명은 set으로 시작되어야 하고, 그 뒤에 속성명이 나와야 하는데, 첫 글자는 대문자로 지정되어야 하며 그 외에는 소문자로 지정*되어야 합니다. 즉, 속성명이 file인 경우 setFile이라고 하는 메소드를 생성해야 하는 것입니다. 입력값의 유형(type)에 맞게 Ant가 자동으로 데이터 타입에 대한 전환을 수행할 것입니다. 이 내용은 아래에서 다루고 있습니다.
  3. 만약 여러분이 작성한 태스크가 포함된 엘리먼트(nested element)로 다른 태스크들을 포함하고 있어야 한다면(parallel 태스크처럼), 작성된 클래스는 반드시 org.apache.tools.ant.TaskContainer 인터페이스를 구현해야 합니다. 만약 그렇게 했다면, 그 태스크는 그 외의 다른 포함 엘리먼트들을 지원할 수 없게 됩니다. 이 내용은 아래에서 다루고 있습니다. 
  4. 먄약 태스크가 character 데이터(시작 태그와 끝 태그 내에 포함되어 있는 문자열**)를 지원해야 한다면, public void addText(String) 메소드를 작성하세요. Ant는 Ant가 태스크에 전달하는 텍스트에 속성(properties)를 확장***하지 않는다는 것에 주의하세요.
  5. 포함되어있는 각각의 엘리먼트들을 위해 create, add 혹은 addConfigured 메소드를 작성하세요. create 메소드는 반드시 public 메소드여야 하고, 입력값이 없어야 하며, Object형을 리턴하여야 합니다. create 메소드의 이름은 반드시 create로 시작되어야 하고, 그 위에 엘리먼트명이 따라와야 합니다. add(혹은 addConfigured) 메소드는 public void  메소드이어야 하며, 입력값이 없는 기본 생성자를 갖고 있는 Object 유형의 입력값 하나만 지정되어야 합니다. add(addConfigured) 메소드의 이름은 add(addConfigured)로 시작되어야 하며, 엘리먼트 이름이 그 뒤에 따라와야 합니다. 자세한 내용은 아래에서 확인할 수 있습니다.
  6. 입력값이 없고 BuildExeption을 던지는 public void execute 메소드를 작성합니다. 이 메소드에서 태스크의 작업이 수행됩니다. 


* 사실 Ant에서 첫 번째 문자 외의 대소문자는 크게 문제가 되지 않지만, 나머지를 모두 소문자로 처리하는 것이 관습적으로 좋습니다.

** CDATA 섹션을 의미합니다. - 옮긴이

*** 속성을 확장(expand properties)한다는 말은 자동 형변환을 의미하는 듯 합니다. - 옮긴이


스크의 생명주기(The Life Cycle of a Task)


  1. 태스크에 대응하는 태그를 포함하고 있는 XML 엘리먼트는 parser가 작동하면서 UnknownElement로 변환됩니다. 이 UnknownElement는 대상 객체 내의 목록에 추가되거나 다른 UnknownElement 내에 재귀적으로(recursively) 추가됩니다. 
  2. 대상 객체가 실행(execute)될 때, 각각의 UnknownElement들은 perform() 메소드를 통해 호출됩니다.  이것이 태스크를 객체화(instantiate)합니다. 이 말은, 태스크들은 오직 런타임시에만 객체화된다는 의미입니다.
  3. 태스크들은 상속받은 project와 location 변수들을 통해 자신을 포함하고 있는 프로젝트와 빌드파일이 속해 있는 위치를 얻습니다.
  4. 만약 사용자가 태스크에 id 속성을 지정하였다면, 프로젝트는 런타임시에 새롭게 생성된 태스크에 대한 참조(reference)를 등록합니다.
  5. 이 태스크는 상속받은 target 변수를 통해 자신이 속해있는 대상(target)에 대한 참조를 얻습니다.
  6. init() 메소드가 런타임시에 호출됩니다.
  7. 이 태스크에 대응하는 XML 엘리먼트의 모든 자식 엘리먼트들은 런타임시에 createXXX() 메소드를 통해 생성되거나, addXXX() 메소드를 통해 객체화되어 태스크에 추가됩니다. addConfiguredXXX() 메소드에 대응되는 자식 엘리먼트들은 addConfiguredXXX() 메소드가 호출되는 시점이 아니라 이 때에 생성됩니다. 
  8. 태스크 내의 모든 속성들이 런타임시에 setXXX 메소드를 통해 설정됩니다.
  9. 이 태스크에 대응하는 XML 엘리먼트 내의 character 데이터 섹션의 내용이 런타임시에 addText 메소드를 통해 태스크에 추가됩니다.
  10. 모든 자식 요소의 모든 속성들이 런타임시에 그에 대응하는 setXXX 메소드를 통해 설정됩니다. 
  11. 만약 이 태스크에 대응하는 XML 엘리먼트의 자식 엘리먼트들이 addConfiguredXXX() 메소드를 위해 생성되었다면, 이 메소드들이 이 때 호출됩니다.
  12. execute() 메소드가 런타임시에 호출됩니다. 만약 target1과 target2가 모두 target3에 의지하고 있다면, 'target1 target2'와 같이 두 개의 target을 동시에 실행하면 target3를 두 번 실행하게 됩니다. 


속성값들에 대한 Ant의 형변환


Ant는 그에 대응하는 setter 메소드로 값을 전달하기 이전에 언제나 그 속성을 확장할 것입니다. Ant 1.8 이후부터, Ant의 속성 처리를 확장하는 것이 가능한데, 하나의 속성 참조를 포함하는 스트링에 대한 처리 결과로 스트링이 아닌 객체가 될 수도 있다는 말입니다. 이는 그 유형에 맞는 setter 메소드를 통해 직접 수행됩니다. 이것이 수행되기 위해서는 기본을 뛰어넘는 개입이 필요하기 때문에, 이것을 사용할 때에는 속성값에 그 의도를 나타내는 플래그를 추가하는 것이 바람직합니다.

속성에 대한 setter를 작성하는 가장 보편적인 방법은 java.lang.String 입력값을 사용하도록 하는 것입니다. 이 경우 Ant는 (속성 확장 후) 그 리터럴 값을 태스크로 전달하게 됩니다. 하지만 이것 외에 추가적으로 처리되는 내용이 있습니다. setter 메소드의 입력값의 유형이 String이 아닐 경우 다음과 같은 작업을 수행합니다.


  • boolean인 경우 : 빌드 파일에서 지정된 값이 true, yes, on 중 하나라면 true 값이, 그렇지 않으면 false 값이 전달됨
  • char 거나 java.lang.Character인 경우 : 빌드 파일에 지정된 값의 첫 번째 문자가 전달됨
  • 원시자료형인 경우 : 해당 원시자료형에 맞는 유형으로 변한되어 전달됨. 그러므로 속성 값으로 숫자가 아닌 값이 입력되지 않도록 확인해야 함
  • java.io.File인 경우 : 일단 빌드파일에서 지정된 값이 파일에 대한 절대 경로인지 확인함. 만약 아니라면 프로젝트의 basedir을 기준으로 상대 경로로 해석되어 전달됨
  • org.apache.tools.ant.types.Resource인 경우 : java.io.File인 경우에서와 마찬가지로 일단 File로 인식된 다음에 그것이 org.apache.tools.ant.types.resources.FileResource로서 전달됨. (Ant 1.8 이후부터)
  • org.apache.tools.ant.types.Path인 경우 : 빌드 파일에 지정된 입력값을 콜론(:)이나 세미콜론(;)으로 자름(tokenize). 상대 경로는 프로젝트의 basedir을 기준으로 한 상대 경로로 처리됨
  • java.lang.Class인 경우 : 빌드 파일에 지정된 값이 해석된 뒤, 시스템 클래스로더로부터 해당하는 이름의 클래스를 로딩하여 전달됨
  • 하나의 스트링 입력값을 받는 생성자를 갖고 있는 유형이라면 이 생성자에 빌드 파일에 포함된 값을 전달하여 객체가 생성됨
  • org.apache.tools.ant.types.EnumeratedAttribute인 경우 : Ant가 이 클래스들의 setValue 메소드를 호출함. 태스크가 열거된 속성(enumerated attribtues)를 지원해야 하는 경우에 사용함.(속성 값은 미리 지정된 값들의 집합에 포함되어 있어야 함)
  • Java5 enum인 경우 : 빌드파일에 지정된 값에 대응되는 enum 상수 값이 전달됨. EnumeratedAttribute를 사용하는 것보다 쉽고 코드가 깔끔해짐. 당연한 이야기지만 JDK1.4 이하에서는 사용할 수 없음. enumeration 내의 모든 재정의된 toString() 메소드들은 무시됨. 빌드 파일은 반드시 정의된 이름을 사용하여야 함(Enum.getName()을 확인할 것) enum 상수명을 빌드 파일 내에서 보기 좋게 하기 위해 Java의 명명 규칙과는 달리 소문자로 사용할 수 있음. Ant 1.7.0에서 처럼.


하나의 속성값에 대해 하나 이상의 setter 메소드가 있는 경우라면 어떻게 될까요? String 입력값을 받는 메소드 대신 다른 유형을 받는 메소드가 항상 선택됩니다. 두 개 이상의 setter 메소드들이 있다면 Ant가 알아서 선택하게 하나만 호출하게 되는데, 어떤 것이 호출될지는 알 수 없습니다. 이는 Java 가상 머신의 구현에 달려 있습니다.


포함된 엘리먼트에 대한 지원(Supporting nested elements)


작성된 태스크가 inner라는 이름의 엘리먼트를 포함해야 한다고 해 봅시다. 먼저 이 포함된 엘리먼트를 표상하는 클래스가 필요할 것입니다. 가끔은 중첩된 fileset 엘리먼트들을 지원하기 위해 org.apache.tools.ant.types.FileSet과 같은 Ant의 클래스들 중 하나를 사용할 수도 있습니다.

포함된 엘리먼트의 속성들이나 그것들에 포함된 자식 엘리먼트들은 태스크를 처리하는 데 사용된 메커니즘과 동일한 메커니즘으로 처리되어야 합니다. (예를 들어, 속성들을 위해서는 setter 메소드를, 중첩된 텍스트를 위해서는 addText를, 그리고 자식 엘리먼트를 위해서는 create/add/addConfigured 메소드를 사용합니다.)

<inner>라는 엘리먼트를 위해 사용할 NestedElement라는 클래스가 있다고 했을 때, 세 가지 옵션을 선택할 수 있습니다.


  1. public NestedElement createInner()
  2. public void addInner(NestedElement anInner)
  3. public void addConfiguredInner(NestedElement anInner)


이들 각각은 어떤 차이가 있을까요?

옵션1은 태스크에 NestedElement의 인스턴스를 생성해 줍니다. 그리고 이것은 유형(Type)에 대한 어떠한 제한도 없습니다. 옵션2와 옵션3의 경우, Ant가 NestedInner의 인스턴스를 태스크에 전달하기 전에 먼저 생성합니다. NestedInner 클래스는 입력값이 없는 생성자(no-arg constructor)나 Project 클래스를 입력값으로 받는 생성자 둘 중 하나를 갖고 있어야 합니다. 옵션1과 옵션2의 차이는 이것 밖에 없습니다.

옵션2와 옵션3의 차이점은 각각의 메소드에 해당 객체를 전달하기 전에 Ant가 어떤 작업을 하느냐 입니다. addInner는 생성자가 호출된 후 바로 객체를 받게 되지만, addConfiguredInner는 새로운 객체를 위한 속성값들과 포함된 자식들이 모두 처리된 다음에 그 객체를 전달받게 됩니다. 

이 옵션들 중 하나 이상을 사용하면 어떻게 될까요? 세 가지 옵션 중 하나가 실행될 것이지만, 어떤 것이 실행될지는 알 수 없습니다. 이 역시 Java 가상 머신의 구현에 따라 결정되기 때문입니다.


포함된 유형들(Nested Types)


<typedef>를 이용하여 정의된 임의의 유형을 포함하는 방법에는 두 가지가 있습니다.


  1. public void add(Type type)
  2. public void addConfigured(Type type)


이 두 옵션들의 차이점은 바로 위 섹션에서 설명했던 차이점과 동일합니다.

예를 들어, org.apache.tools.ant.taskdefs.condition.Condition 클래스 우형의 객체를 다뤄야 한다면 다음과 같이 하면 됩니다.

public class MyTask extends Task {
    private List conditions = new ArrayList();
    public void add(Condition c) {
        conditions.add(c);
    }
    public void execute() {
     // iterator over the conditions
    }
}


이 클래스는 다음과 같이 사용할 수 있습니다.

<taskdef name="mytask" classname="MyTask" classpath="classes"/>
<typedef name="condition.equals" classname="org.apache.tools.ant.taskdefs.conditions.Equals"/>
<mytask>
    <condition.equals arg1="${debug}" arg2="true"/>
</mytask>


조금 더 복잡한 예제는 다음과 같습니다.

public class Sample {
    public static class MyFileSelector implements FileSelector {
         public void setAttrA(int a) {}
         public void setAttrB(int b) {}
         public void add(Path path) {}
         public boolean isSelected(File basedir, String filename, File file) {
             return true;
         }
     }

    interface MyInterface {
        void setVerbose(boolean val);
    }        

    public static class BuildPath extends Path {
        public BuildPath(Project project) {
            super(project);
        }
        
        public void add(MyInterface inter) {}
        public void setUrl(String url) {}
    }

    public static class XInterface implements MyInterface {
        public void setVerbose(boolean x) {}
        public void setCount(int c) {}
    }
}



이 클래스는 Path를 구현하거나 상속하는 MyFileSelecteor와 MyInterface 클래스들을 정의하고 있습니다.  이 클래스들은 다음과 같은 방법으로 사용될 수 있습니다.

<typedef name="myfileselector" classname="Sample$MyFileSelector"
         classpath="classes" loaderref="classes"/>
<typedef name="buildpath" classname="Sample$BuildPath"
         classpath="classes" loaderref="classes"/>
<typedef name="xinterface" classname="Sample$XInterface"
         classpath="classes" loaderref="classes"/>

<copy todir="copy-classes">
   <fileset dir="classes">
      <myfileselector attra="10" attrB="-10">
         <buildpath path="." url="abc">
            <xinterface count="4"/>
         </buildpath>
      </myfileselector>
   </fileset>
</copy>


TaskContainer


TaskContainer는 addTask라는 하나의 메소드로 구성되어 있는데, 이 메소드는 포함된 엘리먼트를 위해 사용되는 add 메소드와 기본적으로 동일합니다. 태스크의 인스턴스는 태스크의 execute 메소드가 호출될 때 설정(속성값들과 포함되니 엘리먼트들이 처리됨)될 것이며 그 전에는 설정되지 않습니다.

execute 메소드가 호출될 것이라고 말했지만 사실은 그렇지 않습니다. 사실, Ant는 org.apache.tools.ant.Task의 perform 메소드를 호출하는데, 그러면 그 다음에 execute 메소드가 호출됩니다. 이 메소드는 빌드 이벤트들이 촉발될 수 있도록 합니다. 만약 태스크 내에서 execute 메소드를 직접 호출해야 한다면, 그 인스턴스의 execute 메소드 대신 perform 메소드를 호출해야 합니다.


예제(Java)


이제 System.out 스트림으로 메시지를 출력하는 우리의 태스크를 만들어봅시다. 이 태스크는 message라 불리는 하나의 속성을 가지고 있습니다.

package com.mydomain;

import org.apache.tools.ant.BuildException;
import org.apache.tools.ant.Task;

public class MyVeryOwnTask extends Task {
    private String msg;

    // The method executing the task
    public void execute() throws BuildException {
        System.out.println(msg);
    }

    // The setter for the "message" attribute
    public void setMessage(String msg) {
        this.msg = msg;
    }
}


정말 쉽죠?

시스템에 이렇게 작성한 태스크를 추가하는 것 역시 매우 쉽습니다.


  1. Ant가 시작될 때 이 태스크를 구현한 클래스가 클래스패스에 있는지 확인합니다.
  2. 프로젝트에 <taskdef> 엘리먼트를 추가합니다. 이를 통해 태스크가 실제로 시스템에 추가됩니다.
  3. 나머지 빌드파일에서 이 태스크를 사용합니다.


예제(XML)


<?xml version="1.0"?>

<project name="OwnTaskExample" default="main" basedir=".">
  <taskdef name="mytask" classname="com.mydomain.MyVeryOwnTask"/>

  <target name="main">
    <mytask message="Hello World! MyVeryOwnTask works!"/>
  </target>
</project>


예제2 


태스크를 생성한 빌드파일에서 태스크를 사용하고 싶다면, target 엘리먼트 내에서 컴파일 수행 후에 <taskdef> 선언을 추가합니다. 그리고 그 코드가 조금 전 컴파일된 위치를 가리키는 taskdef의 classpath 속성을 사용합니다.

<?xml version="1.0"?>

<project name="OwnTaskExample2" default="main" basedir=".">

  <target name="build" >
    <mkdir dir="build"/>
    <javac srcdir="source" destdir="build"/>
  </target>

  <target name="declare" depends="build">
    <taskdef name="mytask"
        classname="com.mydomain.MyVeryOwnTask"
        classpath="build"/>
  </target>

  <target name="main" depends="declare">
    <mytask message="Hello World! MyVeryOwnTask works!"/>
  </target>
</project>


태스크를 (좀 더 영구적으로) 추가하는 또 다른 방법은, 태스크의 이름과 구현된 클래스명을 org.apache.tools.ant.taskdefs 패키지 내의 default.properties 파일에 추가하는 것입니다. 이렇게 하면 이 클래스가 마치 빌트-인 클래스인 것 처럼 사용할 수 있습니다.




빌드 이벤트들(Build Events)


Ant에서는 프로젝트를 빌드하는 데 필요한 태스크를 수행하는 과정에서 빌드 이벤트가 발생되도록 할 수 있습니다. 이 이벤트들을 처리하기 위해 Ant에 리스너를 추가할 수도 있습니다. 이 기능은, 예를 들어, Ant와 GUI를 연결하거나 Ant와 IDE를 통합할 때 사용될 수 있습니다.

빌드 이벤트를 사용하기 위해 Ant의 Project 객체를 생성할 필요가 있습니다. 그리고 나서 addBuildListener 메소드를 이용하여 프로젝트에 작성한 리스너를 추가할 수 있습니다. 리스너들은 org.apache.tools.antBuildListener 인터페이스를 구현해야 합니다. 리스너는 다음 이벤트들이 발생할 때 BuildEvent를 전달 받을 수 있습니다.


  • 빌드 시작
  • 빌드 종료
  • 타겟 시작
  • 타겟 종료
  • 태스크 시작
  • 태스크 종료
  • 메시지 로그됨

만약 빌드 파일이 <ant>나 <subant> 혹은 <antcall>을 이용하여 다른 빌드 파일을 실행하는 경우라면, 그 자신의 타겟과 태스크 수준의 이벤트를 발생시키는 새로운 "project"를 생성하는 것이 되므로, 이 경우에는 빌드 시작이나 종료와 같은 이벤트가 전달되지 않게 됩니다. Ant 1.6.2 부터는 이와 같은 경우 새로운 두 가지 이벤트를 처리하기 위한 BuildListener를 확장한 SubBuildListener라고 하는 인터페이스를 제공합니다. 이것이 처리하는 이벤트들은 다음과 같습니다.


  • 서브빌드 시작
  • 서브빌드 종료


이것을 처리해야 하는 경우라면, BuildListener 대신 이 새로운 인터페이스를 구현하기만 하면 됩니다.(물론 이 리스너를 등록해야 하구요.)

명령실행줄에서 리스너를 추가하고 싶다면 -listener 옵션을 이용할 수도 있습니다. 예를 들어 다음과 같습니다.


ant -listener org.apache.tools.ant.XmlLogger


위 명령을 사용하면 Ant가 실행됨과 동시에 빌드 처리 과정이 XML로 출력되게 됩니다. 이 리스너는 Ant에 포함되어 있는 기본 리스너인데, 표준 출력으로 결과를 출력하게 됩니다.


참고 : 리스너가 직접 System.out이나 System.err에 접근하는 것을 삼가해야 하는데, 그 이유는 System 스트림에 대한 출력은 이벤트 시스템을 빌드하기 위해 Ant의 코어에 의해 리다이렉트되기 때문입니다. 이들 스트림에 직접 접근하는 것은 무한루프를 야기할 수 있습니다. Ant의 버전에 따라, 빌드가 예기치 않게 종료될 수도 있고 JVM상에서 StackOverflowError가 발생할 수도 있습니다. 로거(logger)도 역시 System.out과 System.err에 직접 접근해서는 안 됩니다. 로거들도 자신에게 설정된 스트림을 이용하도록 해야 합니다.

참고2 : "빌드 시작"과 "빌드 종료" 이벤트를 제외한 모든 BuildListener 메소드들은 여러 스레드에서 동시에 호출될 수 있습니다. - 예를 들어 Ant가 <parallel> 태스크를 수행하는 경우가 그렇습니다.


**** 마지막 Source code Integration 섹션은 주제와 직접적인 관계가 없으므로 생략합니다.



이 문서에는 다뤄져 있지 않지만, taskdef를 별도의 xml 파일로 만들고 이 파일을 <taskdef>로 등록할 수도 있습니다. 다음 예는 SVNANT의 taskdef XML 파일입니다. 참고하시기 바랍니다.

<?xml version="1.0"?>
<antlib>
<!-- Tasks -->
<taskdef classname="org.tigris.subversion.svnant.SvnTask" name="svn"/>

<!-- Types -->
<typedef classname="org.tigris.subversion.svnant.types.SvnFileSet" name="svnFileSet"/>
<typedef classname="org.tigris.subversion.svnant.types.SvnSetting" name="svnSetting"/>

<!-- Selectors -->
<typedef classname="org.tigris.subversion.svnant.selectors.Normal" name="svnNormal"/>
<typedef classname="org.tigris.subversion.svnant.selectors.Added" name="svnAdded"/>
<typedef classname="org.tigris.subversion.svnant.selectors.Replaced" name="svnReplaced"/>
<typedef classname="org.tigris.subversion.svnant.selectors.Modified" name="svnModified"/>
<typedef classname="org.tigris.subversion.svnant.selectors.Conflicted" name="svnConflicted"/>
<typedef classname="org.tigris.subversion.svnant.selectors.Ignored" name="svnIgnored"/>
<typedef classname="org.tigris.subversion.svnant.selectors.Unversioned" name="svnUnversioned"/>
<typedef classname="org.tigris.subversion.svnant.selectors.Locked" name="svnLocked"/>
<typedef classname="org.tigris.subversion.svnant.selectors.Missing" name="svnMissing"/>
<typedef classname="org.tigris.subversion.svnant.selectors.Deleted" name="svnDeleted"/>

<!-- Conditions -->
<typedef classname="org.tigris.subversion.svnant.conditions.Exists" name="svnExists"/>
<typedef classname="org.tigris.subversion.svnant.conditions.Available" name="svnAvailable"/>

</antlib>

신고
Posted by Layered 트랙백 0 : 댓글 0

댓글을 달아 주세요


이 장에서는 Tigris.org에서 제공하는 SVNANT를 이용하여 Apache ANT 내에서 SVN을 관리하는 방법에 대해 다루어보도록 하겠습니다.


이 글은 Apache ANT와 SVN를 이해하고 사용할 수 있다는 전제 하에 작성되었습니다. Apache ANTSVN(혹은 SCM)에 대해 잘 모르시는 분들은 필요한 그 각각에 대한 내용를 먼저 숙지하시기 바랍니다.

SVNANT를 이용하기 위해서는 JSDK와 ANT가 각각 설치되어 있어야 합니다.


지금이 때가 어느 때인데, Maven이 대세(?)인 이 시대에 먼 ANT냐, 그리고 Maven에도 SCM Plugin이 있으니 ANT는 필요 없다...라고 하실 수 있습니다만, 여전히 ANT를 이용하여 빌드자동화 및 배포자동화를 수행하는 프로젝트가 적지 않은데, 거기에는 몇 가지 이유가 있습니다. 여전히 Maven을 숙달하지 못한 개발자들이 많다는 것과 기존 프로젝트를 리뉴얼하는 경우 Maven을 적용하기 어렵다는 것이 가장 큰 이유가 될 것입니다. 그리고 더불어, 이클립스 Maven 플러그인의 간헐적인 오작동, 사내 Maven Repository 설치 곤란과 그로 인한 속도저하 등등의 기술적인 문제도 Maven의 선택을 막는 데에 일조하고 있습니다.


자 이제 시작해볼까요.


목차

  1. Download and Installation
  2. SVNANT Configuration
  3. SVNANT Tasks
    1. checkout
    2. copy
    3. diffSummarize
    4. update
  4. Conclusion


1. Download and Installation

SVNANT는 [여기]에서 다운로드 받으실 수 있습니다. 이 글을 쓰고 있는 시점에 배포되어 있는 SVNANT는 버전 1.3.1 입니다.

다운로드 완료 후 압축을 해제하면 다음과 같은 디렉토리 구조를 확인할 수 있습니다.

이제 lib 디렉토리 내에 있는 모든 jar 파일들을 $ANT_HOME/lib 디렉토리에 복사합니다. 이렇게 복사하는 것만으로 SVNANT에 대한 설치가 완료됩니다.


2. SVN ANT Configuration

SVNANT는 당연히 ANT 스크립트 내에서 실행됩니다. 하지만 SVNANT의 Task들은 ANT의 기본 Task가 아니므로 이를 사용하기 위해 별도의 설정이 필요합니다. 다음과 같이 말이죠. 

<?xml version="1.0" encoding="utf-8"?>
<project name="svn-info" basedir=".">
	<property file="build.properties"/> <!-- 설정파일로 build.properties 사용 -->
		
	<path id="ant.classpath"> <!-- SVNANT를 위한 추가 클래스패스 설정 -->
            <fileset dir="${ant.lib.dir}">
                <include name="**/*.jar"/>
            </fileset>
        </path>
	    
	<taskdef classpathref="ant.classpath" resource="net/sf/antcontrib/antlib.xml"/> <!-- ant-contrib task 설정 파일 -->
	<taskdef classpathref="ant.classpath" resource="org/tigris/subversion/svnant/svnantlib.xml"/> <!-- SVNANT task 설정 파일 -->
	
	<svnSetting
		id="svn.settings"
		username="${svn.user.id}"
	        password="${svn.user.pw}"
		svnkit="true"
		javahl="false"/> <!-- SVNANT 사용을 위한 svnSetting 정보 -->
</project>

이 ANT Script는 build.properties 파일을 사용하고 있습니다. 위에서 사용한 대부분의 변수들, 즉 ${ant.lib.dir}이나 ${svn.user.id} 등은 모두 build.properties 파일 내에 정의되어 있습니다.


예전 버전에서는 <svn> 태그 내에 SVN에 대한 설정 정보(username, password 등)들을 입력하였습니다. 하지만 1.3.0 버전 이후로는 <svnSetting> 태그를 이용하여 SVN 설정 정보를 처리하도록 변경되었습니다. 추후 <svn> 태그에서 이 설정 정보를 이용하게 될 것입니다.


3. SVNANT Tasks

SVNANT의 전체 Task들은 [여기]에서 확인할 수 있습니다. 이 글에서는 자주 사용하는 몇 가지만 다루도록 하겠습니다.


(1) checkout

당연한 얘기지만, 프로젝트 소스를 최초 체크아웃할 때 사용합니다. 참고로 svn checkout과 svn update의 차이는 [여기]서 확인하시기 바랍니다.

<svn refid="svn.settings"> 
	<checkout
		url="${svn.repository.url}/${svn.source.branch.name}/${projectName}" 
		destPath="${source.root.dir}/${projectName}"/> 
</svn>
  • url : 체크아웃 받고자 하는 프로젝트의 Full URL
  • destPath : 체크아웃받은 프로젝트가 저장될 로컬 디렉토리. 반드시 프로젝트 명을 추가해야 프로젝트 내에 파일이 체크아웃된다.

(2) copy
말 그대로 복사하는 기능입니다. 하지만 우리가 일반적으로 생각하는 파일/디렉토리 복사와는 다소 다르게 동작합니다. 자세한 내용은 Copy Documentation을 참고하시기 바랍니다. 아래 예제는 trunk에서 branches로 바로 Merge하는 예제입니다. Merge를 하는 데 Delete를 하는 이유는, SVNANT의 Copy 기능이 overwrite를 지원하지 않기 때문입니다. 묻지도 따지지도 않고 Target을 삭제한 뒤 Copy하는 것이므로 함부로 사용하면 안 됩니다.
	<property name="srcFile" value="x2-spring-core/src/main/java/org/x2framework/spring/X2Constants.java"/>
	
    <!-- File Merge를 수행한다. -->
	<target name="copy-svn-file">
		<propertyregex 
			property="targetDirectory"
			input="${srcFile}"
			regexp="^(.+)/([^/]+)$"
			select="\1"
		/>
		
		<echo message="copy file started : src=[${srcFile}] dest=[${targetDirectory}]"/>
		
        <svn refid="svn.settings">
        	<delete url="${svn.repository.url}/${svn.target.branch.name}/${srcFile}"
        	        force="true"
        	        message="delete file for merging"
        	/>
        </svn>
        <svn refid="svn.settings">
            <copy
            	srcUrl="${svn.repository.url}/${svn.source.branch.name}/${srcFile}"
                revision="${revision}"
            	destUrl="${svn.repository.url}/${svn.target.branch.name}/${targetDirectory}"
            	makeParents="true"
                message="merging from source"/>
        </svn>
        
        <echo message="copy file finished : src=[${srcFile}] dest=[${targetDirectory}]"/>
        
	</target>


(3) diffSummarize

Documentation에는 설명되어 있지 않지만, diff와 동일하게 동작합니다. 차이점이라면 diff는 파일의 내용까지 비교해주는 반면, diffSummarize는 revision 간 변경된 파일 목록만 보여줍니다. 특정 revision에서 배포된 파일 목록만 확인하고 싶다면 diffSummarize를 이용하면 됩니다.

<target name="diff-revision-of-directory">
	<svn refid="svn.settings">
	    <diffSummarize
	    	oldUrl="${svn.repository.url}/${svn.source.branch.name}"
	    	newUrl="${svn.repository.url}/${svn.source.branch.name}"
	    	oldTargetRevision="${oldRevision}"
	    	newTargetRevision="${newRevision}"
	    />
	</svn>
</target>

(4) update

저장소의 내용을 working directory로 갱신할 때 사용합니다. copy와 마찬가지로 revision 값을 이용하여 특정 revision으로 업데이트 할 수 있습니다.

<svn refid="svn.settings">
  <update
    dir="${source.root.dir}/${projectName}"
    recurse="true"/>
</svn>


4. Conclusion

간략하게나마 SVNANT를 이용하여 SVN을 ANT로 제어하는 방법을 살펴보았습니다. 개발/테스트/운영 환경을 위한 브랜치가 따로 있고, 테스트가 완료된 소스만 운영 서버로 merge를 해야 할 때, merge할 목록을 텍스트 파일로 만들고 그 파일을 ANT로 로딩하여 순차적으로 merge를 처리하도록 하면, merge부터 빌드, 배포까지 손쉽게 처리할 수 있습니다.


신고
Posted by Layered 트랙백 0 : 댓글 0

댓글을 달아 주세요

이클립스 플러그인이나 RCP(Rich Client Platform) 어플리케이션을 개발하다보면 No Schema found for the 'XXX' extention point 라는 Warning을 접할 때가 있습니다. 


이 문제는 PDE(Plug-in Development Environment) 버전이 아닌 Eclipse를 사용할 때 발생합니다. 그러므로, PDE 버전의 이클립스를 사용하면 해결이 됩니다만.... 설치한 플러그인이나 개발 환경 등등의 이유로 지금 사용하고 있는 이클립스를 그대로 사용하고 싶다면, 이를 해결하기 위해 무엇이든 방법을 찾아야 합니다.


이클립스의 Install/Update Manager를 이용하여 PDE 환경을 모두 update 받아도 문제가 해결되지 않습니다. 위의 몇 가지 Extension point에 대한 warning은 제거됩니다만, PDE 환경과 직접적인 관련이 없는 extension point의 경우에는 해결되지 않습니다. 간단한 해결 방법이 없는 것일까요...


놀라울 것도 없이, 간단한 해결 방법이 있습니다. schema가 포함되어 있는 plugin 파일을 이클립스의 Plugin 디렉토리에 옮겨주기만 하면 됩니다. 결국, schema를 포함하고 있는 jar 파일을 찾기만 하면 문제가 해결되는 것입니다.

그리고, "해당 extension point의 source jar 파일에 포함되어 있"습니다. 즉, org.eclipse.ui.editors extension point를 위한 schema 파일은 org.eclipse.ui.editors.source_[version].jar 파일에 포함되어 있습니다.


이제 source jar 파일을 다운로드 받을 수만 있다면 문제를 해결할 수 있습니다. 그리고 Extension Point를 위한 source jar 파일들은 이클립스 PDE 버전에 모두 포함되어 있습니다.(아... 결국 PDE 버전을 받아야 하는군요.) 아무튼, 이클립스 PDE 버전의 플러그인 디렉토리에 포함되어 있는 Extension Point들의 source jar 파일들을 원하는 이클립스의 플러그인 디렉토리에 복사하면 됩니다.


여기까지 한 뒤, 이클립스를 재기동하면 더 이상 No Schema.... 문제가 발생하지 않음을 확인할 수 있습니다.



신고
Posted by Layered 트랙백 0 : 댓글 0

댓글을 달아 주세요

헐리웃의 영화 관계자들 사이에서 격언처럼 사용되는 말이 있다고 합니다. 그것은 바로 "Don't call us, we'll call you." 인데요. 번역하면 "우리가 연락할테니 연락하지 마" 정도가 되겠죠.

눈치채셨겠지만 이 말이 그 바닥에서 유명하게 된 이유는, 바로 스타 지망생들의 전화공세 때문이라죠. 자신이 오디션에 합격했는지 확인하려는 지망생들의 전화 때문에, 관계자들이 업무를 볼 수 없을 정도가 되었던 것입니다. 그래서 생각해 낸 것이 바로, "네가 필요할 일이 있으면 우리가 연락할거야"라는 전략인 것이죠.


참으로 귀찮아 보이기까지 하는 이 짧은 말에는, 몇 가지 사전 조건이 포함되어 있습니다. 다음과 같은 조건들 말이죠.

- 영화 관계자는 지망생들의 목록을 관리하고 있다. (만난 적이 있다, 혹은 오디션을 본 적이 있다 등등)

- 영화 관계자에게는 지망생에게 연락해야 하는 특정 이유가 있다. (주연/조연/엑스트라/스텝 등 어떤 이유로든)

- 영화 관계자는 지망생의 연락처를 알고 있다. 혹은 연락할 수 있는 방법이 있다. (직접방문/전화/우편/지인을 통해 등등)


디자인 패턴 중 관찰자 패턴(Observer Pattern)은, 이 전략을 프로그래밍 영역으로 끌어온 것이라 할 수 있습니다. 개념적으로 간단하고 구현이 간편하기 때문에 수 많은 영역에서 이미 사용되고 있죠. 많은 프로그래밍 언어들이 언어 내에 관찰자 패턴의 기본적인 골격을 포함하고 있는 경우도 많이 있습니다.

그럼 이제, 관찰자 패턴에 대해 다룬 포스트 하나를 보면서 Java 프로그래밍 언어에서 관찰자 패턴이 어떻게 사용되고 어떻게 구현되는지 살펴보도록 하겠습니다. 


이 포스트의 원문 주소는 http://www.vogella.com/articles/DesignPatternObserver/article.html 입니다.


관찰자 패턴 (Observer Pattern)

1) 정의

관찰자 패턴은, 여러 객체 간의 일대다(one-to-many) 의존관계를 정의하는 디자인 패턴으로, 한 객체의 상태가 변경되었을 때 그 객체에 의존하고 있는 나머지 객체들이 변경 사항을 통보받고 자동적으로 어떤 동작을 수행하도록 하기 위해 사용됩니다.

여기서 관찰이 되는 객체를 대상(subject)이라 하고, 대상의 상태 변화를 주시하고 있는 객체를 관찰자(observer) 혹은 리스너(listener)라고 합니다.

 

2) 사용 예

관찰자 패턴은 Java 내에서 이미 흔하게 사용되고 있습니다. 예를 들어 사용자 인터페이스(UI)에서 사용되는 버튼(button)을 위해 리스너(listener)를 정의하는 것이 관찰자 패턴의 입니다. 버튼이 클릭되면 리스너가 그 내용을 통지받아 특정 동작을 수행하게 됩니다.

하지만 관찰자 패턴이 사용자 인터페이스 컴퍼넌트에서만 사용될 수 있는 것은 아닙니다.

예를 들어 어플리케이션 내에 현재 온도를 표시해주는 partA라고 하는 부분이 있다고 가정해봅시다. 그리고 partB는 현재 온도가 20도가 넘으면 녹색 불빛을 표시하기로 했다고 하면, partA의 상태 변화를 감지하기 위해 partBpartA에 자기 자신을 리스너로 등록해야 할 것입니다.

partA에서 기온이 변경되면 이벤트가 발생(trigger)될 것입니다. 그리고 이 이벤트는 등록되어 있는 모든 리스너들에게 전달될 것인데, 위의 경우에서는 partB가 전달을 받습니다. 이제 partB는 이벤트와 같이 전달된 정보를 얻어 화면을 갱신할 것입니다.

다음은 버튼에 리스너를 설정하는 예제 코드입니다.


Button button = new Button(shell, SWT.PUSH); 
button.addSelectionListener(new SelectionAdapter() {   
    @Override   
    public void widgetSelected(SelectionEvent e) {     
        // Handle the selection event         
        System.out.println("Called!");
    } 
}); 


3) 예제 코드

다음 예제는 Person 객체 내의 List 정보의 변경을 확인하는 관찰자를 만들 것입니다. 이를 위해 com.vogella.java.designpattern.observer.MyObserver 클래스와 그 외의 몇 가지의 클래스들을 이용할 것입니다.


package com.vogella.java.designpattern.observer;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;
import java.util.ArrayList;
import java.util.List;

public class MyModel {

    private List persons = new ArrayList();
    private List listeners = 
            new ArrayList();

    public class Person {

        private String firstName;

        private String lastName;

        public Person(String firstName, String lastName) {
            this.firstName = firstName;
            this.lastName = lastName;
        }

        public String getFirstName() {

            return firstName;
        }

        public void setFirstName(String firstName) {
            notifyListeners(this, "firstName", 
                    firstName, 
                    this.firstName = firstName);
            
        }

        public String getLastName() {
            return lastName;
        }

        public void setLastName(String lastName) {
            notifyListeners(this, "lastName", 
                    lastName, 
                    this.lastName = lastName);
        }
    }

    public List getPersons() {
        return persons;
    }

    public MyModel() {
        // Just for testing we hard-code the persons here:
        persons.add(new Person("Lars", "Vogel"));
        persons.add(new Person("Jim", "Knopf"));
    }

    private void notifyListeners(Object object, 
            String property, String oldValue, String newValue) {
        for (PropertyChangeListener listener : listeners) {
            listener.propertyChange(new PropertyChangeEvent(this, 
                            property, 
                            oldValue, 
                            newValue));
        }
    }

    public void addChangeListener(PropertyChangeListener newListener) {
        listeners.add(newListener);
    }
} 


package com.vogella.java.designpattern.observer;

import java.beans.PropertyChangeEvent;
import java.beans.PropertyChangeListener;

public class MyObserver implements PropertyChangeListener {
    public MyObserver(MyModel model) {
        model.addChangeListener(this);
    }

    @Override
    public void propertyChange(PropertyChangeEvent event) {
        System.out.println("Changed property: " + event.getPropertyName() + " old:"
                + event.getOldValue() + " new: " + event.getNewValue());
    }
}


package com.vogella.java.designpattern.observer;

import com.vogella.java.designpattern.observer.MyModel.Person;

public class Main {


  public static void main(String[] args) {
    MyModel model = new MyModel();
    MyObserver observer = new MyObserver(model);
    // We change the last name of the person, observer will get notified
    for (Person person : model.getPersons()) {
        person.setLastName(person.getLastName() + "1");
    }
    // We change the  name of the person, observer will get notified
    for (Person person : model.getPersons()) {
        person.setFirstName(person.getFirstName() + "1");
    }
  }
}


4) 평가

관찰자 패턴은 객체 지향 프로그래밍의 원칙 중 하나인 OCP(Open Closed Principle)을 가능케 합니다. 이 원칙은, 클래스들은 확장에는 열려(Open)있어야 하나 변경에는 닫혀(Closed) 있어야 함을 의미합니다.

관찰자 패턴을 이용하면, 대상(subject)은 관찰자를 숫자에 제한 없이 등록할 수 있게 됩니다. 새로운 리스너가 대상 객체를 관찰하려 한다 하더라도 대상 클래스의 코드에는 아무런 변경도 필요하지 않습니다.

, 관찰자 패턴은 대상 클래스를 관찰자 클래스로부터 분리(decouple)하도록 해 줍니다. 관찰자만이 대상 클래스의 존재를 인식할 뿐입니다.



아래는 관찰자 패턴의 UML 입니다.위의 내용을 잘 이해했다면 UML에 익숙하지 않은 분이라 하더라도 쉽게 이해가 되시리가 생각합니다. 위에서 이미 설명된 내용을 그저 UML로 표기한 것 뿐이니(UML이란 원래 그런 겁니다.) 더 이상의 자세한 설명은 생략하....


UML of Observer Design Pattern (출처 : Wikipedia)



지금까지 관찰자 패턴에 대해 간략하게 알아보았습니다. 

위 포스트에서 얼핏 소개가 된 내용이 있는데요, 객체 지향 프로그래밍의 원칙에는 크게 다섯 가지 원칙이 있습니다. 이른바, [SOLID principles] 라고 불리는 것인데요, 나중에 기회가 된다면 이에 대해 다뤄 보도록 하겠습니다.


여기까지 보신 분들 중, EDA라고 불리는 Event Driven Architecture와 관찰자 패턴은 어떻게 다른가 하는 의문이 드신 분들이 있을 것입니다. 실제로 특정 이벤트에 대한 처리를 위해 관찰자 패턴을 이용하여 구현한 경우가 많기에, 많은 개발자들이 이러한 질문을 해 오고 있습니다. 


과연 관찰자 패턴과 EDA는 완전히 같은 것일까요? 엇비슷한 것일까요? 아니면 혹시 포함 관계에 있는 것일까요? 이에 대한 내용은 다음에 EDA에 대해 다루면서 다시 한 번 살펴보도록 하겠습니다. 성격이 급한 분이라면, [Observer Pattern vs Event Driven Architecture]로 구글링하여 먼저 확인해 보실 수 있겠죠?

신고
Posted by Layered 트랙백 0 : 댓글 0

댓글을 달아 주세요