저작권 안내: 저작권자표시 Yes 상업적이용 No 컨텐츠변경 No

스프링5 입문

JSP 2.3

JPA 입문

DDD Start

인프런 객체 지향 입문 강의
비즈니스 로직 분리를 위해 Drools 룰 엔진을 사용하는 방법을 간단하게 살펴본다.

룰 엔진의 필요성

프로그래밍은 로직을 구현하는 과정이라고 할 수 있다. 로그인 처리부터, 사용자 권한 인증 처리, 금융 관련 처리 등 모든 것이 로직과 관련된 것이다.

이런 로직 중에서는 거의 변하지 않는 것도 있지만, 매우 빈번하게 변경되는 것도 있다. 예를 들어, 보험을 생각해보자. 보험상품은 매년 다양하게 출시되고 이들 보험은 보험자의 나이에 따라, 그리고 보험자의 이력에 따라서 보험금액이 달라진다. 또한, 중간에 다양한 조건에 따라 보험료가 변경되는 경우도 있다.

로직이 변경되는 건 금융과 같은 분야만 그런 것이 아니다. 일반적인 웹 사이트도 수시로 로직이 변경된다. 예를 들어, 쇼핑 사이트를 생각해보자. 개학 시즌을 맞이하여 사이트 전면에 학생들을 위한 컴퓨터 제품을 배치했는데, 낮 시간대엔 오히려 일반 가전이 많이 나갔다고 해 보자. 원인을 파악해보니 혼수 준비를 하는 사람들이 낮시간대에 주로 주문을 한 것이었다. 그래서 이 쇼핑 사이트는 낮 시간대에는 혼수를 전면에 세우고 나머지 시간대에는 컴퓨터를 전면에 세우기로 했다.

두가지 상황을 살펴봤는데, 이 두가지 상황 모두 수시로 로직이 변경되는 경우이다. 이렇게 로직이 수시로 변경되는 경우 개발자들은 매번 프로그램을 수정해줘야만 한다. 아주 작은 조건의 변화라도 프로그램을 수정하고, 수정한 프로그램을 배포하는 작업을 해야만 한다.

이렇게 로직이 수시로 변경되는 경우에 사용할 수 있는 것이 바로 룰 엔진(Rule Engine)이다. 룰 엔진을 롤(Rule; 규칙, 즉 로직)을 별도로 저장해두고 프로그램에서 룰을 가져다 쓸 수 있도록 해 주는 기능을 제공한다. 프로그램에 있던 로직이 룰 엔진으로 옮겨가고, 프로그램에서는 롤 엔진을 통해서 룰(로직)을 실행하기만 하면 된다.

로직이 변경되는 경우에는 프로그램을 변경할 필요 없이 룰 엔진을 통해서 룰만 변경해주면 된다. 더 이상 프로그램을 수정하고 재배포하는 등의 작업이 필요없는 것이다. 따라서, 로직이 수시로 변경되는 곳에서는 룰 엔진을 적용함으로써 개발 시간을 단축시킬 수 있는 장점이 있다. 또한, 동일한 로직이 여러 코드에서 사용되는 경우에도 룰 엔진의 적용을 생각해볼 수 있다.

자바에서 사용가능한 룰 엔진에는 Jess, Drools, Jena 등 다양한 것들이 존재하는데, 본 글에서는 무료로 사용할 수 있는 Drools 룰 엔진의 사용방법에 대해서 살펴볼 것이다.

Drools 룰 엔진 빠르게 사용해보기

Drools 룰 엔진을 사용하는 절차는 다음과 같다.

  1. Drools 엔진 다운로드
  2. 룰 파일 작성
  3. 룰 사용하기
Drools 엔진 다운로드 및 설치

Drools 엔진은 다음의 사이트에서 다운로드 받을 수 있다.

이 글을 쓰는 시점에서 최신 버전은 2.5이다. 이 글에서는 Drools가 사용하는 라이브러리 모듈이 함께 포함된 배포 버전인 drools-2.5-final-bin-withdeps.zip 파일을 다운로드 받았다. 이 파일의 압축을 풀면 매우 많은 jar 파일이 풀리는 데, 이 중에서 다음의 파일을 파일을 클래스패스에 추가해주면 된다.

  • drools-all-jdk5-2.5-final.jar
  • antlr-2.7.5.jar
  • janino-2.3.15.jar
  • xml-apis-2.0.2.jar
  • xercesImpl-2.7.1.jar
클래스패스에 추가해주었다면 Drools 엔진을 사용할 준비가 끝난 것이다.

Drools 엔진은 자바의 룰 엔진 표준인 JSR-94를 지원하는데, JSR-94 API를 사용하는 경우 추가적으로 JSR 관련 jar 파일을 추가해주어야 한다. 또한, 자바 언어가 아닌 Python이나 Groovy로 룰 파일을 작성할 수가 있는데, 이 경우에도 약간 클래스패스의 설정이 달라진다. 이에 대한 배포파일이 포함된 README.txt 파일을 참고하기 바란다.
룰 파일 작성하기

Drools 룰 엔진을 설치한 다음에 할 작업은 룰 파일을 작성하는 것이다. 룰 파일은 비즈니스 로직을 담는 파일로서 Drools 룰 엔진을 사용하는 데 있어서 핵심 요소에 해당한다. 룰 파일의 요소에 대해서 살펴보기 전에 먼저 아주 간단한 룰 파일을 살펴보자.

    파일명: hello.drl    
    <?xml version="1.0" encoding="euc-kr" ?>
    
    <rule-set name="Hello"
              xmlns="http://drools.org/rules"
              xmlns:java="http://drools.org/semantics/java"
              xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
              xs:schemaLocation="http://drools.org/rules rules.xsd
                                 http://drools.org/semantics/java java.xsd">
        
        <import>java.lang.Object</import>
        <import>java.lang.String</import>
    
        <java:functions>
            public static void helloWorld(java.lang.String hello) {
                System.out.println(hello + " World");
            }
            public static void goodbyeWorld(String goodbye) {
                System.out.println(goodbye + " Cruel World");
            }
        </java:functions>    
        <rule name="Hello World">
            <parameter identifier="hello">
                <class>String</class>
            </parameter>            
            <java:condition>hello.equals("Hello")</java:condition>            
            <java:consequence>
                helloWorld( hello );
            </java:consequence>
        </rule>        
        <rule name="Goodbye Cruel World">
            <parameter identifier="goodbye">
                <class>String</class>
            </parameter>
            
            <java:condition>goodbye.equals("Goodbye")</java:condition>
    
            <java:consequence>
                goodbyeWorld( goodbye );
            </java:consequence>
        </rule>
    
    </rule-set>

룰 파일에서 각 태그는 다음과 같은 역할을 한다.

  • import
    자바의 import와 동일하게 룰 파일 내에서 사용할 클래스를 명시

  • java:functions
    룰 파일에서 사용될 함수. 자바의 메소드와 동일하다.

  • rule
    하나의 규칙을 표현. 룰을 실행할 때 필요한 파라미터, 실행 조건 그리고 조건 충족시 실행될 코드로 구성된다.
여기서 가장 중요한 태그는 rule 태그이다. rule 태그는 하나의 룰을 표현해주는 태그로서 다음의 3가지로 구성된다.

  • parameter
    룰을 실행하는 데 필요한 파라미터 값. identifier 속성을 사용해서 룰에서 사용할 파라미터의 이름을 지정한다.

  • java:condition
    룰을 실행하기 위한 조건

  • java:consequence
    조건이 충족될 경우 실행될 코드

예를 들어, 앞서 봤던 룰 파일에서 이름이 'Hello World'인 룰을 보자. 이 룰은 다음과 같이 실행된다.

  1. String 타입인 hello 파라미터를 입력받는다.
  2. 전달받은 hello 파라미터의 값이 "hello"인지 검사한다.
  3. 검사 결과가 true라면, helloWorld( hello )를 실행한다.
만약 조건이 맞지 않을 경우에는 롤이 실행되지 않는다.

룰 사용하기

앞서 작성한 룰 파일을 실제로 사용해보자. 룰 파일을 사용하는 자바 코드는 다음과 같다.

    파일명: HelloExample.java    
    package javacan.drools.test;
    
    import java.io.IOException;
    
    import org.drools.FactException;
    import org.drools.IntegrationException;
    import org.drools.RuleBase;
    import org.drools.WorkingMemory;
    import org.drools.io.RuleBaseLoader;
    import org.xml.sax.SAXException;
    
    public class HelloExample {
        public static void main(String[] args) 
        throws IntegrationException, SAXException, IOException, FactException {
            RuleBase ruleBase = RuleBaseLoader.loadFromUrl(
                    HelloExample.class.getResource("hello.drl") );
    
            WorkingMemory workingMemory = ruleBase.newWorkingMemory();
            workingMemory.assertObject("Hello");
            workingMemory.fireAllRules(); // 첫번째 실행
            
            workingMemory = ruleBase.newWorkingMemory();
            workingMemory.assertObject("Goodbye");
            workingMemory.fireAllRules(); // 두번째 실행
        }
    }

룰을 사용하는 코드는 크게 4가지 순서로 구성된다.

  1. 룰 파일로부터 RuleBase를 생성한다: RuleBaseLoader.loadFromUrl()
  2. RuleBase로부터 WokringMemory를 생성한다: ruleBase.newWokringMemory()
  3. WokringMemory에 파라미터로 사용될 객체를 추가한다: workingMemory.assertObject()
  4. 룰을 적용한다: workingMemory.fireAllRules()
WorkingMemory.fireAllRules()를 실행하면 알맞은 룰이 실행된다. HelloExample 클래스를 실행한 결과를 토대로 추가 설명을 하겠다. HelloExample 클래스의 실행 결과는 다음과 같다.

    Hello World
    Goodbye Cruel World

실행결과의 첫번째 줄인 "Hello World"는 첫번째 workingMemory.fireAllRules() 실행에서 출력된 결과이고, 두번째 줄인 "Goodbye Cruel World"는 두번째 workingMemory.fireAllRules() 실행에서 출력된 결과이다.

먼저 첫번째 실행 결과부터 분석해보자. 첫번째 실행 때에는 파라미터로 "Hello"가 전달됐다. 앞서 살펴봤던 룰 파일에는 두개의 룰이 있는데, 두 룰 부분만 다시 살펴보면 아래와 같다.

        <rule name="Hello World">
            <parameter identifier="hello">
                <class>String</class>
            </parameter>
            
            <java:condition>hello.equals("Hello")</java:condition>            
            <java:consequence>
                helloWorld( hello );
            </java:consequence>
        </rule>
        
        <rule name="Goodbye Cruel World">
            <parameter identifier="goodbye">
                <class>String</class>
            </parameter>
            
            <java:condition>goodbye.equals("Goodbye")</java:condition>    
            <java:consequence>
                goodbyeWorld( goodbye );
            </java:consequence>
        </rule>

"Hello World" 룰은 파라미터의 값이 "Hello"인 경우에 실행되고, "Goodbye Cruel World" 룰은 파라미터 값이 "Goodbye"인 경우에만 실행된다. 그런데, 첫번째 실행에서는 파라미터 값을 "Hello"로 지정했으므로(workingMemory.assertObject("Hello")) "Hello World" 룰만 실행되고, 다른 룰을 실행되지 않는다.

반대로 파라미터 값이 "goodbye"인 경우에는 "Goodbye Cruel World" 룰이 실행된다. 그래서 실행 결과가 위와 같은 것이다.

작업 메모리와 룰 적용

룰 엔진에는 전향 연결(Forward chaining) 방식과 후향 연결(Reverse Chaining) 방식이 있는데, Drools는 전향 연결 방식을 사용한다. 전향 연결은 귀납법적인 연결방식으로 다음과 같은 방식으로 룰을 적용한다.

    if 조건 
    then 룰 실행

즉, 조건을 비교한 다음에 조건을 충족할 때에만 룰을 실행하는 방식이다.

조건을 검사할 때에는 비교 대상이 될 정보가 필요한데, 이 정보가 저장된 장소를 작업 메모리(Working Memory)라고 부른다. 작업 메모리에 비교할 정보를 추가할 때에는 WorkingMemory.assertObject() 메소드를 사용한다. 앞서 살펴봤던 예제에서는 작업 메모리에 한개의 객체만 추가했지만 여러개의 객체를 추가할 수도 있다. 예를 들어 앞서 살펴봤던 HelloExample을 다음과 같이 변경할 수도 있다.

    workingMemory.assertObject(new Integer(3));
    workingMemory.assertObject("Hello");
    workingMemory.assertObject("안녕");
    workingMemory.assertObject("Goodbye");

이 경우 각 객체에 대해서 차례대로 룰을 검사해본다. 먼저 첫번째 정보는 Integer 객체인데 이 경우에는 어떤 룰도 적용되지 않는다. 왜냐면, hello.drl에 있는 모든 룰은 파라미터의 타입이 String 이기 때문에, Integer는 룰에 전달되지 않는다. "안녕" 값의 경우에는 두 룰에 모두 전달되지만 어떤 룰의 실행 조건도 만족하지 못하기 때문에, 역시 룰이 적용되지 않는다. "Hello" 값에 대해서는 "Hello World" 룰이 적용되며, "Goodbye" 값에 대해서는 "Goodbye Cruel World" 룰이 적용된다.

조건을 충족시키는 여러개의 룰이 존재할 수도 있다. 예를 들어, 아래의 룰 집합을 생각해보자.

    <rule name="Goodbye Cruel World">
        <parameter identifier="goodbye">
            <class>String</class>
        </parameter>
        
        <java:condition>goodbye.equals("Goodbye")</java:condition>

        <java:consequence>
            goodbyeWorld( goodbye );
        </java:consequence>
    </rule>

    <rule name="Debug">
        <parameter identifier="obj">
            <class>Object</class>
        </parameter>
        
        <java:consequence>
            System.out.println("객체 : "+obj);
        </java:consequence>
    </rule>

이때 다음과 같이 작업 메모리에 객체를 추가했다면,

    workingMemory.assertObject("Goodbye");

이 경우, "Goodbye Cruel World" 룰과 "Debug" 룰 모두 적용된다. ("Debug"룰은 조건 부분이 없으므로 파라미터 타입이 Object 이기만 하면 실행된다.) 이 때에는 정해진 규칙에 따라 룰의 적용 순서가 결정된다. 다음은 기본적인 룰의 적용 순서 규칙이다.

  • Salience: 룰의 실행 순서를 할당한 값. 큰 값이 먼저 호출
  • Recency: 얼마나 많이 룰이 사용되었는지의 여부
  • Complexity: 더욱 복잡한 값을 가진 룰이 먼저 호출
  • LoadOrder: 룰의 로딩 순서에 따라 호출
본 글에서는 간단한 예를 통해서 Salience와 LoadOrder에 대해 살펴보도록 하겠다.

상품 할인 적용 예

대형 할인마트 같은 곳을 가보면 할인 조건이 다양하게 제시되곤 한다. 예를 들어, 다음과 같은 할인 행사가 있었다고 해보자.

  • 전 상품 3개 이상 구매시 3% 할인
  • 초코파이 10박스와 빅파이 10박스를 함께 구매하면 둘다 추가로 2.5% 추가할인
이를 위한 로직 파일은 다음과 같이 작성할 수 있다.

    파일명: discountRate.drl    
    <?xml version="1.0" encoding="euc-kr" ?>
    
    <rule-set name="Hello"
              xmlns="http://drools.org/rules"
              xmlns:java="http://drools.org/semantics/java"
              xmlns:xs="http://www.w3.org/2001/XMLSchema-instance"
              xs:schemaLocation="http://drools.org/rules rules.xsd
                                 http://drools.org/semantics/java java.xsd">
        
        <import>java.lang.Object</import>
        <import>java.lang.String</import>
        <import>javacan.drools.test.Order</import>
    
        <rule name="Rule1 3개 초과 3% 할인">
            <parameter identifier="order">
                <class>Order</class>
            </parameter>
            
            <java:condition>order.getNumber() >= 3</java:condition>
            <java:condition>order.getDiscountRate() == 0.0</java:condition>
            
            <java:consequence>
                order.setDiscountRate(3.0);
                System.out.println(order.getName() + " 3% 할인됨");
            </java:consequence>
        </rule>
    
        <rule name="Rule2 초코파이 빅파이 동시 10박스 구매시 추가 2.5% 할인">
            <parameter identifier="order1">
                <class>Order</class>
            </parameter>
            <parameter identifier="order2">
                <class>Order</class>
            </parameter>
            
            <java:condition>order1.getName().equals("초코파이") &&
                order1.getNumber() == 10</java:condition>
            <java:condition>order2.getName().equals("빅파이") &&
                order2.getNumber() == 10</java:condition>
            
            <java:consequence>
                order1.setDiscountRate(order1.getDiscountRate() + 2.5);
                order2.setDiscountRate(order2.getDiscountRate() + 2.5);
                System.out.println(order1.getName() + " 2.5% 추가 할인됨");
                System.out.println(order2.getName() + " 2.5% 추가 할인됨");
            </java:consequence>
        </rule>
    </rule-set>

먼저 Rule1은 Order.getNumber()가 3 이상이고 Order.getDiscountRate()가 0.0인 경우에 적용된다. Rule2는 두개의 파라미터를 받는데, order1.getName()이 초코파이이고 order1.getNumber()가 10개이고 그리고 order2.getName()이 "빅파이"이고, order2.getNumber()가 10개인 경우에 실행된다.

Rule1이 적용되면 discountRate를 3.0 으로 지정한다. Rule2가 적용되면, 각 Order 객체의 discountRate 값을 2.5 증가시킨다.

실제로 이 룰이 올바르게 적용되는 지 확인하기 위해 다음과 같은 코드를 작성해보았다.

    파일명: BuyItemTest.java    
    package javacan.drools.test;
    
    import java.io.IOException;
    
    import org.drools.FactException;
    import org.drools.IntegrationException;
    import org.drools.RuleBase;
    import org.drools.WorkingMemory;
    import org.drools.io.RuleBaseLoader;
    import org.xml.sax.SAXException;
    
    public class BuyItemTest {
        public static void main(String[] args)
        throws IntegrationException, SAXException, IOException, FactException {
            Order order1 = new Order("초코파이", 10, 3000);
            Order order2 = new Order("수퍼타이", 5, 1000);
            Order order3 = new Order("빅파이", 10, 2000);
            Order order4 = new Order("콜라", 2, 800);
            
            RuleBase ruleBase = RuleBaseLoader.loadFromUrl(
                    BuyItemTest.class.getResource( "discountRate.drl" ) );
    
            WorkingMemory workingMemory = ruleBase.newWorkingMemory( );
            workingMemory.assertObject(order1);
            workingMemory.assertObject(order2);
            workingMemory.assertObject(order3);
            workingMemory.assertObject(order4);
            workingMemory.fireAllRules();
            
            System.out.println("\n\n할인 적용 결과---");
            System.out.println(order1.getName()+":"+order1.getNumber()+" - " + order1.getDiscountRate() +"% 할인");
            System.out.println(order2.getName()+":"+order2.getNumber()+" - " + order2.getDiscountRate() +"% 할인");
            System.out.println(order3.getName()+":"+order3.getNumber()+" - " + order3.getDiscountRate() +"% 할인");
            System.out.println(order4.getName()+":"+order4.getNumber()+" - " + order4.getDiscountRate() +"% 할인");
        }
    }

이 코드는 "초코파이"와 "빅파이"를 10개 사고, "수퍼타이"와 "콜라"를 각각 5개, 2개 사고 있다. 앞서 할인 조건에 의해 "수퍼타이"는 3% 할인되고, "초코파이"와 "빅파이"는 5.5% 할인되어야 한다. 실제로 BuyItemTest 클래스를 실행해보면 다음과 같이 룰이 올바르게 적용되는 것을 확인할 수 있다.

    초코파이 3% 할인됨
    수퍼타이 3% 할인됨
    빅파이 3% 할인됨
    초코파이 2.5% 추가 할인됨
    빅파이 2.5% 추가 할인됨
    
    
    할인 적용 결과---
    초코파이:10 - 5.5% 할인
    수퍼타이:5 - 3.0% 할인
    빅파이:10 - 5.5% 할인
    콜라:2 - 0.0% 할인

만약 Rule1과 Rule2의 순서를 변경하면 어떻게 될까? 그러면, 다음과 같은 결과가 출력된다.

    초코파이 3% 할인됨
    수퍼타이 3% 할인됨
    초코파이 2.5% 추가 할인됨
    빅파이 2.5% 추가 할인됨
    빅파이 3% 할인됨
    
    
    할인 적용 결과---
    초코파이:10 - 5.5% 할인
    수퍼타이:5 - 3.0% 할인
    빅파이:10 - 3.0% 할인
    콜라:2 - 0.0% 할인

"초코파이"는 올바르게 적용됐는데, "빅파이"는 원하는 결과가 출력되지 않은 것을 알 수 있다. 이는 작업 메모리에서 "빅파이"에 해당하는 Order 객체를 검사할 때 "Rule1"과 "Rule2"과 모두 적용되는데 LoadOrder 원칙에 따라 "Rule2"가 먼저 적용되고 "Rule1"이 나중에 적용됐기 때문에 발생한 결과이다. 이럴 때 규칙의 로딩 순서에 상관없이 Rule1을 먼저 적용하고 Rule2를 나중에 적용하고 싶다면 다음과 같이 salience 속성을 사용하면 된다.

    <rule name="Rule2 초코파이 빅파이 동시 10박스 구매시 추가 2.5% 할인" salience="-1">
        ...
    </rule>
    
    <rule name="Rule1 3개 초과 3% 할인">
        ...
    </rule>

그러면 "Rule2"가 "Rule1" 보다 이후에 실행되기 때문에 원하는 결과를 얻을 수 있다.

결론

본 글에서는 간단하게 Drools 엔진을 사용하는 방법을 살펴보았다. Drools 룰 엔진은 조건 비교와 룰의 실행 코드에서 자바 언어를 그대로 사용할 수 있기 때문에 자바에 익숙한 개발자라면 누구나 쉽게 사용할 수 있다는 장점을 갖고 있다. 게다가 자바 룰 엔진 표준인 Java Rule Engine API(JSR-94)를 지원하기 때문에 이후에 자바 표준 API로의 전환되 쉽다. 따라서, 룰 엔진이 필요한 경우, Drools는 상당히 매력적인 선택이 될 수 있다.

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요

Commons Email을 사용하여 이메일을 발송하는 방법을 살펴본다.

Commons Email을 사용한 메일 발송하기

Jakarta Commons Email API는 이메일 발송을 쉽게 처리하기 위한 클래스를 제공하는 모듈로서, 내부적으로 Java Mail API와 JavaBeans Activation API를 사용한다. Commons Email API는 메일 발송을 처리해주는 SimpleEmail, HtmlEmail과 같은 클래스를 제공하고 있으며, 이들 클래스를 사용하여 일반 텍스트메일, HTML 메일, 첨부 메일 등을 매우 간단(simple!!)하게 발송할 수 있다. 본 글에서는 Jakarta Commons Email API를 사용하여 쉽게 메일을 발송하는 방법에 대해서 살펴보도록 하겠다.

Commons Email 다운로드 및 설치

Commons Email은 아래의 URL에서 다운로드 받을 수 있다.

위 URL에서 바이너리 파일 commons-email-1.0.zip을 다운로드 받은 후 압축을 풀면 commons-email-1.0.jar 파일이 생성되는데, 이 파일을 클래스패스에 추가해주면 Commons Email API를 사용할 수 있게 된다. 앞서 언급한대로 Commons Email API는 Java Mail API와 JavaBeans Activation API를 내부적으로 사용하고 있기 때문에 이 두 API도 클래스패스에 추가해주어야 한다. 이 두 API는 아래의 URL에서 다운로드 받을 수 있다.

Commons Email API, Java Mail API 그리고 JavaBeans Activation API가 준비됐으면 이제 메일 발송할 준비가 끝난 셈이다.

간단한 텍스트 메일 보내기

org.apache.commons.mail.SimpleEmail 클래스를 사용하면 일반 텍스트 메일을 발송할 수 있다. SimpleEmail 클래스를 사용해서 텍스트 메일을 발송하는 코드는 다음과 같다. (별다른 설명이 필요없을 정도로 간단하다.)

    SimpleEmail email = new SimpleEmail();
    email.setCharset("euc-kr");    email.setHostName("mail.somehost.com");  // SMTP 서버를 지정
    email.addTo("madvirus@empal.com", "최범균"); // 수신자를 추가
    email.setFrom("madvirus@madvirus.net", "범균"); // 보내는 사람 지정
    email.setSubject("텍스트 테스트 메일입니다."); // 메일 제목
    email.setContent("테스트 메일의 내용입니다.", "text/plain; charset=euc-kr");
    email.send(); // 메일 발송

위에서 주의할 코드는 굵게 표시한 부분의 코드이다. 먼저, email.setCharset("euc-kr")을 사용해서 메일의 캐릭터셋이 euc-kr 이라고 지정하고 있는데, 이를 표시하지 않으면 메일 제목이나 보내는 사람 이름등에 있는 한글이 깨지게 된다. 더 중요한 점은 email.setCharset("euc-kr")을 실행해도 메일 내용의 한글은 올바르게 처리되지 않는다는 점이다. 메일 내용에 포함된 한글을 올바르게 처리하기 위해서는 위 코드에서와 같이 email.setContent()를 실행할 때, 두번째 인자값으로 캐릭터셋을 지정해주어야 한다. (이를 지정하지 않으면 내용의 한글이 깨지므로 꼭 기억해야 한다.)

HTML 메일 보내기

HTML 형식으로 된 이메일을 보내는 방법은 텍스트 형식의 메일을 보내는 것과 매우 비슷하다. 차이점은 org.apache.commons.mail.HtmlEmail 클래스를 사용한다는 점이다. 다음 코드는 HtmlEmail 클래스를 사용하여 HTML 형식의 메일을 보내는 예제 코드이다.

    HtmlEmail email = new HtmlEmail();
    email.setCharset("euc-kr");
    email.setHostName("mail.somehost.com");
    email.addTo("madvirus@empal.com", "최범균");
    email.setFrom("madvirus@madvirus.net", "범균");
    email.setSubject("HTML 테스트 메일입니다.");
    
    email.setHtmlMsg("<html>아파치 로고 - <img src=\"http://www.apache.org/images/asf_logo_wide.gif\"></html>");
    email.send();

SimpleEmail을 보내는 경우와 달리 email.setHtmlMsg()를 사용하여 메일 내용을 입력할 때 캐릭터셋을 별도로 지정하지 않아도 한글이 깨지지 않는다.

파일 첨부하기

파일을 첨부하기 위해서는 org.apache.commons.mail.EmailAttachment 클래스와 org.apache.commons.mail.MultiPartEmail 이메일을 사용하면 된다. EmailAttachment 클래스는 첨부할 파일을 지정할 때 사용하고, MultiPartEmail 이메일을 파일을 이메일에 첨부해주는 기능을 제공한다. 다음은 파일 첨부 예제 코드이다.

    EmailAttachment attachment = new EmailAttachment();
    attachment.setPath("d:\\commons-email-1.0.zip");
    attachment.setDisposition(EmailAttachment.ATTACHMENT);
    attachment.setDescription("commons-email api");
    attachment.setName("commons-email-1.0.zip"); // 파일의 이름을 지정
    
    MultiPartEmail email = new MultiPartEmail();
    email.setCharset("euc-kr");
    email.setHostName("mail.somehost.com");
    email.addTo("madvirus@empal.com", "최범균");
    email.setFrom("madvirus@madvirus.net", "범균");
    email.setSubject("첨부 파일 테스트 메일입니다.");
    email.setMsg("이건 내용입니다.");
    
    email.attach(attachment);    
    email.send();

EmailAttachment 객체를 생성한 뒤 email.attach()를 사용해서 첨부할 파일을 추가해주기만 하면 된다. 주의할 점은 1.0 버전의 Commons Email은 파일명을 한글로 전달할 경우, 파일명이 올바르게 전달되지 않고 깨져서 간단하는 점이다. (파일 자체는 올바르게 전송된다.) 따라서 1.0 버전의 Common Email을 사용하여 파일을 전송할 때에는 알파벳과 숫자로만 구성된 이름의 파일을 전송하도록 하자.) 실제 파일명은 한글이 포함되더라도, EmailAttachment.setName() 메소드를 사용해서 파일명을 변경해서 전송할 수도 있다.

앞서 살펴봤던 HtmlEmail 클래스는 MultiPartEmail 클래스를 상속받기 때문에 HtmlEmail 클래스를 사용할 때에도 attach() 메소드를 사용하여 파일을 첨부할 수 있다.

SMTP 인증 처리

SMTP 서버를 사용할 때에는 SMTP 인증이 필요한 경우도 있는데, 이럴 때에는 다음과 같이 setAuthentication() 메소드를 사용하면 된다.

    SimpleEmail email = new SimpleEmail();
    email.setCharset("euc-kr");
    email.setHostName("mail.somehost.com");
    email.setAuthentication("madvirus", "password");
    ...

관련링크:


Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 아라리요 2013.07.17 14:15 신고  댓글주소  수정/삭제  댓글쓰기

    작성하신지 오래되어서 그런지 링크걸어주신곳에서 받을 수 있는 jar가 java mail api뿐입니다. 혹시 갱신해주실 수 있으신가요?

  2. 아라리요 2013.07.24 12:25 신고  댓글주소  수정/삭제  댓글쓰기

    감사합니다 ^^
    근데 제 로컬에서는 잘 되는데 서버에 올리니까 메일 발송에서 에러가 나네요 ㅎ;;
    메일 서버에 릴레이를 설정해주라는 말을 어디서 본거 같은데 무슨 말인지...

    • 최범균 madvirus 2013.07.29 11:38 신고  댓글주소  수정/삭제

      이건,, 메일 서버의 릴레이 허용 부분을 설정해 주어야 할 것 같은데요. 이 부분은 메일 서버 마다 설정하는 방법이 달라서, 메일 서버를 관리하시는 분과 얘기를 해 보셔야 할 것 같습니다.

Template Method 패턴을 JDBC 코드에 적용함으로써 중복 코드를 없애본다.

Template Method 패턴의 구현방식

Template Method 패턴은 거의 동일한 코드로 구성된 메소드가 여러 클래스에 존재할 때 사용된다. 예를 들어, DB로부터 사용자 정보를 읽어와 로그인을 수행하는 DBLogin 클래스와 중앙서버로부터 사용자 정보를 읽어와 로그인을 수행하는 SSOLogin 클래스가 있다고 가정해보자. 이 두 클래스는 사용자 정보를 읽어오는 부분의 코드만 다를 뿐 나머지 코드는 완전히 동일하다. 예를 들어, 두 클래스의 login 메소드는 다음과 같이 비슷할 것이다.

    public void login() {
        Dialog loginDialog = new LoginDialog();
        while(true) {
            ...
            if (loginDialog.isCancel()) {
                return;
            }
            String userID = loginDialog.getUserID();
            String password = loginDialog.getPassword();
            
            try {
                // 사용자 정보 로딩
                // 입력한 정보와 동일한지 검사
                // 동일한 경우 인증성공
                // 실패한 경우 예외 발생                
                
            } catch(Throwable e) {
                // 인증 처리 실패 메시지 출력!
            }
        }
    }

DBLogin 클래스와 SSOLogin 클래스는 인증을 처리하는 부분의 코드(위 코드에서 굵게 표시한 부분)만 다를 뿐 나머지 코드는 완전히 동일하다. 이렇게 거의 모든 코드가 동일하고 일부 코드만 다를 때 사용되는 패턴이 Template Method 패턴이다.

앞서 작성한 DBLogin 클래스와 SSOLogin 클래스에서 Template Method 패턴을 적용하려면 먼저 이 두 클래스가 공통으로 상속받을 클래스인 AbstractLogin 클래스를 작성해야 한다. AbstractLogin 클래스는 다음과 같은 코드를 갖는다.

    public abstract class AbstractLogin {
        public void login() {
            Dialog loginDialog = new LoginDialog();
            while(true) {
                ...
                if (loginDialog.isCancel()) {
                    return;
                }
                String userID = loginDialog.getUserID();
                String password = loginDialog.getPassword();
                
                try {
                    authenticate(userId, password);
                } catch(Throwable e) {
                    // 인증 처리 실패 메시지 출력!
                }
            }
        }
        
        public abstract void authenticate(String userId, String password)
        throws Throwable;
    }

AbstractLogin 클래스의 login() 메소드는 앞서 살펴봤던 login() 메소드와 거의 동일하다. 차이점이 있다면 인증 부분의 처리를 authenticate() 메소드를 통해서 수행한다는 점이다. 여기서, login() 메소드가 템플릿 메소드에 해당하며, authenticate() 메소드는 서로 다른 인증 코드를 수행하게 된다.

이제 남은 작업은 DBLogin 클래스와 SSOLogin 클래스가 AbstractLogin 클래스를 상속받도록 한 뒤, 알맞게 authenticate() 메소드를 구현하는 것이다. 예를 들어, DBLogin 클래스는 다음과 같이 구현할 수 있을 것이다.

    public class DBLogin extends AbstractLogin {
        
        public void authenticate(String userId, String password) throws Throwable {
            // DB로부터 사용자 정보를 읽어온다.
            // 암호화 아이디를 비교한다.
            ....
        }
    }

SSOLogin 클래스도 역시 authenticate() 메소드는 알맞게 구현해주면 된다.

Template Method 패턴을 알맞게 설계하면, 개발자는 하위 클래스에서 기본 구조를 그대로 사용하면 특정한 부분의 코드만 구현하면 된다. 즉, 어느 정도 로직 흐름의 보호를 받게 된다.

꼭 추상 클래스에 템플릿 메소드를 만들고 하위 클래스에서 구현하는 방식을 사용할 필요는 없다. Template Method 패턴의 핵심은 템플릿 메소드가 있고, 템플릿 메소드에서 다르게 실행되어야 할 부분을 쉽게 설정할 수 있다는 데이 있다. (상속을 사용하는 경우에는 특정 메소드를 호출함으로써 이를 구현한 것이다.) 뒤에서 JDBC 코드에 Template Method를 적용하는 방법을 살펴볼 것인데, 여기서는 추상 클래스를 상속받는 방식이 아닌 외부의 코드를 실행하는 방식을 사용할 것이다.

Template Method 패턴을 JDBC 코드에 적용하기

Template Method 패턴은 동일한 구조를 갖는 코드에 아주 유용하게 사용할 수 있다. 특히 try-catch 블럭이 전체 코드의 30~40%를 차지하는 JDBC 프로그래밍에서는 Template Method 패턴을 응용해서 적용함으로써 상당부분의 불필요한 코드를 줄일 수 있게 된다.

JDBC 코드의 중복

JDBC 프로그래밍을 하면 다음과 같은 형태의 코드를 많이 사용하게 된다.

    Connection conn = null;
    Statement stmt = null;
    try {
        conn = DriverManager.getConnection(...);
        stmt = conn.createStatement();
        int count = stmt.executeUpdate(someQuery);        ...
    } catch(SQLException ex) {
        // 예외 처리
    } finally {
        if (stmt != null) try { stmt.close(); } catch(SQLException ex) {}
        if (conn != null) try { conn.close(); } catch(SQLException ex) {}
    }

