바로 이전 포스트에서 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

댓글을 달아 주세요

다소 도발적으로 들리는 제목이고, 실제로 도발적이지만, 사실 이 글은 작성한지 2년이나 지난 포스트를 '이제서야'보고 작성하는 글입니다. 먼저 그 문제의 글부터 보시죠.


ORM is an anti-Pattern

원문 : http://seldo.com/weblog/2011/08/11/orm_is_an_antipattern


그리 긴 포스트가 아님에도 불구하고, 친절하게 11줄 요약(... 이게 요약인지)까지 해 주었습니다. (참고 : TL; DR은 Too Long, Didn't Read의 약자입니다. 한 마디로 '너무 길어 읽지 않음' 이랄까요..

  • ORM is initially simpler to understand and faster to write than SQL-based model code
       ORM은 SQL기반 모델 코드에 비해 최초에는 쉽게 이해할 수 있고 빠르게 작성할 수 있다.
  • Its efficiency in the early stages of any project is adequate
       대부분의 프로젝트의 초기에는 ORM을 사용하는 것이 꽤 효율적이다.
  • Unfortunately, these advantages disappear as the project increases in complexity: the abstraction breaks down, forcing the dev to use and understand SQL
       불행히도, 프로젝트의 복잡성이 커질 수록 이러한 장점은 사라진다: 추상화는 무너지고, 개발자들은 SQL을 직접 사용해야하고 이해해야 한다.
  • Entirely anecdotally, I claim that the abstraction of ORM breaks down not for 20% of projects, but close to 100% of them.
       정말 개인적인 경험에 따르면, ORM의 추상화는 프로젝트의 20%에서만 사용될 수 없는 것이 아니라 거의 100%에서 사용할 수 없게 된다.
  • Objects are not an adequate way of expressing the results of relational queries.
       객체는 관계형 쿼리들의 결과를 표현하는 데 좋은 방식이 아니다.
  • The inadequacy of the mapping of queries to objects leads to a fundamental inefficiency in ORM-backed applications that is pervasive, distributed, and therefore not easily fixed without abandoning ORM entirely.
       쿼리 결과를 객체로 매핑하는 것이 적절치 않기 때문에, 여기저기서 마구 존재하는, ORM을 사용하는 어플리케이션들은 근원적으로 비효율적이다. 또 그러므로, ORM을 완전히 포기하지 않는 이상 고치기도 쉽지 않다. 
  • Instead of using relational stores and ORM for everything, think more carefully about your design
       모든 상황에 대해 관계형 저장소와 ORM을 사용하는 대신 설계에 대해 좀 더 주의 깊게 생각해보라.
  • If your data is object in nature, then use object stores ("NoSQL"). They'll be much faster than a relational database.
       사용하는 데이터가 본질적으로 객체형이라면, 객체 저장소("NoSQL")를 이용하라. 객체 저장소들이 관계형 데이터베이스보다 훨씬 빠르다.
  • If your data is relational in nature, the overhead of a relational database is worth it.
       사용하는 데이터가 본질적으로 관계형이라면, 관계형 데이터베이스에 오버헤드가 있다고 해도 쓸만한 가치가 있다.
  • Encapsulate your relational queries into a Model layer, but design your API to serve the specific data needs of your application; resist the temptation to generalize too far.
       관계형 쿼리들을 모델 레이어 내에 캡슐화하라. 하지만 특정 데이터에 대한 어플리케이션의 요구에 맞게 API를 설계하라. 과도하게 일반화하려는 유혹을 견뎌내라.
  • OO design cannot represent relational data in an efficient way; this is a fundamental limitation of OO design that ORM cannot fix.
       객체지향 설계는 관계형 자료를 효율적으로 표현할 수 없다. 이것은 ORM도 해결할 수 없는 객제지향 설계의 근원적인 한계이다.

  • 이마저도 길다고 하시는 분들을 위해 이 11줄 요약을 다시 한 번 요약해보면 다음과 같습니다.

    1. ORM은 개발 초기에만 쓸만하다. 

    2. 어플리케이션이 조금만 복잡해져도 HELL이 된다. 해 본 사람은 알 거다.

    3. 관계형 데이터베이스와 ORM을 쓰지 말라는 게 아니라, 관계형 데이터의 모든 것을 객체지향 설계에 주입하려고 하지 말라는 거다.


    어떤 분은 이 글을 읽고 ORM의 사용에 대해 찬성하기도, 혹은 반대하기도 할 것입니다. 하지만 ORM에 대한 절대적인 반대, 반감을 이끌어 내려는 것이 원 저자의 의도는 아니라고 생각합니다. 원 저자가 얘기하려고 하는 것, 그리고 제가 받아들인 바는, '객체 지향 설계라는 것이 지고지순한 선인 것은 아니다. 필요에 따라 우회할 줄 알아야 한다.' 라는 것이라고나 할까요.

    개인적으로 이런 글을 좋아합니다. 어떤 문제에 대한 '해결책'이라 불리는 것이 나왔을 때, 기계적으로 그것을 대입하는 것이 아니라, 똥인지 된장인지 알고 쓰자는 그런 류의 글 말이지요. 

    과거 JavaWorld에 이런 류의 글이 나와 논란이 된 적이 있었습니다. 바로 그 유명한 "Why extends is evil" 입니다. 수 많은 독자들이 이 글에 문제를 제기했고 급기야 '당신이 뭔데 OOP의 기본을 부정하냐?'라는 비난섞인 댓글까지 난무했었습니다.(한때, JavaWorld는 엄청 활동적인 사이트였었죠...) 하지만 이 글의 저자는, 이 도발적인 제목 아래, '클래스 상속, 제대로 설계하지 않고 쓰면 독이 된다. 그리고 상속보다는 인터페이스를 이용해라.'라는, 지금에와서는 지극히 상식적인 내용을 다뤘던 것이었습니다.  나중에 이 저자는 다시 글을 작성하여 본래의 의도를 다시 설명하기에 이릅니다. .... 나중에 기회가 되면 이 글에 대해서도 언급해보기로 하죠.


    그래서 저의 입장은 뭐냐구요? "애매"합니다. 하지만 확실한 것은, ORM을 사용한다 하더라도 결과적으로 Native SQL 혹은 ORM specific Query Language를 사용할 수 밖에 없다는 것입니다. 비즈니스의 복잡성으로 인해, 성능 이슈로 인해, DBMS에 특화된 기능을 사용하기 위해 등등. 결국 ORM이 어떤 문제를 해결해 주지만 또다른 많은 문제들을 야기함을 경험적으로 알고 있습니다. (이에 대해 CQRS - Command Query Responsibility Segregation을 주장하는 사람도 있습니다.) 저의 입장은 결국, '상황을 명확히 이해한 뒤에 결정해야 할 듯요?'가 되겠네요... 무책임하게도 말이죠.


    이 부분은 마치, 정규 표현식에 대한 이 미친듯한 센스를 가진 명언(?)을 생각나게 할지도 모르겠습니다. 이 명언을 처음 접하고 미친듯이 웃었었는데 말이죠..

    Some People, when confronted with a problem, think "I know, I'll use regular expressions". 
    Now they have two problems.


    여담입니다만, 외산 솔루션들을 보면(제가 하는 일의 성격상 외산 솔루션들을 분석하는 작업을 많이 하게 됩니다...), ORM을 적극적으로 사용하면서 그로 인한 성능 문제를 해결하기 위해 External Cache를 적극적으로 사용하는 것을 많이 봅니다. 이것이 해결책이 될 수 있을까요? 역시 어떤 부분에서는 해결책이, 또 다른 부분에서는 또다른 문제점을 야기할 수 있을 것입니다. 어쨌든 나중에 기회가 되면 External Cache에 대해서도 다뤄보기로 하죠.


    아참! 혹시나 오해하실까봐 말씀드립니다만, iBatis/MyBatis는 엄밀히 말해 ORM이 아닙니다. 이에 대한 수많은 글들을 찾아보실 수 있을 겁니다.


    무엇이든 객관적인 시각에서, 장점과 단점을 명확히 파악하고, 장점은 취하고 단점은 최소화하는 노력을 기울여야 함을, 우리 모두 알고 있지만 결코 쉬운 일이 아닌 듯 합니다. 인간은, 특히 저는 그다지 객관적이지 않거든요.

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

    댓글을 달아 주세요

    SyntaxHighlighter는 웹 페이지 내에서 여러가지 언어로 작성된 코드들을 보기 좋게 만들어주는 자바스크립트 엔진(?)이라 할 수 있습니다. 한 마디로 말해, 코드를 보기 좋게 해 준다는 것이지요. 아래 그림과 같이 말이죠.

    SyntaxHighlighter를 설치려면, 해당 홈페이지에 언급되어 있는 대로 다음 순서를 따르면 됩니다.


    1. Add base files to your page : shCore.js and shCore.css
        기반 파일들을 웹 페이지에 추가하세요 : shCore.js와 shCore.css

    2. Add brushes that you want (for example, shBrushJScript.js for Javascript, see the list of all available brushes)
        원하는 브러시를 추가하세요.(예를 들어 자바스크립트를 위한 브러시는 shBrushJScript.js입니다. 가용한 브러시 목록을 확인하세요)

    3. Include shCore.css and shThemeDefault.css
        shCore.css와 shThemeDefault.css를 포함시키세요.

    4. Create a code snippet with either <pre /> or <script /> method
        코드 조각을 <pre /> 태그나 <script /> 태그와 함께 작성하세요.

    5. Call SyntaxHighlighter.all() Javascript method
        SyntaxHighlighter.all() 자바스크립트 메소드를 호출하세요.


    실로 아주 간단한 방법으로 훌륭한 Code Highlighting 기능을 사용할 수 있게 됩니다. 

    그럼 이제 나의 티스토리 블로그에 SyntaxHighlighter를 설치하기로 합시다. 이 글을 쓰는 시점(2013.08.07)에 배포되어 있는 SyntaxHighlighter의 최신 버전은 3.0.83입니다.


    1. SyntaxHighlight 3.0.83 버전을 다운로드합니다.

    2. 다운로드한 ZIP 파일을 임시 디렉토리에 압축해제합니다. 다음과 같은 모양이 될 것입니다.

    3. 이들 파일들 중, 업로드해야 하는 파일들은 scripts 디렉토리와 styles 디렉토리 내의 js 파일과 css 파일들 입니다. 이들 파일을 티스토리에 업로드합니다. 블로그 관리모드로 들어가 HTML/CSS편집 > 파일업로드 > 추가 버튼을 이용합니다.

    4. 추가 버튼을 이용하여 모든 scripts 디렉토리와 styles 디렉토리 내의 js, css 파일들을 업로드합니다. 여러 개의 파일을 한꺼번에 업로드할 수 있으니 단 두 번의 업로드로 모든 파일을 업로드할 수 있습니다. 아쉬운 점은, 업로드한 파일들이 모두 images/ 디렉토리 하위에 위치하게 된다는 것이죠. 티스토리는 사용자가 특정 하위 디렉토리를 생성하는 것을 왜 막았을까요?

    5. 아무튼 이렇게 업로드가 되었다면, 업로드된 파일이 정상적으로 접근되는지 확인해봅니다. 티스토리는 스킨 컨텐츠를 CDN으로 서비스하며, 각각의 블로그마다 별도의 디렉토리를 만들어 파일을 관리하고 있습니다. 제 블로그에서 업로드한 파일은 다음 URL로 접근할 수 있습니다. http://ts.daumcdn.net/custom/blog/156/1568579/skin/images/shCore.js 즉, 제 블로그의 CDN 디렉토리는 156/1568579 네요. 

    6. 이제 HTML/CSS 탭으로 이동하여, SyntaxHighlight를 설치합니다. 아래와 같이 필요한 파일들을 추가하고 SyntaxHighlighter.all()을 호출합니다. 물론, 만일을 위해 기존의 skin.html 파일은 백업해 두고, 새로 추가하는 부분 외에는 건드리지 않도록 합니다.

    7. 마지막으로 저장 버튼을 누르면 SyntaxHighlight에 대한 설치가 완료됩니다.


    당연한 이야기지만, SyntaxHighlight는 HTML 태그에 대해 적용되므로, 블로그에 글을 작성할 때 HTML 직접 작성을 이용하야 합니다. 한 번 해보죠.


    public static void main(String args[]){
        System.out.println("Hello, World!");
    }
    


    잘 되나요? 저는 잘 되는 듯 하네요. 

    상세 설정은 [여기]로, brush 목록은 [여기]에서 확인해 보시기 바랍니다.

    여러 가지 설정을 추가하여 보기 좋게 만들어 봅시다. 지금까지 SyntaxHighlight를 티스토리 스킨에 추가하는 방법에 대해 알아보았습니다.

    저작자 표시 비영리 변경 금지
    신고
    Posted by Layered 트랙백 0 : 댓글 0

    댓글을 달아 주세요