달력

42024  이전 다음

  • 1
  • 2
  • 3
  • 4
  • 5
  • 6
  • 7
  • 8
  • 9
  • 10
  • 11
  • 12
  • 13
  • 14
  • 15
  • 16
  • 17
  • 18
  • 19
  • 20
  • 21
  • 22
  • 23
  • 24
  • 25
  • 26
  • 27
  • 28
  • 29
  • 30
MVC 모델과 Observer 패턴
eapoeash (2004-03-24 16:58:12)

저자: 김대곤

이 패턴을 설명하기 전에 MVC 모델을 먼저 살펴보자. MVC 모델은 조금은 거창하게 알려져 있는 듯하다. 거의 그런 경우가 드물지만, 몇 년 전 필자가 본 누군가의 이력서에는 자신의 강점을 프로젝트에 MVC모델을 적용한다는 것이라고 적혀 있는 걸 보았다. 필자가 보기에 MVC 모델을 적용한다는 것은 달리기를 한다는 말과 같아 보인다. 필자도 달리기를 한다. 그러나 잘 하지는 못한다. 국민학교(초등학교)때부터 체육은 언제나 주저하게 되는 분야였다. 달리기를 잘하는 것과 달리기를 하는 것은 다른 문제이다.

MVC 모델에서 MVC는 각각 Model, View, Control를 말한다. Model은 데이터 또는 기본 기능을 말하고, View는 유저 인터페이스를 말한다. 이 두가지는 시스템 개발에 있어서 없어서는 안되는 부분이며, 누가 개발을 하더라도 반드시 있기 마련이다. MVC 모델은 C 모델이라고 불러도 상관없을 만큼 한마디로 말해서 Control이라는 레이어 계층을 두자는 것이다. Graphical User Interface를 사용할 때, Model 계층과 View 계층 사이에 Control 계층을 만들어서 사용하자는 것이다. 왜 그렇게 해야 하는가? 여기에는 몇가지 가정이 담겨 있다.

두 계층 사이에 필수적이지 않은 다른 계층을 두는 이유는 간단하게 말해서 두 계층이 직접적으로 Coupling되는 것을 피하려고 하기 때문이다. 즉, Control 계층은 Model 계층과 View 계층의 high Coupling을 막아준다. Model 계층과 View 계층의 high coupling이 좋지 않은 이유는 Model과 View의 생명주기가 다르기 때문이다. 일반적으로 Model 계층의 생명주기가 길며, 변경이 드물게 일어난다고 여겨진다. 예를 들어, 물건을 파는 웹 사이트에서 사이버 매장을 새 단장하는 경우를 생각해 보자. 일반적으로 View만 바뀌지 Model은 바뀌지 않는다. 같은 기능을 가진 시스템에서 사용자에 따라서 다른 View가 존재할 수도 있다. 또는 웹사이트에서도 보고, PDA에서도 보는 경우도 같은 Model이 서로 다른 View로 표현되는 예라고 할 수 있다. 직접적인 Coupling은 Model과 View를 같이 바뀌도록 만든다. 이것을 Control 계층이 막아준다. 그럼 View가 바뀌면 Control이 바뀌어야 하는 것이면 마찬가지 아닌가 하는 의문이 생긴다. Control 계층의 로직은 매우 간단한다. 아니 간단하게 설계해야 한다. 적어도 Model를 바꾸는 것보다 훨씬 쉽게. Control 계층은 단지 Coordinator에 불과해야 하기 때문이다.

MVC 모델은 Layered Style이고 Layered Style에서는 아래 Layer가 상위 Layer를 사용하는 것을 권장하지 않는다. A 계층이 B 계층을 사용하게 되면, A 계층은 B 계층에 종속된다. 즉, B 계층이 없으면 A 계층은 자신의 역할을 수행하지 못하는 것이다. B 계층은 A 계층이 없어도 문제가 없다. 이러한 의존관계를 Usage dependency 라고 부른다. Layered Style에서 상위 계층은 하위 계층에 대해 Usage dependency가 있다. 하위 계층이 더 생명주기가 길며, 변경되는 일이 적어야 한다. 그런데 하위 계층이 상위 계층에 있는 기능을 사용하면 하위 계층이 상위 계층에 종속되어 버린다. 만약, MVC 모델에서 Model이 View의 기능을 사용해야 하는 경우에 Model이 View에 종속되는 것을 어떻게 막을 것인가?

이것이 Observer 패턴을 사용하게 되는 중에 하나이다. 이제 Observer 패턴에 대해서 살펴보자. GoF의 디자인 패턴에는 Observer 패턴의 목적을 이렇게 정의하고 있다. "Define a one-to-many dependency between objects so that when one object changes state, all its dependents are notified and update automatically." "한 객체의 상태가 바뀔 때 다른 객체들에게 그 사실을 알려주고 자동적으로 업데이트 되도록 하고자 한다면 이 패턴을 쓰시오" 정도 될 것이다. 좀 걸리는 단어는 one-to-many dependency다. 이제 하나씩 살펴보도록 하자.

한 객체의 상태에 변화가 생기면, 그 객체의 상태에 관심있는 객체들에게 알려주는 방식이다. 객체에게 무엇인가를 알려주는 방법은 객체의 메소드를 통해서 이다. 관심 있는 객체의 메소드를 호출해주면 된다. 하지만 객체의 메소드를 사용하면, 이 객체에 대해 usage dependency가 생기게 된다. 즉, coupling이 높아진다. 이 패턴은 특정한 알려주는 방식을 제안하고 있다.

이 패턴이 어떻게 작동되는지 살펴보자. 예제 파일[Download]을 실행해 보자. 아래의 두 화면이 보인다. Input data 에 데이터를 입력하면 Observer 화면에 나타나도록 되어 있다.
그림1

그림2

"Subject.java"에 있는 Vector 데이타가 바뀔 때 자동적으로 바뀐 내용을 보여주려고 한다. Subject 객체가 Observer객체의 내용을 바꾸기 위해서는 Observer객체의 메소드를 호출해야 하고, 메소드를 호출하기 위해서는 그 객체의 Reference가 필요하다.

Subject 객체의 addObserver라는 메소드를 통해 Observer는 자신의 Reference를 제공하고, Subject는 Observer 객체(MyFrame.java)의 update(Vector v) 메소드를 통해 화면을 바꾸어 준다. 별 무리 없이 작동하고 있다. 문제점은? MyFrame 클래스를 삭제해 보면 알게 된다. Subject 클래스가 더 이상 컴파일 되지 않는다. 이유는 간단하다. 자신의 Observer였던 MyFrame 클래스를 소스안에 포함하고 있었기 때문이다. 만약, 객체의 상태에 관심을 가지고 있는 객체가 오직 하나이고, 변경될 소지가 적다면, 위와 같이 코딩해도 별 문제는 없을 것이다. 하지만 Observer가 한 개가 아니라 20개 정도 되면 20 개나 되는 타입의 클래스를 포함하고 있게 된다. 그 중에 updata(Vector v)라는 메소드를 가지고 있지 않은 클래스라도 있으면 그건 바로 에러가 된다.

이러한 문제를 해결하는 방법은 Subject가 각 Observer들의 클래스 타입 대신 인터페이스를 사용하고, 이 인터페이스는 자신을 구현하는 클래스들에게 update(Vector v)라는 메소드를 구현하도록 강제한다. [Download]

코드를 자세히 살펴보면, 이 Observer 패턴과 자바의 이벤트 처리가 같음을 알 수 있다. 리스너(ActionListener)를 만들고, 리스너를 Subject(JButton)에 등록하고 JButton의 상태가 변하면 ActionListener로 강제된 actionPerformed 메소드가 호출된다. 이 패턴도 Observer 인터페이스를 구현하는 객체를 만들고, 만든 객체를 Subject에 등록하고, Subject 객체는 자신의 상태가 바뀌면, 인터페이스에 정의된 메소드를 호출한다. 즉 Observer 패턴은 이벤트 시스템을 구현할 때 사용된다. 이벤트 시스템의 구현은 Observer 패턴이 주는 또 하나의 강점이다.

하지만 기본은 전혀 바뀌지 않았다. 객체에게 무엇인가를 알려주는 방식은 메소드이고 메소드의 호출을 위해서는 객체의 Reference가 필요하는 것. 조금 다른 방식일 뿐 이 두가지 작업은 패턴을 적용해도 피할 수 없는 것이다. 즉, 패턴은 어떤 문제들을 교묘히 피해가는, 평소에는 잘 생각나지 않는 방법을 알려주고 있다. 그래서 패턴을 언제 사용하는가에 대한 이해는 외우지 않고 있으면 잘 생각나지 않고, 사용하지 않으면 이해되지 않는 측면이 있다. 패턴은 처음 적용해 볼 때 가장 어렵다. 한 번 하고 나면 생각만큼 어렵지는 않다.

필자의 패턴 설명은 전체적으로 모든 내용을 다루고 있진 않다. 일반적으로 Subject는 attach(Obsever obs), detach(Observer obs), notifyAll() 이라는 메소드를 가지고 있다. 하지만 detach 메소드는 다루지 않았다. 인터페이스가 강제하는 메소드도 여러 형태가 될 수 있으며, 그 메소드에 내용이 전달되지 않고 Subject의 다른 메소드를 호출함으로 Subject의 상태를 가져오기도 한다. 자세한 사항은 Design Pattern 책을 참고하기 바란다. Jstorm(www.jstorm.pe.kr)에서도 Design Pattern에 관련된 자료들을 찾을 수 있으리라 생각된다.

'JAVA > JSP_Servlet' 카테고리의 다른 글

<html:link /> 태그 중에서 ^^  (0) 2004.04.07
[Struts] MessageResource 여러개 쓰기  (0) 2004.04.01
[펌] 스트럿츠 이디엄  (0) 2004.04.01
[펌] JavaServer Faces 입문  (0) 2004.04.01
[펌] Jakarta Digester 학습하기  (0) 2004.04.01
Posted by tornado
|
나만의 자바 컴포넌트를 만들자
eapoeash (2004-03-22 17:40:14)

저자: 김영익 / (주)콘텔라


자바 프로그래머라면 당연히 AWT나 Swing 컴포넌트에 대해서 알고 있고 GUI를 위해서 여러 형태의 컴포넌트들을 조합해서 원하는 화면을 만들어 보았을 것이다. 그러나 때로는 JDK 에서 지원하는 표준 컴포넌트들만으로는 무엇인가 부족함을 느껴 본적은 없는가? 자신의 입맛에 딱 맞는 컴포넌트가 없어서 오랜 시간을 허비하거나 고생해본 경험이 있을 수도 있다. 이번 기사에서는 자신만의 간단한 컴포넌트를 작성해보기로 하자.


1. 이렇게 만들어 주세요~~~

필자가 작년에 수행했던 프로젝트 중에 이런 기능을 넣어 달라는 요구 사항이 있었다.
“유닉스 시스템의 CPU, 메모리, 파일 시스템의 상태를 감시하는 기능을 챠트를 이용해서 보기 좋게 넣어달라” 요구 사항 자체는 간단했지만 챠트를 만들기 위해서는 상당한 노력이 필요하기 때문에 다른 라이브러리를 사용하기로 결정했다. 비용 절감을 위해 “JFreeChart” 라는 오픈 소스 프로젝트를 사용해서 챠트 문제를 해결했지만 한가지 남은 문제가 있었다.
CPU의 상태를 윈도우의 작업 관리자에서 보이는 형태처럼 만들어 달라는 것이었다.

null


JProgressBar를 사용하면 비슷한 형태가 나오기는 하지만 아무래도 많이 다르다는 느낌이 들어서 직접 만들어서 사용하기로 결정했다. 최종적으로 구현된 결과는 아래 화면을 참고하자.



원래 이 컴포넌트만 단독으로 보여 주는 것은 아니었고 다른 그래프들과 함께 프레임에 포함된 것이었다. JFreeChart로 그려진 그래프도 구경할 겸해서 아래 그림도 첨가한다.




2. 주요 구현 설명

일반적으로 자바 컴포넌트를 만들기 위해서 Sun 에서는 자바 빈즈 컴포넌트 명세를 따르기를 권고한다. 그러나 지침대로 모든 사항을 맞추기에는 시간과 노력이 너무 많이 들기 때문에 최대한 간단하게 원하는 기능만을 수행하도록 작성 해보도록 하자.

소스의 양도 그리 길지 않고 어려운 부분도 없으므로 소스에 삽입된 주석만을 주의 깊게 살펴보면 된다.

import javax.swing.*;
import java.awt.*;
import javax.swing.border.*;
import java.awt.event.*;

public class PmTickChartPanel extends JPanel {
  BorderLayout borderLayout1 = new BorderLayout();
  JPanel centerPanel = new JPanel();
  JPanel southPanel = new JPanel();
  JPanel northPanel = new JPanel();
  JLabel dataLabel = new JLabel();
  GridLayout gridLayout1 = new GridLayout();
  BorderLayout borderLayout2 = new BorderLayout();
  JLabel titleLabel = new JLabel();
  BorderLayout borderLayout3 = new BorderLayout();
  JPanel westPanel = new JPanel();
  JPanel eastPanel = new JPanel();

  // 편의상 눈금은 10개만 보이고 색상 변경도 10 단위로만
  int tickCount = 10;

  Border centerPanelBorder;
  private Border border1;

  public PmTickChartPanel() {
    try {
      jbInit();
    } catch (Exception e) {
      e.printStackTrace();
   }
  }

  private void jbInit() throws Exception {
    centerPanelBorder = BorderFactory.createLineBorder(Color.green, 1);
    border1 = BorderFactory.createBevelBorder(BevelBorder.LOWERED, new Color(4, 4, 4), new Color(3, 3, 3), Color.black, Color.black);
    this.setBackground(Color.black);
    this.setForeground(Color.green);
    this.setBorder(border1);
    this.setLayout(borderLayout1);
    centerPanel.setBackground(Color.black);
    centerPanel.setFont(new java.awt.Font("Dialog", 0, 12));
    centerPanel.setForeground(Color.green);
    centerPanel.setBorder(centerPanelBorder);
    centerPanel.setLayout(gridLayout1);
    southPanel.setBackground(Color.black);
    southPanel.setForeground(Color.green);
    southPanel.setLayout(borderLayout2);
    northPanel.setBackground(Color.black);
    northPanel.setForeground(Color.green);
    northPanel.setLayout(borderLayout3);
    dataLabel.setBackground(Color.black);
    dataLabel.setFont(new java.awt.Font("Dialog", 1, 12));
    dataLabel.setForeground(Color.green);
    dataLabel.setHorizontalAlignment(SwingConstants.CENTER);
    dataLabel.setHorizontalTextPosition(SwingConstants.CENTER);
    dataLabel.setText("");
    titleLabel.setBackground(Color.black);
    titleLabel.setFont(new java.awt.Font("Dialog", 1, 12));
    titleLabel.setForeground(Color.green);
    titleLabel.setHorizontalAlignment(SwingConstants.CENTER);
    titleLabel.setHorizontalTextPosition(SwingConstants.CENTER);
    this.titleLabel.setText("CPU");
    gridLayout1.setColumns(1);
    gridLayout1.setRows(tickCount);
    gridLayout1.setHgap(1);
    gridLayout1.setVgap(1);
    eastPanel.setBackground(Color.black);
    eastPanel.setForeground(Color.green);
    westPanel.setBackground(Color.black);
    westPanel.setForeground(Color.green);
    this.add(centerPanel, BorderLayout.CENTER);
    this.add(southPanel, BorderLayout.SOUTH);
    southPanel.add(dataLabel, BorderLayout.CENTER);
    this.add(northPanel, BorderLayout.NORTH);
    northPanel.add(titleLabel, BorderLayout.CENTER);
    this.add(westPanel, BorderLayout.WEST);
    this.add(eastPanel, BorderLayout.EAST);
    setPreferredSize(new Dimension(90, 180));

    for (int i = 0; i < tickCount; i++) {
      Tick tick = new Tick(); // 눈금(tick)을 10개 생성하여 추가
      this.centerPanel.add(tick);
    }
  }

  /* 실제 컴포넌트에 값을 설정하면 눈금에 색상을 변경한다
  *  값의 범위는 0~100 으로 하자
  */
  public void setValue(int value) {
    if (value > 100) { // 100을 넘어가면 100으로
      value = 100;
    }
    else if (value < 0) { // 0보다 작으면 0으로
      value = 0;
    }

    // 일단 전체 tick 을 검정색으로 칠하고
    for (int j = 0; j < tickCount; j++) {
      Tick tick = (Tick)this.centerPanel.getComponent(j);
      tick.setBackground(Color.black);
      tick.repaint();
    }

    // 입력된 value 값에 해당하는 tick들만 다시 녹색으로 칠한다
    for (int i = 0; i < tickCount; i++) {
      Tick tick = (Tick)this.centerPanel.getComponent(i);
      if (tickCount - i > value / 10) {
        // nothing to do
      } else {
        tick.setColor(Color.green);
        tick.repaint();
      }
    }

    // 하단에 숫자로 값을 표시
    this.dataLabel.setText(value + " %");
  }

  public static void main(String args[]) {
    JFrame frame = new JFrame(); // 테스트용 프레임을 생성하자
    frame.addWindowListener(new WindowAdapter() {
      public void windowClosing(WindowEvent e) {
        System.exit(0);
      }
    });
    PmTickChartPanel tickPanel = new PmTickChartPanel();
    frame.getContentPane().setLayout(new BorderLayout());
    frame.getContentPane().add(tickPanel, BorderLayout.CENTER);

    frame.setSize(300, 300);
    frame.setVisible(true);
    
    for (int i = 0 ; i < 100 ; i++) { // 테스트를 위해 값을 설정하는 부분
      try {
        Thread.sleep(100); // 잠시 쉬었다가
        tickPanel.setValue(i); // 1씩 증가되는 값을 설정, 10 단위로 눈금 색상 변경
      }
      catch (InterruptedException ie) {
        ie.printStackTrace();
      }
    }
  }
}

/* 10 단위의 눈금 하나에 해당하는 컴포넌트
* 같은 값을 갖는 것이 좌우 하나씩 쌍으로 구성된다
*/
class Tick extends JPanel {
  JPanel lPanel = new JPanel();
  JPanel rPanel = new JPanel();
  GridLayout layout = new GridLayout(1, 2);

  public Tick() {
    this.setBackground(Color.black);
    this.setForeground(Color.black);
    lPanel.setBackground(Color.black);
    lPanel.setForeground(Color.green);
    rPanel.setBackground(Color.black);
    rPanel.setForeground(Color.green);
    layout.setHgap(1);
    setLayout(layout);
    add(lPanel);
    add(rPanel);
  }

  protected void setColor(Color color) {
    lPanel.setBackground(color);
    rPanel.setBackground(color);
  }
}

3. 나만의 컴포넌트 작성 소감

위의 소스를 작성하고 컴파일까지 이상 없이 진행되었다면 실행해보자. 앞에서 보여진 그림처럼 값이 증가됨에 따라 눈금이 올라가는 것을 볼 수 있다. 대단한 것은 아니지만 예상보다 예쁘게 보이는 컴포넌트가 대견하기만 하다. 여러분은 용도에 따라 별도의 구현만 추가한다면 입맛에 바꿔 사용할 수 도 있고, 이 예제를 기반으로 다른 컴포넌트를 작성할 준비가 된 것이다. 언제까지 남이 만든 것만 사용할 것인가? 머리 속에 무엇인가 떠오르는 것이 있다면 지금 당장 준비하자 !!!
Posted by tornado
|
 
객체지향 방법론
(Object Oriented Methodology)



- 본 글은 객체지향 방법론의 개략이며 Rational社의 객체지향 방법론인 Objectory Process의 개요서 부분 중 일부를 번역한 내용입니다.

1. 생명주기 구조(LifeCycle Structure)

소프트웨어 생명 주기는 여러주기로 나누어 지며, 각 주기는 제품의 새로운 생산물을 만든다. 본 방법론은 4개의 연속적인 단계로 하나의 개발 주기를 이룬다.
  • 개념화(Inception)
  • 상세화(Elaboration)
  • 구축(Construction)
  • 전이(Transition)
각 단계는 정의된 이정표(어떤 중요한 결정이 만들어지며 따라서 주요 목표가 취득되는 시점)와 함께 종결된다.


[그림 1] 프로세스의 단계들과 주요 이정표

주어진 제품에 대하여 처음 4단계를 한번 수행한 것을 초기 개발 주기(Initial Development Cycle)라 한다. 만약 제품이 계속적으로 지속된다면, 개념화, 상세화, 구축, 전이 단계를 계속적으로 반복하면서 기존의 제품은 다음 세대로 발전(Evolve)할 것이다. 이 기간을 "발전화(Evolution)"단계라 한다. 초기 개발 주기 다음에 이루어지는 개발 주기를 "발전주기(Evolution Cycles)"라 한다.


[그림 2] 두개의 중요한 개발 사이클

1.1 개념화 단계(Inception Phase)

개념화 단계동안 시스템에 대한 비즈니스 사례(Business Case)를 만들고 프로젝트 범위(Scope)를 정의한다. 이를 성취하기 위해서는 시스템과 상호작용을 하는 모든 외부 엔티티(Actor)를 명시하고, 상위 레벨(High Level)에서 상호작용의 특징을 정의한다. 여기에는 모든 유스케이스(Use Case)를 명시하고 중요한 몇 가지를 설명하는 것도 포함된다. 비즈니스 사례는 성공기준(Success Criteria), 위험관리(Risk Management), 필요한 자원의 평가, 주요한 이정표 날짜를 보여주는 단계별 계획을 포함한다.
개념화 단계의 마지막에는 프로젝트 목적을 검사하고 개발 진행여부를 결정한다.

1.2 상세화 단계(Elaboration Phase)

상세화 단계의 목표는 문제영역(Problem Domain)을 분석하고 견고한 아키텍쳐 토대를 마련하고 프로젝트 계획을 개발하며 프로젝트에서 가장 위험한 요소를 제거하는 것이다. 아키텍쳐에 대한 결정은 전체 시스템의 충분한 이해를 통하여 이루어져야 한다. 이것은 유스케이스를 기술하고 추가적인 요구사항과 같은 제약사항을 고려하는 것을 의미한다. 아키텍쳐를 검증하기 위해서는 선정된 아키텍쳐를 실현하고(Demonstrates) 중요한 유스케이스를 실행하는 시스템을 구현하는 것이다.
상세화 단계의 마지막에는 상세한 시스템의 목적과 범위 및 아키텍쳐 선정과 주요 위험을 검사한다.

1.3 구축 단계(Construction Phase)

구축 단계에서는 사용자들(User Community)에게 전이할 수 있도록 반복 및 점증적으로 제품을 완전히 개발하는 것이다. 이것은 나머지 유스케이스를 기술하고 설계부문을 더욱 충실하게 하며, 구현을 완전히 끝내고 소프트웨어를 테스트하는 것을 의미한다.
구축단계의 마지막에는 소프트웨어와 환경과 사용자들이 운영될 준비가 되었는지를 결정한다.

1.4 전이 단계(Transition Phase)

전이 단계에서는 소프트웨어를 사용자들(User Community)에게 전달하는 것이다. 제품이 사용자의 손에 전해졌을 때에는, 시스템에 적합하도록 추가적인 개발 및 발견되지 않은 문제점을 수정하고 미루어 놓은 사항들(Features)을 마무리 짓는다. 일반적으로 이 단계는 시스템의 "베타 릴리즈(Beta Release)"로 시작된다.
전이 단계의 마지막에는 생명주기 목적의 충족여부 및 또 다른 개발 주기의 시작여부를 결정해야 한다. 또한 프로세스를 개선하기 위해 프로젝트에서 경험한 것을 여기서 정리한다.

1.5 반복(Iteration)

방법론 프로세스에서의 각 단계는 여러 번의 반복으로 나누어질 수 있다. 반복은 하나의 완전한 개발 루프로서 내부적 혹은 외부적 실행가능한 제품(개발내에서 최종 제품의 부분적인 집합)을 만들어낸다. 이 실행 가능한 제품은 반복과 반복을 거듭하므로서 점증적으로 발전하여 최종 시스템이 된다.


[그림 3] 방법론 단계 내에서의 반복

각 반복은 소프트웨어 개발의 모든 관점을 거치고 지나가며 단계에 따라 각 프로세스 컴포넌트가 다양하게 강조될지라도 이것은 모두 프로세스 컴포넌트이다.
이는 다음 그림과 같이 여러 반복들이 서로 다른 부분에 강조를 하면서 4단계를 수행하는 모습으로 나타날 수 있다.


[그림 4] 프로세스 컴포넌트

본 방법론에서 단계들은 여러 반복들로 구성되어 있으며, 각 반복은 모든 프로세스 컴포넌트를 포함하고 있다. 각 프로세스 컴포넌트는 산출물에 대하여 책임이 있다. 곡선의 높이는 자원의 양을 표시하고 있다.
이 반복적인 접근방법의 중요한 점은 산출물은 초기에 기술되며 계속적으로 단계가 지나감에 따라 증대되며 성숙해진다.


[그림 5] 개발단계별 정보 집합의 발전

2. 프로세스 컴포넌트(Process Component)

2.1 프로세스 표현(Process Representation)

본 방법론의 프로세스가이드에서는 작업자(Workers)와 활동(Activities)과 작업흐름(Workflows)에 의하여 프로세스 컴포넌트를 기술한다. 수행하는 산출물은 산출물가이드에서 기술하고 있다.
  • 작업자(Workers)는 개개인의 행동 및 책임을 정의하거나 또는 함께 수행하는 개개인들의 집합 즉, 팀으로 정의된다. 왜냐면 일반적으로 생각하기에 작업자는 개인 혹은 팀 그 자체로 간주되지만 객체지향 방법론에서의 작업자는 개개인이 어떻게 작업을 수행하는냐 하는 그 역할에 중점을 두고 있기 때문에 이를 구분한는 것은 중요하다.
  • 활동(Activities)은 연관된 작업의 최소 단위이다. 활동의 한 부분만 수행하는 것은 적절하지 못하다. 하지만 개발의 감시를 용이하게 하기위하여 작업을 분할한다. 프로젝트에서 한 활동의 60% 완료되었다기 보다는 5개의 활동 중 3개가 완료되었다고 표현하는 것이 더 알기 쉽고 바람직하기 때문이다.
  • 산출물(Artifacts)은 개선(Evolve), 유지보수(Maintain) 혹은 입력자료로 사용되는 모델과 문서이다.
각 작업자는 연관성 있고 응집된(Cohesive) 활동 집합을 가지고 있다. "응집되어 있다는 것은(Cohesive)" 한 개인에 의해 활동이 가장 잘 수행되는 것을 의미한다. 각 작업자의 책임은 일반적으로 문서와 같이 연관된 산출물로 정의된다. 작업자의 예로는 유스케이스 설계자, 설계자, 아키텍트 그리고 프로세스 엔지니어가 있다.


[그림 6] 각 작업자별 고유의 활동집합과 산출물

활동의 연관된 집합을 둘러보면, 작업자는 또한 예상되는 작업수행 능력을 함축적으로 정의하고 있다.
프로젝트 관리자에 적합한 사람으로는 그 관련된 능력을 소지한 사람으로 볼 수 있다.
주어진 개개인이 한 작업자의 연관된 활동, 행위를 수행하고, 작업자와 연관되어 있는 모든 산출물에 대한 책임을 갖는 방법으로, 프로젝트 관리자는 이들 개개인을 작업자와 연관시킬 것이다.


[그림 7] 프로젝트에서 개인은 여러 작업자가 된다.

개인과 작업자의 관계는 시간에 따라 유동적이다. 적절한 능력을 소지한 개개인의 이용여부에 의한 제약사항 및 주어진 시간에 의해 수행되어지는 활동에 의한다. 보통은 한 개인이 하나의 작업자가 되지만 다음과 같은 경우도 있다.
  • 동일한 날에 개개인은 몇 개의 다른 작업자로서 일을 하기도 한다 : 이종범은 오전에는 설계 검토(Reviewer)를 하지만 오후에는 유스케이스 설계자로 대치될 수 있다.
  • 동시에 개개인은 몇 개의 작업자로서 일을 하기도 한다 : 어떤 클래스(Class)에서 허정무는 아키텍트와 설계자로서 존재할 수 있다.
  • 몇 명의 개인들은 동일한 작업자 즉 팀의 관계로서 어떤 활동을 함께 수행하기도 한다 : 선동렬과 박찬호는 동일한 유스케이스의 유스케이스 설계자로서 작업을 할 수 있다.
다양한 프로세스 컴포넌트와 활동을 계획할 때 프로젝트 관리자는 매끄럽게 이어지도록(Seamless Fashion) 개개인의 작업을 할당해야 한다. 이 말은 즉, 산출물이 한 개인으로부터 다른 개인으로 넘겨지는 것을 최대한 줄이고자 하는 것이다.
예를 들어서, 클래스의 설계자는 구현자(Implementer)가 될 수 있다. 주어진 유스케이스에서 동일한 개인이 유스케이스 명세자(Specifier)와 유스케이스 설계자로 행동할 수 있다.

2.2 프로세스 컴포넌트와 모델(Process Components and Models)

프로세스 컴포넌트는 연관된 집합의 활동과 산출물을 가지고 있다. 가장 중요한 산출물은 각 프로세스 컴포넌트가 산출하는 모델이다. 즉 유스케이스 모델, 설계 모델, 구현 모델 그리고 테스트 모델이다. 아래 그림은 프로세스 컴포넌트와 모델의 관계도를 보여주고 있다.


[그림 8] 각 프로세스 컴포넌트는 하나의 특정한 모델과 연관된다.

2.3 요구사항 수집(Requirement Capture)

프로세스 컴포넌트 중 요구사항 수집의 목표는 시스템이 무엇이며 무엇을 하는가를 기술하고 개발자와 고객이 기술서에 동의하도록 하는 것이다. 이를 위해서 시스템의 범위를 정하고 시스템의 주위 환경 및 행동을 정의한다. 시스템 요구사항이 존재할 뿐만 아니라 고객과 잠재적인 사용자는 정보의 중요한 소스(Sources)이다.
요구사항 수집을 하므로써 유스케이스 모델과 추가 요구사항(Supplementary Requirements)을 만들어 낸다. 유스케이스 모델은 사용자와 개발자에게 필수적인 요소이다. 사용자에겐 예상한 시스템이 모델에 잘 반영되었는가를 평가하기위해 모델이 필요하며, 개발자에게는 시스템상에서 요구사항을 보다 잘 이해하기 위해서 모델이 필요하다.
유스케이스 모델은 프로젝트에 참여하고 있는 모든 사람에게 유용하다.
유스케이스 모델은 액터(Actors)와 유스케이스(Use Cases)로 구성되어 있다. 액터는 사용자와, 개발되어질 시스템과 상호작용을 하는 다른 시스템을 의미한다. 액터는 시스템 범위를 정하는 것을 도와주고, 시스템이 어떻게 될 것이다라는 명확한 그림을 제공한다.
유스케이스는 시스템의 행위를 표현한다. 유스케이스는 액터의 요구사항에 의해 개발되어지기 때문에 시스템은 사용자에게 보다 적절하게 될 수 있다. 아래 그림은 재활용품 수집기(Recycling-machine) 시스템에 대한 유스케이스 모델의 예를 보여주고 있다.


[그림 9] 유스케이스 모델의 예

각각의 유스케이스는 상세히 기술된다. 유스케이스 설명서(Description)는 시스템이 단계적으로 액터와 어떻게 상호작용 하는가와 시스템이 무엇을 하는지를 설명한다.
유스케이스 역할은 시스템의 개발 주기 전체에 걸쳐 통합된 매개체(Thread)의 기능을 하는 것이다. 동일한 유스케이스 모델이 요구사항 수집, 분석 및 설계와 테스트 동안에 사용된다.

▶ 작업자와 작업흐름(Workers and Workflow)

각 작업자는 활동 집합과 행동에 대해 책임을 진다. 각 프로세스 컴포넌트는 고유의 작업자들과 그 작업자가 활동을 진행하는 논리적 방법인 작업흐름을 가지고 있다. 아래의 작업자들은 요구사항 수집에서 정의된다.
  • 유스케이스 모델 아키텍트(Use-Case Model Architect)
  • 유스케이스 명세자(Use-Case Specifier)
  • 요구사항 검토자(Requirements Reviewer)
  • 아키텍트(Architect)
아래 그림은 요구사항 수집에서 작업흐름에 대한 전체적인 개요를 보여주고 있다.


[그림 10] 요구사항 수집 단계

2.4 분석 및 설계(Analysis & Design)

프로세스 컴포넌트 중 분석 및 설계의 목표는 구현 단계에서 시스템이 어떻게 실현되는가를 보여주는 것이다. 아래 사항을 만족하는 시스템을 구축하여야 한다.
  • 유스케이스 설명서에 명시된 작업(Task)과 기능(Functions)을 구체적인 구현 환경에서 수행한다.
  • 시스템의 모든 요구사항을 충족한다.
  • 견고한 구조를 가진다(기능적 요구사항 변화가 있을 때 쉽게 변경할 수 있는가)
유스케이스 모델은 추가 요구사항 명세서(Supplementary Specifications)와 함께 설계의 토대가 된다.
분석 및 설계에서는 소스 코드의 추상적 개념을 보여주는 설계 모델을 만들어 낸다. 설계 모델은 어떻게 소스 코드가 구조화되어져 있고 쓰여졌는지를 보여주는 청사진과 같은 역할을 한다. 설계 또한 유스케이스의 내부에 관한(Inside-view) 설명서 또는 참여하는 객체/클래스의 관점에서 유스케이스가 어떻게 실현되는지를 기술한 유스케이스 실현(Use-Case Realization)을 만든다.
설계 모델은 설계 패키지에 구조화된 설계 클래스들로 구성되어 있다. 또한 설계 모델은 유스케이스를 수행하기 위해서 어떻게 이들 설계 클래스의 객체들이 상호작용하고 있는지를 기술한 내용을 담고 있다.
설계 활동은 아키텍쳐 측면에 집중한다. 이 아키텍쳐의 생산(Production) 및 확인(Validation)은 초기 설계 반복의 주요한 초점이 된다. 아키텍쳐는 몇 개의 아키텍쳐(Architectural) 뷰에 의해 표현된다. 이들 뷰에는 주요하고 구조적인 설계 결정사항들이 수집된다. 본질적으로 아키텍쳐 뷰는 상세부분에서 탈피하여 중요한 특성을 보다 쉽게 볼 수 있도록 전체 설계를 추상화 혹은 단순화하는 것이다. 아키텍쳐는 좋은 설계 모델을 개발하는 것 뿐만 아니라 시스템 개발 동안 모델의 품질을 증대시키기 위한 중요한 수단이다.

▶ 작업자와 작업흐름(Workers and Workflow)

아래의 작업자들은 분석 및 설계에서 정의된다.
  • 아키텍트(Architect)
  • 유스케이스 설계자(Use-Case Designer)
  • 설계자(Designer)
  • 설계 검토자(Design Reviewer)
다음의 그림은 분석 및 설계에서 작업흐름의 전체적인 개요를 보여주고 있다. 작업흐름은 아키텍쳐-레벨(Architectre-Level) 설계와 클래스-레벨(Class-Level) 설계로 나뉘어 진다.


[그림 11] 분석 및 설계 단계

2.5 구현(Implementation)

시스템은 구현을 통해서 생성되는 소스, 즉 소스 코드 파일(Source-Code Files), 헤더 파일(Header Files), 메이크 파일(Make Files) 등에 의해서 실현된다. 이 소스는 실행 가능한 시스템을 만들어 낼 것이다. 그리고 소스는 구현 패키지(Implementation Packages)에 구성된 모듈(Modules)로 이루어져 있는 구현 모델(Implementation Model)에서 설명된다. 설계 모델은 구현을 위한 기초 자료가 된다. 구현은 별개의 클래스 또는 패키지의 테스팅을 포함하고 있지만, 동시에 패키지와 클래스를 함께 테스팅하는 것은 포함하지 않는다. 이것은 다음 프로세스 컴포넌트 "테스트(Test)"에서 설명된다.

▶ 작업자와 작업흐름(Workers and Workflow)

아래의 작업자들은 분석 및 설계에서 정의된다.
  • 아키텍트(Architect)
  • 시스템 통합자(System integrator)
  • 구현자(Implementor)
  • 코드 검토자(Code Reviewer)
다음의 그림은 구현에서 작업흐름의 전체적인 개요를 보여주고 있다. 작업흐름은 구현 뷰(Implementation View)를 정의하는 것으로부터 클래스의 구현과 통합의 계획 및 수행 까지의 활동을 연결(Span)한다.


[그림 12] 구현 단계

2.6 테스트(Test)

테스트는 시스템 전체를 검증한다. 참여하고 있는 클래스들이 잘 어울려져 올바르게 작동하는가를 검증하기 위해서는, 먼저 개별적으로 유스케이스를 테스트한다. 그런 후에 유스케이스 설명서를(어떤 관점에서의) 테스트의 입력 자료로 사용하여 전체적으로 시스템을 테스트 한다. 테스트를 끝낸 후에 시스템은 전달되어 진다.

▶ 작업자와 작업흐름(Workers and Workflow)

아래의 작업자들은 분석 및 설계에서 정의된다.
  • 테스트 설계자(Test Designer)
  • 통합 테스터(Integration Tester)
  • 시스템 테스터(System Tester)
  • 설계자(Designer)
  • 구현자(Implementer)
다음의 그림은 테스트에서 작업흐름의 전체적인 개요를 보여주고 있다. 작업흐름은 계획에서부터 설계, 구현 그리고 수행하는 테스트 절차(Procedures)까지 전체적인 활동을 연결(Span)한다.


[그림 13] 테스트 단계




Posted by tornado
|
웹 서비스와 SOA(Service Oriented Architecture)
hanbit (2004-03-09 09:14:50)

저자: 이한수(KT 운용시스템 연구소)


필자가 "IT EXPERT, 웹 서비스 실전 프로그래밍" 이라는 제목의 웹 서비스 관련 책을 집필한지도 벌써 1년이 넘었다.

그동안 웹 서비스 관련 기술들에는 많은 변화들이 있었다. 예를 들면 보안, 호환, 트랜젝션 등을 비롯하여 작년부터 활발하게 진행되고 있는 프로세스 관리 등이 이들 변화의 큰 틀이라 볼 수 있다. 하지만 아쉽게도 아직 이러한 기술들의 성숙도가 우리가 기대하는 만큼의 수준에 이르지는 못한 것이 사실이다. 흔히들 웹 서비스 도입의 가장 큰 걸림돌로 확실한 보안 정책의 미비 혹은 웹 서비스간의 연동 시 발생하는 트랜젝션과 프로세스 관리의 문제를 들고 있기 때문이다.

그런데 조금 다른 관점에서 이 문제를 바라 본다면,

대규모 기업 시스템이나 웹사이트들이 겪고 있고 고민하고 있는 문제들 중에는 여전히 보안이나 연동시의 문제점들이 지적되고 있는 사실을 주목해 본다면 이제 3~4년 밖에 안된 유아 수준의 기술인 웹 서비스가 기술 성숙에 필요한 당연한 진화과정을 겪고 있다고도 볼 수 있는 것이다.

필자는 이러한 관점에서 웹 서비스를 생각할 때 "아직 보안이 미비하기 때문에 고려할 필요없다. 혹은 타 서비스와 연동 시 발생할 수 있는 문제 때문에 적용할 수 없다" 라는 식의 결론은 타당치 않다고 말하고 싶다.

왜냐하면 최근에 웹 서비스 기술에서 발현된 근본적이고도 혁신적인 개념이 앞으로 우리가 컴퓨터 시스템을 바라보는 시각을 송두리째 바꾸어 버릴 것이기 때문이다. 너무나 큰 반향 앞에 미시적인 문제들이 장애가 될 수 없다는 것이다.

시장조사 기관인 가트너 그룹은 2004년을 이끌고 갈 10대 핵심 기술로서 웹 서비스를 그 네 번째 항목으로 지목하였다. 그들은 왜 아직 기술적인 성숙도가 높지 않다고 생각되는 웹 서비스를 이렇게 중요하게 생각하고 있는 것일까?

그 생각의 중심에 SOA(Service oriented Architecture: 이하 SOA)라는 중요한 개념이 자리잡고 있다.

가트너는 더 나아가서 2006년까지 전 세계 비즈니스 애플리케이션의 80% 이상이 SOA 를 기반으로 개발될 것이며 향후 5년간 IT 애플리케이션 개발의 80%가 웹 서비스를 기반으로 하는 .Net과 J2EE로 개발될 것이라고 예측하고 있다.

그럼 도대체 SOA 라는 것이 무엇이길래 이렇게 중요하게 생각되는 것일까?

Service

SOA 가 무엇이냐는 정의에 앞서 더 중요한 정의를 짚어 보도록 하자.

도대체 우리가 말하는 '서비스'라는 것이 무엇일까?

필자는 이것이 우리가 심각하게 고민해 보아야 할 주제라고 생각한다. 서비스란 "무엇인가가 제공하는 어떠한 형태의 용역 혹은 역무"를 의미한다. 다시 말해 우리는 서비스를 이용하며 어떠한 과정을 거치는 것은 관심 밖이고 단지 우리가 원하는 결과물을 기대하는 것이다.

114 전화번호 서비스를 이용한다고 가정해 보자. 일단 찾고자 하는 상호를 말하고 우리는 단순히 약 2~3 초 간의 기다림 후에 서비스의 결과물인 전화번호를 기대하는 것이다. 전화번호를 찾아내기 위한 시스템의 구현 형태, 연동, 보안 따위의 문제는 서비스를 이용하는 고객이 걱정할 문제가 아닌 것이다.

더 나아가 만약 우리가 찾고자 하는 전화번호가 존재하지 않을 때를 생각해 보자. 친절하게도 안내 해주시는 분께서 "찾으시는 전화 번호가 없는데 상호를 잘못 말씀해 주신 것은 아닌지요?"와 같은 질문을 서비스 이용자에게 던지게 된다. 즉, 일종의 연동이라고나 할까? 사용자와 서비스 사이에서 중계하는 역할이라고 할 수 있다.

그런데 만약 이때 "고객님은 .Net을 쓰고 계시기 때문에 안내해 드릴 수 없습니다" 라고 말한다면 고객은 어떤 기분이 들 것인지는 굳이 언급하지 않아도 될 듯하다.

그럼 이제 우리가 다루는 시스템에 좀 더 가깝게 얘기해 보자.

만약 시스템을 개발하다가 사용자가 성인인지 아닌지를 판별하여 주는 기능이 필요하다고 하자. 이를 손수 개발하자고 하니 쌓여있는 데이터가 없어 곤란한 상황이다. 이럴 때 이 기능을 서비스로 제공하는 사업자가 있다면 조회 건수당 얼마씩의 비용을 내고 손쉽게 이용할 수 있을 것이다. 우리는 바로 이러한 것들을 서비스로 생각할 수 있을 것이다. 즉, 우리가 원하는 결과를 어떻게 해서든 제공해 줄 수 있고 손쉽게 이용할 수 있는 것!

Service Oriented Architecture

SOA는 지금까지 컴퓨터 시스템을 만들기 위해 소위 아키텍쳐라는 것을 만들어 가면서 혁신적인 개념으로 생각했던 객체지향적인 방법(Object Oriented)에서 컴포넌트 중심적인 방법(Component Based Development)과 모델 기반의 접근 방법(Model Driven Architecture)의 연장 선상에 있는 가장 포괄적이고도 현실적인 개념으로 인식되고 있다.

웹 서비스 이전에도 비즈니스 로직을 컴포넌트화 하여 이것을 서비스로 보고자 하는 DCOM, EJB, CORBA와 같은 개념도 있었지만 이들은 웹 서비스를 이용한 SOA와 명확한 차이를 가지고 있다. 필자가 생각 하기에 DCOM, EJB, CORBA 같은 개념들은 오히려 컴포넌트 중심적인 방법론에 가깝다고 할 수 있다. 왜냐하면 이들은 자신이 가지는 프레임워크 내부에서 비즈니스 로직을 어떻게 잘 모듈화 해서 최대의 효율성을 가지게 하는가에 주안점을 두고 있기 때문이다.

만약 DCOM으로 만들어진 시스템을 리눅스 사용자가 사용하려 한다면?

바로 이것이 기존의 컴포넌트 중심적인 개념들이 가지는 한계점이다. 각자의 Native 방식으로는 아주 효율적이고 강력하게 개발되고 사용되어질 수 있지만 자신의 시스템 경계를 넘어설 경우에는 당면하는 문제점들이 너무나 많다. 즉, 구현된 시스템을 마치 서비스를 사용하듯 자유롭게 연동하기가 어렵다는 것이다.

하지만 웹 서비스를 이용한 SOA는 이들과 근본적인 차이점을 가지고 있다.

시스템을 누구나 이용 가능한 서비스로 간주하고 연동과 통합을 전제로 아키텍쳐를 만든다는 것이다. 즉, 시스템을 개발하면서 처음부터 불특정 다수의 외부 시스템 혹은 고객과의 연동을 고려한다는 얘기이다. 여기서 불특정 다수라는 것이 큰 의미를 가지는데, 이는 어떠한 플랫폼에 있는 사용자가 요청을 하더라도 문제 없이 처리할 수 있도록 한다는 것이다.

그리고 더 나아가 서비스를 이용하기 위해서 특정 기술(EJB, DCOM, CORBA)에 얽매이지 않아도 되기 때문에 매우 유연한 시스템을 만들 수 있다는 장점이 있다.
유연하다는 장점은 별다른 수정 없이 'A' 라는 회사에서 제공하던 서비스를 같은 기능의 'B' 라는 회사에서 제공하는 서비스로 변경할 수 있다는 것이다.

SOA가 주목을 받고 있는 또 하나의 이유는 최근 엔터프라이즈 컴퓨팅의 큰 변화인 'Utility Computing' 과 관련이 있다. Utility Computing은 솔루션이나 시스템을 소비자가 예전처럼 구매하지 않고 기존에 구축된 내용을 이용한 만큼만 비용을 내게 한다는 개념이다.
따라서 손쉽게 사용할 컴퓨팅 자원을 선택하고 교체하려면 이들이 SOA로 구축이 되어야 한다는 것이다. 전문가들은 SOA가 IT 업계가 가야 할 궁극적인 목표인 Utility Computing 으로 가기 위한 중요한 포석이 되어 줄 것이라고 생각하고 있다.

이러한 웹 서비스를 이용한 SOA를 어떻게 설계할 수 있는지에 대한 좀더 기술적인 내용은 다음 글에서 다루기로 하고 마지막으로 우리가 웹 서비스 기술을 어떻게 준비해야 하는지에 대한 간단한 가이드 라인을 제시해 보겠다.
  1. 현재 웹 서비스를 사용하고 있지 않다면? 지금 즉시 테스트용 소규모 웹 서비스 시스템을 구축해야 한다.
  2. 이미 구축한 웹 서비스 시스템이 있다면? 좀더 기능이 강화된 다음 버전으로 개발해야 한다.
  3. 웹 서비스를 사내 시스템 통합 뿐 아니라 타 시스템 연동에도 사용하도록 한다.
  4. 개발자가 아직 웹 서비스에 대해 지식이 없다면? 지금 당장 서점에 가서 웹 서비스 관련 서적을 구해 공부해야만 한다.
즉, 지금 이 시점이 웹 서비스를 통해 이루어 지는 혁명적인 변화에 동참하기 위한 마지막 기차를 탈 수 있는 기회라고나 할까?

지금까지 간단하게 나마 웹 서비스를 이용한 SOA에 대하여 알아 보았다.
아직 구체적으로 기술적인 내용에 대해 짚어 보지는 않았지만 SOA가 무엇이며 웹 서비스 기술이 그 가운데 있음을 확인할 수 있었으리라 생각한다.

만약 지금도 "웹 서비스가 과연 도입될 수 있을까?" 라는 질문을 던지는 분이 계시다면 그분은 위의 4번 항목에 해당되므로 즉시 사고의 전환이 필요함을 말하고 싶다.
Posted by tornado
|
사용성을 만족시키는 인포메이션 아키텍처
hanbit (2003-12-26 11:45:52)

로우 로젠펠드(Lou Rosenfeld)는 인포메이션 아키텍처 컨설턴트이자 『Information Architecture for the World Wide Web, 2nd Edition』의 공동저자이다. 스티브 크럭(Steve Krug)는 웹 사용성 전문가이며 『Don't Make Me Think』의 저자이다. 이 둘은 각각 자신이 속한 분야에서 세미나를 이끄는 등 왕성한 활동을 하고 있으며 심포지움에서 연속 세션을 맡아 함께 진행하기도 했다.

이에 오라일리에서는 이 둘을 만나 그들이 함께 해나갈 조인트 세미나와 웹 사용성과 인포메이션 아키텍처에서 흔하게 빠지는 함정 및 오늘날 웹 산업 현황에 대해 의견을 나누었다.

스튜어트: 어떻게 해서 두 분이 함께 일을 벌이게 되셨나요?

크럭: 우리 두 사람이 같이 강연을 한다는게 꽤 괜찮아보이지 않나요? 실제로 사람들이 제 웹 사이트에 요청했던 바를 토대로 몇몇 도시에서의 워크샵을 계획하고 있긴 했습니다. 그러다가 로우가 제 일정에 대해 물어왔고 서로 이런 저런 이야기를 나누다가 ‘같이 강연을 해보는 것은 어떨까’라는 결론을 내리게 되었지요. 평소에도 저는 로우의 성실한 작업 스타일과 작업에 임하는 태도에 대해 칭찬을 아끼지 않았던 터라 우리 둘이 함께 강연을 한다는 것이 아주 근사하게 생각되더군요.

로젠펠드: 우리 작업 스타일이 서로 비슷하기 때문에 함께 일하는데 있어 전혀 거부감이 없었습니다. 둘 다 철저한 원리원칙 주의자도 아닌데다가 서로 재미있게 일하는걸 좋아하기 때문이죠. 사실 스티브는 아주 재밌는 사람입니다. 전 옆에서 그가 계속해서 재밌게 일할 수 있도록 격려해주기만 하면 되는 걸요.

스튜어트: 그래도 세미나는 두 분이 따로따로 하시는 걸로 알고 있습니다. 각자의 세미나에 같은 대상들을 끌어들일 생각이십니까?

로젠펠드: 지금까지는 등록자의 절반 정도가 두 세미나에 동시등록을 했습니다. 우리 둘이 다루는 주제가 상호보완적이어서 두 세미나에 등록했는지, 그냥 동시 등록의 경우 주어지는 할인 혜택 때문인지, 등록자들의 저의는 잘 모르겠지만 말입니다.

스튜어트: 사용성(usability)과 인포메이션 아키텍처! 제 생각엔 두 개가 똑같아 보이는데요.

크럭: 네, 정말 묘하게도 (사실 정확하게 알고 있는 사람은 거의 없지만) 그 두 개는 똑같은 것이랍니다. : ) (당황한 인터뷰어의 눈치를 살피며) 농담입니다. 농담! 분명 겹쳐지는 부분이 있긴 합니다만 절대 겹쳐질 수 없는 나름의 독자적인 영역이 있지요. 정보를 조직하는 구성 및 메커니즘을 실제로 설계하여 구축하는 것이 인포메이션 아키텍처라면 유저빌리티는 이와는 달리 이미 형성된 토대를 비판하여 허물어뜨리는 것에 가깝다고 할 수 있으니까요. 물론 이런 비판은 개선의 일환으로 시도되는 것이구요.

어떤 때는 내가 하고있는 일을 가장 잘 표현해주는 단어가 ‘쇼 전문가’라고 생각될 때가 있습니다. 흥행 전에 쇼를 관람하고 어떻게 하면 쇼가 더욱 흥미진진하게 보이는지를 연구하는 쇼 전문가 말입니다. 이런 사람들은 정말 보는 능력이 탁월해야 합니다. 웹 프로젝트를 해본적이 있다면 제 말이 어떤 뜻인지 더 확실하게 알 수 있을 겁니다. 내부의 정치적 분쟁에 개입되어 있지 않고, 외부인이자 최종 사용자의 입장에서, 상황을 판단하고 평가할 혜안을 갖고 있어야 하죠. 즉 만든 사람이 아니라 사용할 사람의 눈으로 판단할 지각을 가지고 있어야 한다는 것입니다.

물론 두 영역 간에 겹쳐지는 부분이 분명히 있어서 사용성 영역에서 너무 복잡하거나 전문적이지만 않다면 클라이언트에게 IA 문제에 대한 조언을 잘 해줄 수 있습니다. 물론 인포메이션 아키텍처 전문가들도 사용성에 대해 좋은 조언을 많이 해줄 수 있구요. 자신의 전문성이 언제 한계에 도달하는지 그 때를 아는 것이 중요하지요.

로젠펠드: 그렇습니다. 분명 훌륭한 인포메이션 아키텍처 전문가들은 사용성에 대해서도 잘 알고 있습니다. 하지만 프로젝트의 크기 및 복잡성에 따라 사용성 문제는 개입될 수도 그렇지 않을 수도 있다고 봅니다. 하지만 제 개인적으로는 사용성 전문가들이 적극적으로 개입되는 것을 추천하지만 말이죠.

스튜어트: 사용성에서 가장 흔하게 범하는 실수로는 무엇이 있습니까?

크럭: 정말 놀랍게도 홈페이지를 만든이가 누구인지, 무엇을 위한 사이트인지와 같이 아주 기본적인 정보를 아직까지도 제공하지 않는 홈페이지가 있습니다. 페이지 레이아웃(사이트 전체에 걸쳐 사용된 페이지 템플릿)의 시각적 계층이 잘못되어 컨텐츠를 말해주는 레이아웃도 전혀 엉뚱하게 되어버리는 경우도 있구요. 가장 고질적인 문제는 페이지에 너무 많은 것을 올려놓으려 한다는데 있습니다. 그 결과 눈에 확실히 띄는 것 없이 모두가 다 비슷하게 보이게 되지요.

스튜어트: 그렇다면 IA의 경우에는 어떻습니까?

로젠펠드: 사이트가 작으면 제자리에 있지 않은 것들을 다시 말쑥하게 수정하는 것이 쉬운편이죠. 실수가 쉽게 용인되고 쉽게 수정할 수도 있으니까요.

제가 우려하는 바는 보다 크고 복잡한 사이트들입니다. 이런 사이트에는 기업의 조직표와 부서별 정책이 반영됩니다. 대부분의 경우, 사용자들이 이와 같은 조직표를 암기하고 이를 지침으로 삼아 방대한 사이트의 컨텐츠를 보기 위해 서핑을 하지는 않을 것으로 생각됩니다. 실제로 큰 실수라고는 할 수 없겠지만, 기업이라는 현실에서는 가장 흔하게 일어나는 불행한 일임에는 틀림없죠.

아주 접근하기 어려운 문제이기도 한 것으로 웹 컨텐츠는 자연스럽게 사업 단위 사이의 경계선을 반영하는 "사일로우"에서 커집니다. 어떻게 이 기업급 웹 컨텐츠를 보다 더 사용자에게 친화적인 방식으로 재조직할 것인가? 그리고 어떻게 새로운 사용자 중심의 정보 골격구조를 지원하도록 기업 그 자체를 변화시킬 것인가? 이런 것들이 이번 세미나에서 접근하는 이슈들입니다.

스튜어트: 언제나 간단할수록 더 좋습니까?

크럭: 아닙니다. 물론 "언제나"는 아니지요. 어떤 상황에서는 복잡하고, 심지어 과도하게 붐비는 것이 더 좋을 때도 있습니다. 그러나 명료성의 경우에는 언제나 더 간단할수록 좋습니다. 사람들은 종종 간결함과 명료성을 혼동해서, 복잡하더라도 명료한 것이 있을 수 있다는 것을 잊어버립니다. 여러분이 듣게 되는 말을 정리하면 다음과 같습니다. "더 간단하게 만들면, 파워 유저들은 화를 낼 것이다." 그러나 나의 경험으로 보아 파워 유저들은 절대로 명료성 때문에 화를 내지는 않습니다. 사실 그들은 누구보다도 명료성을 높이 평가합니다. 그들이 싫어하는 것은 일을 파악하느라 시간을 낭비하는 것입니다. 그 이유가 그 사이트를 구축한 사람들이 명료하게 만드는 수고를 하지 않았기 때문에 말입니다.

스튜어트: 디자이너들이 아주 복잡하거나 아주 많은 정보를 다루는 사이트를 다룰 때 반드시 생각해야 할 것이 있다면 어떤 것들이 있지요?

로젠펠드: 많은 사용자들에게 혜택을 주는(페이지 안내에서부터 최적 탐색 결과에 이르기까지) "가벼운" 아키텍처 접근법이 많이 있습니다. 게다가 개발하고 구현하는데 많은 비용이 들지 않습니다. 이런 것들에 재빨리 참여하면 시간을 벌 수 있을 뿐만 아니라 시각적 편의를 증진하면서 더욱 위협적이고 장기적인 도전에 어떻게 대처해야 할지 결정할 수 있습니다.

또 다른 사항으로는 윤리적 속임수를 사용하는 것입니다. 주의깊게 생각해서 계획하면, 보통 자기가 속한 부서의 웹 컨텐츠, 사용자, 그리고 정략에 초점을 맞추고 있는 사람들을 속여서 전체 조직의 이익에 초점을 두도록 만들 수 있습니다. 예를 들어 나는 가끔 청중 분류 과정을 슬쩍 돌려서 조직표에 전혀 관련이 없는 당면과제 샘플(subject samples)들을 마련하는데 초점을 맞출 수도 있다고 생각합니다. 그래도 정책 결정자들은 여전히 모를 것입니다(none the wiser).


효율적인 웹사이트 구축을 위한 인포메이션 아키텍처

참고 도서

효율적인 웹사이트 구축을 위한 인포메이션 아키텍처
피터 모빌(Peter Morville), 루이스 로젠펠드(Louis Rosenfeld)




스튜어트: 사용성이나 인포메이션 아키텍처에 순서를 매길만한 규칙이 있습니까?

크럭: 글쎄요, 저는 "생각하게 만들지마(Don't make me think)"라는 메시지를 유달리 좋아합니다. 그리고 다음과 같이 최우선으로 삼을 좋은 원칙을 발견했습니다. 일에 몰두하라! 그리고 그것을 사람들에게 계속 보여주라! 스스로 명백해져서 이해가 될 때까지 말이다! 사용자들의 머리에 떠 다니는 의문표를 모조리 없애 버리자!

그렇지만 대부분의 경우, 저는 규칙보다는 태도와 전략을 가르치려고 합니다. 잘 작동하는 사이트를 디자인하는 작업은 언제나 아슬아슬한 작업입니다. 공간의 활용을 최우선으로 해서 청중과 출간인 모두 그들의 목표를 완수하도록 돕습니다. 조감을 적절하게 조절하고 포맷해서 충분하게 유용한/적절한 주목을 받도록 해 줍니다. 항상 여러분은 수 많은 변수들에 대해서 최적화하려고 시도합니다(보통 너무 많습니다). 동시에, 저것을 너무 많이 보여주지 않고 이것을 충분히 보여줍니다. 그리고 보편적 규칙이 아주 많이 존재하지는 않는데, 한 디자인 맥락에서 잘 작동하는 해결책이라도 다른 디자인 상황에서는 작동하지 않기 때문입니다.

로젠펠드: 규칙은 인포메이션 아키텍처(IA)에서도 잘 작동하지 않습니다. 사실, 까다로운 IA 질문에 대한 "정답"은 "~에 따라서"입니다. 머리에 떠 오르는 유일한 "규칙"은 파레토 원리(Pareto Principle)라는 것인데, 이른바 80/20 규칙이라고 알려져 있습니다. 이 규칙은 실제로는 전혀 규칙이 아닙니다.

파레토 규칙이 IA에서 작동하는 방식은 기본적으로 상당히 많은 사용자들이 가능한 모든 아키텍처 접근법에서 몇 가지 선택으로부터 혜택을 입을 것이라는 것입니다. 그래서 여러분과 그 사용자들에게 돈을 지불한 대가(bang for your buck)를 최대로 돌려주는 가장 좋은 방식을 몇 가지 선택합니다. 파레토 규칙에는 다른 변형이 많습니다. 예를 들어, 가장 흔한 탐색 몇 가지는 수 없이 많은 탐색으로 구성됩니다(그리고 수작업으로 개발된 "최적 일치(best bet)" 결과로 접근할 수 있습니다). 믿지 못 하시겠다면 여러분의 탐색 기록을 점검해 보세요.

스튜어트: 브라우저의 역사에서 오늘날 웹 디자이너들은 얼마나 거슬러 올라가 신경써야 할까요? 테스트하기를 추천하는 가장 오래된 브라우저는 무엇인가요?

크럭: 정말 어려운 질문이군요. 뭐라고 말해도 곤란하겠네요. 개인적으로 전자우편 클라이언트를 좋아하기 때문에, 저는 거의 대부분 구식 Netscape 4.77 브라우저를 여전히 사용하고 있는데, 어떤 사이트는 열리지 않습니다(페이지 상단에는 로고가 보이지만, 나머지는 비어 있지요). 그러나 내가 구형 브라우저를 사용하기로 결정했기 때문에 그것은 제 개인적인 문제라는 것이죠. 그래서 그런 일이 일어나더라도 그 사이트의 개발자를 원망하지는 않습니다.

그러나 어떤 사람들은 예전 브라우저의 사용 고집합니다(회사 정책, 정말 오래된 하드웨어, 혹은 새로운 브라우저를 내려받을 수 없을 정도로 느린 연결 때문에), 혹 어떤 사람들은 오프-브랜드 브라우저를 정말 좋아합니다. 점차 시대에 뒤떨어지고 있다는 사실(graceful degradation)을 완전히 인지하고 있으면(구형 브라우저에서도 완전하지는 않지만, 쓸모 있는 경험을 제공할 수 있다는 확신을 가지고), 아마도 괜찮을 것입니다. 그러나 또다시, 그것은 종종 교환관계에 봉착합니다. 모든 브라우저를 지원하는 데 얼마나 많은 노력을 투입할 수 있는가?

스튜어트: 사용자 테스트가 중요하다는 것은 누구라도 압니다. 이를 성공적으로 완수하는 열쇠는 무엇입니까?

크럭: 가장 중요한 일은 최대한 간단하게 사용자 테스트를 유지하여 실제로 수행할 수 있도록 해 주는 것입니다. 디자인 과정에서 일찍 사용자 테스트를 시작해서, 일정 간격으로 사용자 테스트를 하고, 테스트 중에 발견된 문제들을 고치는 테스트는 절대 간과할 수 없을 귀중한 단계입니다. 그러나 여전히 많은 사람들은 "유효한" 샘플을 얻기 위해 다섯 혹은 여덟 또는 열 두 명의 사람만 테스트하면 된다고 생각합니다. 그것은 완전히 잘못된 태도입니다. 그렇게 되면 결과적으로 테스트를 단 한번으로 끝내게 되고, 디자인이 거의 끝나갈 무렵에야 테스트를 하게 됩니다. 이렇게 테스트를 하기보다는 여러 사람을 한 번(두 번이나 세 번도 좋음)에 끌어 들인 후, 자유로운 분위기를 유지하면서 매 3주나 4주 마다 사용자 테스트를 해야 하죠.

스튜어트: 로우(Lou)씨, 왜 기업급 설정에 초점을 두고 있습니까? 뭐 "스타 트랙(Star Trek)" 집착 같은 것일까요?

로젠펠드: TV에서 "The Enterprise"라는 이름은 잘못 붙인 겁니다. 왜냐하면, 거대하고 복잡한 개체가 효율적으로 실행되고 있기 때문이지요. (물론 커크(Kirk)는 제외하고) 자아를 문 앞에 버려두고 온 고도의 능력을 가진 정책 결정자들로 이루어진 소규모 간부 그룹에 의해서 말입니다. 그들이 원하는 모든 정보는 거대한 인공 지능 존재에 의해서 즉시 열람되고, 조합되고, 분석되고, 다듬어져 튀어 나옵니다. 이 존재는 분명히 윈도우즈보다는 더 안정적인 어떤 것 위에서 실행 중인 것 같습니다. 현실세계의 기업은 거대하고 복잡하다는 점에서만 비슷합니다. 현실의 기업은 효율적이지 않아서, 자주 웹 컨텐츠를 다시 또 만들어내고 또다시 소프트웨어를 중복 구입합니다. 현실세계의 기업은 어쩔 수 없이 정치적이라서, 막강한 권력을 쥐고 있는 부사장이 디자인을 결정하는 경우가 자주 있습니다. 자율적인 비즈니스 단위 전쟁의 틈바구니에서 현실세계의 기업은 중앙 집권(예, 집행부) 때문에 조율되지 않습니다.

이렇게 이리 저리 하다보니, 사용자들은 대상에서 제외됩니다. 그래서 사용자의 필요를 만족시키기 위해 디자인되기 보다, 기업급 정보 골격구조는 조직적으로 자라나서 조직표의 정치적인 경계선을 반영합니다. 왜 내가 이런 이슈에 집착하는가? 음, 개인적으로, 나는 소형의 단순한 사이트에 대해서 골격구조를 디자인해 주고, 수표를 집어 들고 돌아와서, 편한 마음으로 자는 것을 더 좋아합니다. 그러나 지난 삼년간 대화를 나누어 본 잠재고객(prospect)과 단골고객(client)은 거의 누구나 조직의 기업급 정보 골격구조를 개선하는 도전과제에 접근하지 못하는 것을 불평했습니다. 그래서 집착은 그들이 하는 거지요. 세미나는 그저 도와주려는 나의 노력일 뿐입니다(그렇다고 세미나 중에 밖에 나가지는 마시기 바랍니다).

스튜어트: 스티브(Steve)씨, 왜 당신은 로우(Lou)씨처럼 이런 "기업적" 맥락에 집착하지 않습니까? "기업급 사용성" 역시 큰 문제가 아닌가요?

크럭: 사용성 문제는 IA 문제보다 정치적인 부담이 약간 더 적은 것으로 드러났습니다. IA는 조직에서 서로 다른 부분들이 웹 사이트에 의해 어떻게 표현되어야 할 지에 대해 더 직접적인 충격을 줍니다("방송 시간"의 할당). 그리고 IA 처리과정은 기업 자체가 조직되어 있는 방식에서 기존의 문제들과 불합리를 훑는 경향이 있습니다. 특히 내부 조직이 외부의 청중에게 아무런 의미도 없는 영역에서 말입니다.

스튜어트: 사용성과 IA에서 다자이너들에게 가장 큰 도전과제는 무엇이라고 보십니까?

크럭: 충분히 잠을 자는 것입니다. 사용성에서 가장 큰 문제는 종종 수정할 시간은 있으나, 수정에 무엇이 필요한지 모르는 것입니다. 사실상 대부분의 웹 디자이너들(그리고 개발자들)은 오히려 시간이 모자랍니다. 다른 큰 도전과제는 일에서 어느 정도 거리를 두는 것입니다. 한 프로젝트에 몇 주(몇 달은 물론이고)동안 매달리게 되면, 다시 되돌아와 사용자의 관점에서 그 프로젝트를 바라보는 것이 거의 불가능합니다. 그 때문에 프로젝트에 참여한 사람들이 자신들이 만든 구축물을 다른 사람들에게 사용해보도록 하고 그들이 사용하는 것을 꾸준히 지켜 보는 일(이른바 "사용성 테스트"가)이 아주 중요한 도전과제가 되지요.

로젠펠드: 디자인을 하는 사람들에게 있어서 가장 큰 도전과제는 자신의 작업에 드는 비용을 정당화하는 문제라고 생각합니다. 어느 경우에나 항상 문제가 뒤따르게 되는데 그것이 바로 투자 수익(ROI, return on investment)입니다.

IA에 대해서 특별히 확실한 투자수익(ROI) 분석 사례는 보지 못했지만, 비용을 정당화하기 어려운 기타 애매한 영역의 가치를 이해하듯이 점점 더 많은 사람들이 IA의 내재적인 가치를 똑같은 방식으로 이해하는 것 같습니다. 예를 들면 시장판매(marketing) 예산이 그렇습니다. 그래서 나는 낙관합니다.

스튜어트: 워크샵에 참석한 사람들이 워크샵이 끝난 후에는 어떤 것들을 가지고 나가길 바라시나요?

크럭: 제 워크샵에 다음과 같이 부제를 붙이고 있습니다. "8시간 안에 사용성 전문가처럼 생각하는 법을 배우자!"라고 말입니다. 그리고 이 선전문구를 자신 만만하게 믿습니다. 나의 목표는 사람들이 더 나은 사용성 감각을 가지고, 사이트를 구축하면서 제기되는 질문에 대한 해답을 알도록 하는데 있습니다. 스스로 사용성 테스트를 하는 법, 그리고 발견된 문제의 해결에 접근하는 법을 알려 주는 것입니다

로젠펠드: 저도 정말 그러기를 바랍니다.

심각한 것은 내가 대화를 나누어 본 사람들은 생각보다 훨씬 더 절망감을 느낀다는 겁니다. 그들의 절망은 종종 반복적인 난관주의나 선임관리자의 무지에 대한 반응입니다. 선임 관리자들은 기업급 정보 골격구조를 다시 디자인하는 도전은 별 문제가 되지 않는 아주 쉬운 일이라고 생각합니다.

제 세미나에 참석한 사람들이 즉시 혹은 빠른 시일 내에 구현할 수 있는 실용적인 디자인 개념을 가지고 떠났으면 합니다. 장기적으로 작업할 현실적인 계획 그리고 목표와 균형을 이루어서 말입니다. 그들이 어느 "교과서"적 연구 방법이 기업적 맥락에서 작동하고 작동하지 않는지 더 좋은 감각을 가지기를 바랍니다. 그리고 한가지 작업틀을 가지고 돌아가 조직에서 기업적 팀을 이루어 IA 문제들을 해결하는데 지침이 되기를 바랍니다.

스튜어트: 바깥 경기를 생각해볼 때 어떻습니까? 사용성과 IA에 돈을 쓸 능력이 될 사람들이 있을까요?

로젠펠드: 불경기이기는 하지만, 적어도 미 연방 정부는 IA에 어느 정도는 지출을 하고 있습니다. 왜 그런지는 묻지 마세요. 거대 기업들초차도 IA 문제들에 접근하는데 필요한 최소한의 자금을 모으고 있는 듯 합니다. 그냥 그들은 소프트웨어 도구들에다 돈을 지출하려고 하지 않을 따름입니다. 전문가를 초빙함으로써 그런 투자가치를 높이려고 더욱 더 노력하고 있습니다. 사실, 많은 기업들이 현재 전업제 IA 전문가들을 간부로 채택하고 있다는 사실에 놀랐습니다. 이런 것들을 볼 때, 낙관적이란 생각이 들지 않습니까?

크럭: 상당히 많은 사람들부터 들은 이야기에 의하면 경기가 여전히 아주 힘들다고 합니다. 이제서야 생각보다 많은 회사들이 사용성에 노력을 기울여야 한다는 사실을 깨닫고 있지만, 프로젝트 예산의 표준 라인에 들기에는 여전히 거리가 멀다고 생각합니다. 들은 바로는 많은 회사들이 관심을 표명하고 사용성에 관하여 문의하지만, 실제로 거기에 돈을 쓰는 결정을 내리는데 이르지는 못하는 것 같습니다.

사용성에 돈을 쓰는 회사들은 웹에 방대한 투자를 하고 있는 거대 회사들이고, 그 보다 작은 회사에서는 관리자들이 이전에 사용성에 경험이 있고, 그냥 비용을 지출하는 대신, 디자인 과정을 실제로 거대회사들 보다 더 쉽고 빠르게 만들 수 있다는 것을 알고 있는 경우에 사용성에 비용을 지출합니다.
Posted by tornado
|
스트럿츠 이디엄
hanbit (2003-12-19 14:19:09)

†역자 주: 본 기사는 원문의 절반도 안되는 내용이나 우선은 역자 자신이 딱 이거다 싶은 내용과 직접 사용해본 내용만을 번역해 놓은 것이다. 스트러츠를 계속 사용하면서 이해가 되는 부분은 차츰 하나씩 번역해나갈 생각이다.
스타일시트나 다른 HTML 자원을 참조할 시에는 <html:link rewrite>를 사용하라

html:link rewrite 태그는 컨텍스트 상대적인 URL을 사용할 수 있다. 또한, 필요하다면 URL을 인코딩하는 것도 가능하기 때문에 컨테이너가 보안을 관리하는 자원의 경우 이용할 수 있다.
예제)<LINK REL=’stylesheet’ HREF=”’<html:rewrite page=’/path/to/CSS”/>’ TYPE=’text/css’>
정적인 페이지 접근을 위한 액션(Action)은 어찌 할 것인가?

모델 2 환경에서 페이지는 깔끔하지만, 처리한 내용만 보여주기 때문에 단순하다고 할 수 있다. 모델 2 환경에서는 액션을 먼저 거친 후, 페이지로 이동을 해야 한다(바로 이 부분이 모델 1에서 웹 프로그래밍 하던 사람들이 고생하는 것으로 역자 또한 그러하다). 그리고 액션은 페이지가 request, session context에 담아서 보낸 정보를 조합하여 나타낼 뷰를 결정해야 한다. 그러나 액션은 뷰의 주소를 모른다(ActionMapping이 알고 있음). 모든 페이지는 하나의 액션 핸들러를 가지고 있어야 한다. 하나의 액션은 다수의 다른 뷰 페이지를 핸들링 할 수 있으며 하나의 페이지는 여러 다른 액션에 의해서 핸들링 될 수 있다. 그러나 하나의 페이지는 적어도 하나의 액션 핸들러를 반드시 가지고 있어야 한다. 여기서 한가지 질문! 그렇다면 static한 페이지에 접근하는 경우, 모두 액션을 가지고 있어야 하는가? 100개의 페이지가 있다면 그것들 모두 액션을 가지고 있어야 하는가? 이런 경우를 대비해서 스트럿츠에는 ForwardAction이라는 액션을 제공한다. 액션 맵핑에 아래와 같이 입력해주면 된다.
예제)<action    name="boardForm"            parameter="/boardcreate.jsp"            path="/boardcreateform"            type="org.apache.struts.actions.ForwardAction"            validate="false"/>
다른 건 다 무시하고, parameter와 type만 살펴보자. Boardcreate.jsp은 새로운 글을 입력하는 페이지로, 실제로 액션이 있어도 아무런 역할을 하지 못한다. 이 경우 org.apache.struts.actions.ForwardAction를 액션 타입으로 지정하고, parameter란에 이동하고자 하는 페이지 주소를 적는다. 이 경우는 /boardcreateform.do를 (*.do으로 서블릿 매핑이 되었다고 가정) 주소창에 치면 우선은 ForwardAction을 통과해서 boardcreate.jsp로 포워드 된다. 이렇게 함으로써 모델 2에서 Action을 거쳐서 페이지로 이동해야 한다는 조건을 만족하게 된다.

액션이 하는 일이 매우 작다면 하나의 액션을 가지고 여러 일을 처리하는 방법을 간구하라

간단히 게시판을 만드는 중이었다. 게시물 삭제, 수정, 수정하는 폼, 등록, 답글 달기, 주석 달기 등 이렇게 하나하나 액션을 만들다 보니 관리해야 할 액션 클래스만 몇 십 개가 되어버렸다. 이렇게 하나의 단위로 묶을 만한 액션들 다수가 존재한다면, DispatchAction의 사용을 고려해봐야 한다. 곧바로 예제를 보겠다.
예제)<action            input="/board/bullcreateform.jsp"            name="bullForm"            parameter="method"            path="/bull"            scope="request"            type="webfx.board.struts.action.BullAction"            validate="false"/>
다른 액션 맵칭하는 방법과 거의 다르지 않다. 여기서 특이하게 볼 내용은 parameter이다. 그리고 BullAction의 코드는 아래와 같다.
…public BullAction extends org.apache.struts.actions.DispatchAction {	public ActionForward create(		ActionMapping mapping,		ActionForm form,		HttpServletRequest request,		HttpServletResponse response)		throws Exception {	…};	public ActionForward remove(		ActionMapping mapping,		ActionForm form,		HttpServletRequest request,		HttpServletResponse response)		throws Exception {	….	}	등등…}
이 소스에는 execute라는 메소드가 없다. 단지 Method Signature가 이름을 제외하고 execute와 다른 create, remove 함수가 있다. 그리고 이 액션은 org.apache.struts.actions.DispatchAction을 상속했다. 우리가 parameter로 지정한 값은 request로 넘어올 파라미터 이름이다. 그리고 이 파라미터에 우리가 실행시키고자 하는 메소드 명을 지정하면 그 메소드가 실행된다. 예를 들어 /bull.do?method=create라고 실행시켰다면 BullAction에서 create 함수를 실행시키게 된다. 이처럼 관리하기 힘들 정도로 액션이 늘어날 경우에는 DispatchAction을 사용하는 것도 고려해봐야 한다.

데이터와 Value Objects을 교환하기 위해서 populate 유틸리티를 사용하라

Jakarta.apache.org 사이트에 있는 Commons에 속한 BeanUtils를 말하는 겁니다. BeanUtils.populate()와 describe() 메소드는 beans와 임의 타입 간의 데이터 전송 사용을 위해서 사용될 수 있다. 요구되는 것은 대응되는 public 형의 프로퍼티 이름들과 네이티브 타입이다. Wrapping Object 즉, Integer 같은 것들은 지원이 되지만 그 외의 타입은 지원되지 않는다(예, Date).

describe 메소드는 소스 빈을 스트링 값을 가지고 있는 맵으로 만든다. populate 메소드는 그러한 맵을 읽어서 타겟이 되는 빈에 프로퍼티를 적용하는 역할을 한다. 만약 타겟 타입이 스티링이 아니라면 필요한 타입으로 변경이 된다. 만약 변환이 실패하면 예외가 발생한다. 다음이 두 빈(Bean) 간의 데이터 이동하는 소스이다.
BeanUtils.populate(target, BeanUtils.describe(source));
몇몇 경우에 필요한 만큼만 조절한 ActionBean에서 또한 조절된 값을 가지고 있는 Map을 만들기 위해서 커스터마이징한 describe 메소드가 필요할 때가 있다. 다른 값들 (즉, 지원되지 않는 타입)은 직접 세팅해 줄 수 있다.
BeanUtils.populate(target, source.describe());tartget.setDate(source.getDateBinary());
BeanUtils을 직접 이용하는 것보다 유틸클래스를 하나 만들어, 그곳에서 BeanUtils 클래스를 이용하도록 만드는 게 더 좋을 것이다. 예를 들면 아래와 같다.
valueUtils.populate(ValueObject target, ActionForm source);valueUtils.populate(ActionForm target, ValuteObject source);
공통적으로 사용되는 속성들에 대한 객체를 만들어 사용하라

웹 애플리케이션 안에서 애플리케이션에 전체적으로 걸쳐서 사용되어야 하는 속성이 있다. 사용자 정보, 북마크, 메뉴 세팅, 다른 프로퍼티 등등이 이것에 속할 수 있다. 이때에는 이런 것들을 각각 따로 관리하는 것보다 이런 것들을 한꺼번에 래핑하는 객체를 만들어서 사용하자.

애플리케이션을 위한 Base Action을 사용하라

애플리케이션 액션들은 몇몇 기본적인 작업을 처리한다. 이러한 태스크를 일관적으로 구현하기 위해, 애플리케이션의 다른 액션들이 서브클래싱하여 사용할 수 있는 Base 클래스를 이용하자! 만약 개개 액션의 execute 메소드 안에서 중요하게 처리되어야 할 일이 있다면 서브 클래스들이 execute() 메소드를 통해서 실행시킬 추상 메소드를 사용하는 것이 좋은 접근 방법이다. Base 클래스는 기본적으로 자신이 해야 할 일을 하고 그것이 모두 잘 처리 되었을 때, 새로운 메소드를 실행시키고 그 결과를 리턴한다. 이 부분 때문에 새로운 메소드의 서명을 변경할 수 있다.
Public ActionForward execute(ActionMapping mapping,				ActionForm form,				HttpServletRequest request,				HttpServletResponse response)			throws IOException, ServletException {		// Application specific bevaviour		// if evevrything is kosher call subclass		return (executeAction(mapping, form, request, response, myParameter))}
executeAction은 Base 클래스의 추상 클래스이다.

이중으로 submit이 일어나는 것을 막기 위해서 Action Token을 사용하라

스트럿츠의 액션에 one-use tokens을 만들어 담는 메소드들이 있다. 폼이 작성될 때 세션에 토큰이 위치하게 되고, 이 토큰은 HTML 폼에 hidden 프로퍼티로 포함된다. 그 폼이 리턴 될 때 토큰은 validation이 된다. 만약 validation에 실패하면 벌써 그 폼은 submit이 된 것이므로 사용자는 그에 대한 알림 메시지를 받을 수 있다.
  • saveToken(request)
  • on the return trip
    • isTokenValid(request)
    • resetToken(request)
스트럿츠를 이용한 웹 애플리케이션 개발 사이클
  1. 화면 설계서를 개발한다. (화면 요구사항 설계)
  2. 화면 요구사항을 만족하기 위한 Helper 클래스를 개발한다. (value object)
  3. 워크플로우 요구사항을 개발한다. (화면 이동 등의 스토리보드 작성)
  4. 워크플로우 요구사항을 만족시키기 위한 ActionMapping을 개발한다.
  5. ActionMapping을 사용하여 워크 플로우를 검증하기 위한 Stub JSP 페이지를 작성한다.
  6. ActionMapping에 의해 호출 되는 액션을 개발한다.
  7. Stub Jsp를 작성한다.

'JAVA > JSP_Servlet' 카테고리의 다른 글

[Struts] MessageResource 여러개 쓰기  (0) 2004.04.01
[펌] MVC 모델과 Observer 패턴  (0) 2004.04.01
[펌] JavaServer Faces 입문  (0) 2004.04.01
[펌] Jakarta Digester 학습하기  (0) 2004.04.01
[펌] Torque프로젝트 사용기  (0) 2004.03.31
Posted by tornado
|

[펌] 방법론

JAVA/JEE 2004. 4. 1. 10:32

소프트웨어공학의 발전사를 보면 크게 3가지의 방법론이 출현하였음을 알 수 있는데 과거 방법론이 없던 무방법론이 없던 무방법론 시대에서 구조적 분석/설계(또는 방법론), 정보공학,객체지향 방법론 들이 그것이다.

  1.구조적 방법론(Structured Methodology)

 구조적 방법론의 시작은 구조적 프로그래밍의 개념이 탄생하면서부터이고,구조적 프로그램밍의 탄생은 소프트웨어공학의 출범을 알리는 것이었다.

 1950년대에 시작한 소프트웨어 개발의 역사는 구조적이지 못한 무원칙의 상향식 개발 방식이었다. 개발자의 손이 가는대로 코딩을 해 나갔으며, 조직의 구성 또한 자신만이 알아볼수 있는 그런 것있었다. 이것은 개발 생산성의 저하와 유지보수의 어려움 등 소프트웨어를 개발하는데 있어서 거의 모든 부분에 문제점을 갖게 만들었고 결국은 "소프트웨어 위기"라는 용어의 탄생을 불려왔다

 1960년대 말 "GO TO"논쟁, 즉 GO TO 문을 쓰지 말고 구조적으로 프로그래밍을 하자라는 주장이 호응을 얻으면서, 분석과 설계도 구조적으로 하자라는 의견으로 확대되면서 구조적 방법로의 틀이 완성되어 갔다.

 구조적 프로그래밍의 개념은 다음과 같다.

  *코드가 계층적인 형식과 제한된 구조로 작성된 순서대로 순차적으로 실행함
  *알고르즘을 기술하는데 순차(sequencing),반복(iteration)구조면 충분하며,단일입구/ 단일출국의 처리구조를 가짐
  *철저한 모듈화로 추상화와 정보은닉을 이루어 프로그램의 구조를 읽기 쉽게 단순화함

 구조적 방법론의 그 특징은 다음과 같다.

  데이터 흐름 지향, 즉 프로세스 위주의 분석과 설계 방식

  모듈의 분할과 정복에 의한 하향식 설계 방식

  SDLC의 구조를 가진 폭포수 모델이 기본

  소프트웨어의 개발이 목표인 프로세스와 산출물의 구성

  데이터의 구성에 대한 설계 방안이 부족

  프로젝트 관리 및 조직, 역활 등 방법론적 다른 요소들의 정의가 없음

 위의 특성들로 구조적 방법론의 단점을 추정할 수 있는데, 이것은 가장 오래전에 규정된 방법론이며, 단순한 소프트웨어 개발을 목표로 한다는 점에서 기인한 것이다.

 2. 정보공학 방법론(Information Engineering Methodology)

  구조적 방법론은 그 후 오랫동안 소프트웨어 개발의 방법론으로 사용되어 왔지만 정보기술의 발전에 따라 새로운 방법론의 필요가 생겼다.

 1970년대 이후 소프트웨어는 기업에서도 많이 응용되었고 80년대를 거치면서 경영정 보시스템(MIS:Management Information System),의사결정지원시스템(DSS:Decision Support System),임원정보시스템(EIS:Executive Information System)등의 형태로 발전하여 갔고, 급기야 전략정보시스템(SIS:Strategic Information System)의 수준에 이르렀다.이것은 단지 소프트웨어 뿐 아니라 데이터베이스,네트워크, 운영조직 등 방대한 주변 정보기술들과 연계 되면서 정보시스템(Information System)이라는 개념의 한 요소에 포함되어 버리고 말았다. 이런 정보시스템은 그 규모도 클 뿐 아니라 구성내용도 복잡해서 개발의 조직도 커지고 개발기관도 몇 년 단위로 늘어났다. 한편으로, 정보기술은 급격히 발전해서 4GT,CASE,등의 자동화 도구, 다이어그래밍 도구의 등장으로 이것들을 활용할 필요도 생겨났다.

 80년대 중반 James Martin에 의해 정보공학(Information Enfineering)의 체계가 정리되면서 정보공학은 본격적으로 활용되었다. 정보공학 방법론의 그 특징은 다음과 같다.

 ~소프트웨어공학의 기본적 사상, 즉 하향식,모듈화, 분할과 정복, 폭포수 등은 동일 ~정보공학은 개별 소프트웨어가 아닌 기업에서 사용되는 정보시스템을 목표로 함 ~단순한 업무지원 시스템의 개발은 의미가 없고 기업의 경영전략을 창출하는 정보전략 계획(ISP:Information Strategy Planning)작업이 포함됨 ~기업의 업무처리에는 안정된 데이터베이스가 중요하므로 데이터 중심의 분석과 설계를 진행 ~CASE 등 자동화를 요구

 정보공학은 대규모 정보시스템의 개발에 가장 적합한 방법론으로 인정되어 왔으나, 경직되고 복잡한 구조로 인해 많은 단점들이 생겨났고, 때 맞춰 객체지향의 개념이 바람을 타고 정보공학의 영역을 허물어 가고 있다.

 3. 객체지향 방법론 (Object Oriented Methodology)

 모든 방법론이 그렇듯이 객체지향 방법론도 객체지향 프로그래밍으로부터 출발했다. 추상화와 캡슐화, 상속, 정보은닉 등 객체를 중심으로 프로그래밍 구조를 단순화하고 재사용 성을 강화하는 프로그래밍 방식의 유용성이 증명되면서, 분석과 설계도 객체지향의 원칙을 적용하려는것이 객체지향 분석/설계요 모든 제반환경이 결합되어 객체지향 방법론이 된 것이다.

  구조적 방법론이나 정보공학 방법론 모두 프로세스와 데이터를 분리하여 처리한다는 단점은 이를 통합하여 처리하는 객체지향의 등장에 가장 큰 배경이 되었다. 프로세스와 데이터가 분리 됨으로써 이를 분석하여 설계와 개발로 연계시키는데 많은 애로를 겪어야 하면 복잡한 기법들이 활용되어야 한다.또한 완성된 시스템에 대한 요구사항의 확인 결과를 너무 늦게 확인할 수 있다는 점, 산출물의 양과 복잡도가 크다는 점 등 여러가지 문제를 객체지향 개발 방법론에서 해결하고 있다.
객체지향 방법론의 특징은 다음과 같다.

 ~반복적, 그리고 점증적인(Iterative and Incremental)개발 방식 ~재사용성의 강조 ~쉽고 표준화된 표기법 존재 ~분산객체 기술의 완벽한 지원 ~OODBMS의 원활한 연계 등 아직 개선의 여지가 많음

 최근에 많은 프로젝트들이 점차 객체지향 방법론으로 진행되고 있으며, 객체지향 모델링 언어의 표준이 UML(Unified Modeling Language)를 기반으로 RUP(Rational Unified Process) 등 상용 방법론들이 등장하여 활용되고 있다.

 3.3방법론간의 비교 이 외에도 기술의 발전에 따라 RAD(Rapid Appilcation Development)방법론 클라이언트/ 서버(Client/Server) 방법론, 패키지(Package) 개발 방법론,웹(Web) 개발 방법론 등 여려 방법론 유형들이 나올 수 있지만 아무래도 위의 3가지가 가장 큰 주류라 하겠다.또한 최근 들어 컴포넌트 기반 개발(CBD:Component Based Development)방식이 큰 반향을 얻고 있고, 조만간 표준화된 방법론도 등장(이미 여러 방법론들이 나와 있지만)할 것으로 보여 컴포넌트 방법론이 제 4의 비교 대상에 올려야 할지도 모르겠다.

 

구조적 방법론

정보공학 방법론

객체지향 방법론

  프로세스 모델링 중심

  데이터 모델링 중심

  데이터 프로세스를 함께 모델링

  모듈화가 관건
  일부 모듈의 재사용 가능

  엔티디 식별이 관견
  데이터의 재사용 기능

  객체이 식별이 관건
  거의 모든 것이 재사용됨

  프로그래밍 기법에 치우침

  기업의 전략 측면 중시, 산출

  기업의 전략 측면 포함

  비정형적 접근 방식, 잘 연계되지 않음   소규모 프로젝트 중심

  물 중심
  구조적인 연계
  대규모 프로젝트 중심

  모든 단계가 Seamless하게 연결
  모든 프로젝트에 적합

프로그래머 중심

  분석가 중심

  분석가/설계자/프로그래머와의 협동중심

Posted by tornado
|
JavaServer Faces 입문
hanbit (2003-10-18 09:15:02)

저자: 부디 쿼니아완(Budi Kurniawan), 역 이상화

자바 웹 프로그래밍 분야에서 JavaServer Faces(이하 JSF)는 새로운 화두로 떠오르고 있다. JSF로 여러분들은 사용자에 의해 발생된 이벤트를 잡아 내거나 웹 페이지에서 웹 컴포넌트를 사용할 수 있다. 가까운 미래에 자바 개발 도구들이 이런 기술을 지원할 것이다. 웹 애플리케이션을 개발하는 것은 현재 드래깅(dragging)과 드로핑(dropping) 및 이벤트 리스너 작성이 가능한 스윙처럼 될 것 이다. 이 기사는 JSF입문 기사정도로 보면 될 것이다. 이 기사는 JSF에서 가장 중요한 것 중에 하나인 이벤트-드리븐(event-driven) 방식에 초점을 맞추고 있다. 본 기사를 무리없이 이해하기 위해서는 서블릿, JSP, 자바빈즈, 커스텀 태그에 관한 선행 지식이 필요하다.

무엇보다도 JSF 애플리케이션은 서블릿/JSP 애플리케이션이다. 웹 디스크립터 파일, JSP 페이지, 커스텀 태그 라이브러리, 정적인 파일들을 가지고있다. 기존 것들과 다른점은 JSF 애플리케이션은 이벤트-드리븐 방식이라는 것이다. 이벤트 리스너 클래스를 사용함으로써 애플리케이션의 작동 방법을 결정할 수 있다. 다음과 같은 단계를 통해 JSF 애플리케이션을 작성할 수 있다.
  1. HTML를 캡슐화하는 JSF 컴포넌트를 사용하여 JSP 페이지를 작성한다.
  2. 사용자 입력과 컴포넌트 데이터의 값을 저장할 수 있는 자바빈즈 파일을 만든다.
  3. 사용자가 버튼을 클릭하거나 폼을 제출할 때 발생하는 이벤트를 처리할 수 있는 이벤트 리스너를 작성한다. JSF는 ActionEventValueChangedEvent 2가지를 지원한다. ActionEvent는 버튼을 클릭하거나 폼을 제출할 때 발생되며, ValueChangedEvent는 JSF 컴포넌트가 변경될 때 일어난다.
이제 JSF가 어떻게 동작하는지 자세히 알아보자.


Mac OS X for Java Geeks

참고 도서

Mac OS X for Java Geeks
Will Iverson




JSF 작동방법

JSP 페이지는 JSF 애플리케이션의 유저 인터페이스 역할을 한다. 각각의 페이지는 폼, 입력박스, 버튼과 같은 웹 컨트롤을 나타내는 JSF 컴포넌트를 포함하고 있다. 폼 내부에 입력박스가 포함 되듯이 컴포넌트들은 다른 컴포넌트 내에 속할 수 있다. 각각의 JSP는 컴포넌트 트리 구조로 표현되며 자바빈즈는 사용자 요청으로부터 데이터를 저장한다.

흥미로운 점은 사용자가 버튼을 클릭하거나 폼을 제출할 때마다 매번 이벤트가 발생한다는 것이다. 모든 이벤트에 의해 발생된 메시지는 HTTP를 통해 서버로 전달된다. 서버는 Faces 서블릿이 실행되고 있는 웹 컨테이너이다. javax.faces.webapp.FacesServlet 클래스인 Faces 서블릿은 모든 JSF 애플리케이션의 실행 엔진이다. 동일한 웹 컨테이너에서 작동하는 각각의 JSF 애플리케이션은 자신만의 Faces 서블릿을 가지고 있다. 다른 중요한 객체는 현재 요청과 관계된 모든 필수 정보를 캡슐화하고 있는 javax.faces.context.FacesContext이다.

애플리케이션의 이면에서 Faces 서블릿이 처리하는 과정은 복잡하지만 세부사항에 대해 모두 알 필요는 없다. 단지 Faces 서블릿이 이벤트를 발생시키는 웹 컨트롤을 포함하고있는 JSP 페이지의 컴포넌트 트리를 생성한다는 것만 알면 된다. Faces 서블릿은 애플리케이션내 모든 페이지에 접근할 수 있기 때문에 트리를 어떻게 구성 할 것인지 알고있다. 또한 Faces 서블릿은 Event 객체를 생성하거나 적절한 리스너에게 통보한다. 요청에 연결된 FacesContext 객체를 통해서 JSP 페이지의 컴포넌트 트리를 얻을 수 있다.

클라이언트 브라우저속 웹 컨트롤에 의해 발생하는 이벤트는 브라우저 종류, 요청 URL과 같은 HTTP 요청으로 캡슐화된다. 그러므로 Faces 서블릿 처리가 필요한 모든 요청은 바로 서블릿으로 전달되어야 한다. 그렇다면 어떻게 모든 HTTP 요청을 Faces 서블릿으로 처리하게 할 것인가? 특정한 URL 패턴을 Faces 서블릿으로 매핑하기 위해 배치 디스크립터에다가 servlet-mapping 요소를 기술하면 된다. 관례상 다음과 같이 /faces/* 패턴을 사용한다.
<!-- Faces Servlet --><servlet>    <servlet-name>Faces Servlet</servlet-name>    <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>    <load-on-startup>1</load-on-startup></servlet><!-- Faces Servlet Mapping --><servlet-mapping>    <servlet-name>Faces Servlet</servlet-name>    <url-pattern>/faces/*</url-pattern></servlet-mapping>
요청하는 URL은 반드시 <url-pattern>에 선언된 패턴을 포함하고있어야 한다. 설정하는 것은 별로 어렵지않으며 Faces 서블릿을 포함하고 있는 <servlet> 요소가 애플리케이션이 시작될 때 서블릿을 로딩 시키는 <load-on-startup> 요소를 가지고 있다는 것을 기억해두자.

컴포넌트에 의해 발생하는 모든 이벤트를 처리하기 위해 반드시 이벤트 리스너를 작성해야 하고 모든 컴포넌트를 등록해야 한다. 이는 컴포넌트를 나타내는 커스텀 태그 속
<action_listener> 요소를 사용하면 된다. 예를 들어 커맨드 버튼에 의해 발생하는 이벤트를 처리하기 위해 jsfApp.MyActionListener라는 리스너를 만들었다면 JSP 페이지 안에 다음과 같이 작성해야 한다.
<h:command_button id="submitButton" label="Add" commandName="submit" >  <f:action_listener type="jsfApp.MyActionListener" /></h:command_button>
액션 리스너는 반드시 javax.faces.event.ActionListener 인터페이스를 상속 받고 값 변경에 관한 리스너는 javax.faces.event.ValueChangedListener를 상속 받아야 한다. 이제 이벤트-드리븐 방식으로 JSF가 동작 하는 방법을 보기위해 간단한 애플리케이션을 작성해 보도록 하겠다.

간단한 JSF 애플리케이션

Source Code

예제 애플리케이션을 작성하기 위한 소스 코드를 다운받는다.



여기에서는 두 개의 숫자를 더하는 간단한 JSF 애플리케이션을 작성할 것이다. 애플리케이션을 실행시키기 위해 Tomcat 5와 JWSDP에 포함된 JSF v1.0 EA4가 사용된다(이것들은 JWSDP(Java Web Services Developer Pack) 1.2에 포함되어 있다). 애플리케이션은 다음과 같은 것들로 구성되어 있다.
  • adder.jsp, JSP 페이지
  • NumberBean, 데이터 저장소 자바빈즈
  • MyActionListener, 액션 리스너
  • web.xml, 배치 디스크립터
JSF 애플리케이션을 실행시키기 위해 JSF 관련 라이브러리를 포함하고있는 .jar 파일이 필요하다. 일단 JWSDP 1.2를 설치하면 jsf/lib 디렉토리 밑에 앞서 말한 파일을 찾을 수 있을 것이다. WEB-INF/lib 디렉토리 밑에 .jar 파일 복사한다. 다음은 복사할 .jar, .tld 파일들이다.
  • jsf-api.jar 파일은 Faces 서블릿과 javax.faces 패키지 내 관련 클래스들을 포함하고 있다.
  • jfs-ri.jar JSF 실행과 관련된 파일이다.
  • jstl_el.jar는 JSTL 표현 수식 문법을 처리한다.
  • standard.jar는 JSTL 태그들을 사용하거나 JFS 실행을 위한 참조 클래스에 의해 참고 된다.
덧붙여 JSF 애플리케이션은 아파치 자카르타의 일부인 다음과 같은 라이브러리를 필요로 한다. 이 라이브러리들은 본 기사와 함께 애플리케이션에 포함되어 있다.
  • commons-beanutils.jar 파일은 JavaBeans 컴포넌트 프로퍼티에 접근해서 정의하기 위한 유틸리티를 포함한다.
  • commons-digester.jar 파일은 Apache Common Digester 클래스들을 포함하고있는 라이브러리이다.
  • commons-logging.jar 파일은 일반적인 목적의 유연한 로깅 도구이다.
아래 소 주제에서는 예제의 각 부분에 대해 논의하고 있다. 마지막 소주제인 "애플리케이션 실행과 컴파일"에서는 JSF 애플리케이션이 어떻게 동작하는지 보여준다.

디렉토리 구조 만들기

디렉토리 구조를 만드는 것부터 예제 JSF 애플리케이션을 시작해보자. 톰캣에서는 webapps 디렉토리 밑이 될 것이다. [그림 1]은 myJSFApp 애플리케이션의 디렉토리 구조를 나타낸다.


[그림 1] JSF 애플리케이션 디렉토리 구조

배치 디스크립터 작성하기

다른 servlet/JSP 애플리케이션과 마찬가지로 예제는 아래 [리스트 1]과 같이 배치 디스크립터를 작성해야 한다.

[리스트 1] 배치 디스크립터 (web.xml 파일)
<?xml version="1.0"?><!DOCTYPE web-app PUBLIC    "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"    "http://java.sun.com/dtd/web-app_2_3.dtd"><web-app>    <!-- Faces Servlet -->    <servlet>        <servlet-name>Faces Servlet</servlet-name>        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>        <load-on-startup> 1 </load-on-startup>    </servlet>    <!-- Faces Servlet Mapping -->    <servlet-mapping>        <servlet-name>Faces Servlet</servlet-name>        <url-pattern>/faces/*</url-pattern>    </servlet-mapping></web-app>
배치 디스크립터에는 두 부분으로 이루어져 있다. <servlet> 요소는 Faces 서블릿을 등록하고 <servlet-mapping> 요소는 URL에 포함된 /faces/ 패턴을 가지는 모든 요청을 Faces 서블릿으로 보낸다.

JSP 페이지 만들기

[리스트 2]와 같이 adder.jsp는 사용자 인터페이스를 제공한다.

[리스트 2] adder.jsp 페이지
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %><%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %><html><head><title>Add 2 numbers</title></head><body><jsp:useBean id="NumberBean" class="jsfApp.NumberBean" scope="session" /><f:use_faces><br />    <h:form id="addForm" formName="addForm" ><br />        First Number: <br />        <h:input_number id="firstNumber" valueRef="NumberBean.firstNumber" /><br />        Second Number:         <h:input_number id="secondNumber" valueRef="NumberBean.secondNumber" /><br />        Result:         <h:output_number id="output" valueRef="NumberBean.result"/><br>        <h:command_button id="submitButton" label="Add" commandName="submit" >            <f:action_listener type="jsfApp.MyActionListener" />        </h:command_button>    </h:form></f:use_faces></body></html>
htmlcore, 두개의 JSF 태그 라이브러리를 사용하기 위해 두개의 taglib directive를 선언해야 한다. 두개 라이브러리에 대한 태그 라이브러리 디스크립터는 jsf-ri.jar 파일에 포함되어 있으므로 걱정하지 않아도 된다. 태그 라이브러리에 대한 접두어는 각각 hf다.
<%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %><%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %>
<jsp:useBean> 액션 요소는 세션 범위를 가진 NumberBean 자바빈즈를 정의한다.
<jsp:useBean id="NumberBean" class="jsfApp.NumberBean" scope="session" />
JSF 컨트롤에 대해 살펴보면 JSF 컨트롤은 반드시 <f:use_faces> 안에 포함되어 있어야 한다.
<f:use_faces>...</f:use_faces>
<f:use_faces> 사이에 폼이 들어 갈 수 있다.
<h:form id="addForm" formName="addForm">...</h:form>
폼 내부에 두 개의 숫자 입력 창(input_number), 하나의 숫자 출력 창(output_numbers), 명령 버튼(command_button)이 있다.
First Number:<h:input_number id="firstNumber" valueRef="NumberBean.firstNumber" /><br />Second Number:<h:input_number id="secondNumber" valueRef="NumberBean.secondNumber" /><br />Result:<h:output_number id="output" valueRef="NumberBean.result" /><br /><h:command_button id="submitButton" label="Add" commandName="submit">    <f:action_listener type="jsfApp.MyActionListener" /></h:command_button>
명령버튼을 위한 액션 리스너에 주의를 기울이자. [그림 2]는 루트 요소가 생략된 JSP 페이지 컴포넌트 트리이다.


[그림 2] adder.jsp 페이지의 컴포넌트 트리

네 개의 자식 컴포넌트를 가지고 있는 폼이 메인 컴포넌트이다.

객체 모델 작성하기

예제를 위해 2개의 숫자와 그 결과값을 기억하고 있는 자바빈즈가 필요하다. [리스트 3]은 NumberBean 자바빈즈이다.

[리스트 3] NumberBean 자바빈즈
package jsfApp;public class NumberBean {    int firstNumber  = 0;    int secondNumber = 0;    public NumberBean () {        System.out.println("Creating model object");    }    public void setFirstNumber(int number) {        firstNumber = number;        System.out.println("Set firstNumber " + number);    }    public int getFirstNumber() {        System.out.println("get firstNumber " + firstNumber);        return firstNumber;    }    public void setSecondNumber(int number) {        secondNumber = number;        System.out.println("Set secondNumber " + number);    }    public int getSecondNumber() {        System.out.println("get secondNumber " + secondNumber);        return secondNumber;    }    public int getResult() {        System.out.println("get result " + (firstNumber + secondNumber));        return firstNumber + secondNumber;    }}
액션 리스너 작성하기

명령버튼을 위한 액션 리스너는 JSF 애플리케이션의 가장 흥미로운 부분 중 하나이다. 이것은 이벤트가 실행되기 위해 리스너를 어떻게 일으키는지 나타낸다. 리스너는 단순히 콘솔 창에 메시지를 출력한다. 하지만 이벤트가 발생한 JSP 페이지의 컴포넌트 트리나 이벤트를 일으킨 컴포넌트의 정보를 나타내는 중요한 역할을 한다.

[리스트 4] 명령버튼을 위한 액션 리스너 (
MyActionListener.java)
package jsfApp;import java.util.Iterator;import javax.faces.component.UIComponent;import javax.faces.context.FacesContext;import javax.faces.event.ActionEvent;import javax.faces.event.ActionListener;import javax.faces.event.PhaseId;import javax.faces.tree.Tree;public class MyActionListener implements ActionListener {    public PhaseId getPhaseId() {        System.out.println("getPhaseId called");        return PhaseId.APPLY_REQUEST_VALUES;    }      public void processAction(ActionEvent event) {        System.out.println("processAction called");        // the component that triggered the action event        UIComponent component = event.getComponent();        System.out.println("The id of the component that fired the action event: "            + component.getComponentId());        // the action command        String actionCommand = event.getActionCommand();        System.out.println("Action command: " + actionCommand);          FacesContext facesContext = FacesContext.getCurrentInstance();        Tree tree                 = facesContext.getTree();        UIComponent root          = tree.getRoot();        System.out.println("----------- Component Tree -------------");        navigateComponentTree(root, 0);        System.out.println("----------------------------------------");    }      private void navigateComponentTree(UIComponent component, int level) {        Iterator children = component.getChildren();        // indent        for (int i=0; i<level; i++)            System.out.print("  ");        // print component id        System.out.println(component.getComponentId());        // navigate children        while (children.hasNext()) {            UIComponent child = (UIComponent) children.next();            navigateComponentTree(child, level + 1);        }    }}
애플리케이션 컴파일과 실행하기

애플리케이션을 컴파일하기 위해 myJSFApp/WEB-INF/classes 디렉토리로 이동한다. 윈도우에서는 아래와 같이 명령어를 타이핑해 넣으면 된다.
$ javac -classpath ../lib/jsf-api.jar;../lib/jsf-ri.jar; \    ../../../../common/lib/servlet.jar jsfApp/*.java
lib 디렉토리의 라이브러리 파일과 servlet.jar 파일을 사용해야 한다는 것에 주의하자. 톰캣에서는 홈 디렉토리의 하위 common/lib에서 servlet.jar 파일을 찾을 수 있다.

리눅스나 유닉스를 사용하고 있다면 라이브러리 파일을 구분하는 세미콜론을 변경해 줘야 한다.
$ javac -classpath ../lib/jsf-api.jar:../lib/jsf-ri.jar: \    ../../../../common/lib/servlet.jar jsfApp/*.java
톰캣을 실행시키고 다음과 같은 URL로 브라우저에서 접근한다.
 http://localhost:8080/myJSFApp/faces/adder.jsp
JSP 페이지 이름 전에 /faces/ 패턴을 사용했다는 점에 주의하자. 브라우저에서는 아래 [그림 3]과 같은 결과를 보게 될 것이다.


[그림 3] 애플리케이션 실행

콘솔 창에 다음과 같은 메시지를 보게 될 것이다.
Model Object Createdget firstNumber 0get secondNumber 0get result 0getPhaseId called
이제 입력 창에 2개의 숫자를 넣고 Add 버튼을 눌러보자. [그림 4]와 같이 브라우저는 덧셈의 결과를 보여줄 것이다.


[그림 4] 덧셈의 결과

콘솔창에 나타난 메시지가 중요하다.
get firstNumber 0get secondNumber 0processAction calledThe id of the component that fired the action event: submitButtonAction command: submit----------- Component Tree -------------null    addForm        firstNumber        secondNumber        output        submitButton----------------------------------------Set firstNumber 10Set secondNumber 20get firstNumber 10get secondNumber 20get result 30
결론

본 기사를 통해 여러분들은 JSF의 가장 중요한 특징에 대해 충분히 살펴보았을 것이다. 이벤트-드리븐 방식처럼 다른 서블릿/JSP 애플리케이션과 JSF 애플리케이션이 어떻게 다른지 잘 알게 되었을 것이다. 또한 하나의 JSP 페이지로 이루어진 아주 간단한 JSF 애플리케이션도 구축해 보았을 것이다. 더욱 중요한 사실은 액션 이벤트에 반응하는 액션 리스너를 작성했다는 사실이다.

실제 JSF 애플리케이션은 훨씬 복잡할 뿐만 아니라 종종 여러 개의 JSP 페이지를 가지고 있는 경우도 있다. 이런 경우에는 한 JSP 페이지에서 다른 JSP 페이지로 이동할 수 있어야 한다. 하지만 이에 관련된 내용은 본 기사의 범위를 넘어서는 것이므로 이에 대한 것은 다른 기사에서 논의할 생각이다.
부디 쿼니아완(Budi Kurniawan)은 인터넷과 객체지향 프로그래밍을 전문으로 하는 IT 컨설턴트로 마이크로소프트와 자바 기술 모두를 가르치고 있다.

'JAVA > JSP_Servlet' 카테고리의 다른 글

[펌] MVC 모델과 Observer 패턴  (0) 2004.04.01
[펌] 스트럿츠 이디엄  (0) 2004.04.01
[펌] Jakarta Digester 학습하기  (0) 2004.04.01
[펌] Torque프로젝트 사용기  (0) 2004.03.31
[펌] Log4J...  (0) 2004.03.30
Posted by tornado
|

소프트웨어 개발 방법론 - Software Development Methodology

 

방법론란 소프트웨어를 개발하기 위해 개발조직환경과 소프트웨어 및 시스템을 사용할 사용자의 환경에 적합한 소프트웨어 개발 방법을 의미한다. 소프트웨어 개발방법은 소프트웨어 공학의 대가에 의해 소프트웨어 개발방법론이라는 학문형태로 자리를 잡고 있다. 따라서 방법이라는 것은 일반적으로 개발 방법론이라 볼 수 있다. 세계적인 소프트웨어 개발방법론은 크게 구조적 분석 및 설계(Structured A/D ), 정보공학(Information Engineering), 그리고 최근에 각광받고 있는 객체지향 방법론(Object_ OrientedMethodology)이 있다. 실제적으로 보면 현재 기업이나 조직에서 사용하는 방법론은 이들을 적절하게 혼합하여 사용하고 있다. 구조적 방법론의 프로세스모델링과 정보공학의 데이타 모델링을 동시에 사용하고 있다. 이는 조직의 환경이나 개발환경에 맞게 적절하게 기본적인 방법론을 변형시켜 적용시키는 것으로 볼 수 있다.

 

  • 구조적분석 및 설계(Structured A/D): 1970년대 중반 ~ 1980년대 중반
  • 정보공학(Information Engineering) : 1980년대 후반 ~ 1990년대 중반
  • 객체지향 방법론(Object_ OrientedMethodology): 1990년대 초반 ~ 현재

 

구조적 분석 및 설계

구조적 방법론은 1970년대 이후 현재 까지 가장 널리 사용되어오던 방법론으로 구조적 분석, 구조적 설계, 구조적 프로그래밍으로 이루어져 있다. 구조적 방법론은 하향식 기능 분할(Fuctional Decomposition)에 기반 둔 프로세스 중심의 방법론으로 단순한 업무처리 시스템의 개발에는 효과적이지만 데이터 분석 및 설계 부분이 취약하며, 분석, 설계, 구현에서 상의한 모델을 사용하므로 단계 간의 변환에 많은 노력이 소요된다. 따라서 대규모의 복잡한 시스템 개발에는 적합하지 않다.

구조적 방법론은 크게 자료구조 중심적 방법론자료흐름 중심적 방법론로 분류할 수 있다("객체지향소프트웨어공학", 최영근/허계범).
데이터 구조적 방법은 업무나 기능에 의한 데이터의 변환에 관점을 둔다. 반면에 처리지향 방법은 기능을 분할하는 방법으로 문제를 독립적으로 분해하여 재검토되어 진다. 데이터 구조적 방법론이 문제에 관심을 둔것이라면 처리 지향적 벙법론은 해결에 촛점을 맞춘 것이라 할 수 있다. 구조적 방법론을 지원하고 있는 기법은 대표적으로
DFD(Data Flow Diagram), DD(Data Dictionary), Mini- Spec(Mini-Specification)이 있다.

 

정보공학

정보공학 방법론은 1980년대 후반에 조직 전반에 걸친 정보 모형화 전략을 채택하며 대두되었다. 정보공학 방법론은 전사적인 차원에서 정보시스템 기획을 통하여 단위시스템을 식별하고  이를 CASE 도구를 이용하여 분석, 설계, 구현하는 방법론으로 기획단계가 대폭 강화되고 CASE를 이용한 생산성 향상 등의 장점으로 인하여 대규모의 시스템 개발에 적합하다. 즉 전체의 계획과 모델 작성을 하고 이에따라 연관관계를 가지고 각각의 시스템을 개발한다. 그러나 정보공학 방법론은 복잡한 시스템의 모델링에 부적합하여 현재는 객체지향 방법론으로 대치되고 있는 추세이다.

정보공학 방법을 지원하고 있는 기법은 대표적으로 ERD(Entity Relation Diagram), PHD(Process Hierachy Diagram), 프로세스대엔티티 Matrix 등이다. 각 기법에 대한 자세한 사항은 기법을 참조한다. 이러한 정보공학의 특징은 아래와 같다.("객체지향소프트웨어공학", 최영근/허계범)

 

정보공학은 전체를 고려하여 시스템을 개발할 수 있도록 돕는다.
정보공학은 정보전략계획, 업무영역분석, 시스템설계, 제작, 구현의 단계를 통하여 하향식으로 개발한다.
정보공학은 각 단계에 맞춰 기업의 정보, 데이터모델, 프로세스모델, 시스템 설계에 대한 정보를 리파지토리에 축적할 수 있도록 돕니다.
정보공학은 기업전체의 정보시스템화를 실현하기 위해 틀을 만들고, 개개의 시스템은 그 틀에 맞춰 개발된다.

 

객체지향 방법론

소프트웨어와 관련된 방법론, 도구 등은 우수한 소프트웨어 구현을 위하여 지금까지 많이 연구개발되어 왔다. 그러나 현재 소프트웨어에 있어 가장 심각한 문제점은 수요가 확대되는 반면에 공급이 부족하여 소프트웨어 개발이 수요를 따라가지 못하고 있으며, 유지보수 비용의 비약적인 증가로 소프트웨어 개발 비용이 급증하고 있다는 것이다. 이러한 문제점은 근본적으로 우수한 소프트웨어 개발방법이 없었다고 할 수 있다. 이러한 문제점을 해결하기 위향 제안된 방법론이 객체지향적 접근법이다. 객체지향 접근법은 소프트웨어의 확장이나 변화를 용이하게 해줄 뿐만 아니라 기존 소프트웨어의 재사용성을 증가시켜 준다. 객체지향 시스템에서는 데이터를 다루는 프로시저를 하나로 묶어 객체라는 개념을 사용하여 실세계를 표현하고 모델링한다. 객체지향의 개념은 객체(Object), 메시지(Message), 상속(Inheritance)의 세가지 요소가 근간이 된다. 실세계의 엔티티들은 시스템 내에서 객체로 모델링되어 각 엔티티들은 인스턴스로 표현되고 공통된 특성을 가진 인스턴스들이 모여 하나의 클래스로 구성하게 된다. 즉, 객체지향 방법은 시스템을 연산과 데이터를 함께 갖는 객체의 집합체로 보고, 문제영역에서 데이터와 함수를 분리할 수 없는 것으로 취급한다. 함수들과 함수의 처리 대상이 되는 데이터는 문제 범위내에 있는 객체로 취급함으로써 문제 영역의 엔티티가 모델 세계로의 객체로 직접 사상이 가능하기 때문에 응용 영역의 구조와 밀접하게 대응하여 개발 비용의 감소를 가져오며, 시스템을 쉽게 이해할 수 있다. 객체지향 방법론을 발전시켜온 삼인방(Booch, Rumbaugh, Jacobson)에 의해 90년대 중반에 표기법을 통일되었는데, 최근에 객체지향 하면 떠오르는 것이 UML(Unified Modeling Language)이다. 이는 객체지향 방법론이 아닌 이를 지원하는 표기법(Modeling Language)이다. 객체지향을 지원하고 있는 기법으로는 Class Diagram, Use Case Diagram 외에 6개정도의 UML표기법에 의한 다이어램이 있다. Class Diagram과 Use Case Diagram에 대한 사항은 기법(Technique)을 참조한다.

 

이상으로 구조적 방법, 정보공학, 객체지향 방법을 서로 비교해 보면 아래 표와 같다("객체지향소프트웨어공학", 최영근/허계범)

 

 
정보공학 방법론
객체지향 방법론
분석
1.현 물리적 모형화
2.현 논리적 모형화
3.신논리적 모형화
4.구조적 설계
1.업무 문맥 결정
2.업무기능 파악
3.업무 결과 모형화

정보전략계획
데이터모델 작성
액티비티 모델 작성
현행시스템작성
현행시스템분석
확인
업무시스템정의

문제 정의
요구 명세화
객체와 객체특성 추출
객체의 속성 식별
객체의 연산 식별
객체의 연관성 정의
설계
1.상세 DFD
2.자료흐름 유형변환
3.자료흐름 트랜잭션
4.개략적 프로그램구조도
5.프로그램구조도 평가 및개선
6.검토
7.상세설계
1.데이터 구조 특성평가
2.데이터 기본형태 표현
3.자료구조를 SW제어계층구조로 Mapping
4.SW 계층구조 개선
5.SW프로시저 기술
1. 프로시저 정의
2.다이얼로그 설계
레이아웃설계
4.온라인 대화의 프로토타이핑
5. 프로시저 로직 설계
1. 객체의 인터페이스설정
2.객체들간의 가시성설정
접근방법
Top_DownTop_DownBottom_UpBottom_Up
DBMS
전통적인DB와 RDB전통적인DB와 RDB전통적인DB와 RDB
CASE의 저장소
전통적인 DB와 OODB
CASE
상위레벨(다이어그램)상위레벨(다이어그램)통합레벨지원통합레벨지원
방법제시자
YourdonJackson
Warnier_Orr
MartinBooch
Rumbaugh
Jacobson외

 

Posted by tornado
|

Jakarta Digester 학습하기

등록일: 2002년 11월 05일

저자: 필립(Philipp K. Janert Ph.D), 역 김대곤

XML문서를 문서와 대응되는 계층구조를 가진 자바 빈 객체로 변환하는 것은 분명 빈번하게 요구되는 작업 중의 하나이다. 필자가 이전에 논했었던 'Simple XML Parsing with SAX and DOM'라는 기사에서 SAX와 DOM이라는 표준 API를 사용하여 이러한 작업을 수행하는 방법에 대해 기술하였다.

SAX과 DOM이 강력하고, 유연함에도 불구하고, 실제로 이 두 가지 API는 특정한 작업을 간단하게 수행하기에는 너무 저수준인 API이다. 뿐만 아니라 unmarshalling 작업(XML문서를 객체로 변환하는 작업) 자체에 상당한 코딩을 해주어야 한다. 즉, SAX를 사용할 때에는 반드시 parse-stack이 관리되어야 하며, DOM를 사용할 때에는 반드시 DOM-tree를 사용해야 한다는 말이다.

Unmarshalling 작업은 Apache Jakarta Commons Digester 프레임워크가 해결할 수 있는 부분이다.

Jarkarta Digester 프레임워크

Jakarta Digester 프레임워크는 Jakarta Struts Web toolkit의 일부로 시작하여 발전되었다. 원래는 주요한 struts-config.xml를 처리하기 위해 개발되었으나 보다 일반적으로 유용하다는 것이 인식되어, Jakarta Commons 프로젝트로 옮기게 되었다. 이 프로젝트는 재사용 가능한 자바 컴포넌트의 리포지토리(repository)를 제공하는 것을 목표로 하고 있다. 최신 버전인 Digester1.3이 2000년 8월 13일에 발표되었다.

Digester 클래스를 통해 애플리케이션 프로그래머는 파서가 XML 문서의 간단한 특정 패턴을 만났을 때, 필요한 작업이 자동적으로 수행되게 해준다. Digester 프레임워크는 미리 정의된 10개의 규칙을 가지고 있다. 이러한 규칙들은 XML문서를 unmashalling하는데 필요한 대부분의 작업들을 포함하고 있지만 필요할 때마다 각 사용자들이 직접 규칙을 정의할 수도 있게 되어있다.

예제 문서와 빈즈

이제 보게 될 예제에서는 이전 기사에서 사용한 XML 문서을 unmarshall할 것이다.
<?xml version="1.0"?><catalog library="somewhere">   <book>      <author>Author 1</author>      <title>Title 1</title>   </book>   <book>      <author>Author 2</author>      <title>His One Book</title>   </book>   <magazine>      <name>Mag Title 1</name>      <article page="5">         <headline>Some Headline</headline>      </article>      <article page="9">         <headline>Another Headline</headline>      </article>   </magazine>   <book>      <author>Author 2</author>      <title>His Other Book</title>   </book>   <magazine>      <name>Mag Title 2</name>      <article page="17">         <headline>Second Headline</headline>      </article>   </magazine></catalog>
자바 빈(Java Bean) 클래스도 한가지 주요한 변화를 제외하고는 XML 파일과 동일하다. 이전 기사에서 빈 클래스들을 하나의 소스 파일로 정의하기 위해 package를 선언하였다. 그렇지만 Digester 프레임워크를 사용할 때에는 이러한 방법을 사용할 수 없다. 모든 클래스는 public으로 선언되어야 한다. public으로 선언하는 것은 자바 빈즈의 규약에서도 요구되는 부분이다.
import java.util.Vector;public class Catalog {   private Vector books;   private Vector magazines;   public Catalog() {      books = new Vector();      magazines = new Vector();   }   public void addBook( Book rhs ) {      books.addElement( rhs );   }   public void addMagazine( Magazine rhs ) {      magazines.addElement( rhs );   }   public String toString() {      String newline = System.getProperty( "line.separator" );      StringBuffer buf = new StringBuffer();      buf.append( "--- Books ---" ).append( newline );      for( int i=0; i < books.size(); i++ ){         buf.append( books.elementAt(i) ).append( newline );      }      buf.append( "--- Magazines ---" ).append( newline );      for( int i=0; i < magazines.size(); i++ ){         buf.append( magazines.elementAt(i) ).append( newline );      }      return buf.toString();   }}
public class Book { private String author; private String title; public Book() {} public void setAuthor( String rhs ) { author = rhs; } public void setTitle( String rhs ) { title = rhs; } public String toString() { return "Book: Author='" + author + "' Title='" + title + "'"; }}
import java.util.Vector;public class Magazine { private String name; private Vector articles; public Magazine() { articles = new Vector(); } public void setName( String rhs ) { name = rhs; } public void addArticle( Article a ) { articles.addElement( a ); } public String toString() { StringBuffer buf = new StringBuffer( "Magazine: Name='" + name + "' "); for( int i=0; i < articles.size(); i++ ){ buf.append( articles.elementAt(i).toString() ); } return buf.toString(); }}
public class Article { private String headline; private String page; public Article() {} public void setHeadline( String rhs ) { headline = rhs; } public void setPage( String rhs ) { page = rhs; } public String toString() { return "Article: Headline='" + headline + "' on page='" + page + "' "; }}
패턴과 규칙

Digester 클래스는 패턴(pattern)과 규칙(rule)을 기준으로 입력되는 XML 문서를 처리한다. 패턴들은 XML 문서 Tree 구조의 이름과 위치에 근거해서 XML Element와 일치해야 한다. 매칭 패턴을 정의하는 문법은 Xpath의 매칭 패턴과 유사하다. Catalog패턴은 <catalog> Element와 일치하고, catalog/book 패턴은 <catalog> Element 안에 포함된(문서의 다른 부분에서 사용되지 않는) <book> Element와 일치한다.

모든 패턴은 절대 경로이다. 즉 루트(root) Element에서 시작되는 전체 경로가 명시되어야 한다. *(와일드카드 문자)가 사용된 패턴만이 예외로 인정된다. 즉, */name 패턴은 문서에 어느 곳에 위치한 <name> Element와 매칭될 수 있다. 또한 모든 경로가 절대경로이기 때문에 root Element를 특별히 명시할 필요가 없다.

Digester 클래스는 정의된 패턴을 만나면 패턴과 연관된 작업을 수행한다. 이때, Digester 프레임워크는 물론 SAX 파서을 사용한다. 실제로 Digester 클래스는 org.xml.sax.ContentHandler를 구현하고 있으며, parse stack를 관리하고 있다. Digester클래스에서 사용하는 모든 규칙(rule)은 org.apache.commons.digester.Rule 클래스를 상속받아야 한다. 이 Rule 클래스는 내부에서 SAX의 ContentHandler의 재귀함수(매칭된 Element가 시작 태그와 종료 태그를 만났을 때 호출되는 begin, end 메소드)와 유사한 메소드들을 사용한다.

Body() 메소드는 매칭된 Element안에 포함된 내용들을 처리하기 위해 호출된다. 마지막으로, finish() 메소드가 있는데, 이것은 종료 태그의 처리가 완료되었을 때, 필요한 마지막 정리작업을 위해 호출된다. 그러나 프레임워크에 포함된 표준 규칙들은 원하는 모든 기능들을 이미 제공하기 때문에 대부분의 애플리케이션 개발자들은 이러한 함수에 대하여 신경쓸 필요가 없다.

XML문서를 unmarshall하기 위해서는 org.apache.commons.digester.Digester 객체를 생성하고, 필요한 경우 객체을 설정하고, 처리하기 원하는 패턴과 규칙들을 정의하고, 마지막으로 XML문서에 대한 참조값을 parse() 메소드에 넘겨준다. 이러한 일련의 작업들은 아래에 있는 DigesterDriver 클래스에 나타나 있다. XML문서의 파일명은 실행시 인자로 넘겨주어야 한다.
import org.apache.commons.digester.*;import java.io.*;import java.util.*;public class DigesterDriver {   public static void main( String[] args ) {      try {         Digester digester = new Digester();         digester.setValidating( false );         digester.addObjectCreate( "catalog", Catalog.class );         digester.addObjectCreate( "catalog/book", Book.class );         digester.addBeanPropertySetter( "catalog/book/author", "author" );         digester.addBeanPropertySetter( "catalog/book/title", "title" );         digester.addSetNext( "catalog/book", "addBook" );         digester.addObjectCreate( "catalog/magazine", Magazine.class );         digester.addBeanPropertySetter( "catalog/magazine/name", "name" );         digester.addObjectCreate( "catalog/magazine/article", Article.class );         digester.addSetProperties( "catalog/magazine/article", "page", "page" );         digester.addBeanPropertySetter( "catalog/magazine/article/headline" );          digester.addSetNext( "catalog/magazine/article", "addArticle" );         digester.addSetNext( "catalog/magazine", "addMagazine" );         File input = new File( args[0] );         Catalog c = (Catalog)digester.parse( input );         System.out.println( c.toString() );      } catch( Exception exc ) {         exc.printStackTrace();      }   }}
이 예제에서는 DTD를 사용하여 XML문서의 유효성을 검사할 수 없다. Catalog 문서에 DTD를 지정하지 않았기 때문이다. 패턴과 관련된 규칙들을 직접 정의하였다. ObjectCreateRule은 지정된 클래스의 인스턴스를 생성하여 parse stack에 넘겨준다. SetPropertiesRule은 bean 속성에 현재 Element의 attribute 값을 지정한다. 이 메소드의 첫 번째 인자는 attribute의 이름이고, 두 번째 인자는 속성의 이름을 나타낸다.

SetPropertiesRule이 attribute로부터 값을 읽어온다면, BeanPropertySetterRule은 현재 Element에 안에 있는 raw character data를 가져온다. BeanPropertySetterRule를 사용할 때에는 Bean 속성의 이름을 특별히 정의할 필요가 없다. BeanPropertySetterRule은 특정한 설정이 없는 경우, Element의 이름을 사용하기 때문이다. 위의 예제에서는 catalog/magazine/article/headline에 규칙을 지정하면서 이러한 설정법을 사용하였다. 마지막으로 남은 SetNextRule은 생성한 객체를 parse stack의 처음으로 옮기고, 생성된 객체를 가진 상위 객체의 메소드로 넘겨준다. 이것은 주로 만들어진 Bean를 상위 클래스에 추가할 때 사용된다.

동일한 패턴에 대해 다수의 규칙을 적용할 수도 있다. 이때에는 Digester 클래스에 입력된 순서대로 규칙이 실행될 것이다. 예를 들어 위 프로그램에서 catalog/magazine/article<article> Element를 다룬 것을 살펴보면 알맞은 Bean 객체를 먼저 생성하고, page의 속성값을 지정하고, 마지막으로 생성된 객체를 상위 객체인 magazine에 추가하였다는 것을 알 수 있다.


Java & XML Data Binding

참고 도서

Java & XML Data Binding
Brett McLaughlin




임의의 함수 호출

Bean의 속성을 설정하는 것 뿐만 아니라 객체에 정의된 임의의 메소드를 호출할 수도 있다. 이것은 CallMethodRule를 사용하여 메소드의 이름과 필요시 인자의 수와 속성을 넘겨줌으로써 가능하다. CallParamRule를 통해 호출되는 메소드에 파라미터 값을 전달할 수 있다. 이 값은 Element의 속성값, 또는 Element의 데이터에서 가져올 수 있다. 예를 들어 위의 예제에서 BeanPropertySetterRule를 사용하기 보다는, 데이터를 파라미터로 넘기면서, 속성 setter 메소드를 직접 호출하여 동일한 작업을 수행할 수 있다.
digester.addCallMethod( "catalog/book/author", "setAuthor", 1 );digester.addCallParam( "catalog/book/author", 0 );
첫번째 라인은 호출할 메소드의 이름과 전달되는 인자의 수를 지정하고, 두 번째 라인은 <author> Element의 데이터를 함수의 인자값으로 사용하고, 인자의 array의 첫번째 값으로 전달함을 의미한다. 이 때 Element의 attribute값을 인자로 넘길 수 있다. (digester.addCallParam("catalog/book/author", 0, "author");)

여기서 주의할 점이 하나 있다. Digester.addCallMethod("pattern", "methodName", 0)이 전달되는 인자가 없다는 의미가 아니라는 것이다. 이것은 호출되는 메소드의 인자값을 현재 Element의 데이터 값을 사용하겠다는 의미이다. 그러므로 위의 예제에서 사용한 BeanPropertySetterRule을 다음과 같이 변환할 수 있다.
digester.addCallMethod( "catalog/book/author", "setAuthor", 0 );
인자를 취하는 않는 메소드를 호출하는 경우에는 digester.addCallMethod("pattern", "methodName")를 사용한다.

표준 규칙 요약

다음은 표준 규칙에 대한 간단한 설명이다.

Creational
  • ObjectCreateRule: 객체의 기본 생성자를 사용하여 객체를 생성하고, 이를 stack에 넘겨준다. Element의 처리가 끝날 때 객체는 반환된다. 객체를 지정하는 방법은 Class 객체를 사용하는 법과 전체 클래스의 이름을 사용하는 법이 있다.
  • FactoryCreateRule: 특정 Factory 객체를 사용하여 객체를 생성하고, 이를 스택에 넘겨준다. 기본 생성자를 제공하지 않는 클래스를 사용할 때 유용하다. 이 Factory 클래스는 org.apache.commons.digester.ObjectCreationFactory 인터페이스를 구현해야 한다.
Property Setters
  • SetPropertiesRule: Element의 속성값을 사용하여 top-level 수준의 하나 또는 여러 개의 속성값을 지정한다. Attribute의 값과 속성의 값은 String 배열을 사용하여 전달한다.
  • BeanPropertySetterRule: Element의 데이터를 top-level 수준의 속성값을 지정한다.
  • SetPropertyRule: 현재 XML Element의 attribute과 attribute의 값을 Bean의 속성과 속성값으로 지정한다.
Parent/Child Management
  • SetNextRule: 생성한 객체를 stack에 넘겨주고, 상위 객체에 정의된 동일한 이름의 메소드를 호출한다. 일반적으로 완성된 객체를 상위 객체에 추가할 때 사용한다.
  • SetTopRule: stack의 second-to-top 객체를 top-level 객체로 전달한다. 이것은 Child 객체에서 setParent 메소드가 사용될 경우에 유용하게 사용된다.
Arbitrary Method Calls
  • CallMethodRule: top-level Bean의 임의의 메소드를 호출한다. 호출되는 메소드의 인자의 수는 제한이 없다. CallParamRule에 의해 인자의 값이 전달된다.
  • CallParamRule: 메소드의 인자의 값을 표시한다. 인자의 값은 Element의 attribute나 데이터로부터 얻어올 수 있다. 이 규칙은 정수 인덱스로 지정되는 인자의 위치를 요구한다.
XML에서의 규칙: xmlrules 패키지 사용하기

지금까지 패턴과 규칙을 컴파일 시점에 설정하는 것에 대하여 살펴보았다. 개념적으로는 간단하고, 직접적으로 보이지만 이러한 방법은 불편하다. 전체 프레임워크는 실행시 각 구조와 데이터를 인식하고 처리하기 위한 것인데, 위의 예제에서는 컴파일 시점에서 이미 구조와 데이터 인식 방법이 결정되고 있기 때문이다. 이러한 방식은 많은 코드를 필요로 하며, 프로그램이라기보다는 설정에 가깝다.

org.apahce.commons.digester.xmlrules 패키지는 위에서 언급한 문제를 다루고 있다. DigesterLoader 클래스는 패턴/규칙이 정의된 XML문서를 이용하여, Digester 객체를 생성한다. Digester 클래스를 설정하는 XML파일은 xmlrules 패키지에 포함된 digester-rules.dtd에 유효한 문서여야 한다. 즉, dtd의 규약대로 작성되어야 한다.

아래는 예제 프로그램을 위한 Digester 클래스 설정 XML파일이다. 몇 가지를 살펴보자.

패턴은 두 가지 방법으로 설정될 수 있다. 첫째 규칙을 나타내는 XML의 attribute로 표시될 수 있고, 둘째 <pattern> Element로 정의될 수 있다. 두 번째 설정방법은 규칙 Element가 포함된 모든 곳에서는 모두 사용할 수 있다. 두 방식은 혼합해서 사용할 수 있으며, <pattern> Element안에 <pattern> Element가 올 수 있다.

<set-properties-rules> Element안에 사용된 <alias> Element는 XML attribute와 Bean의 속성을 매핑할 때 사용한다.

마지막으로, 현재의 Digester 패키지에서는 설정파일에서 SetPropertySetterRule를 사용할 수 없다. 대신, CallMehtodRule를 사용하여 동일한 작업을 수행할 수 있다.
<?xml version="1.0"?><digester-rules>   <object-create-rule pattern="catalog" classname="Catalog" />   <set-properties-rule pattern="catalog" >      <alias attr-name="library" prop-name="library" />   </set-properties-rule>   <pattern value="catalog/book">      <object-create-rule classname="Book" />      <call-method-rule pattern="author" methodname="setAuthor"	                paramcount="0" />      <call-method-rule pattern="title" methodname="setTitle" 	                paramcount="0" />      <set-next-rule methodname="addBook" />   </pattern>   <pattern value="catalog/magazine">      <object-create-rule classname="Magazine" />      <call-method-rule pattern="name" methodname="setName" paramcount="0" />      <pattern value="article">         <object-create-rule classname="Article" />         <set-properties-rule>            <alias attr-name="page" prop-name="page" />         </set-properties-rule>         <call-method-rule pattern="headline" methodname="setHeadline" 		           paramcount="0" />         <set-next-rule methodname="addArticle" />      </pattern>      <set-next-rule methodname="addMagazine" />   </pattern></digester-rules>
실제 작업을 Digester 클래스와 DigesterLoader에서 수행하면 DigesterDriver 클래스는 아주 단순해진다. 이제 DigesterDriver 클래스를 실행하기 위해서는 catalog XML문서와 rules.xml문서를 실행하는 순서대로 인자로 넘겨줘야 한다. 이때 rules.xml 파일은 File 클래스나 org.xml.sax.InputSource 클래스가 아닌 URL을 요구한다.
import org.apache.commons.digester.*;import org.apache.commons.digester.xmlrules.*;import java.io.*;import java.util.*;public class XmlRulesDriver {   public static void main( String[] args ) {      try {         File input = new File( args[0] );         File rules = new File( args[1] );         Digester digester = DigesterLoader.createDigester( rules.toURL() );         Catalog catalog = (Catalog)digester.parse( input );         System.out.println( catalog.toString() );        } catch( Exception exc ) {         exc.printStackTrace();      }   }}
결론

이상으로 Jakarta Commons Digester 패키지에 대한 간단한 설명을 끝내겠다. 물론 더 많은 내용이 있지만. 여기에서 다루지 않았던 하나의 주제는 XML 네임스페이스(Namespace)에 관한 것이다. Digester는 특정 네임스페이스에 속한 Element들에 패턴과 규칙을 정의할 수 있다.

우리는 위에서 Rule클래스를 사용하여 사용자가 직접 작성한 규칙을 정의할 수 있다는 사실을 살펴보았다. 또한 Digester 클래스는 사용자가 정의한 push(), peek(), pop() 메소드를 통해, 개발자로 하여금 parse stack를 직접 수정할 수 있는 기능을 제공한다.

최근에 RSS(Rich-Site-Summary)를 다루는 Digester 클래스를 제공하는 추가 패키지가 제공되었다. 이 패키지는 Javadoc을 사용하여 살펴볼 수 있다.

참고사이트
필립(Philipp K. Janert Ph.D)은 소프트웨어 프로젝트 컨설턴트이자, 서버 프로그래머와 설계자로 활동하고 있다.

'JAVA > JSP_Servlet' 카테고리의 다른 글

[펌] 스트럿츠 이디엄  (0) 2004.04.01
[펌] JavaServer Faces 입문  (0) 2004.04.01
[펌] Torque프로젝트 사용기  (0) 2004.03.31
[펌] Log4J...  (0) 2004.03.30
[펌] JSF 사용하기  (0) 2004.03.29
Posted by tornado
|
 

■Torque사용을 위한 기초작업들

http://jakarta.apache.org/builds/jakarta-turbine/torque/release/3.1/에서

torque-3.1.zip  <==실행

Torque.properties의 편집

클래스 패스는 lib안의 필요한 것(어떤 것? )에 통한다

클라이언트의 코딩과 실행

torque-gen-3.1.zip  <==생성

build.properties의 편집

JDBC 드라이버는 lib에 카피

Ant 태스크를 실행


torque-3.1.zip에 있는 lib폴더 내부의 jar파일들을 사용하는 웹폴더(즉,C:\Tomcat 5.0\webapps-test)의 C:\Tomcat 5.0\webapps-test\ROOT\WEB-INF\lib 에 저장한다. lib폴더엔 스트럿의 jar파일과 중복되는 부분이 있어 torque의 lib폴더의 jar파일의 일부만 가져와서 덮어쓴다. 그 내용은 다음과같다

avalon-framework-4.1.4.jar

commons-configuration-1.0-dev-3.20030607.194155.jar

commons-dbcp-20030825.184428.jar

commons-pool-20030825.183949.jar

jcs-20030822.182132.jar

jdbc-2.0.jar

jndi-1.2.1.jar

junit-3.8.1.jar

log4j-1.2.8.jar

logkit-1.0.1.jar

stratum-1.0-b3.jar

torque-3.1.jar

village-2.0-dev-20030825.jar

xercesImpl-2.0.2.jar

xmlParserAPIs-2.0.2.jar


C:\javatool\torque-3.1에 있는 설정파일들을 사용하는 웹폴더(C:\Tomcat 5.0\webapps-test\ROOT\WEB-INF)의 WEB-INF에 복사한다.


web.xml 에 Torque를 설정해준다.

<servlet>

      <servlet-name>initTorque</servlet-name>

<!--torque를 사용할 때 매번 초기화 하는부분을 이 설정파일(web.xml)에서 설정해준다.-->

      <servlet-class>initialize.TorqueInit</servlet-class>

      <init-param>

      <param-name>config</param-name>

<!--torque를 사용할 때 참조할 properties파일 위치 설정-->

      <param-value>/WEB-INF/classes/properties/Torque.properties</param-value>

      </init-param>

      <init-param>

         <param-name>debug</param-name>

         <param-value>0</param-value>

      </init-param>

      <load-on-startup>1</load-on-startup>

    </servlet>


 ========================================================

<!--torque를 사용할 때 매번 초기화 하는부분을 이 설정파일(web.xml)에서 설정해준다.-->

      <servlet-class>initialize.TorqueInit</servlet-class>  파일이름 : TorqueInit.java(임의로 작성)

package initialize;


/**

* @author  : Kwoen BongJe(2002-12-21)

*/


import javax.servlet.ServletException;

import javax.servlet.http.HttpServlet;


import org.apache.torque.Torque;


public class TorqueInit extends HttpServlet{

    private static boolean hasInitialized = false;


        public void init() throws ServletException{

                

                synchronized(TorqueInit.class){

                        

                        if(!hasInitialized){

                                try{

                                        String propFileName = getServletConfig().getInitParameter("config");

                                if(propFileName==null) {

                                   propFileName="/WEB-INF/classes/properties/Torque.properties";

                                }

                                String propFile = getServletContext().getRealPath(propFileName);

                                        Torque.init(propFile);

                                        hasInitialized = true;

                                }catch(Exception e){

                                        throw new ServletException("\nComstar.framework : Can't initialize Torque!\n"+e);

                                }

                        }

                }

                

        }

}

 

===================================================

<!--torque를 사용할 때 참조할 properties파일 위치 설정-->

      <param-value>/WEB-INF/classes/properties/Torque.properties</param-value> 프로퍼티 파일을 사용환경에 맞게 변경해주어야 한다. 아래는 예제파일


 

torque.applicationRoot = /usr/local/tomcat

log4j.category.org.apache.torque = ALL, org.apache.torque

log4j.appender.org.apache.torque = org.apache.log4j.FileAppender

log4j.appender.org.apache.torque.file = ${torque.applicationRoot}/logs/testdb/torque.log

log4j.appender.org.apache.torque.layout = org.apache.log4j.PatternLayout

log4j.appender.org.apache.torque.layout.conversionPattern = %d [%t] %-5p %c - %m%n

log4j.appender.org.apache.torque.append = false


torque.defaults.pool.logInterval = 0

torque.defaults.pool.connectionWaitTimeout = 10

torque.defaults.pool.defaultMaxConnections = 80

torque.defaults.pool.maxExpiryTime = 3600


torque.defaults.connection.driver = org.gjt.mm.mysql.Driver

torque.defaults.connection.url = jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=euc-kr

torque.defaults.connection.user = test

torque.defaults.connection.password = test


torque.database.default=testdb

torque.database.testdb.adapter=mysql


torque.dsfactory.testdb.factory=org.apache.torque.dsfactory.SharedPoolDataSourceFactory

torque.dsfactory.testdb.pool.defaultMaxActive=10

torque.dsfactory.testdb.pool.testOnBorrow=true

torque.dsfactory.testdb.pool.validationQuery=SELECT 1

torque.dsfactory.testdb.connection.driver = org.gjt.mm.mysql.Driver

torque.dsfactory.testdb.connection.url = jdbc:mysql://localhost:3306/testdb?useUnicode=true&characterEncoding=euc-kr

torque.dsfactory.testdb.connection.user = test

torque.dsfactory.testdb.connection.password = test


torque.idbroker.cleverquantity=true

torque.manager.useCache = true


======

 

설치한 torque-gen폴더 아래에 C:\javatool\torque-gen-3.1 torque가 사용될 폴더를 만들어준다 폴더이름은 임으로 만들지만 프로젝트 명으로 하는 것이 좋겠다.


C:\javatool\torque-gen-3.1\webapps-address 아래에 schema 폴더를 만든다.


schema 폴더에 사용할 DB에서의 사용할 태이블 구조를 xml형식으로 만든다

이때 파일 이름의 형식은 “*-schema.xml”의 형식을 따라야 한다. 이는 C:\javatool\torque-gen-3.1에 있는build-torque.xml파일을 torque가 참조한다. build-torque.xml파일에는 DB를 생성과 참조 등등이 정의 되어있다.

참고자료 : http://db.apache.org/torque-31/generator/schema-reference.html

<예>

<?xml version="1.0" encoding="ISO-8859-1" standalone="no"?>

<!DOCTYPE database SYSTEM "http://db.apache.org/torque/dtd/database_3_1.dtd">


<!-- ==================================================================== -->

<!--                                                                      -->

<!-- I D  B R O K E R  S C H E M A                                        -->

<!--                                                                      -->

<!-- ==================================================================== -->

<!-- This is the XML schema use by Torque to generate the SQL for         -->

<!-- ID_TABLE table used by the id broker mechanism in Torque.            -->

<!-- ==================================================================== -->

<!-- @author: <a href="mailto:jvanzyl@apache.org">Jason van Zyl</a>       -->

<!-- @version $Id: id-table-schema.xml,v 1.2 2003/07/24 12:40:41 mpoeschl Exp $ -->

<!-- ==================================================================== -->


<database name="struts-address">

'JAVA > JSP_Servlet' 카테고리의 다른 글

[펌] JavaServer Faces 입문  (0) 2004.04.01
[펌] Jakarta Digester 학습하기  (0) 2004.04.01
[펌] Log4J...  (0) 2004.03.30
[펌] JSF 사용하기  (0) 2004.03.29
[펌] Struts, Tiles, JavaServer Faces 통합하기  (0) 2004.03.29
Posted by tornado
|

[펌] Log4J...

JAVA/JSP_Servlet 2004. 3. 30. 14:11

베낌 + 모방  + 편집.. ^^

 

/**
 * Created on 2004. 3. 05.
 *
 *
 * @author 짱가
 *
 * *******************************************************
 *                             코드수정히스토리
 * 날짜                               작업자                      내용
 * 2004. 3. 3.                       짱가
 * *******************************************************
 *
 */
/**
 * System.out.println("[WARN]  : " +  e.toSting()  );
 * 이런식으로 코딩해본적 있을것이다. 혹시라도 로그를 빼달라고 하면 모두들
 * System.out.println("[WARN] : " +  e.toSting()  ); //라고 해본사람들이 꽤될것이다.
 * 컴파일 했다가 말았다가 어느게 시스템 Debug용이고 어느게 뭔지 .....
 * 프로그래밍이 끝나고 운용에 들어가면서 속도 향상을 위해 클라이언트가 FATAL에러만 빼고 모두 빼달라고 했다던지
 * 혹은 운용중에 에러를 잡기위해 어느단계까지는 모두 나오게 해야한다던지 할때 이런기능이 없다면
 * 아마 소스코드는 IF문과 System.out.println() 으로 뒤범벅이 될것이다.
 *
 *
 * 로그레벨이라는 개념을 사용하면 약간 수고를 덜수 있다.
 * DEBUG < INFO < WARN < ERROR < FATAL
 * 보통 로그level에서 DEBUG가 가장작고 FATAL이 가장크다. 그래서 위의 예제 결과는 아래와 같이 나온다.
 * 결과를 봐서 알겠지만 예제에서 WORN을 LogLevel로 삼았기 때문에 위의 그림과 같게 아래의 결과가 나온다.
 * java.lang.Object
 *|
 * +--org.apache.log4j.Category
 *       |
 *       +--org.apache.log4j.Logger
 * 보는 바와 같이 Logger class는 Category의 child class였다.
 *
 *
 *
 *
 * API의 일부....
 *
 * Constructor Summary
 * protected Logger(String name)
 *
 * Method Summary
 *
 * 1) static Logger getLogger(Class clazz) --- Same as calling getLogger(clazz.getName()).
 *
 * 2) static Logger getLogger(String name) --- Retrieve a logger by name.
 *
 * 3) static Logger getLogger(String name, LoggerFactory factory) ---Like getLogger(String)
 *     except that the type of logger instantiated depends on the type
 *     returned by the LoggerFactory.makeNewLoggerInstance(java.lang.String) method of the factory parameter.
 *
 * 4) static Logger getRootLogger() --- Retrieve the root logger. 
 *
 * ----------------------------------------------------------------------------------------------------
 * 1)과 2번은 클라스로 근본적으로 같고 예를들면 Logger.getLogger( xxxx.class.getName( ) )
 * 이런식으로 쓰므로 1번과 2번은 근본적으로 같다고 볼수 있다.
 * 3)은 LoggerFactory에 의해 불리우는것에 따라 logger type이 달라지고....
 * 4)째는 모든 Logger는 부모가 있는데 이 부모logger를 사용가능하지만 별로 권장하지 않는다고 한다.
 * http://www.onjava.com/lpt/a/2525 을 참조하기 바란다.
 * 이글에 보면 Tomcat에서의 사용법까지 나와있으며 어떻게 로그를 남기는것이 효율적인지에 대하여 적고 있다.
 *--------------------------------------------------------------------------------------
 *
 * logger class는 Category 클라스의 setLevel( )을 호출하게 되면 level이 정해진다.
 * http://logging.apache.org/log4j/docs/api/org/apache/log4j/Level.html 에서 보는바와같이 level의 종류는
 * 코드:
 * static Level ALL
 * static Level DEBUG
 * static Level ERROR
 * static Level FATAL
 * static Level INFO
 * static Level OFF
 * static Level WARN 와같다.
 * http://logging.apache.org/log4j/docs/api/org/apache/log4j/Category.html
 * 실제 로그를 뿌리는 메서드들을 발견할수 있을것이다. 
 *

 * Log4j 는 기본적으로 다섯개의 우선권(이하 Priority) 등급으로 메세지를 로깅할 수 있다.
 * 1. 완성된 어플리케이션에서는 출력되지 않아야 할 디버깅 메세지들을 쓰기위해 debug 를 사용하라.
 * 2. 어플리케이션의 verbose 모드에서 출력될만한 메세지들을 로깅하기 위해 info 를 사용하라. 
 * 3. 어플리케이션이 이상없이 계속 실행될 수 있는 정도의 경고메세지를 로깅하기 위해 warn 을 사용하라.
 * 4. 어플리케이션이 그럭저럭 돌아갈만한 정도의 에러베세지를 로깅하기 위해 error 를 사용하라.
   예를들어 관리자에 의해 주어진 설정인자가 올바르지 않아 하드코딩된 기본값을 사용해야 할 경우.
 * 5. 로깅후에 애플리케이션이 비정상적으로 종료될 치명적인 메세지를 로깅하기 위해 fatal 을 사용하라. 

 *****************************************************************************
 *
 *  여기서 작성한 클래스는 각 기능 별로 txt로 생성하는 로그
 *                                          html로 생성하는 로그
 *           pattern matching을 사용하는 로그
 *            xml로 생성하는 로그
 *           그리고 configuration file로 xml을 사용하여 log파일로 생성하고
 *           그 log파일이 일정 사이즈가 될때 증가시키는 로그로 나뉘어져 있다
 *  참고로 각 실행시에 main으로 실행한후 메소드 명만 수정하였으므로 실행은 그렇게 하면 실행이 가능하다.
 *
 *****************************************************************************
 *
 */

import java.io.FileOutputStream;

import org.apache.log4j.BasicConfigurator;
import org.apache.log4j.ConsoleAppender;
import org.apache.log4j.FileAppender;
import org.apache.log4j.HTMLLayout;
import org.apache.log4j.Level;
import org.apache.log4j.Logger;
import org.apache.log4j.PatternLayout;
import org.apache.log4j.SimpleLayout;
import org.apache.log4j.WriterAppender;
import org.apache.log4j.xml.DOMConfigurator;
import org.apache.log4j.xml.XMLLayout;

public class Logging4J {

 //  Logger.getInstance()의 실체는 Category.getInstance()이며,
 //  Category가 반환되기때문이다. 그리고 이 Category 클래스나 getInstance()
 //     메소드는 추천 되지 않는 API인것으로 알려져있다.
 //  Logger의 취득은 Logger.getLogger()로 하기 바란다
 static Logger logger = Logger.getLogger(Logging4J.class);

 public void consoleLog() {

  Logger logger = Logger.getLogger("loggerTest");
  BasicConfigurator.configure();
  logger.setLevel(Level.WARN);
  logger.debug("this is debug!!!");
  logger.info("this is info!!!");
  logger.warn("this is warn!!!");
  logger.error("this is error!!!");
  logger.fatal("this is fatal!!!");

 }
 /**
  * 생성 로그  txt
  * DEBUG - debug!!!
  * INFO - info!!!
  * WARN - warn!!!
  * ERROR - error!!!
  * FATAL - fatal!!!     *
  */
 public void txtLog() {

  SimpleLayout layout = new SimpleLayout();

  FileAppender appender = null;
  try {
   appender = new FileAppender(layout, "Logging4J.txt", false);
  } catch (Exception e) {
  }

  //  FileAppender(Layout layout, String filename)  : constructor
  //  FileAppender(Layout layout, String filename, boolean append)

  logger.addAppender(appender);

  logger.setLevel(Level.DEBUG);
  logger.debug("debug!!!");
  logger.info("info!!!");
  logger.warn("warn!!!");
  logger.error("error!!!");
  logger.fatal("fatal!!!");
 }
 // html은 너무 길다.. ^^;;
 public void htmlLog() {

  HTMLLayout layout = new HTMLLayout();

  WriterAppender appender = null;
  try {
   FileOutputStream output = new FileOutputStream("Logging4J.html");
   appender = new WriterAppender(layout, output);
  } catch (Exception e) {
  }

  //  FileAppender(Layout layout, String filename)  : constructor
  //  FileAppender(Layout layout, String filename, boolean append)
  //  Logger.getInstance()의 실체는 Category.getInstance()이며,
  //  Category가 반환되기때문이다. 그리고 이 Category 클래스나 getInstance() 메소드는
  //     추천 되지 않는 API인것으로 알려져있다.
  //  Logger의 취득은 Logger.getLogger()로 하기 바란다

  logger.addAppender(appender);

  logger.setLevel(Level.DEBUG);
  logger.debug("HERE is some Debug!!!");
  logger.info("HERE is some info!!!");
  logger.warn("HERE is some WARN!!!");
  logger.error("HERE is some ERROR!!!");
  logger.fatal("HERE is some FATAL!!!");
 }
 /**
  * Classname: Logging4J
  * Date in ISO8601 format: 2004-03-04 11:08:43,950
  * 이벤트 위치: Logging4J.main(Logging4J.java:194)
  * 메시지 : Here is some DEBUG
  * 
  * Classname: Logging4J
  * Date in ISO8601 format: 2004-03-04 11:08:43,950
  * 이벤트 위치: Logging4J.main(Logging4J.java:195)
  * 메시지 : Here is some INFO
  * 
  * Classname: Logging4J
  * Date in ISO8601 format: 2004-03-04 11:08:43,950
  * 이벤트 위치: Logging4J.main(Logging4J.java:196)
  * 메시지 : Here is some WARN
  * 
  * Classname: Logging4J
  * Date in ISO8601 format: 2004-03-04 11:08:43,950
  * 이벤트 위치: Logging4J.main(Logging4J.java:197)
  * 메시지 : Here is some ERROR
  * 
  * Classname: Logging4J
  * Date in ISO8601 format: 2004-03-04 11:08:43,950
  * 이벤트 위치: Logging4J.main(Logging4J.java:198)
  * 메시지 : Here is some FATAL
  *
  *
  */
 public void patternLog() {
  // Note, %n is newline
  String pattern = "Classname: %C %n";
  pattern += "Date in ISO8601 format: %d{ISO8601} %n";
  pattern += "이벤트 위치: %l %n";
  pattern += "메시지 : %m %n %n";

  PatternLayout layout = new PatternLayout(pattern);
  ConsoleAppender appender = new ConsoleAppender(layout);

  logger.addAppender(appender);
  logger.setLevel((Level) Level.DEBUG);

  logger.debug("Here is some DEBUG");
  logger.info("Here is some INFO");
  logger.warn("Here is some WARN");
  logger.error("Here is some ERROR");
  logger.fatal("Here is some FATAL");
 }
 // xml로 기록
 public void xmlLog() {
  XMLLayout layout = new XMLLayout();

  //option setting
  layout.setLocationInfo(true);

  FileAppender appender = null;
  try {
   appender = new FileAppender(layout, "Logging4J.xml", false);
  } catch (Exception e) {
  }

  logger.addAppender(appender);
  logger.setLevel((Level) Level.DEBUG);

  logger.debug("Here is some DEBUG");
  logger.info("Here is some INFO");
  logger.warn("Here is some WARN");
  logger.error("Here is some ERROR");
  logger.fatal("Here is some FATAL");
 }

 //--------참고.    http://www.vipan.com/htdocs/log4jhelp.html  --------//
 // for using xml conf

 // properties를 이용하는 것은 다루지 않았다.

 /**
  * conf file
  * <?xml version="1.0" encoding="UTF-8" ?>
  * <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
  * 
  * <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
  * 
  *    <appender name="cad" class="org.apache.log4j.FileAppender">
  *       <param name="File"   value="Logging4J.xml" />
  *       <param name="Append" value="false" />
  *       <layout class="org.apache.log4j.xml.XMLLayout"/>
  *    </appender>
  * 
  *    <root>
  *       <priority value ="debug" />
  *       <appender-ref ref="cad"/>
  *    </root>
  * 
  * </log4j:configuration>
  *
  */
 public void confLog() {
  //dom conf start
  String conf = "log4jconf.xml";
  DOMConfigurator.configure(conf);
  //dom conf end

  logger.debug("Here is some DEBUG");
  logger.info("Here is some INFO");
  logger.warn("Here is some WARN");
  logger.error("Here is some ERROR");
  logger.fatal("Here is some FATAL");
 }
 //LollingFileAppender
 //log4j가 쌓는 로그의 순서
 //최근것은 항상 Logging4J.log 에 쌓이고 이것이 예에서 1000바이트가 차들어가면
 //Logging4J.log를 복사하여
 //Logging4J.log.1을 만들고 Logging4J.log에는 최근게 계속 쌓인다.
 /**
  * <?xml version="1.0" encoding="UTF-8" ?>
  * <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
  * 
  * <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
  * 
  *    <appender name="cad" class="org.apache.log4j.RollingFileAppender">
  *       <!--<param name="File"   value="Logging4J.xml" />-->
  *       <param name="File"   value="Logging4J.log" />
  *       <param name="Append" value="true" />
  *       <param name="MaxFileSize" value="1000"/>
  *       <param name="MaxBackupIndex" value="3"/>
  * 
  * 
  *       <!--<layout class="org.apache.log4j.xml.XMLLayout">
  *       </layout>-->
  * 
  *       <layout class="org.apache.log4j.PatternLayout">
  *          <param name="ConversionPattern"
  *                 value="%d %-5p [%t] %C{2} (%F:%L) - %m%n"/>
  *       </layout>
  * 
  *    </appender>
  * 
  *    <root>
  *       <priority value ="debug" />
  *       <appender-ref ref="cad"/>
  *    </root>
  * 
  * </log4j:configuration>
  */
 public void lollingLog() {
  //dom conf start
  String conf = "log4jconfLolling.xml";
  DOMConfigurator.configure(conf);
  //dom conf end

  logger.debug("Here is some DEBUG");
  logger.info("Here is some INFO");
  logger.warn("Here is some WARN");
  logger.error("Here is some ERROR");
  logger.fatal("Here is some FATAL");
 }

 // pattern layout
 // --참고 -- http://logging.apache.org/log4j/docs/api/org/apache/log4j/PatternLayout.html
 /**
  * 출력패턴
  *
  * * ------+-------------------------------------------------------------------------------
 * %c    | 로그 이벤트의 카테고리명을 출력한다. 
 * ------|-------------------------------------------------------------------------------
 * %C    | 로깅 요구를 실시하는 클래스명을 출력한다. 
 * ------|-------------------------------------------------------------------------------
 * %d    | 로그 이벤트의 일시를 출력한다.
 *       | %d{HH:mm:ss} 나 %d{dd MMM yyyy HH}로서보다 유연하게 일시 정보를 출력할 수가 있다. 
 * ------|-------------------------------------------------------------------------------
 * %F(*) | 로그 요구가 발생한 파일명을 출력한다. 
 * ------|-------------------------------------------------------------------------------
 * %l(*) | 로그가 생성되었을 때에 불려 간 위치(소스명, 행)를 출력한다. 
 * ------|-------------------------------------------------------------------------------
 * %L(*) | 로깅 요구를 행한 행 번호를 출력한다. 
 * ------|-------------------------------------------------------------------------------
 * %m    | 로깅이벤트로 설정된 메세지를 출력한다. 
 * ------|-------------------------------------------------------------------------------
 * %M(*) | 로그 요구가 행해진 메소드명을 출력한다. 
 * ------|-------------------------------------------------------------------------------
 * %n    | 플랫폼 의존의 개행 문자를 출력한다. 
 * ------|-------------------------------------------------------------------------------
 * %p    | 로그의 우선도를 출력합니다. 
 * ------|-------------------------------------------------------------------------------
 * %r    | 어플리케이션이 개시하고 나서, 로그가 출력될 때까지의 시간을 밀리 세컨드 단위로 출력한다. 
 * ------|-------------------------------------------------------------------------------
 * %t    | 로그를 생성한 thread의 이름을 출력한다. 
 * ------|-------------------------------------------------------------------------------
 * %x    | 로그가 생성된 thread의 NDC(네스트화 진단 문맥)를 출력한다. 
 * ------|-------------------------------------------------------------------------------
 * %%    | %를 출력한다. 
 * ------+-------------------------------------------------------------------------------
 *
 *(*)이것들을 출력할 때의 퍼포먼스는 좋지 않기 때문에, 어플리케이션의 실행 속도가 문제가 되지 않는 경우에게만
 *사용하는 것이 추천 되고 있다
 *
 *
 ** 여러가지 출력포멧 : 위의 xml에 아래의 포멧들을 적용해서 이것저것 찍어보도록 하자코드:
 * ----------+-------------------------------------------------------------------------
 * %10m      | 출력 캐릭터 라인이 10 문자 이하의 경우,
 *           | 캐릭터 라인의 좌측으로 공백이 삽입된다. 
 * ----------+-------------------------------------------------------------------------
 * %-10m     | 출력 캐릭터 라인이 10 문자 이하의 경우,
 *           | 캐릭터 라인의 우측으로 공백이 삽입된다. 
 * ----------+-------------------------------------------------------------------------
 * %.10m     | 출력 캐릭터 라인이 10 문자를 넘는 경우,
 *           | 캐릭터 라인의 오른쪽으로부터 세어 11 문자눈 이후의 문자(선두의 문자)가 잘라내진다. 
 * ----------+-------------------------------------------------------------------------
 * %10.20m   | 출력 캐릭터 라인이 10 문자 이하의 경우,
 *           | 캐릭터 라인의 좌측으로 공백이 삽입된다.
 *           | 출력 캐릭터 라인이 20 문자를 넘는 경우,
 *           | 캐릭터 라인의 오른쪽으로부터 세어
 *           | 21 문자 이후의 문자(선두의 문자)가 잘라내진다. 
 * ----------+-------------------------------------------------------------------------
 * %-10.20m  | 출력 캐릭터 라인이 10 문자 이하의 경우,
 *           | 캐릭터 라인의 우측으로 공백이 삽입된다.
 *           | 출력 캐릭터 라인이 20 문자를 넘는 경우,
 *           | 캐릭터 라인의 오른쪽으로부터 세어 21 문자이후의 문자(선두의 문자)가 잘라내진다. 
 * ----------+-------------------------------------------------------------------------
 *
 *
 *  패턴 layout을 쓰는 예제 xml conf 파일코드:
 * <?xml version="1.0" encoding="UTF-8" ?>
 * <!DOCTYPE log4j:configuration SYSTEM "log4j.dtd">
 *
 * <log4j:configuration xmlns:log4j="http://jakarta.apache.org/log4j/">
 *
 *    <appender name="cad" class="org.apache.log4j.RollingFileAppender">
 *       <!--<param name="File"   value="Logging4J.xml" />-->
 *       <param name="File"   value="Logging4J.log" />
 *       <param name="Append" value="true" />       
 *       <param name="MaxFileSize" value="1000"/>
 *       <param name="MaxBackupIndex" value="3"/>
 *      
 *      
 *       <!--<layout class="org.apache.log4j.xml.XMLLayout">
 *       </layout>-->
 *      
 *       <layout class="org.apache.log4j.PatternLayout">
 *          <param name="ConversionPattern"
 *                 value="%d %-5p [%t] %C{2} (%F:%L) - %m%n"/>
 *       </layout>           
 *       
 *    </appender>
 *
 *    <root>
 *       <priority value ="debug" />
 *       <appender-ref ref="cad"/>
 *    </root>
 *
 * </log4j:configuration> 
  */
 
 public static  void main(String[] args) {
   //dom conf start
   String conf = "log4jPattern.xml";
   DOMConfigurator.configure(conf);
   //dom conf end

   logger.debug("Here is some DEBUG");
   logger.info("Here is some INFO");
   logger.warn("Here is some WARN");
   logger.error("Here is some ERROR");
   logger.fatal("Here is some FATAL");
  }
/**
 *  결과
 * 2004-03-04 11:27:57,038 DEBUG [main] Logging4J (Logging4J.java:440) - Here is some DEBUG
 * 2004-03-04 11:27:57,038 INFO  [main] Logging4J (Logging4J.java:441) - Here is some INFO
 * 2004-03-04 11:27:57,038 WARN  [main] Logging4J (Logging4J.java:442) - Here is some WARN
 * 2004-03-04 11:27:57,038 ERROR [main] Logging4J (Logging4J.java:443) - Here is some ERROR
 * 2004-03-04 11:27:57,038 FATAL [main] Logging4J (Logging4J.java:444) - Here is some FATAL
 */
 
}

'JAVA > JSP_Servlet' 카테고리의 다른 글

[펌] Jakarta Digester 학습하기  (0) 2004.04.01
[펌] Torque프로젝트 사용기  (0) 2004.03.31
[펌] JSF 사용하기  (0) 2004.03.29
[펌] Struts, Tiles, JavaServer Faces 통합하기  (0) 2004.03.29
[펌] 리팩토링(Refactoring)  (0) 2004.03.29
Posted by tornado
|

출처 : http://network.hanbitbook.co.kr/view_news.htm?serial=698

 

JSF 사용하기

등록일: 2003년 11월 11일

저자: 안드레이 키오뢰아누(Andrei Cioroianu), 역 이상화

JSF는 자바 개발자에게 웹 기반의 사용자 인터페이스를 작성하기 위한 API와 태그 라이브러리를 제공한다. 아파치 스트럿츠 프레임워크를 만든 저작자이면서 썬의 JSF 프로젝트의 공동작업자로 활동하고 있는 크레이그 맥클라나한(Craig McClanahan)은 이러한 것들이 아파치 프로젝트를 JSF 표준으로 쉽게 갈 수 있게 해준다고 믿고있다. 스트럿츠 프레임워크와 같이 JSF는 자바 빈즈 속성에 연결된 HTML 폼 요소를 생성하는 JSP 태그 집합을 가지고 있다. 개발자의 관점에서는 이 두 프레임워크가 비슷해 보이지만 JSF는 자바 표준이기 때문에 좀더 많은 개발 도구의 지원을 받게 될 것이다. 앞으로는 모든 J2EE 애플리케이션 서버가 JSF를 지원하게 될 것으로 예상된다.

Sun은 최근 JSF 1.0 EA4 버전을 JWSDP 1.2(
Java Web Services Developer Pack 1.2)에 포함하였다. EA4 버전은 액션, 빈즈 관리, 네비게이션 규칙과 같은 새로운 기능을 가지고 있다. 본 기사는 이러한 새로운 모습에 초점을 두고 폼을 작성하거나 사용자 입력을 검증하고 사용자 인터페이스 컴포넌트를 자바 빈즈에 연결하기 위한 JSF의 장점을 보여줄 것이다.

관련기사:

JavaServer Faces 입문 -- JSF는 서버측 프로그래밍의 차세대 주자이다. JSF는 프로그래밍환경을 좀더 편하고 즐겁게 그리고 유지보수가 쉽게 만들 것이다. Budi Kurniawan는 JSF가 유용성과 요구사항을 어떻게 만족 하는지 설명하고 있다.



본 기사는 4개의 메인 컴포넌트로 이루어진 웹 애플리케이션을 포함하고 있다. 자바 빈즈 클래스(PBean.java)는 폰트, 크기, 컬러, 정렬과 같은 속성을 가지고있는 데이터 모델 역할을 한다. JSF 기반의 폼(edit.jsp)은 사용자가 자바 빈즈의 속성값을 변경할 수 있게 해준다. 다른 자바 클래스(PBuilder.java)는 텍스트와 속성들로 구성된 HTML 문단을 생성해낸다. 마지막으로 JSP페이지(view.jsp)는 생성된 문단을 보여준다.


[그림 1] JSF 기반 폼

JSF 폼 만들기

소스 코드

여기에서 소스코드를 다운받을 수 있다.

HTML 폼을 처리하는 작업은 웹 애플리케이션 개발에 필수적인 일상작업 중 하나이다. 좋은 프레임워크의 경우 반복적인 작업들을 자동화 할 수 있거나 XML 파일 내에 관련 설정을 줄일 수 있기 때문에 개발시간을 단축시킬 수 있다. 또한 JSP 태그 라이브러리를 사용하여 웹 개발을 단순화시키는 것도 가능하다. JSF 프레임워크는 HTML 폼을 표현할 수 있는 JSP 태그 라이브러리를 제공하며, 폼의 상태를 유지하고 있거나, 사용자 입력을 검증하고, 자바빈즈 속성과 사용자 인터페이스 컴포넌트를 묶는 작업을 한다. 이런 사항들 외에도 생산성을 높이기 위해 다른 많은 일들을 한다. JSF는 또한 커스텀 사용자 인터페이스, 커스텀 검증 클래스, 서버측 이벤트 리스너를 위한 풍부한 API를 가지고 있다.

JSF는 Core와 HTML Basic의 두 가지 태그 라이브러리를 포함하고 있다. 전자는 UI 컴포넌트에 이벤트 리스너, 검증자를 등록하는 태그와 일반적인 태그들을 제공한다. 후자는 버튼, 텍스트 입력창, 체크박스와 같은 HTML UI 컴포넌트를 표현하는 JSP 태그를 가지고 있다. edit.jsp 페이지는 폼을 만들기 위해 이러한 많은 태그를 이용한다. 두개의 태그 라이브러리는 f와 h라는 표준 접두어를 갖고 edit.jsp 상단에 선언된다.
<%@ taglib prefix="f" uri="http://java.sun.com/jsf/core" %><%@ taglib prefix="h" uri="http://java.sun.com/jsf/html" %>
<f:use_faces> 태그는 페이지에 쓰이는 모든 JSF 태그를 반드시 포함해야 하는 컨테이너 태그이다. HTML 내용을 만들어 내지는 않지만 내부 JSF 메커니즘을 호출한다. <h:form> 태그는 UI 컴포넌트를 포함하고 있는 <form> HTML 요소를 생성한다.
<html><head><title>Edit</title></head><body><f:use_faces>    <h:form formName="pform">    ..........    </h:form></f:use_faces></body></html>
상단의 JSP 코드는 다음과 같은 HTML 코드를 만들어 낸다.
<html><head><title>Edit</title></head><body>    <form method="post" action="/usingjsf/faces/edit.jsp">    ..........    </form></body></html>
다음 부분에서는 애플리케이션의 자바 빈즈 모델을 설명한다.

빈즈 다루기

다른 웹 프레임워크처럼 JSF는 데이터와 애플리케이션 로직을 캡슐화 하는 모델 객체로 부터 UI를 분리시킨다. HTML UI가 JSF 태그로 부터 생성되면 JSF 프레임워크는 자바 빈즈 모델으로 부터 데이터를 얻거나 HTML 폼을 만들어내는 UI 컴포넌트의 상태를 조정한다. 사용자가 폼을 제출하면 JSF는 사용자 입력을 검증한다. 검증 결과가 정상이면 JSF는 사용자 입력을 자바 빈즈에 저장하고 HTTP 요청은 "네비게이션 규칙"에 따라 다른 페이지로 이동하게 된다.

자바 빈즈 패턴을 따르고 java.io.Serializable를 상속받은 PBean 클래스는 속성들(텍스트, 크기, 폰트, 컬러, 정렬, 볼드체/이탤릭체/밑줄 등과 같은 폰트 종류: text, size, font, color, align, bold, italic, underline)의 get , set 메소드를 제공한다. JSF는 각각의 애플리케이션 사용자에 대해 faces-config.xml 파일에 설정된 PBean ID를 가지는 session 범위의 PBean 인스턴스를 만든다. 또한 JSF는 faces-config.xml 파일에 제공되어지는 값들을 가지고 자바 빈즈 인스턴스의 속성값을 초기화 한다. 이 파일은 다음 기사에서 소개할 네비게이션 규칙과 같은 JSF 설정 파라미터를 가지고 있다.

다음 XML 부분은 JSF에 의해 유지되는 자바 빈즈에 관련된 설정을 보여주고 있다.
<?xml version="1.0"?><!DOCTYPE faces-config PUBLIC    "-//Sun Microsystems, Inc.//DTD JavaServer Faces Config 1.0//EN"    "http://java.sun.com/dtd/web-facesconfig_1_0.dtd"><faces-config>    ..........    <managed-bean>       <managed-bean-name>pbean</managed-bean-name>       <managed-bean-class>            com.devsphere.articles.usingjsf.PBean        </managed-bean-class>        <managed-bean-scope>session</managed-bean-scope>        <managed-property>            <property-name>text</property-name>            <null-value/>        </managed-property>        <managed-property>            <property-name>size</property-name>            <value>3</value>        </managed-property>        <managed-property>            <property-name>font</property-name>            <values>                <value>Arial</value>                <value>Courier New</value>            </values>        </managed-property>        <managed-property>            <property-name>color</property-name>            <value>green</value>        </managed-property>        <managed-property>            <property-name>align</property-name>            <value>left</value>        </managed-property>        <managed-property>            <property-name>bold</property-name>            <value>false</value>        </managed-property>        <managed-property>            <property-name>italic</property-name>            <value>true</value>        </managed-property>        <managed-property>            <property-name>underline</property-name>            <value>false</value>        </managed-property>    </managed-bean></faces-config>
JSF에 의해 생성된 빈은 <managed-bean-scope> 요소에 따라 요청, 세션, 애플리케이션 범위 안에 저장될 수 있다. 정해진 범위 안에서 주어진 ID가 객체가 이미 등록되어 있다면 JSF는 자바 빈즈 인스턴스를 생성하지 않는다.

UI 컴포넌트와 검증

edit.jsp의 <h:form> 요소는 다음 부분에서 상세하게 설명할 몇몇 UI 컴포넌트를 포함하고 있다. 각 컴포넌트의 HTML 코드는 <h:input_textarea> 태그나 <f:validate_required>와 같이 사용자 입력의 검증여부를 JSF에게 알리는 역할의 태그에 의해 생성된다. 사용자 입력을 처리하는 컴포넌트들은 valueRef="pbean.property"를 통해 자바 빈즈 속성들과 연결되어 있다. JSF 는 이전에 설명한 빈즈 속성들을 위해 get, set 메소드를 사용한다. JSF 컴포넌트 태그들 중에서 사용자 입력을 처리하지 않는 것도 있다. 예를 들어 <h:output_text> 태그는 텍스트 출력이나 읽기 전용 자바 빈즈 속성값을 위해 사용된다. 각각의 컴포넌트는 id 속성에 의해 정의 되거나 JSF에 의해 자동 설정된 유일한 ID를 가지고 있다. 검증이 필요한 UI 컴포넌트는 에러를 출력하기 위한 <h:output_errors for="id"/> 태그를 사용하기 위해 id 속성이 필요하다.


[그림 2] 검증 에러들

텍스트 영역

JSF 폼의 텍스트 영역 컴포넌트는 PBuilder.java, view.jsp에 포함되어 사용자로 하여금 내용을 입력할 수 있게 한다. edit.jsp 페이지는 <h:output_text> 태그를 사용하여 라벨을 나타내고 30행 3열의 <textarea> HTML 요소를 표현하기 위해 <h:input_textarea> 태그를 사용하였다. <f:validate_required> 태그는 사용자가 텍스트 공간에 어떤 것도 입력하지 않을 때 에러를 보내는 JSF 검증자를 등록한다. 에러가 발생하면 <h:output_errors> 태그에 의해 에러가 출력되고 그렇지 않으면 화면에 아무것도 나타나지 않는다. <h:output_errors> 태그를 위한 속성은 <h:input_textarea> 태그의 id 속성과 같은 값을 갖는다.
<f:use_faces>    <h:form formName="pform">        <p><h:output_text value="Text:"/><br>        <h:input_textarea id="text" valueRef="pbean.text"                rows="3" cols="30">            <f:validate_required/>        </h:input_textarea>        <br><h:output_errors for="text"/>        ..........    </h:form></f:use_faces>
상단의 JSP 코드는 다음의 HTML을 만들어 낸다.
<form method="post" action="/usingjsf/faces/edit.jsp">    <p>Text:<br>    <textarea name="text"        cols="30" rows="3">JavaServer Faces</textarea>    <br>    ..........</form>
<h:input_textarea> 태그의 valueRef="pbean.text" 속성은 JSF에게 자바 빈즈 인스턴스의 ID가 pbean인 것을 찾고 사용자가 입력한 텍스트를 인스턴스의 text 속성에 저장하는 것을 의미한다. HTML 폼이 생성되면 JSF는 <textarea> HTML 요소에 텍스트 속성의 값을 삽입한다. PBean 클래스는 속성의 값을 변경, 검색하기 위해 JSF에 의해 호출된 get, set 메소드를 실행한다.
public class PBean implements java.io.Serializable {    private String text;    public String getText() {        return text;    }    public void setText(String text) {        this.text = text;    }    ..........}
<h:input_textarea> 태그와 더불어 JSF는 텍스트 필드를 생성하는 <input_text>, <h:input_number>, <input_secret> (패스워드용), <input_date>, <input_datetime>, <input_time>과 같은 몇몇 태그를 생성한다. <input_hidden> 태그는 숨겨진 폼 필드를 만든다.

텍스트 필드

edit.jsp의 텍스트 필드는 1부터 7까지의 숫자만을 입력받는다. <h:input_number> 태그에 의해 생성된 HTML 코드는 2개의 검증자를 가지고 있다. <f:validate_required> 태그는 이전에 설명하였다. <f:validate_longrange> 검증자는 사용자 입력이 주어진 범위의 값을 만족하는지 검사한다. 그렇지 않다면 <h:output_errors>를 사용하여 에러를 사용자에게 보여준다.
<f:use_faces>    <h:form formName="pform">        ..........        <p><h:output_text value="Size: [1-7]"/><br>        <h:input_number id="size" valueRef="pbean.size" size="2">            <f:validate_required/>            <f:validate_longrange minimum="1" maximum="7"/>        </h:input_number>        <br><h:output_errors for="size"/>        ..........    </h:form></f:use_faces>
상단의 JSP 코드는 다음의 HTML 코드를 만든다.
<form method="post" action="/usingjsf/faces/edit.jsp">    ..........    <p>Size: [1-7]<br>    <input type="text" name="size" id="size" value="3" size="2">    <br>    ..........</form>
텍스트 필드는 int형의 size 속성과 연결된다. value 속성의 값 3은 HTML 폼이 생성될 때 숫자 입력 값의 기본값을 의미한다. 검증을 통해 에러가 없었다고 가정하면 JSF는 value 속성 값을 포함하는 사용자 입력을 받을 때 자바 빈즈를 갱신 한다. <h:input_number> 태그의 size 속성은 텍스트 필드의 문자열 길이를 정의하고 자바 빈즈 속성에는 영향을 미치지 않는다.
public class PBean implements java.io.Serializable {    ..........    private int size;    public int getSize() {        return size;    }    public void setSize(int size) {        this.size = size;    }    ..........}
<f:validate_required>, <f:validate_longrange> 태그 이외에도 JSF는 <validate_doublerange>, <validate_stringrange>, <validate_length>, <validator> 태그들의 검증 관련 태그들을 제공한다. 마지막 태그는 UI 컴포넌트에 커스텀 검증자를 등록할 수 있게 한다. 또한 자신만의 검증 태그 라이브러리를 만들 수도 있다.

리스트 박스

<h:selectone_listbox> 태그와 <h:selectmany_listbox> 태그는 웹 브라우저에서 리스트 박스를 나타내는 <select> 요소를 생성한다. <h:selectone_listbox> JSF 태그는 사용자에게 단일 선택만 허용하고 <h:selectmany_listbox> 태그는 다중선택을 지원한다. edit.jsp 페이지는 여러 개의 폰트를 포함하고 있는 리스트 박스를 생성하기 위해 <h:selectmany_listbox> 태그를 사용한다. 리스트 아이템을 나타내는 HTML <option> 요소는 <h:selectitem> 태그에 의해 생성된다.
<f:use_faces>    <h:form formName="pform">        ..........        <pgt;<h:output_text value="Font:"/gt;<br>        <h:selectmany_listbox id="font" valueRef="pbean.font">            <h:selectitem itemValue="Arial"                itemLabel="Arial"/>            <h:selectitem itemValue="Courier New"                itemLabel="Courier New"/>            <h:selectitem itemValue="Times New Roman"                itemLabel="Times New Roman"/>        </h:selectmany_listbox>        ..........    </h:form></f:use_faces>
상단의 JSP 코드는 다음의 HTML 코드를 만든다.
<form method="post" action="/usingjsf/faces/edit.jsp">    ..........    <pgt;Font: <br>    <select name="font" multiple size="3">        <option value="Arial" selectedgt;Arial</option>        <option value="Courier New" selected>Courier New</option>        <option value="Times New Roman">Times New Roman</option>    </select>    ..........</form>
리스트 박스는 String[] 속성을 갖는 font 속성에 연결된다. 처음 getFont() 메소드는 외부 접근으로부터 보호 받아야 하는 내부 배열의 복사본을 반환하는 clone() 메소드를 사용한다. setFont() 메소드는 두 번째 setFont() 메소드가 수정할 수 있는 배열의 복사본을 유지하기 위해 clone()를 사용한다.
public class PBean implements java.io.Serializable {    ..........    private String fontArray[];    public String[] getFont() {        return (String[]) fontArray.clone();    }    public void setFont(String fontArray[]) {        this.fontArray = (String[]) fontArray.clone();    }    public String getFont(int index) {        return fontArray[index];    }    public void setFont(int index, String font) {        if (fontArray == null)            fontArray = new String[index+1];        else if (fontArray.length <= index) {            String oldFontArray[] = fontArray;            fontArray = new String[index+1];            for (int i = 0; i < oldFontArray.length; i++)                fontArray[i] = oldFontArray[i];        }        fontArray[index] = font;    }    ..........}
HTML 폼이 생성될 때 JSF는 selected HTML 속성을 자바 빈즈 모델의 폰트 속성 배열 값과 일치하는 리스트 아이템에 적용한다. 검증을 통해 에러가 없었다고 가정하면 JSF는 새로운 폰트를 선택할 때 자바 빈즈를 갱신한다.

드롭-다운 리스트

<h:selectone_menu> 태그는 몇 가지 색깔을 가지고 있는 드롭-다운 리스트를 생성하며, 리스트 박스에서처럼 <h:selectitem> 태그를 가지고 있다.
<f:use_faces>    <h:form formName="pform">        ..........        <p><h:output_text value="Color:"/><br>        <h:selectone_menu id="color" valueRef="pbean.color">            <f:validate_required/>            <h:selectitem itemValue="black" itemLabel="Black"/>            <h:selectitem itemValue="red" itemLabel="Red"/>            <h:selectitem itemValue="blue" itemLabel="Blue"/>            <h:selectitem itemValue="green" itemLabel="Green"/>        </h:selectone_menu>        <br><h:output_errors for="color"/>        ..........    </h:form></f:use_faces>
상단의 JSP 코드는 다음의 HTML 코드를 만든다.
<form method="post" action="/usingjsf/faces/edit.jsp">    ..........    <p>Color:<br>    <select name="color" size="1">        <option value="black">Black</option>        <option value="red">Red</option>        <option value="blue">Blue</option>        <option value="green" selected>Green</option>    </select>    <br>    ..........</form>
드롭-다운 리스트는 String 형을 갖는 color 속성과 연결된다.
public class PBean implements java.io.Serializable {    ..........    private String color;    public String getColor() {        return color;    }    public void setColor(String color) {        this.color = color;    }    ..........}
HTML 폼이 생성될 때 JSF는 selected HTML 속성을 자바 빈즈 모델의 color 속성이 가지고 있는 값과 같은 리스트 아이템에 적용한다. 검증을 통해 에러가 없었다면 JSF는 새로운 색깔을 포함하는 사용자 입력을 받을 때 자바 빈즈를 갱신할 것이다.

라디오 버튼

<h:selectone_radio>, <h:selectitem> 태그는 라디오 버튼 그룹을 생성한다.
<f:use_faces>    <h:form formName="pform">        ..........        <p><h:output_text value="Alignment:"/><br>        <h:selectone_radio id="align" valueRef="pbean.align"                layout="LINE_DIRECTION">            <f:validate_required/>            <h:selectitem itemValue="left" itemLabel="Left"/>            <h:selectitem itemValue="center" itemLabel="Center"/>            <h:selectitem itemValue="right" itemLabel="Right"/>        </h:selectone_radio>        <br><h:output_errors for="align"/>        ..........    </h:form></f:use_faces>
상단의 JSP 코드는 다음과 같은 HTML 코드를 만든다.
<form method="post" action="/usingjsf/faces/edit.jsp">    ..........    <p>Alignment:<br>    <table border="0">        <tr>            <td><input type="radio" checked                name="align" value="left"> Left</td>            <td><input type="radio"                 name="align" value="center"> Center</td>            <td><input type="radio"                name="align" value="right"> Right</td>        </tr>    </table>    <br>    ..........</form>
라디오 버튼은 align 속성에 연결되어 있다.
public class PBean implements java.io.Serializable {    ..........    private String align;    public String getAlign() {        return align;    }    public void setAlign(String align) {        this.align = align;    }    ..........}
HTML 폼이 생성될 때 JSF는 checked HTML 속성을 자바 빈즈 모델의 align 속성이 가지고 있는 값과 같은 라디오 버튼에 적용한다. 검증을 통해 에러가 없었다면 JSF는 새로운 정렬 값을 포함하고 있는 사용자 입력을 받을 때 자바 빈즈를 갱신한다.

체크박스

edit.jsp 페이지는 <h:selectboolean_checkbox> 태그에 의해 생성된 체크박스를 가지고 있다. 이것들은 <h:panel_grid>, <h:panel_group> 태그에 의해 생성된 HTML 테이블 내에 포함된다.
<f:use_faces>    <h:form formName="pform">        ..........        <p><h:output_text value="Style:"/><br>        <h:panel_grid columns="3" border="0" cellspacing="5">            <h:panel_group>                <h:selectboolean_checkbox id="bold"                    valueRef="pbean.bold"/>                <h:output_text value="Bold"/>            </h:panel_group>            <h:panel_group>                <h:selectboolean_checkbox id="italic"                    valueRef="pbean.italic"/>                <h:output_text value="Italic"/>            </h:panel_group>            <h:panel_group>                <h:selectboolean_checkbox id="underline"                    valueRef="pbean.underline"/>                <h:output_text value="Underline"/>            </h:panel_group>        </h:panel_grid>        ..........    </h:form></f:use_faces>
상단의 JSP 코드는 다음의 HTML 코드를 만든다.
<form method="post" action="/usingjsf/faces/edit.jsp">    ..........    <p>Style:<br>    <table border="0" cellspacing="5">        <tr>            <td><input type="checkbox"                 name="bold">Bold</td>            <td><input type="checkbox"                 name="italic" checked>Italic</td>            <td><input type="checkbox"                   name="underline">Underline</td>        </tr>    </table>    ..........</form>
세 개의 체크박스는 bold, italic, underline Boolean 속성들에 연결되어 있다.
public class PBean implements java.io.Serializable {    ..........    private boolean bold;    public boolean isBold() {        return bold;    }    public void setBold(boolean bold) {        this.bold = bold;    }    private boolean italic;    public boolean isItalic() {        return italic;    }    public void setItalic(boolean italic) {        this.italic = italic;    }    private boolean underline;    public boolean isUnderline() {        return underline;    }    public void setUnderline(boolean underline) {        this.underline = underline;    }    ..........}
HTML 폼이 생성될 때 자바 빈즈와 연결된 속성 값이 ture이면(참이면), JSF는 checked HTML 속성을 체크박스에 적용한다. 검증을 통해 에러가 없었다면 JSF는 사용자 입력을 받을 때 자바 빈즈를 갱신 한다.

예제에서 체크박스는 분리되어 생성된다. JSF는 체크박스 그룹을 생성할 수 있는 <h:selectmany_checkboxlist> 태그를 제공한다. 이외에도 JSF는 테이블을 생성할 수 있는 <h:panel_list>, <h:panel_data> 두 개의 태그를 갖고있다.

명령 버튼

faces-config.xml 파일은 네비게이션 규칙을 정의한다. 규칙이란 사용자가 페이지 버튼 중 하나를 클릭했을 때 <from-tree-id> 태그에 설정된 경로를 JSF에게 알려주는 것이다. <navigation-case> 요소 안에는 두 개의 네비게이션 케이스가 있다.
..........<faces-config>    <navigation-rule>        <from-tree-id>/edit.jsp</from-tree-id>        <navigation-case>            <from-outcome>editOutcome</from-outcome>            <to-tree-id>/edit.jsp</to-tree-id>        </navigation-case>        <navigation-case>            <from-outcome>viewOutcome</from-outcome>            <to-tree-id>/view.jsp</to-tree-id>        </navigation-case>    </navigation-rule>    ..........</faces-config>
/edit.jsp 페이지는 <h:command_button> 태그에 의해 생성되는 두 개의 버튼을 가지고 있다. 버튼 각각은 ID, 라벨, JSF에 의해 사용되는 명령이름, action, actionRef 속성을 가지고 있다.
<f:use_faces>    <h:form formName="pform">        ..........        <p>        <h:command_button id="view" label="View"            commandName="viewCmd" action="viewOutcome"/>        <h:command_button id="boldUpperCase"            label="Bold Upper Case / View"            commandName="boldUpperCaseCmd"            actionRef="pbean.boldUpperCaseAction"/>    </h:form></f:use_faces>
상단의 JSP 코드는 다음의 HTML 코드를 만든다.
<form method="post" action="/usingjsf/faces/edit.jsp">    ..........    <p>    <input type="submit" name="view" value="View">    <input type="submit" name="boldUpperCase"         value="Bold Upper Case / View"></form>
JSF는 매번 사용자 입력이 브라우저로부터 제출될 때 폼 데이터를 검증한다. 이상이 없고 데이터 형 변환 에러가 없다면 JSF는 네비게이션 규칙을 고려한다. 첫번째 버튼의 경우 JSF는 두 번째 이동 케이스의 <from-outcome> 요소 값과 같은 action 속성을 얻는다. 그러므로 JSF는 HTTP 요청을 <to-tree-id> 요소 값인 view.jsp 페이지로 포워드 한다.

사용자가 두 번째 버튼을 눌렀을 때 JSF는 PBean 객체의 getBoldUpperCaseAction() 메소드를 호출한다. 이 메소드는 PBean의 내부 클래스 BoldUpperCaseAction 인스턴스를 반환한다. 그러면 JSF는 HTML에 하드 코딩되어있을 때가 아니라 런타임에서 출력을 반환하는 invoke() 메소드를 호출한다.
public class PBean implements java.io.Serializable {    ..........    public BoldUpperCaseAction getBoldUpperCaseAction() {        return new BoldUpperCaseAction();    }    public class BoldUpperCaseAction            extends javax.faces.application.Action {        public String invoke() {            String ucText = getText().toUpperCase();            if (isBold() && getText().equals(ucText))                return "viewOutcome";            else {                setBold(true);                setText(ucText);                return "editOutcome";            }        }    }}
bold 속성이 true(참)이고 text의 값이 같으면 JSF는 두 번째 이동 케이스를 따라 HTTP 요청을 view.jsp 페이지로 포워딩한다. 그렇지 않으면 invoke() 메소드는 bold 속성을 true(참)으로 만들고 모든 텍스트 속성의 문자를 대문자로 변환한 다음 editOutcome을 반환한다. JSF는 현재 페이지 /edit.jsp를 유지하는 첫번째 네비게이션 케이스를 따른다.

폼 데이터 처리

Pbuilder 클래스는 Pbean 인스턴스를 파라미터로 받아 빈즈의 속성을 가지고 HTML 문단을 만들어내는 static 메소드를 포함하고 있다.
package com.devsphere.articles.usingjsf;public class PBuilder {    public static String toHTML(PBean pbean) {        StringBuffer buf = new StringBuffer();        buf.append("<p align=\"");        buf.append(pbean.getAlign());        buf.append("\">");        buf.append("<font size=\"");        buf.append(pbean.getSize());        buf.append("\" color=\"");        buf.append(pbean.getColor());        buf.append("\"");        Object font[] = pbean.getFont();        if (font != null && font.length > 0) {            buf.append(" face=\"");            for (int j = 0; j < font.length; j++) {                if (j > 0)                    buf.append(',');                buf.append(font[j]);            }            buf.append("\"");        }        buf.append(">");        if (pbean.isBold())            buf.append("<b>");        if (pbean.isItalic())            buf.append("<i>");        if (pbean.isUnderline())            buf.append("<u>");        String s = pbean.getText();        int n = s.length();        for (int i = 0; i < n; i++) {            char ch = s.charAt(i);            switch (ch) {                case '<':                    buf.append("<");                    break;                case '>':                    buf.append(">");                    break;                case '&':                    buf.append("&");                    break;                case '"':                    buf.append(""");                    break;                default:                    buf.append(ch);            }        }        if (pbean.isUnderline())            buf.append("</u>");        if (pbean.isItalic())            buf.append("</i>");        if (pbean.isBold())            buf.append("</b>");        buf.append("</font>");        buf.append("</p>");        return buf.toString();    }}
view.jsp 페이지는 JSF에 의해 관리되는 자바빈즈 인스턴스를 얻기위해 <jsp:useBean>를 사용하고 PBuildertoHTML() 메소드를 호출하여, 결과적으로 HTML 문단을 출력한다. JSF의 <h:command_hyperlink> 태그는 edit.jsp를 가리키는 링크를 제공한다.
<%@ taglib prefix="f" uri="http://java.sun.com/jsf/core" %><%@ taglib prefix="h" uri="http://java.sun.com/jsf/html" %><html><head><title>View</title></head><body><jsp:useBean id="pbean" scope="session"    class="com.devsphere.articles.usingjsf.PBean"/><%= com.devsphere.articles.usingjsf.PBuilder.toHTML(pbean) %><f:use_faces>    <h:command_hyperlink label="Back to Editing" href="edit.jsp"/></f:use_faces></body></html>


[그림 3] view.jsp에 의해 나타나는 텍스트

애플리케이션 설정

JSF 프레임워크는 JSP 페이지로 향하는 요청을 가로채는 FacesServlet 제어 서블릿을 가지고있다. 서블릿은 web.xml 파일 안에 설정되고 /faces/* URL 패턴에 매핑된다. 서블릿을 활성화시키기 위해 JSP 페이지는 반드시 "faces"로 시작해야 한다. 애플리케이션의 index.jsp 홈페이지는 브라우저 요청을 faces/edit.jsp 페이지로 보내기 위해 response.sendRedirect()를 사용한다.
<% response.sendRedirect("faces/edit.jsp"); %>
faces-config.xml 파일의 위치는 web.xml의 context 파라미터 값에 의해 결정된다. web.xml 파일은 리스너 등록 설정과 JSF 실행에 관련된 파라미터 값을 가지고 있다. 다음은 web.xml 파일의 내용이다.
<?xml version="1.0"?><!DOCTYPE web-app PUBLIC  "-//Sun Microsystems, Inc.//DTD Web Application 2.3//EN"  "http://java.sun.com/dtd/web-app_2_3.dtd"><web-app>    <context-param>        <param-name>avax.faces.application.CONFIG_FILES</param-name>        <param-value>/WEB-INF/faces-config.xml</param-value>    </context-param>    <context-param>        <param-name>com.sun.faces.saveStateInClient</param-name>        <param-value>false</param-value>    </context-param>    <context-param>        <param-name>com.sun.faces.validateXml</param-name>        <param-value>true</param-value>    </context-param>    <listener>        <listener-class>com.sun.faces.config.ConfigListener</listener-class>    </listener>    <servlet>        <servlet-name>FacesServlet</servlet-name>        <servlet-class>javax.faces.webapp.FacesServlet</servlet-class>        <load-on-startup>1</load-on-startup>    </servlet>    <servlet-mapping>        <servlet-name>FacesServlet</servlet-name>        <url-pattern>/faces/*</url-pattern>    </servlet-mapping></web-app>
처음 폼이 생성될 때 JSF는 컴포넌트 트리, UI 컴포넌트에 관한 정보를 가진 객체, 객체에 등록된 검증자를 만든다. 사용자가 폼 데이터를 제출하면 JSF는 컴포넌트 트리로부터 정보를 얻어 사용자 입력을 검증한다. 기본적으로 JSF는 session 범위에 컴포넌트 트리를 저장한다. 이 작업은 애플리케이션이 끝날 때 한번 일어난다. 개발기간 동안 UI 컴포넌트를 추가하거나 제거하여 JSF 폼을 변경 하면 JSF는 JSP 페이지가 수정될 때 컴포넌트 트리 값을 버리지 않기 때문에 예외를 던진다. web.xml 파일에 선언된 saveStateInClient 플래그가 true(참)으로 바뀌면 JSF는 session 속성으로 저장하지 않고 컴포넌트 트리를 HTML hidden 필드로 직렬화시킨다.

요약

본 기사에서는 JSF 태그로 폼 작성하는 방법을 학습하면서 프레임워크의 기본적인 모습을 살펴보았다. 여타의 EA 패키지처럼 JSF EA4도 아직 배포단계는 아니지만 대부분의 기능을 사용할 수 있을 것이다. 자바 개발자에게 웹 사용자 인터페이스 작성을 위한 표준 태그 라이브러리와 커스텀 웹 컴포넌트 작성을 위한 표준 API가 절실했기 때문에 JCP에서 2년의 시간을 보낸 JSF는 현재 환영 받고 있다.

참고자료

'JAVA > JSP_Servlet' 카테고리의 다른 글

[펌] Torque프로젝트 사용기  (0) 2004.03.31
[펌] Log4J...  (0) 2004.03.30
[펌] Struts, Tiles, JavaServer Faces 통합하기  (0) 2004.03.29
[펌] 리팩토링(Refactoring)  (0) 2004.03.29
[펌] 스트럿츠 이디엄  (0) 2004.03.29
Posted by tornado
|

출처 : http://www-903.ibm.com/developerworks/kr/java/library/j-jmx3.html

JMX Agent를 네트워크 관리 시스템에 연결하기

Level: Intermediate

Sing Li
작가, Wrox Press
2002년 12월 17일

실제 Network Management System (NMS)를 사용하여 JMX로 만들어진 자바 애플리케이션을 모니터링한다. NMS/JMX 통합에 사용된 일반적인 기술은 물론 JMX를 전개할 때 발생하는 일반적인 어려움들을 설명한다.

JMX는 자바 플랫폼의 새로운 표준 확장으로서 현대적 Network Management Systems (NMS)를 통해 장치, 애플리케이션, 서비스들을 관리 및 제어, 감시할 수 있다. JMX가 소프트웨어 애플리케이션/서비스를 어떻게 활용하는지를 확실히 알고 싶다면 실제 오픈 소스 NMS(OpenNMS)를 사용하여 ClickMeter 애플리케이션을 모니터링한다. 광범위한 가용성과 단순한 디자인 때문에 OpenNMS를 선택했다. 여러분들도 다른 NMS 제품에 설명된 통합 기술을 적용할 수 있다. OpenNMS 스팩으로 작업하기 전에 일반적인 디자인 엘리먼트와 대부분의 NMS 제품이 공유하는 작동 모델을 보도록 하자.

네트워크 관리 시스템- 그 근본적인 복잡성
네트워크 관리 사용자 인구가 다양해진 덕택에 지금의 NMS 제품은 가장 광범위하고 복잡한 소프트웨어 시스템이 되었다. 예를 들어 분산 작동 센터를 통해 통신 네트워크 장비를 관리 및 감시하는 다국적 조직은 가상 프라이빗 네트워크(VPN), PC 자산, 데이터 통신 자산, 애플리케이션 서버 관리에 필요한 지역 비지니스의 다양한 요구를 안게 될 것이다.

전형적인 NMS는 수천, 수만, 심지어 수백만 엔드포인트(노드)를 관리해야 한다. NMS의 목표 고객들은 중간 규모 이상의 조직들이다. 이러한 고객들은 다양한 비지니스를 하기 때문에 NMS는 채택성이 높은 설정 메커니즘과 고객의 다양한 요구를 만족시킬 사용자 인터페이스를 갖춰야 한다. 전형적인 NMS 솔루션의 고비용을 정당화 할 수 있어야 한다.

관련 기술자료

Part 1, "Management, JMX 1.1 style" (2002년 9월)

Part 2, "Beans, JMX 1.1 style" (2002년 10월)

OpenNMS: 오픈 소스 NMS
OpenNMS는 오픈 소스 분야에서는 하나의 소프트웨어 시스템이다. 이것은 개발 커뮤니티에 복잡하지만 채택가능한 NMS 솔루션의 실행파일과 소스 코드를 무료로 제공하려는 시도이다.

OpenNMS는 레거시 없이 탄생했기 때문에 보다 새로운 접근방식이 필요했다. OpenNMS는 사용자 중심의 NMS이며 전형적인 네트워크 관리자를 싱글 포인트 포커스로 두어 필요한 기능을 결정하게한다. 몇몇 안되는 구성원으로 이루어진 초기 팀은 네트워크 관리 컨설턴트이며 그들의 지식을 총동원하여 객체, 태스크, 워크플로우 관련 작동 모델을 규명했다. 이것은 상용 NMS 제품에 대비되며 디바이스 중심, 네트워크 중심, 소프트웨어 서비스 중심 성격이된다.

NMS 분석
많은 NMS 제품들은 애플리케이션의 특정 도메인, 코드 유산, 벤더와 상관 없이 유사한 개념적 구성을 갖고있다. 그림 1은 이러한 일반적 구성을 설명하고 있다.

그림 1. NMS의 개념적 구성
Figure 1. Conceptual composition of an NMS

구성은 일반적으로 3-TIER 이다. 프론트(front) 티어는 관리되고 있는 디바이스와 서비스의 네트워크, 시스템 사용자, 외부 시스템과 인터페이싱한다. 미들 (middle) 티어에는 NMS의 기능 세트를 제공하는 많은 로직이 포함되어 있다. 백엔드(back-end) 티어는 데이터를 유지하고 조작한다.

광범위한 정보와 동적이지만 강력한 기반에서 트래킹되어야 할 복잡한 상호관계 때문에 NMS는 상용 관계형 데이터베이스 관리 시스템(RDBMS)을 백엔드 티어에 전개했다.

첫 번째 티어에서 모니터링, 관리, 제어 컴포넌트는 NMS 작동을 실행해야 하는 많은 동시 태스크들로 구성되어 있다. 액티브 네트워크 발견, 디바이스 정지 및 서비스 등록하기, 디바이스, 서비스, 고급 분산 에이전트에서 비동기식 이벤트 개입 및 프로세싱을 맡고 있다. 이 컴포넌트는 설정 로직, 제공 로직, 워크플로우 실행자, 미들 티어의 커스텀 로직에서 온 작동들 역시 수행한다.

OpenNMS의 역사
2002년 봄, OpenNMS 1.0이 탄생했다. 오픈 소스로 사용할 수 있는 최초의 엔터프리이즈급 네트워크 관리 플랫폼이 탄생한 것이다. 2002년 5월, 프로젝트의 담당자들은 OpenNMS에 지원과 컨설팅 서비스를 제공하는 Sortova Consulting Group으로 옮겼지만 개발 진화는 계속되고 있다.

1999년, 시스템 프로그래머들과 네트워크 관리 컨설턴트들은 관리 제품의 현황에 실망하고 해결책을 모색할 것을 결정했다. 상용 NMS 제품의 천문학적 비용을 감지한 팀은 오픈 소스 개발 모델을 사용하여 보다 나은 것을 개발하기로 결정했다.

또한, 프론트 티어는 사용자 인터페이스 컴포넌트이다. NMS는 "fat client GUI" 인터페이스는 물론 쉽게 커스터마이징 할 수 있는 웹 기반 사용자 인터페이스도 가지고 있다. 리포팅 장치가 이 레이어에 있으며 UI와 인터랙팅하여 실시간 소프트 카피 리포트는 물론 배치 하드 카피 리포트도 제공한다.

첫 번째 티어의 외부 인터페이싱 컴포넌트는 사용자와 외부 시스템에 공지 프로세스를 할 수 있다. 페이저호출, 이메일, 전호 호출, 특정 이벤트를 외부 관리 시스템에 전한다. API와 확장 플러그인 기반구조로 인해 NMS는 커스터마이징 될 수 있고 다른 시스템과 인터페이싱 및 통합될 수 있다.

미들 티어는 NMS의 로직이 존재하고 있는 곳이다. NMS에 구별된 기능과 특성을 부여한다. 재고 또는 자산 관리, 데이터 수집과 분석, 사용자 또는 역할 관리, 워크 플로우 관리/실행 등이 알려진 기능들이다.

재고(또는 자산) 관리는 대부분의 NMS 제품의 주요 기능이다. 트래킹되는 아이템들은 NMS에서 장비, 서킷, 서버, 설치된 소프트웨어 서비스와 애플리케이션에 이르기까지 다양하다. 이 컴포넌트의 사용자 인터페이스와 애플리케이션 로직은 관리되는 다른 아이템들에게도 유연하게 적용될 수 있다.

데이터 수집과 분석 기능은 관리되고 있는 디바이스와 서비스에 대한 현재와 기존의 통계를 제공한다. 네트워크와 서브 네트워크, 디바이스/서비스 가용성을 드러내는 통계적 분석을 제공한다. 이 컴포넌트는 커스텀 워크플로우를 핸들하기 위해 워크플로우 실행 로직과 인터랙팅 할 수 있다.

사용자와 역할 관리 컴포넌트는 NMS에 다양한 레벨의 접근을 제공한다. 가장 넓은 네트워크는 하나 이상의 팀이 관리한다. 사용자들에게 할당된 역할은 디자인과 커스텀 보안 정책의 구현 뿐만 아니라 커스텀 워크플로우와 이벤트 증대 플로우에 사용될 수 있다.

워크플로우 관리 및 실행은 장단기 관리 태스크의 관리 및 자동 실행을 제공한다. 자동화된 커스텀 워크플로우는 다양한 조직의 다양한 비지니스 요구사항에 NMS가 채택되는데 필수적인 요소이다.

NMS 모든 NMS가 이 모든 컴포넌트들을 갖고 있는 것은 아니고 어떤 것은 보다 구체화된 변이를 갖고 있을 수도 있다. 이 글에서 검토되는 것들은 NMS를 구성하고 있는 것들에 대한 일반적 개요라 할 수 있다.

OpenNMS 아키텍쳐
그림 1을 레퍼런스로 사용하여 레퍼런스 구성과 OpenNMS의 아키텍쳐 사이의 구체적 구성도를 그릴 수 있다. 그림 2는 OpenNMS를 구성하고 있는 컴포넌트이다.

그림 2. OpenNMS의 컴포넌트
Figure 2. Components of OpenNMS

백엔드 티어에서 OpenNMS는 RDBMS로서 PostgreSQL (참고자료)을 사용한다. 프론트 티어에서 Apache Jakarta Tomcat (참고자료)의 JSP와 서블릿 기술을 사용하여 유연하게 커스터마이징 할 수 있는 사용자 인터페이스를 제공한다. 기존 Perl 기반의 사용자 인터페이스 역시 사용할 수 있다. 여러 관리 유틸리티들은 UNIX 쉘 스크립트와 Perl로 작성된다. OpenNMS는 그림 1의 모니터링, 관리, 제어 컴포넌트에 상응하는 컨커런트 자바 태스크를 사용하여 이 기능을 제공한다. 이러한 동시 태스크들은 그림 2에 원으로 표현되었다.

OpenNMS 데몬: 컨커런트(concurrent) 관리 태스크
OpenNMS의 모니터링, 제어, 데이터 수집 기능들은 데몬(daemon) 이라고 하는 컨커런트 태스크에 의해 핸들된다. (BSD UNIX 규약). 표 1은 컨커런트 관리 태스크들 이다.

표 1. OpenNMS의 컨커런트 관리 태스크

Concurrent Taskdaemon 이름 Description
Action daemonactiond자동 액션 실행 장치. 인커밍 이벤트에 근거한 자동화된 액션.
Collection daemoncollectd관리되는 노드에서 데이터 수집.
Capability daemoncapsd발견된 노드 상에서 기능 검사 수행. 알려진 서비스 프로토콜 지원을 위한 인터페이스 포트 점검.
DHCP daemondhcpdOpenNMS에 DHCP 클라이언트 기능 제공.
Discovery daemondiscovery관리되는 네트워크 노드의 초기 및 지속적인 발견.
Events manager daemoneventd다른 컨커런트 태스크에서 나온 이벤트의 관리 및 (RDBMS에) 저장
Notification daemonnotifd외부 공지 수행.
Outage manager daemonoutaged이벤트를 결합하여 각 관리되는 노드/서비스에 outage 뷰 제공.
Poller daemonpollerd관리되는 노드/서비스를 정기적으로 등록하여 작동 상태 결정.
RTC manager daemonrtcd실시간으로 데이터를 수집하여 관리되는 노드/서비스의 사용자 정의 카테코리에 가용성 정보 제공.
SNMP trap daemontrapdSNMP 트랩(이벤트) 핸들.
Threshold service daemonthreshd특정 임계값에 도달한 애트리뷰트 값에 근거하여 노드/서비스 감시.

OpenNMS 확장하기
OpenNMS의 프론트 티어에서 특정 수직 애플리케이션 도메인에 시스템을 적용하는 것은 매우 단순하다. JSP 기술의 유연성 덕택에 OpenNMS의 사용자 인터페이스는 자바 개발자들이 쉽게 커스터마이징할 수 있다. 새로운 NMS 애플리케이션 로직은 JSP 파일과 서블릿 코딩의 결합을 통해 빠르게 추가될 수 있다.

OpenNMS는 관리되는 디바이스/서비스용 SNMP 지원이 됨으로서 표준이 되었다. SNMP는 관리가 편리한 디바이스/서비스용 산업 표준이다. 이 표준은 OpenNMS가 TCP/IP 네트워크 상에서 다양하고 광범위한 기존 디바이스를 관리 할 수 있도록 한다.

SNMP 밖에서, OpenNMS는 대중적인 소프트웨어 서비스(FTP, 파일 서버, 데이터베이스 서버)를 탐지하고 관리할 수 있다. 서비스 스팩의 플러그인을 제공하고 이를 수행하는지를 감시한다. 새로운 플러그인과 모니터를 만듦으로서 OpenNMS는 새로운 디바이스 또는 서비스를 탐지하고 감시할 정도로 확장될 수 있다.

커스텀 기능의 플러그인 만들기
OpenNMS의 디스커버리 데몬(discovery)은 관리되는 네트워크의 초기 및 지속적인 발견을 맡고있다. 일단 discovery가 노드를 발견하면 기능 데몬에게 노드가 지원하는 서비스가 무엇인지를 요청한다. 기능 데몬은 특정 프로토콜 플러그인을 통해 반복하여 서비스가 지원하는 것을 검사한다. 커스텀 프로토콜 플러그인을 작성하는 것은 단순하다. 단계는 다음과 같다:

  1. org.opennms.netmgt.capsd.Plugin 인터페이스를 구현하는 클래스를 만든다. 특히 이 인터페이스는 플러그인이 구현해야 하는 세 개의 메소드를 갖고있다. (표 2):

    표 2. org.opennms.netmgt.capsd.Plugin 인터페이스의 메소드

    메소드 이름과 사인Description
    String getProtocolName();이 플러그인의 프로토콜 이름을 리턴한다.
    Boolean isProtocolSupported(java.nnet.InetAddress address);
    boolean isProtocolSupported(java.nnet.InetAddress address, java.util.Map properties);
    필요할 경우 인커밍 매개변수용 프로퍼티 맵을 사용하여 이 프로토콜이 구체적인 InetAddress의 지원을 받는지를 결정한다.



  2. /etc/capsd-configuration.xml 파일에 프로토콜-플러그인 정의를 만든다. 이것은 capsd가 시작하는 동안 사용할 프로토콜 플러그인을 결정하기 위해 읽게된다.

커스텀 폴러(poller) 모니터 만들기
OpenNMS가 관리 노드상의 서비스를 결정한 후에 폴러 데몬(pollerd)을 사용하여 관리되는 디바이스/서비스를 정기적으로 등록한다. 그들이 작동하고 있다는 것을 확인하기 위해서이다. pollerd는 지정된 폴러 모니터 "플러그인"을 통해 반복하면서 개별 서비스 폴들을 수행한다:

  1. org.opennms.netmgt.poller.ServiceMonitor 인터페이스를 구현하는 클래스를 만든다. 특히 이 인터페이스는 모니터가 구현해야 하는 다섯 개의 메소드를 갖고있다. (표 3):

    표 3. org.opennms.netmgt.poller.ServiceMonitor 인터페이스의 메소드

    메소드 이름과 사인Description
    void initialize(java.util.Map parameters);
    void initialize(org.opennms.netmgt.poller.NetworkInterface iface);
    첫 번째 형식은 시작하는 동안 호출되어 플러그인이 초기화 중에 실행할 수 없도록 할 기회를 제공한다. 두 번째 형식은 새로운 노드가 발견될 때마다 서비스를 지원하기 위해, 그리고 첫 번째 poll()이 호출되기 전에 호출된다.
    void release();
    void release(java.net.NetworkInterface iface);
    첫 번째 형식은 OpenNMS 정지 시 호출된다. 두 번째 형식은 서비스를 지원하기 위해 발견된 노드가 더이상의 폴링 스케쥴에서 제거될 때 마다 호출된다.
    int poll(java.net.NetworkInterface iface, org.opennms.netmgt.utils.EventProxy eproxy, java.util.Map parameters);감시되고 있는 서비스를 여전히 사용할 수 있는지를 결정할 때 사용되는 메소드이다. 서비스가 실행 중일 경우에는 ServiceMonitor. SERVICE_AVAILABLE을그렇지 않으면 ServiceMonitor. SERVICE_UNAVAILABLE을 리턴해야 한다.

  2. /etc/poller-configuration.xml 파일에 서비스 정의와 모니터 정의를 만든다.

OpenNMS 다운로드 및 설치
리눅스 배포판과 OpenNMS의 시스템 요구사항을 숙지해야 한다. 자세한 설치 및 업데이트 절차는 이 글에서 다루지는 않는다. 참고자료 섹션에서 OpenNMS의 최신 버전 다운로드 정보를 검색할 수 있다.

OpenNMS의 최신 안정 버전은 1.0.2이다. 이 글의 모든 코드는 이 안정 버전으로 테스트를 한 것이다. OpenNMS1.0.2는 세 가지 리눅스 스팩(Red Hat Linux 7 & 8, Mandrake 8)에서 테스트되었다. 복잡한 시스템으로 초기 실험 시 이 세 배포판 중 하나를 사용하기 바란다.

OpenNMS를 사용하여 JMX 가능한 ClickMeter 감시하기
capsd 플러그인과 pollerd 모니터를 만듦으로서 OpenNMS와 JMX 구동의 ClickMeter를 통합할 수 있다는 것을 알고 있다. 하지만 한가지 질문이 남아있다. OpenNMS는 JMX를 지원하지 않는가?

심지어 유명한 상용 NMS 오퍼링에서도 같은 경향을 엿볼 수 있다. JMX가 기본적으로 지원되지 않는다. JMX 구동의 디바이스와 서비스는 진행중에 있기 때문이기도하고 SNMP 호환이 제 3의 디바이스와 서비스의 관리에 제공되었기 때문이기도 하다.

JMX의 통합 과정이 NMS 벤더들 사이에서 비교적 느리게 진행되는 중요한 이유가 있다. JMX 사용 레벨과 에이전트 레벨이 1.1 스팩에는 적절히 정의되어있더라도 네트워크를 통해 JMX 에이전트에 액세스 하는 방법은 아직까지는 모호하다.

JMX Remoting 딜레마
JMX 에이전트가 네트워크 관리 애플리케이션이나 분산 서비스에 의해 네트워크를 통해 액세스되는 방법은 JMX Remoting이라고 알려져있고 이것이 JSR 160의 핵심 사안이다.(참고자료).

JSR 160은 JMX 에이전트의 발견이 네트워크를 통해서나 에이전트의 기능에 원격으로 액세스하는 구체적인 방법을 통해 어떻게 수행되는지를 정한다. 여기에는 사용될 프로토콜 어댑터 관련 스팩들이 포함될 것이다. JMX Remoting 1.2 스팩이 마감될 때까지 OpenNMS와 ClickMeter MBean을 관리하는 JMX 에이전트 사이의 통신 대안을 찾아야 한다.

JMX remoting 솔루션
물론, JMX 에이전트의 주요 목적은 이것이 관리하는 JMX MBeans로의 원격 액세스를 제공하는 것이다. 에이전트들은 커넥터와 프로토콜 어댑터를 사용하여 NMS 애플리케이션 또는 분산 서비스들과 통신한다.

ClickMeter의 경우 JMX 레퍼런스 구현의 간단한 에이전트를 사용했다. 여기에는 HTTP 프로토콜 어댑터가 포함되어 있다. 이 간단한 에이전트는 HTTP 프로토콜 어댑터를 통해 모든 MBeans의 애트리뷰트와 작동을 노출한다.

HTTP 어댑터를 사용하여 작업을 최소화하기
OpenNMS와 ClickMeter MBean 사이의 통신을 위해 JMX 레퍼런스 구현에서 간단한 에이전트와 HTTP 프로토콜을 쉽게 사용할 수 있다. 그림 3은 통신 방식을 제안한 것이다. 작동중에 OpenNMS의 capsd는 탐지된 노드를 검사하여 "CLICKMETER" 서비스가 지원되는지의 여부를 본다. pollerd는 탐지된 서비스를 정기적으로 등록하여 이들이 작동중임을 확인한다. 이러한 탐침들을 수용하기 위해서 incPanelValue 호출 작동을 사용할 수 있다. 탐침 메커니즘으로서 incPanelValue 작동을 사용하는 것의 이점은 OpenNMS의 탐지 및 등록 액션을 시각적으로 볼 수 있다는 것이다. ClickMeter의 값은 탐침 때마다 증가한다.

그림 3. HTTP 어댑터로 ClickMeter에 액세스하기
Figure 3. Accessing ClickMeter with the HTTP adapter

이제 남은 일은 ClickMeter MBean 에서 incPanelValue 작동에 액세스 하기 위해 HTTP 프로토콜 어댑터를 사용하는 방법을 결정하는 일이다. 브라우저를 사용하여 다음 URL에서 JMX 에이전트에 액세스할 수 있다:

http://<host of agent>:8082/

물론 이를 수행하기 전에 에이전트가 실행되는지 확인해야 한다. 그런 다음 버튼을 클릭하여 노출된 incPanelValue 작동을 실행한다. (그림 4).

그림 4. 탐침 URL 결정하기
Figure 4. Determining the probe URL

그림 4 에서, 이 작동에 액세스하기 위해 사용된 URL은 다음과 같다:

http://<host of agent>:8082/InvokeAction//MBean:name=ClickMeter/action=incPanelValue?action=incPanelValue

ClickMeter 애플리케이션이 올바르게 기능을 발휘하고 있다면 결과페이지에는 incPanelValue Successful 이라는 문장이 뜬다. 이 정보를 사용하여 OpenNMS에서 원격으로 MBean에 액세스하겠다.

OpenNMS 통합
이제 새로운 "CLICKMETER" 서비스를 OpenNMS에 통합할 준비가 되었다. 이를 위해 capability 데몬(capsd)용 플러그인과 poller 데몬(pollerd)용 모니터를 만들것이다.

OpenNMS 디스커버리용 ClickMeterPlugin
capability 데몬 플러그인 코드는 org.opennms.netmgt.capsd.ClickMeterPlugin 클래스에 있다. 이 플러그인은 org.opennms.netmgt.capsd.Plugin 인터페이스를 구현해야 한다는 것을 알고있다. Listing 1은 AbstractPlugin 의 클래스이다. OpenNMS가 제공되는 클래스는 두 개의 매개변수 추출 메소드(getKeyedInteger() & getKeyedString())를 제공한다.

Listing 1. ClickMeterPlugin 관리 클래스

public final class ClickMeterPlugin extends AbstractPlugin {    private static final String   PROTOCOL_NAME = "CLICKMETER";    private static final int  DEFAULT_PORT  = 8082;    private final static int  DEFAULT_RETRY = 0;    private final static int  DEFAULT_TIMEOUT = 5000; // in milliseconds

getProtocolName() 메소드는 Listing 2의 코드로 간단히 구현된다:

Listing 2. getProtocolName() 메소드

public String getProtocolName() {     return PROTOCOL_NAME;     }

Listing 3은 isProtocolSupported() 메소드가 구현되는 방법이다:

Listing 3. isProtocolSupported() 메소드

public boolean isProtocolSupported(InetAddress address) {  return isClickMeter(address, DEFAULT_PORT, DEFAULT_RETRY, DEFAULT_TIMEOUT);  }

보다 긴 버전의 isProtocolSupported() 메소드에서는, 원격 JMX 에이전트에 액세스하는 URL을 만드는데 사용되는 인커밍 매개변수를 처리한다. 일단, IP와 포트가 매개변수에서 얻어지면, isProtocolSupported()isClickMeter() 메소드를 호출하여 ClickMeter가 이 노드에서 활성인지를 결정한다. Listing 4는 isClickMeter()에 대한 코드이다. URL용으로 정의된 두 개의 상수가 ClickMeter의 incPanelValue 작동을 위한 HTTP 프로토콜 어댑터 액세스 URL과 상응한다는 것에 주목하라.

Listing 4. isClickMeter() 메소드

private final static String CLICK_METER_BEAN_LOCATOR_URL = "/InvokeAction//MBean:name=ClickMeter/action=incPanelValue?   action=incPanelValue";      private final static String CLICK_METER_ID = "incPanelValue Successful";      private boolean isClickMeter(InetAddress host, int port,         int retries, int timeout) {          Category log = ThreadCategory.getInstance(getClass());          boolean foundClickMeter = false;          for (int attempts=0; attempts <= retries && !foundClickMeter;             attempts++)          {                                    URL jmxLink = null;                  InputStream iStream = null;               try               {                      String hostIP = host.getHostAddress();                      jmxLink = new URL("http", hostIP, port,                         CLICK_METER_BEAN_LOCATOR_URL);                      if (scanURL(jmxLink, CLICK_METER_ID, log)) {                            foundClickMeter = true;                            break;   // get out of the for loop                           }                     }          catch(IOException e) {                    log.info("ClickMeterPlugin: Error communicating                        with host " + host.getHostAddress(), e);                    foundClickMeter = false;              }          }          return foundClickMeter;  }

scanURL() 메소드(Listing 5)는 인자로서 URL과 스트링을 취하는 헬퍼 메소드이다. 이것은 URL에 액세스하고 결과 페이지에서 지정된 스트링을 찾는다. 스트링이 발견되면 true를 리턴한다. 그렇지 않을 경우 false를 리턴한다. 이 경우, ClickMeter의 incPanelValue 작동용 URL에 액세스한 후에 incPanelValue Successful 스트링을 찾고있다.

Listing 5. scanURL() 메소드

private boolean scanURL(URL inURL, String toSearch, Category log)    {                  InputStream iStream = null;           try {                                            iStream = inURL.openStream();                     BufferedReader urlReader =                        new BufferedReader(new InputStreamReader(iStream));                          String curLine = urlReader.readLine();                        do  {                                if (curLine.indexOf(toSearch) != -1) {                                       return true;                                      }                           curLine = urlReader.readLine();                         } while (curLine != null);                        }                 catch  (IOException e) {                              e.fillInStackTrace();                      log.debug("ClickMeterMonitor.poll:                         Error reading from URL:"                         + inURL, e);                      }               finally  {                   try  {                       if(iStream != null)                             iStream.close();                    }                    catch(IOException e) {                        e.fillInStackTrace();                        log.debug("ClickMeterMonitor.poll:                           Error closing stream to URL.", e);                    }               }      return false;   }}

플러그인 통합에 필요한 두 번째 단계는 <OpenNMS installation directory>/etc/capsd-configuration.xml 파일을 편집하고 프로토콜-플러그인 정의를 추가하는 것이다.(Listing 6):

Listing 6. 프로토콜-플러그인 정의

  <protocol-plugin     protocol="CLICKMETER"      class-name="org.opennms.netmgt.capsd.ClickMeterPlugin"     scan="on" >    <property key="port" value="8082"/>    <property key="timeout" value="2000"/>    <property key="retry" value="1"/>  </protocol-plugin>

OpenNMS가 관리되는 노드를 검사해야할 IP 범위를 제한하기 위해 discovery-configuration.xml 파일을 편집하고 싶을 것이다. Listing 7에서는 두 개의 IP로 제한하여 ClickMeter 애플리케이션이 빨리 위치할 수 있도록 한다:

Listing 7. 디스커버리 범위 제한

  <include-range retries="2" timeout="3000">    <begin>192.168.23.75</begin>    <end>192.168.23.76</end>  </include-range>

OpenNMS용 ClickMeterMonitor
다음에는, poller 데몬(pollerd) 모니터 플러그인, org.opennms.netmgt.poller.ClickMeterMonitor 클래스를 만든다: 이 클래스는 org.opennms.netmgt.poller.ServiceMonitor 인터페이스를 구현해야 한다. OpenNMS는 이 인터페이스의 모든 메소드용 디폴트 구현을 갖고있는 기본 클래스(org.opennms.netmgt.poller.IPv4Monitor)를 제공한다. 특별한 초기화나 릴리스가 필요없기 때문에 poll() 메소드를 오버라이딩 하면된다.(Listing 8):

Listing 8. poll() 메소드

public int poll(NetworkInterface iface, Map parameters) {       if(iface.getType() != NetworkInterface.TYPE_IPV4)              throw new NetworkInterfaceNotSupportedException("Unsupported                     interface type, only TYPE_IPV4 currently supported");    Category log = ThreadCategory.getInstance(getClass());    int retry   =       getKeyedInteger(parameters, "retry", DEFAULT_RETRY);    int port    =       getKeyedInteger(parameters, "port", DEFAULT_PORT);    int timeout =       getKeyedInteger(parameters, "timeout", DEFAULT_TIMEOUT);    InetAddress ipv4Addr = (InetAddress)iface.getAddress();            String hostIP = ipv4Addr.getHostAddress();    if (log.isDebugEnabled())               log.debug("ClickMeterMonitor.poll: Polling interface: "                      + hostIP + " timeout: " + timeout + " retry: " + retry);        int serviceStatus = ServiceMonitor.SERVICE_UNAVAILABLE;    for (int attempts=0; attempts <= retry && serviceStatus !=               ServiceMonitor.SERVICE_AVAILABLE; attempts++) {                  URL jmxLink = null;                  InputStream iStream = null;        try {                          jmxLink = new URL("http", hostIP, port,                                     CLICK_METER_BEAN_LOCATOR_URL);                        if (scanURL(jmxLink, CLICK_METER_ID, log)) {                            serviceStatus = ServiceMonitor.SERVICE_AVAILABLE;                            break;                             }                     }          catch(IOException e) {        e.fillInStackTrace();        log.debug("ClickMeterMonitor.poll:                IOException while polling address: "                + ipv4Addr, e);      }    }  // of for       return serviceStatus;  }

scanURL() 헬퍼 메소드(Listing 8)를 사용하여 ClickMeter MBean에 액세스한다는 것에 주목하라.

<OpenNMS installation directory>/etc/poller-configuration.xml 파일에서, 서비스 정의 엔트리를 추가해야 한다. Listing 9에서 폴링 간격을 10,000 밀리초로 정했다:

Listing 9. poller 데몬 서비스 정의

  <service name="CLICKMETER" interval="10000"     user-defined="false" status="on">      <parameter key="timeout" value="3000"/>      <parameter key="port" value="8082"/>  </service>

같은 poller-configuration.xml 파일에 모니터 정의를 삽입해야 한다. (Listing 10):

Listing 10. poller 데몬 모니터 정의

<monitor service="CLICKMETER"    class-name="org.opennms.netmgt.poller.ClickMeterMonitor" />

ClickMeter 서비스로 OpenNMS 테스트하기
소스에서 두 개의 플러그인을 구현하려면 compile.bat 파일을 사용한다. 몇몇 OpenNMS 라이브러리 파일을 <article code distribution>/lib 디렉토리에 복사해야 할 것이다. compile.bat 파일은 dwClickMeterJMX.jar를 만든다. 이 결과 JAR 파일을 <OpenNMS installation directory>/lib 디렉토리에 놓으면 OpenNMS는 시작할 때 자동적으로 새로운 플러그인을 로딩하게 된다.

반드시 OpenNMS의 웹 기반 GUI에 액세스하여 시스템을 시작하라. 일반적인 URL은 다음과 같다:

http://<host of OpenNMS>:8080/opennms/

프롬프트되면 userid에는 admin을 패스워드에는 admin을 사용한다. (그림 5):

그림 5. OpenNMS 시작 스크린
Figure 5. OpenNMS startup screen

OpenNMS가 시작하는 동안 두번이나 빠르게 증가하는 ClickMeter 카운트를 보게된다. ClickMeter는 pollerd가 작동할 때 보통 10초 간격으로 증가한다.

일단 capsd가 ClickMeter 애플리케이션을 탐지하면 이를 관리 서비스로서 RDBMS에 추가 할 것이다. 제품의 경우 OpenNMS의 자산 관리 기능들은 이 서비스를 보다 자세히 범주화 해 놓을 수 있다. 우리 실험에서는 OpenNMS GUI를 사용하여 ClickMeter 서비스를 연구하고 이에 개입된 자세한 이벤트를 볼 수 있다.(그림 6):

그림 6. ClickMeter 서비스를 관리하는 OpenNMS
Figure 6. OpenNMS managing ClickMeter service

참고자료

'JAVA > JSE' 카테고리의 다른 글

[펌] JAVA application 성능향상..  (0) 2004.10.07
[펌 http://raytrust.pe.kr/] signed applet  (0) 2004.09.06
[펌] javadoc 유틸리티  (0) 2004.09.01
[nio] 기초 정리 잘된곳  (0) 2004.08.09
[펌] 나만의 자바 컴포넌트를 만들자  (0) 2004.04.01
Posted by tornado
|

출처 : http://www-903.ibm.com/developerworks/kr/java/library/j-integrate.html

Struts, Tiles, JavaServer Faces 통합의 힘

Level: Advanced

Srikanth Shenoy, J2EE Consultant, Objectseek Inc.
Nithin Mallya , J2EE Consultant, Objectseek Inc.

2003년 9월 23일

JavaServer Faces (JSF)의 프론트-엔드 성능, Tiles의 콘텐트 포맷 능력, Struts 콘트롤러의 유연성이 J2EE 웹 애플리케이션에 들어있다면? 엔터프라이즈 자바 전문가 Srikanth Shenoy와 Nithin Mallya는 이 세 개의 특징을 통합하는 방법을 보여준다.

Struts, Tiles, JavaServer Faces (JSF)를 함께 사용함으로서 개발자들은 관리 및 재활용이 쉬운 강력하고 표현성이 뛰어난 웹 애플리케이션을 만들 수 있다.

Struts 프레임웍은 꽤 오랫동안 존재했고 사실상 J2EE 웹 애플리케이션을 개발할 때 의존하게 되는 표준이다. Tiles 프레임웍은 Struts 이후에 등장했고 컴포넌트 부분들을 사용하여 프리젠테이션 페이지를 어셈블링하는 기능을 선보임으로서 위상을 다졌다. 웹 애플리케이션 프레임웍의 신생아인 JSF는 인풋의 타당성검사와 유저 이벤트의 핸들링을 위한 메커니즘을 제공한다. 무엇보다 중요한것은 이것이 프로토콜에서 독립적인 방식으로 사용자 인터페이스 컴포넌트들을 렌더링한다는 점이다. ("The major players")

Struts와 JSF의 몇몇 기능들은 겹치기는 하지만 다른 방식으로 보완이 된다. 이 세 기술의 결합은 웹 애플리케이션을 개발하고 프리젠테이션을 구성하고 커스텀 유저 인터페이스 컴포넌트를 프로토콜과 독립적으로 렌더링하는 효율적인 방법을 제공할 수 있다.

이 글의 샘플 코드를 실행하려면 Struts 1.1, Tiles, JavaServer Faces Reference Implementation (JSF-RI) Early Access Release 4.0, Struts-Faces 0.4가 필요하다.

JSF
JSF 애플리케이션은 JSF 프레임웍을 사용하는 일반적인 J2EE 웹 애플리케이션이다. 이 프레임웍은 풍부한 GUI 컴포넌트 모델을 제공한다. 어떤 특정 기술이 놀라워도 룩앤필은 더욱 발전할 필요가 있다는 말을 들어본적이 있을 것이다. HTML 컴포넌트로 평이한 바닐라 페이지가 있던 시절은 지나갔고 뛰어난 GUI 룩앤필을 구현할 수 있다. 트리 컴포넌트, 메뉴 컴포넌트, 그래프는 기존에 존재했던 UI 컴포넌트이고 이것은 JSF가 당연히 제공해야 하는 것이다. JSF는 이에 더하여 쉽게 API를 사용할 수 있는 방법을 제공하여 커스텀 컴포넌트의 생성을 돕는다.

Note: 위에 언급한 UI 컴포넌트는 Sun에서 제공한 샘플의 일부이다.

Model-View-Controller (MVC) 패턴을 사용하는 전통적인 웹 애플리케이션에서, GUI 컴포넌트들은 프리젠테이션과 비지니스 로직을 핸들하는 커스텀 태그에 의해 표현된다. 이는 클라이언트 장치까지 코딩해야하는 문제를 낳았다.

JSF는 구조적으로 프리젠테이션 로직과 UI 컴포넌트의 비지니스 로직이 분리되어있다. JSP 페이지에서 JSF 태그를 사용하여 렌더러(RENDERER)와 UI 컴포넌트를 결합할 수 있다. 하나의 UI 컴포넌트는 다른 렌더러를 사용하여 다양한 방식으로 렌더링 될 수 있다. UI 컴포넌트 스팩의 코드는 서버에서 실행되고 사용자 액션으로 생성된 이벤트에 응답한다.

JSF-RI는 UI 컴포넌트에서 HTML을 렌더링하는 데 쓰이는 커스텀 태그 라이브러리와 함께 렌더 킷을 제공한다. 컴포넌트들의 룩앤필을 커스터마이징 하는 기능도 제공한다. 특별한 컴포넌트가 필요하면 특정 클라이언트 장치용 커스텀 태그를 만들 수 있고 UI 컴포넌트와 커스텀 렌더러와 이를 결합할 수 있다. 다른 디바이스의 경우 다양한 렌더러를 지정하면 된다.

JSF와 UI 컴포넌트
Java AWT 또는 Swing API를 사용하여 Java GUI 애플리케이션을 만든 경험이 있다면 JSF의 UIComponent 에도 익숙할 것이다. 이것은 자식 컴포넌트의 트리를 저장하고 클라이언트 쪽에서 발생한 액션들에 대해 표준 이벤트를 만든다. 이 이벤트들은 FacesContext에 캐싱된다. 커스텀 태그를 사용하여 이들 각 이벤트용 핸들러들을 결합할 수 있다. 예를 들어 커스텀 ActionListener가 있다면 사용자 클릭 또는 형식 제출을 핸들하게 된다.

JSF UIComponent, Renderer, 태그는 언제나 함께한다. 모든 JSF 커스텀 태그는 UIComponentTag를 하위클래스로 나눔으로서 생성된다. doStartdoEnd 메소드는 UIComponentTag 클래스에 이미 구현되었다. 이 태그 클래스에 추가 기능만 제공하면 된다.

그림 1은 커스텀 태그, UI 컴포넌트, 렌더러의 관계이다. 클라이언트 브라우저가 UI 컴포넌트 (MyComponent)용 JSF 태그(jsf:myTag)를 이용하여 JSP 페이지에 액세스한다. UI 컴포넌트는 서버에서 실행되고 적절한 렌더러(MyRenderer)를 사용하는 HTML로서 다시 클라이언트로 렌더링된다. JSP 페이지는 HTML에서 이들을 코딩하기 보다는 JSF-RI의 커스텀 태그를 사용하여 사용자 인터페이스 컴포넌트를 표현한다.

예를 들어, 그림 1은 h:panel:group 태그의 사용방법을 보여주고 있다. 이 태그는 한 부모 밑의 컴포넌트들을 그룹핑하는데 사용된다. panel_gridpanel_data 같은 다른 패널 태그와의 결합에 사용될 때 이것은 런타임시 HTML 테이블의 칼럼을 위한 마크업을 만든다. JSF-RI가 제공되는 html_basic 태그 라이브러리는 텍스트 필드, 버튼 등의 HTML 컴포넌트를 표현하는데 사용된다.

그림 1. JSF 페이지 렌더링
Rendering a JSF page

JSF 수명 주기
JSF 수명 주기는 6개의 단계로 구성되어 있다. 인커밍 리퀘스트는 모든 것을 통과하거나 리퀘스트, 밸리데이션, 수명 주기안에 일어나는 변환 에러, 응답 유형에 따라 어떤 단계도 통과하지 않을 수 있다. JSP 페이지에서 생성된 faces request는 JSF 프레임웍에 의해 핸들되고 faces 또는 non-faces 응답이 리턴된다.

JSF 폼(form)이 제출되거나 사용자가 URL에 /faces 라는 접두사를 가진 페이지를 가르키는 링크를 클릭할 때 faces request가 발생한다. 모든 faces requestFacesServlet에 의해 핸들된다.

어떤 JSF 컴포넌트도 없이 요청이 서블릿이나 JSP 페이지로 보내진 리퀘스트는 non-faces request 라고 한다. 결과 페이지에 JSF 태그가 있으면 faces response 라고 한다. JSF 태그가 없으면 이것은 non-faces response이다.

JSF 수명 주기에는 여섯 단계가 있다:

  • Reconstitute request tree(리퀘스트 트리 재구성)
  • Apply request values(리퀘스트 값 적용)
  • Process validations(타당성 검사)
  • Update model values(모델 값 업데이트)
  • Invoke application(애플리케이션 호출)
  • Render response(응답 렌더링)

JSF 스팩에 따르면 각 단계는 리퀘스트 프로세싱 수명주기의 논리적 개념을 나타내고 있다. 하지만 JSF-RI에서 이 단계들은 실제 클래스와 이것의 이름으로 표현된다.

faces 리퀘스트 다루기
JSF 리퀘스트 프로세싱을 이해하려면 FlightSearch.jsp를 연구해야 한다.(Listing 1) JSF 형식에는 도착/출발 도시용 인풋 텍스트 필드, 출발/도착 날짜, 형식 제출 및 리셋용 버튼이 포함되어 있다.

리퀘스트가 FacesServlet에 의해 받아들여지면 응답이 클라이언트에게로 렌더링되기 전에 다양한 단계를 거친다. 그림 2는 JSF 리퀘스트가 처리되는 방법이다.

1. 리퀘스트 받기
FacesServlet은 리퀘스트를 받아 FacesContextFactory 에서 FacesContext의 인스턴스를 취한다.

2. 수명 주기 프로세싱 전달하기
FacesServlet은 faces 콘텍스트로 전달된 Lifecycle 구현에 대한 execute 메소드를 호출함으로서 Lifecycle 인터페이스로 수명 주기 프로세싱을 전달한다.

3. 각 단계 마다 라이프사이클 실행
Lifecycle 구현은 Reconstitute Component Tree 단계를 시작으로 하여 각 단계를 실행한다.

4. 컴포넌트 트리 생성
Reconstitute Component Tree 단계에서 컴포넌트 트리가 travelForm의 컴포넌트들과 함께 만들어진다. 이 트리는 루트(root)로서 UIForm을, 이것의 자식으로서 다양한 텍스트 필드와 버튼을 갖는다.

fromCity 필드는 공백이 될 수 없음을 지정하는 타당성 규칙을 갖고있다. (validate_required 태그) 이 태그는 JSF Validator가 있는 fromCity 텍스트 필드를 링크한다.

JSF는 여러 빌트인 밸리데이터가 있다. 상응하는 Validator는 이 단계에서 초기화된다. 이 컴포넌트 트리는 FacesContext에 캐싱되고 콘텍스트는 다음 단계에서 사용되어 트리에 액세스하고 이벤트 핸들러를 호출한다. UIForm 스테이트는 자동으로 저장된다. 따라서 이 페이지가 리프레쉬 되면 이 형식의 원리 콘텐츠가 디스플레이된다.

5. 트리에서 값 뽑아내기
Apply Request Values 단계에서 JSF 구현은 컴포넌트 트리를 트래버스하고 decode 메소드를 사용하여 리퀘스트에서 값을 뽑아 각 컴포넌트에 맞게 로컬로 설정한다. 이 프로세스 동안 에러가 있다면 FacesContext에 놓이게되고 Render Response 단계에서 사용자에게 디스플레이된다.

또한 사용자 액션의 결과로 만들어져서 이 단계에 놓이게 된 이벤트들은 등록된 리스너에게 브로드캐스팅된다. 리퀘스트 버튼을 클릭하면 텍스트 필드의 값이 원래 값으로 설정된다.

6. Process Validations
Process Validations 단계에서 각 컴포넌트와 관련된 밸리데이션은 Apply Request Values 단계에서 설정된 로컬 값에 대해 수행한다. 이것은 JSF 구현이 각각의 등록된 밸리데이터에 대해 validate 메소드를 호출할 때 발생한다.

밸리데이션이 실패하면 수명 주기는 같은 페이지가 렌더링되는 Render Response 단계로 진행된다. 하지만 에러 메시지도 함께다. 이 단계에서 놓여진 이벤트들은 등록된 리스너에게 브로드캐스팅 된다.

JSF 구현은 소스 필드에 대한 밸리데이터를 처리한다. 데이터가 무효가 되면 콘트롤은 Render Response 단계로 전달되는데 이곳은 FlightSearch.jsp가 관련 컴포넌트에 대해 디스플레이된 밸리데이션 에러와 함께 다시 렌더링 되는 곳이다. JSP 페이지에서 output_errors를 선언함으로서 이 페이지의 모든 에러는 페이지 하단에 디스플레이된다.

7. 모델 객체 값 설정하기
Update Model Values 단계에서 모든 밸리데이션이 성공적으로 처리된 후에 JSF 구현은 각 컴포넌트에 대한 updateModel 메소드를 호출하여 유효한 것으로 모델 객체 값을 설정한다. 로컬 데이터를 모델 객체 속성에 의해 지정된 유형으로 변환하는 동안 에러가 발생하면 수명 주기는 Render Response 단계로 옮겨간다. 이곳에서 에러가 디스플레이된다. 형식 필드 속성의 값은 모델 객체의 애트리뷰트 값으로 파퓰레이트 된다.

8. ActionListener 호출
ActionListener와 사용자 액션을 결합할 수 있다.(Listing 1) Invoke Application 단계에서 processAction 메소드는 FlightSearchActionListener에 호출된다. 호출 시 processAction 메소드는 실제 시나리오에서 기준에 맞는 비행 데이터베이스를 검색하고 컴포넌트의 액션 애트리뷰트에서 결과를 검색한다.

9.응답 렌더링
Render Respons 단계에서 faces 설정 파일에서 검색되어 얻어진 페이지(FlightList.jsp)는 faces 콘텍스트에 에러가 없다면 디스플레이된다. 이전 페이지의 에러 때문에 콘트롤이 이 단계에 오면 FlightSearch.jsp는 에러 메시지와 함께 다시 디스플레이된다.

그림 2. JSF 리퀘스트 프로세스
그림보기.

Listing 1. FlightSearch.jsp
 <%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %> <%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %> <f:use_faces> <h:form id="flightForm" formName="flightForm" > <h:input_text id="fromCity" valueRef="FlightSearchBean.fromCity"> <f:validate_required/> <h:input_text/> <h:input_text id="toCity" valueRef="FlightSearchBean.toCity"> <h:input_text id="departureDate" valueRef="FlightSearchBean.departureDate"> <h:input_text id="arrivalDate" valueRef="FlightSearchBean.arrivalDate"> <h:command_button id="submit" action="success" label="Submit" commandName="submit" > <f:action_listener type="foo.bar.FlightSearchActionListener"/> </h:command_button> <h:command_button id="reset" action="reset" label="Reset" commandName="reset" /> <h:output_errors/> </h:form> </f:use_faces> 

JSF-RI의 두 개의 태그 라이브러리가 이 코드에 사용된다. html_basic 태그 라이브러리는 일반적으로 사용되는 HTML 컴포넌트용 태그를 정의하고 jsf-core 태그 라이브러리에는 리스너와 벨리데이터를 등록하는데 사용되는 태그가 포함되어 있다. 기타 태그로는:

  • f:use_faces 태그는 JSF 구현에 이 태그가 faces 태그라는 것을 가르킨다.

  • f:validate_required 태그는 어태치 되는 필드가 형식이 제출되기 전에 값을 가져야 한다는것을 나타낸다.

  • h:formh:input_text 태그는 flightSearchForm이라고 하는 HTML 형식과 다양한 텍스트 필드를 각각 나타낸다.

  • h:command_button 태그는 Submit과 Reset버튼을 구현에 사용된다.

  • 마지막으로 h:output_errors 태그는 Struts html:errors 태그와 비슷하고 형식 필드의 밸리데이션 동안 발생한 에러를 디스플레이한다.

FlightSearchBean이라고 하는 JavaBean은 Updated Model Values 단계에 있는 동안 UIComponent 데이터 부터 업데이트되는 모델을 나타낸다. 일반적으로 JavaBean은 jsp:useBean 태그와 함께 JSP 페이지에서 선언된다. FlightSearch.jsp에서 수행되지 않았다는 것에 주목하라. faces 설정 파일의 JSP 페이지들에 의해 사용되는 모든 JavaBean 컴포넌트들을 선언할 수 있는 곳에서 Managed Beans 라고 하는 JSF 기능을 사용할 수 있기 때문이다. 설정할 때 서블릿 콘테이너는 이러한 JavaBean 컴포넌트를 초기화한다. faces-config.xml 파일의 FlightSearchBean용 엔트리는 Listing 2에 나타나있다:

Listing 2. TravelInfoBean용 faces-config.xml 엔트리
 <managed-bean> <managed-bean-name>FlightSearchBean</managed-bean-name> <managed-bean-class> foo.bar.FlightSearchBean </managed-bean-class> <managed-bean-scope>session</managed-bean-scope> </managed-bean> 

faces 응답 렌더링
결과 JSP 페이지들이 JSF 태그를 포함하고 있을 때 faces 응답은 Faces 애플리케이션에 의해 생성된다. 이 응답은 faces 또는 non-faces 리퀘스트의 결과가 될 수 있다.

우리 예제에서 Listing 1의 페이지 렌더링은 faces 응답이다. doStartTag()doEndTag()메소드에 익숙할 것이다. JSF와 Struts-Faces에서 각 태그는 UIComponentTag에서 확장된다. UIComponentTagdoStartTag()doEndTag() 메소드를 구현한다.

또한 추상 메소드인 getComponentType()getRendererType()을 갖고있다. 이 구체적 태그 클래스에 이 두 개의 메소드를 구현함으로서 컴포넌트와 렌더러를 각각 지정할 수 있다.

텍스트 필드를 가진 JSF 형식을 생각해보자. 다음 순서는 JSF 형식이 렌더링 될 때 실행된다.

1. doStartTag() 메소드 호출하기
Servlet 콘테이너는 FormTagdoStartTag() 메소드를 호출한다.

2. UIComponent 얻기
FormTaggetComponentType() 메소드에서 UIComponent를 얻는다. UIComponentTaggetComponentType()을 사용하여 faces-config.xml 파일로부터 이 컴포넌트에 대한 클래스 이름을 살피고 UIComponent(FormComponent)의 인스턴스를 만든다.

3. 렌더러 얻기
FormTaggetRendererType 메소드에서 Renderer를 얻는다. 컴포넌트 유형에서처럼 렌더러 이름은 faces-config.xml 파일에서 검색된다.

4.인코딩 메소드 호출
FormComponentFormRenderer가 만들어진 후에 encodeBegin() 메소드는 FormComponent에 호출된다. 각 태그를 위해 렌더링은 encodeBegin()으로 시작하고 encodeEnd()로 끝난다. encodeBegin() 메소드는 중첩 순서로 호출된다.

5. 태그의 끝과 HTML 렌더링
서블릿 컨테이너는 이 태그에 대해 doEndTag() 메소드를 호출한다. encodeEnd() 메소드는 각 컴포넌트에 대해 중첩의 역순으로 호출된다. 결국 Form과 모든 중첩된 컴포넌트들은 HTML로서 렌더링된다. 이 지점에서 HTML 생성은 종료되고 JSP와 동등한 HTML이 렌더링된다.

그림 3은 faces 응답의 생성을 구성하는 이벤트 순서이다.

그림 3. faces 응답 렌더링
그림보기.

통합의 이유
JSP와 관련 스팩이 발전함에 따라 JSF와 JSP Standard Tag Library 같은 새로운 표준이 등장하고 있다. 다음은 이 새로운 기술들을 하나의 통합된 전체로서 사용할 때의 이점들이다:

  • 작동과 프리젠테이션의 깨끗한 분리. 태그, 렌더러, 컴포넌트의 분리로 페이지 작성자와 애플리케이션 개발자의 역할은 정의가 더 분명해진다.

  • 컴포넌트의 프리젠테이션 변경이 큰 영향을 끼치지 못한다. 이제는 렌더러만 쉽게 변경하면 된다. 전통적인 MVC 모델에서 태그의 모든 변경은 비지니스 로직의 변경까지 수반했다.

  • 렌더러 독립. 다중의 렌더러로 다중의 프리젠테이션 디바이스용 컴포넌트 로직을 재사용함으로서 프로토콜 독립도 가능하다. 다양한 렌더러를 사용할 수 있다는 것은 특정 디바이스에 대한 전체 프리젠테이션 티어의 코딩 필요성도 감소한다는 것을 의미한다.

  • 커스텀 컴포넌트의 어셈블링과 재사용 표준. JSF는 "형식과 필드"를 넘어 생각하고 커스텀 GUI 컴포넌트 렌더링에 풍부한 컴포넌트 모델을 제공한다. JSF를 사용하여 각 페이지마다 컴포넌트의 모습과 작동을 커스터마이징 할 수 있다. 개발자들은 그들만의 GUI 컴포넌트를 만들 수 있는 데다가 간단한 커스텀 태그로 모든 JSP 페이지에 쉽게 포함시킬 수 있다. 자바 프론트엔드 GUI 컴포넌트인 AWT와 Swing과 마찬가지로 웹 페이지에 대한 커스텀 컴포넌트들이 있다!

Struts는 이미 사용자 기반이 넓게 형성되어 있는 프레임웍이다. 많은 IT 부서들은 MVC 프레임웍의 가치를 인식했고 꽤 오랫동안 사용해왔다. JSF는 Struts의 강력한 컨트롤러 아키텍쳐를 갖고있지 않다. 마찬가지로 표준화된 ActionFormActions도 없다. Tiles를 통합하면 완벽한 방식으로 레이아웃을 재사용 및 변경할 수 있다.

JSF가 가능한 Struts 애플리케이션의 마이그레이션은 두 가지 도전과제가 있다. 하나는 Struts 태그가 JSF 호환이 아니라는 점이다. 다시말하면 JSF 스팩에서 요구하는 대로 UIComponentTag를 확장하지 않는다. 따라서 JSF는 UIComponentRenderers를 해석할 수도 결합할 수도 없다.

두 번째, FacesServletRequestProcessor 사이에 어떤 연결도 없다. Struts 애플리케이션에서 RequestProcessor는 콜백 메소드로 ActionFormActions 클래스로의 모습을 관리한다. ActionForm 프로퍼티용 게터와 세터와 validate()ActionForm의 콜백 메소드이다. Action의 경우 execute()가 콜백 메소드이다. RequestProcessor가 호출되지 않는한 Struts ActionFormActions 클래스의 콜백 메소드는 비지니스 로직을 호출할 기회를 가질 수 없다.

Struts와 JSF를 Struts-Faces와 통합하기
이쯤되면, Struts와 JSF를 통합할 수 있는 소프트웨어가 있다면 또는 통합 소프트웨어를 직접 작성하는 방법에 대해 궁금할 것이다.

좋은 소식이 있다. 소프트웨어는 이미 존재하고 있다. Struts-Faces는 Struts JSF 통합 라이브러리의 초기 릴리스이다. 이 라이브러리는 Craig McClanahan이 만들었고 기존 Struts 애플리케이션을 JSF에 마이그레이션할 수 있도록 했다. Struts-Faces는 JSF를 이용한 깔끔한 통합을 위해 노력하고 있다. JSF가 프론트엔드에서 사용되고 백엔드에는 익숙한 Struts 컴포넌트가 있도록 말이다.

그림 4는 Struts-Faces와 JSF 클래스 관계이다. 파란색 클래스들은 Struts-Faces에 속한다.

그림 4. Struts-Faces 클래스 다이어그램
그림보기.

다음은 Struts-Faces의 주요 컴포넌트들이다:

  • FacesRequestProcessor 클래스는 모든 faces 리퀘스트를 핸들한다. Non-faces 리퀘스트는 부모인 RequestProcessor로 보내진다.

  • ActionListenerImpl 클래스는 ActionEvents를 핸들한다.

  • FormComponent는 JSF Form Component 로 부터 확장하지만 Struts 수명 주기 내에 호출된다.

  • 렌더러 FormComponent용 태그.

  • 아웃풋 전용으로 렌더링되는 데이터용 태그와 렌더러.

  • ServletContextListener의 구현은 초기화 동안 적절한 RequestProcessor를 등록한다.

  • filefaces-config.xml 파일. struts-faces.jar 파일에 번들된다.

Listing 3은 Struts-Faces 태그를 사용하는 FlightSearch.jsp 이다.

Listing 3. Struts-Faces 태그를 사용하는 FlightSearch.jsp
 <%@ taglib uri="http://java.sun.com/jsf/html" prefix="h" %> <%@ taglib uri="http://java.sun.com/jsf/core" prefix="f" %> <%@ taglib uri="http://jakarta.apache.org/struts/tags-faces" prefix="s" %>  <f:use_faces> <s:form action="/listFlights"> <h:input_text id="fromCity" valueRef="FlightSearchForm.fromCity"/> <h:input_text id="toCity" valueRef="FlightSearchForm.toCity"/> <h:input_text id="departureDate" valueRef="FlightSearchForm.departureDate"> <h:input_text id="arrivalDate" valueRef="FlightSearchForm.arrivalDate"> <h:command_button id="submit" action="success" label="Submit" commandName="submit" /> <h:command_button id="reset" action="reset" label="Reset" commandName="reset" /> <s:errors/> </s:form> </f:use_faces> 

s:form 태그는 HTML 형식을 만드는데 사용된다. 이 형식 액션 애트리뷰트는 Listing 1에 지정된 형식 이름 flightForm 대신 /listFlights 이다. JSF에서 형식 이름은 UIForm에 할당된 이름일 뿐이다.

FlightSearchBean은 JSF Form용 모델이고 Update Model Values 단계에서 값을 얻는다. 하지만 Struts에서 형식 액션은 Struts Configuration File, struts-config.xml의 ActionMapping을 가르킨다. 작동 방법을 알려면 Listing 4의 struts-config.xml 파일을 참조한다.

/listFlightsActionMapping이, URI-path용 ActionFormfoo.bar.FlightSearchForm 이고 Action 클래스는 foo.bar.FlightSearchAction을 나타낸다는 것을 알게될 것이다.

Listing 4. struts-config.xml의 Action 선언
 <form-bean name="FlightSearchForm" type="foo.bar.FlightSearchForm"/> <!-- ========== Action Mapping Definition ========================= --> <action-mappings> <!-- List Flights action --> <action path="/listFlights" type="foo.bar.FlightSearchAction" name="FlightSearchForm" scope="request" input="/faces/FlightSearch.jsp"> <forward name="success" path="/faces/FlightList.jsp"/> </action> </action-mappings> 

Struts와 Tiles를 통합하는 다섯 단계

1. JSP를 만들어 사이트 레이아웃을 표현한다. 이것이 마스터 JSP로서 헤더, 바디, 풋터를 위한 플레이스홀더를 갖고있다. 이들 각각은 Tiles 태그를 사용하여 메인 JSP 페이지에 추가된다.

2. Tiles 정의 파일을 만들고 어떤 JSP 페이지가 전체 페이지의 플레이스홀더에 포함되어야 하는지를 정한다. 모든 페이지를 고유의 이름으로 지정한다.

3. struts-config.xml 파일에서 글로벌과 로컬 포워드를 변경하여 이전 단계에서 지정한 고유 이름들을 사용한다.

4. TilesPlugIn을 사용하여 시작하는 동안 Tiles 정의 파일을 로딩한다. TilesPlugIn 엔트리를 struts-config.xml 파일에 추가한다.

5. TilesRequestProcessor 엔트리를 struts-config.xml 파일에 추가한다. 이것이 Tiles-Struts 애플리케이션용 디폴트 요청 프로세스이다.

.do에 액션 애트리뷰트가 없는것을 눈치챘을 것이다. Struts-Faces가 형식 이름으로 형식 액션을 사용하기 때문이다.

또한 JSF validation 태그를 여기서는 사용하지 않았다. Struts에서 밸리데이션은 ActionForm 클래스에 대한 validate() 메소드에서 발생하기 때문이다. s:errors 태그는 Struts 에러 태그와 비슷하고 밸리데이션 동안 발생한 에러 메시지를 디스플레이하는데 사용된다.

어떤 ActionListener도 Submit 버튼과 결합되지 않았다. ActionListener가 이미 Struts-Faces에 제공되고 언제나 ActionEvents와 함께 faces 리퀘스트를 FacesRequestProcessor로 보내기 떄문이다.

Struts 애플리케이션을 JSF에 마이그레이션 하기
다음은 Struts 웹 애플리케이션을 JSF와 통합하는 단계이다:

  • JSF-specific JARs (jsf-api.jar, jsf-ri.jar)와 함께 struts-faces.jar 파일을 웹 애플리케이션의 WEB-INF/lib 디렉토리에 추가한다.

  • JSF와 JSTL을 사용 할 계획이면 JSTL-specific JARs (jstl.jar, standard.jar)를 WEB-INF/lib 폴더에 추가한다. 이 단계는 일반적 Tomcat에 전개 할 경우에만 필요하다. JWSDP는 이미 이러한 JAR를 제공한다.

  • 웹 애플리케이션 전개 디스크립터 (/WEB-INF/web.xml)를 변경하여 Faces Servlet 정의(Listing 5)용 엔트리를 갖는다.

  • JSP 페이지를 변경하여 Struts 태그 대신 JSF와 Struts-Faces 태그를 사용한다. 특히 html, base, form, errors 태그를 Struts-Faces의 해당하는 것으로 대체한다. text, textarea, radio 태그를 이에 상응하는 JSF 태그로 대체한다. Struts-Faces는 이를 위한 개별 태그를 갖고있지 않다. 필수사항은 아니지만 Struts Logic 태그를 해당 JSTL 태그로 대체하는 것도 고려할 수 있다.

  • JSF 태그를 사용하고 struts-config.xml 파일을 변경하여 JSP을 가르키는 Action Mappings의 global-forwardslocal-forwards/faces 접두사를 추가한다.

  • 웹 애플리케이션이 자신이 만든 커스텀 컴포넌트를 사용한다면 이들을 JSF 구현의 디폴트 RenderKit으로 등록해야한다. WEB-INF 폴더의 faces-config.xml에 파일을 만들고 각 컴포넌트와 렌더러용 엔트리를 추가하여 수행한다. 하지만 faces-config.xml 파일은 Struts-Faces자 파일에 이미 번들되어 있다. struts-faces.jar 파일에서 이를 빼어내어 콘텐츠에 추가하고 WEB-INF 폴더 밑에 놓는다.

Listing 5. web.xml의 FacesServlet 선언
 <!-- JavaServer Faces Servlet Configuration --> <servlet> <servlet-name>faces</servlet-name> <servlet-class>javax.faces.webapp.FacesServlet</servlet-class> <load-on-startup>1</load-on-startup> </servlet> <!-- JavaServer Faces Servlet Mapping --> <servlet-mapping> <servlet-name>faces</servlet-name> <url-pattern>/faces/*</url-pattern> </servlet-mapping> 

Struts-Faces와 Tiles 통합의 문제점
Struts-Faces 라이브러리는 Struts와 JSF 사이에 효율적인 다리를 제공한다. J2EE 웹 애플리케이션에 풍부한 프리젠테이션 레이어를 가능하게 한다. 이 결합물에 Tiles를 추가하여 보다 풍부하게 프리젠테이션 레이어를 만들 수 있다. 따라서 Struts와 JSF 결합에서 이득도 보고 다양한 JSP 페이지를 효율적으로 재사용할 수 있다.

이 글에서는 이미 Struts와 JSF의 통합을 설명했다. 그렇다면 Tiles를 여기에 추가하는 것은 식은죽 먹기 아니겠는가?

불행히도 JSF는 아직 초기 단계에 머물러있다. Struts-Faces 통합 소프트웨어는 계속 개발되어 JSF의 다양한 기능을 수용하지만 아직 Tiles는 지원하지 않는다.

Struts와 Tiles는 완벽히 상호 작동할 수 있다. 하지만 통합할 때 장애도 있다. 다음 섹션에서는 Struts-Faces 통합 라이브러리를 Tiles와 함께 사용할 때의 일반적인 문제점을 설명한다.

Listing 6은 Flight Search 페이지용 레이아웃이다. 이를 FlightSearch.jsp가 아닌 Flight Search로 부르는것에 주목하라. FlightSearch JSP가 foobar 여행사 웹 사이트상에서 사용자가 보는 전체 페이지의 바디이기 때문이다.

이제부터 FlightSearch.jsp를 있는 그대로 적용할 것이다. 앞으로 진행해가면서 변경할 것이다. 여러분은 Flight Search 페이지용 정의와 함께 Tiles 정의 파일을 만들어야 한다. Listing 7은 Tiles 정의 파일의 Flight Search Page용 엔트리이다. 마스터 레이아웃 템플릿과 extends 애트리뷰트가 재사용되었다.

Listing 6. Flight Search 레이아웃 예제
 <%@ taglib uri="/WEB-INF/struts-tiles.tld" prefix="tiles" %> <%@ taglib uri="http://jakarta.apache.org/struts/tags-faces"prefix="s" %> <!-- Layout component parameters: header, menu, body, footer --> <s:html> <head> <title> <tiles:getAsString name="title"/></title> <s:base/> </head> <body> <TABLE border="0" width="100%" cellspacing="5"> <tr> <td><tiles:insert attribute="header"/></td> </tr> <tr> <td><tiles:insert attribute="body"/></td> </tr> <tr><td><hr></td></tr> <tr> <td><tiles:insert attribute="footer" /></td> </tr> </TABLE> </body> </s:html> 


Listing 7. Flight Search Page에 대한 Tiles 정의.
 <!-- Master Layout definition --> <definition name="foobar.master-layout" path="/faces/layout/MasterLayout.jsp"> <put name="title" value="Welcome to Foo Bar Travels" /> <put name="header" value="/faces/common/header.jsp" /> <put name="footer" value="/faces/common/footer.jsp" /> <put name="body" value="" /> </definition> <!-- Definition for Flight Search Page --> <definition name="/foobar.flight-search" extends="foobar.master-layout"> <put name="body" value="/faces/FlightSearch.jsp" /> </definition> 

응답 커밋
Flight Search Form에 액세스를 시도하자마자 보게될 첫 번째 문제가 바로 이것이다. 스택 트레이스를 자세히 살펴보자. 문제는 com.sun.faces.lifecycle.ViewHandlerImpl 클래스에 있다는 것을 알 수 있다. 이것은 ViewHandler 인터페이스를 구현하는 JSF-RI 클래스이다.

그림 2ViewHandler가 수행하는 역할이다. 이것은 리퀘스트를 다음 페이지로 보내는 클래스이다. 리퀘스트를 보낼 때 보내기 전에 응답 상태를 검사하지 않는다. 이것은 Tiles를 사용할 때만 발생하는데 Tiles는 JSP 페이지를 응답에 포함시키기 때문이다. 그리고 JSF-RI는 첫 번째 포워드 후에 응답을 커밋하고 그런다음 다시 다음 Tiles로 보내기를 시도한다.

이 문제를 해결하려면 응답이 커밋 되었는지의 여부를 결정할 수 있도록 응답 상태를 검사할 커스텀 ViewHandler를 구현해야 한다. 응답이 커밋되지 않으면 리퀘스트는 다음 페이지로 넘어간다. 그렇지않으면 리퀘스트는 포함되고 적당한 JSP가 디스플레이된다. ViewHandler 인터페이스를 구현하고 필요한 renderView() 메소드를 구현하는 STFViewHandlerImpl 클래스를 만들 것이다. (Listing 8):

Listing 8. STFViewHandlerImpl의 renderView() 메소드
 RequestDispatcher rd = null; Tree tree = context.getTree(); String requestURI = context.getTree().getTreeId(); rd = request.getRequestDispatcher(requestURI); /** If the response is committed, include the resource **/ if( !response.isCommitted() ) { rd.forward(request, context.getServletResponse()); } else { rd.include(request, context.getServletResponse()); } 

커스텀 ViewHandler를 구현했으니 JSF-RI에게 디폴트 구현 대신 자신의 ViewHandler를 사용한다고 어떻게 공지하겠는가? 이 질문에 대한 답으로 FacesServlet의 작동을 이해해야 한다.

Faces 초기화 과정 중 FacesServletLifecycleFactory 구현에 Lifecycle 클래스 구현을 리턴할 것을 의뢰한다. (Listing 9):

Listing 9. FacesServlet의 Faces 초기화
 //Get the LifecycleFactory from the Factory Finder LifecycleFactory factory = (LifecycleFactory) FactoryFinder.getFactory("javax.faces.lifecycle.LifecycleFactory"); //Get the context param from web.xml String lifecycleID = getServletContext().getInitParameter("javax.faces.lifecycle.LIFECYCLE_ID"); //Get the Lifecycle Implementation Lifecycle lifecycle = factory.getLifecycle(lifeCycleID); 

Lifecycle 구현 객체는 Render Response 단계에서 사용될 ViewHandler를 갖고 있다. Lifecycle 구현에 setViewHandler 메소드를 호출하여 ViewHandler 구현이 디폴트가 되도록 할 수 있다.

이제 질문은 디폴트 Lifecycle 구현을 어떻게 얻을 수 있느냐이다. 답은 이럴 필요가 없다는 것이다. 그저 새로운 구현을 만들어 고유 ID를 가진 LifecycleFactory로 이를 등록하면된다. (Listing 10):

Listing 10. 커스텀 ViewHandler와 Lifecycle 등록하기
 //Get the LifecycleFactory from the Factory Finder LifecycleFactory factory = (LifecycleFactory) FactoryFinder.getFactory("javax.faces.lifecycle.LifecycleFactory"); //Create a new instance of Lifecycle implementation - //com.sun.faces.lifecycle.LifecycleImpl //According to the documentation, factory.getLifecycle("STFLifecycle") //should work, but JSF-RI has a defect. //Hence this workaround of creating a RI class explicitly. LifecycleImpl stfLifecycleImpl = new LifecycleImpl(); //Create a new instance of our STFViewHandler and set it on the Lifecycle stfLifecycleImpl.setViewHandler(new STFViewHandlerImpl()); //Register the new lifecycle with the factory with a unique //name "STFLifecycle" factory.addLifecycle("STFLifecycle", stfLifecycleImpl); 

lifecycleIdSTFLifecycle 처럼 하드코딩된 것을 볼 수 있다. 사실 그렇지 않다. Listing 9를 다시 보면 명확해진다. FacesServlet은 lifecycle ID를 javax.faces.lifecycle.LIFECYCLE_ID라는 이름과 함께 web.xml 파일에서 선언된 콘텍스트 매개변수에서 취한다:

 <context-param> <param-name>javax.faces.lifecycle.LIFECYCLE_ID</param-name> <param-value>STFLifecycle</param-value> </context-param> 

FacesServlet이 초기화 동안 Lifecycle 구현 클래스에 대해 결정하기 때문에 Listing 10의 코드는 FacesServlet이 초기화 되기 전에 실행되어야 한다. 또 다른 서블릿을 만들어 FacesServlet 전에 이를 초기화하여 이를 수행할 수 있다.

보다 현명한 방법은 ServletContextListener 인터페이스를 구현하는 것이다. 이 클래스는 두 개의 메소드인 contextInitialized()contextDestroyed()를 선언한다. 이것은 웹 애플리케이션이 구현될 때와 웹 애플리케이션이 없어지기 전에 호출된다. Listing 10의 코드는 contextInitialized() 메소드에서 실행되고 커스텀 ViewHandlerSTFLifecycle이라는 이름으로 구분된 Lifecycle로 등록된다. FacesServlet도 가능하다. ServletContextListener 클래스 자체는 web.xml 파일에서 선언된다.

 <listener> <listener-class>foo.bar.stf.application.STFContextListener </listener-class> </listener> 

이것이 Lifecycle을 커스텀 ViewHandler로 등록하는 유일한 접근방식은 아니다. 사실 FactoryFinder는 고유의 발견 알고리즘을 구현하여 LifecycleFactory를 포함하여 Factory 객체를 발견한다. 이 메커니즘은 쉽기는 하지만 매력적이지는 않다.

404 Resource Not Found
커밋된 응답 문제가 해결되면 아무 Tiles 스팩 링크를 클릭하거나 Faces 응답을 렌더링하는 URL을 입력한다. 이 경우 URL을 입력하여 FlightSearchForm을 디스플레이 할 수 있다.

이렇게 하면 foobar.flight-search - 404 Resource Not Found 에러가 나타난다. foobar.flight-search는 Flight Search 페이지용 Tiles 정의 이름이다. FacesRequestProcessor는 Tiles 리퀘스트를 처리 할 능력이 없다. 따라서 에러가 생긴다.

이 문제를 해결하려면 STFRequestProcessor라는 새로운 리퀘스트 프로세서를 만들어야 한다. 지금까지는 FacesRequestProcessor의 모든 코드를 새로운 클래스에 복사했다. 유일한 차이점은 일반적인 RequestProcessor를 하위클래스로 나누는 대신 STFRequestProcessorTilesRequestProcessor를 하위클래스로 나눈다는 점이다. 이 새로운 RequestProcessor가 Tiles 리퀘스트를 핸들할 수 있다. (Listing 11)

Listing 11. STFRequestProcessor.java
 public class STFRequestProcessor extends TilesRequestProcessor { protected void doForward(String uri, HttpServletRequest request, HttpServletResponse response) throws IOException,ServletException { //copy code from FacesRequestProcessor } protected void doInclude(String uri, HttpServletRequest request, HttpServletResponse response) throws IOException, ServletException { //copy code from FacesRequestProcessor } protected String processPath(HttpServletRequest request, HttpServletResponse response) throws IOException { //copy code from FacesRequestProcessor } protected String processPopulate(HttpServletRequest request, HttpServletResponse response) throws IOException { //copy code from FacesRequestProcessor } private void selectTree(FacesContext context, String uri) { //copy code from FacesRequestProcessor } } 

알다시피 Struts 프레임웍용 RequestProcessor는 struts-config.xml 파일에서 지정된다. STFRequestProcessor는 다음 엔트리가 struts-config.xml 파일에 추가될 때 프로세서가 된다.

 <controller processorClass="foobar.stf.application.STFRequestProcessor" /> 

Form submission
STFRequestProcessor 덕택에 Flight Search 페이지를 검색하고 볼 수 있다. 하지만 Flight Search 형식을 제출하자마자 응답으로 같은 형식을 받게된다. 그렇지만 헤더와 풋터가 없다. 밸리데이션 에러도 없다. 사실 밸리데이션 자체가 없는것이다.

어떤 상황인지 알고싶다면 Flight Search 페이지로 다시가서 HTML 소스를 보자. 다음과 같은 엔트리를 보게 될 것이다.

 <form name="FlightSearchForm" method="post" action="/flightapp/faces/FlightSearch.jsp"> 

형식 액션이 대신 JSP 페이지를 가르키고 있다. 이것이 바로 문제이다. Tiles와 Struts-Faces를 사용했기 때문에 발생하는 새로운 문제는 아니다. 이것은 형식 액션과 같은 JSP 이름을 갖기위한 Struts-Faces의 디폴트 작동이다. 이것은 하나의 JSP 페이지를 갖게 되면 문제없이 실행된다. Listing 3은 원래 FlightSearch.jsp이다. 액션을 다음과 같이 변경해보자.

 <s:form action="/listFlights.do> 

물론 변경 자체로는 문제를 해결할 수 없다. 이정도의 변경으로는 STFRequestProcessorActionForm을 찾을수 없다는 것도 알게될 것이다. 더 많은 변경이 필요하다.

계속 진행하기 전에 그림 5를 보자. Struts-Faces 형식용 faces 응답을 렌더링할때의 이벤트 결과를 보여주고 있다. FormComponent의 하이라이트된 createActionForm()메소드를 제외하고는 그림 3과 같다. Struts-Faces API에 의해 제공된 FormComponent 클래스는 javax.faces.component.UIForm의 특별한 서브클래스고 리퀘스트 또는 세션 범위에서 형식 빈들의 자동 생성을 지원한다.

그림 5. Struts-Faces 응답 렌더링
그림보기.

createActionForm() 메소드는 액션 이름을 사용하여 Struts 설정 파일에서 ActionMapping을 얻는다. /listFlights.doActionMapping이 없기때문에 Struts는 ActionForm을 찾을 수 없다.

org.apache.struts.util.RequestUtils를 사용하여 문제를 해결할 수 있다. RequestUtils의 정적 메소드인 getActionMappingName()은 경로(/x/y/z)나 접미사(.do) 매핑을 적절한 ActionMapping 으로 분해할 수 있다.

Listing 12는 createActionForm 메소드의 변경사항을 굵게 표시하여 보여주고 있다. Struts-Faces의 FormComponent를 변경하는 대신 FormComponent를 하위클래스로 나누고 createActionForm()메소드를 오버라이드 하여 새로운 STFFormComponent를 만든다.

Listing 12. FormComponent의 변경된 createActionForm() 메소드
 // Look up the application module configuration information we need ModuleConfig moduleConfig = lookupModuleConfig(context); // Look up the ActionConfig we are processing String action = getAction(); String mappingName = RequestUtils.getActionMappingName(action); ActionConfig actionConfig = moduleConfig.findActionConfig(mappingName); .... .... 

새로운 STFFormComponent에 변경이 더 필요하다. Struts-Faces는 액션 이름을 형식 이름으로 취급한다. 액션에는 .do 접미사가 있고 형식 이름에는 .do 접미사가 없기 때문에 변경이 필요하다. 따라서 새로운 속성인 actionSTFFormComponent에 추가하고 getAction()setAction() 메소드를 오버라이드한다.

FormRenderer 변경
Listing 10에서 했던 변경을 FormRendererencodeBegin 메소드에도 비슷하게 적용해야 한다.

FormRenderer를 하위클래스로 나누면 된다. HTML로 작성된 형식 액션도 변경해야 한다. (Listing 13):

Listing 13. FormRenderer 변경
 protected String action(FacesContext context, UIComponent component) { String treeId = context.getTree().getTreeId(); StringBuffer sb = new StringBuffer (context.getExternalContext().getRequestContextPath()); sb.append("/faces"); // sb.append(treeId); -- This is old code, replaced with // the two lines below. STFFormComponent fComponent = (STFFormComponent) component; sb.append(fComponent.getAction());  return (context.getExternalContext().encodeURL(sb.toString())); } 

FormTag 변경
이미 알고있겠지만 컴포넌트와 렌더러를 변경할 때 이 태그 역시 변경해야 한다. 이 경우 Struts-Faces의 FormTag에서 하위클래스로 나누어 STFFormTag 라는 새로운 태그를 만든다. 기능적인 부분은 바

'JAVA > JSP_Servlet' 카테고리의 다른 글

[펌] Log4J...  (0) 2004.03.30
[펌] JSF 사용하기  (0) 2004.03.29
[펌] 리팩토링(Refactoring)  (0) 2004.03.29
[펌] 스트럿츠 이디엄  (0) 2004.03.29
[펌] 동적 서블릿 이미지 메이킹을 마음대로!  (0) 2004.03.29
Posted by tornado
|

출처 : http://www-903.ibm.com/developerworks/kr/java/library/j-gss-sso.html

GSS-API와 Kerberos 티켓을 사용하여 SSO를 구현하는 클라이언트/서버 자바 애플리케이션 디자인

Level: Intermediate

Faheem Khan
컨설턴트
2003년 9월 9일

점점 더 많은 패스워드 보안 애플리케이션을 조직의 컴퓨터 환경에 추가한다면 이는 인증의 복잡함도 함께 가중시키는 것이고 결국 개발자와 사용자에게 부담을 주게 된다. 대부분의 엔터프라이즈 애플리케이션 통합 프로젝트들은 싱글사인온(SSO) 기능을 포함하고 있다. 이는 사용자가 다양한 애플리케이션을 사용하기 위해 단 한번만 로그인 하도록 하는 기능이다. 이 글에서 자바 플랫폼에 SSO를 구현하는 방법을 설명한다.

당신의 기업은 많은 자바 애플리케이션들을 구동하고 있으며 그 엔터프라이즈 리소스에 접근하기 위해서는 인증이 필요합니까? 그렇다면 싱글사인온(SSP) 보안 기능을 구현하여 사용자 침입을 최소화 할 필요가 있다. 이 글에서 Kerberos와 Java Generic Security Services API (GSS-API)를 사용하여 SSO를 구현하는 방법을 배우게 될 것이다. 우선 SSO의 정의를 설명하고 강력한 애플리케이션을 설명할 것이다. 그런 다음 Kerberos 기반의 SSO를 구현할 때 발생하는 메시지 교환 시퀀스도 연구한다. 자바 GSS-API와 GSS를 사용하여 SSO를 달성하는 전형적인 자바 애플리케이션 아키텍쳐를 간단히 소개한다. 마지막으로 이 모두를 종합하여 코드 예제를 선보인다. 자바 개발자가 GSS Kerberos 티켓으로 SSO를 어떻게 구현하는지 보게 될 것이다.

'싱글사인온'이란 무엇인가?
근본적으로 싱글사인온 인증이 의미하는 것은 인증 데이터의 공유이다. 예를 들어 웨어하우징 회사의 많은 사원들은 엔터프라이즈 리소스(데이터베이스 테이블 등)에 접근하여 그들의 업무에 필요한 것을 수행해야 한다. 동시에 다른 사원들도 작업 내용에 따라 다양한 리소스가 필요하다. 회계 담당자는 회계 관련 데이터베이스 테이블에 접근해야 하는 반면 판매 담당자는 판매 관련 데이터베이스 테이블에 접근해야 한다. CEO라면 기업의 데이터베이스 어디에나 접근이 필요할 것이다.

분명한 것은, 이 기업은 무엇보다도 제대로 된 인증 메커니즘이 필요하다. 어떤 사원이 특정 리소스에 접근을 시도했는지 결정할 수 있도록 말이다. 일단 기업 인증 모듈이 사원의 존재를 알면 엔터프라이즈 구현 안의 인증 모듈은 인증이 있는 사용자가 적절한 권한을 갖고 리소스에 접근했는지의 여부를 검사할 수 있다.

코드
j-gss-sso.zip 파일에는 이 글에 사용된 모든 코드가 포함되어 있다. 이 글의 각 리스팅은 파일 이름이 포함되어 있는 주석으로 시작한다. 이 이름들을 사용하여 각 리스팅을 j-gss-sso.zip 아카이브에 있는 상응하는 파일에 매치한다.

이 아카이브에는 Setup.txt 파일도 있다. 코드 실행 전에 이 파일을 읽기 바란다.

사원들이 인증용 유저네임과 패스워드를 사용한다고 가정해보자. 이 기업의 인증 모듈은 당연히 유저네임과 패스워드의 데이터베이스를 갖고 있을 것이다. 들어오는 인증 요청은 유저네임-패스워드 쌍으로 동반될 것이다. 이 인증 모듈은 내부 데이터베이스의 쌍과 비교를 하게 된다.

이제 우리의 웨어하우징 회사는 이 범위안에서 실행되는 여러 애플리케이션들을 갖고있다. 다양한 애플리케이션들이 같은 엔터프라이즈의 다른 모듈을 형성한다. 각각의 애플리케이션은 그 자체로 완벽하다. 이것 나름대로 사용자 기반의 여러 다른 티어(백엔드 데이터베이스, 비지니스 로직, 사용자용 GUI)를 갖고있다는 것을 의미한다. 엔터프라이즈 애플리케이션 통합(EAI)는 이와 같은 독립된 애플리케이션들을 하나의 엔터프라이즈에 통합하는 프로젝트를 일반적으로 가르킨다.

EAI 프로젝트에서 인증 프로세스에 두드러지는 하나의 일반적인 요소가 있다. 특정 애플리케이션의 사용자는 그 엔터프라이즈 내의 또 다른 애플리케이션에 액세스 해야한다. 예를 들어 판매 데이터베이스를 사용하고 있는 판매 담당자가 특정 부품을 검색하기 위해 재고 데이터베이스에 액세스 해야 할 경우도 있다. 이러한 유형의 크로스-애플리케이션 인증을 실현시킬 수 있을까?

두 가지 선택이 있다:

  • 두 애플리케이션에서 유저네임과 패스워드 데이터베이스를 중복시킬 수 있다. 따라서 이러한 두 개의 애플리케이션들이 웨어하우징 회사의 모든 사원들을 위해 인증 요청을 효율적으로 처리할 수 있도록 하는 것이다. 사용자는 두 개의 애플리케이션에서 개별적으로 인증을 처리하게 된다. 다시 말해서 자신의 유저네임과 패스워드로 들어가면서 두 애플리케이션 중 하나에 액세스하고 그 애플리케이션은 모든 인증 단계를 수행하게 된다. 유저네임과 패스워드 데이터베이스를 중복할 뿐만 아니라 인증 프로세스 오버헤드까지 중복하는 것이다. 이 솔루션에서 중복의 양은 명확해진다.

  • 두 번째 선택은 판매 애플리케이션과 재고 애플리케이션 간 싱글사인온을 통한 인증 데이터 공유를 가능하게 하는 것이다. 만일 사용자가 한 애플리케이션 인증을 갖고 있다면 그의 인증 정보는 다른 것에 전달 될 수 있다. 이 두 번째 애플리케이션은 그 모든 인증 단계를 거치지 않고도 인증 정보를 수락한다. 이 솔루션에는 중복이 없다. 두 개의 애플리케이션이 서로 신뢰하여 각 애플리케이션이 서로의 인증 데이터를 수락할 수 있도록 하는 것이 유일한 필요조건이다.

일반적으로 SSO는 개별 인증 모듈로서 구현된다. 사용자 인증이 필요한 모든 애플리케이션들은 SSO 기반의 인증 모듈에 의지하여 사용자를 확인한다. 이 인증 정보에 따라 다양한 애플리케이션들은 각자의 인증 정책을 발효한다.

이 웨어하우징 기업 예제는 사용자 관점에서 SSO가 무엇인지를 설명한 것이다. 다음 질문은 당연히 "SSO를 어떻게 구현하는가?"이다. 여러 방법이 있다. SSO를 포함하여 다양한 보안 서비스를 제공하는 Kerberos에 대해 알아보자.

Kerberos 사용하기
Kerberos는 Internet Engineering Task Force (IETF) 표준으로서 전형적인 키 교환 메커니즘을 정의한다. 애플리케이션들은 Kerberos 서비스를 사용하여 사용자에게 인증을 주고 암호 키를 교환한다. Kerberos는 티켓 발상에 기인한다. 하나의 티켓은 암호 키와 몇몇 정보 조각을 래핑하는 데이터 구조일 뿐이다. 키 발급 센터(KDC)는 Kerberos 티켓을 인증 사용자에게 발급한다. KDC는 두 가지 유형의 티켓을 발행한다:

  • 마스터 티켓(티켓 발행 티켓 (TGT))
  • 서비스 티켓

KDC는 우선 TGT를 클라이언트에 발행한다. 이 클라이언트는 TGT에 여러 서비스 티켓을 요청할 수 있다. TGT와 서비스 티켓의 작동 방법을 설명해주는 다음의 키 교환 시나리오를 보자:

  1. 클라이언트가 TGT의 발행을 요청하며 KDC에 메시지를 보낸다. 이 요청은 평이한 텍스트 형식(암호화 되어있지 않음)이고 클라이언트의 유저네임이 포함되어있다. 패스워드는 포함되지 않는다.

  2. KDC는 TGT를 클라이언트에 발급한다. TGT는 암호화된 형식안에 세션 키를 포함하고 있다. 이 세션 키를 암호화하려면 KDC는 클라이언트의 패스워드에서 나온 키를 사용한다. 클라이언트만이 TGT의 암호를 해독할 수 있고 세션 키를 가져올 수 있다는 것을 의미한다. 따라서 클라이언트 애플리케이션이 TGT를 요청하기 위해 패스워드를 알 필요는 없어도 TGT를 처리하고 사용할 패스워드는 필요하다.

  3. 클라이언트가 TGT의 암호를 해독하고 여기에서 세션 키를 추출한다. 클라이언트는 서비스 티켓을 위한 요청을 작성한다. 서비스 티켓은 양방간 커뮤니케이션용으로만 가능하다. 즉 클라이언트와 그 클라이언트가 통신하기 원하는 엔터티. 어떤 누구도 이 서비스 티켓을 사용할 수 없다. 따라서 서비스 티켓을 요청하는 동안 클라이언트는 서비스 티켓으로 사용 할 서버 이름을 지정한다. 이 서버는 KDC에 이미 존재하고 있어야 한다.

  4. KDC는 서버용 서비스 티켓을 작성한다. 이 티켓은 클라이언트의 인증 데이터와 새로운 암호 키(하위 세션 키)를 포함하고 있다. KDC는 서버의 비밀 키로 서비스 티켓을 암호화한다. 오직 서버만이 서비스 티켓 암호를 해독할 수 있다.

  5. KDC는 메시지를 작성하고 이 안에 서비스 티켓을 래핑한다. KDC는 이 메시지 내부에 하위 세션 키를 복사한다. 하위 세션 키는 메시지에 두 번 포함되었다는 것을 기억하라.

  6. KDC는 2, 3 단계에서 나온 세션 키로 전체 메시지를 암호화한다. 따라서 오직 클라이언트만 메시지를 해독하고 하위 세션 키와 서비스 티켓을 추출할 수 있다. 클라이언트가 서비스 티켓 암호를 해독하지 못하면 오직 서버만이 할 수 있다. 따라서 어떤 누구도 다른 목적으로 서비스 티켓을 사용할 수 없다. 그런 다음 KDC는 메시지를 클라이언트에 보낸다.

  7. 클라이언트는 KDC에서 받은 메시지를 해독하고 메시지 내부에 있는 하위 세션 키와 서비스 티켓을 가져온다. 서비스 티켓은 서버에 보낸다.

  8. 서버는 서비스 티켓을 받고 이를 해독하여 요청 클라이언트의 인증 데이터와 하위 세션 키를 가져온다. 서버는 클라이언트의 요청을 확인하고 새로운 보안 세션이 클라이언트와 서버 사이에 만들어진다. 클라이언트와 서버 모두 같은 하위 세션 키를 마련하고 있고 서로 보안 통신을 위해 이를 사용할 수 있다.

클라이언트는 3 단계에서 8 단계 까지 또 다른 서버 애플리케이션에 반복할 수 있다. Kerberos 서비스가 인증 데이터를 공유하는 데 사용될 수 있고 같은 클라이언트가 다른 애플리케이션에도 인증을 받을 수 있다는 것을 의미한다. 이는 SSO를 효과적으로 만든다 .

Java Generic Security Services API
IETF는 Generic Security Services API (GSS-API)를 고급 보안 API로 정의했다. 이것은 Credential성, 메시지 무결성, 보안 등의 기능을 제공한다. GSS-API는 클라이언트-서버 환경에서 쉽게 작동한다. IETF는 GSS-API를 언어 독립 방식으로 정의했다.

자바 GSS-API는 자바 스팩 형식의 GSS-API이다. Sun은 Java Community Process를 통해 자바 GSS-API를 개발했고 레퍼런스 구현도 제공했다. 이는 JDK 1.4 버전에 번들 될 것이다. (참고자료).

보다 간단히 하기 위해 GSS-API를 GSS로 통칭하겠다.

GSS는 다른 저급 보안 서비스의 상단에 고급 추상 레이어를 제공하는 것이 목적이다. Kerberos는 GSS 추상화에서 사용할 수 있는 기술 중 하나이다. GSS 추상 레이어에서 프로그래머는 보안 메커니즘을 개발 할 수 있다. 보다 낮은 레벨에서 어떤 메커니즘이 작동하고 있는지 걱정하지 않아도 된다. 이 글에서 GSS 하에서 작동하는 저급 보안 메커니즘으로서 Kerberos의 사용법에 초점을 맞추겠다.

GSS는 GSSName, GSSCredential, GSSContext의 개념에서 작동한다. GSSName은 이메일을 확인하기 위해 입력하는 유저네임 같이 당신을 개별적 존재로 구분한다. GSSCredential은 당신의 존재를 증명하기 위해 내보이는 어떤 것이다. 이메일 확인 시 입력하는 패스워드와 같다. Kerberos 서비스 티켓은 GSSCredential의 또 다른 예제이다. GSSContext는 보안 관련 정보(암호 키)를 캡슐화하는 보안 세션 같은 것이다. GSS 기반의 애플리케이션은 GSSContext를 사용하여 안전하게 통신한다.

KDC 설정하기
GSS에서의 Kerberos를 구현할 때 특별한 KDC 구현이 필요하지 않다. 따라서 GSS 기반의 모든 코드는 GSS 호완의 KDC로 작동이 될 것이다. (참고자료).

이 글의 코드를 테스트 하기위해 Microsoft의 KDC 구현을 사용했다. 이것은 Windows 2000 Server 버전 및 이후 버전에서 제공한다. 이 글에 쓰인 모든 코드가 포함되어 있는 j-gss-sso.zip 파일에는 Setup.txt 파일도 포함되어 있어 Microsoft KDC 설정에 필요한 단계를 설명하고 있다.

Microsoft KDC는 모든 Windows 사용자들을 이 서비스의 사용자로 받아들인다. KDC가 Windows 네트워크에 등록된 모든 사용자를 위해 TGT를 발급할 수 있다는 것을 의미한다.

GSS 클라이언트/서버 애플리케이션
이 섹션에서는 GSS 기반 보안 자바 애플리케이션의 실제 구현을 설명하겠다. 재사용 가능한 JavaBeans 컴포넌트 두 개를 개발 할 것이다. 하나의 빈은 요청 클라이언트로서 작동하여 새로운 GSS 세션의 초기화를 요청한다. 다른 빈은 서버로서 작동하여 요청을 리스닝하고 클라이언트에서 오는 요청을 수락한다. 그런 다음 보안 컨텍스트를 만들고 클라이언트와 통신한다.

이 글에서 SSO를 설명해야하기 때문에 제 삼자에 의해 호스팅되는 애플리케이션으로 KDC를 사용하여 우리 애플리케이션을 단순하게 할 것이다. 클라이언트와 서버 모두 이 KDC를 믿고 있으며 여기에서 오는 인증 데이터를 받아들인다. 따라서 이 글에 쓰인 샘플 코드를 사용하기 위해서는 Kerberos 실행이 필요하다. 하지만 GSS 호완의 KDC를 사용할 수 있다. ("KDC 설정하기" 참조)

JAAS 인증 클라이언트
일단 KDC 서비스를 실행하면 계속 진행하여 TGT를 발급하도록 KDC에게 요청할 수 있다. 요청하는 GSS 클라이언트는 KDC에게 TGT를 발급하도록 요청 할 것이다.

작은 문제가 있다. GSS는 사용자에게서 온 유저네임-패스워드 쌍을 가져올 메소드가 없다. 따라서 GSS 애플리케이션은 다른 비 GSS 메커니즘에 의존하여 로그인 정보를 얻어야 한다. 우리는 여기서 Java Authentication and Authorization Service (JAAS)를 사용하여 요청 클라이언트가 유저네임과 패스워드를 제공하고 TGT를 얻도록 하겠다.

Listing 1에서는 GSSClient 라는 클래스를 보게 될 것이다. 이 클래스는 원격 GSS 서버와 보안 세션의 설치를 원하는 GSS 클라이언트의 기능을 나타낸다. GSSClient 구조체는 많은 매개변수를 취한다:

  1. 클라이언트 피어 이름

  2. 클라이언트 피어의 패스워드

  3. 원격 서빙(serving) 피어의 이름

  4. 원격 서빙(serving) 피어의 주소

  5. 서버의 포트

  6. Kerberos 영역 또는 도메인

  7. KDC 주소

  8. 로그인 설정 파일의 이름과 위치 경로

  9. 클라이언트 설정 이름

main() 메소드는 간단한 애플리케이션을 시뮬레이팅하고 있다. Listing 1에 이 메소드를 추가했다. 명령행에서 이를 실행하여 이 클래스의 작동을 설명하기 위해서이다. main() 메소드는 명령행에서 매개변수 값을 읽고 GSSClient() 구조체를 호출하면서 매개변수 값을 이 구조체에 보낸다.

GSSClient() 구조체는 다른 필드에 있는 명령행에서 오는 매개변수를 저장하고 세 개의 시스템 프로퍼티를 지정한다. java.security.krb5.realm 시스템 프로퍼티는 KDC 범위를 지정한다. java.security.krb5.kdc 프로퍼티는 KDC 서버의 주소를 지정한다. java.security.auth.login.config 프로퍼티는 로그인 설정 파일의 이름과 위치 경로를 지정한다. GSS 프레임웍은 이 속성들을 내부에서 사용 할 것이다.

GSSClient 객체를 인스턴스화 한 후에, main() 메소드는 GSSClient.login() 메소드를 호출한다. 이 메소드는 LoginContext 객체를 인스턴스화 하는데 JAAS 프레임웍의 일부이다. LoginContext 구조체는 두 개의 매개변수를 취한다. 두 번째는 BeanCallBackHandler 클래스이다. 이 둘을 자세히 살펴보자.

confName과 설정파일
confName은 JAAS 설정의 이름을 수반한다. 아홉 번째 명령행 매개변수는 클라이언트가 인증용으로 사용할 JAAS를 설정한다. 여덟 번째 매개변수로 지정된 JAAS 설정 파일에는 한 개 이상의 클라이언트 설정이 포함되어 있다. 클라이언트는 이 중 하나를 사용할 수 있다.

JAAS 설정은 인증에 사용될 메커니즘을 정의한다. 설정 파일은 자바 애플리케이션이 인증 로직과 독립된 인증 메커니즘을 선택할 수 있도록 한다.

JAAS 설정은 .conf 파일로 저장된다. Listing 2 의 JAAS 설정 파일 샘플에는 두 가지 설정이 있다. GSSClient 설정은 다음과 같다:

 GSSClient { com.sun.security.auth.module.Krb5LoginModule required; }; 

이 JAAS 설정은 com.sun.security.auth.module.Krb5LoginModule 라는 자바 클래스 이름을 지정한다. 이 클래스는 JAAS의 Kerberos 로그인 모듈이고 GSSClient 설정은 로그인에 이것을 사용한다. 따라서 이 설정을 지정한다는 것은 인증 메커니즘으로서 Kerberos를 사용한다는 것을 의미한다.

Listing 2. GSSClient와 GSSServer 용 JAAS 로그인 설정
 /**** Login.conf ****/ GSSClient{ com.sun.security.auth.module.Krb5LoginModule required; }; GSSServer{ com.sun.security.auth.module.Krb5LoginModule required storeKey=true; }; 

BeanCallBackHandler
Listing 1의 클라이언트를 다시 살펴보자. LoginContext 구조체 메소드 호출과 함께 전달된 두 번째 매개변수는 인증 과정 중에 콜백을 핸들할 객체를 지정한다. Callback은 자바 애플리케이션이 인증 프로세스 중에 JAAS 구현과 인터랙팅 할 수 있도록 한다. 정상적으로 콜백 기능을 사용하여 유저네임과 패스워드를 Kerberos 인증 모듈에 전달 할 수 있다. BeanCallBackHandler 구조체 call에 있는 usernamepassword를 전달했다는 것에 주목하라.

Listing 3은 BeanCallBackHandler 클래스이다. 이 클래스는 CallBackHandler 라는 인터페이스를 구현한다. 이 인터페이스는 JAAS 프레임웍의 일부이고 단 하나의 메소드인 handle()을 포함하고 있다.

Listing 3. JAAS 프레임웍에서 콜백 핸들링하기
 /**** BeanCallbackHandler.java ****/ import java.io.*; import java.security.*; import javax.security.auth.*; import javax.security.auth.callback.*; public class BeanCallbackHandler implements CallbackHandler { // Store username and password. String name = null; String password = null; public BeanCallbackHandler(String name, String password) { this.name = name; this.password = password; }//BeanCallbackHandler public void handle (Callback[] callbacks) throws UnsupportedCallbackException, IOException { for(int i=0; i<callbacks.length; i++) { Callback callBack = callbacks[i]; // Handles username callback. if (callBack instanceof NameCallback) { NameCallback nameCallback = (NameCallback)callBack; nameCallback.setName(name); // Handles password callback. } else if (callBack instanceof PasswordCallback) { PasswordCallback passwordCallback = (PasswordCallback)callBack; passwordCallback.setPassword(password.toCharArray()); } else { throw new UnsupportedCallbackException(callBack, "Call back not supported"); }//else }//for }//handle }//BeanCallbackHandler 

CallBackHandler 객체의 handle() 메소드는 인증이 이루어지는 동안 제어를 자동으로 받는다.

JAAS 프레임웍은 CallBack 객체의 어레이를 CallBackHandler 인스턴스의 handle() 메소드로 전달한다. CallBack 객체의 어레이에는 다른 유형의 콜백 객체들이 포함되어 있다. 하지만 여기에서는 두 가지 유형(NameCallBackPasswordCallBack)을 중심으로 설명하겠다.

NameCallBack 객체는 JAAS 프레임웍에 username을 제공하는데 사용된다. PasswordCallBack은 인증 동안 password를 수반한다. 핸들 메소드(Listing 3)내부에서 NameCallBack 객체의 setName() 메소드를, PasswordCallBacksetPassword() 메소드를 호출했다.

LoginContext 구조체 호출에 따르는 두 개의 매개변수에 대한 위 논의를 요약해보면 사용자 인증에 필요한 LoginContext 객체에 모든 정보를 제공해 줄 수 있다. 따라서 Listing 1의 다음 단계는 LoginContext.login() 메소드를 호출한다. 이는 실제 로그인 프로세스를 수행할 것이다.

인증 프로세스 중에 무엇인가 잘못된다면-예를 들어, password가 정확하지 않다거나-javax.security.auth.login.LoginException이 던져진다. 로그인 메소드 호출의 결과로 어떤 예외도 없다면 인증 프로세스가 성공했다는 것으로 간주할 수 있다.

Kerberos 로그인을 사용하고 있기 때문에 JAAS 프레임웍은 내부적으로 KDC와의 모든 통신을 관리하고 Kerberos TGT를 보내면서 고급의 사용이 편리한 JAAS 인터페이스 뒤에 모든 기술적 상세들을 숨긴다.

성공적인 인증은 LoginContext 객체 안에 Kerberos TGT를 로딩하게 된다. LoginContext 클래스의 getSubject() 메소드를 호출할 수 있다. 이것은 Subject라는 클래스 인스턴스를 리턴한다. Subject 인스턴스는 TGT를 래핑한다.

Subject 클래스를 사용하여 우리가 로그인 했던 액션을 수행할 것이다. 보안 GSS 콘텍스트를 설치하는 것이다. 인증이 성공한 후 필요한 액션을 호출하는 방법을 보자.

Subject 클래스에는 doAs()라는 정적 메소드가 포함되어 있다. 이것은 두 개의 매개변수를 갖는다. Listing 1의 Subject.doAs() 메소드 호출을 보자. 첫 번째 매개변수 값은 Subject 인스턴스로서 성공적인 인증 후에 획득한 것이다. doAs() 메소드는 인증된 Subject를 사용하여 권한 결정을 내린다. 이 Subject가 주어진 작동을 호출할 권한을 받았는지를 판단하는데 사용하는 것이다.

지금까지 Subject 클래스는 TGT를 보내는데에만 사용했다. 따라서 GSSClient에 대한 권한 정책을 지정하지 않았다. 어떤 사용자라도 이 클라이언트를 실행할 수 있다. GSSClient는 주어진 작동을 실행할 때 보안 권한을 요구하지 않는다. 하지만 애플릿 같은 웹 클라이언트는 엄격한 보안 콘텍스트 하에서 실행된다. Setup.txt에는 애플릿 기반의 GSS 클라이언트를 위한 권한 정책을 작성하는 지침이 포함되어 있다.

doAs() 메소드 호출의 두 번째 매개변수 값인 thisGSSClient 객체(Listing 1)를 지정한다. 이 매개변수는 PriviledgedAction 인터페이스를 노출하는 객체를 기대한다. GSSClient는 그 PriviledgedAction 인터페이스를 구현한다. 하나의 클래스에 모든 클라이언트측 코드를 조합했지만 PriviledgedAction 인터페이스를 구현하는 개별 클래스를 가질 수 있다. 원한다면 말이다. 만일 그렇게 한다면 그 객체를 인스턴스화 하고 doAs() 메소드 호출과 함께 두 번째 매개변수 값으로서 이를 전달한다.

PriviledgedAction 인터페이스에는 단 하나의 메소드 run()이 포함되어 있다. 권한 정책이 Subject로 하여금 GSSClient 클래스의 run() 메소드에 액세스 할 수 있도록 한다면 그 메소드는 개별 쓰레드 실행에서 제어를 받는다. run() 메소드가 제어를 받으면 보안 콘텍스트(TGT)가 따라온다. GSS 로직은 자동으로 그 보안 콘텍스트를 사용하고 TGT를 보낸다.

GSS 클라이언트 디자인
Listing 1의 run() 메소드에서 다음 단계를 수행하여 GSS 세션을 만들어야 한다:

  1. GSSManager를 인스턴스화 한다. GSSManager 클래스의 getInstance() 정적 메소드를 호출했던 Listing 1을 주목하라. 이것은 GSSManager 객체를 리턴한다. 이 GSSManager 객체는 적어도 Kerberos 메커니즘을 지원하고 그 밖에 다른 메커니즘도 지원할 것이다. GSSManager 클래스에는 GSS 애플리케이션이 다른 보안 메커니즘을 정의하기 위해 호출할 수 있는 메소드들이 포함되어 있다. 지금은 Kerberos 메커니즘을 중심으로 설명하는 것이니 만큼 getInstance() 메소드를 호출하여 Kerberos 메커니즘을 사용한다. Listing 1의 GSSManager 객체를 인스턴스화 한 후에 kerberos라는 Oid (Object ID) 객체를 인스턴스화 했다. 이것은 Kerberos 메커니즘을 확인한다. ."

  2. GSSName 객체를 만든다. 이것은 GSS 엔터티를 나타낸다. 투웨이 통신을 하는 동안 GSS 엔터티를 통신 피어로 생각할 수 있다. 따라서 두 개의 GSSName 객체를 만드는 것이다. 하나는 요청 클라이언트 피어(clientPeerName)용이고 또 다른 하나는 원격 피어(remotePeerName)용이다.

  3. Credential 세트를 만든다. GSS는 일반적인 보안 메커니즘이다. 따라서 기저의 기술에 의존하여 이러한 Credential을 만든다. Kerberos를 사용하고 있으므로 Kerberos 티켓은 실제 Credential이다. GSSCredential 객체를 얻으려면 GSSManager 클래스의 createCredential() 메소드를 사용한다. createCredential() 메소드는 GSSCredential 인터페이스를 노출하는 객체를 리턴한다.

  4. 보안 GSS 콘텍스트를 만든다. 이것은 두 개의 통신 피어 간 보안 통신을 확립하는데 사용된다. GSSManagercreateContext() 메소드는 GSSContext 인터페이스의 인스턴스를 만들어 리턴한다. GSSContext 객체는 실제 보안 콘텍스트를 래핑한다. Listing 1에서 GSSNameGSSCredential 객체를 전달했다는 것에 주목하라.

보안 콘텍스트를 래핑하는 GSSContext 객체를 가졌지만 콘텍스트 그 자체는 아직 설치되지 않았다. GSSContext 객체를 얻은 후 요청 피어가 이것의 requestConf() 메소드를 호출 할 것이다. 이 메소드는 애플리케이션이 기밀성과 데이터 무결성을 요청하게 하여 원격으로 보내는 모든 데이터가 암호화된 형식이 되도록 한다.

Listing 1에서 requestConf() 메소드를 호출 한 후에 byteToken이라는 바이트의 어레이를 선언했고 이를 제로 사이즈 바이트로 인스턴스화 했다. byteToken 어레이는 GSS 클라이언트가 서버에서 송수신하는 데이터 바이트를 갖고있게된다.

GSSContext 인터페이스의 initSecContext() 메소드를 while 루프에서 반복적으로 호출한다. 이 메소드는 실제 바이트 교환을 수행하여 요청 피어와 서빙 피어 간 보안 콘텍스트를 확립한다.

while(!peerContext.isEstablished()) 블록(Listing 1)은 실제로 GSS 클라이언트와 서버 간 투웨이 통신을 수행한다. 이 블록은 보안 콘텍스트가 설치되었을 때에만 종료된다.

while 루프가 첫번째로 실행될 때 byteToken 어레이에는 데이터가 없다. peerContext.isEstablished() 메소드는 false를 리턴할 것이다. 보안 콘텍스트가 아직 설치되지 않았기 때문이다.

while 루프 내부에서 우리가 수행하는 첫 번째 일은 byteToken 어레이상에서 initSecContext() 메소드를 전달하는 것이다. 이 메소드는 두 가지의 일을 한다. 클라이언트가 서버에 보내는 바이트를 만들어내고 서버에서 오는 바이트를 받아들인다. 바이트 교환은 GSS 콘텍스트가 확립될 때 까지 계속된다.

처음에 initSecContext() 메소드를 호출 할 때 전달할 바이트가 없다. 따라서 비어있는 byteToken 어레이에서 첫 번째 호출용 메소드에 전달한다. initSecContext() 메소드는 몇몇 바이트를 리턴하는데 이것은 같은 byteToken 어레이에 저장되었다. 그 다음 byteToken을 아웃풋 스트림에 작성하고 스트림을 플러시 하여 byteToken 어레이가 원격 서버로 보내지도록 한다.

원격 서버에 보내진 바이트에 대한 응답으로 원격 서버는 어떤 것을 보낼것으로 기대할 수 있다. 따라서 우리는 인풋 스트림에서 바이트를 읽고 같은 byteToken 어레이에 이 바이트를 저장하고 byteToken 어레이를 다시 initSecContext() 메소드에 보낸다. 이 메소드는 다시 바이트 어레이를 리턴한다. 이러한 바이트 교환은 while 루프 내부에서 보안 콘텍스트가 확립되고 peerContext.isEstablished() 메소드가 true를 리턴할 때 까지 지속된다.

보안 콘텍스트의 설치가 의미하는 것은 적절한 Kerberos 키들이 클라이언트와 서버 양측에서 사용가능한 상태가 된다는 것을 의미한다. 양측에서 이 키들을 사용하여 보안 통신에 쓴다. 보안 콘텍스트가 성립되면 GSSContext 객체를 리턴한다.

GSS 콘텍스트 사용하기
GSSClient 클래스의 main() 메소드에서 GSSContext 객체를 사용하는 방법을 살펴보겠다. login() 메소드를 호출한 후 login() 메소드가 리턴한 GSSContext 이 null인지를 확인한다. null이 아니라면 보안 콘텍스트가 원격 서버와의 보안 통신에 사용될 수 있다.

main() 메소드는 원격 서버가 기밀성과 메시지 무결성이라는 클라이언트 요청에 합당한지를 점검한다. 서버가 요청에 합당하면 GSSContext 클래스의 getConfState() 메소드는 true를 리턴한다.

main() 메소드는 원격 메소드로 보내는 데이터가 무엇이든 보낼 수 있다. sendMessage() 헬퍼 메소드를 작성하여 데이터를 서버에 보냈다. GSSContext 클래스의 wrap() 메소드는 데이터의 바이트 어레이를 받아들이고 바이트 어레이를 리턴한다. sendMessage() 메소드는 플레인 텍스트 데이터를 wrap() 메소드에 보내고 응답으로 암호화된 바이트 어레이를 받는다. 아웃풋 스트림 상에서 바이트 어레이를 작성하여 암호화된 바이트 어레이를 원격 서버로 보낼 수 있다.

sendMessage() 메소드는 서버에서 오는 인커밍 데이터를 리스닝한다. 서버에서 데이터를 받으면 인커밍 데이터를 GSSContext 클래스의 unwrap() 메소드에 보낼 수 있다. 이것은 평이한 텍스트 형식의 데이터를 리턴한다.

GSS 서버 애플리케이션
지금까지 GSS 클라이언트 애플리케이션이 작동 방법을 보았다. 이제 이것과 인터랙팅 할 서버를 구현해본다.

Listing 4GSSServer 클래스용 코드이다. GSSServerstartServer() 메소드는 GSSClient 클래스의 login() 메소드를 설명하면서 거론되었던 것과 같은 기능을 수행한다.

GSSServer 클래스의 run() 메소드 내부에서 GSSManagerGSSName 객체를 만들었다. 지금까지 클라이언트측 코드와 서버측 코드 사이에 차이점이 거의 없었다. 하지만 좀더 자세히 보면 서버는 단 하나의 GSSName 객체를 만든다는 것을 알 수 있다. 이것은 서버를 나타낸다. GSSName을 만든 후에 이 서버는 createCredential() 메소드를 호출하여 GSSCredential 객체에 credentials를 로딩한다.

다음 단계는 GSS 콘텍스트를 만들기 위해 createContext() 객체를 호출하는 것이다. createContext() 메소드 호출은 GSS 클라이언트 애플리케이션에서 이루어진 createContext() 호출과는 다르다. 지금의 createContext() 메소드는 단 하나의 매개변수의 서버의 credential을 갖는다. 이 서버측 콘텍스트가 양측 사이에 있지 않다는 것을 의미한다.

다음에는 통신용 인풋과 아웃풋 스트림을 만든다. 그런 다음 보안 콘텍스트가 설치될 때까지 계속 루핑 할 while 루프로 들어간다. 이 루프 안에서 요청 클라이언트를 기다려 연결 구축 요청을 보낸다. while 루프 내부의 인풋 스트림 상에서 데이터를 받으면 데이터를 바이트 어레이로 읽고 바이트 어레이 토큰을 GSSContext 클래스의 acceptSecContext() 메소드에 제공한다. acceptSecContext() 메소드는 데이터 바이트를 리턴한다. 이것은 아웃풋 스트림으로 되돌아간다.

GSSContextinitSecContext()acceptSecContext() 메소드는 결합하여 작동한다. GSS 클라이언트 애플리케이션을 설명하면서 initSecContext() 메소드의 사용법을 설명했다. initSecContext() 메소드는 GSS 클라이언트가 GSS 서버 애플리케이션으로 보내는 초기 바이트를 만들어낸다. acceptSecContext() 메소드는 이러한 인커밍 바이트를 받아들이고 이것의 바이트 어레이를 만들어낸다. 이것을 클라이언트로 보낸다. 이러한 바이트 교환은 보안 GSS 콘텍스트가 구축될 때 까지 지속된다.

GSS는 모든 통신을 바이트 어레이 토큰으로서 핸들한다. 클라이언트에서 서버로 바이트 어레이를 나르기위해 어떤 유형의 전송 서비스를 사용할 수 있다. GSS는 데이터 전송에 사용되는 것에는 관심이 없다.

보안 세션의 구축 과정은 요청 클라이언트의 인증으로 끝을 맺는다. GSSContext 클래스의 getSrcName() 메소드를 호출하여 인증된 클라이언트의 GSSName을 보낼 수 있다. 반면 GSSContext 클래스의 getTargName() 메소드는 원격 클라이언트의 요청을 수락한 서버의 GSSName을 리턴한다.

while (!context.isEstablished()) 루프가 리턴한 후에(Listing 4) run() 메소드는 클라이언트로 부터 통신을 기다린다. 인커밍 데이터를 계속해서 리스닝하고 인커밍 바이트 스트링을 받게되면 GSSContext 클래스의 unwrap() 메소드를 통해 스트링을 보낸다. unwrap() 메소드는 클라이언트에서 온 평이한 텍스트 형식의 메시지를 리턴한다.

브라우저 속의 GSS
Listing 5는 애플릿이 Listing 1의 GSS 클라이언트를 사용하여 Listing 4의 GSS 서버와의 보안 통신을 구축하는 방법이 나와있다.

이 애플릿은 HTML 페이지에서 실행될 것이다. (Listing 6):

Listing 6. GSS 애플릿을 사용하는 HTML 페이지
 <!-- E-Commerce Login.html --> <HTML> <HEAD> <TITLE>E-Commerce Login... </TITLE> </HEAD> <BODY> <p align="center"> <table bgcolor="Gray"> <tr> <td align="center"> <b>E-Commerce Site Login Page </b> </td> </tr> <tr> <td> <Applet CODE="GSSClientApplet.class" archive="GSSClientApplet.jar" name="GSSClientApplet" width="500" height="280"> </Applet> </td> </tr> </table> </p> </BODY> </HTML> 

이 애플릿이 e-커머스 웹 사이트의 메인 페이지에서 실행된다고 생각해보자. 그림 1은 실행 모습을 나타낸 것이다. 두 개의 텍스트 엔트리 필드, 버튼 세 개, 하나의 텍스트 영역이 있다. 세 개의 버튼 각각은 e-커머스 웹 사이트의 파트너의 서버측 구현에 상응한다.

그림 1. GSS 애플릿 사용
An HTML page showing GSS applet usage

e-커머스 웹 사이트 고객은 어떤 사이트 파트너라도 인증받을 수 있다. 애플릿의 텍스트 필드에 유저네임과 패스워드를 입력하고 인증을 원하는 파트너 사이트에 해당하는 Login 버튼을 누른다. 이 버튼의 이벤트 핸들러는 GSSClient 구조체에 필요한 매개변수를 제공한다. 나머지 작업은 GSS 클라이언트가 할 일이고 이것은 이미 설명했다.

참고자료

Posted by tornado
|

출처 : http://network.hanbitbook.co.kr/view_news.htm?serial=709

 

†역자 주: 본 기사는 원문의 절반도 안되는 내용이나 우선은 역자 자신이 딱 이거다 싶은 내용과 직접 사용해본 내용만을 번역해 놓은 것이다. 스트러츠를 계속 사용하면서 이해가 되는 부분은 차츰 하나씩 번역해나갈 생각이다.

스타일시트나 다른 HTML 자원을 참조할 시에는 <html:link rewrite>를 사용하라

html:link rewrite 태그는 컨텍스트 상대적인 URL을 사용할 수 있다. 또한, 필요하다면 URL을 인코딩하는 것도 가능하기 때문에 컨테이너가 보안을 관리하는 자원의 경우 이용할 수 있다.
예제)<LINK REL=’stylesheet’ HREF=”’<html:rewrite page=’/path/to/CSS”/>’ TYPE=’text/css’>
정적인 페이지 접근을 위한 액션(Action)은 어찌 할 것인가?

모델 2 환경에서 페이지는 깔끔하지만, 처리한 내용만 보여주기 때문에 단순하다고 할 수 있다. 모델 2 환경에서는 액션을 먼저 거친 후, 페이지로 이동을 해야 한다(바로 이 부분이 모델 1에서 웹 프로그래밍 하던 사람들이 고생하는 것으로 역자 또한 그러하다). 그리고 액션은 페이지가 request, session context에 담아서 보낸 정보를 조합하여 나타낼 뷰를 결정해야 한다. 그러나 액션은 뷰의 주소를 모른다(ActionMapping이 알고 있음). 모든 페이지는 하나의 액션 핸들러를 가지고 있어야 한다. 하나의 액션은 다수의 다른 뷰 페이지를 핸들링 할 수 있으며 하나의 페이지는 여러 다른 액션에 의해서 핸들링 될 수 있다. 그러나 하나의 페이지는 적어도 하나의 액션 핸들러를 반드시 가지고 있어야 한다. 여기서 한가지 질문! 그렇다면 static한 페이지에 접근하는 경우, 모두 액션을 가지고 있어야 하는가? 100개의 페이지가 있다면 그것들 모두 액션을 가지고 있어야 하는가? 이런 경우를 대비해서 스트럿츠에는 ForwardAction이라는 액션을 제공한다. 액션 맵핑에 아래와 같이 입력해주면 된다.
예제)<action    name="boardForm"            parameter="/boardcreate.jsp"            path="/boardcreateform"            type="org.apache.struts.actions.ForwardAction"            validate="false"/>
다른 건 다 무시하고, parameter와 type만 살펴보자. Boardcreate.jsp은 새로운 글을 입력하는 페이지로, 실제로 액션이 있어도 아무런 역할을 하지 못한다. 이 경우 org.apache.struts.actions.ForwardAction를 액션 타입으로 지정하고, parameter란에 이동하고자 하는 페이지 주소를 적는다. 이 경우는 /boardcreateform.do를 (*.do으로 서블릿 매핑이 되었다고 가정) 주소창에 치면 우선은 ForwardAction을 통과해서 boardcreate.jsp로 포워드 된다. 이렇게 함으로써 모델 2에서 Action을 거쳐서 페이지로 이동해야 한다는 조건을 만족하게 된다.

액션이 하는 일이 매우 작다면 하나의 액션을 가지고 여러 일을 처리하는 방법을 간구하라

간단히 게시판을 만드는 중이었다. 게시물 삭제, 수정, 수정하는 폼, 등록, 답글 달기, 주석 달기 등 이렇게 하나하나 액션을 만들다 보니 관리해야 할 액션 클래스만 몇 십 개가 되어버렸다. 이렇게 하나의 단위로 묶을 만한 액션들 다수가 존재한다면, DispatchAction의 사용을 고려해봐야 한다. 곧바로 예제를 보겠다.
예제)<action            input="/board/bullcreateform.jsp"            name="bullForm"            parameter="method"            path="/bull"            scope="request"            type="webfx.board.struts.action.BullAction"            validate="false"/>
다른 액션 맵칭하는 방법과 거의 다르지 않다. 여기서 특이하게 볼 내용은 parameter이다. 그리고 BullAction의 코드는 아래와 같다.
…public BullAction extends org.apache.struts.actions.DispatchAction {	public ActionForward create(		ActionMapping mapping,		ActionForm form,		HttpServletRequest request,		HttpServletResponse response)		throws Exception {	…};	public ActionForward remove(		ActionMapping mapping,		ActionForm form,		HttpServletRequest request,		HttpServletResponse response)		throws Exception {	….	}	등등…}
이 소스에는 execute라는 메소드가 없다. 단지 Method Signature가 이름을 제외하고 execute와 다른 create, remove 함수가 있다. 그리고 이 액션은 org.apache.struts.actions.DispatchAction을 상속했다. 우리가 parameter로 지정한 값은 request로 넘어올 파라미터 이름이다. 그리고 이 파라미터에 우리가 실행시키고자 하는 메소드 명을 지정하면 그 메소드가 실행된다. 예를 들어 /bull.do?method=create라고 실행시켰다면 BullAction에서 create 함수를 실행시키게 된다. 이처럼 관리하기 힘들 정도로 액션이 늘어날 경우에는 DispatchAction을 사용하는 것도 고려해봐야 한다.

데이터와 Value Objects을 교환하기 위해서 populate 유틸리티를 사용하라

Jakarta.apache.org 사이트에 있는 Commons에 속한 BeanUtils를 말하는 겁니다. BeanUtils.populate()와 describe() 메소드는 beans와 임의 타입 간의 데이터 전송 사용을 위해서 사용될 수 있다. 요구되는 것은 대응되는 public 형의 프로퍼티 이름들과 네이티브 타입이다. Wrapping Object 즉, Integer 같은 것들은 지원이 되지만 그 외의 타입은 지원되지 않는다(예, Date).

describe 메소드는 소스 빈을 스트링 값을 가지고 있는 맵으로 만든다. populate 메소드는 그러한 맵을 읽어서 타겟이 되는 빈에 프로퍼티를 적용하는 역할을 한다. 만약 타겟 타입이 스티링이 아니라면 필요한 타입으로 변경이 된다. 만약 변환이 실패하면 예외가 발생한다. 다음이 두 빈(Bean) 간의 데이터 이동하는 소스이다.
BeanUtils.populate(target, BeanUtils.describe(source));
몇몇 경우에 필요한 만큼만 조절한 ActionBean에서 또한 조절된 값을 가지고 있는 Map을 만들기 위해서 커스터마이징한 describe 메소드가 필요할 때가 있다. 다른 값들 (즉, 지원되지 않는 타입)은 직접 세팅해 줄 수 있다.
BeanUtils.populate(target, source.describe());tartget.setDate(source.getDateBinary());
BeanUtils을 직접 이용하는 것보다 유틸클래스를 하나 만들어, 그곳에서 BeanUtils 클래스를 이용하도록 만드는 게 더 좋을 것이다. 예를 들면 아래와 같다.
valueUtils.populate(ValueObject target, ActionForm source);valueUtils.populate(ActionForm target, ValuteObject source);
공통적으로 사용되는 속성들에 대한 객체를 만들어 사용하라

웹 애플리케이션 안에서 애플리케이션에 전체적으로 걸쳐서 사용되어야 하는 속성이 있다. 사용자 정보, 북마크, 메뉴 세팅, 다른 프로퍼티 등등이 이것에 속할 수 있다. 이때에는 이런 것들을 각각 따로 관리하는 것보다 이런 것들을 한꺼번에 래핑하는 객체를 만들어서 사용하자.

애플리케이션을 위한 Base Action을 사용하라

애플리케이션 액션들은 몇몇 기본적인 작업을 처리한다. 이러한 태스크를 일관적으로 구현하기 위해, 애플리케이션의 다른 액션들이 서브클래싱하여 사용할 수 있는 Base 클래스를 이용하자! 만약 개개 액션의 execute 메소드 안에서 중요하게 처리되어야 할 일이 있다면 서브 클래스들이 execute() 메소드를 통해서 실행시킬 추상 메소드를 사용하는 것이 좋은 접근 방법이다. Base 클래스는 기본적으로 자신이 해야 할 일을 하고 그것이 모두 잘 처리 되었을 때, 새로운 메소드를 실행시키고 그 결과를 리턴한다. 이 부분 때문에 새로운 메소드의 서명을 변경할 수 있다.
Public ActionForward execute(ActionMapping mapping,				ActionForm form,				HttpServletRequest request,				HttpServletResponse response)			throws IOException, ServletException {		// Application specific bevaviour		// if evevrything is kosher call subclass		return (executeAction(mapping, form, request, response, myParameter))}
executeAction은 Base 클래스의 추상 클래스이다.

이중으로 submit이 일어나는 것을 막기 위해서 Action Token을 사용하라

스트럿츠의 액션에 one-use tokens을 만들어 담는 메소드들이 있다. 폼이 작성될 때 세션에 토큰이 위치하게 되고, 이 토큰은 HTML 폼에 hidden 프로퍼티로 포함된다. 그 폼이 리턴 될 때 토큰은 validation이 된다. 만약 validation에 실패하면 벌써 그 폼은 submit이 된 것이므로 사용자는 그에 대한 알림 메시지를 받을 수 있다.
  • saveToken(request)
  • on the return trip
    • isTokenValid(request)
    • resetToken(request)
스트럿츠를 이용한 웹 애플리케이션 개발 사이클
  1. 화면 설계서를 개발한다. (화면 요구사항 설계)
  2. 화면 요구사항을 만족하기 위한 Helper 클래스를 개발한다. (value object)
  3. 워크플로우 요구사항을 개발한다. (화면 이동 등의 스토리보드 작성)
  4. 워크플로우 요구사항을 만족시키기 위한 ActionMapping을 개발한다.
  5. ActionMapping을 사용하여 워크 플로우를 검증하기 위한 Stub JSP 페이지를 작성한다.
  6. ActionMapping에 의해 호출 되는 액션을 개발한다.
  7. Stub Jsp를 작성한다.
Posted by tornado
|

출처 : http://www-903.ibm.com/developerworks/kr/java/library/os-ecref.html

Eclipse의 자동화 리팩토링 기능

Level: Intermediate

David Gallardo
소프트웨어 컨설턴트
2003년 9월 9일

Eclipse는 강력한 자동 리팩토링을 제공한다. 이것을 통해 자바 엘리먼트를 재명명하고, 클래스와 패키지를 옮기며 실제 클래스에서 인터페이스를 만들고 중첩 클래스를 상위레벨 클래스로 변환하고 구 메소드의 코드 섹션에서 새로운 메소드를 가져올 수 있다. Eclipse의 리팩토링 툴에 익숙해진다면 생산성을 향상할 수 있는 좋은 방법이다.

리팩토링
리팩토링(Refactoring)은 프로그램의 기능은 변화시키지 않고 프로그램의 구조를 변화시키고 있다. 리팩토링은 강력한 기술이지만 조심스럽게 다뤄져야 한다. 가장 위험한 것은 에러들이 갑자기 발생할 수 있다. 특히 리팩토링을 수동으로 수행할 때 그렇다. 바로 이것이 리팩토링의 주요 비판 요소이다.

코드를 리팩토링하는 여러가지 이유가 있다. 첫 번째는 전설 때문이다. 훌륭한 제품을 위한 진부한 오래된 코드 기반은 전승되거나 그렇지 않다면 신비스럽게 나타난다. 그리고 원래 개발팀은 사라졌다. 새로운 기능들을 갖춘 새로운 버전이 만들어져야 하지만 그 코드는 더 이상 이해할 수 없는 것이 되고 만다. 새로운 개발팀은 밤낮으로 일하며 이것을 판독하고 매핑하며 많은 계획과 디자인 후에 코드를 산산조각으로 만든다. 마침내 그들은 새로운 버전에 따라 이 모두를 다시 제자리에 갖다 놓는다. 이것은 영웅적 스케일의 리팩토링이고 일부만이 살아남아 이 이야기를 할 수 있다.

보다 현실적인 시나리오는 디자인 변화를 필요로하는 프로젝트에 새로운 요구사항이 만들어졌다는 것이다. 이러한 요구사항이 원래 계획의 부주의한 간과 때문인지 또는 반복적인 접근방식이 개발 프로세스 전반에 걸쳐 요구사항들을 정교하게 만들어내는데 사용되었기 때문이지는 불분명하다. 이것은 훨씬 작은 스케일의 리팩토링이다. 그리고 여기에는 일반적으로 클래스 계측을 변경하는것이 포함되어있는데 아마도 인터페이스 또는 추상 클래스 도입, 클래스 나누기, 클래스 재조정등을 통해 수행된다.

마지막으로, 자동화된 리팩토링 툴이 사용가능하다면, 첫 번째 단계에서 코드 생성을 위한 지름길로서의 리팩토링을 하는 이유가 되겠다. 리팩토링을 이렇게 사용한다면 툴에 익숙하다면 효과적으로 시간을 절약할 수 있다.

코드가 깨지는 위험을 감소시킬 수 있는 두 가지 중요한 방법이 있다. 하나는 코드용 단위 테스트 세트를 갖추는 것이다. 이 코드는 리팩토링 전후에 걸쳐 테스트를 통과해야한다. 두 번째 방법은 Eclipse의 리팩토링 기능 같은 자동화된 툴을 사용하여 리팩토링을 수행하는 것이다.

전체 테스팅과 자동화된 리팩토링의 결합은 특별히 강력하며 언제나 유용한 툴로 변모된다. 기능을 추가하거나 관리능력을 향상시키기 위해 기능을 변화시키지 않고 코드의 구조를 변경하는 능력은 코드를 설계하고 개발하는 방식에 큰 영향을 미칠 수 있다.

Eclipse의 리팩토링 유형
Eclipse의 리팩토링 툴은 크게 세 가지 범주로 나눌 수 있다.(이것은 Refactoring 메뉴에 나타나는 순서이다):

  1. 코드의 이름과 물리적 조직을 변경하고, 재명명 필드, 변수, 클래스, 인터페이스를 추가하고, 패키지와 클래스 옮기기
  2. 클래스 레벨에서 코드의 논리적 조직을 변경하고, 익명의 클래스들을 중첩 클래스로 옮기며, 중첩 클래스들을 상위 레벨 클래스로 전환하고 구체 클래스에서 인터페이스를 만들고 클래스에서 하위 클래스 또는 상위 클래스로 메소드 또는 필드 옮기기
  3. 클래스 내에 있는 코드 변경하고, 로컬 변수를 클래스 필드로 변환하고 메소드내의 선택된 코드를 개별 메소드로 전환하고 getter와 setter 메소드 생성하기

많은 리팩토링이 이러한 세 가지 범주에 딱 맞아떨어지는 것은 아니다. 특히 세 번째 카테고리에 있는 Change Method Signature가 그렇다. 이 예외를 제외하고 나머지 섹션들은 이 순서대로 Eclipse의 리팩토링 툴을 설명할 것이다.

물리적 재구성(reorganization)과 재명명(renaming)
특별한 툴 없이 파일 시스템에 있는 파일들을 재명명하거나 옮길 수 있다. 하지만 자바 소스 파일로 이일을 수행하려면 import 또는 package 문을 업데이트 하기 위해 많은 파일들을 편집해야 한다. 텍스트 에디터의 검색을 사용하여 클래스, 메소드, 변수를 쉽게 재명명 할 수 있고 기능을 대체시킬 수 있다. 하지만 다양한 클래스들이 같은 이름을 가진 메소드 또는 변수를 갖기 때문에 조심해야 한다. 모든 인스턴스가 정확히 구분되고 변경되도록 확인해야 한다.

Eclipse의 Rename과 Move는 사용자가 개입하지 않고 이러한 변경들을 지능적으로 수행할 수 있다. Eclipse는 코드 문법을 이해하고 특정 메소드, 변수, 클래스 이름에 대한 레퍼런스를 구분할 수 있기 때문이다.

코드가 원래 계획과는 다르게 작동하도록 변경되었기 때문에 부적절한 이름이나 오도된 이름을 가진 코드를 찾는 것은 매우 기본적인 일이다. 예를 들어 파일에서 특정 단어를 찾는 프로그램은 InputStream을 얻기 위해 URL 클래스를 사용함으로서 웹 페이지와 함께 작동하도록 확장될 수 있다. 이 인풋 스트림이 원래 파일로 호출되었다면 좀더 일반적인 성질을 반영하도록 변경되어야 한다. 개발자들은 이 같은 변경 작업을 실패하곤 한다.

자바 엘리먼트를 재명명하려면 Package Explorer 뷰에 있는 rename을 클릭하거나 자바 소스 파일에서 이를 선택한 다음 Refactor > Rename을 선택한다. 다이얼로그 박스에서 새로운 이름을 선택하고 Eclipse가 이 이름에 대한 레퍼런스도 변경하는지의 여부를 선택한다. 디스플레이되는 정확한 필드는 선택한 엘리먼트 유형에 의존한다. 예를 들어 getter와 setter 메소드를 가진 필드를 선택하면 이 새로운 필드를 반역하기 위해 이 메소드들의 이름을 업데이트 할 수 있다. (그림 1).

그림 1. 로컬 변수의 재명명
Renaming a local variable

모든 Eclipse 리팩토링과 마찬가지로 리팩토링을 수행하는데 필요한 모든 것을 설정한 후에 Preview를 눌러 변경사항을 볼 수 있다. 비교 다이얼로그로 되어있는데 거부 또는 모든 개별 파일의 변경사항 마다 거부 또는 승인을 할 수 있다. Eclipse가 변경작업을 정확하게 수행했다는 확신이 있다면 OK만 눌러도 된다. 분명히 리팩토링이 무엇을 수행하는지 확실하지 않다면 프리뷰를 우선 눌러야 한다. 하지만 이것은 Rename과 Move 같은 간단한 리팩토링에는 해당하지 않는다.

Move는 Rename과 비슷하다: 자바 엘리먼트(대게, 클래스)를 선택하고 이것의 새로운 위치를 지정하고 레퍼런스의 업데이트 여부를 지정한다. 그런다음 Preview를 선택해 변경 사항을 검토하거나 OK를 눌러 리팩토링을 즉시 수행한다. (그림 2).

그림 2. 하나의 패키지에서 다른 패키지로 클래스 옮기기
Moving a class

몇몇 플랫폼 상에서는 (주로 Windows), 하나의 패키지 또는 폴더에서 또 다른 패키지나 폴더로 클래스를 이동할 수 있다. Package Explorer 뷰에서 이를 드래그 & 드랍 방식을 이용한다. 모든 레퍼런스들은 자동으로 업데이트 될 것이다.

클래스 관계 재정의
Eclipse의 리팩토링 세트를 이용하면 클래스 관계를 자동으로 변경할 수 있다. 이러한 리팩토링은 Eclipse가 제공하는 다른 유형의 리팩토링 만큼 일반적인 것은 아니지만 매우 복잡한 태스크를 수행한다는 점에서 가치가 있다.

익명의 클래스와 중첩 클래스 사용하기
두 개의 리팩토링(Convert Anonymous Class to Nested & Convert Nested Type to Top Level)은 현재 범위에서 enclosing 범위까지 클래스를 옮긴다는 점에서 비슷하다.

익명의 클래스는 클래스 이름을 주지않고 필요한 곳에 추상 클래스나 인터페이스를 구현하는 클래스를 인스턴스화할 수 있는 지름길이다. 사용자 인터페이스에 리스너를 만들 때 일반적으로 사용된다. Bag이 두 개의 메소드(get() & set())를 선언하는 곳에서 정의된 인터페이스라고 가정해보자. (Listing 1).

Listing 1. Bag 클래스
 public class BagExample { void processMessage(String msg) { Bag bag = new Bag() { Object o; public Object get() { return o; } public void set(Object o) { this.o = o; } }; bag.set(msg); MessagePipe pipe = new MessagePipe(); pipe.send(bag); } } 

익명의 클래스가 너무 커져서 코드 판독이 불가능하게 되면 익명의 클래스를 정식 클래스로 만드는 것을 고려해봐야 한다. 캡슐을 유지하려면 상위 레벨 클래스 보다는 중첩 클래스로 만들어야 한다. 익명 클래스 안에서 클릭하여 Refactor > Convert Anonymous Class to Nested를 선택한다. 클래스 이름을 입력하고 Preview 또는 OK를 선택한다. (Listing 2).

Listing 2. 리팩토링이 수행된 Bag 클래스
 public class BagExample { private final class BagImpl implements Bag { Object o; public Object get() { return o; } public void set(Object o) { this.o = o; } } void processMessage(String msg) { Bag bag = new BagImpl(); bag.set(msg); MessagePipe pipe = new MessagePipe(); pipe.send(bag); } }

Convert Nested Type to Top Level은 다른 클래스에도 사용 가능한 중첩 클래스를 만들 때 유용하다. 소스 파일에서 클래스 이름을 강조하고(또는 아웃라인 뷰의 클래스 이름을 클릭하여) Refactor > Convert Nested Type to Top Level을 선택한다.

이 리팩토링은 enclosing 인스턴스용 이름을 요청할 것이다. OK를 누른 후 enclosing BagExample 클래스용 코드는 변경된다. (Listing 3).

Listing 3. 리팩토링이 수행된 Bag 클래스
 public class BagExample { void processMessage(String msg) { Bag bag = new BagImpl(this); bag.set(msg); MessagePipe pipe = new MessagePipe(); pipe.send(bag); } } 

클래스가 중첩되면 외부 클래스의 멤버에 대한 액세스를 가진다는 것에 주목하라. 이 기능을 유지하기 위해 리팩토링은 enclosing 클래스 BagExample의 인스턴스를 이전에 중첩된 클래스에 추가 할 것이다. 이것은 이전에 이름을 제공하도록 요청받았던 인스턴스 변수이다. 이것은 또한 인스턴스 변수를 설정하는 컨스트럭터를 만든다. 다음은 리팩토링이 만든 이 새로운 BagImpl 클래스이다. (Listing 4).

Listing 4. BagImpl 클래스
 final class BagImpl implements Bag { private final BagExample example; /** * @paramBagExample */ BagImpl(BagExample example) { this.example = example; // TODO Auto-generated constructor stub } Object o; public Object get() { return o; } public void set(Object o) { this.o = o; } } 

BagExample 클래스에 대한 액세스를 보일필요가 없다면 인스턴스 변수와 컨스트럭터를 제거하고 BagExample 클래스를 no-arg 컨스트럭터로 변경한다.

클래스 계층 내에서 멤버 옮기기
두 개의 다른 리팩토링(Push Down & Pull Up)은 클래스 메소드 또는 필드를 하나의 클래스에서 이것의 하위클래스 또는 상위 클래스로 각각 이동시킨다. 추상 클래스 Vehicle이 있다고 가정해보자. (Listing 5).

Listing 5. Vehicle 추상 클래스
 public abstract class Vehicle { protected int passengers; protected String motor; public int getPassengers() { return passengers; } public void setPassengers(int i) { passengers = i; } public String getMotor() { return motor; } public void setMotor(String string) { motor = string; } } 

Automobile이라는 Vehicle의 하위클래스도 있다. (Listing 6).

Listing 6. Automobile 클래스
 public class Automobile extends Vehicle { private String make; private String model; public String getMake() { return make; } public String getModel() { return model; } public void setMake(String string) { make = string; } public void setModel(String string) { model = string; } } 

Vehicle의 애트리뷰트 중 하나가 motor라는 것에 주목하자. 모터로 구동되는 탈것 만을 다룰 것이라면 괜찮지만 보트 같은 것도 허용하려면 motor 애트리뷰트를 Vehicle 클래스에서 Automobile 클래스로 밀어내려야 한다. 이를 위해서는 Outline 뷰에서 motor를 선택하고 Refactor > Push Down을 선택한다.

Eclipse는 여러분이 필드를 언제나 옮길 수는 없다는 것을 인식할 정도로 똑똑하기 때문에 Add Required 라는 버튼을 제공한다. 하지만 Eclipse 2.1에서는 언제나 정확하게 작동하는 것은 아니다. 이 필드에 근거하는 모든 메소드가 "push down" 되었는지를 확인해야 한다. 이 경우 motor 필드를 수반하는 getter와 setter 메소드가 있다. (그림 3).

그림 3. Add Required
Adding required members

OK를 누른 후, motor 필드와 getMotor() & setMotor() 메소드는 Automobile 클래스로 이동한다. Listing 7은 Automobile 클래스의 리팩토링 후의 모습이다.

Listing 7. 리팩토링 후의 Automobile 클래스
 public class Automobile extends Vehicle { private String make; private String model; protected String motor; public String getMake() { return make; } public String getModel() { return model; } public void setMake(String string) { make = string; } public void setModel(String string) { model = string; } public String getMotor() { return motor; } public void setMotor(String string) { motor = string; } } 

Pull Up 리팩토링은 Push down과 거의 동일하다. 클래스 멤버를 클래스에서 상위클래스로 옮긴다는 것을 제외하면...

Automobile 클래스에 motor가 있다는 것은 Bus 같이 Vehicle의 다른 하위클래스를 만든다면 모터를 Bus 클래스에 추가해야한다는 것을 의미한다. 이와 같은 관계를 나타내는 한 가지 방법은 Motorized라는 인스턴스를 만드는 것이다. AutomobileBus는 구현하지만 RowBoat는 그렇지 않다.

Motorized 인터페이스를 만드는 가장 쉬운 방법은 Automobile에 Extract 인터페이스 리팩토링을 사용하는 것이다. Outline 뷰에서 Automobile 클래스를 선택하고 메뉴에서 Refactor > Extract Interface를 선택한다. (그림 4).

그림 4. Motorized 인터페이스
Motorized interface

OK를 선택한 후, 인터페이스가 만들어진다. (Listing 8).

Listing 8. Motorized 인터페이스
 public interface Motorized { public abstract String getMotor(); public abstract void setMotor(String string); } 

Automobile용 클래스 선언은 다음과 같다:

 public class Automobile extends Vehicle implements Motorized 

supertype 사용하기
이 범주에 포함된 마지막 리팩토링은 Use Supertype Where Possible이다. 자동차의 재고를 관리하는 애플리케이션을 생각해보자. 이것은 Automobile 유형의 객체를 사용한다. 모든 유형의 탈것을 핸들하려면 이 리팩토링을 사용하여 Automobile에 대한 레퍼런스를 변경할 수 있다. (그림 5). instanceof 오퍼레이터를 사용하여 코드에서 모든 유형 체킹을 수행하면 특정 유형을 사용하는 것이 적절한지 또는 supertype을 선택하는 것이 적절한지 결정해야하고 첫 번째 옵션을 체크하게 될 것이다. 대게 'instanceof' 익스프레션에서 선택된 supertype을 사용한다.

그림 5. Automobile을 supertype인 Vehicle로 바꾸기
Supertype

supertype의 필요성은 자바 언어를 사용할 때 종종 발생한다. 특히 Factory Method 패턴이 사용될 때 그렇다. 일반적으로 이것은 추상 클래스를 구현하는 구체적 객체를 리턴하는 정적 create() 메소드를 갖고 있는 추상 클래스를 갖추면서 구현된다. 구현되어야 할 구체적 객체의 유형이 클라이언트 클래스와 관계가 없는 구현 상세에 의존한다면 유용하게 쓰인다.

클래스 내부에서 코드 변경하기
가장 광범위한 리팩토링은 클래스 내에서 코드를 인식하는 리팩토링이다. 무엇보다도 이들은 중간 변수를 가져오고 오래된 메소드의 한 부분에서 새 메소드를 만들고 필드용 setter 및 getter를 만든다.

Extracting & inlining
Extract: Extract Method, Extract Local Variable, Extract Constants로 시작하는 많은 리팩토링이 있다. Extract Method는 여러분이 선택한 코드에서 새로운 메소드를 만든다. 클래스의 main() 메소드 예를 들어보자(Listing 8). 이것은 명령행 옵션을 계산하고 -D로 시작하는 무엇인가를 발견하면 Properties 객체에 이름 값 쌍으로 그들을 저장한다.

Listing 8. main()
 import java.util.Properties; import java.util.StringTokenizer; public class StartApp { public static void main(String[] args) { Properties props = new Properties(); for (int i= 0; i < args.length; i++) { if(args[i].startsWith("-D")) { String s = args[i].substring(2); StringTokenizer st = new StringTokenizer(s, "="); if(st.countTokens() == 2) { props.setProperty(st.nextToken(), st.nextToken()); } } } //continue... } } 

메소드에서 몇 개의 코드를 가져다가 이를 또 다른 메소드에 넣는 곳에 두 가지 경우가 있다. 첫 번째 경우는 메소드가 너무 길고 두 개 이상의 구별된 논리적 작동을 수행한다. 두 번째 경우는 다른 메소드에 의해 재사용 될 수 있는 코드의 구별된 섹션이 있다면 두 번째 케이스이다.

이름 값 쌍을 파싱하고 그들을 Properties 객체에 추가해야하는 또 다른 장소가 있고 StringTokenizer 선언과 뒤따르는 if 절을 포함하는 코드 색션을 추출할 수 있다고 가정해보자. 이를 수행하기 위해서는 이 코드를 강조하고 그런다음 Refactor > Extract Method를 메뉴에서 선택한다. 메소드 이름이 프롬프트되면 addProperty를 입력하고 이 메소드가 두 개의 매개변수(Properties prop & Strings)를 갖고 있음을 확인한다. (Listing 9).

Listing 9. 추출된 addProp()
 import java.util.Properties; import java.util.StringTokenizer; public class Extract { public static void main(String[] args) { Properties props = new Properties(); for (int i = 0; i < args.length; i++) { if (args[i].startsWith("-D")) { String s = args[i].substring(2); addProp(props, s); } } } private static void addProp(Properties props, String s) { StringTokenizer st = new StringTokenizer(s, "="); if (st.countTokens() == 2) { props.setProperty(st.nextToken(), st.nextToken()); } } } 

Extract Local Variable 리팩토링은 직접 사용되고 로컬 변수에 먼저 할당되는 익스프레션을 취한다. 이 변수는 익스프레션이 있었던 곳에서 사용된다. 변수를 제공하라는 프롬프트가 뜨면 key를 입력한다. 선택된 모든 익스프레션을 새로운 변수에 대한 레퍼런스로 대체하는 옵션이 있다. 이것은 종종 적절하지만 nextToken() 메소드의 경우에는 그렇지 않다. 호출될 때마다 매번 다른 값을 리턴한다.이 옵션에는 선택되지 않았다. (그림 6).

그림 6. 선택된 모든 익스프레션을 대체하지 않는다.
Extract variable

그런 다음 st.nextToken()에 대한 두 번째 호출에 대해 이 리팩토링을 반복한다. 이번에는 새로운 로컬 변수 value를 호출한다. Listing 10은 두 개의 리팩토링 후의 코드 모습이다 .

Listing 10. 리팩토링 후의 코드
 private static void addProp(Properties props, String s) { StringTokenizer st = new StringTokenizer(s, "="); if(st.countTokens() == 2) { String key = st.nextToken(); String value = st.nextToken(); props.setProperty(key, value); } } 

이러한 방식으로 변수를 도입할 때 여러 이점이 있다. 우선 익스프레션에 의미가 풍부한 이름을 제공함으로서 코드가 무엇을 수행하는지가 분명해진다. 둘째로, 익스프레션이 리턴하는 값을 쉽게 검사할 수 있기 때문에 코드 디버깅이 더욱 쉽다. 마지막으로 익스프레션의 많은 인스턴스들이 하나의 변수로 대체될 수 있는 경우 더욱 효율적이다.

Extract Constant는 Extract Local Variable와 비슷하지만 정적 상수 익스프레션을 선택해야한다. 이것은 하드 코딩된 숫자와 스트링을 코드에서 제거하는데 유용하다. 예를 들어, 위 코드에서 이름 값 쌍을 정의하면서 명령행 옵션을 위해 -D" 를 사용했다. 코드에서 -D" 를 하이라이트 하고 Refactor > Extract Constant를 선택한 다음 상수 이름으로 DEFINE을 입력한다. 이 리팩토링은 코드를 변경할 것이다. (Listing 11).

Listing 11. 리팩토링된 코드
 public class Extract { private static final String DEFINE = "-D"; public static void main(String[] args) { Properties props = new Properties(); for (int i = 0; i < args.length; i++) { if (args[i].startsWith(DEFINE)) { String s = args[i].substring(2); addProp(props, s); } } } // ... 

각 Extract... 리팩토링의 경우 상응하는 인라인 ... refactoring이 있어 반대 작동을 수행한다. 예를 들어 위 코드의 변수 s를 강조하고 Refactor > Inline...를 선택하고 OK를 누르면 Eclipse는 addProp()에 대한 호출에 직접 args[i].substring(2) 익스프레션을 사용한다:

 if(args[i].startsWith(DEFINE)) { addProp(props,args[i].substring(2)); }

필드의 캡슐화
객체의 내부 구조를 노출하는 것은 좋은 관례로 여겨지지 않았다. Vehicle 클래스와 이것의 하위클래스가 프라이빗 필드 또는 보호 필드를 갖고 있고 퍼블릭 setter와 getter 메소드가 액세스를 제공하는 이유도 그것이다. 이러한 메소드들은 두 가지 다른 방법으로 만들어진다.

이러한 메소드들을 만드는 한 가지 방법은 Source > Generate Getter and Setter를 사용하는 것이다. 각각의 필드가 갖고 있지 않은 제안된 getter와 setter 메소드가 있는 다이얼로그 박스를 디스플레이 할 것이다. 이것은 리팩토링은 아니다. 새로운 메소드를 사용하기 위해 필드에 대한 레퍼런스를 업데이트 하지 않기 때문이다. 이 옵션은 시간 절약에는 효과가 있지만 클래스를 처음 만들 때 가장 적합하다. 또는 새로운 필드를 클래스에 추가할 때도 적절하다.

getter와 setter 메소드를 만드는 두 번째 방법은 필드를 선택하고 메뉴에서 Refactor > Encapsulate Field를 선택하는 것이다. 이 메소드는 한번에 한 필드에 대한 getter와 setter를 만들지만 Source > Generate Getter and Setter와는 달리 필드에 대한 레퍼런스를 새로운 메소드에 대한 호출로 변경한다.

새로운 Automobile 클래스로 시작해보자 (Listing 12).

Listing 12. Automobile 클래스
 public class Automobile extends Vehicle { public String make; public String model; } 

Automobile을 인스턴스화하는 클래스를 만들고 make 필드에 직접 액세스한다. (Listing 13).

Listing 13. Automobile의 인스턴스화
 public class AutomobileTest { public void race() { Automobilecar1 = new Automobile(); car1.make= "Austin Healy"; car1.model= "Sprite"; // ... } }

필드 이름을 강조하고 Refactor > Encapsulate Field를 선택하여 make 필드를 캡슐화한다. 다이얼로그에서 getter와 setter 메소드용 이름을 입력한다. 예상했겠지만 이는 기본적으로 getMake()setMake() 이다. 필드와 같은 클래스에 있는 메소드가 필드에 직접 액세스 할 것인지 또는 이 레퍼런스가 변경되어 모든 다른 클래스 처럼 액세스 메소드를 사용할 것인지를 선택한다. (그림 7).

그림 7. 필드의 캡슐화
Encapsulating a field

OK를 누른 후, Automobile 클래스에 있는 make 필드는 프라이빗이 되고 getMake()setMake() 메소드를 갖게된다. (Listing 14).

Listing 14. 리팩토링 후의 Automobile 클래스
 public class Automobile extends Vehicle { private String make; public String model; public void setMake(String make) { this.make = make; } public String getMake() { return make; } } 

AutomobileTest 클래스는 업데이트 되어 새로운 액세스 메소드를 사용한다. (Listing 15).

Listing 15. AutomobileTest 클래스
 public class AutomobileTest { public void race() { Automobilecar1 = new Automobile(); car1.setMake("Austin Healy"); car1.model= "Sprite"; // ... } }

Change Method Signature
마지막 리팩토링(Change Method Signature)은 사용하기 가장 어렵다. 이것이 무엇을 수행하는지는 매우 명확하다. 매개변수, 가시성, 메소드의 리턴 타입을 변경한다. 분명하지 않은 것은 이러한 변경이 메소드 또는 그 메소드를 호출하는 코드에 대한 영향이다. 여기에 마술은 존재하지 않는다. 리팩토링되는 메소드에서 변경으로 인해 문제가 생긴다면 리팩토링 작동은 이를 거부한다. 어쨌든 리팩토링을 수락하여 나중에 문제를 정정하거나 리팩토링을 취소해야 한다. 리팩토링이 다른 메소드에서 문제를 일으키면 이것은 무시되고 리팩토링 후에 스스로 문제를 해결해야 한다.

Listing 16. MethodSigExample 클래스
 public class MethodSigExample { public int test(String s, int i) { int x = i + s.length(); return x; } } 

test() 메소드는 다른 클래스의 메소드에 의해 호출된다. (Listing 17).

Listing 17. callTest 메소드
 public void callTest() { MethodSigExample eg = new MethodSigExample(); int r = eg.test("hello", 10); }

첫 번째 클래스에서 test를 하이라이팅하고 Refactor > Change Method Signature를 선택한다. 다이얼로그 박스가 나타난다. (그림 8).

그림 8. Change Method Signature 옵션
Change Method Signature options

이 첫 번째 옵션은 메소드의 가시성을 변경하는 것이다. 이 예제에서 이를 protected 또는 private으로 변경하여 두 번째 클래스의 callTest() 메소드가 접근할 수 없도록 한다. Eclipse는 리팩토링을 실행하는 동안 이 에러를 정지하지 않는다. 적절한 값을 선택하는 것은 여러분의 몫이다.

다음 옵션은 리턴 유형을 변경하는 것이다. 예를 들어 리턴 유형을 float로 변경한다고 해서 에러를 정지할 수 없다. test() 메소드의 리턴 문의 int가 자동으로 float을 프롬프트하기 때문이다. 그럼에도 이것은 두 번째 클래스의 callTest()에서 문제를 일으킨다. floatint로 변환될 수 없기 때문이다. test()에 의해 리턴된 리턴 값을 int에 캐스팅하거나 callTest()r 타입을 float으로 변경한다.

String에서 int으로 첫 번째 매개변수 유형을 변경한다면 비슷한 것이 고려될 수 있다. 이것은 리팩토링 실행 중 정지된다. 리팩토링 되고있는 메소드에 문제를 일으키기 때문이다: intlength() 메소드를 갖고 있지 않다. 이를 StringBuffer로 바꾸면 문제로 인해 정지되지 않는다. length() 메소드가 없기 때문이다. 물론 이것 역시 callTest() 메소드에 문제를 일으킨다. test() 를 호출할 때 String을 전달하기 때문이다.

앞서 언급했지만 리팩토링이 에러라는 결과를 내는 경우, 정지 되든 안되든 경우에 따라 에러를 정정하여 지속할 수 있다. 또다른 방법으로는 에러를 사전점유하는 것이다. i 매개변수를 제거하려면 리팩토링 되고있는 메소드에서 이에 대한 레퍼런스를 제거할 수 있다. 이 매개변수를 제거하면 더욱 부드러워 진다.

Default Value 옵션을 마지막으로 설명하겠다. 이것은 매개변수가 메소드 신호에 추가될 때에만 사용된다. 매개변수가 콜러에 추가될 때 값을 제공하는데 사용된다. 예를 들어 n이라는 이름을 가진 String 타입의 매개변수와 world의 디폴트 값을 callTest() 메소드의 test()에 대한 호출에 추가하면 다음과 같이 된다 :

 public void callTest() { MethodSigExample eg = new MethodSigExample(); int r = eg.test("hello", 10, "world"); } 

요약
Eclipse의 툴을 사용하면 리팩토링이 쉽다. 이 툴에 익숙해지면 생산성도 향상된다. 프로그램 기능을 반복적으로 추가하는 개발 방식에서는 프로그램 디자인의 변경 및 확장 기술로서 리팩토링에 의존한다. 하지만 리팩토링에서 요구하는 공식적 방법을 사용하지 않더라도 Eclipse의 리팩토링 툴은 일반적인 코드 변경을 가능하게 하는 방식을 제공한다.

참고자료

  • Refactoring: Improving the Design of Existing Code by Martin Fowler, Kent Beck, John Brant, William Opdyke, and Don Roberts (Addison-Wesley, 1999).

  • Eclipse In Action: A Guide for Java Developers, by David Gallardo, Ed Burnette, and Robert McGovern (Manning, 2003).

  • Design Patterns: Elements of Reusable Object-Oriented Software by Erich Gamma, Richard Helm, Ralph Johnson, John Vlissides (Addison-Wesley, 1995).

  • Design Patterns use C++; for a book that translates patterns to the Java language, see Patterns in Java, Volume One: A Catalog of Reusable Design Patterns Illustrated with UML, Mark Grand (Wiley, 1998).

  • Extreme Programming Explained: Embrace Change, by Kent Beck (Addison-Wesley, 1999).

웹 사이트

developerWorks 튜토리얼

Posted by tornado
|
 

 

 

 

동적 서블릿 이미지 메이킹을 마음대로!
Level: Introductory
 

Dan Becker
소프트웨어 개발자, IBM Software Group
2002년 11월

웹 사이트를 코딩하고 지원하거나 인터넷에 페이지를 갖고 있다면 독자들의 필요를 충족시키는 이미지를 제공하는 것이 얼마나 어려운 일인지 알 것이다. JavaServer Pages (JSP) 태그를 이용하여 이미지 관리를 시도해보자.
이 글은 IBM developerWorks 저널에 2002년 11월 이슈 부분에 기재되었음을 밝혀둔다.

세상을 지배하라! 그렇지 않다면, 적어도 이미지라도 지배하라!
사이트 개발자 또는 페이지 작성자로서 다양한 선호를 만족시키는 것은 어려운 일이라는 것을 안다. 이를 수작업으로 수행하기 위해서는 사이트의 이미지 하나하나를 웹 사이트가 지원하는 이미지 사이즈로 변환해야한다. 그런다음 사이트의 각 페이지에 이미지 태그를 조정하여 각각의 태그가 이미지 사이즈를 적절하게 반영할 수 있도록 해야한다. 이미지를 변경하지 않고서는 HTML img 태그의 넓이와 높이를 간단히 변경시킬 수 없다. 이는 저대역 사용자들은 큰 이미지를 다운로드하여 클라이언트 측에서 리사이징을 해야한다는 것을 뜻한다. 이러한 종류의 이미지 관리는 성가실 뿐 아니라 에러를 양산해낸다. 그리고 대부분의 웹 사이트가 다양한 이미지 사이즈를 제공하지 않는 이유를 쉽게 알 수 있다.

문제는 기술의 문제가 아니다. 자바 프로그래밍으로 이미지를 다양한 사이즈나 포맷으로 변환하기는 쉽다. 서비스만의 문제 또한 아니다. 웹 서비스를 사용하여 각자의 필요에 맞춰 페이지를 커스터마이징하는 것이 일반적이기 때문이다. 문제는 전개와 관리가 쉽도록 기술과 서비스를 조합하는 일이다.

이 글은 JavaServer Pages (JSP) 태그를 사용하는 솔루션으로 이미지를 관리하는 방법을 다룬다. 예를들어 HTML에서 이미지 태그를 코딩하고 다양한 버전의 이미지 사이즈를 갖추는 대신 다음과 같이 하는것이다:


<img src="images/LazyDog.jpg" width="800" height="600" >

 

사용자의 선호에 맞춰 이미지를 자동으로 사이징하는 태그를 갖추는 것이 합리적이다:


<util:imagesizer src="images/LazyDog.jpg"/>

 

사용자가 다양한 사이즈 중에서 선택하고 선호도에 따라 사이트상의 모든 이미지가 바뀌도록 하는 것이 가능하다. 그림 1의 샘플 이미지를 보자. 또한 넓이와 높이 속성을 삽입하고 태그를 수동으로 편집하는 일 따위는 하지 않아도 된다.

그림 1. 이미지 선택이 있는 JSP 페이지

 


한번도 JSP 커스텀 태그를 본 적이 없다면 예제에서 신택스를 연구해보자. JSP 커스텀 태그를 HTML 태그와 비교해보자:

태그 개발자가 만든 imagesizer가 있다.


이 태그는 프리픽스 util이 있는데 태그 세트들을 자바 프로그래밍의 패키지 이름과 흡사한 라이브러리들로 그룹핑한다. 새로운 프리픽스를 만들거나 라이브러리에 있는 디폴트 이름을 사용할 수 있다.


이 태그는 새로운 XML 방식의 끝내는(ending) 태그를 가지고 있다: "/>".
HTML 태그처럼, JSP 태그도 많은 속성이 있다. 여기에서는 src가 제시되었다. 또한 바디가 있으며 바디 안에는 다른 태그들도 가질 수 있다. HTML img 태그를 모방하는 것이기 때문에 JSP 이미지 사이징 태그용 바디는 없다.

JSP 페이지가 커스텀 image-sizer 태그를 사용할 때, 태그의 자바 구현은 이미지 파일을 찾아 적절한 사이즈로 변환하고 이 이미지를 독자에게 제공한다. 태그는 퍼블리싱하기 전에 이미지 변환 부터 사이트 매니저를 저장한다. 또한 웹 페이지 작성 작업을 단순화한다. 단지 하나의 페이지가 많은 이미지 사이즈 선택을 핸들하는데 필요하기 때문이다. 무엇보다도, 중요한 것은 각자의 사이트 이미지에 이러한 유연성을 제공한다면 인기있는 사이트가 될 것이다.

웹 서버
이제는, 클라이언트(웹 브라우저를 이용하는 독자)가 JSP 페이지를 제공하는 사이트를 방문할 때 그 배후에 어떤일이 벌어지는지 알아보자. 세 개의 인터랙션이 이루어진다. (그림 2):

그림 2. 웹 클라이언트와 서버 간 인터랙션

 


첫 번째 경우, 브라우저가 HTML 파일이나 이미지 파일 같은 정적 문서를 요청한다고 가정해보자. 서버는 파일 공간에 리소스를 위치시키고 브라우저에 파일을 제공한다. 문서를 요청하고 요청에 응답하는 것은 HTTP에 정의되어 있는데 이것은 인터넷 상에서 클라이언트-서버 인터랙션의 기초가 된다. 웹 서버는 요청을 완벽하게 핸들하고 웹 애플리케이션 서버나 서블릿 콘테이너와 인터랙션 할 필요가 없다.

두 번째 경우 브라우저가 자바 서블릿을 포함하는 웹 리소스를 요청한다고 가정해보자. 자바 서블릿은 웹 서버가 자바 프로그래밍 언어를 사용하여 서버상에서 태스크를 수행하도록 한다. 서블릿들은 효율적이어서 CGI와 서버측 JavaScript 같은 오래된 기술보다 메모리와 프로세싱 파워를 덜 사용한다. 서블릿은 IBM WebSphere Application Server과 Apache Tomcat 같은 웹 서버에서 다른 기술들에 비해 이동성이 강하고, 다양한 플랫폼 상에서 같은 서블릿을 구동할 수 있는 서블릿 콘데이너를 지원한다. 마지막으로, 자바언어 고유의 안정성으로 인해 잘못된 서블릿이 웹 서버에 영향을 주는 경우도 드물다. 웹 서버는 적절한 서블릿을 찾아 필요할 경우 서블릿 소스 코드를 컴파일하고 서블릿 프로세싱 결과를 요청자에게 리턴한다. 자주 요청되는 서블릿은 서버 메모리에 캐싱된다. (그림 2).

세 번째 경우, 브라우저가 JSP 페이지를 포함하는 웹 페이지를 요청한다는 것을 가정해보자. JSP 페이지는 정보를 디스플레이 하는 작업을 쉽게 하고 동적 콘텐트를 정적 페이지와 분리하는 것을 돕는다. 웹 페이지 디자이너는 HTML 라이브러리에 있는 다른 태그인 것 처럼 하여 JSP 태그를 사용한다. JSP 프로그래머는 JSP 프로그래밍 스팩에 따라 태그를 구현한다.

이번에는 이미지-사이징 JSP 태그를 구현하고 JSP 코드를 작성하는 방법을 알아보자. 웹 콘테이너의 관점에서 볼 때 JSP 페이지는 자바 서블릿과 밀접하게 관련되어 있다. 텍스트 기반 JSP 페이지는 웹 콘테이너에 의해 자바 구현으로 변환된다. 웹 콘테이너는 자바 구현을 찾아 자바 서블릿 같은 구현을 처리하고 코드를 구동하여 프로세싱 결과를 클라이언트에 리턴한다. 많은 레이어와 리다이렉트가 있는 듯이 보이지만 디스패칭은 빠르고 사용자에게도 투명하다. 서블릿과 마찬가지로 자주 요청되는 JSP 페이지는 서버 메모리에 캐싱된다.

JSP 커스텀 태그 작성하기
웹 서비스가 JSP 페이지 요청을 어떻게 처리하는지 보았다. 이제는 JSP 커스텀 태그를 어떻게 구현하는지 보자. JSP 태그는 Java Standard Template Library(JSTL) 같은 표준 라이브러리와 일명 커스텀 태그라고 하는 스스로 작성한 라이브러리에도 해당된다는 것을 명심하라. 일반적으로 커스텀 태그는 특정 문제 도메인을 다룬다. 이 글에서는 이미지를 관리하는 방법을 다루겠다. 현재 Java 2 Extended Edition (J2EE) Versions 1.2와 1.3은 JSP Version 1.2를 사용한다. 현재 Sun은 JSP 스팩 2.0 버전을 발표했다. 이 새로운 스팩은 커스텀 태그를 구현하는 방식에 있어서 큰 변화가 없다.

표준과 커스텀 태그 라이브러리를 taglib 디렉티브를 통해 JSP 페이지로 반입할 수 있다:


<%@ taglib uri='imagesizer.tld' prefix='util' %>

 

이 디렉티브는 태그 라이브러리 디스크립터 파일의 위치를 명명한다. 여기서는 imagesizer.tld로 명명되었다. 그리고 사용된 프리픽스는 util로 명명되었다. 태그 예제에서 보았듯이 프리픽스와 이름으로 태그를 사용할 수 있다:


<util:imagesizer src="images/LazyDog.jpg"/>

 

태그 라이브러리 디스크립터는 웹 콘데이너에게 사용가능한 태그가 무엇인지 그들이 어떻게 작용하는지를 설명한다. Listing 1은 그 예제이다. 이 파일은 XML 포맷으로 되어있고 읽기 쉽다. 하지만 IBM WebSphere Studio Application Developer 같은 애플리케이션 개발 플랫폼은 필드를 채우고 파일을 검사하는 것을 돕는다. 가장 중요한 정보는 태그 엘리먼트이다. 이것은 JSP 커스텀 태그의 이름과 태그를 구현하는 자바 클래스를 정의한다. 또한 태그가 받아들이는 모든 속성과 바디 콘텐트를 보여준다.

Listing 1. Tag Library Descriptor (TLD) 발췌
<taglib >
  <tlibversion> 1.0 </tlibversion>
  <jspversion> 1.1 </jspversion>
  <tag>
    <name>imagesizer</name>
    <tagclass>tags.ImageSizerTag</tagclass>
    <bodycontent>empty</bodycontent>
    <attribute>
      <name>src</name>
      <required>required</required>
    </attribute>
    <attribute>
      <name>alt</name>
    </attribute>
    <attribute>
      <name>quality</name>
    </attribute>
  </tag>
</taglib>

 

이 예제에서 태그는 src 속성이 요구하는 세 개의 속성을 갖고 있다. alt 속성은 HTML img alt 속성을 모방한 것이다. 이 JSP 태그를 확장하여 다른 이미지 속성을 포함시켜도 된다.

JSP 커스텀 태그를 작성하는 다음 단계는 태그 용 자바 코드를 구현하는 것이다. 이 글에서 imagesizer 태그는 tags.ImageSizerTag 자바 클래스에서 구현된다. J2EE 커스텀 태그 지원 대부분은 javax.servlet.jsp.tagext 패키지에 위치해있다. imagesizer 클래스는 표준 TagSupport를 확장한다. 이것은 바디 없이 태그를 구현한다. 이것의 자손 클래스는 BodyTagSupport인데 이것은 바디가 있는 태그를 구현한다. 두 클래스 모두 Tag 인터페이스를 구현한다. doStartTag과 doEndTag 메소드는 태그가 처음 읽혀지고 태그가 웹 컨테이너에 의해 완전히 읽혀진 후에 호출된다. ImageSizer 태그는 doEndTag 만 구현한다.

TagSupport 클래스에서 PageContext 클래스는 JSP 페이지와 관련된 중요한 정보에 액세스 하도록 한다. 예를들어, PageContext는 HttpRequest와 HttpResponse 객체에 액세스를 가능하게 한다. 이러한 객체들은 값을 읽고 응답을 작성하는데 필수적이다. 요청은, 사용자 선택을 트래킹하고 페이지에서 페이지로 값을 수행할 경우 HttpSession로 액세스를 허용한다. PageContext는 ServletContext로 액세스를 허용하는데 이것은 서블릿 경로, 이름, 기타정보를 위치시키는데 도움이 된다. ImageSizer 코드(Listing 2)에서, PageContext 객체에 대한 레퍼런스와 정보가 있다. 그림 3은 이들 클래스들의 관계이다. 다른 표준 클래스 다이어그램 처럼 실선으로 된 박스는 클래스를 나타내고 점선 박스는 인터페이스를 나타낸다. 상속은 파생된 클래스나 인터페이스부터 부모까지 선으로 이어져있다.

Listing 2. ImageSizerTag doEndTag 구현
// Implement the tag once the complete tag has been read.
public int doEndTag() throws JspException {

  // Move request data to session.
  int outputSize = 0;
  String sizeVal = request.getParameter( REQUESTSIZEKEY );
  if ( sizeVal != null ) {
    session.setAttribute( REQUESTSIZEKEY, sizeVal );
    sizeVal = (String) session.getAttribute( REQUESTSIZEKEY );
    if ( sizeVal != null ) {
      outputSize = Integer.parseInt( sizeVal );
    }
  }
 
  // Get specified image locally.
  String contextPath = getContextPath( request );
  Image image = Toolkit.getDefaultToolkit().getImage(contextPath + src );
  ImageSizer.waitForImage( image );
  int imageWidth = image.getWidth( null );
  int imageHeight = image.getHeight( null );

  if (( imageWidth > 0 ) && ( imageHeight > 0 )) {
    if (( outputSize > 0 ) && ( outputSize != imageWidth )) {
      // Convert image to new size.
      Image outputImage = ImageSizer.setSize( image, outputSize, -1 );
      ImageSizer.waitForImage( outputImage );
      int outputWidth = outputImage.getWidth( null );
      int outputHeight = outputImage.getHeight( null );
 
      if ( outputWidth > 0 && outputHeight > 0 ) {
        // Change image file name to xxxx.size.jpg
        String originalSrc = src;
        int lastDot = src.lastIndexOf( '.' );
        if ( lastDot > -1 ) {
          src = src.substring( 0, lastDot + 1 );
        }
        setSrc( src + outputSize + ".jpg" );

        // Write new size image to JPEG file.
        File file = new File( contextPath + src );
        if ( !file.exists() ) {
          out.println( "" );
          FileOutputStream fos = new FileOutputStream( contextPath + src );
          ImageSizer.encodeJPEG( fos, outputImage, quality );
          fos.close( ) ;
        }
       
        imageWidth = outputWidth;
        imageHeight = outputHeight;
      }
    } // if outputSize
  } // if image found

  // Produce output tag.
  out.print( "<img src=\"" + src + "\"" );
  // Add alt text, if any
  if ((alt != null ) && ( alt.length() > 0 )) {
    out.print( " alt=\"" + alt + "\"" );
  }
 
  // Add proper width, height.
  out.print( " width=\"" + imageWidth + "\" height=\"" +
    imageHeight + "\"" );
  out.println( ">" );

  return EVAL_PAGE;
} // doEndTag

 

그림 3. javax.servlet.jsp.tagext 클래스

 


ImageSizerTag 클래스의 doEndTag 메소드는 Listing 2에 나와있다. 이것은 JSP 커스텀 태그를 구현하는데 필요한 거의 모든 자바 코드라고 볼 수 있다. 큰 블록이지만 메소드를 완벽하게 보는데 도움이 된다. 우선 모든 HTTP 요청 매개변수는 HTTP 세션에 저장된다. 이것은 이미지 사이즈에 대한 사용자 선택 같은 속성을 가져다가 세션에 저장하여 페이지에 따라 사용자가 이를 따르도록 한다. 이 태그의 기능을 확장하기 위해서 이것을 확장하여 쿠키에 선택을 저장하여 사용자가 다음에 이 사이트를 방문할 때 선택을 사용하도록 할 수 있다.

다음 단계는 디폴트 이미지를 로딩하는 것이다. java.awt.Toolkit은 이미지를 요청하는데 이것은 ImageSizer.waitForImage와 함께 로드되고 적절한 사이징이 검토된다. 로딩을 위해 일시정지가 필요하다. 왜냐하면 자바 이미지는 비동기식으로 로딩되고 요청할 때마다 항상 사용할 수 있는 것이 아니기 때문이다. ImageSizer 보조 클래스는 이 예제에서 전체 이미지 프로세싱을 수행하고 다음 섹션에서는 더 자세하게 다루겠다. 넓이와 높이가 맞으면 이미지는 리사이징이 필요없다. HTML 이미지 태그는 이미지 이름과 현재 사이즈를 사용하여 작성된다. 이것은 HTML 이미지 태그를 모방하기위해 필요한 JSP 구현의 모든것이다.

ImageSizer 보조 클래스는 사용자가 새로운 이미지 사이즈를 요청하면 이미지를 리사이징한다. 이미지 파일은 파일 크기에 따라 새로운 이름이 된다. 이 파일은 JPEG 인코딩이고 파일 시스템에 작성된다. 결과적으로 새롭게 리사이징된 파일은 HTML 이미지 태그 아웃풋에 사용된다. 이 태그의 대안 구현은 파일을 GIF나 PNG 포맷으로 저장하거나 디스크 공간을 저장하기위해 메모리에서 이미지를 제공하는 것이다. Listing 2는 리사이징된 파일을 앞으로 사용할 경우에 대비하여 디스크에 캐싱한다. 따라서 처음의 리사이징은 약간의 서버 프로세싱 시간이 필요하지만 연속적인 이미지 사이즈 요청은 프로세싱이 전혀 필요하지 않다. 사용가능한 디스크 공간을 체크하여 제한된 파일 공간과 클라이언트에게 제공하는 정보의 긴급성 간에 균형을 맞추도록 할 수 있다.

이미지 사이징
지금까지 JSP 태그를 작성하는 단계를 검토했다. ImageSizerTag 클래스는 이미지를 자동으로 리사이징하여 사용자 선택에 맞춘다. 이번에는 ImageSizer 클래스를 사용하여 JPEG 파일로서 이미지를 리사이징하고 저장하는 구체적인 방법을 설명하겠다. 자바 코드로 이미지를 리사이징 할 때 java.awt.Image 클래스의 getScaledInstance 메소드를 사용할 때 쉬워진다. 이 메소드를 새로운 넓이와 높이로 하거나 형태 비율을 보존하기위해 -1에서 1까지의 매개변수 값을 제공한다. 그리고 나서 새롭게 리사이징된 이미지를 얻는다. 하지만 모든 자바 이미지는 즉시 사용할 수 있는 것은 아니다. 따라서 java.awt.MediaTracker 를 사용하여 이미지가 완전히 로딩될 때까지 기다려야한다. ImageSizer의 waitForImage 메소드는 이 코드를 캡슐화한다.

이 예제에서 가장 어려운 디자인 포인트는 이미지 저장 방법을 결정하는 일이다. 자바 프로그래밍에는 이미지 인코딩과 저장에 많은 선택권이 있어 비교를 해야한다.

com.sun.image.codec. 이 패키지는 Java 2 SDK 1.2와 1.3 구현에 사용할 수 있지만 개인 패키지어서 Java 2 버전까지 기다려야한다. 이 패키지는 JPEG 인코딩에 제한이 있다.


Java Image I/O API. 이 패키지는 Java 2 SDK 1.4의 표준이다. 하지만 작성 당시 어떤 J2EE 버전도 SDK 1.4를 사용하지 않는다. 이 패키지는 뛰어난 이미지 조작 기능과 인코딩 옵션을 갖추고 있다.


Java Advanced Imaging API. 이 API는 표준 확장이지만 이것을 사용하려면 웹 마스터가 지원하지도 않는 패키지를 설치해야한다.


ACME GIF Encoder. 이 패키지는 유용하며 예제 코드와 결합할 수 있다. 하지만 비용과 유지 문제가 남아있다. 다른 옵션들과는 다르게 이 소프트웨어는 유료이며 GIF 표준을 완전히 지원하지 않는다.
Listing 3의 경우 com.sun.image.codec 패키지를 사용했는데 IBM WebSphere와 Apache Tomcat 같은 J2EE 1.2 와 1.3 웹 서버 콘테이너에서 사용할 수 있기 때문이다. 인코더는 단순하며 100% 순수한 자바 코드이지만 com.sun 패키지에 있다. 장기적인 시각으로 볼 때 Java Image I/O 패키지는 보완해야 할 사항이 많이있다. 이미지 변형 기능과 많은 파일 포맷을 저장하는 기능이 훌륭하다. Java Image I/O 패키지는 Java 2 version 1.4 이전까지는 표준이 아니다.

어떤 이미징 패키지를 사용할 것인가에 대해 결정이 끝났다면 JPEG 파일을 저장하는 코드는 간단하다. ImageSizer의 encodeJPEG 메소드는 다음 절차를 캡슐화한다:


java.awt.image.BufferedImage 객체-자바 Image 자손-는 리사이징된 아웃풋 이미지에서 만들어진다. 주석은 로고, watermark, 타임스탬프, 이미지 저작권 정보를 추가할 때 코드에 주석이 달린다.


Image에서 BufferedImage로 변환한 후에, 아웃풋 스트림에 JPEGImageEncoder 객체를 만들어라. 아웃풋 인코딩 평가는 0.0 (최악)부터 1.0 (최고) 까지 이다. 0.75는 디폴트이지만 0.95는 좀더 자세한 이미지와 함께 큰 파일 사이즈로 만들어진다.


이미지는 아웃풋 스트림으로 인코딩되고 스트림은 모든 정보가 이미지 파일에 나타나도록 플러쉬된다.

Listing 3. ImageSizer encodeJPEG 구현
// Encodes the given image at the given
// quality to the output stream.
public static void encodeJPEG
  ( OutputStream outputStream,
  Image outputImage, float outputQuality )
  throws java.io.IOException {

  // Get a buffered image from the image.
  BufferedImage bi = new BufferedImage
    ( outputWidth, outputHeight,
      BufferedImage.TYPE_INT_RGB );
  Graphics2D biContext =
    bi.createGraphics( );
  biContext.drawImage
    ( outputImage, 0, 0, null );
  // Additional drawing code, such as
  // watermarks or logos can be placed here.

  // com.sun.image.codec.jpeg package
  // is included in Sun and IBM sdk 1.3.
  JPEGImageEncoder encoder =
    JPEGCodec.createJPEGEncoder
    ( outputStream );
  // The default quality is 0.75.
  JPEGEncodeParam jep =
    JPEGCodec.getDefaultJPEGEncodeParam
    ( bi );
  jep.setQuality( outputQuality, true );
  encoder.encode( bi, jep );
  outputStream.flush();
} // encodeImage

 

여기까지는 이미지를 리사이징하고 저장하는데 필요한 모든 단계이다.

WebSphere또는 Tomcat에서 패키징 및 전개
이번에는 Application Server version 4.0이나 Apache Tomcat version 4.0에서 ImageSizer JSP 태그의 패키지 및 전개 방법을 설명하겠다. 그림 4는 Application Developer 이다. 왼쪽 칼럼의 상단에 있는 Navigator는 웹 애플리케이션의 디렉토리 구조를 나타내고 JSP 태그가 J2EE 스팩에 따라 어떻게 패키지되어야 하는지를 보여준다. J2EE 스팩에 필요하기 때문이 이 디렉토리 구조는 모든 웹 애플리케이션에 일반적이다. 일단 구조가 저장되면 Web Archive (WAR) 파일이 되고 WebSphere, Tomcat, 기타 웹 콘테이너에 쉽게 전송된다. Application Developer 같은 좋은 개발 환경은 다음의 스팩을 따르고 훌륭한 애플리케이션을 만들어낸다.

그림 4. WebSphere Studio Application Developer에서 ImageSizer 패키징

 


ImageSizer 프로젝트에는 소스 코드용 디렉토리가 있다. 최종 WAR 파일에 이 디렉토리를 추가할 것인지의 여부는 개발자가 선택하기 나름이다. webApplication 디렉토리에는 실제 프로그램 코드가 포함되어 있다. 예제 프로젝트에는 PickASize.jsp이라고 하는 테스트 JSP 페이지와 LazyDog.jpg 이라고 하는 테스트 이미지가 포함되어 있다. 일반적으로 이들은 ImageSizer 커스텀 태그의 라이브러리 버전에 포함되지 않는다. 이 태그의 구현은 WEB-INF 디렉토리에 있다. 자바 클래스는 WEB-INF/classes에 있고 Tag Library Descriptor 파일은 WEB-INF/tlds에 있다. 이들은 모든 웹 애플리케이션에 적용되는 표준 디렉토리 위치이다. 이 트리에 있는 다른 파일들은 서버 옵션 설정에 도움을 주지만 그렇지 않더라도 WAR 파일에 필수적인 것은 아니다. Application Developer나 Java SDK를 사용하여 이 애플리케이션 용 WAR 파일을 만들어보라.

To m c a t 에 웹 애플리케이션을 설치하려면 ROOT/webapps 디렉토리에 파일을 놓고 서버가 WAR 파일을 디렉토리 구조로 확장하도록 해야한다. Application Server의 경우, Administrators Console의 웹 애플리케이션 마법사(Web Application wizard)를 사용하여 애플리케이션을 설치할 수 있다. 전개한 후에, http://yourhostname:port/ImageSizer/PickASize.jsp를 방문하여 JSP 페이지를 구동해보라.

결론
지금까지 이미지 사이징을 자동으로 관리하는 JSP 커스텀 태그를 만들었다. 커스텀 태그는 이미지 리사이징 작업에 도움이 되고 웹 사이트를 방문하는 사용자들이 선택을 할 수 있도록 한다. 예제 태그를 확장하여 모든 종류의 이미지 조작을 할 수 있다. 저작권 텍스트, 타임스탬프, 로고, 워터 마크 등이 그것이다. Application Server나 Apache Tomcat에 이를 전개하고 이미지 기반 JSP 페이지를 작성하거나 예제를 사용하여 코드를 실험할 수 있다. 이 글이 JSP 태그에 대해 실질적인 팁을 주었기를 바라며 각자의 필요에 맞춰 기능을 확장해보기를 바란다.


 

참고자료


 
 
필자소개
Dan Becker는 IBM(Austin, Texas)의 소프트웨어 그룹에서 일하고 있다. 현재는 Interactive Financial Services 프로젝트에서 온라인 웹 뱅킹 시스템 작업을 수행하고 있다.

 

 

 

참고문서 : http://www-903.ibm.com/developerworks/kr/java/library/j-jspdwj.html

'JAVA > JSP_Servlet' 카테고리의 다른 글

[펌] 리팩토링(Refactoring)  (0) 2004.03.29
[펌] 스트럿츠 이디엄  (0) 2004.03.29
[펌] 고급DAO 사용하기  (0) 2004.03.29
[펌] Torque로 DB에 Query문 출력하기  (0) 2004.03.26
[펌] Torque프로젝트 사용기  (0) 2004.03.26
Posted by tornado
|
DAO 구현 기술

Level: Advanced

Sean C. Sullivan
소프트웨어 엔지니어
2003년 10월 7일

J2EE 개발자들은 Data Access Object (DAO) 디자인 패턴을 사용하여 저수준의 데이터 액세스 로직과 고급 비즈니스 로직을 분리한다. DAO 패턴을 구현하는 것은 단순히 데이터 액세스 코드를 작성하는 것 이상이다.

지난 18개월 동안, 재능 있는 소프트웨어 엔지니어들과 함께 웹 기반의 공급체인 관리 애플리케이션을 구현했다. 우리가 만든 애플리케이션은 선적 상황, 공급 체인 메트릭스, 창고 재고, 운송장, 프로젝트 관리 데이터, 사용자 프로파일 등의 광범위한 데이터에 접근했다. JDBC API를 사용하여 회사의 다양한 데이터베이스 플랫폼에 연결했고 이 애플리케이션에 DAO 디자인 패턴을 적용했다.

그림 1은 애플리케이션과 데이터 소스의 관계이다:

그림 1. 애플리케이션과 데이터 소스
Web applications

DAO의 기초
DAO 패턴은 표준 J2EE 디자인 패턴들 중 하나이다. 이 패턴을 사용하여 저수준 데이터 액세스와 고급 비지니스 로직을 분리한다. 전형적인 DAO 구현에는 다음과 같은 요소들이 있다:

  • DAO 팩토리 클래스
  • DAO 인터페이스
  • DAO 인터페이스를 구현하는 구체적 클래스
  • 데이터 전송 객체들(밸류(value) 객체)

구체적인 DAO 클래스에는 특정 데이터 소스로 부터 데이터에 액세스하는데 쓰이는 로직이 포함되어 있다.

트랜잭션 경계설정(demarcation)
DAO는 트랜잭션 객체들이라는 것을 반드시 기억해야 한다. DAO에 의해 수행되는 각각의 작동(데이터 구현, 업데이트, 삭제)은 트랜잭션과 관련있다. 따라서 트랜잭션 경계설정(demarcation) 개념은 매우 중요하다.

트랜잭션 경계설정은 트랜잭션 영역들이 정의 되는 방식이다. J2EE 스팩은 두 가지 모델의 트랜잭션 경계설정을 설명하고 있다. (표 1):

표 1. 트랜잭션 경계설정

선언적(Declarative) 트랜잭션 경계설정프로그램에 입각한(Programmatic) 트랜잭션 경계설정
프로그래머는 EJB 전개 디스크립터를 사용하여 트랜잭션 애트리뷰트를 선언한다.프로그래머는 트랜잭션 로직을 코딩해야한다.
런타임 환경(EJB 컨테이너)은 트랜잭션을 자동으로 관리하기 위해 이 애트리뷰트를 사용한다.이 애플리케이션은 API를 통해 트랜잭션을 제어한다.

프로그램에 입각한(Programmatic) 트랜잭션 경계설정을 중점적으로 설명하겠다.

디자인 고려사항
앞서 언급했지만, DAO는 트랜잭션 객체이다. 전형적인 DAO는 구현, 업데이트, 삭제 같은 트랜잭션 작동을 수행한다. DAO를 설계할 때 다음 사항들을 점검한다:

  • 트랜잭션은 어떻게 시작하는가?
  • 트랜잭션은 어떻게 끝나는가?
  • 트랜잭션 시작을 담당하는 객체는 무엇인가?
  • 트랜잭션 종료를 담당하는 객체는 무엇인가?
  • DAO가 트랜잭션의 시작과 종료를 담당해야 하는가?
  • 이 애플리케이션이 다중의 DAO를 통해 데이터에 액세스해야 하는가?
  • 트랜잭션에 포함될 DAO의 수는?
  • 하나의 DAO가 또 DAO에 대한 메소드를 호출할 수 있는가?

이러한 질문들에 대한 답을 알고있다면 자신의 DAO에 가장 잘 맞는 트랜잭션 경계설정 전략을 선택하는데 도움이 된다. DAO에는 두 가지 주요한 트랜잭션 경계설정 전략이 있다. 하나는 DAO가 트랜잭션의 경계를 설정하도록 하는 것이다. 또 다른 방법은 트랜잭션 경계설정을 DAO의 메소드를 호출하는 객체에 맡기는 것이다. 전자를 선택한다면 DAO 클래스 안에 트랜잭션 코드를 임베딩해야 한다. 후자의 방법을 선택한다면 트랜잭션 경계설정 코드는 DAO 클래스의 외부에 있게 될 것이다. 코드 예제를 보고 이 두 가지 방법을 자세히 보도록 하자.

Listing 1- DAO와 두 개의 데이터 작동;구현 및 업데이트:

Listing 1. DAO 메소드
       public void createWarehouseProfile(WHProfile profile);       public void updateWarehouseStatus(WHIdentifier id, StatusInfo status);

Listing 2은 간단한 트랜잭션이다. 트랜잭션 경계설정 코드는 DAO 클래스 외부에 있다. 이 예제에서 콜러(caller)가 다중의 DAO 작동들을 이 트랜잭션 내에서 결합하는 방법을 주목해보자.

Listing 2. 콜러(Caller)에 의해 관리되는 트랜잭션
      tx.begin();    // start the transaction      dao.createWarehouseProfile(profile);      dao.updateWarehouseStatus(id1, status1);      dao.updateWarehouseStatus(id2, status2);      tx.commit();   // end the transaction

이 트랜잭션 경계설정 전략은 단일 트랜잭션에서 다중의 DAO에 액세스 해야하는 애플리케이션에 특별히 어울린다.

JDBC API 또는 Java Transaction API (JTA)를 사용하여 트랜잭션 경계설정을 구현할 수 있다. JDBC 트랜잭션 경계설정은 JTA 보다 단순하다. 하지만 JTA는 보다 유연하다.

JDBC를 이용한 트랜잭션 경계설정
JDBC 트랜잭션은 Connection 객체를 사용하여 제어된다. JDBC Connection 인터페이스(java.sql.Connection)는 두 개의 트랜잭션 모드를 제공한다. (auto-commit과 manual commit). java.sql.Connection은 트랜잭션 제어에 다음의 메소드를 제공한다:

  • public void setAutoCommit(boolean)
  • public boolean getAutoCommit()
  • public void commit()
  • public void rollback()

Listing 3은 JDBC API를 사용하여 트랜잭션의 경계설정을 하는 방법이다:

Listing 3. JDBC API를 이용한 트랜잭션 경계설정
      import java.sql.*;      import javax.sql.*;      // ...      DataSource ds = obtainDataSource();      Connection conn = ds.getConnection();      conn.setAutoCommit(false);      // ...      pstmt = conn.prepareStatement("UPDATE MOVIES ...");      pstmt.setString(1, "The Great Escape");      pstmt.executeUpdate();      // ...      conn.commit();      // ...

JDBC 트랜잭션 경계설정을 이용하면 여러 개의 SQL 문장을 하나의 트랜잭션으로 결합할 수 있다. JDBC 트랜잭션의 단점 중 하나는 트랜잭션 범위가 하나의 데이터베이스 연결로 제한되어 있다는 점이다. JDBC 트랜잭션은 다중 데이터베이스로 확장할 수 없다. 다음에는 JTA를 사용한 트랜잭션 경계설정이다.

JTA 개요
Java Transaction API (JTA)와 Java Transaction Service (JTS)는 J2EE 플랫폼에 분산 트랜잭션 서비스를 제공한다. 분산 트랜잭션에는 트랜잭션 매니저와 한 개 이상의 리소스 매니저가 포함된다. 리소스 매니저는 일종의 영속 데이터스토어이다. 트랜잭션 매니저는 모든 트랜잭션 참여자들 간 통신을 조정하는 역할을 담당한다. 트랜잭션 매니저와 리소스 매니저 관계는 다음과 같다. (그림 2):

그림 2. 트랜잭션 매니저와 리소스 매니저
A transaction manager and resource managers

JTA 트랜잭션은 JDBC 트랜잭션 보다 강력하다. JDBC 트랜잭션이 하나의 데이터베이스 연결로 제한되어 있다면 JTA 트랜잭션은 다중의 참여자들을 가질 수 있다. 다음의 자바 플랫폼 컴포넌트 중 어떤 것이든지 JTA 트랜잭션에 참여할 수 있다:

  • JDBC 커넥션
  • JDO PersistenceManager 객체
  • JMS 큐
  • JMS 토픽
  • Enterprise JavaBeans
  • J2EE Connector Architecture 스팩에 호환하는 리소스 어댑터

JTA를 이용한 트랜잭션 경계설정
JTA로 트랜잭션 경계를 설정하기 위해 애플리케이션은 javax.transaction.UserTransaction 인터페이스에 대한 메소드를 호출한다. Listing 4는 UserTransaction 객체의 전형적인 JNDI이다:

Listing 4. UserTransaction 객체의 JNDI
      import javax.transaction.*;      import javax.naming.*;      // ...      InitialContext ctx = new InitialContext();      Object txObj = ctx.lookup("java:comp/UserTransaction");      UserTransaction utx = (UserTransaction) txObj;

애플리케이션 UserTransaction 객체에 대한 레퍼런스를 가진 후에 트랜잭션을 시작한다. (Listing 5):

Listing 5. 트랜잭션 시작
      utx.begin();      // ...      DataSource ds = obtainXADataSource();      Connection conn = ds.getConnection();      pstmt = conn.prepareStatement("UPDATE MOVIES ...");      pstmt.setString(1, "Spinal Tap");      pstmt.executeUpdate();      // ...      utx.commit();      // ...

애플리케이션이 commit()을 호출하면, 트랜잭션 매니저는 2 단계 커밋(two-phase commit) 프로토콜을 사용하여 트랜잭션을 종료한다.

트랜잭션 제어용 JTA 메소드
javax.transaction.UserTransaction 인터페이스는 다음의 트랜잭션 제어 메소드를 제공한다:

  • public void begin()
  • public void commit()
  • public void rollback()
  • public int getStatus()
  • public void setRollbackOnly()
  • public void setTransactionTimeout(int)

JTA 트랜잭션을 시작할 때 이 애플리케이션은 begin()을 호출한다. 트랜잭션을 끝내려면 commit() 또는 rollback()을 호출한다. (참고자료).

JTA와 JDBC 사용하기
개발자들은 DAO 클래스에서 저수준 데이터 작동에 JDBC를 사용하곤 한다. JTA를 이용하여 트랜잭션의 경계를 설정하려면 javax.sql.XADataSource, javax.sql.XAConnection, javax.sql.XAResource 인터페이스를 구현하는 JDBC 드라이버가 필요하다. 이러한 인터페이스를 구현하는 드라이버는 JTA 트랜잭션에 참여할 수 있게된다. XADataSource 객체는 XAConnection 객체용 팩토리이다. XAConnection는 JTA 트랜잭션에 참여하는 JDBC 커넥션이다.

애플리케이션 서버의 관리 툴을 사용하여 XADataSource를 설정해야 한다. 애플리케이션 서버 문서와 JDBC 드라이버 문서를 참조하라.

J2EE 애플리케이션은 JNDI를 사용하여 데이터 소스를 검색한다. 일단 애플리케이션이 데이터 소스 객체에 대한 레퍼런스를 갖게 되면 이것은 javax.sql.DataSource.getConnection()을 호출하여 데이터베이스로의 커넥션을 획득하게 된다.

XA 커넥션은 비 XA 커넥션과는 다르다. XA 커넥션은 JTA 트랜잭션에 참여하고 있다는 것을 언제나 기억하라. XA 커넥션은 JDBC의 자동 커밋 기능을 지원하지 않는다. 또한 이 애플리케이션은 XA 커넥션 상에서 java.sql.Connection.commit() 또는 java.sql.Connection.rollback()을 호출하지 않는다. 대신 UserTransaction.begin(), UserTransaction.commit(), UserTransaction.rollback()을 사용한다.

최상의 접근방법 선택하기
JDBC와 JTA를 이용한 트랜잭션 경계설정에 대해 이야기했다. 각 접근방식 대로 장점이 있기 때문에 자신의 애플리케이션에 가장 알맞는 것을 선택해야 한다.

최근 많은 프로젝트에서 우리팀은 JDBC API를 사용하여 DAO 클래스를 구현했다. 이 DAO 클래스는 다음과 같이 요약된다:

  • 트랜잭션 경계설정 코드는 DAO 클래스 안으로 임베딩된다.
  • DAO 클래스는 트랜잭션 경계설정에 JDBC API를 사용한다.
  • 콜러가 트랜잭션 경계를 설정할 방법은 없다.
  • 트랜잭션 범위는 하나의 JDBC Connection으로 제한된다.

JDBC 트랜잭션이 복잡한 엔터프라이즈 애플리케이션에 언제나 적합한 것은 아니다. 트랜잭션이 다중 DAO 또는 다중 데이터페이스로 확장한다면 다음과 같은 구현 전략이 보다 적합하다:

  • 트랜잭션은 JTA로 경계 설정된다.
  • 트랜잭션 경계설정 코드는 DAO와 분리되어 있다.
  • 콜러가 트랜잭션 경계설정을 담당하고 있다.
  • DAO는 글로벌 트랜잭션에 참여한다.

JDBC 접근방법은 간단함이 매력이다. JTA 접근방법은 유연성이 무기이다. 애플리케이션에 따라 구현 방법을 선택해야 한다.

로깅과 DAO
잘 구현된 DAO 클래스는 런타임 작동에 대한 세부사항을 파악하기 위해 로깅(logging)을 사용한다. 예외, 설정 정보, 커넥션 상태, JDBC 드라이버 메타데이터, 쿼리 매개변수 중 어떤 것이든 선택하여 기록해야 한다. 기록은 모든 개발 단계에 유용하다.

로깅 라이브러리 선택하기
많은 개발자들은 단순한 형식의 로깅을 사용한다: System.out.printlnSystem.err.println. Println 문장은 빠르고 편리하지만 완벽한 로깅 시스템은 제공하지 않는다. 표 2는 자바 플랫폼을 위한 로깅 라이브러리이다:

표 2.자바 플랫폼을 위한 로깅 라이브러리

로깅 라이브러리오픈소스여부URL
java.util.loggingNohttp://java.sun.com/j2se/
Jakarta Log4jYeshttp://jakarta.apache.org/log4j/
Jakarta Commons LoggingYeshttp://jakarta.apache.org/commons/logging.html

java.util.logging은 J2SE 1.4 플랫폼을 위한 표준 API이다. Jakarta Log4j가 더 많은 기능성과 유연성을 제공한다는 것에는 많은 개발자들이 동의하고 있다. java.util.logging의 이점 중 하나는 J2SE 1.3과 J2SE 1.4 플랫폼을 지원한다는 것이다.

Jakarta Commons Logging은 java.util.logging 과의 연결 또는 Jakarta Log4j에 사용될 수 있다. Commons Logging은 로깅 추상 레이어로서 애플리케이션을 기저의 로깅 구현에서 고립시킬 수 있다. Commons Logging을 사용하여 설정 파일을 변경하여 기저의 로깅 구현을 바꿀 수 있다. Commons Logging은 Jakarta Struts 1.1과 Jakarta HttpClient 2.0에 사용된다.

로깅 예제
Listing 7은 DAO 클래스에서 Jakarta Commons Logging을 사용하는 방법이다:

Listing 7. Jakarta Commons Logging
import org.apache.commons.logging.*;class DocumentDAOImpl implements DocumentDAO{      static private final Log log = LogFactory.getLog(DocumentDAOImpl.class);      public void deleteDocument(String id)      {          // ...          log.debug("deleting document: " + id);          // ...          try          {              // ... data operations ...          }          catch (SomeException ex)          {              log.error("Unable to delete document", ex);              // ... handle the exception ...  }      }}

로깅은 미션 수행에 중요한 애플리케이션의 중요한 일부이다. DAO에서 오류가 생기면 무엇이 잘못되었는지를 이해할 수 있게끔 최고의 정보를 로그가 제공한다. 로깅과 DAO를 결합하면 디버깅과 문제해결은 확실하다.

DAO에서의 예외 핸들링

DAO 패턴을 구현할 때 다음 사항을 자문해보라:

  • DAO의 퍼블릭 인터페이스의 메소드들이 검사된 예외를 던지는가?
  • 그렇다면 어떤 예외들인가?
  • DAO 구현 클래스에서 예외는 어떻게 처리되는가?

DAO 패턴을 작업하는 과정에서 우리 팀은 예외 핸들링에 대한 가이드라인을 개발했다:

  • DAO 메소드는 의미있는 예외를 던져야한다.

  • DAO 메소드는 java.lang.Exception을 던져서는 안된다. java.lang.Exception은 너무 일반적이다. 근본 문제에 대한 정보를 제공하지 않을 것이다.

  • DAO 메소드는 java.sql.SQLException 메소드를 던져서는 안된다. SQLException은 저급 JDBC 예외이다. DAO는 JDBC를 나머지 애플리케이션에 노출하는 것이 아니라 JDBC를 캡슐화 해야한다.

  • DAO 인터페이스의 메소드들은 콜러가 예외를 처리할 수 있다고 합리적으로 판단될 때 에만 검사된예외를 던져야 한다. 콜러가 예외를 핸들할 수 없다면 검사되지 않은(런타임) 예외를 던질 것을 고려하라.

  • 데이터 액세스 코드가 예외를 잡으면 이를 무시하지 말아라. 잡힌 예외를 무시하는 DAO는 문제해결에 애를 먹는다.

  • 연쇄 예외를 사용하여 저급 예외를 고급 예외로 트랜슬레이팅 한다.

  • 표준 DAO 예외 클래스를 정의하라. Spring Framework (참고자료)은 사전 정의된 DAO 예외 클래스를 제공한다.

구현 예제: MovieDAO
MovieDAO는 이 글에 논의된 모든 기술을 보여주는 DAO이다: 트랜잭션 경계설정, 로깅, 예외 핸들링. 코드는 세 개의 패키지로 나뉜다. (참고자료):

  • daoexamples.exception
  • daoexamples.movie
  • daoexamples.moviedemo

DAO 패턴 구현은 클래스와 인터페이스로 구성된다:

  • daoexamples.movie.MovieDAOFactory
  • daoexamples.movie.MovieDAO
  • daoexamples.movie.MovieDAOImpl
  • daoexamples.movie.MovieDAOImplJTA
  • daoexamples.movie.Movie
  • daoexamples.movie.MovieImpl
  • daoexamples.movie.MovieNotFoundException
  • daoexamples.movie.MovieUtil

MovieDAO 인터페이스는 DAO의 데이터 작동을 정의한다. 이 인터페이스는 다섯 개의 메소드를 갖고 있다:

  • public Movie findMovieById(String id)
  • public java.util.Collection findMoviesByYear(String year)
  • public void deleteMovie(String id)
  • public Movie createMovie(String rating, String year, String, title)
  • public void updateMovie(String id, String rating, String year, String title)

daoexamples.movie 패키지에는 두 개의 MovieDAO 인터페이스 구현이 포함되어 있다. 각 구현은 트랜잭션 경계설정에 다른 접근방식을 사용한다. (표 3):

표 3. MovieDAO 구현

MovieDAOImplMovieDAOImplJTA
MovieDAO 인퍼페이스 구현YesYes
JNDI를 통한 DataSourc 획득YesYes
DataSource에서 java.sql.Connection 객체 획득YesYes
DAO가 트랜잭션을 내부적으로 경계설정하는가?YesNo
JDBC 트랜잭션 사용YesNo
XA DataSource 사용NoYes
JTA 트랜잭션 참여NoYes

MovieDAO 데모 애플리케이션
이 데모 애플리케이션 이름은 daoexamples.moviedemo.DemoServlet 이다. DemoServlet은 Movie DAO를 사용하여 무비 데이터를 쿼리 및 업데이트 한다.

Listing 8은 싱글 트랜잭션 에서의 JTA-aware MovieDAO와 Java Message Service의 결합 방법이다.

Listing 8. MovieDAO와 JMS 코드 결합
  UserTransaction utx = MovieUtil.getUserTransaction();  utx.begin();  batman = dao.createMovie("R",      "2008",      "Batman Reloaded");  publisher = new MessagePublisher();  publisher.publishTextMessage("I'll be back");  dao.updateMovie(topgun.getId(),      "PG-13",      topgun.getReleaseYear(),      topgun.getTitle());  dao.deleteMovie(legallyblonde.getId());  utx.commit();

데모를 실행하려면 XA 데이터소스와 비 XA 데이터소스를 애플리케이션 서버에 설정해야 한다. 그런다음 daoexamples.ear 파일을 전개한다.

참고자료

Posted by tornado
|