위 코드에서 로직과 관련된 코드는 전체 코드 중에서 파란색으로 표시한 부분이다. (위 코드에선 단지 한줄이다.) 나머지 코드는 JDBC 프로그래밍을 하기 위해 커넥션을 얻고, 처리가 완료된 뒤 자원을 반환하는 등의 로직과 관련없는 코드들이다. 상황에 따라 다르겠지만 일반적으로 JDBC 프로그래밍에서 많게는 코드의 70~80%, 적게는 30~40%의 코드가 로직과 관련없는 코드로 구성된다.

그런데, JDBC 프로그래밍의 대부분은 다음과 같이 중복된 코드를 갖는다.

    try {
        // 커넥션 생성
        
        [로직실행]        
    } catch(SQLException ex) {
        // 예외 처리
    } finally {
        // 자원반환
    }

이 시점에서 뭔가 떠오른 사람들도 있을 것이다. 바로 위의 코드는 JDBC 프로그래밍에서 늘 사용되는 템플릿 형태의 코드라는 점이다. 이는 곧 JDBC 프로그래밍을 할 때 Template Method 패턴을 응용함으로써 개발자는 try-catch-fianlly와 관련된 코딩을 상당량 없애고 로직코드를 작성하는 데에 집중할 수 있게 된다.

Template Method 패턴 적용1: Statement 단위 적용

먼저 살펴볼 내용은 Statement의 코드 중복을 없애는 방법이다. Statement.executeUpdate()를 실행하는 코드는 다음의 형태를 갖게 된다.

    Statement stmt = null;
    try {
        stmt = conn.createStatement();
        // 로직과 관련된 코드
        int count = stmt.executeUpdate(
                        "update member set email = 'a@a.com' where memberid = 'a'");
    } finally {
        if (stmt != null) try { stmt.close(); } catch(SQLException ex) {}
    }

여기서 바뀌는 부분은 Statemet.executeUpdate()에 전달되는 SQL 쿼리이다. 따라서, 템플릿을 제공하는 클래스를 다음과 같이 작성할 수 있을 것이다.

    public class QueryTemplate {
        public int executeUpdate(Connection conn, String query) throws SQLException {
            Statement stmt = null;
            try {
                stmt = conn.createStatement();
                return stmt.executeUpdate(query);
            } finally {
                if (stmt != null) try { stmt.close(); } catch(SQLException ex) {}
            }
        }
    }

위 QueryTemplate 클래스를 사용해서 다시 코드를 작성해보면 다음과 같이 간단하게 작성된다.

    QueryTemplate template = new QueryTemplate();
    int count = template.executeUpdate(conn,
                "update member set email = 'a@a.com' where memberid = 'a'");

PreparedStatement를 사용한 방식도 비슷하게 처리할 수 있다. 예를 들어, 다음과 같은 템플릿 메소드를 추가할 수 있을 것이다.

    public int executeUpdate(Connection conn, String query, Object[] parameters) 
    throws SQLException {
        PreparedStatement stmt = null;
        try {
            stmt = conn.prepareStatement(query);
            for (int i = 0 ; i < parameters.length ; i++) {
                if (parameters[i] instanceof String) {
                    stmt.setString(1, (String)parameters[i]);
                } else if (...) {
                    ...
                }
            }
            return stmt.executeUpdate();
        } finally {
            if (stmt != null) try { stmt.close(); } catch(SQLException ex) {}
        }
    }

위의 추가된 템플릿 메소드는 다음과 같이 사용가능할 것이다.

    QueryTemplate template = new QueryTemplate();
    Object[] params = new Object[2];
    params[0] = "a@a.com";
    params[1] = "a";
    int count = template.executeUpdate(conn, "update member set email = ? " +
                       "where memberid = ?", params);

ResultSet과 관련된 템플릿 메소드는 다음의 형태를 띄게 될 것이다.

    public List executeQuery(Connection conn, String query) throws SQLException {
        Statement stmt = null;
        try {
            stmt = conn.createStatement();
            ResultSet rs = stmt.executeQuery(query);
            if (rs.next()) {
                List result = new java.util.ArrayList();
                do {
                    // ResultSet으로부터 값을 읽어와
                    // 알맞은 객체에 담은 뒤,
                    // 그 객체를 result에 추가
                } while(rs.next());
                return result;
            } else {
                return Collections.EMPTY_LIST;
            }
        } finally {
            if (rs != null) try { rs.close(); } catch(SQLException ex) {}
            if (stmt != null) try { stmt.close(); } catch(SQLException ex) {}
        }
    }

ResultSet에 저장된 값을 어떤 형태로 가져올 것인가가 위 코드에서 이슈가 되는 부분이다. 몇가지 방식이 있을 수 있는데, 대표적인 방식으로 다음을 생각해 볼 수 있다.

  • ResultSet의 각 행의 값을 Object[]에 담기
  • ResultSet의 각 행의 값을 Map에 담기
  • ResultSet의 각 행의 값을 원하는 객체에 담기
이에 대한 자세한 내용은 여기서는 다루지 않겠다. Jakarta Commons DBUtil과 Jakarta Commons BeanUtils 같은 것들을 사용하면 좀더 쉽게 위의 기능을 구현할 수 있으니 구현에 관심이 있다면 참고해보기 바란다.

Template Method 패턴 적용2: Connection 단위 적용

앞에서 살펴본 코드는 Statement 단위의 코드였다. 따라서, Connection과 관련된 코드는 여전히 중복된 형태의 코드가 산재하게 된다. 예를 들어, 트랜잭션 처리를 하는 코드는 다음과 같아질 것이다.

    Connection conn = null;
    try {
        conn = .... // 커넥션을 생성
        conn.setAutoCommit(false);
        
        QueryTemplate template = new QueryTemplate();
        int count = template.executeUpdate(conn, "update member set ... ");
        int count1 = template.executeUpdate(conn, "update member_property set ... ");
        
        conn.commit();
    } catch(SQLException ex) {
        conn.rollback();
    } finally {
        if (conn != null) {
            conn.setAutoCommit(true);
            conn.close();
        }
    }

Connection과 관련된 코드도 중복을 없애고 싶은 충동이 일어날 것이다. 앞서 Statement나 PreparedStatement의 템플릿 코드는 실행할 쿼리와 할당할 파라미터 목록만 필요했기 때문에 코드가 단순했는데, Connection의 경우는 경우가 다르다. Connection의 트랜잭션을 시작(conn.setAutoCommit(true))하고 커밋(conn.commit())하는 사이에 코드가 정해지 있지 않기 대문이다.

이를 해결하려면 다음과 같이 트랜잭션 안에서 실행될 코드를 담은 객체를 전달받는 방법을 생각해볼 수 있다.

    public Object executeInTransaction(Worker worker) throws SQLException {
        Connection conn = null;
        try {
            conn = ... // 커넥션 구함
            conn.setAutoCommit(false);
            Object returnValue = worker.work(conn);
            conn.commit();
            
            return returnValue;
        } catch(SQLException ex) {
            conn.rollback();
        } finally {
            if (conn != null) try { conn.close(); } catch(SQLException ex) {}
        }
    }

여기서 worker 파라미터는 트랜잭션 안에서 실행될 코드를 담고 있는 객체로서, Worker는 다음과 같은 인터페이스가 될 것이다.

    public interface Worker {
        public Object work(Connection conn) throws SQLException;    }

이제 다음과 같이 Worker 인터페이스를 활용하여 트랜잭션 관련 템플릿 메소드를 사용할 수 있게 된다.

    QueryTemplate template = new QueryTemplate();
    List resultList = (List)template.executeInTransaction(new Worker() {
        public Object work(Connection conn) throws SQLException {
            QueryTemplate t = new QueryTemplate();
            int count = t.executeUpdate(conn, "query");
            List result = t.executeQuery(conn, somequery);
            ....
            return result;
        }
    });

Template Method 패턴을 응용함으로써 Connection의 10여줄이나 되던 트랜잭션 처리 관련 코드를 단 두줄로 줄일 수 있게 되었다. (한줄은 QueryTemplate을 생성하는 코드, 또 다른 한줄은 QueryTemplate.executeInTransaction() 메소드를 호출하는 코드이다.)

결론

본 글에서는 Template Method 패턴과 JDBC 프로그래밍에 Template Method 패턴을 적용함으로써 로직과 관련되지 않은커넥션 생성, Statement 생성, 자원 반환 등의 코드를 대폭 줄이고 로직관련 코드에 집중할 수 있다는 것을 살펴보았다.

Template Method 패턴은 JDBC 프로그래밍에서 뿐만 아니라 try-catch-finally의 형태를 지닌 대부분의 코드에 적용할 수 있다. 거의 대부분의 try-catch-finally 블럭은 내부의 로직 부분을 제외하면 중복된 코드로 구성되어 있는 경우가 많기 때문에 코드를 상당부분 줄일 수 있게 된다. 뿐만 아니라 로직 코드 위주로 코드가 작성되기 때문에 코드의 가독성도 높아지게 된다.

아마 이 글을 읽는 개발자중에는 본 글에서 보여준 Template Method 패턴의 응용에는 추상 클래스도 나오지 않고, 추상 클래스를 상속한 클래스도 나오지 않으므로 이는 Template Method 패턴이 아니라고 할지도 모르겠다. 하지만, 패턴에서 중요한 건 얼마나 그 패턴에서 소개한 클래스를 그대로 쓰느냐고 아니라, 패턴을 만들게 된 이유를 정확하게 알고 그 이유에 맞게 패턴을 적용하는 것이라고 생각한다. 그런 이유로, 본 글에서는 Template Method 패턴을 먼저 간단하게 소개하고, 그 응용 방법을 설명해보았다.

관련링크:


Posted by 최범균 madvirus

댓글을 달아 주세요

  1. eveript 2013.10.12 21:31 신고  댓글주소  수정/삭제  댓글쓰기

    하앍 ... 완전 짱이다 템플릿 메서드패턴....
    JSP2.1책 정독중에 url이 소개 되어있어서 들러보았습니다..
    소중한 노하우, 정보 감사감사~

  2. dlehdgns 2013.11.14 10:44 신고  댓글주소  수정/삭제  댓글쓰기

    jdbc 코드느 이렇게 줄이는 것이구나 알게 되었네요

  3. 보배곰 2017.03.07 14:10 신고  댓글주소  수정/삭제  댓글쓰기

    감사합니다. 소스 잘 봤습니다! 근데 중간에 소스가 잘리는거같아요ㅠㅠ

어플리케이션에 AOP 개념을 추가하기 위해 프록시 체인을 만드는 방법을 살펴본다.

정적인 방식의 프록시와 다이나믹 프록시

 자바월드(http://www.javaworld.com)에 실린 'Generically chain dynamic proxies' 글을 바탕으로 필자가 필요한 부분을 재구성고 편집하여 본 글을 작성하였다.

최근에, POJO(plain-old java object)를 사용한 프로그래밍 기법이 많이 사용되고 있다. EJB의 엔티티빈과 세션빈을 대신하여 Hibernate와 Spring과 같은 프레임워크가 사용되는데, 이들 프레임워크는 POJO를 지원하고 있다. POJO를 사용하면 EJB를 사용하는 것보다 쉽게 OOP를 적용할 수 있다. 하지만, 때로는 POJO를 사용한 비즈니스 객체에 새로운 관점(aspect) 또는 기능을 삽입해야 할 때가 있다. 예를 들어, 대부분의 비즈니스 객체에 보안이나 로깅을 추가하해야 하는 경우가 있을 수 있는데, 이를 POJO 자체로 구현하는 것은 쉽지 않다. (모든 비즈니스 객체에 보안 관련 코드를 넣는다고 생각해보라!) 이런 문제를 쉽게 해결해주는 것이 있는데, 그것이 바로 다이나믹 프록시이다.

다이나믹 프록시의 기본 개념은 기존에 존재하는 객체의 코드나 인터페이스를 변경하지 않은 채로 새로운 기능을 동적으로 추가한다는 것이다. GoF의 Decorator 패턴을 사용하면 객체의 코드 변경없이 객체에 새로운 행동을 추가하거나 새로운 관점을 추가할 수 있다. 하지만, 정적인 방식으로 Decorator 패턴을 구현하게 되면 몇가지 문제가 발생하게 되는데, 이에 대해서는 뒤에서 설명할 것이다.

코드 생성 도구를 사용해서 정적으로 Decorator 패턴이 구현된 클래스의 소스 코드를 생성하기도 하지만, 이는 코드 생성을 위한 추가적인 작업을 필요로 할 뿐만 아니라 생성된 코드를 관리해야 하는 부담도 발생한다. 이런 문제를 해결할 수 있는 방법이 있는데, 그것이 바로 다이나믹 프록시를 사용하는 것이다.

다이나믹 프록시가 어떻게 동작하는 지 살펴보기 전에, 다이나믹 프록시를 사용하지 않고 정적인 방식으로 Decorator 패턴과 프록시 체인에 대해서 먼저 살펴보도록 하자. 이를 통해 정적인 방식의 구현에서 발생할 문제점과 이를 다이나믹 프록시를 사용하여 해결하는 과정을 앞으로 보게 될 것이다.

정적인 방식의 Decorator 및 프록시 구현

다음과 같은 비즈니스 인터페이스가 있다고 하자.

    public interface IMyBusinessObject {
        public String doExecute(String in);
    }

그리고, 위 비즈니스 인터페이스를 구현한 클래스의 구현체가 아래와 같다고 하자.

    public class MyBusinessObject implements IMyBusinessObject {
        public String doExecute(String in) {
            System.out.println("Here in MyBusinessObject doExecute: input :" + in);
            return in;
        }
    }

우리가 하고자 하는 건 doExecute() 메소드 앞뒤로 특정한 기능(예를 들어, 로깅)을 추가하는 것이다. Decorator 패턴을 사용하면 이 기능을 쉽게 추가할 수 있다.

Decorator 패턴을 사용하기 위해서 다음과 같은 추상 Decorator 클래스를 작성하였다.

    public abstract class ADecorator implements IMyBusinessObject {
        protected IMyBusinessObject target;
        
        // target은 이 객체가 처리할(decorate) 대상으로서
        // 실제 비즈니스 객체가 대상이 된다.
        public void setTarget(IMyBusinessObject target_) {
            this.target = target_;
        }
        
        public ADecorator(){}
        
        public ADecorator(IMyBusinessObject target_) {
            setTarget(target_);
        }
    }

이제 ADecorator 추상 클래스를 구현한 DebugConcreteDecorator 클래스를 작성해보자. 이 클래스는 실제 비즈니스 클래스 객체의 메소드 호출 앞뒤로 디버그 메시즈를 출력하는 기능을 제공한다.

    public class DebugConcreteDecorator extends ADecorator {
        public String doExecute(String in) {
            System.out.println("DebugConcreteDecorator: before method : doExecute ");
            
            String ret = target.doExecute(in);
            System.out.println("DebugConcreteDecorator: after method : doExecute ");
            return ret;
        }
    }

이제, 클라이언트 코드는 다음과 같은 코드를 사용해서 비즈니스 객체를 호출하면 된다.

    // 원래 비즈니스 객체
    IMyBusinessObject aIMyBusinessObject = new MyBusinessObject();
    // 비즈니스 객체를 Decorator함
    IMyBusinessObject wrappedObject =
       new DebugConcreteDecorator(aIMyBusinessObject);
    // wrappedObject를 통해서 원래 객체를 호출
    wrappedObject.doExecute("Hello World");

위 코드에서 DebugConcreteDecorator 객체는 원본 비즈니스 객체를 감싼다. DebugConcreteDecorator의 deExecute() 메소드를 호출하게 되면 DebugConcreteDecorator.doExecute() 메소드가 실행된다. DebugConcreteDecorator의 doExecute() 메소드는 '호출 전 디버그 메시지 출력 > 원본 객체의 doExecute() 호출 > 호출 후 디버그 메시지 출력' 순서대로 실행된다. 실제 위 코드의 실행 결과는 아래와 같다.

    DebugConcreteDecorator: before method : doExecute
    Here in MyBusinessObject doExecute: input :Hello World
    DebugConcreteDecorator: after method : doExecute

이 결과로부터, 우리는 비즈니스 메소드 호출 앞뒤로 디버그 메시지를 추가할 수 있다는 것을 확인할 수 있다.

하나의 Decorator가 또 다른 Decorator를 호출하도록 함으로써 Decorator의 체인을 만들 수도 있다. Decorator의 체인에 대해 설명하기 위해서 또 다른 Decorator 구현체를 만들어보자.

    public class AnotherConcreteDecorator extends ADecorator {
        public String doExecute(String in) {
            System.out.println("AnotherConcreteDecorator: Going to execute method : doExecute");
            // 파라미터 값을 변경해서 원본 비즈니스 객체에 전달
            in = in + " Modified by AnotherConcreteDecorator"; 
           String ret = target.doExecute(in);
            System.out.println("AnotherConcreteDecorator: After execute method : doExecute");
            return ret;
        }
    }

위 Decorator는 전달받은 문자열 파라미터에 새로운 문장(" Modified by AnotherConcreteDecorator")을 추가하여, 원본 비즈니스 객체에 변경한 파라미터값을 전달한다.

DebugConcreteDecorator와 AnotherConcreteDecorator를 체인으로 연결하기 위해 클라이언트는 다음과 같은 코드를 사용하게 될 것이다.

    IMyBusinessObject aIMyBusinessObject = new MyBusinessObject();
    IMyBusinessObject wrappedObject =
       new AnotherConcreteDecorator(
          new DebugConcreteDecorator(aIMyBusinessObject));
    wrappedObject.doExecute("Hello World");

AnotherConcreteDecorator 인스턴스를 생성할 때 실제 비즈니스 객체 대신 DebugConcreteDecorator 인스턴스를 전달함으로써 두 Decorator의 체인을 생성하게 된다. 따라서 wrappedObject.doExecute()를 실행하면 먼저 AnotherConcreteDecorator.doExecute() 메소드가 실행되고, 이 메소드에서는 순차적으로 DebugConcreteDecorator.doExecute()를 호출한다. 그리고 DebugConcreteDecorator.doExecute() 에서는 MyBusinessObject.doExecute()를 호출하게 된다.

실제 실행해보면 다음과 같은 결과가 출력된다. 이 출력 결과를 보면 체인이 어떻게 순차적으로 실행되는 지 알 수 있을 것이다.

    AnotherConcreteDecorator: Going to execute method : doExecute
    DebugConcreteDecorator: before method : doExecute
    Here in MyBusinessObject doExecute: input :Hello World Modified by AnotherConcreteDecorator
    DebugConcreteDecorator: after method : doExecute
    AnotherConcreteDecorator: After execute method : doExecute

지금까지 살펴본 정적인 방식의 Decorator 패턴의 클래스 다이어그램은 다음과 같다.


정적인 방식의 Decorator 구현의 큰 문제점은 메소드 호출이 하드코딩으로 처리된다는 점이다. 두개의 Decorator 클래스(DebugConcreteDecorator 및 AnotherConcreteDecorator)의 doExecute() 메소드를 보면 직접 비즈니스 객체의 doExecute() 메소드를 호출하고 있다. 메소드 이름이 변경되거나 또는 비즈니스 객체에 새로운 메소드가 추가될 경우, 관련 Decorator 클래스의 코드도 함께 작업해주어야 한다.

다이나믹 프록시를 사용하면 하드코딩된 메소드 호출 때문에 발생하는 문제를 없앨 수 있다. 더불어, 비즈니스 인터페이스의 각각의 메소드를 Decorator 클래스 안에 구현하거나 오버라이딩 할 필요가 없어진다.

J2SE 1.3의 다이나믹 프록시 예제

J2SE 1.3부터 추가된 다이나믹 프록시는 런타임에 지정한 인터페이스를 구현한 클래스를 생성할 수 있도록 해 준다. java.lang.reflect.Proxy 클래스를 사용해서 지정한 인터페이스와 관련된 프록시 인스턴스를 생성할 수 있다. 이때, 각각의 프록시 인스턴스는 연관된 java.lang.reflect.InvocationHandler 객체를 갖고 있으며, 이 InvocationHandler 객체를 통해서 Decorator 패턴을 구현하게 된다.

앞서 작성했던 IMyBusinessObject 비즈니스 인터페이스와 MyBusinessObject 비즈니스 클래스의 Decorator였던 DebugConcreteDecorator 클래스를 InvocationHandler를 사용하여 구현하면 다음과 같다.

    public class MyDebugInvocationHandler
    implements java.lang.reflect.InvocationHandler {
    
        private Object target = null;

        public void setTarget(Object target_) {
            this.target = target_;
        }
        
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            try {
                System.out.println("Going to execute method : " + method.getName);
                Object retObject = method.invoke(target, args);
                System.out.println("After execute method : " + method.getName());
                return retObject;
            } catch(InvocationTargetException e) {
                throw e.getTargetException();
            } catch(Exception e) {
                throw e;
            }
        }
    }

위 코드에서 중요한건 invoke() 메소드이다. invoke() 메소드는 java.lang.reflect.Proxy 클래스에 의해서 호출된다. invoke() 메소드는 여분의 처리를 수행한 뒤 실제 대상 객체(예제의 경우에는 MyBusinessObject가 될것이다)에 처리를 전달한다.

클라이언트는 다음과 같이 MyDebugInvocationHandler 클래스와 JDK의 다이나믹 프록시를 사용하여 프록시 객체를 생성하고 호출할 수 있다.

    IMyBusinessObject bo = new MyBusinessObject(); // 실제 비즈니스 객체
    MyDebugInvocationHandler aMyDebugInvocationHandler = new MyDebugInvocationHandler();
    aMyDebugInvocationHandler.setTarget(bo);
    
    IMyBusinessObject proxyObject =
       (IMyBusinessObject) Proxy.newProxyInstance  // 프록시 객체 생성
          (IMyBusinessObject.class.getClassLoader(),
          new Class[] { IMyBusinessObject.class }, // 프록시할 인터페이스
          aMyDebugInvocationHandler); // InvocationHandler
    
    // 프록시 객체 사용
    System.out.println(proxyObject.doExecute("Hello World"));

위 코드를 보면, MyBusinessObject를 생성한 후, 이 객체를 MyDebugInvocationHandler 객체에 전달한다. (aMyDebugInvocationHandler.setTarget(bo) 부분) 이후, Proxy.newProxyInstance를 사용하여 IMyBusinessObject에 대한 프록시 객체를 생성하게 된다. Proxy.newProxyInstance를 사용하여 생성한 프록시 객체인 proxyObject의 doExecute() 메소드를 호출하면 InvocationHandler의 invoke() 메소드가 호출된다. 따라서 위 코드에서는 MyDebugInvocationHandler의 invoke() 메소드가 메소드 호출을 처리하게 된다.

이 코드에서 중요한 점은 InvocationHandler.invoke() 메소드는 java.lang.reflect.Method를 인자로 전달받기 때문에 비즈니스 인터페이스에 의존적이지 않다는 점이다. 또한, 비즈니스 인터페이스의 모든 메소드를 구현하지 않아도 비즈니스 객체의 모든 메소드의 앞뒤로 새로운 기능을 추가할 수 있다.

프록시 체인을 만들고 싶다면 setTarget() 메소드에 원본 객체 대신 프록시 객체를 전달하면 된다. 예를 들어, 앞서 작성한 MyDebugInvocationHandler와 MyAnotherInvocationHandler를 체인으로 형성하고 싶다면 앞서 작성했던 코드에 다음의 코드를 추가로 작성하면 된다.

    MyAnotherInvocationHandler aMyAnotherInvocationHandler = new MyAnotherInvocationHandler ();
    
    //실제 비즈니스 객체 대신에 프록시 객체를 전달
    aMyAnotherInvocationHandler.setTarget(proxyObject);
    
    IMyBusinessObject nextProxyObject =
       (IMyBusinessObject) Proxy.newProxyInstance
          (IMyBusinessObject.class.getClassLoader(),
          new Class[] { IMyBusinessObject.class },
          aMyAnotherInvocationHandler);
    
    System.out.println(nextProxyObject.doExecute("Hello World"));

지금까지 살펴본 예제를 통해서, 다이나믹 프록시를 사용하면 정적인 Decorator 체인을 사용하는 경우보다 적은 분량의 코드로 런타임에 동적으로 기능을 추가할 수 있다는 것을 알게 되었다. 하지만, 다이나믹 프록시를 사용하더라도 여전히 문제가 남아 있다. 그것은 바로 체인을 형성하려면 많은 코드를 작성해야만 한다는 점이다. 또한 일반 객체를 생성하기 위해 Proxy 클래스를 사용하는 것은 일반적인 사용자에게는 친숙한 방법이 아니며, 매번 위와 같은 코드가 반복된다는 것도 바람직하지 않다.

본 글의 나머지 부분에서는 이런 문제를 해결하면서, 간단한 API를 사용해서 쉽게 다이나믹 프록시 체인을 만들 수 있는 범용적인 프록시 팩토리 클래스를 작성해볼 것이다.

범용 다니아믹 프록시 체인 구현: 방식1

다음과 같은 코드를 사용해서 비즈니스 객체에 대한 프록시 체인을 만들 수 있다면 얼마나 좋을까?

    String[] interceptorClasses = {"MyDebugInterceptor",
                                  "MyAnotherInterceptor"};
    
    IMyBusinessObject aIMyBusinessObject =
       (IMyBusinessObject)MyProxyFactory.getProxyObject
          ("MyBusinessObject", interceptorClasses);
    
    String ret = aIMyBusinessObject.doExecute("Hello World");  

위 코드에서 핵심은 생성할 비즈니스 클래스의 이름과 프록시로 사용할 클래스의 이름을 String으로 MyProxyFactory 클래스에 전달하면, 프록시 체인 객체를 생성해준다는 점이다. MyProxyFactory가 생성한 프록시 객체의 메소드를 호출하면 체인으로 연결된 프록시가 차례대로 실행되고 최종적으로 원본 비즈니스 객체에 전달된다.

프록시 체인을 형성할 MyDebugInterceptor와 MyAnotherInterceptor가 비즈니스 인터페이스인 IMyBusinessObject에 의존적이지 않고 범용적이라면 더욱 좋을 것이다.

이를 구현하기 위해서 먼저 비즈니스 메소드의 호출 앞뒤에 독립적으로 메소드 가로채기를 할 수 있다고 가정하자. 다음과 같이 가로채기를 위한 인터페이스를 정의할 수 있을 것이다.

    public interface IMethodInterceptor {
        
        Object interceptBefore(Object proxy, Method method, Object[] args, Object realtarget);
        
        void interceptAfter(Object proxy, Method method, Object[] args, 
            Object realtarget, Object retObject, Object interceptBeforeReturnObject);
    }

interceptBefore() 메소드는 비즈니스 메소드가 호출되기 이전에 실행되고, interceptAfter() 메소드는 비즈니스 메소드가 성공적으로 실행된 이후에 실행된다.

이제 IMethodInterceptor 인터페이스를 구현한 두개의 클래스를 작성하자.

    public class MyDebugInterceptor implements IMethodInterceptor {
    
        public Object interceptBefore(Object proxy, Method method,
        Object[] args, Object realtarget) {
    
            System.out.println("MyDebugInterceptor: Going to execute method : ");
            return null;
        }
        
        public void interceptAfter(Object proxy, Method method, Object[] args,
        Object realtarget, Object retObject, Object interceptBefore) {
    
            System.out.println("MyDebugInterceptor: After execute method : " );
        }
    }

첫번째 구현 클래스는 비즈니스 객체의 대상 메소드가 메소드가 호출되기 전과 호출된 후에 단순히 콘솔에 디버깅을 위한 메시지를 출력한다.

    public class MyAnotherInterceptor implements IMethodInterceptor {
    
        public Object interceptBefore(Object proxy, Method method,
        Object[] args, Object realtarget) {
        
            System.out.println("MyAnotherInterceptor: Going to execute method : ");
            if ( method.getName().equals("doExecute") &&
                 args != null && args.length >= 1 ) {
            
                if ( args[0] instanceof String ) {
                    args[0] = args[0] +
                              " Modified by MyAnotherInterceptor";
                }
                return null;
            }
        }
    
        public void interceptAfter(Object proxy, Method method, Object[] args,
        Object realtarget, Object retObject, Object interceptBefore) {
        
            System.out.println("MyAnotherInterceptor: After execute method : ");
        }
    }

두번째 구현체인 MyAnotherInterceptor의 interceptBefore()는 대상 객체의 메소드를 호출하기 전에, 메소드의 이름이 doExecute이고 파라미터 길이가 1인 경우, 첫번째 파라미터의 값을 변경한다.

프록시 체인에서 사용할 IMethodInterceptor 구현체를 작성했으니, 이제 앞서 작성한 MyAnotherInterceptor 구현체를 사용하는 범용 InvocationHandler를 작성해보자.

    public class GenericInvocationHandler
            implements java.lang.reflect.InvocationHandler {
    
        private Object target = null;
        public void setTarget(Object target_) {
            this.target = target_;
        }
        
        private Object realtarget = null;
        public void setRealTarget(Object realtarget_) {
            this.realtarget = realtarget_;
        }
        
        IMethodInterceptor methodInterceptor = null;
        public void setMethodInterceptor(IMethodInterceptor methodInterceptor_) {
            this.methodInterceptor = methodInterceptor_;
        }
        
        public Object invoke(Object proxy, Method method, Object[] args)
            throws Throwable {
            try {
                Object interceptBeforeReturnObject = null;
                if ( methodInterceptor != null ) {
                    interceptBeforeReturnObject =
                        methodInterceptor.interceptBefore
                          (proxy, method, args, realtarget );
                }
                Object retObject = method.invoke(target, args);
                if ( methodInterceptor != null ) {
                    methodInterceptor.interceptAfter
                        (proxy, method, args, realtarget,
                        retObject, interceptBeforeReturnObject );
                }
                return retObject;
            }
            catch(InvocationTargetException e) {
                throw e.getTargetException();
            }
    
            catch(Exception e) {
                throw e;
            }
        }
    }

GenericInvocationHandler의 invoke() 메소드를 보면 대상 객체의 메소드를 호출하기 이전에 그리고 호출한 다음에 각각 IMethodInterceptor의 interceptBefore() 메소드와 interceptAfter() 메소드를 호출하는 것을 알 수 있다.

이제 다음에 할일은 범용적인 프록시 체인을을 생성해주는 MyProxyFactory 클래스를 작성하는 것이다. 이 클래스를 통해서 여러분은 다이나믹 프록시를 사용하여 여러분 자신만의 프레임워크를 만드는 데 필요한 아이디어를 얻을 수 있을 것이다.

    public class MyProxyFactory {
    
        public static Object getProxyObject( String className,
            String[] interceptors ) throws Throwable {
            // 대상 클래스의 객체를 생성
            Object inputObject = getTargetObject(className);
            if ( interceptors != null && interceptors.length > 0  ) {
    
                Object inputProxiedObject = inputObject;
                // for 구문에서 프록시의 체인을 생성한다.
                for ( int i=0; i < interceptors.length; i++ ) {
                    inputProxiedObject =
                        getProxyObject(inputObject, interceptors[i],
                            inputProxiedObject);
                }
                return inputProxiedObject;
            }
            else {
                return inputObject;
            }
        }
    
        private static Object getProxyObject(Object inObject,
            String interceptor,Object inProxiedObject) throws Throwable {
    
            GenericInvocationHandler invocationHandler =
                new GenericInvocationHandler();
            IMethodInterceptor interceptorObject =
                (IMethodInterceptor)getInterceptor(interceptor);
            if ( interceptor == null ) {
                return inProxiedObject;
            }
            invocationHandler.setTarget(inProxiedObject);
            invocationHandler.setRealTarget(inObject);
            invocationHandler.setMethodInterceptor(interceptorObject);
    
            return Proxy.newProxyInstance
                        (inObject.getClass().getClassLoader(),
                        inObject.getClass().getInterfaces(),
                        invocationHandler) ;
        }
    
        private static Object getInterceptor( String interceptors )
            throws Exception {     
            //...
            //클래스 이름으로부터 인스턴스를 생성해서 리턴
            //Class.forName() 과 newInstance() 를 사용해서
            //인스턴스를 생성하고 리턴한다.
        }
        private static Object getTargetObject( String className )  
            throws Exception {
            //...
            //클래스 이름으로부터 인스턴스를 생성해서 리턴
            //Class.forName() 과 newInstance() 를 사용해서
            //인스턴스를 생성하고 리턴한다.
        }
    }

public static Object getProxyObject(String className, String[] interceptors) 메소드는 private static getProxyObject 메소드를 사용하여 interceptors로 전달한 프록시 객체를 생성하고, 각각의 프록시 체인을 연결하여 체인을 형성한다. private getProxyObject 메소드는 GenericInvocationHandler를 생성하고 setTarget, setRealTarget, setMethodInterceptor를 호출하여 프록시할 대상 객체, 실제 원본 비즈니스 객체, 그리고 IMethodInterceptor 객체를 지정한 뒤, Proxy.newProxyInstance를 사용하여 프록시 객체를 생성한다.

위 코드에서 여러분은 두가지 아이디어를 얻을 수 있다. 한가지는 클라이언트 코드가 아닌 MyProxyFactory 클래스에서 프록시 체인을 형성한다는 점과, 체인에 있는 IMethodInterceptor 개수만큼 GenericInvocationHandler 인스턴스를 생성한다는 점이다.

예제에서 String으로 클래스 이름을 전달했고 Class.forName()과 Class.newInstance()를 사용하여 대상 클래스와 IMethodInterceptor 구현 클래스의 인스턴스를 생성했다. 하지만, 실제 구현에서 프록시 필요할 때 마다 매번 Class.forName과 newInstance()를 사용하는 것은 문제가 될 수 있다. 예를 들어, 메소드를 호출하기 이전에 비즈니스 클래스나 인터셉터에서 어떤 변수를 설정하고 싶을 수가 있다. 또는 비즈니스 클래스나 인터셉터를 싱글톤으로 정의하고 싶을 수도 있다. 이런 세밀한 부분은 실제 프록시 체인에서 고려되어야 한다. 방식3에서 이런 부분을 처리할 수 있는 방법을 살펴볼 것이다.

방식1의 결과는 앞서 작성한 정적 방식의 decorator의 결과와 동일하다. 하지만, 정적인 방식에 비해 매우 큰 유연함을 갖고 있다. MyProxyFactory와 GenericInvocationHandler는 범용적이기 때문에, 이 클래스를 어떤 비즈니스 객체에든지 적용할 수 있다. 물론, 비즈니스 객체가 미리 정의된 인터페이스를 구현해야 하지만, 인터페이스를 사용하는 것은 좋은 프로그래밍 습관이다. (CGLIB를 사용하면 인터페이스 없이 다이나믹 프록시를 구현할 수 있는데, 이에 대한 내용은 자바캔의 'CGLIB를 이용한 프록시 객체 만들기' 글을 참고하기 바란다.)

MyDebugInterceptor와 MyAnotherInterceptor는 범용적으로 사용될 수 있기 때문에, 각각의 비즈니스 인터페이스 및 메소드를 위해서 별도로 작성할 필요가 없다. 만약 비즈니스 메소드를 실행하기 전에 보안 검사를 하고 싶다면, interceptBefore() 메소드에서 보안 검사를 수행할 수 있다. 보안 인터셉터는 데이터베이스나 파일로부터 보안 설정을 읽어와 보안 검사를 수행할 것이다. 다음과 같은 XML 파일을 사용해서 보안 설정 정보를 저장할 수 있다.

    <security>
        <businessClass>
            <classname>MyBusinessObject<classname>
            <methodPermission>
    
                <method>doExecute</method>
                <validRoles>admin,manager</validRoles>
            </methodPermission>
        </businessClass>
    </security>

보안 인터셉터는 이 설정 파일로부터 정보를 읽어와 메소드의 이름과 역할을 검사하여, 메소드를 실행하기 전에 보안 검사를 수행할 수 있을 것이다.

보안 인터셉텨의 interceptBefore() 메소드는 다음과 같은 코드를 가질 것이다.

    public Object interceptBefore(Object proxy, Method method,
            Object[] args, Object realtarget) {
            
        String nameOfMethod = method.getName();
        String targetClassName = realtarget.getClass().getName();
        MethodPermissions mPerm =
            SecurityFactory.getPermission(targetClassName);
        If ( !mPerm.isAuthorized(MyThreadLocalRoleStore.getRole(),
                                        nameOfMethod ) ) {
            throw new RuntimeException("User not authorized");
        }
        return null;
    }

방식의 클래스 다이어그램은 다음 그림과 같다.


위 클래스 다이어그램에서 눈여겨 볼점은 프록시 체인과 관련된 인터페이스 및 클래스가 비즈니스 인터페이스와 전혀 연관을 맺고 있지 않다는 것이다. (정적인 방식에서는 비즈니스 인터페이스와 Decorator가 연관되어 있었던 점을 기억하자.)

범용 다니아믹 프록시 체인 구현: 방식2

방식2는 방식1과 동일한 클래스를 사용하며, 클라이언트 입장에서 보면 동일하다. 코드는 GenericInvocationHandler와 MyProxyFactory에서만 발생한다. 앞서 방식1에서는 인터셉텨의 개수가 증가하는 만큼 GenericInvocationHandler 인스턴스와 프록시 인스턴스의 개수가 증가했다. 방식2에서는 이 문제를 해결하면서 방식1과 거의 같은 효과를 내도록 구현해볼 것이다.

따라서, 클라이언트에서 사용하는 코드는 방식1과 동일하다.

    String[] interceptorClasses = {"MyDebugInterceptor", "MyAnotherInterceptor"};
    IMyBusinessObject aIMyBusinessObject =
        (IMyBusinessObject)MyProxyFactory.getProxyObject
            ("MyBusinessObject", interceptorClasses);
    String ret = aIMyBusinessObject.doExecute("Hello World");

방식2에서 핵심은 GenericInvocationHandler와 프록시의 인스턴스를 단 한개만 사용한다는 점이다. 새롭게 작성한 GenericInvocationHandler 코드는 다음과 같다.

    public class GenericInvocationHandler
            implements java.lang.reflect.InvocationHandler {
    
        private Object realtarget = null;
        public void setRealTarget(Object realtarget_) {
            this.realtarget = realtarget_;
        }
        Object[] methodInterceptors = null;
        public void setMethodInterceptors
            (Object[] methodInterceptors_) {
            this.methodInterceptors = methodInterceptors_;
        }
        public Object invoke(Object proxy, Method method, Object[] args)
                throws Throwable {
            try {
                Object[] retInterceptBefore = null;
                if ( methodInterceptors != null &&
                    methodInterceptors.length > 0 ) {
    
                    retInterceptBefore = new Object[methodInterceptors.length];
                    for ( int i= methodInterceptors.length - 1; i >= 0; i-- ) {
                        if ( methodInterceptors[i] != null ) {
                            retInterceptBefore[i] =
                                ((IMethodInterceptor)methodInterceptors[i]).
                                    interceptBefore(proxy,
                                        method,args, realtarget );
                        }
                    }
                }
                Object retObject = method.invoke(realtarget, args);
                if ( methodInterceptors != null ) {
                    for ( int i= 0; i < methodInterceptors.length; i++ ) {
                        if ( methodInterceptors[i] != null ) {
                     ((IMethodInterceptor)methodInterceptors[i]).
                         interceptAfter(proxy, method, args, realtarget,
                             retObject, retInterceptBefore[i] );
                        }
    
                    }
                }
                return retObject;
            }
            catch(InvocationTargetException e) {
                throw e.getTargetException();
            }
            catch(Exception e) {
                throw e;
            }
        }
    }

GenericInvocationHandler는 인터셉터의 배열을 입력받고, 비즈니스 객체의 메소드를 호출하기 전에 각 인터셉터의 interceptBeofore() 메소를 역순으로 실행한다. 비슷하게, 비즈니스 객체의 메소드를 호출한 뒤 인터셉터의 interceptAfter() 메소드를 순차적으로 실행한다. (방식은 한개의 인터셉터만 입력받는 것과 대조된다.)

MyProxyFactory의 경우 getInterceptor()와 getTargetObject() 메소드를 제외한 나머지 코드가 일부 바뀐다. 다음은 새롭게 작성한 MyProxyFactory 코드의 일부이다.

    public static Object getProxyObject( String className,
        String[] interceptors ) throws Throwable {
    
    
        Object inputObject = getTargetObject(className);
        if ( interceptors != null && interceptors.length > 0  ) {
            return getProxyObject(inputObject, interceptors);
        }
        else {
            return inputObject;
        }
    }
    private static Object getProxyObject(Object inObject,
        String[] interceptors) throws Throwable {
        
        // 단 한개의 GenericInvocationHandler 인스턴스
        GenericInvocationHandler invocationHandler =
            new GenericInvocationHandler();
        Object[] interceptorObjects = getInterceptors(interceptors);
        invocationHandler.setRealTarget(inObject);
        invocationHandler.setMethodInterceptors(interceptorObjects);
        
        return Proxy.newProxyInstance // 단 한개의 프록시 인스턴스
                        (inObject.getClass().getClassLoader(),
                        inObject.getClass().getInterfaces(),
                        invocationHandler) ;
    }
    private static Object[] getInterceptors(String[] interceptors)
        throws Exception {
    
        Object[] objInterceptors = new Object[interceptors.length];
        for ( int i=0; i < interceptors.length; i++ ) {
            objInterceptors[i] = getInterceptor(interceptors[i]);
        }
        return objInterceptors;
    }

위 코드를 보면 GenericInvocationHandler 인스턴스가 사용되는 것을 알 수 있다. 또한, 인터셉터의 개수에 상관없이 프록시 인스턴스도 단 한개만 생성되는 것을 알 수 있다. 따라서, 메모리 사용량이 적을 뿐 아니라, 더불어 방식1에 비해 수행속도도 약간 빨라진다. (뒤에서 간단한 테스트를 사용해서 각 방식의 수행속도를 비교해 볼 것이다.)

방식2에서 중요한 포인트는 MyProxyFactory가 아닌 GenericInvocationHandler에서 모든 인터셉터를 반복처리한다는 점이다. 또 한가지 중요한 포인트는 방식1과 방식에서 생성되는 인터셉터의 개수는 동일하지만, GenericInvocationHandler가 프록시 인스턴스의 개수가 다르다는 점이다. 방식1은 인터셉터의 개수만큼 GenericInvocationHandler과 프록시의 인스턴스가 생성되지만, 방식2는 인터셉터의 개수에 상관없이 각각 1개씩만 생성된다.

방식2의 클래스 다이어그램은 다음과 같다.


IMethodInterceptor 인터페이스에 메소드를 추가함으로써 더욱 유용하게 방식1과 방식2를 사용할 수 있다. (예를 들어, interceptException() 메소드). 또한, 각각의 메소드(interceptBefore(), interceptAfter(), interceptException())마다 서로 다른 인터페이스를 제공할 수도 있다. 원문 저자는 IMethodInterceptor 인터페이스를 작은 단위의 서로 다른 인터페이스로 나눌 것을 권하고 있다. 예를 들어, 메소드를 실행하기 이전에만 수행을 가로채고 싶다면, 오직 interceptBefore() 메소드만 사용될 뿐 나머지 메소드는 불필요하다.AOP(aspect-oriented programming)에서 프로그램의 서로 다른 단위를 가로채는 것을 advice 라고 표현한다. 그래서 before advice, after return advice, 또는 throws advice 를 위해 각각 별도의 인터페이스를 작성할 수 있다. 또 다른 타입의 advice는 범용적이고 유연한 advice로서 이런 모든 타입을 합친 것이 있다. 하지만, 범용적인 만큼 개발자의 책임이 커지게 된다. (개발자는 다음 대상에 대해서 메소드 호출이 전달되도록 해야 한다.) 이런 advice 타입을 around advice 라고 부른다. 방식 3에서는 AOP의 around advice와 비슷한 형태로 구현해볼 것이다.

방식1과 방식2는 모두 한계를 갖고 있으며, 큰 수정 없이는 다이나믹 프록시의 모든 장점을 사용할 수 없다. 예를 들어, interceptBefore()와 interceptAfter()는 별도로 호출되기 때문에, interceptBefore()와 interceptAfter() 사이에 연결되는 것을 구현하는 것은 쉽지 않다. 예를 들어, 특정한 원본 비즈니스 객체를 호굴하기 전에 다음과 같이 동기화를 하고 싶다고 해 보자.

    synchronized(realtarget) {
        retObject = method.invoke(target, args);
    }

방식1과 방식2에서는 이를 구현하는 것이 어렵다. 이를 해결하기 위한 방법을 방식3에서 살펴보도록 하자.

범용 다니아믹 프록시 체인 구현: 방식3

방식3에서, 클라이언트는 방식1 및 방식2와 비슷한 방법을 프록시 객체를 사용한다.

    String[] invocationHandlers = {"MyDebugInvocationHandler",
        "MyAnotherInvocationHandler"};
    IMyBusinessObject aIMyBusinessObject =
        (IMyBusinessObject)MyProxyFactory.getProxyObject
            ("MyBusinessObject", invocationHandlers);
    String ret = aIMyBusinessObject.doExecute("Hello World");

이 방식에서, IMyInvocationHandler 인터페이스를 다음과 같이 정의하였다.

    public interface IMyInvocationHandler {
        void setTarget(Object target_);
        void setRealTarget(Object realtarget_);
    }

또한, IMyInvocationHandler 인터페이스와 java.lang.reflect.InvocationHandler 인터페이스를 구현한 추상클래스를 다음과 같이 정의하였다.

    public abstract class AMyInvocationHandler
        implements IMyInvocationHandler,
            java.lang.reflect.InvocationHandler {
    
        protected Object target = null;
        protected Object realtarget = null;
    
        public void setTarget(Object target_) {
            this.target = target_;
        }
        public void setRealTarget(Object realtarget_) {
            this.realtarget = realtarget_;
        }
    }

방식3은 GenericInvocationHandler 클래스나 IMethodInterceptor 인터페이스를 사용하지 않는다. 대신, 모든 인터셉터가 위의 AMyInvocationHandler 추상 클래스를 상속받는다. 방식1과 방식2에서 살펴봤었던 두개의 인터셉터를 작성해보도록 하자. 이 두 인터셉터는 AMyInvocationHandler를 상속받으며, Invocationhandler 인터페이스에 정의된 invoke() 메소드를 구현한다.

  1.     public class MyDebugInvocationHandler extends AMyInvocationHandler {
        
           public Object invoke(Object proxy, Method method, Object[] args)
              throws Throwable {
        
              try {
                 System.out.println("MyDebugInterceptor: Before execute method : "
                    + method.getName());  
                 Object retObject = method.invoke(target, args);
                 System.out.println("MyDebugInterceptor: After execute method : "
                    + method.getName());
                 return retObject;
              }
              catch(InvocationTargetException e) {
                 throw e.getTargetException();
              }
              catch(Exception e) {
                 throw e;
              }
           }
        }

  2.     public class MyAnotherInvocationHandler extends AMyInvocationHandler {
        
           public Object invoke(Object proxy, Method method, Object[] args)
              throws Throwable {
        
              try {
                 System.out.println("MyAnotherInvocationHandler: Before execute method : "
                    + method.getName());        
                 if ( method.getName().equals("doExecute")
                    && args != null && args.length >= 1 ) {
                    if ( args[0] instanceof String ) {
                       args[0] = args[0] + " Modified by MyAnotherInvocationHandler";
                    }
                 }
                 Object retObject = method.invoke(target, args);
                 System.out.println("MyAnotherInvocationHandler: After execute method : "
                    + method.getName());
                 return retObject;
              }
              catch(InvocationTargetException e) {
                 throw e.getTargetException();
              }
              catch(Exception e) {
                 throw e;
              }
           }
        }

MyDebugInvocationHandler 클래스와 MyAnotherInvocationHandler 클래스는 doExecute()를 실행하기 이전/이후에 호출되거나 예외가 발생할 때 호출될 별도의 메소드를 갖고 있지 않다. 방식3은 서블릿의 Filter 인터페이스가 doBefore()나 doAfter()가 없는 것과 유사하다.

방식3은 방식1 및 방식2와 달리 개발자에게 더 많은 유연함을 제공한다. 예를 들어, 방식3을 사용하면 앞서 말했었던 동기화를 수행할 수 있다. 단지 AMyInvocationHandler를 상속받고 invoke() 메소드에서 동기화 로직을 수행하기만 하면 된다. 예를 들면, 다음과 같이 구현할 수 있을 것이다.

    public class MySynchronizeInvocationHandler extends AMyInvocationHandler {
    
       public Object invoke(Object proxy, Method method, Object[] args)
          throws Throwable {
          
          Object retObject = null;
          synchronized(realtarget) {
             retObject = method.invoke(target, args);
          }
          return retObject;
       }
    }

이제 방식3의 MyProxyFactory 코드를 살펴보자.

    public static Object getProxyObject( String className,
       String[] invocationHandlers ) throws Throwable {
    
       Object inputObject = getTargetObject(className);
       if ( invocationHandlers != null &&
          invocationHandlers.length > 0  ) {
    
          Object inputProxiedObject = inputObject;
          for ( int i=0; i < invocationHandlers.length; i++ ) {
             AMyInvocationHandler myInvocationHandler =
                (AMyInvocationHandler)getInvocationHandler
                   (invocationHandlers[i]);
             inputProxiedObject = getProxyObject(inputObject,
                myInvocationHandler, inputProxiedObject);
          }
          return inputProxiedObject;
       }
       else {
          return inputObject;
    
       }
    }
    public static Object getProxyObject( Object inputObject,
       Object[] invocationHandlers ) throws Throwable {
    
       if ( invocationHandlers != null
          && invocationHandlers.length > 0  ) {
    
          Object inputProxiedObject = inputObject;
          for ( int i=0; i < invocationHandlers.length; i++ ) {
             inputProxiedObject = getProxyObject(inputObject,
                (AMyInvocationHandler)invocationHandlers[i],
                   inputProxiedObject);
          }
    
          return inputProxiedObject;
       }
       else {
          return inputObject;
       }
    }
    private static Object getProxyObject(Object inObject,
       AMyInvocationHandler myInvocationHandler,
          Object inProxiedObject) throws Throwable {
    
       if ( myInvocationHandler == null ) {
          return inProxiedObject;
       }
       myInvocationHandler.setTarget(inProxiedObject);
       myInvocationHandler.setRealTarget(inObject);
    
       return Proxy.newProxyInstance
                      (inObject.getClass().getClassLoader(),
                      inObject.getClass().getInterfaces(),
                      myInvocationHandler) ;
    }

이 코드는 방식의 MyProxyFactory와 거의 유사하다. 위 코드는 또한 InvocationHandler의 배열을 순차적을 처리하고, InvocationHandler와 프록시 객체를 각각의 반복마다 한개씩 생성한다. 방식1에서는 GenericInvocationHandler 인스턴스, 인터셉터 인스턴스, 프록시 객체가 반복때마다 각각 한번씩 생성되었지만, 방식3에서는 InvocationHandler 인스턴스와 프록시 객체만 반복때마다 한번씩 생성된다. (인터셉터가 없으므로 그런 것이다.)

위 코드를 보면 MyProxyFactory 클래스에 다음과 같은 또다른 public static 메소드가 추가되었다.

    public static Object getProxyObject
       ( Object inputObject, Object[] invocationHandlers )

Class.forName()을 사용해서 비즈니스 원본 객체를 생성하게 되면 비즈니스 객체/InvocationHandler/인터셉터를 사용하기 전에 특정 처리를 수행할 수 없게 된다. 그래서, 그런 처리를 하기 위해 위와 같은 메소드를 추가하였다. (방식1과 방식2에서도 비슷한 메소드를 추가할 수 있다.) 이제, 기존에 존재하는 비즈니스 객체나 InvocationHandler/인터셉터를 MyProxyFactory에 전달하여 프록시 체인을 생성할 수 있다. 예를 들어, 다음과 같이 코드를 작성할 수 있게 된 것이다.

    MyDebugInvocationHandler dHandler = new MyDebugInvocationHandler();
    MyAnotherInvocationHandler aHandler = new MyAnotherInvocationHandler();
    IMyBusinessObject bo = new MyBusinessObject();
    
    //비즈니스 객체(bo)나 InvocationHandler(dHandler, aHander)에 대해
    //별도 처리를 수행할 수 있다. 또한 이들 객체를 싱글톤으로 구할수도 있다.
    
    Object[] invocationHandlers = { dHandler, aHandler };
       (IMyBusinessObject)MyProxyFactory.getProxyObject
          (bo , invocationHandlers);
    String ret = aIMyBusinessObject.doExecute("Hello World");

방식3에서 클래스 다이어그램은 다음과 같다.


 
각 방식의 성능 비교 및 결론

앞서 소개한 각각의 접근 방식의 수행속도의 차이를 비교하기 위해서 간단한 테스트를 실행해보았다. 테스트 기계(CPU 개수, CPU 속도, 메모리 사용 등)를 고려하지 않고 테스트하였으므로 확장성은 고려되지 않았다. 하지만, 단순한 테스트를 통해서 각 구현 방식의 성능에 대한 개략적인 아이디어를 얻을 수 있을 것이다.

테스트는 프록시 팩토리로 생성한 프록시 객체의 메소드를 천번 호출하는 것으로 진행하였다. 다음 정보는 천번 호출에 대한 평균을 1/1000초 단위로 정리한 것이다.

  • 정적 decorator 체인: 51.7
  • 방식1: 166.5
  • 방식2: 125.1
  • 방식3: 159.25
동적 프록시 체인에서 수행 속도가 중요하다면 방식2를 고려해 볼 수 있다는 것을 알 수 있다.

결론

이 글에서는 static 메소드를 사용하여 프록시 체인을 생성했는데, 만약 팩토리 클래스를 상속하거나 서로 다른 설정을 가진 팩토리 인스턴스가 필요한 경우에는 문제가 된다. 이런 경우에는 알맞은 팩토리 패턴을 사용해서 여러분 스스로 팩토리 클래스를 작성하기 바란다.

본 글에서는 자바로 다이나믹 프록시를 구현하는 방법을 살펴봄으로써 AOP의 일부 개념을 다뤄보았다. join point나 point cut과 같은 다른 AOP의 개념 역시 팩토리 클래스에서 구현할 수 있을 것이다. 이에 대한 자세한 내용은 하단의 '관련 링크'를 참고하기 바란다.

기존에 존재하는 AOP 프레임워크가 제공하는 모든 기능이 필요없고 일부의 기능만 필요하다면 여러분이 직접 범용 다이나믹 프록시를 사용하여 작은 AOP 프레임워크를 구현할 수 있게 되었다. 본 글에서 살펴본 내용이 여러분이 직접 AOP 프레임워크를 구현하는데 필요한 기본 아이디어를 제공했으리라 생각한다. 또한, 기존에 다이나믹 프록시 방식을 사용하는 AOP 프레임워크를 사용하고 있었다면, 본 글을 통해서 해당 AOP 프레임워크의 사용법에 대한 이해를 높일 수 있게 되었을 것이다.

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요

  1. corenel 2015.03.27 10:38 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 내용 감사합니다.

CGLIB를 이용하여 고성능의 프록시 객체를 만드는 방법에 대해서 살펴본다.

CGLIB 소개 및 설치

CGLIB는 코드 생성 라이브러리로서(Code Generator Library) 런타임에 동적으로 자바 클래스의 프록시를 생성해주는 기능을 제공한다. CGLIB를 사용하면 매우 쉽게 프록시 객체를 생성할 수 있으며, 성능 또한 우수하다. 더불어, 인터페이스가 아닌 클래스에 대해서 동적 프록시를 생성할 수 있기 때문에 다양한 프로젝트에서 널리 사용되고 있다. 예를 들어, Hibernate는 자바빈 객체에 대한 프록시를 생성할 때 CGLIB를 사용하며, Spring은 프록시 기반의 AOP를 구현할 때 CGLIB를 사용하고 있다.

CGLIB의 주요 구성 요소

CGLIB는 프록시 생성과 관련된 모듈은 아래 그림과 같이 Enhancer 클래스, Callback 인터페이스 그리고 CallbackFilter 인터페이스이다. 이 세가지만 있으면 아주 손쉽게 원하는 프록시 객체를 생성할 수 있게 된다.


Callback 인터페이스를 구현한 인터페이스를 두가지(MethodInterceptor, NoOp)만 표시했는데 이외에도 몇가지 인터페이스가 더 존재한다. 하지만, MethodInterceptor 인터페이스가 주로 사용되며, 본 글에서도 MethodInterceptor를 사용한 프록시 기능에 대해서만 살펴볼 것이다.

CGLIB 설치하기

CGLIB의 설치 방법은 매우 간단하다. 필요한 jar 파일을 클래스패스로 설정해주기만 하면 된다. CGLIB 모듈은 CGLIB 프로젝트 사이트인 http://cglib.sourceforge.net/ 에서 다운로드 받을 수 있다. 현재 버전은 2.1.3 버전으로서 다운로드 사이트에서 다음의 두가지 파일 중 하나를 받으면 된다.

  • cglib-2.1_3.jar - ASM 모듈을 필요로 하는 버전
  • cglib-nodep-2.1_3.jar - ASM 모듈이 포함되어 있는 버전
필자의 경우는 ASM 모듈이 필요하지 않는 cglib-nodep-2.1_3.jar 파일을 사용하고 있다. 만약 ASM 모듈이 필요한 cglib-2.1_3.jar 파일을 사용하려면 ASM 프로젝트 사이트에서 ASM 모듈을 다운로드 받아야 한다.

이제 남은 작업은 cglib-nodep-2.1_3.jar 파일을 클래스패스에 추가해주는 것 뿐이다. 그럼, CGLIB를 사용할 준비가 끝난다.

Enhancer를 사용한 프록시 객체 생성 및 MethodInterceptor 사용하기

CGLIB를 사용하여 프록시를 생성할 때에는 크게 크게 두가지 작업을 필요로 한다.

  • net.sf.cglib.proxy.Enhancer 클래스를 사용하여 원하는 프록시 객체 만들기
  • net.sf.cglib.proxy.Callback을 사용하여 프록시 객체 조작하기
위의 두가지만 익히면 CGLIB를 사용하여 매우 손쉽게 프록시 객체를 생성할 수 있다.

본 글에서는 아래의 클래스를 프록시 대상으로 사용할 것이다. 테스트 결과를 눈으로 확인할 수 있도록 하기 위해 System.out.println()을 사용해서 알맞은 문자를 출력하도록 했다.

    package test;
    
    public class MemberServiceImpl implements MemberService {
        public MemberServiceImpl() {
            System.out.println("create MemberServiceImpl");
        }
        public void regist(Member member) {
            System.out.println("MemberServiceImpl.regist");
        }
        public Member getMember(String id) {
            System.out.println("MemberServiceImpl.getMember:"+id);
            return new Member();
        }
    }

Enhancer 클래스를 사용한 프록시 객체 생성 하기

이제 net.sf.cglib.proxy.Enhancer 클래스를 사용하여 MemberServiceImpl 클래스의 프록시 객체를 생성해보자. 생성하는 코드는 다음과 같이 매우 간단한다.

    // 1. Enhancer 객체를 생성
    Enhancer enhancer = new Enhancer();
    // 2. setSuperclass() 메소드에 프록시할 클래스 지정
    enhancer.setSuperclass(MemberServiceImpl.class);
    enhancer.setCallback(NoOp.INSTANCE);
    // 3. enhancer.create()로 프록시 생성
    Object obj = enhancer.create();
    // 4. 프록시를 통해서 간접 접근
    MemberServiceImpl memberService = (MemberServiceImpl)obj;
    memberService.regist(new Member());
    memberService.getMember("madvirus");

단 4줄의 코드로 프록시 객체를 생성할 수 있다. 위 코드는 다음과 같은 구조의 프록시 객체를 생성하게 된다.


즉, enhancer.create()로 생성한 객체는 프록시 객체가 되고, 이 객체의 메소드를 호출하게 되면 프록시 객체를 거쳐서 실제 객체의 메소드가 호출된다. 실제로 위 코드를 실행하면 다음과 같은 결과가 출력된다.

    create MemberServiceImpl
    MemberServiceImpl.regist
    MemberServiceImpl.getMember:madvirus

MethodInterceptor 사용하여 프록시 객체 다루기

앞서 실행 결과를 보면 직접 MemberServiceImpl 객체를 생성해서 실행하는 것과 별반 차이가 없어보인다. 이는 프록시 객체가 단순히 원본 객체의 메소드를 직접적으로 호출하기 때문이다. 하지만, 대부분의 프록시 객체는 원본 객체에 접근하기 전에 별도의 작업을 수행하며, CGLIB는 Callback을 사용해서 별도 작업을 수행할 수 있도록 하고 있다.

CGLIB가 제공하는 여러가지 Callback 중 앞서 코드에서도 나왔던 net.sf.cglib.proxy.NoOp 는 아무 작업도 수행하지 않고 곧바로 원본 객체를 호출하는 Callback 이다.

CGLIB가 제공하는 Callback 중 가장 많이 사용되는 것은 net.sf.cglib.proxy.MethodInterceptor 이다. MethodInterceptor는 다음과 같이 프록시와 원본 객체 사이에 위치하여 메소드 호출을 조작할 수 있도록 해 준다.


프록시 객체에 대한 모든 호출이 MethodInterceptor를 거친뒤에 원본 객체에 전달된다. 따라서, MethodInterceptor를 사용하면 원본 객체 대신 다른 객체의 메소드를 호출할 수 있도록 할 수 있으며, 심지어 원본 객체에 전달될 인자의 값을 변경할 수도 있다.

MethodInterceptor 인터페이스는 다음과 같 1개의 메소드를 선언하고 있다.

  • public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy) throws Throwable
intercept() 메소드는 4개의 파라미터가 있는데, 이들 파라미터는 다음과 같다.

  • object: 원본 객체
  • method: 원본 객체의 호출될 메소드를 나타내는 Method 객체
  • args: 원본 객체에 전달될 파라미터
  • methodProxy: CGLIB가 제공하는 원본 객체의 메소드 프록시.
MethodInterceptor 인터페이스를 구현한 클래스는 intercept() 메소드에서 원본 객체의 메소드를 알맞게 호출해주어야 한다. 원본 객체의 메소드를 호출하는 방법은 다음과 같은 두가지가 있다.

    // 방법1: 자바의 리플렉션 사용
    Object returnValue = method.invoke(object, args);
    
    // 방법2: CGLIB의 MethodProxy 사용
    Object returnValue = methodProxy.invokeSuper(object, args);

이 두가지 방법중에서 어떤 것을 사용해도 무방하지만, 리플렉션 방식보다 MethodProxy 방식이 빠르다고 하니 MethodProxy를 사용할 것을 추천한다.

MethodInterceptor를 사용하면 원본 객체의 메소드를 호출하기 전에 여러가지 작업을 수행할 수 있게 된다. 예를 들어, MemberServiceImpl에 대한 모든 메소드 호출 기록을 로그로 남기고 싶다면 다음과 같이 MethodInterceptor 구현 클래스를 작성하면 된다.

    package test;
    
    import java.lang.reflect.Method;
    
    import net.sf.cglib.proxy.MethodInterceptor;
    import net.sf.cglib.proxy.MethodProxy;
    
    public class MethodCallLogInterceptor implements MethodInterceptor {
    
        public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy)
        throws Throwable {
            System.out.println("before MemberServiceLogger.intercept()");
            Object returnValue = methodProxy.invokeSuper(object, args);
            System.out.println("after MemberServiceLogger.intercept()");
            return returnValue;
        }
    }

Enhancer.setCallback() 메소드를 통해서 사용할 MethodInterceptor 객체를 지정해주기만 하면 MethodInterceptor가 사용된다. 앞서 작성한 MethodCallLogInterceptor을 MemberServiceImpl 프록시에 적용하려면 다음과 같은 코드를 사용하면 된다.

    Enhancer enhancer = new Enhancer();
    enhancer.setSuperclass(MemberServiceImpl.class);
    enhancer.setCallback(new MethodCallLogInterceptor());
    Object obj = enhancer.create();
    MemberServiceImpl memberService = (MemberServiceImpl)obj;
    memberService.regist(new Member());

위와 같이 Enhancer.setCallback()을 통해서 MethodInterceptor 를 지정하면, 프록시의 메소드를 호출할 때 마다 MethodInterceptor.intercept() 메소드가 호출된다. 다음은 위 코드의 실행 결과인데, 원본 객체의 메소드가 호출되기 전에 MethodCallLogInterceptor.intercept() 메소드가 실행된 것을 확인할 수 있다.

    
    before MethodCallLogInterceptor.intercept() -> MethodCallLogInterceptor
    MemberServiceImpl.regist                    -> 원본 객체 메소드 실제 호출
    after MethodCallLogInterceptor.intercept()  -> MethodCallLogInterceptor
    before MethodCallLogInterceptor.intercept()
    MemberServiceImpl.getMember:madvirus        -> 원본 객체 메소드 실제 호출
    after MethodCallLogInterceptor.intercept()

MethodCallLogInterceptor 는 원본 객체의 메소드를 호출하는 앞뒤로 간단한 문장을 출력하는 정도로 끝났지만, 다음과 같이 다양한 형태로 MethodInterceptor 를 구현할 수 있을 것이다. 예를 들어, 다음과 같이 조건에 따라서 원본 객체가 아닌 다른 객체의 메소드를 호출할 수도 있을 것이다.

    public Object intercept(Object object, Method method, Object[] args, MethodProxy methodProxy)
    throws Throwable {
        Object returnValue = null;
        if (어떤 조건) {
            returnValue = otherService.getMember((String)args[0]);
        } else {
            returnValue = methodProxy.invokeSuper(object, args);
        }
        return returnValue;
    }

CallbackFilter를 사용하여 알맞은 Callback 실행하기

여러개의 Callback 중에서 상황에 따라서 원하는 Callback을 사용하고 싶은 경우도 있을 것이다. 예를 들어, MemberServiceImpl.regist() 메소드와 MemberServiceImpl.getMember() 메소드에 대해서 서로 다른 Callback을 적용하고 싶은 경우가 있을 것이다. 이럴 땐 net.sf.cglib.proxy.CallbackFilter 인터페이스를 사용하면 된다.

CallbackFilter 인터페이스는 다음과 같은 메소드를 정의하고 있다.

  • public int accept(Method method) - 메소드에 적용될 Callback의 인덱스를 리턴
accept 메소드는 사용할 Callback의 인덱스를 리턴하는데, 이때 인덱스 값은 Enhancer.setCallbacks(Callback[]) 메소드에 전달된 Callback 배열에서의 인덱스를 의미한다. 예를 들어, 다음과 같이 두개의 Callback을 Enhancer.setCallbacks()에 전달했다고 해 보자.

    ...
    enhancer.setSuperclass(MemberServiceImpl.class);
    Callback[] callbacks = new Callback[] {
        new RegistMemberInterceptor(), // 인덱스 0
        new GetMemberInterceptor() // 인덱스 1
    };
    enhancer.setCallbacks(callbacks); // 배열로 Callback 목록 지정
    enhancer.setCallbackFilter(new MemberServiceCallbackFilter());
    

이때 CallbackFilter 인터페이스를 구현한 MemberServiceCallbackFilter 클래스는 다음과 같이 구현할 수 있다.

    package test;
    
    import java.lang.reflect.Method;
    
    import net.sf.cglib.proxy.CallbackFilter;
    
    public class MemberServiceCallbackFilter implements CallbackFilter {
    
        public int accept(Method method) {
            if (method.getName().equals("regist")) {
                return 0; // RegistMemberInterceptor를 선택
            } else {
                return 1; // GetMemberInterceptor를 선택
            }
        }
    }

RegistMemberInterceptor와 GetMemberInterceptor는 intercept() 메소드에서 단순히 클래스 이름을 출력한 뒤 원본 객체의 메소드를 호출하도록 구현하였으며,

    enhancer.setSuperclass(MemberServiceImpl.class);
    Callback[] callbacks = new Callback[] {
        new RegistMemberInterceptor(),
        new GetMemberInterceptor()
    };
    enhancer.setCallbacks(callbacks);
    enhancer.setCallbackFilter(new MemberServiceCallbackFilter());
    Object obj = enhancer.create();
    MemberServiceImpl memberService = (MemberServiceImpl)obj;
    memberService.regist(new Member());
    memberService.getMember("madvirus");

위 코드를 실행할 경우 다음과 같이 메소드 이름에 따라서 알맞은 Callback이 실행되는 것을 확인할 수 있다.

    RegistMemberInterceptor.intercept()   -> regist 메소드 호출시 콜백
    MemberServiceImpl.regist
    GetMemberInterceptor.intercept()      -> getMember 메소드 호출시 콜백
    MemberServiceImpl.getMember:madvirus    

맺음말

CGLIB는 강력하면서도 고성능의 코드 생성 라이브러리로서, 인터페이스를 필요로 하는 JDK의 다이나믹 프록시 대신 사용될 수 있다. 더불어, CGLIB는 바이트 코드를 조작하는 프레임워크인 ASM을 사용함으로써 리플렉션을 사용하는 JDK 다이나믹 프록시보다 빠르다. 이런 이유로 Hibernate, Spring, dynaop와 같은 다양한 프레임워크에서 CGLIB를 사용하고 있는 것이다. 여러분도 고성능의 프록시 객체가 필요할 경우 CGLIB를 사용해보기 바란다.

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요

틀 서치 유틸을 이용하여 일반 SQL과 Hibernate의 HQL, Criteria의 검색 조건을 간단하게 처리하는 방법을 살펴본다.

SearchFilter를 이용한 검색 조건 생성

검색 조건이 다양한 형태로 결정되는 경우가 있다. 예를 들어, 검색 조건으로 기간, 이름, 공지여부의 세가지가 있다고 할 경우 다음과 같이 다양한 형태의 검색 조건이 나올 수 있다.

  • 최근 한달간 등록된 공지글
  • 3개월 전부터 2개월 전 사이에 등록된 글
  • 공지글이거나 또는 등록자 이름이 '관리자'인 글
이 외에도 수많은 조합으로 검색 조건이 결정될 수 있는데, 이를 처리하기 위해서는 조건에 따라 쿼리를 다르게 생성해주어야만 한다. PreparedStatement를 사용하는 경우 파라미터(쿼리에서 물음표로 표시되는 것)의 인덱스를 결정하기 위해 아래와 같은 형태의 코드를 사용하기도 한다.

    StringBuffer query = new StringBuffer();
    ...
    if (조건1) {
        query.append("name like ?");
    }
    if (조건2) {
        query.append("and register_date between ? and b");
    }
    pstmt = conn.prepareStatement(query.toString());
    int paramIdx = 1;
    if (조건1) {
        pstmt.setString(paramIdx++, value);
    }
    if (조건2) {
        pstmt.setTimestamp(paramIdx++, fromDate);
        pstmt.setTimestamp(paramIdx++, toDate);
    }
    ...

검색 조건이 적은 경우에만 위와 같은 코드가 볼만하지만, 조건이 많아질수록 코드가 복잡해지고 검색 조건이 조금이라도 변경되면 코드에 버그가 생길 가능성이 높아진다. 물론 Hibernate와 같은 API는 Criteria를 통해서 검색 조건을 알맞게 지정해주는 기능이 있지만 HQL을 사용해야 하는 경우는 PreparedStatement를 사용하는 경우와 마찬가지로 복잡한 코드를 만들게 된다.

TLESearchUtil은 PreparedStatement를 사용하든, HQL을 사용하든, 또는 Criteria를 사용하든 검색 조건을 동일한 코드로 지정할 수 있게 함으로써 검색 조건 처리 코드를 쉽게 작성할 수 있도록 도와주는 유틸리티 모듈이다.

TLESearchUtil을 사용하기 위한 준비

TLESearchUtil을 사용하려면 먼저 아래의 순서대로 TLESearchUtil 모듈을 준비해야 한다.

  1. 틀 프로젝트 사이트(http://kldp.net/projects/tle)에서 TLESearchUtil을 다운로드 받는다.
  2. 다운로드 받은 TLESearchUtil-1.x.zip 파일의 압축을 푼다.
  3. 압축을 풀면 생성되는 TLESearchUtil-1.x.jar 파일을 클래스패스에 복사한다.
웹 어플리케이션에서 사용할 경우 /WEB-INF/lib 폴더에 TLESearchUtil-1.x.jar 파일을 복사하면 된다.

SearchFilter를 사용한 검색 조건 생성

검색 조건은 아래의 두 클래스를 통해서 생성할 수 있다.

  • tle.searchutil.SearchFilter - 검색 조건을 저장한다.
  • tle.searchutil.Restriction - SearchFilter에 검색 조건을 추가할 때 사용된다.
이 두가지 클래스를 사용해서 다양한 조합의 검색 조건을 지정할 수 있다. 아래 코드는 위의 두 클래스를 사용하여 검색 조건을 지정하는 방식을 보여주고 있다.

    SearchFilter filter = new SearchFilter();    filter.addCond(Restriction.eq("BOARD_ID", "1111"));
    filter.addCond(Restriction.like("NAME", "최", Match.ANYWHERE);

검색 조건을 생성할 때에는 위 코드에서 볼 수 있듯이 두가지 단계를 거친다.

  1. 검색 조건을 저장할 SearcFilter 객체를 생성한다.
  2. Restriction 이 제공하는 메소드를 사용하여 검색 조건을 생성한 뒤, SearchFilter.addCond() 메소드를 호출해서 검색 조건을 추가한다.
위의 코드는 아래와 같은 검색 조건을 의미한다.

  • "BOARD_ID" 컬럼 값이 "1111" 이고
  • "NAME" 컬럼 값에 "최"를 포함
Restriction 클래스는 다양한 검색 조건을 제공하는 데 많이 사용되는 검색 조건은 아래와 같다.

tle.searchutil.Restriction 클래스가 제공하는 메소드
메소드 설명
eq(String name, Object value) 이름이 name인 컬럼의 값이 value인지 검사한다
eq(String name, byte value) 이름이 name인 컬럼의 값이 value인지 검사한다
eq(String name, short value) 이름이 name인 컬럼의 값이 value인지 검사한다
eq(String name, int value) 이름이 name인 컬럼의 값이 value인지 검사한다
eq(String name, long value) 이름이 name인 컬럼의 값이 value인지 검사한다
eq(String name, float value) 이름이 name인 컬럼의 값이 value인지 검사한다
eq(String name, double value) 이름이 name인 컬럼의 값이 value인지 검사한다
ne(String name, Object value) 이름이 name인 컬럼의 값이 value가 아닌지 검사한다
ge(String name, Object value) 이름이 name인 컬럼의 값이 value 보다 크거나 같은 지 검사
gt(String name, Object value) 이름이 name인 컬럼의 값이 value 보다 큰지 검사
le(String name, Object value) 이름이 name인 컬럼의 값이 value 보다 작거나 같은 지 검사
lt(String name, Object value) 이름이 name인 컬럼의 값이 value 보다 작은 지 검사
between(String name, Object lo, Object hi) 이름이 name인 컬럼의 값이 lo와 hi 사이에 존재하는 지 검사
in(String name, Collection values) 이름이 name인 컬럼의 값이 values에 포함된 값 목록에 포함되는 지 검사
in(String name, Object[] values) 이름이 name인 컬럼의 값이 values에 포함된 값 목록에 포함되는 지 검사
notIn(String name, Collection values) 이름이 name인 컬럼의 값이 values에 포함된 값 목록에 포함되지 않는 지 검사
notIn(String name, Object[] values) 이름이 name인 컬럼의 값이 values에 포함된 값 목록에 포함되지 않는 지 검사
like(String name, String value) 이름이 name인 컬럼의 값에 value가 포함되는 지 검사
like(String name, String value, int matchMode) 이름이 name인 컬럼의 값에 value가 포함되는 지 검사. matchMode에 따라 검사 기준이 달라짐
isNotNull(String name) 이름이 name인 컬럼의 값이 null이 아닌지 검사
isNull(String name) 이름이 name인 컬럼의 값이 null인지 검사

(위의 메소드 외에 몇가지 조건들이 더 존재하나 대체적으로 위의 것들이 많이 사용된다. 나머지 메소드들은 다운로드 받은 TLESearchUtil 배포판에 포함된 API 문서를 통해서 확인하기 바란다.)

예를 들어, 최근 30일 이내에 가입한 회원 중에 메일을 수신하고 이메일 주소가 유아시스인 회원 목록을 추출할 때 사용되는 검색 조건을 생성할 때에는 다음과 같이 검색 조건을 지정할 수 있다.

    SearchFilter filter = new SearchFilter();
    Calendar cal = Calendar.getInstance();
    cal.set(Calendar.DATE, -30);
    
    filter.addCond( Restriction.gt("REGISTER_DATE", cal.getTime()) );
    filter.addCond( Restriction.eq("MAILING_YN", YesNo.TRUE) );
    filter.addCond( Restrictoin.like("EMAIL", "@uasis.com", Match.END) );

like 검색 모드는 tle.searchutil.Match 인터페이스에 정의된 아래의 상수값을 사용하여 지정한다.

  • ANYWHERE - 문장을 포함
  • START - 문장으로 시작
  • END - 문장으로 시작
  • EXACT - 정확하게 일치
예를 들어, 이름이 "최"로 시작하는 검색 조건을 지정할 때에는 다음과 같은 코드를 사용한다.

    filter.addCond( Restriction.like("NAME", "최", Match.START) );

boolean 타입의 처리

자바의 boolean 타입을 DB에 저장할 때에는 일반적으로 다음과 같이 3가지 정도의 방법을 사용한다.

  • true를 'Y'로, false를 'N'으로 저장한다. - Yes No 스타일
  • true를 'T'로, false를 'F'으로 저장한다. - True False 스타일
  • true를 1로, false를 0 으로 저장한다. - 숫자 스타일
이 세가지 방식에 따라서 true/false의 값이 달라지게 되는데 TLESearchUtil은 이를 명확하게 표현하기 위해서 다음과 같은 세 개의 클래스를 제공하고 있다.

  • tle.searchutil.YesNo - Yes No 스타일로 true/false 값을 지정할 때 사용된다.
  • tle.searchutil.TrueFalse - True False 스타일로 true/false 값을 지정할 때 사용된다.
  • tle.searchutil.OneZero- 숫자 스타일로 true/false 값을 지정할 때 사용된다.
예를 들어, "MAILING_YN" 컬럼은 메일을 수신할 경우 "Y"를 값으로 갖고 수신하지 않을 경우 "N"을 값으로 갖고, 반면에 "MALE_TF" 컬럼은 남자인 경우 "T"를 여자인 경우 "F"를 값으로 갖는다고 해 보자. 이 경우 다음과 같이 조건을 지정할 수 있다.

    filter.addCond( Restriction.eq("MAILING_YN", YesNo.TRUE) );
    filter.addCond( Restriction.eq("MALE_TF", TrueFalse.FALSE) );

YesNo 클래스, TrueFalse 클래스 그리고 OneZero 클래스는 모두 TRUE와 FALSE 상수필드를 정의하고 있으므로, 위 코드에서처럼 TRUE 또는 FALSE 상수필드를 사용해서 값을 지정할 수 있다.

and와 or의 처리

검색 조건은 and와 or를 통해서 복잡하게 조합될 수 있다. 예를 들어 아래의 조건을 생각해보자.

  • 가입일이 30일 이내이면서, 유효한 회원이고, 로그인 회수가 20회 이상인 회원
  • 또는
  • 가입일이 30일 이전이면서, 유효한 회원이고, 최근 로그인 시간이 1루 이내인 회원
위의 조건은 or를 통해서 다음과 같은 쿼리로 표현될 것이다.

    (
        REGISTER_DATE >= '2005-07-25' and 
        VALID_YN = 'Y' and 
        LOGIN_COUNT >= 20
    )
    
    or
    
    (
        REGISTER_DATE <= '2005-07-24' and 
        VALID_YN = 'Y' and 
        LAST_LOGIN_DATE >= '2005-08-24
    )

위와 같이 or 조건이나 and 조건을 표시할 때에는 Restriction 클래스가 제공하는 or()와 and() 메소드를 사용하면 된다. 먼저 여러 조건을 and 연산으로 묶을 때에는 다음과 같이 and() 메소드를 사용해서 지정하면 된다.

    filter.addCond(
        Restriction.and()
                         .addCond( Restriction.ge("REGISTER_DATE", day10) )
                         .addCond( Restriction.eq("VALID_YN", YesNo.TRUE) )
                         .addCond( Restriction.ge("LOGIN_COUNT", 20) )
    );

Restriction.and() 메소드를 호출하면 tle.searchutil.CondAnd 객체를 리턴하는데, 이 클래스의 addCond() 메소드를 사용해서 연속적으로 and로 묶일 조건을 지정할 수 있다. CondAnd.addCond() 메소드를 통해서 추가될 검색 조건은 SearchFilter.addCond()와 마찬가지로 Restriction 클래스가 제공하는 메소드를 통해서 생성할 수 있다.

or 조건은 Restriction.or() 메소드를 사용하여 지정하며 Restriction.and()와 동일한 방법으로 사용된다. 예를 들면 아래와 같다.

    filter.addCond(
        Restriction.or()
                         .addCond( Restriction.ge("REGISTER_DATE", day10) )
                         .addCond( Restriction.eq("VALID_YN", YesNo.TRUE) )

이제 앞서 SQL 쿼리로 표현했던 검색 조건을 SearchFilter를 사용해서 작성해보면 다음과 같다.

    
    filter.addCond(
        Restriction.or()
            .addCond( // or의 첫번째
                Restriction.and()
                        .addCond(Restriction.ge("REGISTER_DATE", before30day))
                        .addCond(Restriction.eq("VALID_YN", YesNo.TRUE))
                        .addCond(Restriction.ge("LOGIN_COUNT", new Integer(20)))
            )
            .addCond( // or의 두번째
                Restriction.and()
                        .addCond(Restriction.lt("REGISTER_DATE", before30day))
                        .addCond(Restriction.eq("VALID_YN", YesNo.TRUE))
                        .addCond(Restriction.ge("LAST_LOGIN_DATE", before1day))
            )
    );

SearchFilter를 사용한 정렬 조건 생성

SearchFilter.addOrder() 메소드를 사용해서 정렬 순서를 지정할 수 있다. 예를 들어, "REGISTER_DATE" 필드의 내림차순으로 정렬하도록 정보를 추가하고 싶다면 다음과 같은 코드를 사용하면 된다.

    SearchFilter filter = new SearchFilter();
    ... // 검색 조건 지정
    filter.addOrder( Restriction.order("REGISTER_DATE", false) );

Restriction.order(String name, boolean isAsc) 메소드는 정렬 옵션을 생성해주는 데, 두번째 파라미터인 isAsc의 값을 true로 지정하면 오름차순을, false로 지정하면 내림차순을 나타낸다.

여러 필드에 대해서 정렬 조건을 지정하고 싶다면 다음과 같이 SearchFilter.addOrder() 메소드를 정렬 순서대로 호출해주면 된다.

    SearchFilter filter = new SearchFilter();
    // 순서대로 정렬 조건을 지정할 수 있다.
    filter.addOrder( Restriction.order("NAME", true) );
    filter.addOrder( Restriction.order("BIRTH", false) );

검색 조건을 쿼리에 적용하기

앞서 SearchFilter와 Restriction을 사용해서 검색 조건을 생성하는 방법에 대해서 살펴봤는데, 검색 조건을 만들면 다음으로 해야 할 일은 검색 조건을 사용해서 쿼리를 수행하는 것이다. TLESearchUtil은 현재 다음의 3가지 타입에 대해서 검색 조건을 처리해줄 수 있다.

  • java.sql.PrepareStatement
  • Hibernate의 Query
  • Hibernate의 Criteria
Hibernate의 경우는 2.1.x 버전과 3.0 버전을 지원하고 있다.

PreparedStatement에 검색 조건 적용하기

먼저 PreparedStatement를 사용해서 SearchFilter에 저장된 검색 조건을 적용하는 방법을 살펴보자. PreparedStatement와 관련된 클래스는 tle.searchutil.sql.SQLQueryHelper 클래스로서, 이 클래스는 다음의 두 기능을 제공한다.

  • PreparedStatement에서 사용되는 SQL 쿼리에서 where 부분에 붙을 SearchFilter에서 지정한 조건에 맞는 쿼리를 생성한다.
  • 쿼리의 파라미터(물음표 ?)에 삽입될 값을 지정한다.
실제 코드를 통해서 어떻게 검색 조건을 쿼리에 적용할 수 있는 지 살펴보자. 아래 코드는 SQLQueryHelper 클래스를 사용해서 실제로 PreparedStatement를 처리하는 코드를 보여주고 있다.

    SearchFilter filter = new SearchFilter();
    ... // filter.addCond()를 통해서 검색 조건 생성
    
    SQLQueryHelper helper = new SQLQueryHelper();    
    try {
        StringBuffer query = new StringBuffer();
        query.append("select * from MEMBER");
        
        List paramValues = null;
        
        if (filter.hasCond()) { // 검색 조건이 있는지의 여부 검사
            query.append(" where ");
            // 검색 조건과 관련된 쿼리를 생성한다
            paramValues = helper.appendWherePart(filter, null, query);
        }
        pstmt = conn.prepareStatement(query.toString());
        
        if (filter.hasConds()) helper.setParameter(pstmt, 1, paramValues);
        
        rs = pstmt.executeQuery();
        ...
        
    } finally {
        if (pstmt != null) try { pstmt.close(); } catch(SQLException ex) {}
        ...
    }

SQLQueryHelper의 appendWherePart(SearchFilter filter, String alias, StringBuffer buffer) 메소드는 SQL 쿼리의 WHERE 부분에 붙을 쿼리를 생성해서 buffer에 추가해준다. 예를 들어, SearchFilter를 아래와 같이 생성했다고 해보자.

    SearchFilter filter = new SearchFilter();
    filter.addCond( Restriction.eq("MAILING_YN", YesNo.TRUE) );
    filter.addCond( Restriction.eq("MALE_TF", TrueFalse.FALSE) );
    
    StringBuffer query = new StringBuffer();
    ...
    SQLQueryHelper helper = new SQLQueryHelper();
    List paramValues = helper.appendWherePart(filter, null, query);
    

위와 같이 검색 조건을 생성한 경우 helper.appendWherePart() 코드는 아래와 같은 SQL 쿼리를 생성해서 StringBuffer query에 추가한다.

    (MAILING_YN = ? and MALE_TF = ?)

SQL 쿼리에서 테이블 이름에 alias를 지정한 경우 appendWherePart() 메소드를 호출할 때 다음과 같이 alias 값을 전달해주어야 한다.

    StringBuffer query = new StringBuffer();
    query.append("select * from MEMBER mem ");
    ...
    helper.appendWherePart(filter, "mem", query);

appendWherePart() 메소드는 파라미터(물음표 ?)에 매핑될 값을 저장한 List를 리턴한다. 이 List는 SQLQueryHelper.setParameter() 메소드에서 파라미터 값을 지정할 때 사용된다. 예를 들면, 아래와 같다.

    StringBuffer query = new StringBuffer();
    ...
    SQLQueryHelper helper = new SQLQueryHelper();
    List paramValues = null;
    if ( filter.hasConds() ) {
        ..
        paramValues = helper.appendWherePart(filter, null, query);
    }
    ...
    pstmt = conn.prepareStatement(query.toString());
    if (filter.hasConds()) helper.setParameter(pstmt, 1, paramValues);
    

setParameter() 메소드는 다음과 같이 세개의 인자를 받는다.

  • 값을 매핑할 PreparedStatement
  • 매핑할 파라미터의 시작 인덱스
  • 파라미터에 매핑될 값을 저장한 List
시작 인덱스는 검색 조건 값들이 매핑될 파라미터의 시작 위치를 의미한다. 예를 들어, 아래와 같이 SearchFilter가 생성한 쿼리 이전에 파라미터를 사용하는 조건이 있을 경우에는 1이 아닌 알맞은 값을 지정해주어야 한다. (아래 코드는 SearchFilter의 검색 조건이 생성하는 파라미터가 3번째이므로 setParameter() 메소드를 호출할 때 시작 인덱스의 값으로 3을 주었다.)

    query.append("where VALID = ? and REGISTER_DATE > ?");
    if ( filter.hasConds() ) {
        query.append(" and ");
        paramValues = helper.appendWherePart(filter, null, query);
    }
    ...
    if (filter.hasConds()) helper.setParameter(pstmt, 3, paramValues);
    

SQLQueryHelper.appendWherePart() 메소드로 쿼리를 생성하고, SQLQueryHelper.setParameter() 메소드로 파라미터 값을 지정해주었다면 이제 남은 작업은 PreparedStatement로 쿼리를 실행해서 결과를 사용하는 것 뿐이다.

Hibernate의 Query(HQL)에 검색 조건 적용하기

Hibernate의 HQL은 SQL과 상당히 비슷한 구조를 갖고 있으며 SearchFilter를 사용해서 HQL의 검색 조건 부분을 생성할 수도 있다. 생성하는 방법은 PreparedStatement를 생성하는 과정과 거의 동일하며, 차이점이 있다면 tle.searchutil.sql.SQLQueryHelper 대신에 다음의 두 클래스 중 하나를 사용한다는 점이다.

  • tle.searchutil.hibernate2.Hibernate2QueryHelper - Hibernate 2.1.x 버전 용
  • tle.searchutil.hibernate3.Hibernate3QueryHelper - Hibernate 3.0.x 버전 용
코드 형태는 PreparedStatement를 사용하는 경우와 거의 동일하며, 아래와 같이 사용된다.

    SearchFilter filter = new SearchFilter();
    // 테이블의 컬럼 이름 대신 매핑되는 객체의 프로퍼티 이름을 사용한다.
    filter.addCond( Restriction.eq("valid", YesNo.TRUE) );
    filter.addCond( Restriction.eq("registerDate", YesNo.TRUE) );
    ...
    
    Hibernate3QueryHelper helper = new Hibernate3QueryHelper();
    List paramValues = null;
    
    StringBuffer query = new StringBuffer();
    query.append("from Member m ");
    if ( filter.hasConds() ) {
        paramValues = helper.appendWherePart(filter, "m", query);
    }
    Query query = session.createQuery(Member.class);
    if ( filter.hasConds() ) helper.setParameter(query, 0, paramValues);

Hibernate3QueryHelper 클래스를 사용할 때에도 appendWherePart() 메소드와 setParameter() 메소드를 사용한다. 단, Hibernate3QueryHelper 클래스나 Hibernate2QueryHelper 클래스의 setParameter() 메소드는 인덱스 값이 0부터 시작된다는 점에 주의해야 한다. (이는 Hibernate Query의 파라미터 인덱스가 0부터 시작하는 것과 동일하다.)

Hibernate의 Criteria에 검색 조건 적용하기

Hibernate3QueryHelper와 Hibernate2QueryHelper 클래스는 Hibernate의 Criteria를 위한 기능도 제공하고 있으며, 관련 메소드는 아래와 같다.

  • appendCriterion(Criteria crit, SearchFilter filter)
SQL/HQL에서 검색 조건을 처리하려면 몇 가지 귀찮은 코드가 필요했는데(예를 들면, 파라미터 값을 처리하기 위한 코드 등) Criteria의 경우는 다음과 같이 appendCriterion() 메소드를 호출하는 것으로 끝난다.

    SearchFilter filter = new SearchFilter();
    filter.addCond(...);
    filter.addCond(...);
    
    Hibernate3QueryHelper helper = new Hibernate3QueryHelper();
    
    Criteria crit = session.createCriteria(Member.class);
    
    if ( filter.hasConds() ) helper.appendCriterion(crit, filter);
    
    List list = crit.list(); // 검색 조건에 해당하는 객체 목록 리턴
    

정렬 순서 적용하기

앞서 SearchFilter.addOrder() 메소드를 사용해서 정렬 순서를 지정하는 방법을 살펴봤는데, 마지막으로 SearchFilter에 저장된 정렬 정보를 사용해서 SQL, Hibernate HQL, 그리고 Hibernate Criteria에서 정렬을 지정하는 방법을 살펴보겠다.

SQL의 order by 부분 생성하기

SQL의 order by 부분을 생성할 때에는 tle.searchutil.sql.SQLQueryHelper 클래스의 appendOrderPart() 메소드를 사용하면 된다. 이 메소드는 다음과 같이 사용된다.

    SearchFilter filter = new SearchFilter();
    filter.addOrder( Restriction.order("NAME", true) );
    filter.addOrder( Restriction.order("AGE", false) );
    
    StringBuffer query = new StringBuffer();
    query.append("select * from MEMBER m");
    ...
    
    SQLQueryHelper helper = new SQLQueryHelper();
    if ( filter.hasOrders() ) { // 정렬 순서를 지정했는지 여부
        query.append(" order by ");
        helper.appendOrderPart(filter, "m", query);
    }
    

위 코드에서 helper.appendOrderPart() 코드는 다음과 같은 쿼리를 생성하게 된다.

    m.NAME asc, m.AGE desc

Hibernate HQL의 order by 부분 생성하기

HQL의 order by 부분을 생성하는 코드는 SQL을 사용할 때와 완전히 동일하다. 차이점이라면 SQLQueryHelper 클래스 대신에 Hibernate2QueryHelper 또는 Hibernate3QueryHelper 클래스를 사용한다는 점 뿐이다. 예를 들면 아래와 같다.

    SearchFilter filter = new SearchFilter();
    filter.addOrder( Restriction.order("NAME", true) );
    filter.addOrder( Restriction.order("AGE", false) );
    
    StringBuffer query = new StringBuffer();
    query.append("select * from MEMBER m");
    ...
    
    Hibernate3QueryHelper helper = new Hibernate3QueryHelper();
    if ( filter.hasOrders() ) { // 정렬 순서를 지정했는지 여부
        query.append(" order by ");
        helper.appendOrderPart(filter, "m", query);
    }
    ...

Hibernate Criteria에 정렬 순서 적용하기

Hibernate Criteria를 사용하는 경우에는 아래와 같이 Hibernate2QueryHelper 또는 Hibernate3QueryHelper 클래스의 appendOrder() 메소드를 호출하면 정렬 정보가 Criteria에 복사된다.

    SearchFilter filter = new SearchFilter();
    filter.addOrder( Restriction.order("NAME", true) );
    filter.addOrder( Restriction.order("AGE", false) );
    
    Hibernate3QueryHelper helper = new Hibernate3QueryHelper();
    
    Criteria crit = session.createCriteria(Member.class);
    if (filter != null && filter.hasOrders()) helper.appendOrder(crit, filter);
    

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요

틀이 무엇이며 틀을 기반으로 한 MVC 웹 어플리케이션을 개발할 수 있도록 간단한 예제를 살펴본다.

틀 프레임워크란?

틀 프레임워크(TLE Framework)는 스트러츠(Struts)나 코쿤(Cocoon)과 같이 MVC(Model-View-Controller) 패턴을 구현한 프레임워크로서, MVC 패턴에 따라 웹 어플리케이션을 구현할 수 있도록 지원해준다. 다른 프레임워크와 달리 틀 프레임워크는 웹 어플리케이션을 구축하는 데 필요한 요소들을 컴포넌트로 분리시켰다. 웹 어플리케이션을 구축하려면 클라이언트의 요청이 무엇인지 파악하고, 요청에 알맞은 로직을 수행하고, 로직 수행 결과를 보여주는 세 가지 요소가 필요한데, 이 세가지 요소를 별도의 컴포넌트로 분리시켜놓았다.

틀 프레임워크의 구성

아래 그림은 틀의 구성 요소들을 보여주고 있는데, 이 그림에서 앞서 말한 요청 분석, 요청 처리, 결과 생성의 세가지 요소를 볼 수 있으며, 또한 웹 어플리케이션을 사용하는 사용자의 인증과 권한 처리를 위한 요소가 있는 것도 확인할 수 있다.


위 그림에서 각 구성 요소의 기능은 아래와 같다.

틀 컴포넌트의 구성요소
구성요소 설명
RequestReceiver 서블릿으로서 클라이언트의 요청을 전달받아 틀 프레임워크의 컨트롤러에 전달한다.
틀 컨트롤러
TLEController
RequestReceiver로부터 전달받은 클라이언트 요청을 각 컴포넌트를 사용하여 알맞게 처리한다. 요청분석->요청처리->결과출력의 일련의 과정을 각 컴포넌트를 사용하여 알맞게 제어하는 역할을 수행한다.
요청분석기
RequestAnalyzer
클라이언트 요청을 분석하여 그 정보를 RequestInfo 인스턴스에 담는다. RequestInfo에는 파라미터 값, 쿠키값, 업로드한 파일, 헤더 등 요청과 관련된 정보가 저장된다. 또한, 요청 처리기는 클라이언트의 요청으로부터 알맞은 명령어ID를 생성해서 RequestInfo에 담는다.
요청처리기
RequestProcessor
요청분석기를 통해 생성된 RequestInfo를 사용해서 클라이언트가 요청한 기능을 수행한다. RequestInfo에 저장된 명령어ID를 사용해서 어떤 기능을 요청했는 지 파악한다. 요청 처리기는 로직을 수행한 결과를 Result에 저장한다.
UI생성기
UICreator
요청처리기가 생성한 Result를 사용하여 알맞은 결과를 출력한다.
권한/인증 관리자
AAManager
클라이언트가 요청한 기능을 사용할 수 있는지의 여부를 확인한다. 요청분석기가 생성한 RequestInfo 객체를 사용해서 클라이언트가 요청한 기능을 파악하며, 틀 컨트롤러에 기능을 사용할 수 있는지의 여부를 알려주게 된다.
컴포넌트 관리자
ComponentManager
네 개의 주요 컴포넌트를 비롯한 틀 프레임워크와 관련된 컴포넌트를 관리한다.

위 표에서 알 수 있듯이, '요청분석기', '요청처리기', 'UI생성기', 그리고 '권한/인증관리자'는 틀 프레임워크의 핵심 컴포넌트인데, 틀은 이 네가지 컴포넌트를 위한 인터페이스를 제공하고 있다. 또한, 기본적으로 이 네가지 컴포넌트의 인터페이스를 구현한 기본 구현체를 제공하고 있다. 아래 그림은 인터페이스와 기본 구현체 사이의 관계를 보여주고 있다.


Component 인터페이스는 틀 프레임워크가 관리하게 될 컴포넌트의 인터페이스이며, ComponentBase 추상 클래스는 이 Component 인터페이스를 구현한 클래스로서 컴포넌트가 구현해야 할 기본 기능을 제공한다.

요청분석기의 기본 구현체는 DefaultRequestAnalyzer 이고, 요청처리기의 기본 구현체는 DefaultRequestProcessor 이고, UI생성기의 기본 구현체는 DefaultUICreator이다. 권한/인증 관리자의 경우는 기본 구현체가 두개가 존재한다. 이 두 컴포넌트 구현체는 아래와 같다.

  • NullAAManager - 권한/인증 검사를 하지 않는다. 개발자가 직접 권한/인증 부분 로직을 작성해야 한다.
  • DefaultAAManager - 권한/인증 검사를 위한 인터페이스를 제공한다. ServiceManager, RoleManager, SessionManager 등의 인터페이스를 사용해서 권한/인증 기능을 수행하며, 개발자는 이들 인터페이스를 구현한 클래스를 제공해야 한다.
대부분의 프로젝트의 경우 DefaultRequestAnalyzer, DefaultRequestProcessor, DefaultUICreator를 그대로 사용할 수 있으며, 권한/인증 부분만 프로젝트에 알맞게 구현해주면 된다.

틀 프레임워크의 요청 처리 순서

틀 프레임워크는 네 개의 컴포넌트(요청분석기, 요청처리기, UI생성기, 권한/인증관리자)를 사용해서 클라이언트의 요청을 처리한다. 요청이 들어왔을 때 틀 프레임워크는 아래의 그림처럼 네 개의 컴포넌트를 순서대로 사용한다. 이 실행 순서를 머리속에 넣고 있으면 보다 쉽게 틀 프레임워크를 사용할 수 있을 것이다.


위 그림을 보면 틀 프레임워크가 Model-View-Controller 패턴에 기반하고 있음을 알 수 있다. 즉, 틀 컨트롤러는 클라이언트의 요청과 모델 사이의 가교 역할을 하는 Controller 역할을 하고, 요청 분석기-권한/인증 관리자-요청처리기는 클라이언트의 요청에 따라 알맞은 로직을 수행하는 Model 역할을 하며, UI생성기는 Model로부터 데이터(Result)를 받아 클라이언트에 보여줄 화면을 생성하는 View 역할을 한다.

명령어 ID

틀 프레임워크를 사용할 때 반드시 이해해야 할 요소가 '명령어 ID(command ID)'이다. 명령어 ID는 클라이언트가 어떤 기능을 요청했는 지 분간할 때 사용되는 식별자로서 각 컴포넌트는 이 명령어 ID를 사용해서 알맞은 기능을 수행하게 된다.

앞서 틀 프레임워크의 요청 처리 순서에서 살펴봤듯이 클라이언트의 요청을 최초로 전달받는 컴포넌트는 요청 분석기이다. 요청 분석기는 클라이언트의 요청을 전달받으면, 클라이언트의 요청을 분석해서 알맞은 명령어 ID를 생성해낸다. 요청 분석기가 생성한 명령어 ID는 이후에 요청 처리기, UI 생성기, 권한/인증 관리자 컴포넌트에서 클라이언트 요청을 식별하는 데 사용된다.

예를 들어, 요청 처리기는 명령어 ID를 사용해서 클라이언트가 요청한 기능을 수행하고, 권한/인증 관리자는 명령어 ID를 통해서 현재 사용자가 요청한 기능을 실행할 권한이 있는 지 검사하게 된다. 또한 UI 생성기는 명령어 ID를 사용해서 클라이언트가 요청한 기능에 알맞은 뷰를 생성하게 된다.

틀 프레임워크를 이용한 MVC 프로그래밍 맛보기

앞서 봤듯이 틀 프레임워크는 MVC 패턴에 기반하여 웹 어플리케이션을 개발할 수 있도록 도와주는데, 본 장에서는 간단하게 틀을 이용해서 MVC 프로그래밍을 하는 방법을 살펴볼 것이다. 더불어, 틀 프레임워크가 제공하는 기본 컴포넌트인 DefaultRequestAnalyzer, DefaultRequestProcessor, DefaultUICreator와 틀 프레임워크 자체의 설정 방법 및 사용방법을 살펴보도록 하겠다.

틀 프레임워크 다운로드 및 설치

틀 프레임워크의 현 버전은 2.0.5.6로서 틀 프레임워크의 프로젝트 사이트인 http://kldp.net/projects/tle에서 다운로드 받을 수 있다. 이 사이트에서 TLEFramework-2.0.5.6.zip 파일을 다운로드 받은 후 압축을 풀면 아래와 같은 폴더가 생성된다.


각 폴더에는 다음과 같은 파일들이 존재한다.

  • / : 틀 프레임워크 jar 파일(TLEFramework-2.0.5.6.jar), readme.txt, chage.txt, src.zip
  • /config : 틀 프레임워크 및 기본 제공 컴포넌트의 설정 파일 예제
  • /doc/api : 틀 프레임워크 API 문서
  • /lib : 틀 프레임워크를 실행하는 데 필요한 jar 파일 포함
  • /license : 틀 프레임워크 및 관련 jar 파일의 라이센스 문서
  • /example : 예제 war 파일 (tle.war)
틀 프레임워크의 실행 예제는 /example 폴더에 tle.war 파일로 존재하는데, readme.txt 파일에 tle.war 파일을 이용하여 예제를 실행하는 방법이 나와 있으니 참고하기 바란다.

틀 프레임워크를 사용하여 웹 어플리케이션을 개발하기 위해서는 먼저 틀 프레임워크를 설치해야 한다. 틀 프레임워크의 설치 순서는 다음과 같다.

  1. 웹 어플리케이션 폴더를 생성한다.
  2. WEB-INF/lib 폴더에 다음의 jar 파일들을 복사한다.:
    - commons-digester.jar commons-logging.jar commons-collections-3.1.jar
    - commons-beanutils.jar commons-fileupload-1.0.jar
    - jakarta-regexp-1.3.jar
    - mx4j.jar mx4j-remote.jar mx4j-tools.jar
    - TLEFramework-2.0.5.6.jar
  3. TLEFramework 설정 파일을 작성한다.
  4. 각 컴포넌트의 설정 파일을 알맞게 작성한다.
  5. web.xml 파일에 틀 프레임워크 관련 설정을 추가한다.
위에서 TLEFramework-2.0.5.6.jar 파일을 제외한 나머지 jar 파일은 배포파일의 /lib 폴더에 포함되어 있다.

프레임워크 기본 설정하기

틀 프레임워크를 실행하는데 필요한 jar 파일들을 웹 어플리케이션의 WEB-INF/lib 폴더에 복사했다면, 이제 남은 일은 틀 프레임워크를 실행하기 위해 필요한 정보들을 설정하는 것이다. 틀 프레임워크의 설정은 크게 틀 프레임워크 자체에 대한 설정과 각각의 컴포넌트에 대한 설정 이렇게 두 가지 부분으로 나뉜다.

먼저 틀 프레임워크 자체에 대한 설정 부분부터 살펴보도록 하겠다.

web.xml 파일에 틀 프레임워크 관련 설정하기

틀 프레임워크를 사용하려면 먼저 어떤 파일을 틀 프레임워크의 설정 파일로 사용할지의 여부를 web.xml 파일을 통해서 지정해주어야 한다. 아래는 web.xml 파일에 틀과 관련된 설정 내용을 추가한 예를 보여주고 있다.

    코드: /WEB-INF/web.xml    
    <?xml version="1.0" encoding="euc-kr" ?>
    
    <web-app xmlns="http://java.sun.com/xml/ns/j2ee"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xsi:schemaLocation="http://java.sun.com/xml/ns/j2ee
                            http://java.sun.com/xml/ns/j2ee/web-app_2_4.xsd"
        version="2.4">
        
        <context-param>
            <param-name>tle.configFile</param-name>
            <param-value>{context.path}WEB-INFconfigTLEFrameworkConfig.xml</param-value>
        </context-param>        
        <listener>
            <listener-class>tle.framework.TLEWebapplicationListener</listener-class>        </listener>
        
        <servlet>
            <servlet-name>TLERequestReceiver</servlet-name>
            <servlet-class>tle.framework.RequestReceiver</servlet-class>
        </servlet>
        
        <servlet-mapping>
            <servlet-name>TLERequestReceiver</servlet-name>
            <url-pattern>*.do</url-pattern>
        </servlet-mapping>
        
    </web-app>

TLEWebapplicationListener 클래스는 웹 어플리케이션이 시작될 때 틀 프레임워크의 초기화작업을 진행하는 리스너로서 콘텍스트 파라미터인 tle.configFile의 값을 틀 설정 파일로 사용한다. 위 예제에서 tle.configFile 콘텍스트 파라미터의 값은 다음과 같다.

    {context.path}WEB-INFconfigTLEFrameworkConfig.xml

여기서 {context.path}는 웹 어플리케이션이 위치한 폴더의 경로로 대체된다. 예를 들어, 웹 어플리케이션 폴더가 c: omcatwebappsROOT 인 경우 위의 값은 실제로 c: omcatwebappsROOTWEB-INFconfigTLEFrameworkConfig.xml이 된다.

tle.framework.RequestReceiver는 클라이언트의 요청을 받아서 틀 컨트롤러에 전달하는 역할을 하는데(이미 앞에서 RequestReceiver에 대해 간단하게 설명한 바 있다), 위 코드에서는 *.do로 들어오는 요청을 받아서 틀 컨트롤러에 전달하도록 설정하였다.

틀 프레임워크 설정 파일 작성하기

web.xml 파일을 설정한 뒤에는 틀 프레임워크의 설정 파일을 작성해주면 된다. 틀 프레임워크의 설정 파일에는 다음의 두 가지 정보가 저장된다.

  • 컴포넌트 목록 및 기본 설정
  • RequestAnalyzer, RequestProcessor, UICreator, AAManager로 사용할 컴포넌트 지정
틀 프레임워크는 네 개의 핵심 컴포넌트인 RequestAnalyzer, RequestProcessor, UICreator, AAManager 뿐만 아니라 다른 기능을 제공하는 컴포넌트도 프레임워크에 장착할 수가 있다. 또한 틀 프레임워크는 JMX를 사용해서 컴포넌트를 관리하고 있기 때문에, JMX API를 이용하여 컴포넌트를 관리할 수가 있다. (JMX를 이용한 틀 컴포넌트의 관리에 대해서는 본 시리즈를 진행하는 동안에 배우게 될 것이다.)

설정 파일의 예를 들면 아래와 같다.

   코드: WEB-INFconfigTLEFrameworkConfig.xml   
   <?xml version="1.0" encoding="euc-kr" ?>
   
   <tle>
     <component-list>
       
       <!-- DefaultRequestAnalyzer에 대한 설정 -->
       <component>
         <component-class>tle.framework.component.impl.ra.DefaultRequestAnalyzer</component-class>
         <component-id>DefaultRequestAnalyzer</component-id>
         <init-param>
           <param-name>configFile</param-name>
           <param-value>{context.path}WEB-INFconfigDefaultRequestAnalyzer.properties</param-value>
         </init-param>
       </component>       
       <!-- DefaultRequestProcessor에 대한 설정 -->
       <component>
         <component-class>tle.framework.component.impl.rp.DefaultRequestProcessor</component-class>
         <component-id>DefaultRequestProcessor</component-id>
         <init-param>
           <param-name>configFile</param-name>
           <param-value>{context.path}WEB-INFconfigDefaultRequestProcessor.xml</param-value>
         </init-param>
       </component>
   
       <!-- DefaultAAManager에 대한 설정 -->
       <component>
         <component-class>tle.framework.component.impl.aamanager.NullAAManager</component-class>
         <component-id>NullAAManager</component-id>
       </component>
       
       <!-- DefaultUICreator에 대한 설정 -->
       <component>
         <component-class>tle.framework.component.impl.uicreator.DefaultUICreator</component-class>
         <component-id>DefaultUICreator</component-id>
         <init-param>
           <param-name>configFile</param-name>
           <param-value>{context.path}WEB-INFconfigDefaultUICreator.xml</param-value>
         </init-param>
       </component>
       
     </component-list>
     
     <framework>
       <core-component>
         <request-analyzer-id>DefaultRequestAnalyzer</request-analyzer-id>
         <request-processor-id>DefaultRequestProcessor</request-processor-id>
         <ui-creator-id>DefaultUICreator</ui-creator-id>
         <aa-manager-id>NullAAManager</aa-manager-id>
       </core-component>
     </framework>
     
   </tle>

먼저, 틀 프레임워크에 장착될 컴포넌트는 <component-list> 태그에서 명시한다. component-list 태그는 component 태그를 갖는데, component 태그의 각 태그는 다음과 같은 의미를 갖는다.

틀 프레임워크 설정 파일의 컴포넌트 설정 관련 태그
태그 설명
component-class 컴포넌트 구현 클래스의 완전한 이름
component-id 컴포넌트를 식별할 때 사용되는 아이디. 유일한 값이어야 한다.
init-param/param-name 컴포넌트를 초기화할 때 전달되는 파라미터의 이름
init-param/param-value 컴포넌트를 초기화할 때 전달되는 파라미터의 값

각 컴포넌트는 필요한 설정 정보를 초기화 파라미터를 사용해서 입력받을 수 있다.

컴포넌트의 설정을 완료했으면, 그 다음으로 설정할 내용은 네 개의 핵심 컴포넌트를 지정하는 것이다. 위 설정 파일에서 핵심 컴포넌트를 지정하는 부분은 다음과 같다.

     <framework>
       <core-component>
         <request-analyzer-id>DefaultRequestAnalyzer</request-analyzer-id>
         <request-processor-id>DefaultRequestProcessor</request-processor-id>
         <ui-creator-id>DefaultUICreator</ui-creator-id>
         <aa-manager-id>NullAAManager</aa-manager-id>
       </core-component>
     </framework>

위 코드에서 각 태그는 다음과 같은 의미를 갖는다.

핵심 컴포넌트 지정 관련 태그
태그 설명
request-analyzer-id RequestAnalyzer로 사용할 컴포넌트의 아이디를 지정한다.
request-processor-id RequestProcessor로 사용할 컴포넌트의 아이디를 지정한다.
ui-creator-id UICreator로 사용할 컴포넌트의 아이디를 지정한다.
aa-manager-id AAManager로 사용할 컴포넌트의 아이디를 지정한다.

네 개의 태그에서 사용할 아이디값은 앞서 컴포넌트를 정의할 때 component-id 태그로 지정한 값을 지정해주면 된다.

기본 컴포넌트의 주요 기능 및 설정

DefaultRequestAnalyzer의 주요기능

DefaultRequestAnalyzer 컴포넌트는 요청 분석기로서 클라이언트의 요청을 분석하게 된다. DefaultRequestAnalyzer 컴포넌트가 제공하는 주요 기능은 다음과 같다.

  • 클라이언트의 요청 URI 중에서 콘텍스트 경로를 제외한 나머지 부분을 명령어 ID로 추출한다.
  • Jakarta Commons FileUpload API를 사용해서 파일 업로드를 처리한다.
  • 업로드하려는 파일의 크기를 제한할 수 있다.
DefaultRequestAnalyzer 컴포넌트는 클라이언트의 요청 URI로부터 명령어 ID를 추출하기 때문에 웹 브라우저에 입력한 경로가 곧 명령어 ID가 된다. 예를 들어, 콘텍스트 경로가 /tle 이고 웹 브라우저에 다음과 같이 경로를 입력했다고 해 보자.

    http://host:port/tle/admin/front.view

이때, 요청 URI인 /tle/admin/front.view 중에서 콘텍스트 경로인 /tle 을 제외한 나머지 부분인 /admin/front.view 가 명령어 ID로 사용된다.

파일 업로드시에는 인코딩을 multipart/form-data 타입으로 하게 되는데, DefaultRequestAnalyzer 컴포넌트는 이 인코딩 타입으로 전송된 데이터를 처리하기 위해서 Jakarta Commons FileUpload API를 사용한다. 틀 프레임워크의 배포판에는 Jakarta Commons FileUpload API와 관련된 jar 파일인 commons-fileupload-1.0.jar 파일이 포함되어 있으므로, commons-fileupload-1.0.jar 파일을 웹 어플리케이션의 WEB-INF/lib 폴더에 복사해야 DefaultRequestAnalyzer 컴포넌트가 올바르게 동작한다.

DefaultRequestAnalyzer 설정하기

앞서 틀 프레임워크 설정 파일에서 DefaultRequestAnalyzer 컴포넌트를 설정한 부분을 살펴보도록 하자.

       <!-- DefaultRequestAnalyzer에 대한 설정 -->
       <component>
         <component-class>tle.framework.component.impl.ra.DefaultRequestAnalyzer</component-class>
         <component-id>DefaultRequestAnalyzer</component-id>
         <init-param>
           <param-name>configFile</param-name>
           <param-value>{context.path}WEB-INFconfigDefaultRequestAnalyzer.properties</param-value>
         </init-param>
       </component>

위 코드에서 볼 수 있듯이, DefaultRequestAnalyzer 컴포넌트는 configFile 초기화 파라미터를 사용해서 사용할 설정 파일의 경로를 입력받는다. 파라미터 값에 있는 {context.path}는 틀 프레임워크 설정 파일에서와 마찬가지로 콘텍스트의 경로를 의미한다.

DefaultRequestAnalyzer가 사용하는 설정 파일은 자바의 프로퍼티 파일로서 아래와 같이 작성된다.

    코드: /WEB-INF/config/DefaultRequestAnalyzer.properties    
    tempDir=C:\WINDOWS\Temp
    max=10M
    encoding=EUC-KR

위 프로퍼티 파일에서 각 프로퍼티는 아래와 같다.

DefaultRequestAnalyzer 설정 파일의 프로퍼티
프로퍼티 설명
tempDir 업로드한 파일이 임시로 저장될 디렉토리. 임시로 파일이 저장되는 공간이며 생성된 임시 파일은 자동으로 삭제된다.
max 최대 업로드 사이즈: 기본적으로 바이트 단위이며, 뒤에 k, m, g를 붙이면 각각 KB, MB, GB를 기본 크기로 사용한다
encoding 요청 파라미터를 읽어올 때 사용할 캐럭터 인코딩

DefaultRequestProcessor 주요 기능

DefaultRequestProcessor 컴포넌트는 클라이언트가 요청을 처리하는 요청 처리기이다. DefaultRequestProcessor는 요청 분석기가 생성한 명령어 ID를 사용해서 알맞은 기능을 수행한다. DefaultRequestProcessor 컴포넌트는 아래 그림과 같이 명령어 ID에 알맞은 핸들러(handler)를 사용해서 클라이언트의 요청을 처리한다.


DefaultRequestProcessor 컴포넌트는 명령어 ID를 전달받으면, 명령어 ID에 해당하는 핸들러를 실행한다. 핸들러는 클라이언트가 요청한 기능을 수행한 뒤 결과를 담아서 요청 처리기에 전달하며, 요청 처리기는 다시 그 결과를 틀 컨트롤러에 전달하게 된다.

(일반적으로 하나의 핸들러가 하나의 명령어 ID를 처리하게 되는데(물론, 하나의 핸들러가 다수의 명령어 ID를 처리하기도 한다), 여기서 DefaultRequestProcessor 컴포넌트가 커맨드 패턴을 사용하고 있다는 것을 알 수 있다. 커맨드 패턴에 대한 글은 자바캔의 '커맨드(Command) 패턴과 그 구현' 글을 참고하기 바란다.)

DefaultRequestProcessor 설정하기

DefaultRequestProcessor 컴포넌트를 사용하기 위해서는 먼저 틀 프레임워크 설정 파일에 다음과 같이 DefaultRequestProcessor 컴포넌트를 사용한다고 명시해주어야 한다.

       <!-- DefaultRequestProcessor에 대한 설정 -->
       <component>
         <component-class>tle.framework.component.impl.rp.DefaultRequestProcessor</component-class>
         <component-id>DefaultRequestProcessor</component-id>
         <init-param>
           <param-name>configFile</param-name>
           <param-value>{context.path}WEB-INFconfigDefaultRequestProcessor.xml</param-value>
         </init-param>
       </component>

DefaultRequestAnalyzer 컴포넌트와 마찬가지로 DefaultRequestProcessor 컴포넌트도 configFile 초기화 파라미터를 사용해서 설정 파일의 경로를 전달받는다. DefaultRequestProcessor 컴포넌트의 설정 파일은 다음의 두 가지 정보를 담는다.

  • 핸들러 목록
  • 명령어 ID와 핸들러 매핑
본 글에서 사용할 DefaultRequestAnalyzer 컴포넌트의 설정 파일은 아래와 같다.

    코드: /WEB-INF/config/DefaultRequestProcessor.xml    
    <?xml version="1.0" encoding="euc-kr" ?>
    
    <tle>
        <default-request-processor usingIndependentClassPath="false">
            <!-- 핸들러 목록을 지정-->
            <handler-list>
                <handler id="greeting" class="test.tle.command.GreetingCommandHandler" />
                <handler id="greeting2" jsp="/handler/hello_handler.jsp" />
            </handler-list>
            
            <!-- 명령어 ID와 핸들러 매핑 -->
            <command-handler-mapping defaultHandler="NOHANDLER" useDefaultOnInvalidMapping="false">
                <mapping commandID="/greeting.do" handlerID="greeting" />
                <mapping commandID="/hello/greeting.do" handlerID="greeting2" />
            </command-handler-mapping>
            
        </default-request-processor>
    </tle>

클라이언트의 요청을 실제로 처리하게 되는 핸들러의 목록을 지정하는 태그는 handler-list/handler로서 handler 태그는 다음과 같은 속성을 갖고 있습니다.

DefaultRequestProcessor 컴포넌트 설정 파일: handler 태그의 속성
속성 설명
id 핸들러를 식별하기 위한 아이디. 각각의 핸들러는 고유의 식별자를 가져야 한다.
class 클래스로 구현된 핸들러의 완전한 클래스 이름 둘 중에 한가지를 지정해주어야 한다.
jsp JSP로 구현된 핸들러의 콘텍스트 내에서의 경로

핸들러는 크게 클래스와 JSP의 두가지 방식으로 구현할 수 있는데 각각의 구현방법에 대해서는 뒤에서 설명하도록 하겠다.

핸들러 목록을 지정했다면 다음으로 할 일은 클라이언트의 요청을 나타내는 명령어 ID와 핸들러를 매핑시키는 작업이다. 이는 command-handler-mapping/mapping 태그를 통해서 지정할 수 있으며, mapping 태그의 각 속성은 아래와 같은 의미를 갖는다.

DefaultRequestProcessor 컴포넌트 설정 파일: mapping 태그의 속성
속성 설명
commandID 명령어 ID
handlerID 지정한 명령어 ID에 해당하는 요청을 처리할 핸들러의 아이디

예를 들어, 아래의 코드는 /hello/greeting.do 라는 요청이 들어오면 greeting2 라는 핸들러를 실행한다는 의미를 갖는다.

    <mapping commandID="/hello/greeting.do" handlerID="greeting2" />

만약 아이디가 greeting2인 핸들러가 /handler/hello_handler.jsp 라면, /hello/greeting.do 요청이 들어오면 /handler/hello_handler.jsp가 실행된다.

DefaultRequestProcessor 핸들러 구현하기

DefaultRequestProcessor 컴포넌트의 핸들러는 클라이언트가 요청한 로직을 수행한 뒤 그 결과를 UI 생성기에 전달해주는 기능을 담당한다. 즉, MVC 패턴에 컨트롤러의 역할을 담당하는 것이 바로 핸들러인 것이다. 핸들러는 모델을 사용해서 클라이언트가 요구한 로직을 수행하게 되며, 그 결과를 뷰에 전달하게 된다.

DefaultRequestProcessor 컴포넌트는 두 가지 방식으로 핸들러를 구현할 수 있도록 해 준다. 한가지는 스트러츠의 Action과 같이 자바 클래스로 핸들러를 구현하는 방식이며, 다른 한가지는 JSP로 핸들러를 구현하는 방식이다. 개발자에 따라 원하는 방식으로 핸들러를 구현하면 되는데, 필자의 경우는 JSP 방식을 선호한다.

두 가지 방식으로 핸들러를 구현하는 방법에 대해서 살펴보고, 어떤 방식이 본인이 사용하기에 편리할지는 직접 판단해보기 바란다.

DefaultRequestProcessor의 클래스 핸들러 작성하기

먼저, 클래스를 사용해서 핸들러를 작성해보도록 하겠다. 핸들러 클래스를 작성하기 위해서는 아래의 규칙을 따라야 한다.

  1. tle.framework.component.impl.rp.handler.CommandHandler 인터페이스를 implements 한다.
  2. public DefaultResult process(RequestInfo requestInfo, DefaultResult result) 메소드를 구현한다.
  3. process() 메소드에서
    1. requestInfo 객체로부터 파라미터, 쿠키 등 로직을 처리하는 데 필요한 값을 읽어온다.
    2. 알맞은 로직을 수행한다.
    3. result.setData() 메소드를 사용해서 뷰에서 사용할 값을 저장한다.
    4. result.setCode() 메소드를 사용해서 결과 코드를 입력한다.
실제로 간단한 핸들러 클래스를 살펴보자. 앞서 살펴본 DefaultRequestProcessor 컴포넌트의 설정 파일에 보면 핸들러 목록에서 test.tle.command.GreetingCommandHandler 클래스를 지정한 부분이 있는데, 바로 이 클래스의 코드는 아래와 같다.

    
    코드: /WEB-INF/src/test/tle/command/GreetingCommandHandler.java    
    package test.tle.command;
    
    import tle.framework.component.impl.rp.DefaultResult;
    import tle.framework.component.impl.rp.handler.CommandHandler;
    import tle.framework.component.spi.ra.RequestInfo;
    
    /**
     * Result의 "greeting" 데이터에 "안녕하세요"라는 데이터를 추가한다.
     * 
     * @author 최범균
     */
    public class GreetingCommandHandler implements CommandHandler {
    
        public DefaultResult process(RequestInfo requestInfo, DefaultResult result) {
            result.setData("greeting", "안녕하세요."); // 데이터 저장
            result.setCode("success"); // 요청 처리 결과 코드 입력
            return result;
        }
    }

위 코드에서 GreetingCommandHandler는 클라이언트의 요청을 전달받으면, "greeting" 데이터의 값을 "안녕하세요."로 지정한 뒤 결과 코드값을 "success"로 지정한다. 이 코드값은 클라이언트의 요청을 처리한 결과를 의미하며, 알맞은 값을 입력할 수 있다. 예를 들어, 다음과 같이 상황에 따라 서로 다른 코드값을 지정할 수 있다.

    public DefaultResult process(RequestInfo requestInfo, DefaultResult result) {
        // 로그인 기능을 수행
        boolean isSuccess = sessionManager.openSession(
            requestInfo.getParameter("id"), 
            requestInfo.getParameter("password"));
        
        if (isSuccess) {
            ...
            result.setCode("loginSuccess");
        } else {
            ...
            result.setCode("loginFail");
        }
        return result;
    }

핸들러는 상황에 알맞은 코드값을 지정하며, Result를 전달받게 될 UI 생성기 컴포넌트는 이 결과 코드값을 사용하여 알맞은 화면을 생성하게 된다.

DefaultRequestProcessor의 JSP 핸들러 작성하기

앞서 클래스를 사용해서 핸들러를 작성하는 방법을 살펴봤는데, 두번째로 JSP를 이용한 핸들러 클래스의 작성 방법에 대해서 살펴보도록 하자.

JSP 핸들러는 아래와 같은 코드를 사용해서 작성한다.

    코드: handlerhello_handler.jsp    
    <%@ page pageEncoding = "euc-kr" %>
    <%@ page import = "tle.framework.component.impl.rp.DefaultResult" %>
    <%@ page import = "tle.framework.component.spi.ra.RequestInfo" %>
    <%
        // 핸들러 클래스의 process() 메소드가 두 개의 파라미터로 전달받는 객체를
        // request의 attribute로부터 가져온다.
        DefaultResult result = (DefaultResult)request.getAttribute("result");
        RequestInfo ri = (RequestInfo)request.getAttribute("requestInfo");        
        // 알맞은 로직을 수행한 뒤 결과 값을 저장한다.
        result.setData("greeting", "헬로우");
        
        // 처리 결과 코드를 기록한다.
        result.setCode("success");
        
        // TLEFramework 2.0.5.5 및 이하 버전에서는 JSP 엔진에 따라
        // out.clearBuffer()를 실행하지 않으면 캐릭터셋 처리가
        // 올바르게 처리되지 않을 수도 있다.
        //out.clearBuffer(); 
    %>

JSP를 사용하여 작성한 핸들러의 코드를 보면 클래스를 사용하여 작성한 핸들러 코드와 크게 다르지 않다는 것을 알 수 있다. JSP 핸들러와 클래스 핸들러 사이의 차이점이라면 다음의 두가지 정도 뿐이다.

  • 클래스 핸들러는 RequestInfo와 DefaultResult를 메소드 인자로 받지만, JSP 핸들러는 request 기본 객체의 attribute로부터 가져온다.
  • 클래스 핸들러는 result를 리턴하는 반면에 JSP 핸들러는 리턴하지 result를 않는다.
JSP 핸들러 작성시 주의할 점은 TLEFramework 2.0.5.6 이상의 버전이 아닌 경우 마지막에 아래의 코드를 실행시켜줘야 한다는 것이다.

    <%@ page pageEncoding = "euc-kr" %>
    <%@ page import = "tle.framework.component.impl.rp.DefaultResult" %>
    <%@ page import = "tle.framework.component.spi.ra.RequestInfo" %>
    <%
        DefaultResult result = (DefaultResult)request.getAttribute("result");
        RequestInfo ri = (RequestInfo)request.getAttribute("requestInfo");
        ...
        result.setCode("success");
        
        // TLEFramework 2.0.5.5 및 이하 버전에선 실행시켜주는 것이 좋음
        out.clearBuffer(); 
    %>

TLEFramework 2.0.5.5 버전까지는 JSP 핸들러에 실제 response에 해당하는 객체를 전달해서 JSP 핸들러가 지정한 컨텐츠 타입이나 출력한 내용 때문에 실제 UICreator가 내용을 생성할 때 글자가 깨지는 경우가 발생했다. 그래서 TLEFramework 2.0.5.5 및 그 이하 버전에서는, 이런 문제가 발생하는 콘테이너의 경우 JSP 핸들러의 마지막에 out.clearBuffer() 메소드를 실행시켜서 문제를 해결할 수 있었다.

TLEFramework 2.0.5.6 버전부터는 JSP 핸들러에 실제 response에 해당하는 객체가 아닌 HttpServletResponseWrapper 객체를 새롭게 생성해서 전달한 뒤 JSP 핸들러가 생성한 내용이나 지정한 컨텐츠 타입을 무시하기 때문에, 더 이상 이런 문제가 발생하지 않는다.

DefaultUICreator의 주요기능

앞서 살펴본 내용은 클라이언트가 요청한 기능을 처리하는 방법에 대한 것이었다. 핸들러는 클라이언트의 요청을 처리한 뒤 결과로 보여줄 내용을 Result에 저장한다. 틀 컨트롤러는 요청 처리기 컴포넌트가 생성한 Result를 UI 생성기에 전달해서 알맞은 UI를 생성하도록 한다. 틀 프레임워크가 기본적으로 제공하는 DefaultUICreator 컴포넌트는 JSP를 사용하여 뷰를 생성하며 다음과 같은 기능을 제공한다.

  • 뷰셋(View Set)을 제공한다.
  • 클라이언트의 타입에 따라 뷰셋을 지정할 수 있다.
  • JSP 템플릿 기능을 제공한다.
  • 뷰의 상속 개념을 제공한다.
  • 특정 명령어ID의 결과코드 값에 따라서 각각 알맞은 뷰를 보여줄 수 있다.
뷰셋은 각각의 기기에 대해 알맞은 화면을 보여주기 위해서 도입되었다. 예를 들어, PDA와 일반 PC에서 동일한 URL을 입력했다고 해보자. 이 경우 처리하는 기능은 동일하지만, 결과 화면은 기기에 따라 알맞게 생성해야 할 것이다. 이렇게 동일한 기능을 수행한뒤(즉 동일한 핸들러를 수행한뒤), 기기에 따라 다른 결과 화면을 보여주기 위해서 도입된 것이 뷰셋이다.

JSP 템플릿 기능을 레이아웃 코드를 중복해서 작성하지 않고 템플릿으로 만들어 UI를 조립하듯이 사용할 수 있도록 해준다.

뷰셋, 템플릿, 뷰의 상속 등 DefaultUICreator 컴포넌트가 제공하는 기능에 대해서는 본 시리즈를 진행하면서 살펴보게 될 것이다.

DefaultUICreator 설정하기

DefaultUICreator 컴포넌트를 사용하기 위해서는 먼저 틀 프레임워크 설정 파일에 다음과 같이 DefaultUICreator 컴포넌트를 사용한다고 명시해주어야 한다.

       <!-- DefaultUICreator에 대한 설정 -->
       <component>
         <component-class>tle.framework.component.impl.uicreator.DefaultUICreator</component-class>
         <component-id>DefaultUICreator</component-id>
         <init-param>
           <param-name>configFile</param-name>
           <param-value>{context.path}WEB-INFconfigDefaultUICreator.xml</param-value>
         </init-param>
       </component>

DefaultRequestProcessor 컴포넌트와 마찬가지로 DefaultUICreator 컴포넌트도 configFile 초기화 파라미터를 사용해서 UI와 관련된 설정 정보가 담긴 파일의 경로를 입력받는다.

DefaultUICreator 컴포넌트의 설정 파일은 다음과 같은 정보를 담는다.

  • 뷰셋(ViewSet>
  • 글로벌 뷰
  • 명령어ID와 결과값에 따른 뷰 매핑
  • 클라이언트 타입
본 글에서 사용할 설정 파일은 다음과 같다.

    코드: WEB-INFconfigDefaultUICreator.xml    
    <?xml version="1.0" encoding="euc-kr" ?>
    
    <tle>
        <default-ui-creator>
            <view-set id="web" 
                      error-view="error" >
                <view id="greeting" page="/view/greeting.jsp" />
                <view id="greeting2" page="/view/greeting2.jsp" />
                <view id="error" page="/view/error.jsp" />
            </view-set>
    
            <command-view-mapping>
                <command id="/greeting.do">
                    <result code="success" view="greeting" />
                </command>
                <command id="/hello/greeting.do" view="greeting2" />
            </command-view-mapping>
            
            <client-view-set default-view-set="web">
                <client type="MSIE" view-set="web" />
                <client type="NETSCAPE" view-set="web" />
            </client-view-set>
        </default-ui-creator>
    </tle>

위 코드에서 핵심 태그는 view-set 태그와 command-view-mapping 태그이다. 먼저 view-set 태그에는 view 태그가 존재하는데 뷰 태그는 아래표와 같은 내용을 설정한다.

view 태그 : 핸들러가 처리한 결과를 보여줄 때 사용될 JSP 정보를 입력한다.
속성 내용
id 뷰셋에 있는 뷰의 고유 식별자값을 뷰 페이지의 식별자
page 뷰를 생성할 때 사용될 JSP

command-view-mapping 태그는 요청 처리기의 결과를 어떤 뷰를 통해서 보여줄지를 지정하기 위해 사용된다. command-view-mapping 태그는 자식 태그로 command 태그를 갖고 있으며, command 태그는 다음과 같은 코드 형태를 띈다.

    <command id="/greeting.do">
        <result code="success" view="greeting" />
        <result code="fail" view="greetingFail" />
    </command>

위 코드는 다음을 의미한다.

  • 클라이언트가 요청한 명령어 ID가 "/greeting.do"인 경우,
    요청 처리기가 생성한 결과 코드가
    • success 이면, greeting 뷰를 통해서 결과를 보여주고
    • fail 이면, greetingFail 뷰를 통해서 결과를 보여준다.
여기서 view 속성의 값은 앞서 살펴본 view-set/view 태그의 id 속성에 명시한 값을 사용한다. 즉, 아래 코드는

    <view-set id="web" 
              error-view="error" >
        <view id="greeting" page="/view/greeting.jsp" />
    </view-set>
    
    <command-view-mapping>
        <command id="/greeting.do">
            <result code="success" view="greeting" />
        </command>
    </command-view-mapping>

명령어 ID가 /greeting.do이고, 요청 처리기가 이 명령어를 처리한 결과 코드값이 "success"이면, "greeting"뷰인 /view/greeting.jsp를 이용해서 결과를 보여준다는 것을 의미한다.

뷰셋 설정에 따라 보여지는 화면이 달라질 수 있는데, 이에 대해서는 틀 프레임워크의 시리즈를 진행하면서 살펴보기로 하자.

DefaultUICreator 뷰 JSP 작성하기

간단하게 뷰 기능을 수행하는 JSP를 작성해보자. 본 글에서 사용하는 예제는 아래와 같이 3개의 뷰를 정의하고 있다.

   <view-set id="web" 
              error-view="error" >
        <view id="greeting" page="/view/greeting.jsp" />
        <view id="greeting2" page="/view/greeting2.jsp" />
        <view id="error" page="/view/error.jsp" />
    </view-set>

"error" 뷰는 view-set 태그의 error-view 속성을 통해서 에러 뷰로 지정됐는데, 에러 뷰에 대해서는 나중에 살펴보기로 하고, 본 글에서는 나머지 두 개의 뷰의 코드를 살펴보도록 하겠다. 먼저 "greeting" 뷰에 해당하는 /view/greeting.jsp 코드를 보도록 하자.

    코드: /view/greeting.jsp    
    <%@ page contentType = "text/html; charset=euc-kr" %>
    <%@ page import = "tle.framework.component.spi.rp.Result" %>
    <%
        Result result = (Result)request.getAttribute("result");
        String greeting = (String)result.getData("greeting");
    %>
    <html>
    <head><title>greeting</title></head>
    <body>
    <%= greeting %>
    </body>
    </html>

위 코드는 DefaultUICreator 컴포넌트와 관련된 뷰 JSP 페이지의 가장 기본적인 코드 형태를 보여주고 있다. 먼저 뷰 JSP는 결과를 참조하기 위해 request의 "result" 속성으로부터 Result 객체를 구한다. 그런 뒤, Result.getData() 메소드를 사용해서 요청 처리기가 생성한 데이터를 읽어와 그 데이터를 알맞게 사용해서 화면을 생성하게 된다.

예제 실행하기

지금까지 살펴본 예제의 전체 코드는 하단의 다운로드 링크를 통해 구할 수 있다. 예제 압축 파일인 tletest.war 파일을 [톰캣]/webapps와 같이 웹 어플리케이을 자동으로 인식할 수 있는 폴더에 복사하면 자동으로 배포된다. 자동 배포가 아니더라도 war 파일의 압축을 푼뒤 어플리케이션 서버의 설정을 알맞게 지정해줘도 된다.

tletest.war를 알맞게 배포한 뒤 어플리케이션 서버를 실행해보자. 톰캣의 /webapps 폴더에 배포했다면 아래의 URL을 사용해서 결과를 확인할 수 있을 것이다.

    http://localhost:8080/tletest/greeting.do
    http://localhost:8080/tletest/hello/greeting.do

두 URL을 실행하면 아래와 같이 결과가 출력될 것이다.

 예제 실행 흐름

http://localhost:8080/tletest/greeting.do 요청이 왔을 때 틀 프레임워크의 순차적인 처리 순서는 아래 그림과 같다.


위 실행 순서를 모르더라도 틀을 사용하는데에 지장은 없다. 하지만, 위의 실행 순서는 틀 프레임워크나 스트러츠 등 MVC 패턴에 기반한 프레임워크들은 대부분 위와 비슷한 순서로 작업을 진행하므로, 위 순서를 이해해두면 프레임워크를 사용하는데 많은 도움이 될 것이다.

다음 글에서는

본 글에서는 간단하게 틀 프레임워크의 기본적인 사용방법을 살펴보았다. 어렴풋이 틀의 설정 방법이나 사용방법, 그리고 틀이 어떤 식으로 돌아가는지에 대한 내용을 이해했으리라 본다. 다음 글에서는 클라이언트의 요청을 처리할때 사용되는 DefaultRequestProcessor의 핸들러의 개발 방법에 대해서 자세하게 살펴보도록 하겠다.

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요

빌드 과정을 처리해주는 Ant Task를 직접 구현해본다.

원하는 기능을 수행하는 Ant Task 구현하기

요즘은 많은 프로젝트에서 Ant를 이용해서 개발을 진행하고 있다. Ant는 기본적으로 제공하는 Task가 풍부할 뿐만 아니라 확장성이 뛰어나며, 실제로 추가적으로 제공되는 Ant Task를 인터넷상에서 손쉽게 찾아볼 수 있다.

Ant는 기본적으로 제공되는 기능만 잘 사용해도 대단하지만, Ant에서 기본적으로 제공하지 않는 아주 사소한 기능때문에 아쉬웠던적도 있을 것이다. (어쩌면, 잘 몰라서 없다고 생각했을지도 모르지만...) 그래서, 본 글에서는 아주 간단하지만 새로운 Task를 만들어서 Ant를 확장하는 방법을 살펴보고자 한다.

구현해볼 Ant Task 기능

본 글에서 만들어보려는 기능은, 알집에서 새로운 zip파일을 만들때 기존 zip파일이랑 겹치지 않게 뒤에 괄호를 붙여서 파일명을 만드는 기능과 비슷하다. 즉, 기존에 backup.zip 이 있었다면 그 다음에는 backup(2).zip, 또 그 다음에는 backup(3).zip와 같이 ZIP 파일을 만들어주는 기능을 구현해보고자 한다.

본 글에서는 아래와 같이 날짜와 시분 그리고 일렬번호를 덧붙여주는 Ant Task를 작성해볼 것이다.

  • backup_20050515_1426.zip -> 처음 생성시
  • backup_20050515_1426(2).zip -> 두번째
  • backup_20050515_1426(3).zip -> 세번째
구현한 Ant Task 소스 코드

일단 소스코드부터 살펴보도록 하겠습니다. 앞서 설명했던 기능을 제공하는 Ant Task의 구현체는 아래와 같습니다.

    package anttest;
    
    import java.io.File;
    import java.text.SimpleDateFormat;
    
    import org.apache.tools.ant.BuildException;
    import org.apache.tools.ant.Task;
    
    /**
     * @author 홍순풍(rocklike@empal.com)
     * 
     * 알집에서는 새로운 압축파일을 만들때 기존의 이름뒤에 (2)이런 식으로 
     * 해서 만든다. (예 : backup.zip, backup(2).zip)
     * 요 테스크는 그런 비슷한건데, 추가로 연월일_시분을 붙여서 만든다.
     * ( 예:backup_20050515_1539.zip, backup_20050515_1539(2).zip )
     * 
     * ant.jar를 classpath로 잡고서 요 파일을 실행시키면, c:\ 디렉토리에 샘플 
     * 파일이 생성되는걸 확인할수 있다.  
     */
    public class NewFilenameTask extends Task {
        //--- 필수
        private String dir; // 디렉토리
        private String prefix; // 파일명 앞쪽에 붙일꺼.
        private String property; // setting해줄 Property name
        
        //--- 선택
        private String extName = "zip"; // 확장자
        private String pattern; // 이름만들때 쓰일 패턴 (기본:yyyyMMdd_HHmm)
        
        //--- 속성 setting
        public void setDir(String _dir) {
            this.dir = _dir;
        }
        
        public void setPrefix(String _prefix) {
            this.prefix = _prefix;
        }
        
        public void setExtName(String _extName) {
            this.extName = _extName;
        }
        
        public void setPattern(String _pattern) {
            this.pattern = _pattern;
        }
        
        public void setProperty(String _property) {
            this.property = _property;
        }
        
        /**
         * 기본 validation 체크
         */
        public void validate() throws BuildException{
            
            // dir, prefix, property 속성은 반드시 지정해야 한다.
            if(dir==null || "".equals(dir)){
                throw new BuildException("dir속성은 반드시 넣어야 합니다. [dir값:" 
                        + dir + "]");
            }
            if(prefix==null) {
                throw new BuildException("prefix속성은 반드시 넣어야 합니다. [prefix값:" 
                        + prefix + "]");
            }
            if(property==null || "".equals(property)) {
                throw new BuildException("property속성은 반드시 넣어야 합니다. [property값:" 
                        + property + "]");
            }
        }
    
    
        /**
         * execute() 메소드
         */
        public void execute() throws BuildException {
            
            validate();
            
            File newFile = getNewFile(dir, prefix, extName, pattern);
            getProject().setProperty(property, newFile.getAbsolutePath());
            
        }
      
        /**
         * 새로운 파일을 리턴.
         * 
         */
        public File getNewFile(String dir, String prefix, String extName,
                String pattern) throws BuildException {
            return getNewFile(new File(dir), prefix, extName, pattern);
        }
        
        /**
         * 날짜랑 시간을 붙인 붙여서 새로 만든 파일을 리턴.
         * 
         */
        public File getNewFile(File dir, String prefix, String extName,
                String pattern){
            if(!dir.isDirectory() || !dir.exists()){
                throw new BuildException("디렉토리가 존재하지 않습니다. [dir명 => " + dir +"]");
            }
            
            // pattern이 null이라면 "yyyyMMdd_HHmm" 으로 하자.
            if(pattern==null){
                pattern = "yyyyMMdd_HHmm";
            }
            SimpleDateFormat formatter = new SimpleDateFormat(pattern);
            String middleName = formatter.format(new java.util.Date());
             
            // 예 : backup_20050515_1745.zip
            String newFileName = prefix + "_" + middleName + "." + extName;
            File newFile = new File(dir, newFileName);
            for(int i=2; newFile.exists(); i++){
                newFileName = prefix + "_" + middleName + "(" + i + ")." + extName;
                newFile = new File(dir, newFileName);
            }
            
            return newFile;
        }
    
        
        /**
         * 테스트용..
         * @param args
         * @throws Exception
         */
        public static void main(String[] args) throws Exception{
            String dir = "c:/";
            File newFile = 
                new NewFilenameTask().getNewFile(dir, "backup", "zip", null);
            System.out.println("AbsolutePath : " + newFile.getAbsolutePath());
            boolean result = newFile.createNewFile();     
            System.out.println("result : " + result);
        }
            
        
    }

새로운 Ant Task를 만드려면 위 코드와 같이 Task 클래스를 상속받으면 된다. (물론, MatchingTask와 같이 원하는 기능 중 일부를 기본적으로 제공해주는 Task를 상속받아도 된다.)

private으로 선언된 필드들을 보면 dir, prefix, property, extName, pattern이 있는데, 이중에서 extName과 pattern은 지정하지 않을 경우 기본값을 갖도록 처리하였다. 이 필드들의 값들은 xml에서 <mytask> 태그의 속성으로 받아오며(예: <mytask dir="c:/test" ...>), Ant가 내부적으로 리플렉션을 이용해서 각 필드의 값을 설정해준다. 예를 들어, dir의 속성값을 build.xml에서 받고 싶은 경우에는 public void setDir(String dir) 이라는 메소드를 만들어 주면 된다. 즉, set변수명() 메소드가 호출되서 우리는 값을 받아올수 있는 것이다. (즉, 자바빈 규약에 따라 작성된 프로퍼티의 값을 설정해준다.)

만일 프로퍼티를 이용해서 값을 지정하지 않고, 아래와 같이 중첩된 요소를 사용해서 값을 지정한다면(아래 코드에서는 <fileset>을 통해 값을 지정하고 있다) create엘리먼트명(), add엘리먼트명(), addConfigured엘리먼트명() 등의 메소드를 호출하면 되는데, 이때도 역시 Ant에서 이 메소드를 자동으로 호출해준다. (이에 대한 보다 자세한 내용은 Ant 문서를 참고하기 바란다.)

    <mytask >
        <fileset dir="/test">
            <include name="**"/>
        </fileset>
    </mytask>

마지막으로, Ant에 의해서 호출되는 메소드는 execute() 메소드이다. execute() 메소드는 Task가 실제로 처리할 작업에 대한 로직 코드를 포함한다. (스트러츠가 로직을 처리하기 위해 execute() 메소드를 호출해 주는 것처럼, Ant도 Task를 실행하기 위해 execute() 메소드를 호출한다.)

execute()메소드를 보면 아래 부분이 있다.

    getProject().setProperty(property, newFile.getAbsolutePath());

위 코드는 이는 build.xml안에서 <property name="xxx" value="yyy"/> 와 같이 프로퍼티를 새롭게 설정해 주는 것이다. 이렇게 프로퍼티 값을 설정하면, 빌드 파일의 다른 부분에서 ${xxx}와 같은 코드로 프로퍼티 값을 참조해서 사용할 수 있게 된다.

구현한 Task를 빌드 파일에서 사용하기

Ant Task 클래스를 구현했으므로 이제 남은 일은 실제로 Ant Task를 사용하는 것 뿐이다. 새로 구현한 Ant Task를 사용하기 위해서는 빌드 파일에 Ant Task를 정의해주어야 한다. 아래와 같이 빌드 파일에서 <taskdef> 태그를 사용해서 사용할 Ant Task를 명시해주어야 한다.

    <target name="init" description="내가 만든 task를 이용해 보자...">

            <echo> ${ant.home} </echo>

            <taskdef name="mytask" classname="anttest.NewFilenameTask" />

            <mytask dir="${basedir}"
                         prefix="backup"
                         property="newFilename"  />

            <echo> New Filename : ${newFilename} </echo>

    </target>

<taskdef> 태그는 새로운 Task를 정의할 때 사용된다. name 속성은 새로운 Task의 태그명을 입력하며, classname 속성은 Task를 구현한 클래스의 이름을 입력한다. 일단 taskdef 태그를 사용해서 커스텀 Task를 지정하면, name 속성에서 명시한 이름을 통해서 커스텀 Task를 사용할 수 있게 된다.

결론

소스등을 백업하기 위해서, (첨부된 build.xml 파일의 예와 같이) 기존 소스를 하나의 zip으로 묶어서 backup디렉토리에 넣어 놓을때 이용할 수 있을 것이다. 이미, ant에 이런 기능이 있을꺼란 생각이 들기도 하지만, 필자의 경우 이런 기능이 참으로 아쉬웠던 적이 있어서 직접 만들어 보았다. 본 글이 새로운 Ant Task를 구현하고 싶은 개발자에게 조금이나마 도움이 되기를 바란다.

관련링크:

본 글의 저작권은 작성자(홍순풍)에 있으며 저작권자의 허락없이 온라인/오프라인으로 본 글을 유보/복사하는 것을 금합니다.
Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 개발자 2013.11.05 17:33 신고  댓글주소  수정/삭제  댓글쓰기

    좋은 정보 감사합니다.
    많은 도움 되었습니다.

객체와 데이터베이스 사이의 그리고 매퍼 자체의 독립성을 유지하는 매퍼 레이어에 대해서 살펴본다.

메타 데이터 매퍼

본 글에서 소개하는 패턴은 '엔터프라이즈 어플리케이션 아키텍처 패턴' (마틴 파울러 저)에 소개된 패턴으로서, 필자가 파울러가 소개한 패턴을 보다 쉽게 이해할 수 있도록 재구성한 것이다.

'데이터 매퍼(Data Mapper)'는 이 글의 시리즈 글인 '데이터 소스 아키텍처 패턴 1'에서 살펴봤던 테이블 데이터 게이트웨이와 상당히 유사하다. 데이터 매퍼는 테이블 데이터 게이트웨이와 마찬가지로 도메인 모델 객체와 데이터베이스 테이블 사이에 존재하는 레이어로서 도메인 모델 객체와 데이터베이스 테이블 사이의 데이터 이동을 처리해준다.

테이블 데이터 게이트웨이는 테이블과 관련된 CRUD 작업을 처리하는 데 초점을 맞추고 있다면, 데이터 매퍼는 도메인 모델 객체와 테이블 사이의 매핑을 처리하는 데에 초점을 맞추고 있다. 이런 관점에서 테이블 데이터 게이트웨이는 테이블과 관련된 값을 RecordSet과 같은 범용타입을 통해서 처리해도 무방하지만, 데이터 매퍼의 경우는 정확한 타입의 객체와 테이블 사이의 매핑을 처리해주어야 한다.

마틴 파울러의 책에서 보여주는 데이터 매퍼의 구현 방법은 사실상 테이블 데이터 게이트웨이의 구현 코드에 '이미 로딩한 객체의 캐싱' 및 '추상 클래스를 통한 공통 코드 처리'를 추가 구현한 정도에 지나지 않으며, 같은 책의 뒤에서 소개하고 있는 메타 데이터 매퍼가 실제로 쓰임새 있는 패턴이라고 볼 수 있다. 메타 데이터 매퍼 패턴은 데이터 매퍼 개념을 범용적으로 확장한 것이므로 본 글에서는 메타 데이터 매퍼에 대해서 살펴볼 것이다.

메타 데이터 매퍼 패턴은 아래 그림과 같이 구성된다.


위 그림에서 DataMapper는 모델 객체가 테이블가 어떻게 연결되는지를 정의한 매핑 정보를 저장하고 있다. 예를 들어, DataMapper는 Team 객체의 id 프로퍼티와 데이터베이스의 TEAM 테이블의 TEAM_ID 필드가 매핑된다는 정보를 갖고 있다. 이 매핑 정보를 이용해서 DataMapper는 TEAM 테이블로부터 데이터를 읽어와 Team 객체를 생성하게 되며, Team 객체에 저장되어 있는 정보를 TEAM 테이블에 반영하게 된다.

DataMapper는 <객체, 테이블> 매핑 정보만 갖고 있으면 모든 CRUD 작업을 처리할 수 있게 된다. 설명에서 느꼈겠지만, 마틴 파울러가 설명한 데이터 매퍼 및 메타 데이터 매퍼 패턴은 객체(Object)-관계 데이터베이스(Relation Database) 매핑, 즉, OR 매핑에 대한 패턴이다.

구현 방법

메타 데이터 매퍼 패턴, 즉, OR 매핑을 구현하기 위해서는 다음의 정보가 필요하다.

  • 객체와 매핑할 테이블은 무엇인가?
  • 객체의 프로퍼티를 테이블의 어떤 필드에 매핑할 것인가?
보통은 위의 매핑 정보를 설정 파일로 처리한다. 예를 들어, 아래와 같은 설정 파일을 사용할 수 있다.

   <?xml version="1.0" ?>
   
   <mapping>
       <object class="madvirus.javacan.Employ" table="EMPLOY">
           <property name="id" field="EMPLOY_ID" />
           <property name="name" field="NAME" />
           <property name="address" field="ADDRESS" />
           
           <primary-key>
               <property>id</property>
           <primary-key>
       </object>
   </mapping>

위의 설정 파일을 보면 Employ 객체와 매핑되는 테이블이 EMPLOY 테이블이며, 객체의 id 프로퍼티, name 프로퍼티, address 프로퍼티는 각각 EMPLOY 테이블의 EMPLOY_ID 필드, NAME 필드, ADDRESS 필드와 매핑된다는 것을 나타내고 있음을 쉽게 알 수 있다.

DataMapper는 위와 같은 형태의 설정 파일로부터 매핑 정보를 읽어와 객체와 테이블 사이의 매핑을 알맞게 처리하게 된다. 위의 매핑 정보는 다음과 같은 형태의 클래스를 사용해서 저장될 것이다.

    public class MetaData {
        private Class object;
        private String tableName;
        private List<FieldMapping> fieldMappingList = new ArrayList<FieldMapping>();
        private List<FieldMapping> pkFieldList = new ArrayList<FieldMapping>();
        
        public List<FieldMapping> getFieldMappingList() {
            return fieldMappingList
        }
        public List<FieldMapping> getPKFieldList() {
            return pkFieldList;
        }
        ...
    }
    
    public class FieldMapping {
        private String property; // <property> 태그의 name 속성값
        private String field; // <property> 태그의 field 속성값
        
        ...
    }

DataMapper는 위의 메타 정보를 사용해서 알맞은 쿼리를 생성한다. 예를 들어, 특정 테이블로부터 데이터를 읽어오는 select() 메소드는 다음과 같은 형태가 될 것이다.

    public class DataMapper {
        Map<MetaData> map = new java.util.HashMap<MetaData>(); // <Object타입, MetaData> 매핑 저장
        
        public Object select(Class objectType, Map keyValue) {
            MetaData metaData = map.get(objectType);
            List<FieldMapping> fieldList = metaData.getFieldMappingList();
            List<FieldMapping> pkList = metaData.getFieldMappingList();
            
            StringBuffer query = new StringBuffer();
            query.append("select ");
            for (int i = 0 ; i < fieldList.size() ; i++) {
                query.append(fieldList.get(i).getField());
                if (i < fieldList.size() - 1) query.append(", ");
            }
            query.append(" from ").append(metaData.getTableName()).append(" where ");
            
            for (int i = 0 ; i < pkList.size() ; i++) {
                query.append(pkList.get(i).getField());
                query.append(" = ? ");
                if (i < pkList.size() - 1) query.append(" and ");
            }
            
            String sql = query.toString();
            
            PreparedStatement pstmt = null;
            ResultSet rs = null;
            
            try {
                pstmt = DB.prepareStatement(sql);
                for (int i = 0 ; i < pkList.size() ; i++) {
                    Object val = keyValue.get(pkList.get(i).getProperty());
                    
                    // setParameter 메소드는 PreparedStatement의 IN 파라미터값을
                    // val 객체의 타입에 알맞게 처리해준다고 가정하자.
                    setParameter(pstmt, i+1, val); 
                }
                rs = pstmt.executeQuery();
                if (rs.next()) {
                    Object obj = objectType.newInstance();
                    ... // rs의 값을 obj에 복사.
                    return object;
                }
            } finally {
                ...
            }
        }
    }

위 코드에서 볼 수 있듯이 객체와 테이블 사이의 관계를 DataMapper가 모두 처리해주기 때문에, 객체는 테이블에 대해서 전혀 알 필요가 없으며, 심지어 DataMapper의 존재에 대해서도 전혀 알 필요가 없다. 도데인 영역의 객체가 테이블과 DataMapper에 의존적이지 않기 때문에 데이터베이스를 변경하거나 테이블이 변경될 경우 모델에 끼치는 영향을 최소화할 수 있다.

데이터 매퍼의 장점

웹 어플리케이션 프로그래밍의 90% 이상이 데이터베이스와 연동하는 코드를 포함하고 있다. CRUD 코드를 일일이 작성하는 것은 굉장히 지루하고 실수를 유발하기 쉬울 뿐만 아니라 코딩 시간의 많은 부분을 차지하는 작업인데, 데이터 매퍼를 사용함으로써 개발 시간을 상당량 줄일 수 있게 되며 잘못된 코딩에 따른 버그도 줄일 수 있게 된다.

CRUD 코드를 작성하는 데 소모되는 시간을 줄일 수 있다는 것은 비즈니스 로직 부분의 코드를 작성하는데 더 많은 시간을 투자할 수 있다는 것을 의미한다. 로직 부분에 집중할 수 있는 시간이 늘어남으로써 빠른 시간에 원하는 어플리케이션을 구현할 수 있게 된다.

또한, 데이터 매퍼를 사용하게 되면, 테이블이 변경될 때 객체의 변경을 최소화시킬 수 있다. 예를 들어, 테이블 스키마에서 필드 하나를 제거했다고 해 보자. 이 경우 객체는 전혀 변경할 필요가 없이 매핑 정보만 변경하면 된다. 필드가 삭제되었다고 해서 변경해야 할 쿼리는 하나도 없는 것이다.

반대로, 테이블에 저장해야 하는 객체의 프로퍼티가 새롭게 추가되었다고 해 보자. 이때 해야 할 작업은 테이블에 관련 필드를 추가하고 매핑 정보를 변경하는 것이다. 앞의 경우와 마찬가지로 변경해야 할 쿼리는 전혀 없다.

마지막으로 객체가 데이터 매퍼에 의존적이지 않기 때문에, 데이터 매퍼를 독립적으로 유지할 수 있다. 즉, 객체에 상관없이 데이터 매퍼를 수정할 수 있으며, 심지어 데이터 매퍼 구현 클래스를 변경할 수도 있다.

데이터 매퍼를 직접 구현할 필요는 없다!

의욕이 높거나 한번 해보고 싶은 마음에 데이터 매퍼를 직접 구현해보는 것은 좋으나, 도메인 모델에 맞는 데이터 매퍼를 직접 구현하는 것은 쉬운 일이 아니다. 또한, 데이터 매퍼를 구현하는 시간이 짧지도 않다. 따라서, 미리 구현해둔 데이터 매퍼가 없거나 또는 기존에 구현한 데이터 매퍼가 프로젝트에서 사용하는 도메인 모델에 잘 맞지 않을 경우에는, 데이터 매퍼를 직접 구현하는 것 보다는 이미 존재하는 데이터 매퍼 라이브러리를 사용하는 것이 가장 좋다. 예를 들어, Hibernate와 같이 널리 사용되는 OR 매핑 도구를 사용하는 것이 직접 구현하는 것보다 프로젝트 개발 시간을 단축시키는 데 도움이 될 것이다.

관련링크:

 

Posted by 최범균 madvirus

댓글을 달아 주세요

테이블의 각각의 레코드에 대해 객체가 하나씩 존재하는 로우 데이터 게이트웨이 패턴에 대해서 살펴본다.

로우 데이터 게이트웨이

본 글에서 소개하는 패턴은 '엔터프라이즈 어플리케이션 아키텍처 패턴' (마틴 파울러 저)에 소개된 패턴으로서, 필자는 파울러가 소개한 패턴을 보다 쉽게 이해할 수 있도록 재구성한 것이다.

'로우 데이터 게이트웨이(Row Data Gateway)'
는 데이터베이스 테이블의 한 레코드를 정확하게 묘사하는 객체의 역할을 한다. 앞서 살펴봤었던 '데이터 소스 아키텍처 패턴 1 - 테이블 데이터 게이트웨이'는 테이블의 모든 행에 대해서 CRUD 작업을 수행하는 반면에, 로우 데이터 게이트웨이는 오직 관련된 하나의 행에 대해서만 작업을 수행한다. (이 패턴의 구현에 대해서는 자바캔에 필자가 기재한 'JSP, 자바빈 컴포넌트 그리고 데이터베이스'에도 명시되어 있으니 참고하기 바란다.)

구현 방법

로우 데이터 게이트웨이를 구현하기 위해서는 보통 다음과 같은 두 가지 클래스를 필요로 한다.

  • 객체 찾기 클래스 - 테이블의 특정 행에 대한 게이트웨이 객체를 생성해주는 클래스. 보통 이름이 ~Finder 로 끝난다.
  • 게이트웨이 클래스 - 테이블의 특정 행의 값을 표현하는 클래스. 테이블의 각 필드에 해당하는 프로퍼티를 갖는다.
(위의 구성은 마치 EJB의 엔티티 빈을 보는 것 같다. EJB의 엔티티 빈은 엔티티 빈 객체를 찾기 위한 홈 인터페이스를 갖고 있으며, 이 홈을 통해서 엔티티 빈에 접근하게 되는데, 로우 데이터 게이트웨이 역시 이와 동일한 방법으로 수행된다.)

먼저 로우 데이터 게이트웨이 클래스의 구현에 대해서 살펴보자. 로우 데이터 게이트웨이 클래스는 테이블의 필드를 나타내는 프로퍼티를 포함하고 있으며, 데이터의 삽입(insert), 변경(update), 삭제(delete) 쿼리를 수행해주는 메소드를 제공한다. 예를 들어, 회원 테이블인 MEMBER 테이블에 대한 로우 데이터 게이트웨이 클래스는 다음과 같이 작성할 수 있다.

    public class MemberGateway {
        
        // 프로퍼티는 테이블의 필드와 매칭
        private String id;
        private String name;
        private String email;
        
        public String getId() {
            return id;
        }
        public void setId(String id) {
            this.id = id;
        }
        public String getName() {
            return name;
        }
        public void setName(String name) {
            this.name = name;
        }
        public String getEmail() {
            return email;
        }
        public void setEmail(String email) {
            this.email = email;
        }
        
        // 행과 관련된 Insert, Update, Delete 처리 메소드
        public void insert() throws SQLException {
            Connection conn = null;
            PreparedStatement pstmt = null;
            try {
                conn = ...
                pstmt = conn.prepareStatement(
                    "insert into MEMBER (ID, NAME, EMAIL) values (?, ?, ?)");
                pstmt.setString(1, id);
                pstmt.setString(2, name);
                pstmt.setString(3, email);
                
                pstmt.executeUpdate();
            } finally {
                ...
            }
        }
        
        public void update() throws SQLException {
            ..
            try {
                conn = ...
                pstmt = conn.prepareStatement(
                    "update MEMBER set NAME = ?, EMAIL = ? where ID = ?");
                pstmt.setString(1, name);
                pstmt.setString(2, email);
                pstmt.setString(3, id);
                
                pstmt.executeUpdate();
            } finally {
                ...
            }
        }
        
        public void delete() throws SQLException {
            ...
        }
    }

로우 데이터 게이트웨이 클래스에서는 SELECT 쿼리를 사용해서 객체를 읽어오는 메소드가 존재하지 않는데, 행에 해당하는 객체를 생성하는 부분은 찾기 클래스에서 수행한다. 찾기 클래스는 다음과 같이 구현한다.

    public class MemberFinder {
        
        public MemberGateway find(String id) throws SQLException, MemberNotFoundException {
            ..
            try {
                conn = ...
                pstmt = conn.prepareStatement(
                    "select ID, NAME, EMAIL from MEMBER where ID = ?");
                pstmt.setString(1, id);
                rs = pstmt.executeQuery();
                if (rs.next()) {
                    MemberGateway member = new MemberGateway();
                    ... // rs의 내용을 member에 복사
                    return member;
                } else {
                    throw new MemberNotFoundException(..);
                }
            } finally {
                ...
            }
        }
        
        public List find(Filter filter) throws SQLException {
            ...
        }
    }

MemberFind 클래스는 두 가지 종류의 find() 메소드를 제공하는 데, 하나는 Primary Key를 사용하여 정확하게 하나의 행에 해당하는 MemberGateway 객체를 리턴하는 것이고, 다른 하나는 주어진 조건에 따라서 여러 행을 읽어와 MemberGateway의 리스트를 리턴하는 것이다. (두번째 find() 메소드의 Filter에 대한 내용은 이 시리즈의 이전 기사인 "데이터 소스 아키텍처 패턴 1 - 테이블 데이터 게이트웨이"에서 설명한 바 있으니 참고하기 바란다.)

테이블 로우 게이트웨이 클래스나 찾기 클래스의 CRUD 관련 메소드가 사용할 데이터베이스 Connection을 어떻게 전달할 것인가의 문제는 앞선 기사인 "데이터 소스 아키텍처 패턴 1 - 테이블 데이터 게이트웨이"에서 설명한 바 있다.

사용 방법

로우 데이터 게이트웨이는 트랜잭션 스크립트 뿐만 아니라 도메인 모델과도 잘 어울리게 사용할 수 있다. 먼저 트랜잭션 스크립트의 경우는 데이터베이스 처리를 로우 데이터 게이트웨이가 처리해주기 때문에 비즈니스 로직에만 신경쓸 수 있게 된다. 예를 들어, 회원 가입과 관련된 코드를 JSP에서 처리할 때, 로우 데이터 게이트웨이를 사용한다면 다음과 같이 작성할 수 있을 것이다.

    <jsp:useBean id="member" class="MemberGateway">
        <jsp:setProperty name="member" property="*" />
    </jsp:useBean>
    <%
        // 값 검증 코드
        ...
        
        member.insert();
    %>

각 테이블마다 그에 해당하는 찾기 클래스와 로우 데이터 게이트웨이를 작성해주어야 한다는 점이 불편할 수도 있는데 이 문제는 코드 자동화 툴을 사용해서 해결할 수 있다. 예를 들어, 테이블의 스키마로부터 자동으로 그에 해당하는 로우 데이터 게이트웨이 클래스를 생성해주는 도구가 있다고 가정해보자. 이 경우 개발자는 테이블과 관련된 코드는 거의 작성하지 않아도 되므로 좀더 많은 시간을 비즈니스 로직을 처리하는 코드에 투자할 수 있게 된다.

도메인 모델을 사용하는 경우에도 로우 데이터 게이트웨이를 사용할 수 있다. 예를 들어, 도메인 영역의 회원을 나타내는 클래스를 Member라고 할 경우, Member 클래스는 다음과 같이 작성할 수 있을 것이다.

    public class MemberImpl implements Member {
        private MemberGateway data;        
        public Member(MemberGateway data) {
            this.data = data;
        }
        
        public String getId() {
            // 데이터 읽기/쓰기 기능을 게이트웨이로 위임
            return data.getId();
        }
        
        ...
    }

도메인 영역의 객체는 내부적으로 게이트웨이를 통해서 데이터에 접근하는 방식을 취한다. 하지만, 도메인 영역의 객체와 데이터베이스 테이블의 필드가 1-1 매핑이 되지 않는 경우에는 위와 같은 방식은 잘 들어맞지 않기 때문에, 테이블과 도메인 객체 사이에 1-1매핑이 되지 않을 때에는 테이블과 도메인 객체 사이의 매핑을 처리해주는 객체인 매퍼(Mapper)를 사용하는 것이 좋다. 매퍼 패턴에 대해서는 이 시리즈의 다음 글에서 살펴볼 것이다.

필자의 경우는 앞서 살펴봤던 테이블 데이터 게이트웨이와 로두 데이터 게이트웨이를 모두 사용해봤었는데, 두 패턴 모두 거의 비슷한 개발 효율을 보여주었다. 물론, 로우 데이터 게이트웨이의 경우 코드 자동화 처리가 더 쉽기 때문에 데이터 베이스와 관련된 코드를 작성하는 시간을 줄일 수 있다는 장점을 갖고 있다. 하지만 이 장점을 제외하면 비즈니스 로직 코드를 작성하는 데 있어서 테이블 데이터 게이트웨이를 사용하든 로우 데이터 게이트웨이를 사용하든 비슷한 업무 효율을 나타낸다.

로우 데이터 게이트웨이와 액티브 레코드(Active Record)의 차이

마틴 파울러의 '엔터프라이즈 어플리케이션 아키텍처 패턴' 책에는 로우 데이터 게이트웨이와 거의 동일한 액티브 레코드 패턴을 소개하고 있다. 마틴 파울러의 정의에 의하면 액티브 레코드는 로우 데이터 게이트웨이와 CRUD 기능을 제공하는 것은 동일하나 추가적으로 도메인 로직까지 제공한다는 점이 다르다. 로우 데이터 게이트웨이의 경우는 도메인 로직을 전혀 포함하지 않고 오직 CRUD와 관련된 기능을 제공한다고 정의하고 있다.

필자의 경우는 액티브 레코드의 경우는 선호하지 않는 편이다. CRUD를 처리하는 기능과 도메인 로직을 처리하는 기능이 함께 섞여 있을 경우 코드가 복잡해질 뿐만 아니라 유지보수할 때에도 용이하지 않기 때문이다. 가급적이면 액티브 레코드를 사용하지 말 것을 권한다.

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요

하나의 클래스가 테이블의 전체 행을 처리하는 테이블 데이터 게이트웨이 패턴의 구현방법을 살펴본다.

테이블 데이터 게이트웨이의 구현

본 글에서 소개하는 패턴은 '엔터프라이즈 어플리케이션 아키텍처 패턴' (마틴 파울러 저)에 소개된 패턴으로서, 필자는 파울러가 소개한 패턴을 보다 쉽게 이해할 수 있도록 재구성한 것이다.

'테이블 데이터 게이트웨이(Table Data Gateway)'
는 데이터베이스 테이블에 대한 게이트웨이 역할을 하는 객체로서, 하나의 테이블 데이터 게이트웨이 객체를 통해서 테이블의 모든 행을 처리하는 패턴이다. 테이블 데이터 게이트웨이는 테이블과 관련된 모든 SQL을 포함한다. 즉, 삽입(INSERT), 선택(SELECT), 갱신(UPDATE), 삭제(DELETE)와 관련된 SQL을 테이블 데이터 게이트웨이가 처리한다.

구현 방법

테이블 데이터 게이트웨이의 구현 방법은 매우 간단하다. CRUD(Create, Read, Update, Delete)와 관련된 SQL 쿼리를 실행하는 메소드를 구현하기만 하면 된다. 보통, 다음과 같이 4개의 메소드를 기본적으로 제공한다.

    public class TableGateWay {
        public void insert(Data data) { ... }
        public void update(Data data) { ... }
        public Data select(String key) { ... }
        public void delete(String key) { ... }
    }

이들 메소드는 이름에서 알 수 있겠지만 관련된 SQL 쿼리를 실행한다. 예를 들어, insert() 메소드의 경우는 파라미터로 전달받은 Data 객체가 제공하는 값을 테이블에 삽입한다. 비슷한 방법으로 update() 메소드는 테이블의 값을 변경한다. select() 메소드와 delete() 메소드는 선택 또는 삭제할 행의 키값을 전달받아 관련 기능을 수행하게 된다. Data 클래스는 테이블과 관련된 데이터를 저장하는 객체의 타입을 나타내며 테이블 데이터 게이트웨이에 따라 알맞은 타입을 선택하면 된다.

테이블 데이터 게이트웨의 코드를 예로 들면, delete() 메소드는 다음과 같은 형태를 갖는다.

    public void delete(String key) throws SQLException {
        ...
        PreparedStatement pstmt = null;
        try {
            pstmt = conn.prepareStatement("delete from MEMBER where MEMBER_ID = ?");
            pstmt.setString(1, key);
            int deletedCount = pstmt.executeUpdate();
            if (deletedCount == 0) {
                throw ...
            }
        } finally {
            if (pstmt != null) try { pstmt.close(); } catch(SQLException ex) {}
        }
    }

select() 메소드는 읽어오고자 하는 방식에 따라 여러가지 형태가 존재할 수 있다. 예를 들어, 다음과 같이 필드값에 따라서 데이터를 읽어오도록 다양한 select() 메소드를 작성할 수 있을 것이다.

    public Data select(String key) { ... }
    public List selectByRegDate(Timestamp date) { ... }
    public List selectByName(String name) { ... }

목록을 읽어오는 select() 메소드가 조건에 따라 다양하게 필요할 경우 위 코드처럼 일일이 select() 메소드를 만드는 것은 비슷한 코드를 중복해서 사용하는 비효율적인 코드를 양산하게 된다. 이런 경우 쿼리의 WHERE 절에 명시될 데이터를 저장하는 필터를 만들어서 필터를 인자로 전달받도록 select() 메소드를 구현하는 것이 좋다. 먼저 조건을 저장하는 필터는 다음과 같이 조건을 명시할 수 있는 메소드를 제공하면 된다.

    public class Filter {
        public void addEqualsFilter(String fieldName, Object value) { ... }
        public void addBetweenFilter(String fieldName, Object from, Object to) { ... }
        public void addGTFilter(String fieldName, Object value) { ... }
        public void addLTFilter(String fieldName, Object value) { ... }
        public void addINFilter(String fieldName, List values) { ... }
        public void addNullFilter(String fieldName) { ... }
        public void addNotNullFilter(String fieldName) { ... }
        public String getWherePart() { ... }
    }

예를 들어, 2004년 1월 1일 이후에 가입한 회원의 목록을 추출하고자 할 경우 다음과 같은 필터를 생성하면 된다.(회원 가입날짜는 REGDATE 필드에 저장된다고 가정한다.)

    Calendar cal = Calendar.getInstance();
    cal.set(2004, 0, 1, 0, 0, 0);
    Date date = new Date(cal.getTime().getTime())
    Filter filter = new Filter();
    filter.addGTFilter("REGDATE", date);

필터를 사용하는 메소드를 테이블 데이터 게이트웨이에 추가할 경우, 아래와 같은 메소드가 추가될 것이다.

    public List selectByFilter(Filter filter) {
        ...
        String query = "select * from MEMBER " + filter.getWherePart();
        
        Statememt stmt = null;
        ResultSet rs = null;
        try {
            stmt = conn.createStatement();
            rs = stmt.executeQuery(query);
            ...
        } finally {
            ...
        }
    }

Filter.getWherePart()는 SQL의 where 조건에 해당하는 부분의 쿼리를 생성하도록 하면 된다. 관리자 툴이나 게시판 등은 다양한 where 조건을 필요로 하는데, 이렇게 다양한 검색 조건을 필요로 하는 경우에는 각 조건마다 select 메소드를 만드는 것 보다는 Filter를 통해서 조건을 명시할 수 있도록 하는 것이 좋다.

데이터 베이스 커넥션 전달 방법

테이블 데이터 게이트웨이를 사용할 때 한가지 정해야 할 것이 커넥션의 전달 규칙이다. 테이블 데이터 게이트웨이는 데이터베이스 커넥션을 사용하게 되는데, 이를 전달하는 방법에 따라 테이블 데이터 게이트웨이의 사용방법이 달라지기 때문이다. 크게 다음과 같은 3가지 방법이 존재한다.

  • 테이블 데이터 게이트웨이를 매번 새롭게 생성하며, 생성할 때에 커넥션을 전달한다.
  • 메소드 마다 커넥션을 파라미터로 전달받는다.
  • 테이블 데이터 게이트웨이의 메소드에서 직접 커넥션을 생성한다.
먼저 첫번째 방법에 대해서 살펴보자. 첫번째 방법은 테이블 데이터 게이트웨이 클래스의 코드를 다음과 같이 작성한다.

    public class MemberTableDataGateWay {
        private Connection conn = null;
        public void setConnection(Connection conn) {
            this.conn = conn;
        }        
        public void releaseConnection() {
            conn = null;
        }        
        // insert, select, update, delete 메소드 정의
        public void insert() {
            ...
            pstmt = conn.prepareStatement(...);
            ...
        }
    }

테이블 데이터 게이트웨이를 사용하는 부분의 코드는 다음과 같은 형태를 띄게 된다.

    
    Connection conn = null;
    MemberTableDataGateWay gateway = null;
    try {
        conn = ... // 커넥션을 얻는 코드
        gateway = new MemberTableDataGateWay();
        gateway.setConnection(conn);
        
        List list = gateway.selectByFilter(someFilter);
        if (list.size() > 0) {
            gateway.update(data);
            ...
        }
    } finally {
        if (gateway != null) gateway.releaseConnection();
        if (conn != null) ... // 커넥션을 닫음
    }
    

게이트웨이가 필요할 때마다 매번 테이블 게이트웨이 객체를 생성하기 때문에 가비지콜렉션이 자주 발생할 수 있는 문제가 있다. 하지만, 객체 풀링을 사용해서 테이블 게이트웨이를 풀 속에 저장해서 필요할 때에만 꺼내 사용하게 되면 테이블 게이트웨이 객체 생성으로 인한 가비지 콜렉션 발생 빈도를 줄일 수 있다.

커넥션을 얻어오는 두번째 방법은 테이블 데이터 게이트웨이의 모든 메소드가 커넥션을 인자로 전달받는 형태이다. 예를 들면 다음과 같이 첫번째 인자를 데이터베이스 커넥션으로 전달받는 형태가 된다.

    public void insert(Connection conn, Data data) { ... }
    public void update(Connection conn, Data data) { ... }
    public Data select(Connection conn, String key) { ... }
    public void delete(Connection conn, String key) { ... }

테이블 게이트웨이를 사용하는 코드는 다음과 같이 게이트웨이의 메소드를 실행할 때 마다 매번 커넥션을 전달해주게 된다.

    
    Connection conn = null;
    try {
        conn = ... // 커넥션을 얻는 코드
        MemberTableDataGateWay gateway = new MemberTableDataGateWay();
        
        List list = gateway.selectByFilter(conn, someFilter);
        if (list.size() > 0) {
            gateway.update(conn, data);
            ...
        }
    } finally {
        if (conn != null) ... // 커넥션을 닫음
    }
    

커넥션을 얻어오는 세번째 방법은 테이블 데이터 게이트웨이가 직접 커넥션을 생성하는 형태이다. 즉, 게이트웨이의 메소드는 다음과 같은 형태의 코드를 사용하게 된다.

    public void insert(Data data) throws SQLException {
        Connection conn = null;
        try {
            conn = someResource.getConnection();
            ...
        } finally {
            if (conn != null) try { conn.close(); } catch(SQLException ex) {}
        }
    }

필자의 경우는 세번째 방법은 사용하지 말것을 권하고 싶다. 그 이유는 비즈니스 로직을 처리하는 데 여러 테이블 게이트웨이의 메소드를 사용할 경우 트랜잭션 처리가 어려워지기 때문이다. 예를 들어, 회원 가입을 처리하는 비즈니스 로직을 생각해보자. 회원 정보 테이블과 메일 관련 테이블이 따로 존재할 경우 첫번째 방식을 사용하게 되면 다음과 같이 코드를 생성할 것이다.

    MemberTableGateway memberGateway = null;
    MailAccountTableGateway mailAccountGateway = null;
    Connection conn = null;
    try {
        conn = someResource.getConnection();
        memberGateway = new MemberTableGateway();
        mailAccountGateway = new MailAccountTableGateway();
        
        memberGateway.setConnection(conn);
        mailAccountGateway.setConnection(conn);
        
        conn.setAutoCommit(false); // 트랜잭션 시작
        memberGateway.insert(data);
        mailAccountGateway.insert(mailAccountData);
        conn.commit(); // 트랜잭션 완료
    } catch(SQLException ex) {
        if (conn != null)
            try { conn.rollback(); } catch(SQLException ex) {}
        ...
    } finally {
        ...
    }
    new MemberTableGateway();

하지만, 세번째 방법처럼 테이블 게이트웨이의 각각의 메소드에서 커넥션을 개별적으로 얻어올 경우 여러 메소드 호출에 대한 트랜잭션 처리를 할 수 없다. 따라서, 테이블 데이터 게이트웨이가 사용할 데이터베이스 커넥션은 첫번째 또는 두번째 방법과 같이 전달해주는 것이 좋다.

정보 전달 객체의 선택 및 예외 처리

정보 전달 객체의 선택

테이블 데이터 게이트웨이의 insert() 메소드나 update() 메소드는 테이블에 적용할 값을 인자로 전달받게 된다. 또한, select() 메소드는 테이블에서 읽어온 데이터를 리턴하게 된다. 이렇게 테이블 데이터 게이트웨이와 테이블 데이터 게이트웨이를 사용하는 클래스 사이에 주고 받을 클래스는 어떤 걸 사용해야 좋을까?

다음과 같이 두 가지 타입중의 한가지를 선택하면 된다.

  • RecordSet과 같이 범용적인 타입 사용
  • 비즈니스 도메인에 알맞은 자바빈 객체 사용
해당 테이블 데이터 게이트웨이의 재사용성이 높지 않을 경우 RecordSet과 같은 범용적인 타입을 사용해서 데이터를 주고 받는 것이 좋다. 예를 들어, 회원 추가의 경우 테이블에 데이터를 삽입하는 insert() 메소드를 호출하게 되는데, RecordSet 클래스를 사용할 경우 다음과 같은 코드를 작성하게 된다.

    RecordSet record = new RecordSet();
    record.set("MEMBER_ID", id);
    record.set("NAME", name);
    ...
    gateway = new MemberTableGateway();
    gateway.insert(record);
    ...

RecordSet은 범용적이기 때문에 어떤 테이블 데이터 게이트웨이에서도 사용할 수 있다. 또한, RecordSet은 어떤 비즈니스 도메인 영역의 개체와도 연관되어 있지 않기 때문에, 모든 비즈니스 영역에 대해서 사용할 수 있다. 하지만 RecordSet 자체는 도메인 영역을 잘 표현하지 못하므로 소스 코드 분석 작업이 수월하지는 않다.

현재 프로젝트 뿐만 아니라 이후 프로젝트에서도 테이블 데이터 게이트웨이를 재사용할 가능성이 높다면 자바빈 객체를 사용하는 것도 나쁘지 않다. 자바빈 객체는 그 자체가 비즈니스 영역의 특정 개체를 나타내기 때문에 범용타입인 RecordSet을 사용할 때보다 소스 코드를 읽기가 더 쉬워진다. 예를 들면 다음과 같이 테이블 데이터 게이트웨이의 메소드에 도메인의 개체를 나타내는 자바빈 객체를 전달한다.

    MemberBean member = new MemberBean();
    MemberBean.setId(id);
    MemberBean.setName(name);
    ...
    gateway = new MemberTableGateway();
    gateway.insert(member);
    ...

예외 처리는 어떻게?

테이블 데이터 게이트웨이 클래스를 작성할 때에는 예외 처리에 대한 것도 미리 정의해 놓아야 한다. 예를 들어, update() 메소드나 delete()를 실행하는 데 변경된 행이 하나도 없을 경우 예외를 발생할 것인가? select() 메소드를 실행할 때 PK로 검색한 행이 존재하지 않을 경우 예외를 발생할 것인가? 이런 문제에 대해서 테이블 데이터 게이트웨이가 어떻게 행동할지에 대해 명확하게 규정짓는 게 좋다.

보통은 다음과 같은 규칙을 적용하면 대부분의 어플리케이션에서 큰 문제없이 사용할 수 있다.

  • insert() 메소드 수행시 이미 같은 PK를 갖는 객체가 존재할 경우 AleadySamePrimaryKeyInserted와 같은 예외를 발생시킨다.
  • update()와 delete() 메소드에서 변경되거나 삭제된 행이 존재하지 않을 경우 NotFoundRecord와 같은 예외를 발생시킨다.
  • select(key) 메소드에서 key로 검색한 행이 존재하지 않을 경우 NotFoundRecord와 같은 예외를 발생시킨다.
이렇게 상황에 따라 알맞은 예외를 발생시키도록 사전에 정의해 놓으면 테이블 데이터 게이트웨이의 사용자들은 보다 명확하게 로직을 구현할 수 있게 된다.

Posted by 최범균 madvirus

댓글을 달아 주세요

웹 어플리케이션의 비즈니스 로직 처리와 관련된 도메인 로직 패턴인 테이블 모듈과 서비스 레이어에 대해서 살펴본다.

테이블 모듈

본 글에서 소개하는 패턴은 '엔터프라이즈 어플리케이션 아키텍처 패턴' (마틴 파울러 저)에 소개된 패턴으로서, 필자는 파울러가 소개한 패턴을 보다 쉽게 이해할 수 있도록 재구성한 것이다.

테이블 모듈(Table Module)
은 형태는 앞서 '도메인 로직 패턴 1'에서 살펴봤던 도메인 모델 패턴과 비슷하지만 결정적인 차이점이 존재한다. 그것은 테이블 모듈에서 사용되는 객체는 고유성이 존재하지 않는다는 점이다. 도메인 모델을 EJB로 구현하든 독자적인 형태로 구현하든 각각의 객체는 그 객체만의 고유성을 가지며, 다른 객체와 다른 객체로 인식된다. 하지만, 테이블 모듈에서는 고유 객체라는 개념은 존재하지 않으며 오직 데이터와 관련된 비즈니스 로직을 다루는 객체만 존재한다.

예를 들어, 회원과 관련된 비즈니스 로직을 도메인 모델과 테이블 모듈로 구현한다고 할 경우 다음과 같은 차이를 보이게 된다.

그림 1 - 좌측-도메인모델, 우측-테이블모듈

먼저 좌측의 도메인 모델을 사용할 경우의 코드를 살펴보자. 도메인 모델을 사용하는 경우 회원 가입 처리가 다음과 같은 코드를 통해서 이루어질 것이다.

    Member member = new Member();
    member.setId(id);
    member.setName(name);
    member.regist();

모메인 모델에서는 하나의 객체가 고유한 값을 갖고 있다. 즉, 도메인 모델에 기반한 Member 객체는 그 자체가 고유의 회원 정보를 나타내는 객체이며 regist() 메소드는 오직 고유 정보를 갖고 있는 회원에 대한 처리를 해준다. 회원 탈퇴를 처리하는 코드는 다음과 같은 형태를 띄게 될 것이다.

    Member member = someFindMemberMethod(memberId);
    member.secede();

도메인 모델은 개체(entity)의 고유성에 기반해서 객체를 생성하고 삭제하는 작업을 처리하게 되는데, 테이블 모듈은 생성된 데이터 객체가 고유성을 갖고 있지 않으며 [그림 1]에서와 같이 범용적인 클래스((RecordSet 클래스)에 데이터를 저장한다. 테이블 모듈에서 회원 등록 및 탈퇴 처리는 다음과 같은 코드를 통해서 이루어진다.

    RecordSet newData = new RecordSet();
    newData.set("id", id);
    newData.set("name", name);
    
    MemberTableModule tableModule = MemberTableModule.getInstance();
    tableModule.regist(newData);
    
    tableModule.secede(someId);

위 코드에서 중요한 건 RecordSet은 단지 데이터만 저장하고 있을 뿐 고유성을 갖지는 않는다는 점이다. 즉, 앞서 도메인 모델에 기반한 Member 클래스의 객체는 테이블 상의 하나의 행과 1대 1로 매핑되며, 서로 다른 Member 객체는 서로 다른 테이블의 행과 매핑되는 것이 도메인 모델의 기본 형태인 반면에, 테이블 모듈에서는 서로 다른 RecordSet 객체가 같은 행을 나타낼 수도 있게 된다.

또한, MemberTableModule 클래스는 오직 비즈니스 로직만을 처리한다. 고유성 문제는 객체에 저장되어 있는 특정 필드의 값이나(예를 들어 RecordSet의 "id" 프로퍼티 값) 메소드의 인자로 객체를 판별하는 고유값을 전달받음으로써(MemberTableModule 클래스의 secede() 메소드) 처리한다.

테이블 모듈의 구현 방법

테이블 모듈은 이름에서 알 수 있듯이 테이블 지향적인 구현 패턴이다. 테이블 모듈에 해당하는 클래스는 보통 한개의 인스턴스만 존재하는 싱글톤 패턴을 사용해서 구현하거나 또는 모든 비즈니스 로직 처리 메소드를 static으로 구현한다. (싱글톤 패턴의 구현 방법은 자바캔 기사인 'Singleton 패턴을 적용한 매니저 클래스와 견고한 웹 어플리케이션의 개발'을 참고하기 바란다.) 예를 들어, 싱글톤 패턴을 사용하여 MemberTableModule 클래스를 다음과 같이 구현할 수 있을 것이다.

    public class MemberTableModule {
        private MemberTableModule singleInstance = new MemberTableModule();
        public static MemberTableModule getInstance() {
            return singleInstance;
        }
        
        public void regist(RecoredSet data) {
            // RecordSet으로부터 데이터 읽어와 DB에 저장
            ...
        }
        ...
    }

도메인 모델이 엔티티 객체와 데이터베이스 사이의 매핑을 쉽게 처리하기 위해 데이터베이스 매퍼를 사용하는 것 처럼 테이블 모듈도 데이터베이스와 관련된 CRUD(Create, Read, Update, Delete) 작업을 쉽게 처리하기 위해 테이블 데이터 게이트웨이(Table Data Gate Way)를 사용한다. 테이블 데이터 게이트웨이는 특정한 테이블과 관련된 CRUD 작업을 수행해주는 보조 클래스라고 생각하면 된다. 예를 들어, 테이블 데이터 게이트웨이가 존재할 경우 아래 그림과 같이 메시지가 흘러갈 것이다.

그림2 - 테이블 모듈은 테이블 데이터 게이트웨이와 같은 테이블 CRUD 처리 모듈을 사용하여 데이터베이스 작업을 수행한다.

RecordSet은 테이블의 행의 집합을 나타내는 데이터로서 JDBC API의 ResultSet과 같은 구조를 갖는다. ResultSet 자체를 RecordSet으로 사용할 수도 있지만, 그것보다는 ResultSet으로부터 행 데이터를 읽어와 표처럼 값을 저장하는 RecordSet 클래스를 따로 구현하는 것이 좋다. 예를 들어, 테이블 데이터 게이트웨이에서는 다음과 같이 ResultSet으로부터 데이터를 읽어와 RecordSet에 저장하도록 코드를 작성할 수 있다.

    Statement stmt = null;
    ResultSet rs = null;
    
    try {
        ...
        rs = stmt.executeQuery(...);
        RecordSet recordSet = new RecordSet();
        
        while(rs.next()) {
            recordSet.newRecord(); // RecordSet에 새로운 레코드 생성
            recordSet.set("id", rs.getString("ID"));
            recordSet.set("name", rs.getString("NAME"));
        }
        return recordSet;
    } finally {
        ...
    }

테이블 모듈의 장단점

테이블 모듈의 장점은 도메인 모델 만큼 복잡하지 않다는 것이다. 고유성을 갖고 있는 객체를 사용하지 않기 때문에 객체의 상태를 관리할 필요가 없으며, 데이터를 참조할 때에만 RecordSet 객체를 생성해서 사용하면 된다.

테이블 모듈 안에서 또 다른 테이블 모듈을 호출할 수도 있기 때문에 손쉽게 하나의 비즈니스 로직 처리 메소드에서 여러 테이블에 쉽게 접근할 수 있다는 장점도 있다.

하지만 테이블 모듈은 테이블 중심의 구현 기법이기 때문에 객체 지향적인 기법을 사용하는 데에 제약이 따른다. 또한, 테이블 모듈이 생성하는 RecordSet은 비즈니스 영역의 특정한 개체(entity)와 매핑되지 않기 때문에 현실 비즈니스 영역의 개념을 테이블 모듈에 투영시키기 어렵다. 예를 들어, 도메인 모델에 기반해서 구현한 Member 객체는 회원 정보를 의미한다는 것을 알 수 있지만, 테이블 모듈의 RecordSet은 어떤 테이블 모듈 클래스로부터 생성된 RecordSet인지 분석해야만 RecordSet이 저장하고 있는 데이터의 의미를 파악할 수 있다.

RecordSet이 저장하는 데이터의 의미를 파악하는 작업을 좀더 쉽게 하기 위해서, 필자의 경우 테이블 모듈과 RecordSet 대신에 해당 데이터를 나타내는 자바빈 객체를 사용하는 방법을 권하곤 한다. 그 방법은 다음과 같다.

  • 테이블 모듈과 테이블 데이터 게이트웨이 사이에서는 RecordSet을 사용한다.
  • 테이블 모듈이 제공하는 비즈니스 로직 메소드는 도메인 영역의 개체를 나타내는 자바빈 객체를 사용하여 데이터를 주고 받는다.
예를 들어, 앞서 예제로 봤던 MemberTableModule의 regist() 메소드를 다음과 같이 구현할 수 있다.

    public class MemberTableModule {
        ...
        
        private MemberTableGateWay gateway = ...;
        
        // MemberBean은 비즈니스 도메인 영역의 회원 개체의 프로퍼티만 포함하는
        // 자바빈 객체로서 회원과 관련된 비즈니스 로직은 포함하고 있지 않다.
        public void regist(MemberBean mb) {
            // MemberBean으로부터 RecordSet 생성
            RecordSet recordSet = new RecordSet();
            recordSet.newRecord();
            recordSet.set("id", mb.getId());
            recordSet.set("name", mb.getName());
            
            // 테이블 데이터 게이트웨이 사이에서는
            // RecordSet을 사용
            gateway.regist(recordSet);        }
    }

테이블 모듈은 굉장히 유용한 패턴!

필자는 테이블 모듈과 비슷한 형태의 구성을 좋아하며, 실제 개발시에도 테이블 모듈과 비슷한 형태로 구현하는 경우가 많은 편이다. 완전한 도메인 모델의 경우는 개체의 고유성 개념이 존재하기 때문에, 고유성을 유지하는 데에 복잡한 코드가 필요하다.(EJB의 경우는 엔티티 빈으로 작성한다.) 하지만 웹 어플리케이션은 필요한 순간에만 데이터가 사용되는 경우가 많으며 객체의 고유성이 중요하지 않은 경우가 많다. 예를 들어, 게시판의 경우 글을 볼 때에만 데이터가 필요하며 그 데이터를 표현하는 객체를 여러번 생성한다 해도 문제가 되지 않는다. 따라서 객체의 고유성이 반드시 필요한 경우가 아니라면 테이블 모듈을 사용하는 것이 여러면에서 편리하다.

서비스 레이어


서비스 레이어(Service Layer)는 서비스를 제공하는 레이어를 별도로 제공하는 구조를 갖는다. 서비스 레이어는 클라이언트들이 공통적으로 필요로 하는 기능을 제공해야 할 때 알맞은 구조이며, 어플리케이션 로직을 담기에 적당한 곳이다.

그림 3 - 서비스 레이어는 서비스를 통합해서 제공한다.

위 그림은 서비스 레이어의 전형적인 구조를 보여주고 있다. 최상단 레이어에 위치한 것들은 서비스(어플리케이션)를 사용하는 클라이언트들이다. 하단 레이어는 비즈니스 로직을 구현하는 데 필요한 도메인 관련 컴포넌트 및 부대 서비스를 제공하는 모듈을 제공한다. 서비스 레이어는 하단 레이어에 있는 컴포넌트와 모듈을 사용하여 상단 레이어에 있는 클라이언트가 필요로 하는 서비스를 제공하게 된다.

즉, 서비스 레이어는 하단 레이어의 컴포넌트 및 서비스를 사용하여 클라이언트가 요구하는 비즈니스 로직을 수행하는 것이다.

서비스 레이어에는 주로 어플리케이션 로직이 위치한다. 서비스 클라이언트(즉, 위 그림에서 UI나 Gateway, Connector 등)들을 위한 흐름을 제어한다든지, 비즈니스 도메인에서 발생한 이벤트를 클라이언트에 전달해준다든지 등의 기능을 수행한다. 실제 업무 영영과 관련된 도메인 로직의 경우는 서비스 레이어의 하단에 위치한 비즈니스 도메인 컴포넌트에서 수행하게 된다.

비즈니스 로직의 두 가지 종류: 도메인 로직과 어플리케이션 로직

비즈니스 로직에는 크게 도메인 로직과 어플리케이션 로직의 두 가지가 존재한다. 먼저 도메인 로직은 문제 영역에 대한 것만을 처리한다. 예를 들어, 회원가입, 회원정보 변경과 같은 로직은 회원관리라는 문제 영역에 속하는 도메인 로직이다. 반면에 '회원가입 처리 후 가입축하 이메일 발송', '약관에 동의한 후 회원가입폼을 출력'과 같이 클라이언트가 따르게 될 워크플로우를 처리하는 것이 어플리케이션 로직에 해당한다.

즉, 서비스 레이어를 선택한 경우 전체적인 구성은 아래 그림과 같아지는 것이다.

그림 4 - 서비스 레이어의 역할은 어플리케이션 로직의 수행인 경우가 많다.

웹 어플리케이션에서 서비스 레이어를 사용하면 전체적인 실행 순서는 아래 그림과 같아진다. 즉, 사용자의 요청이 JSP에 전달되면 JSP는 서비스 레이어가 제공하는 기능을 실행하고, 서비스 레이어에서는 도메인 컴포넌트를 사용해서 클라이언트가 원하는 기능을 수행한 뒤 결과를 JSP에 리턴해준다. 그러면, 서비스 레이어로부터 결과를 전달받은 JSP는 클라이언트에 알맞은 결과 화면을 출력한다.

그림 5 - 서비스 레이어를 사용한 경우의 흐름

서비스 레이어의 구현 방법

웹 어플리케이션에서 서비스 레이어를 구현하기 위해서는 먼저 서비스 레이어가 제공해야 할 기능부터 찾아야 한다. 서비스 레이어가 제공하게 될 기능은 대부분 웹 어플리케이션에서 한번의 클릭시 실행되어야 할 기능과 동일한 구조를 갖게 된다. 예를 들어, '회원 가입', '아이디 중복 검사', '암호 이메일 발송' 등의 기능이 서비스 레이어에서 실행된다.

서비스 레이어 역할을 인터페이스로 추상화시켜주는 것이 좋지만 재사용성이 높지 않은 서비스 레이어라면 클래스로 구현해도 문제가 되지는 않는다. 회원과 관련된 기능을 처리하는 서비스 레이어를 인터페이스로 정의할 경우 다음과 같이 인터페이스를 작성할 수 있을 것이다.

    public interface MemberServiceLayer extends ServiceLayer {
        public void regist(MemberBean data) throws MemberServiceException;
        public void exists(String memberID) throws MemberServiceException;
        public void findPassword(String memberID) throws MemberServiceException;
    }

위 서비스 레이어 인터페이스의 구현체는 다음과 같이 회원 도메인 모델을 구현한 컴포넌트를 사용해서 어플리케이션에 알맞게 서비스 레이어를 구현하게 될 것이다.

그림 6 - 서비스 레이어를 사용한 구현 방식

위 그림에서 MemberComponent는 회원과 관련된 도메인 모델 컴포넌트에 해당하는 것으로서 서비스 레이어를 구현한 MemberServiceImpl은 MemberComponent를 사용하여 클라이언트가 원하는 기능을 수행하게 된다.

도메인 컴포넌트 부분은 앞서 '도메인 로직 패턴 1 - 트랜잭션 스크립트, 도메인 모델'에서 살펴봤던 '도메인 모델'을 사용하거나 또는 본 글의 앞 부분 주제인 '테이블 모듈'을 사용해서 구현할 수 있을 것이다. 서비스 레이어 부분은 '트랜잭션 스크립트'와 같은 방식으로 구현하게 된다.

위 그림에서 MemberServiceFactory는 MemberServiceLayer 인터페이스의 구현체를 생성할 때 사용되는 팩토리 클래스이다. JSP와 같이 서비스 레이어를 필요로 하는 곳에서 MemberServiceFactory 클래스를 사용해서 서비스 레이어를 구하게 된다.

    // MemberServiceFactory.createMemberService() 메소드는
    // MemberServiceImpl 클래스의 인스턴스를 리턴한다.
    MemberServiceLayer memberService = MemberServiceFactory.createMemberService();
    memberService.regist(..);
    ...

서비스 레이어의 장점

서비스 레이어의 장점은 어플리케이션 로직과 도메인 로직을 구분함으로써 각각의 재사용성을 높여준다는 점이다. 어플리케이션 로직을 나타내는 서비스 레이어의 인터페이스는 그와 비슷한 로직을 필요로 하는 다른 어플리케이션에서 재사용될 수 있으며, 도메인 로직만을 포함하고 있는 도메인 컴포넌트 역시 같은 도메인 로직을 필요료 하는 어플리케이션에서 재사용될 수 있을 것이다.

만약 하나의 컴포넌트가 어플리케이션 로직과 도메인 로직을 모두 포함하고 있다면, 이 컴포넌트는 어플리케이션 로직과 도메인 로직을 모두 필요로 하는 경우에 한해서만 재사용이 가능할 것이므로, 서비스 레이어를 통한 두가지 로직의 구분은 매우 큰 장점이 된다.

또한 서비스 레이어를 EJB의 세션빈으로 구현하게 될 경우 EJB 콘테이너를 통해서 트랜잭션을 처리할 수 있다는 장점도 얻을 수 있다.

Posted by 최범균 madvirus

댓글을 달아 주세요

웹 어플리케이션의 비즈니스 로직 처리와 관련된 도메인 로직 패턴인 트랜잭션 스크립트와 도메인 모델에 대해서 살펴본다.

트랜잭션 스크립트

본 글에서 소개하는 패턴은 '엔터프라이즈 애플리케이션 아키텍처 패턴' (마틴 파울러 저)에 소개된 패턴으로서, 필자는 파울러가 소개한 패턴을 보다 쉽게 이해할 수 있도록 재구성한 것이다.

은행의 계좌 이체 서비스를 생각해보면 '잔고 확인->받는 사람 확인->이체 실행->잔고 감소'의 로직 순서가 하나의 로직으로 처리되어야 하고, 이 과정에서 한번이라도 오류가 발생하면 모든 처리가 취소되고, 모든 과정이 성공해야 비로서 처리가 완료된다. 즉, All-or-Nothing 개념의 트랜잭션 처리가 되는 것이다.

트랜잭션 스크립트(Transaction Script) 패턴은 이렇게 하나의 트랜잭션으로 구성된 로직을 단일 함수 또는 단일 스크립트에서 처리하는 구조를 갖는다. 그래서 패턴의 이름이 트랜잭션 스크립트이다. 트랜잭션 스크립트는 JSP를 처음 공부하는 사람들이 가장 먼저 몸에 습득하는 패턴으로서 모델 1 구조가 가장 간단한 트랜잭션 스크립트 패턴에 해당한다.

트랜잭션 스크립트의 구현 방법

트랜잭션 스크립트의 가장 손쉬운 구현방법은 하나의 트랜잭션 처리를 하나의 JSP 코드에서 처리하는 것이다. 예를 들어, 트랜잭션 스크립트 패턴으로 계좌 이체 처리를 하는 JSP를 작성하면 다음과 같은 형태의 코드를 갖게 될 것이다.

    <%
        ...
        try {
        
            SomeTransaction tx = ...;
            tx.begin();
            
            // 1. 잔고확인
            ...
            // 2. 받는 사람 확인
            ...
            // 3. 이체 실행
            ...
            // 4. 잔고 감소
            ...
            tx.commit();
            
        } catch(..) {
            tx.rollback();
            ...
        } finally {
            ...
            ...
        }
    %>

JSP 페이지에 뷰 코드와 로직 코드를 함께 섞는 것보다는 커맨드 패턴(자바캔 컨텐츠 '커맨드(Command) 패턴과 그 구현' 참고)을 사용해서 JSP로부터 로직 코드를 커맨드 처리 클래스로 이동시키기도 한다. 예를 들어, 다음과 같이 하나의 트랜잭션을 처리하는 메소드를 생성하고 JSP에서 이 메소드를 실행하는 형태를 취하면 된다.

    public class SomeTransactionScript {
        public Result do(...) {
            try {
            
                SomeTransaction tx = ...;
                tx.begin();
                
                // 1. 잔고확인
                ...
                // 2. 받는 사람 확인
                ...
                // 3. 이체 실행
                ...
                // 4. 잔고 감소
                ...
                tx.commit();
                
            } catch(..) {
                tx.rollback();
                ...
            } finally {
                ...
                ...
            }
        }
    }

아마도 위와 같이 트랜잭션 스크립트 코드를 클래스로 옮기게 되면 JSP에서는 다음과 같이 클래스의 인스턴스를 생성해서 트랜잭션을 처리하는 메소드를 호출하는 형태의 코드를 사용하게 될 것이다.

    <%
        SomeTransactionScript ts = new SomeTransactionScript();
        Result result = ts.do(...);
    %>
    ...

스트러츠와 같이 모델 부분을 커맨드 패턴으로 처리해주는 프레임워크를 사용하면 보다 표준화된 방법으로 JSP 페이지로부터 로직 처리 코드를 클래스로 옮길 수 있다. 스트러츠는 모델 2 구조를 기반으로 커맨드 패턴을 사용하는데, 모델 2 구조를 기반으로 커맨드 패턴을 구현하는 방법에 대한 기초적인 내용은 'JSP Model 2 Architecture 2부, 커맨드 패턴의 적용 '에서 설명하고 있으니 참고하기 바란다.

트랜잭션 스크립트의 장/단점

트랜잭션 스크립트 방식의 최대 장점은 구현이 매우 쉽다는 것이다. ASP.NET이나 JSP 등이 프로그래밍 언어에 기반하고 있는 스크립트 언어임에도 불구하고 사람들은 ASP나 JSP가 프로그래밍 언어보다 쉽다고 느끼는데, 그 이유는 트랜잭션 스크립트 방식의 구현 방법의 단순함 때문이다. 스트러츠와 같이 MVC 패턴과 커맨드 패턴을 함께 사용하더라도 이러한 단순함은 그대로 유지되기 때문에, 스트러츠 코드에 조금만 익숙해지면 손쉽게 스트러츠로도 트랜잭션 스크립트 패턴을 구현할 수 있게 된다.

하지만, 트랜잭션 스크립트로 구성된 어플리케이션은 비즈니스 로직이 복잡해질수록 난잡한 코드를 만들게 된다. 특히, 트랜잭션 스크립트 코드는 애초에 도메인에 대한 분석/설계 개념이 약하기 때문에 코드의 중복 발생을 막기 어려워진다. 또한, 트랜잭션 스크립트를 사용하게 되면 쉬운 개발에 익숙해지기 때문에 공통된 코드를 공통 모듈로 분리하지 않고 복사&붙이기 방식으로 중복된 코드를 만드는 유혹에 빠지기 쉽다.

요즘은 트랜잭션 스크립트 방식의 단점만 많이 부각되고 장점은 부각되지 않는 경우가 많은데, 이는 잘못된 현상이라 생각한다. 트랜잭션 스크립트 방식의 최대 장점이라면, 트랜잭션 스크립트 패턴이 이미 많은 개발자들의 몸에 익은 (최상은 아니지만) 최적의 개발 방법중의 하나라는 것이다. 또한, 얼마나 모듈화를 잘 하느냐에 따라서 트랜잭션 스크립트 만으로도 높은 효율을 낼 수 있기 때문에 트랜잭션 스크립트 패턴은 결코 무시할 수 없는 패턴이다.

도메인 모델

도메인 모델(Domain Model)은 흔히 말하는 객체 지향 분석 설계에 기반해서 구현하고자 하는 도메인(비즈니스 영역)의 모델을 생성하는 패턴이다. 도메인 모델은 비즈니스 영역에서 사용되는 객체를 판별하고, 객체가 제공해야 할 목록을 추출하며, 각 객체간의 관계를 정립하는 과정을 거친다. 명사와 동사를 구분해서 명사로부터 객체를 추출해내고, 동사로부터 객체의 기능 및 객체 사이의 관계를 유추해낸다.

객체를 기반으로 하는 도메인 모델의 주요 특징은 데이터와 프로세스가 혼합되어 있다는 것이며, 객체 간의 복잡한 연관 관계를 갖고 있고, 상속 등을 통해서 객체의 기능과 역할을 확장할 수 있다.

현업에서는 보통 두 가지 형태의 도메인 모델이 사용된다. 첫번째는 단순한 형태로서 데이터베이스 테이블 하나당 하나의 도메인 객체를 가지는 구조를 갖는 방식이다. 이런 모델에서는 하나의 테이블에 해당하는 하나의 자바빈 객체가 존재하는 것이 보통이며, 자바빈 객체에 프로세스가 함께 포함되기도 하고 프로세스를 처리하는 객체를 따로 생성하기도 한다. EJB의 엔티티 빈은 데이터베이스트의 테이블과 1대 1로 매핑되며 세션빈은 프로세스를 처리하기 때문에 단순한 형태의 모델에 가장 알맞은 구현 방법이라고 볼 수 있다.

현업에서 사용되는 도메인 모델의 두번째 형태는 상속, 다양한 패턴, 그리고 각각의 객체가 서로 연결된 복잡한 그물망을 갖는 구조이다. 단순 도메인 모델은 데이터베이스 모델링과 비슷한 구조의 객체가 사용되지만, 풍부한 도메인 모델은 '객체지향분석설계' 관련 서적에서 나오는 방식에 의해 객체를 판별하고 객체 사이의 관계를 유추하는 등의 작업을 통해서 객체를 판별하기 때문에 데이터베이스와의 매핑이 쉽지많은 않다. 따라서 풍부한 도메인 모델을 사용하는 경우에는 도메인 모델과 데이터베이스 사이의 매핑을 처리할 수 있는 보조 유틸리티를 사용하는 것이 좋다.

모데인 모델의 구현 방법

도메인 모델을 사용할 때 최대 관건은 도메인 모델과 데이터베이스 테이블 사이의 매핑을 어떻게 처리할 것인가 하는 것이다. 웹 어플리케이션의 (대부분의) 비즈니스 로직은 데이터 삽입, 데이터 조회, 데이터 변경 그리고 데이터 삭제와 관련되어 있기 때문에, 비즈니스 로직을 자바 객체로 표현한 도메인 모델에 알맞은 데이터베이스 매핑 패턴을 사용하는 것이 좋다. (데이터 매퍼를 사용함으로써 데이터베이스로부터 도메인 모델을 독립적으로 유지할 수 있으며, 유지보수도 간단하게 만들 수 있다.)

도메인 모델을 구현하기 위해서는 먼저 객체 지향 개념에 기반한 클래스 설계 기법을 몸에 익혀야 한다. 요즘 나오는 책들은 UML을 기반으로 설명하고 있지만, 꼭 UML 기반이 아니어도 상관없다. 객체 지향 개념을 익히고 싶다면 다음의 책들을 참고하기 바란다.

객체 지향 설계와 더불어 익혀야 할 것은 도메인 모델과 테이블 사이의 매핑을 처리해주는 기법이다. 이 매핑 처리 기법은 '단일객체-단일테이블'의 1-1 매핑에서 '다수객체-다수테이블'의 n-n 매핑까지 다양하게 존재하며, 또한 데이터베이스 매핑을 지원해주는 API도 상업용에서 오픈소스까지 다양하게 존재한다. 오브젝트와 데이터베이스 사이의 매핑에 대한 정보는 아래의 사이트에서 구할 수 있을 것이다.

아직까지 널리 사용되는 도메인 모델은 EJB의 엔티티빈과 세션빈과 같은 단순 도메인 모델이다. 즉, 테이블과 1대 1로 매핑되는 엔티티가 존재하고 엔티티를 사용해서 로직을 수행하는 세션이 존재하는 단순 도메인 모델이 대세를 이루고 있다. 특히 EJB의 경우 CMP와 EJB QL(Query Language)을 통해서 데이터베이스 매핑을 처리할 수 있기 때문에 도메인 모델의 비즈니스 로직에 집중할 수 있도록 도와주는 장점이 있다.

도메인 모델의 장/단점

도메인 모델의 장점은 역시 객체 지향에 기반한 재사용성, 확장성, 그리고 유지 보수의 편리함에 있다. 일단 도메인 모델을 구축하고나면 (필요에 따라 약간의 수정이 필요하겠지만) 언제든지 재사용할 수 있다. 예를 들어, 한번 커뮤니티(클럽, 게시판 등) 도메인 모델을 구축하고 나면 비슷한 커뮤니티 시스템에 대해서 도메인 모델을 재사용할 수 있게 된다. 또한, 상속/인터페이스, 더 나아가 컴포넌트 개념을 바탕으로 도메인 모델을 개발하게 되면 무한한 확장성을 갖게 된다.

도메인 모델의 단점은 하나의 도메인 모델을 구축하는 데 많은 노력이 필요하다는 것이다. 객체를 판별해내야 하고 객체들 간의 관계를 정립해야 하며, 더 나아가 객체와 데이터베이스 사이의 매핑에 대해서 고민해야 하기 때문이다. 이는 이론과 경험을 함께 겸비하지 않으면 쉽게 풀수 없는 문제이기 때문에, 도메인 모델에 능숙한 개발자가 팀에 없을 경우 도메인 모델을 구축하는 것 자체가 힘들어질 수도 있다.

따라서 사용 기간이 1-6개월에 해당하는 어플리케이션이나 단순한 비즈니스 로직만 필요한 경우에는 도메인 모델을 사용하는 것이 오히려 개발 일정에 부담을 줄 수 있으므로, 프로젝트의 규모, 비즈니스 로직의 복잡함, 개발할 어플리케이션의 사용 기간 등을 고려해서 도메인 모델의 사용여부를 결정하는 것이 좋다.

도메인 모델의 예

트랜잭션 스크립트 방식과 도메인 모델의 방식을 비교하기 위해 게시판 글 쓰기 예를 사용해보도록 하겠다. 요즘 인터넷 게시판은 글을 쓰거나 내 글에 답글이 달렸을 경우 이를 이메일을 통해서 통지하는 기능을 제공하고 있다. 트랜잭션 스크립트 방식을 사용하게 되면 다음과 같은 형태로 JSP 코드를 작성할 것이다.

    <%
        ...
        try {
        
            SomeTransaction tx = ...;
            tx.begin();
            ...
            DB에 데이터 기록
            ...
            tx.commit();
            
            // 성공하면 이메일 발송
            ...
            
        } catch(..) {
            tx.rollback();
            ...
        } finally {
            ...
            ...
        }
    %>

만약 글을 쓸 때마다 게시판 목록을 미리 캐싱해두는 작업을 수행해야 한다면 다음과 같이 위 코드에 캐싱 처리 코드가 추가될 것이다.

    <%
        ...
        try {
        
            SomeTransaction tx = ...;
            tx.begin();
            ...
            DB에 데이터 기록
            ...
            tx.commit();
            
            // 성공하면 이메일 발송
            ...
            // 캐시 생성 코드
            ...
        } catch(..) {
            tx.rollback();
            ...
        } finally {
            ...
            ...
        }
    %>

도메인 모델을 사용할 경우에는 트랜잭션 스크립트 방식과 달리 아래 그림과 같은 도메인 관련 객체를 분석하고 관계를 맺어주게 된다.

[도메인 모델 예]

위 클래스 다이어그램은 게시판에 글을 쓸 경우 이벤트가 발생하여 등록된 이벤트 리스너가 실행되는 모델을 보여주고 있다. 이 도메인 모델을 사용하게 되면 먼저 다음과 같이 이벤트를 처리할 리스너를 등록하게 된다.

    Board someBoard = ...;
    someBoard.registerBoardListener(new EmailNoticer());
    someBoard.registerBoardListener(new BoardCacheMaker());

이후 게시판 글쓰기를 처리하는 코드에서는 다음과 같이 글쓰기만 처리하면 된다.

    <%
        Board someBoard = ...;
        BoardRow row = ...;
        someBoard.write(row);
    %>

글쓰기 과정에서 새로운 처리가 추가되는 경우 BoardListener 인터페이스를 구현한 이벤트 리스너 클래스를 작성한 뒤 registerBoardListener() 메소드를 사용해서 이벤트 리스너로 등록시켜주면 된다. 트랜잭션 스크립트 방식이었다면 계속해서 글쓰기 부분에 코드가 추가되었을 것이다. 만약 글삭제/글수정할 때에도 똑같은 형태로 메일을 발송하고 캐시를 생성해야 하는 경우, 트랜잭션 스크립트 방식에서는 소스 코드를 그대로 복사하는 방법을 주로 택하지만(그래서 로직 처 코드마다 방식이 달라질 수도 있지만), 도메인 모델에서는 Board 클래스만 변경해주면 되므로 표준화된 처리 방식을 그대로 유지할 수 있게 된다.

이런 유지보수의 장점 뿐만 아니라 도메인 모델을 조금만 변경하면 다른 게시판 시스템에서 사용할 수 있기 때문에, 재사용을 통한 빠른 개발도 이끌어낼 수 있다.

도메인 모델에서 엔티티를 표현하는 객체는 고유성을 갖는다!

도메인 모델에는 하나의 개체(Entity)를 하나의 객체로 표현한다. 예를 들어, EJB에서 하나의 엔티티 빈 객체는 테이블 상의 하나의 행을 나타낸다. 또한, 서로 다른 엔티티 빈 객체는 테이블의 서로 다른 행에 해당한다. 즉, 각각의 엔티티 빈은 서로 다른 개체를 나타내는 고유 객체인 것이다.

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 햄과함께 2018.02.27 08:29 신고  댓글주소  수정/삭제  댓글쓰기

    덕분에 이해하고 갑니다. 감사합니다. :)

DBCP API를 이용하여 커넥션 풀을 사용하는 방법에 대해서 살펴본다.

커넥션 풀과 자카르타 DBCP API

DBCP API는 커넥션 풀 기능을 제공하는 API로서 자카르타의 또 다른 프로젝트인 Pool API에 기반하고 있다. DBCP API는 사용방법이 비교적 쉬우며, 파일을 통해서 커넥션 풀을 설정할 수 있고 또한 프로그램에서 직접 커넥션 풀을 설정할 수 있기 때문에 커넥션 풀을 사용하려는 개발자에게 매우 유용한 API이다.

커넥션 풀이란?

커넥션 풀에 대한 개념이 없는 사람을 위해 DBCP API를 이용한 커넥션 풀에 대해서 살펴보기 전에 간단하게 커넥션 풀이 무엇인지에 대해서 살펴보도록 하자. 커넥션 풀 기법이란 데이터베이스와 연결된 커넥션을 미리 만들어서 풀(pool)이란 저장소에 저장해 두고 있다가 필요할 때에 커넥션을 풀에서 가져다 쓰고 다시 풀에 반환하는 기법을 의미한다


커넥션 풀 기법에서는 위 그림과 같이 풀 속에 데이터베이스와 연결된 커넥션을 미리생성해놓고 있는다. 데이터베이스 커넥션이 필요할 경우, 커넥션을 새로 생성하는 것이 아니라 풀 속에 미리 생성되어 있는 커넥션을 가져다가 사용하게 된다. 다 사용한 커넥션은 다시 풀에 반환한다. 풀에 반환된 커넥션은 다음에 다시 사용된다.

커넥션 풀의 특징은 다음과 같다.

  • 풀 속에 미리 커넥션이 생성되어 있기 때문에 커넥션을 생성하는 데 드는 연결 시간이 소비되지 않는다.
  • 커넥션을 계속해서 재사용하기 때문에 생성되는 커넥션 수가 많지 않다.
커넥션을 생성하고 닫는 데 필요한 시간이 소모되지 않기 때문에 그 만큼 어플리케이션의 실행 속도가 빨라지며, 또한 한번에 생성될 수 있는 커넥션 수를 제어하기 때문에 동시 접속자수가 몰려도 웹 어플리케이션이 쉽게 다운되지 않는다.

커넥션 풀을 사용하면 전체적인 웹 어플리케이션의 성능 및 처리량이 높아지기 때문에 많은 웹 어플리케이션에서 커넥션 풀을 기본으로 사용하고 있다.

DBCP API의 사용방법

자카르타 프로젝트의 DBCP API를 사용할 때에는 다음과 같은 과정을 거치면 된다.

  1. DBCP 관련 Jar 파일 및 JDBC 드라이버 Jar 파일 설치하기
  2. 커넥션 풀 관련 설정 파일 초기화하기
  3. 커넥션 풀 관련 드라이버 로딩하기
  4. 커넥션 풀로부터 커넥션 사용하기
이 네 가지 절차에 대해서 차례대로 살펴보도록 하자.

필요한 Jar 파일 복사

DBCP API를 사용하기 위해서는 다음과 같은 라이브러리가 필요하다.

  • DBCP API 관련 Jar 파일
  • DBCP API가 사용하는 자카르타 Pool API의 Jar 파일
  • Pool API가 사용하는 자카르타 Collection API의 Jar 파일
이들 라이브러리의 최신 버전은 http://jakarta.apache.org/site/binindex.cgi 에서 다운로드 받을 수 있으며, 이 글에서는 다음 버전을 사용하여 예제를 작성하였다.

  • DBCP 1.2.1 - commons-dbcp-1.2.1.zip
  • Pool 1.2 - commons-pool-1.2.zip
  • Collection 3.1 - commons-collections-3.1.zip
이들 파일의 압축을 풀면 다음과 같은 Jar 파일들을 발견할 수 있는데, 이들 Jar 파일들을 사용하면 된다.

  • commons-dbcp-1.2.1.jar
  • commons-pool-1.2.jar, commons-collections-3.1.jar
예제로 제공되는 파일에는 pool\WEB-INF\lib 폴더에는 이미 이들 Jar 파일들이 포함되어 있으므로 별도로 복사하지 않더라도 DBCP를 사용하는 본 장의 예제들을 실행할 수 있게 된다.

커넥션 풀 설정 파일 작성하기

DBCP를 사용하는 방법에는 소스 코드 상에서 커넥션 풀을 설정하는 방법과 설정 파일을 통해서 커넥션 풀을 설정하는 방법 두가지 존재하는데 본 장에서는 설정 파일을 이용한 커넥션 풀 설정 방법에 대해서 살펴보도록 하겠다.

DBCP Pool API에서 사용되는 커넥션 풀 설정 파일의 기본 골격은 아래 코드와 같다.

    
    파일명: pool\WEB-INF\classes\pool1.jocl    
    <object class="org.apache.commons.dbcp.PoolableConnectionFactory"
        xmlns="http://apache.org/xml/xmlns/jakarta/commons/jocl">
      <object class="org.apache.commons.dbcp.DriverManagerConnectionFactory">
        <string value="jdbc:mysql://localhost:3306/.." />
        <string value="jspexam" />
        <string value="jspex" />
      </object>      
      <object class="org.apache.commons.pool.impl.GenericObjectPool">
        <object class="org.apache.commons.pool.PoolableObjectFactory" null="true" />
      </object>
      
      <object class="org.apache.commons.pool.impl.GenericKeyedObjectPoolFactory"
              null="true" />
            <string null="true" />
            <boolean value="false" />
            <boolean value="true" />
    </object>

위 코드에서 나머지 부분은 그대로 입력하고 다음 부분만 알맞게 변경하면 된다.

    <object class="org.apache.commons.dbcp.DriverManagerConnectionFactory">
      <string value="jdbc:mysql://localhost:3306/..." />
      <string value="jspexam" />
      <string value="jspex" />
    </object>

위 코드에는 세 개의 <string> 태그가 사용되는데, 이들 태그는 각각 순서대로 JDBC URL, 데이터베이스 사용자 계정, 암호를 나타낸다.

설정 파일의 위치

DBCP API는 클래스패스로부터 설정 파일을 읽어온다. 따라서 앞서 작성한 커넥션 풀 설정 파일은 클래스패스에 위치해 있어야 한다. 웹 어플리케이션에서 DBCP API와 관련된 설정 파일의 위치로 가장 좋은 곳은 WEB-INF\classes 폴더이다. 본 글의 예제에서 사용하는 커넥션 풀 설정 파일은 모두 WEB-INF\classes 폴더에 위치시켰다.

커넥션 풀 초기화

DBCP API를 통해서 커넥션 풀을 사용하기 위해서는 커넥션 풀과 관련된 JDBC 드라이버를 로딩해주어야 한다. DBCP API를 사용할 때에 로딩해주어야 할 JDBC 드라이버는 다음과 같다.

  • org.apache.commons.dbcp.PoolingDriver - DBCP API의 JDBC 드라이버
  • DBMS에 연결할 때 사용될 JDBC 드라이버
웹 어플리케이션 시작할 때 위에서 언급한 두 가지 형태의 JDBC 드라이버를 로딩하도록 하면 편리할 것이다. 웹 어플리케이션이 시작할 때 자동으로 시작되는 JDBC 드라이버를 로딩하도록 구현한 서블릿 클래스는 다음 코드와 같다.

    
    파일명: pool\WEB-INF\src\madvirus.jdbcdriver\DBCPInit.java    
    package madvirus.jdbcdriver;
    
    import javax.servlet.http.HttpServlet;
    import javax.servlet.ServletConfig;
    import javax.servlet.ServletException;
    import java.util.StringTokenizer;
    
    public class DBCPInit extends HttpServlet {
    
        public void init(ServletConfig config) throws ServletException {
            try {
                String drivers = config.getInitParameter("jdbcdriver");
                StringTokenizer st = new StringTokenizer(drivers, ",");
                while (st.hasMoreTokens()) {
                    String jdbcDriver = st.nextToken();
                    Class.forName(jdbcDriver);
                }
                
                Class.forName("org.apache.commons.dbcp.PoolingDriver");                
                System.setProperty("org.xml.sax.drvier",
                       "org.apache.crimson.parser.XMLReaderImpl");
            } catch(Exception ex) {
                throw new ServletException(ex);
            }
        }
    }

DBCPInit 서블릿은 "jdbcdriver" 초기화 파라미터로부터 로딩할 JDBC 드라이버를 입력받아 JDBC 드라이버를 차례대로 로딩한다. 그런 후, DBCP API의 JDBC 드라이버인 PoolingDriver 을 로딩한다. 마지막으로 설정 파일을 분석할 때 사용할 XML 파서를 지정한다. 위 코드는 Sun 사에서 배포한 JDK 1.4를 기준으로 XML 파서를 지정하였는데, 만약 다른 XML 파서를 사용한다면 알맞게 변경해주어야 한다.

WEB-INF\web.xml 파일에 DBCPInit 서블릿 클래스에 대한 설정 정보를 추가함으로써 웹 어플리케이션이 시작될 때 DBCPInit 서블릿 클래스가 시작될 수 있도록 할 수 있다. 예를 들면, 아래와 같은 코드를 web.xml 파일에 추가해주면 된다.

  <servlet>
     <servlet-name>DBCPInit</servlet-name>
     <servlet-class>madvirus.jdbcdriver.DBCPInit</servlet-class>
     <load-on-startup>1</load-on-startup>
     <init-param>
        <param-name>jdbcdriver</param-name>
        <param-value>com.mysql.jdbc.Driver</param-value>
     </init-param>
  </servlet>

위와 같이 코드를 web.xml 파일에 추가해주면 웹 어플리케이션이 시작할 때 DBCPInit 서블릿 클래스가 자동으로 시작되고 init() 메소드가 호출된다.

커넥션 풀로부터 커넥션 사용하기

커넥션 풀을 위한 JDBC 드라이버 및 DBMS에 연결할 때 사용할 JDBC 드라이버를 로딩하면 커넥션 풀로부터 커넥션을 가져와 사용할 수 있다. 커넥션 풀로부터 커넥션을 가져오는 코드는 별반 다르지 않으며, 다음과 같은 형태의 코드를 사용하면 된다.

    Connection conn = null;
    ....
    try {
        String jdbcDriver = "jdbc:apache:commons:dbcp:/pool1";
        conn = DriverManager.getConnection(jdbcDriver);
        ...
    } finally {
        ...
        if (conn != null) try { conn.close(); } catch(SQLException ex) {}
    }

위 코드를 보면 DBCP API 기반의 커넥션 풀을 사용한다고 해서 특별히 코드가 달라지는 부분이 없다는 것을 알 수 있다. 일반 경우와 마찬가지로 DriverManager.getConnection() 메소드를 사용해서 커넥션을 구해오고, 커넥션을 다 사용하면 close() 메소드를 사용하여 사용한 커넥션을 닫는다. 차이점이라면 JDBC URL이 다음과 같은 형태를 띈다는 점이다.

    jdbc:apache:commons:dbcp:/[풀이름]

[풀이름]은 여러 개의 커넥션 풀 중에서 사용할 커넥션 풀의 이름을 나타내는 것으로서 커넥션 풀 설정 파일에서 확장자를 제외한 나머지 이름을 [풀이름]으로 사용한다. 예를 들어, 앞서 작성했었던 pool1.jocl 파일이 설정한 커넥션 풀을 사용하고 싶다면 다음과 같은 JDBC URL을 사용한다.

    jdbc:apache:commons:dbcp:/pool1

실제로 커넥션 풀을 사용하는 완전한 예제는 다음 코드와 같다. (아래 코드를 여러분의 환경에 알맞게 변형시켜서 실행하기 바란다.)

    
    파일명: pool\usePool1.jsp    
    <%@ page contentType = "text/html; charset=euc-kr" %>
    
    <%@ page import = "java.sql.DriverManager" %>
    <%@ page import = "java.sql.Connection" %>
    <%@ page import = "java.sql.Statement" %>
    <%@ page import = "java.sql.ResultSet" %>
    <%@ page import = "java.sql.SQLException" %>
    
    <html>
    <head><title>회원 목록</title></head>
    <body>
    
    MEMBMER 테이블의 내용
    <table width="100%" border="1">
    <tr>
        <td>이름</td><td>아이디</td><td>이메일</td>
    </tr>
    <%
        
        Connection conn = null;
        Statement stmt = null;
        ResultSet rs = null;
        
        try {
            String jdbcDriver = "jdbc:apache:commons:dbcp:/pool1";
            String query = "select * from MEMBER order by MEMBERID";
            conn = DriverManager.getConnection(jdbcDriver);
            stmt = conn.createStatement();
            rs = stmt.executeQuery(query);
            while(rs.next()) {
    %>
    <tr>
        <td><%= rs.getString("NAME") %></td>
        <td><%= rs.getString("MEMBERID") %></td>
        <td><%= rs.getString("EMAIL") %></td>
    </tr>
    <%
            }
        } finally {
            if (rs != null) try { rs.close(); } catch(SQLException ex) {}
            if (stmt != null) try { stmt.close(); } catch(SQLException ex) {}
            if (conn != null) try { conn.close(); } catch(SQLException ex) {}
        }
    %>
    </table>
    
    </body>
    </html>

위 코드에서 커넥션 풀에서 구한 Connection의 close() 메소드를 호출하면, 커넥션이 닫히는 것이 아니라 커넥션 풀로 반환된다. 이렇게 커넥션 풀에 커넥션을 반환하는 메소드를 close()로 지정한 이유는 기존의 코드를 최소한으로 변경하는 범위 내에서 커넥션 풀을 사용할 수 있도록 하기 위함이다. 물론, JDBC 프로그래밍의 코딩 형태를 동일하게 유지하기 위한 것도 close() 메소드를 사용하는 이유이다.

커넥션 풀 속성 지정하기

앞에서 살펴본 커넥션 풀 설정 파일인 pool1.jocl은 커넥션 풀과 관련된 속성을 지정하지 않고 있다. DBCP의 커넥션 풀은 최대 커넥션 개수, 최소 유휴 커넥션 개수, 최대 유휴 커넥션 개수, 유휴 커넥션 검사 여부 등의 속성을 지정할 수 있다. pool1.jocl을 보면 다음과 같은 코드가 있는데,

  <object class="org.apache.commons.pool.impl.GenericObjectPool">
    <object class="org.apache.commons.pool.PoolableObjectFactory" null="true" />
  </object>

이 코드에 커넥션 풀과 관련된 속성 정보를 추가하면 된다. 예를 들면, 아래 코드와 같이 커넥션 풀 속성 정보를 추가하면 된다.

    
    파일명: pool\WEB-INF\classes\pool2.jocl    
    <object class="org.apache.commons.dbcp.PoolableConnectionFactory"
        xmlns="http://apache.org/xml/xmlns/jakarta/commons/jocl">
    
      <object class="org.apache.commons.dbcp.DriverManagerConnectionFactory">
        <string value="jdbc:mysql://localhost:3306/chap11?..." />
        <string value="jspexam" />
        <string value="jspex" />
      </object>
      
      <object class="org.apache.commons.pool.impl.GenericObjectPool">
        <object class="org.apache.commons.pool.PoolableObjectFactory" null="true" />
        <int value="10" />  <!-- maxActive -->
        <byte value="1" />  <!-- whenExhaustedAction -->
        <long value="10000" /> <!-- maxWait -->
        <int value="10" /> <!-- maxIdle -->
        <int value="3" /> <!-- minIdle -->
        <boolean value="true" /> <!-- testOnBorrow -->
        <boolean value="true" /> <!-- testOnReturn -->
        <long value="600000" /> <!-- timeBetweenEvctionRunsMillis -->
        <int value="5" /> <!-- numTestsPerEvictionRun -->
        <long value="3600000" /> <!-- minEvictableIdleTimeMillis -->
        <boolean value="true" /> <!-- testWhileIdle -->
      </object>      
      <object class="org.apache.commons.pool.impl.GenericKeyedObjectPoolFactory"
          null="true" />
      
      <string null="true" />
      
      <boolean value="false" />
      
      <boolean value="true" />
    </object>

굵게 표시한 부분이 커넥션 풀의 속성과 관련된 부분인데, 각 속성의 값이 무엇을 의미하는 지 우측에 주석으로 표시하였다. 각 속성이 의미하는 것은 다음표와 같다.

속성 설명
maxActive 커넥션 풀이 제공할 최대 커넥션 개수
whenExhaustedAction 커넥션 풀에서 가져올 수 있는 커넥션이 없을 때 어떻게 동작할지를 지정한다. 1일 경우 maxWait 속성에서 지정한 시간만큼 커넥션을 구할 때 까지 기다리며, 0일 경우 에러를 발생시킨다. 2일 경우에는 일시적으로 커넥션을 생성해서 사용한다.
maxWait whenExhaustedAction 속성의 값이 1일 때 사용되는 대기 시간. 단위는 1/1000초이며, 0 보다 작을 경우 무한히 대기한다.
maxIdle 사용되지 않고 풀에 저장될 수 있는 최대 커넥션 개수. 음수일 경우 제한이 없다.
minIdle 사용되지 않고 풀에 저장될 수 있는 최소 커넥션 개수.
testOnBorrow true일 경우 커넥션 풀에서 커넥션을 가져올 때 커넥션이 유효한지의 여부를 검사한다.
testOnReturn true일 경우 커넥션 풀에 커넥션을 반환할 때 커넥션이 유효한지의 여부를 검사한다.
timeBetweenEvctionRunsMillis 사용되지 않은 커넥션을 추출하는 쓰레드의 실행 주기를 지정한다. 양수가 아닐 경우 실행되지 않는다. 단위는 1/1000 초이다.
numTestsPerEvictionRun 사용되지 않는 커넥션을 몇 개 검사할지 지정한다.
minEvictableIdleTimeMillis 사용되지 않는 커넥션을 추출할 때 이 속성에서 지정한 시간 이상 비활성화 상태인 커넥션만 추출한다. 양수가 아닌 경우 비활성화된 시간으로는 풀에서 제거되지 않는다. 시간 단위는 1/1000초이다.
testWhileIdle true일 경우 비활성화 커넥션을 추출할 때 커넥션이 유효한지의 여부를 검사해서 유효하지 않은 커넥션은 풀에서 제거한다.

몇몇 속성은 성능에 중요한 영향을 미치기 때문에 웹 어플리케이션의 사용량에 따라서 알맞게 지정해주어야 하는데, 다음과 같이 고려해서 각 속성의 값을 지정하는 것이 좋다.

  • maxActive - 사이트의 최대 커넥션 사용량을 기준으로 지정. 동시 접속자수에 따라서 지정한다.
  • minIdle - 사용되지 않는 커넥션의 최소 개수를 0으로 지정하게 되면 풀에 저장된 커넥션의 개수가 0이 될 수 있으며, 이 경우 커넥션이 필요할 때 다시 커넥션을 생성하게 된다. 따라서 커넥션의 최소 개수는 5개 정도로 지정해두는 것이 좋다.
  • timeBetweenEvctionRunsMillis - 이 값을 알맞게 지정해서 사용되지 않는 커넥션을 풀에서 제거하는 것이 좋다. 커넥션의 동시 사용량은 보통 새벽에 최저이며 낮 시간대에 최대에 이르게 되는데 이 두 시간대에 필요한 커넥션의 개수 차이는 수십개에 이르게 된다. 이때 최대 상태에 접어들었더가 최소 상태로 가게 되면 풀에서 사용되지 않는 커넥션의 개수가 점차 증가하게 된다. 따라서 사용되지 않는 커넥션은 일정 시간 후에 삭제되도록 하는 것이 좋다. 보통 10~20분 단위로 사용되지 않는 커넥션을 검사하도록 지정하는 것이 좋다.
  • testWhileIdle - 사용되지 않는 커넥션을 검사할 때 유효하지 않은 커넥션은 검사하는 것이 좋다.
관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요

  1. 나이유미 2010.05.19 10:42 신고  댓글주소  수정/삭제  댓글쓰기

    가져갑니다 감사합니다.

  2. MIREA 2013.08.22 19:39 신고  댓글주소  수정/삭제  댓글쓰기

    잘 보고 갑니다.

쿠키를 좀더 쉽게 다룰 수 있도록 해 주는 쿠키 유틸티리 클래스를 작성해본다.

쿠키 유틸리티 클래스 CookieBox 만들기

쿠키는 웹 어플리케이션에서 클라이언트의 정보를 임시로 저장하기 위해 많이 사용된다. 또한, 클라이언트의 상태를 유지할 때 사용되는 세션을 구현하기 위해 쿠키를 사용하기도 한다. 쿠키는 약방의 감초와 같은 존재로서, 쿠키를 사용함으로써 좀더 쉽고 간결한 방법으로 웹 어플리케이션을 구현할 수 있게 되는 경우가 많다.

쿠키가 사용되는 부분은 많은데, 서블릿 API의 쿠키 지원 클래스는 2.4 버전이 나올 때 까지 여전히 빈약하다. 서블릿 API의 javax.servlet.http.HttpServletRequest 인터페이스가 제공하는 쿠키 관련 메소드는 아래에 표시한 한개뿐이다.

   public Cookie[] getCookies()

HttpServletRequest 인터페이스가 쿠키 관련된 메소드를 빈약하게 제공하기 때문에(진짜 심하게 빈약함이 느껴진다!!), JSP나 서블릿 등에서 쿠키를 사용할 때에는 쿠키를 다루기 위한 보조 클래스를 작성해서 작업하는 것이 좋다. 본 글에서는 편리하게 쿠키를 처리할 수 있도록 해 주는 CookieBox 라는 클래스를 작성해볼 것이다.

CookieBox 클래스의 소스 코드

말보다는 소스 코드를 직접 보면서 설명하는 것이 이해가 빠를 것 같으므로, CookieBox 클래스의 소스 코드부터 살펴보도록 하자.

    package jsp.util;
    
    import javax.servlet.http.HttpServletRequest;
    import javax.servlet.http.Cookie;
    import java.util.Map;
    import java.net.URLEncoder; 
    import java.net.URLDecoder; 
    import java.io.IOException; 
    
    public class CookieBox {
        
        private Map cookieMap = new java.util.HashMap();
        
        public CookieBox(HttpServletRequest request) {
            Cookie[] cookies = request.getCookies();
            if (cookies != null) {
                for (int i = 0 ; i < cookies.length ; i++) {
                    cookieMap.put(cookies[i].getName(), cookies[i]);
                }
            }
        }
        
        public static Cookie createCookie(String name, String value)
        throws IOException {
            return new Cookie(name, URLEncoder.encode(value, "euc-kr"));
        }
    
        public static Cookie createCookie(
                String name, String value, String path, int maxAge) 
        throws IOException {
            Cookie cookie = new Cookie(name, 
                                    URLEncoder.encode(value, "euc-kr"));
            cookie.setPath(path);
            cookie.setMaxAge(maxAge);
            return cookie;
        }
        
        public static Cookie createCookie(
                String name, String value,  
                String domain, String path, int maxAge) 
        throws IOException {
            Cookie cookie = new Cookie(name, 
                      URLEncoder.encode(value, "euc-kr"));
            cookie.setDomain(domain);
            cookie.setPath(path);
            cookie.setMaxAge(maxAge);
            return cookie;
        }
        
        public Cookie getCookie(String name) {
            return (Cookie)cookieMap.get(name); 
        }
        
        public String getValue(String name) throws IOException {
            Cookie cookie = (Cookie)cookieMap.get(name);
            if (cookie == null) return null;
            return URLDecoder.decode(cookie.getValue(), "euc-kr");
        }
        
        public boolean exists(String name) {
            return cookieMap.get(name) != null;
        }
    }

CookieBox 클래스는 다음과 같이 두 가지 종류의 메소드를 제공한다.

  • Cookie 객체를 생성할 때 사용할 수 있는 static 메소드인 createCookie()
  • HttpServletRequest의 Cookie 객체 및 쿠키값을 읽어올 수 있는 메소드
CookieBox 클래스를 이용한 쿠키값 읽기

먼저, CookieBox 클래스를 사용하면 손쉽게 쿠키를 사용할 수 있다. CookieBox는 다음과 같은 형태로 사용할 수 있다.

    
    // CookieBox 클래스의 생성자는 request로부터 쿠키 정보를 추출
    CookieBox cookieBox = new CookieBox(request);
    
    Cookie idCookie = cookieBox.getCookie("id"); // 쿠키가 존재하지 않으면 null 리턴
    
    // 지정한 이름의 쿠키가 존재하는지의 여부
    if (cookieBox.exists("name")) {
        ...
    }
    
    // 지정한 이름의 쿠키가 존재하지 않으면 값으로 null 리턴
    String value = cookieBox.getValue("ROLE");

일단 CookieBox 객체를 생성한 이후에는 세 개의 메소드(getCookie(), exists(), getValue())를 사용해서 손쉽게 Cookie 객체 및 쿠키값을 사용할 수 있게 된다. 별도의 유틸리티 클래스를 사용하지 않고 쿠키를 사용할 때는 다음과 같은 방식을 사용하게 되는데, 아래 코드와 비교하면 얼마나 위 코드가 간단한 형태인지를 알 수 있을 것이다.

    Cookie[] cookies = request.getCookies();
    Cookie idCookie = null;
    
    if (cookies != null) {
        for (int i = 0 ; i < cookies.length ; i++) {
            if (cookies[i].getName().compareTo("id") == 0) {
                idCookie = cookies[i];
            }
        }
    }

CookieBox 클래스를 이용한 Cookie 생성

javax.servlet.http.Cookie 클래스가 제공하는 생성자는 다음과 같이 한개 뿐이기 때문에,

    Cookie(java.lang.String name, java.lang.String value)

쿠키에 대한 도메인, 경로, 유효시간 등을 설정하기 위해서는 다음과 같은 코드를 사용해야 한다.

    Cookie cookie = new Cookie(name, URLEncoder.encode(value, "euc-kr"));
    cookie.setDomain(domain);
    cookie.setPath(path);
    cookie.setMaxAge(maxAge);

CookieBox 클래스는 static 메소드인 createCookie() 메소드를 통해서 적은 코딩량으로 손쉽게 Cookie 객체를 생성할 수 있도록 지원한다. 예를 들어, CookieBox.createCookie() 메소드를 사용하면 위 코드를 다음과 같이 한줄로 변경할 수 있다.

    Cookie cookie = CookieBox.createCookie(name, value, domain, path, maxAge);

CookieBox.createCookie() 메소드가 세 가지 형태로 존재하기 때문에, 그때 그때 알맞은 메소드를 사용해서 Cookie 객체를 생성할 수 있을 것이다.

관련링크:
Posted by 최범균 madvirus

댓글을 달아 주세요