본문 바로가기

Dev.../플밍 관련 자료

[펌] 오픈 소스 자바 프로젝트를 응용한 웹 어플리케이션개발

출처 -
[struts velocity eclipse hibernate jsp xDoclet java] 카페

오픈 소스 자바 프로젝트를 응용한 웹 어플리케이션개발

-마크 이글(Mark Eagle)


04/07/2004

자바로 어느 정도 규모의 웹 어플리케이션을 개발하는 일은 보통 일이 아니다. 어플리케이션이 개발될 아키텍처를 만드는 데에는 무수히 많은 고려할 사항이 있기 때문이다. 높은 레벨에서 살펴보면 개발자들은 사용자 인터페이스를 어떻게 제작할 것인지, 비즈니스 로직은 어디쯤에 위치할 것인지, 그리고 어플리케이션 데이터를 어떻게 퍼시스턴스 상태로(물리적 저장 공간에 저장하는 것) 유지할것인지에 대한 결정에 직면하게 된다. 이 각각의 계층들은 그 각자의 계층들만으로도 해결되어야 할 문제들을 가지고 있다. 그것들을 질문으로 표현하면 다음과 같다. 각각의 계층을 연결하는 데 있어 어떤 기술이 사용될 것인가? 변화에 유연하게 대처하기 위하여 어떻게 하면 각각의 계층들간의 결합도를 약하게(원문:loosely coupled) 할 수 있을 것인가? 그 구조가 각각의 계층들에 변화를 주지 않고 특정 계층을 교체할 만큼 유연할 수 있을까? 컨테이너 레벨의 서비스들을 트랜잭션으로 처리하기 위해서는 어떻게 해야 할 것인가?

당신의 웹 어플리케이션 아키텍쳐를 생성하는 데 있어 나열되어야 할 질문들에는 한도 끝도 없다. 다행히도 그런 과정에 있어 반복적으로 재기되는 문제들의 해결을 위해 일해온 많은 개발자들이 있다. 좋은 프레임웍은 복잡한 문제들에 대한 부품들을 재개발해야하는 문제로부터 개발자들을 좀 더 편안하게 해 준다. 좋은 프레임웍의 요건들은 다음과 같다 : 내부 커스터마이징에 있어 유연해야 한다. 그리고 그 프레임웍을 지원하는 강력한 유저 커뮤니티가 있어야 한다. 각각의 프레임웍들은 일반적으로 한 가지 문제에 대한 효과적인 해결책을 제시해 준다. 그렇지만 당신의 어프리케이션은 그 어플리케이션만의 프레임웍이 필요로하는 여러 층의 계층이 존재한다. 단지 UI 문제를 해결한다는 것은 UI컴포넌트에다 비즈니스 로직과 퍼시스턴스 로직(persistence logic)을 집어넣어 개발하는 것을 의미하는 것은 아니다. 예를 들면,  당신은 JDBC코드를 포함한 비즈니스 로직을 컨트롤러에다 집어넣으면 안되는데, 왜냐하면 이것은 컨트롤러가 제공하려는 기능에 맞지 않기 ??문이다. UI 컨트롤러는 다른 어플리케이션 계층을 UI 계층의 요청으로부터 분리하는 가벼운 컴포넌트로써 그 의미를 가지기 ??문이다. 좋은 프레임웍은 자연스럽게 어떤 코드가 어디에 위치해야하는지의 가이드라인을 형성하게 된다. 더 중요한 점은, 프레임웍은 개발자로 하여금 동일한 패턴의 코드를 계속적으로 제작하는 작업을 줄여 주며 훨씬 더 중요한 어플리케이션 로직 자체에 집중할 수 있는 여유를 준다는 점이다.

 

이 글은 약한 결합도(loose coupling)를 가진 아키텍처를 구성하기 위해 잘 알려지 몇몇의 프레임웍을 결합하는 법에 대해 기술하고 있다. 그러기 위하여 어떻게 아키텍처를 설계하는지, 그리고 각각의 어플리케이션 계층간의 디자인을 일관되게 유지하는 방법에 대해 알아볼 것이다. 프리젠테이션 계층에는 스트럿츠(Struts), 비즈니스 계층에는 스프링(Spring), 퍼시스턴스 계층에는 하이버네이트(Hibernate)를 사용할 것이다. 물론 각각의 계층에 사용된 프레임웍을 다른 것으로 교체해서 사용할 수도 있다. 그림 1은 이 프레임웍들이 결합되었을 때 어떤 모양을 이루는지를 보여준다.

Figure 1
그림 1. 스트럿츠, 스프링, 하이버네이트로 구성된 아키텍처의 개략적 구조

어플리케이션 계층화

대부분의 중,대규모 웹 어플리케이션들은 많은 경우에 있어 4개의 계층으로 구성된다 이 계층들은 프리젠테이션, 퍼시스턴스 비즈니스, 도메인 모델(Domain model) 계층들이다. 각각의 계층들은 어플리케이션에서 명확히 구별되는 기능을 가지고 있으며 서로 다른 계층을 침범하거나 그 기능적인 면에 있어 중복되는 점이 없어야 한다. 각각의 어플리케이션 계층들은 다른 계층들과  완전히 분리되어 있지만 각 계층들간의 통신을 위하여 인터페이스를 제공한다. 각각의 계층들을 개괄적으로 훑어보고 각 계층들이 제공해야 할 것이 무엇인지, 제공하지 말아야 할 것이 무엇인지를 알아보자.

프리젠테이션 계층(표현 계층)

Related Reading

Hibernate: A Developer's Notebook

Hibernate: A Developer's Notebook
By James Elliott

일반적인 웹 어플리케이션의 맨 끝에는 프리젠테이션 계층이 존재한다. 많은 자가 개발자들은 스트럿츠가 제공하는 것이 무엇인지 이해하고 있지만 많은 경우에 있어 비즈니스 로직에 관한 코드를 org.apache.struts.Action 클래스를 확장한 Action에다가 같이 집어넣어 구현하는 우를 범하는 것이 사실이다. 그래서 스트럿츠와 같은 프레임웍이 제공해야 할 것이 무엇인지에 대하여 먼저 생각해 보도록 하자. 여기에 스트럿츠가 제공해야 할 것들을 나열해 보았다.

  • 사용자를 위한 request와 response의 처리
  • 비즈니스 로직과 다른 상위 프로세스로의 호출을 웹 환경으로부터 분리하기 위한 컨트롤러 제공
  • 다른 계층(tier)로부터 스트럿츠 액션으로 던져지는 익셉션들을 처리하는 기능
  • 뷰에 표현될 모델들을 논리적으로 엮는 기능
  • UI 검증(UI validation) 수행

보통 스트럿츠를 사용한 계층 안에  같이 코딩되면 안되는 것을 집어넣는 경우가 있다. 그 예들은 다음과 같다.

  • JDBC호출과 같은 데이터베이스와의 직접적인 통신
  • 비즈니스 로직이나 어플리케이션과 관련된 request 검증
  • 트랜잭션 관리

이런 코드들을 프리젠테이션 계층에다 넣는 것은 각 계층들을 복잡하게 엮이게 만들어 유지보수시 성가신 문제를 발생시킨다.

퍼시스턴스 계층(영구 저장 계층)

웹 어플리케이션의 또 다른 끝 계층은 퍼시스턴스 계층이다. 여기서도 흔히 제어권을 벗어난 일들이 발생하는 경우가 있다. 개발자들은  그들 자신만의 퍼시스턴스 프레임웍을 구축하는 데 드는 비용을 경시하는 경향이 있다. 그 상황에 특수화되고 시스템 안에 심어진 퍼시스턴스 계층은 단지 엄청난 개발 시간을 필요로 할 뿐만 아니라 기능도 부족하고 또한 통제 불가능한 상황이 되는 경우가 종종 있다. 이런 많은 문제에 도움이 되는 오픈 소스 객체-관계 맵핑(object-to-relational mapping, 역자주: 자바의 객체를 RDB의 테이블형태로 맵핑시키는 것을 말함))  - ORM - 프레임웍이 다수 있다.  특별히 하이버네이트(Hibernate) 프레임웍은 객체-관계 퍼시스턴스와 자바를 위한 쿼리 서비스를 제공한다. 하이버네이트는 이미 SQL와 JDBC API에 익숙한 자바 개발자들이 익히기에 중간 정도의 노력을 필요로 한다. 하이버네이트 퍼시스턴스 객체(Hibernate persistent objects)는 일반적인 형태의 자바 객체와 자바 콜렉션에 기반을 두고 있다. 게다가 하이버네이트와 흔히들 사용되는 IDE툴을 같이 사용하는 것은 문제가 되지 않는다.  다음에 나열된 것은 퍼시스턴스 프레임웍에 코딩해야할 것들이다.

  • 관계형 정보를 빼내어 객체화 시키는 작업. 하이버네이트는 이것을 HSQL이라 불리는 OO 쿼리를 쓰거나 criteria API를 써서 작업을 수훨하게 한다. HQL은 테이블명 대신에 객체명, 컬럼명 대신에 객체의 필드명을 사용한다는 것만 빼고는 SQL과 아주 유사하다. 몇몇 새로이 익혀야 할 HQL 언어 항목들이 있지만 배우기도 쉽고 HQL 에 관한 문서도 많기 때문에 걱정할 거리가 안 된다.

  • 데이터베이스의 자료를 저장하고, 수정하고, 삭제하는 작업.

  • 하이버네이트와 같은 진보된 객체-관계 맵핑 프레임웍은 대부분의 SQL 데이터베이스를 지원한다. 또한 객체의 부모/자식 관계, 트랜젝션, 상속,  다형성등을 지원한다.

퍼시스턴스 계층에 넣지 말아야 할 것들을 살펴보자.

  • 비즈니스 로직은 더 높은 계층에 포함되어야 한다. 퍼시스턴스 계층엔 단지 데이터 억세스에 관련된 작업만 허용된다..

  • 프리젠테이션 로직과 퍼시스턴스 로직을 뒤섞어서 코딩하면 안 된다. JSP나 서블릿 같은 프리젠테이션 컴포넌트에다 직접적으로 데이터 억세스를 수행하는 코드를 넣지 말아야 한다. 퍼시스턴스 로직을 퍼시스턴스 계층 안에만 한정시킴으로써 어플리케이션은 다른 계층의 변화에 구애받지 않고 퍼시스턴스 계층을 변화시킬 수 있는 유연함을 지니게 된다. 예를 들면 하이버네이트는 다른 계층의 어떤 코드도 바꾸지 않고 다른 퍼시스턴스 프레임웍으로 교체될 수 있다.

비즈니스 계층

일반적인 웹 어플리케이션의 중간 계층에는 비즈니스나 서비스 계층이 존재한다. 코딩에 대한 관점으로 볼 때 이런 서비스 계층은 대부분의 경우에 있어 무시되는 경우가 많다.  비즈니스 계층의 코드들이 UI계층이나 퍼시스턴스 계층에 여기저기 흩어져 있는 어플리케이션을 찾아보기란 그리 어려운 일이 아니다. 이런 구조는 어플리케이션의 시스템이 스파게티처럼 이리저리 뒤섞여 각각의 계층을 모호하게 만들어 유지보수시 많은 시간을 필요로 하게 만든다. 다행히도 이런 문제를 해결하기 위한 몇몇 프레임웍이 있다. 가장 유명한 두가지가 스프링(Spring) 프레임웍과 피코컨테이너(PicoContainer) 프레임웍이다. 이것들은 어떻게 객체들을 엮는지를 결정해주는 조그만 마이크로컨테이너(microcontainer)라고 불리기도 한다. 이런 프레임웍들은 연관성 삽입(dependency injection, inversion of control)이라는 간단한 개념 위에서 동작한다. 이제부터는 스프링 프레임웍에서 빈의 프로퍼티를 동적으로 세팅하는 기능, 즉 설정 파일에서 그 이름을 변경할 수 있는 빈의 프로퍼티의 삽입 기능에 촛점을 맞추어 얘기할 것이다. 스프링은 빈의 setter를 사용한 빈의 프로퍼티 동적 삽입 방식 외에도 생성자를 사용한 방법 또한 지원한다.(역자주: 지금은 무슨 말인지 잘 이해 안가셔도 계속 읽다 보면 이해가 가실 겁니다.^^). 객체들은 트랜잭션 관리자, 객체 Factory, 비즈니스 로직을 담고 있는 서비스 객체, 그리고 퍼시스턴스 계층에 속하는(data access object, DAO) 객체의 레퍼런스를 명시하고 있는 간략한 XML파일에 의하여 그 구조를 형성하게 된다.  그리고 스프링이 사용하고 있는 개념들은 예제를 통하여 훨씬 더 쉽게 알아볼 수 있을 것이다.

그리고 비즈니스 계층은 다음과 같은 요건을 가져야 한다.

  • 어플리케이션 비즈니스 로직 처리와 비즈니스에 관련된 빈의 적합성 검증
  • 트랜잭션 처리
  • 다른 계층들과 통신하기 위한 인터페이스 제공
  • 비즈니스 레벨에 있는 객체들간의 관계를 관리
  • 프리젠테이션 계층과 퍼시스턴스 계층 사이의 다리 역할을 해 그 둘이 직접 통신하지 않도록 함으로써 어플리케이션에 유연성을 더함.
  • 비즈니스 서비스를 얻기 위하여 프리젠테이션 계층으로부터 컨텍스트 객체를 비즈니스 계층으로 넘겨주는 기능
  • 비즈니스 로직과 퍼시스턴스 계층 사이의 실제 구현 관리

도메인 모델 계층

마지막으로 우리는 중대규모의 웹 어플리케이션을 얘기하고 있기 때문에 도메인 객체 계층은 Order, OrderLineItem, Product 객체들과 같은 실제 비즈니스 객체를 표현하게 된다. 이 계층은 개발자가 도메인 객체와 일치시키기 위해 불필요한 데이터 전송용 객체(data transfer objects, DTO) 를 제작하는 것을 막아 준다.  예를 들어 하이버네이트는 데이터베이스의 정보를 도메인 객체에 저장하여 UI 에 표현할 수 있도록 해 준다. 그리고 이 객체들은 수정되거나 하여 다시 퍼시스턴스 계층, 하이버네이트로 돌아와 데이터베이스의 정보를 업데이트하는 데 사용된다.  그리고 여러 계층을 통과하면서 계층들간의 통신시 데이터 전송용 객체(DTO)를 사용하였을 때 발생할 수 있는 데이터의 유실 때문에 데이터 전송용 객체를 사용하지 말 것을 권한다. 이 모델은 자바 개발자들이 별다른 코딩 없이도 객체지향적 방식으로 일할 수 있도록 도와 준다.

간단한 샘플 예제를 통한 각 계층 엮어 보기

이제 고수준 관점에서 각각의 계층을 알아보았으니 실제로 적용해 보도록 하자. 다시 얘기하지만 이 예제에서 스트럿츠, 스프링, 그리고 하이버네이트 프레임웍을 사용할 것이다. 각각의 프레임웍에 대해서 자세히 다루기에는 이 글 하나로는 너무도 부족하고 단지 여기서는 각각의 프레임웍을 어떻게 엮어서 사용하는지를 살펴보기로 한다. 샘플 어플리케이션은 사용자 요청이 각각의 계층을 어떻게 통과하는지를 보여 줄 것이다. 이 샘플 어플리케이션의 사용자는 새로운 주문(Order)를 데이터베이스에 저장하고 이미 데이터베이스에 존재하는 주문(Order)를 살펴보는 기능을 사용하게 될 것이다. 게다가 보강된 버젼에서는 주문(Order)에 대한 수정과 삭제도 지원하게 될 것이다.

이 샘플 어플리케이션의 소스 코드는 여기서 다운받을 수 있다. 

 

샘플 어플리케이션을 제작할 때 우리는 먼저 각각의 계층들이 상호협동하여 동작하기 위해 필요한 도메인 객체들을 생성할 것이다. 이 도메인 객체들은 도대체 어떤 데이터가 DB에 저장될 것인지, 그리고 어떤 비즈니스 로직이 필요할 것인지, 그리고 그 객체들을 보여주기 위해서는 어떠한 화면들이 필요할 것인지를 우리가 정의하는 데 있어 기초가 된다.

다음으로 우리는 퍼시스턴스 계층들 세팅할 것이며 하이버데이트를 통하여 OR 매핑 규칙을 정할 것이다.

그 다음으로는 비즈니스 로직이 포함된 비즈니스 객체를 정의한다.

그 다음으로는 스프링을 사용하여 이 두 개의 계층, 즉 퍼시스턴스 계층과 비즈니스 계층을 역을 것이다.

마지막으로 스트럿츠를 사용하여 프리젠테이션 계층을 생성할 것이다.

도메인 객체 계층(Domain Object Layer)

이 객체들이야말로 각 계층들간을 자유롭게 오가며 상호교류할 수 있게 하는 객체이므로 코딩을 시작하기에 있어 좋은 것들이다. 이 샘플 도메인 모델은 주문(Order)과 주문 항목(OrderLineItem) 객체들로 이루어져 있다. Order객체는 OrderLine객체에 대하여 일대다 관계를 가지고 있다. 예제 코드는 도메인 객체 계층에 대한 간단한 코드를 포함하고 있다.

  • com.meagle.bo.Order.java: 주문에 대한 헤더 정보를 포함
  • com.meagle.bo.OrderLineItem.java: 주문에 대한 자세한 세부정보를 포함

어플리케이션이 어떻게 계층화되어있는지를 알아보기 쉽게 어플리케이션의 페키지 명을 정하는 것이 좋다. 예를 들면 샘플 어플리케이션에서는 도메인 모델 객체들은 com.meagle.bo 패키지 안에 포함되어 있다. 그리고 더 세부화된 도메인 모델 객체들은 아마도 com.meagle.bo 패키지의 서브 패키지로 생성될 패키지 이래에 포함되게 될 것이다. 비즈니스 로직이 포함된 패키지는 com.meagle.service 로 패키지명이 시작되며 DAO 객체들이 포함된 패키지는 com.meagle.service.dao.hibernate 패키지이다. 프리젠테이션 클래스들은  com.meagle.actioncom.meagle.forms 패키지 아래에 위치하게 된다. 정확한 패키지 네이밍 룰을 적용하게 되면 각 클래스들이 제공하는 기능들에 대한 명확한 구분이 되며 유지 보수시에 상당한 비용 절감의 효과를 기대할 수 있다.

퍼시스턴스 계층 설정

하이버네이트를 가지고 퍼시스턴스 계층을 세팅하려면 몇 가지 과정을 거쳐야 한다. 첫 번째 과정은 영구 저장 매체에 저장될 도메인 모델 객체를 설정하는 일이다. 왜냐하면 하이버네이트는 퍼시스턴스 객체로 사용할 도메인 객체로 POJO(Plain, old Java object) 를 사용하기 때문이다. 따라서 Order와 OrderLine객체는 그 객체들의 필드에 대하여 getter와 setter 메소드들을 가진 빈 형태의 객체여야만 한다.  Order객체는 표준 자바빈 스펙의 객체와 같이 ID, UserName, Total, 그리고 OrderLineItems 프로퍼티에 대한 getter와 setter 메소드들을 가지고 있다.  물론 OrderLine객체 또한 그 객체의 필드들에 대하여 자바빈 스펙을 따른다.

하이버네이트는 도메인 객체를 RDB로 맵핑하는 데 있어서 XML 파일을 사용한다. 우리의 Order와 OrderLineItem 객체에 대하여 각각에 대해 그 맵핑 관계를 설명하는 두 개의 XML파일이 있다. 필요하다면 그 맵핑 XML파일 생성을 도와주는  XDoclet 과 같은 툴을 사용할 수도 있다. 하이버네이트는 도메인 객체 - RDB 맵핑에 있어 다음과 같은 파일을을 사용한다.

  • Order.hbm.xml
  • OrderLineItem.hbm.xml

예제 어플리케이션에서 이렇게 생성된 파일을 WebContent/WEB-INF/classes/com/meagle/bo 디렉토리에서 찾을 수 있다. 하이버네이트 SessionFactory 는 그것이 어떤 데이터베이스와 통신할 것인가, 그리고 어떤 DataSource나 데이터베이스 커넥션 풀을 사용할 것인가, 그리고 퍼시스턴스 대상이 되는 퍼시스턴스 객체들에는 어떤 것들이 있는가를 설정함으로써 설정이 완료된다.  Session 객체는 SessionFactory객체에 의해 생성되는데, 그것은 자바 객체와 퍼시스턴스 계층의 기능들의 호출(selecting, saving, updating, deleting) 사이의 인터페이스를 제공해주는 객체이다. 우리는 뒤에 이어지는 섹션에서 하이버네이트가 Session객체를 생성하기 위해 어떻게 SessionFactory객체를 설정해야 하는지를 살펴볼 것이다.

비즈니스 계층 설정

이제 우리는 어플리케이션 로직을 수행하고 퍼시스턴스 계층의 작업을 수행하고 UI 계층으로부터 request를 받고 트랜젝션 서비스를 적용할 대상 도메인 객체들을 수중에 넣게 되었다.  이것들을 엮는 작업과 차후 관리를 좀 더 편하게 하기 위해서 스프링 프레임웍의 빈 관리 기능을 사용하면 된다. 스프링은 제어 역행화(inversion of control , IoC 역자주 : 마틴 파울러의 '리팩토링' 에 이 패턴에 대한 설명이 나옵니다. 한번 읽어보시는 것도 괜찮을 듯) 나 연관 삽입(setter dependency injection)을 사용하여 외부 XML파일에 기술된 대로 객체들을 엮어 준다. 제어 역행화(Inversion of control)는 어떤 객체로 하여금 더 고수준의 레벨에서 생성되는 객체들을 받아들여 사용하게 할 수 있는 간단한 개념이다. 이런 방법을 사용함으로써 어떤 객체의 인스턴스가 생성되어 사용될 것인지로부터, 그리고 객체들간의 연관성으로부터 객체들이 더 자유로워질 수 있다.

여기에 IoC를 사용치 않아서 객체들간의 연관성이 심화된 상태의 객체 관계에 대한 그림을 예로 들어 보았다.( 역자주: 그림에서 보듯이 객체 A가 B,C를 생성시키므로 A의 구현은 B와 C에 의존하게 됩니다.그림 3과 비교해서 보시면 됩니다.)

Figure 2
그림 2. IoC를 적용하지 않은 객체 관계. 객체 A가 객체 B와 C를 생성시키는 것을 볼 수 있다..

 

그리고 여기에는 고수준 레벨에서 생성된 객체들이 삽입되어 사용될 수 있도록 IoC패턴을 적용한 예제 그림이 있다. 그럼으로써 객체 A는 객체 B와 C의 구현을 직접적으로 사용할 수가 있다.

Figure 3
그림 3. IoC가 적용된 객체 관게. 객체 A는 객체 B와 C의 인터페이스를 받아들이는 setter 매소드를 가지고 있다. 물론 받아들이는 과정은 setter메소드를 통해서뿐만 아니라 객체 A의 생성자를 통해서도 가능하다.

비즈니스 서비스 객체 작성

비즈니스 객체의 setter에 실제 구현 클래스 대신 인터페이스를 사용하는 것은 실제로 구현될 버젼의 클래스를 비즈니스 객체로부터 분리시키는 역할을 한다.  예제에서는 도메인 객체들의 퍼시스턴스 작업을 처리하기 위한 DAO 객체를 서비스 객체가 받아서 처리하도록 할 것이다. 예제에서 다른 퍼시스턴스 프레임웍으로의 전환이 용이한 하이버네이트를 사용할 것이며 퍼시스턴스 프레임웍의 변화에 대해서는 단지 스프링의 설정을 새로운 DAO 객체 구현을 사용하도록 변경하기만 하면 된다. 예제에서 또한 퍼시스턴스 구조로부터 비즈니스 로직을 분리시키기 위하여 인터페이스와 연관성 삽입(dependency injection)을 사용하는 것을 볼 수 있을 것이다.

여기에 DAO객체 연관에 있어 기초가 되는 비즈니스 서비스 객체 코드가 있다.:

public interface IOrderService { public abstract Order saveNewOrder(Order order) throws OrderException, OrderMinimumAmountException; public abstract List findOrderByUser( String user) throws OrderException; public abstract Order findOrderById(int id) throws OrderException; public abstract void setOrderDAO( IOrderDAO orderDAO); }

위 예제 코드에 DAO 객체에 대한 setter메소드가 존재함을 주목하라. getOrderDAO 메소드는 없는데 한번 서비스 객체와 결합된 DAO 객체가 외부에서 참조될 일은 없기 때문에 DAO 객체에 대한 getter메소드를 생성하지 않은 것이다. DAO 객체는 우리의 퍼시스턴스 계층과 통신하이 위하여 사용될 것이다. 우리는 스프링을 사용하여 비즈니스 객체와 DAO 객체를 결합시킬 것이다. 그리고 인터페이스를 사용하였기 때문에 실제 구현을 제한되지 않는다.

다음 단계는 DAO 객체에 대한 실제 구현 객체를 작성하는 일이다. Spring은 자체 내장된 하이버네이트 지원 툴을 지니고 있기 때문에 이번 예제에서는 스프링의 하이버네이트 지원 DAO 클래스인 HibernateDaoSupport 를 확장하여 DAO 클래스를 작성하게 된다. HibernateDaoSupport 클래스를 확장하게 되면 HibernateTemplate 객체에 대한 레퍼런스를 얻어서 사용할 수가 있게 된다.   HibernateTemplate객체를 통하면 하이버네이트 세션과 HibernateExceptions을 처리할 수가 있게 된다. 여기에 DAO 객체를 위한 인터페이스가 있다.:

public interface IOrderDAO { public abstract Order findOrderById( final int id); public abstract List findOrdersPlaceByUser( final String placedBy); public abstract Order saveOrder( final Order order); }

아직도 비즈니스 계층을 위하여 결합되어야 할 몇몇개의 클래스들이 남아 있다. 그중에 HibernateSessionFactory 객체와 TransactionManager 객체가 있는데 이것은 스프링 설정 파일을 통하여 비즈니스 계층에 결합될 수가 있다.  스프링은 HibernateTransactionManager 을 제공하여 스레드에 바인드된 하이버네이트 세션의 트랜잭션 기능을 쉽게 사용할 수 있도록 해 준다. (더 자세히 알고 싶으면 ThreadLocal 를 참고하라.). 여기에 HibernateSessionFactoryHibernateTransactionManager 를 설정하기 위한 Spring 설정 파일의 한 부분이 있다.:

<bean id="mySessionFactory" class="org.springframework.orm.hibernate. LocalSessionFactoryBean"> <property name="mappingResources"> <list> <value> com/meagle/bo/Order.hbm.xml </value> <value> com/meagle/bo/OrderLineItem.hbm.xml </value> </list> </property> <property name="hibernateProperties"> <props> <prop key="hibernate.dialect"> net.sf.hibernate.dialect.MySQLDialect </prop> <prop key="hibernate.show_sql"> false </prop> <prop key="hibernate.proxool.xml"> C:/MyWebApps/.../WEB-INF/proxool.xml </prop> <prop key="hibernate.proxool.pool_alias"> spring </prop> </props> </property> </bean> <!-- 단지 Hibernate SessionFactory 만 사용할 때의 트랜잭션 매니져 --> <bean id="myTransactionManager" class="org. springframework. orm. hibernate. HibernateTransactionManager"> <property name="sessionFactory"> <ref local="mySessionFactory"/> </property> </bean>

각각의 객체들은 스프링 설정 파일의 <bean> 태그 안에서 레퍼런싱 될 수가 있다. 위 예제에서 mySessionFactory빈은 HibernateSessionFactory 타입의 객체를 나타내고 myTransactionManager빈은 하이버네이트 트랜잭션 매니저를 나타낸다. transactionManger 빈이 그 프로퍼티로써 sessionFactory를 가지고 있음을 유의해서 보라. transactionManger 의 클래스인 HibernateTransactionManager 클래스는 sessionFactory에 대한 setter메소드와 getter메소드를 가지고 있다. setter와 getter를 가짐으로 인해서 스프링 프레임웍 컨테이너가 시작될 때 설정 파일에 설정된 대로 setter를 호출해서 실제 구현 객체를 HibernateTransactionManager에 집어 넣어 transactionManger 객체를 생성하게 된다. 이런 흐름을 바로 연관성 삽입(dependency injection)이라고 부른다. sessionFactory 프로퍼티에  mySessionFactory 빈이 삽입되게 되는 것이다. 이 두 객체들은 스프링이 초기화될 때에 setter메소드를 사용하여 결합된다.  이런 결합 방식은 싱글톤 객체를 생성하고 그러한 객체들의 생성에 사용되는 Factory를 작성하는 수고를 덜어 준다. mySessionFactory 빈은 두개의 로퍼티(mappingResources,hibernateProperte)를 가지고 있다. 보통 스프링을 사용하지 않고 하이버네이트를 단독으로 사용할 ? 이런 프로퍼티에 대한 설정은 hibernate.cfg.xml 에 저장된다. 그렇지만 스프링은 스프링 설정 파일에다 하이버네이트 설정을 결합시킬 수 있게 함으로써 좀 더 설정을 쉽게 할 수 있게 한다. 좀 더 많은 정보를 원한다면  Spring API 문서를 참고하라.

비즈니스 서비스 객체와 DAO 객체를 결합시키기 위해 필요한 컨테이너 서비스 빈의 설정을 완료했다. 그러면 여기에다가 트랜잭션 매니져 객체만 결합시키면 된다.

여기에 완성된 스프링 설정 파일이 있다.:

<!-- ORDER SERVICE --> <bean id="orderService" class="org. springframework. transaction. interceptor. TransactionProxyFactoryBean"> <property name="transactionManager"> <ref local="myTransactionManager"/> </property> <property name="target"> <ref local="orderTarget"/> </property> <property name="transactionAttributes"> <props> <prop key="find*"> PROPAGATION_REQUIRED,readOnly,-OrderException </prop> <prop key="save*"> PROPAGATION_REQUIRED,-OrderException </prop> </props> </property> </bean> <!-- ORDER TARGET PRIMARY BUSINESS OBJECT: Hibernate implementation --> <bean id="orderTarget" class="com. meagle. service. spring. OrderServiceSpringImpl"> <property name="orderDAO"> <ref local="orderDAO"/> </property> </bean> <!-- ORDER DAO OBJECT --> <bean id="orderDAO" class="com. meagle. service. dao. hibernate. OrderHibernateDAO"> <property name="sessionFactory"> <ref local="mySessionFactory"/> </property> </bean>

그림 4는 우리가 결합시켰던 것들을 개괄적으로 보여준다. 이 그림은 스프링에 의해 각각의 객체들이 어떻게 연관되어 있고 다른 객체에 어떻게 setting되는지를 보여준다. 예제 어플리케이션의 스프링 설정파일과 이 그림에서의 연관 관계를 같이 참고하면서 보면 이해가 빠를 것이다.

Figure 4
그림 4. 스프링이 설정 파일에 기초하여 빈들을 결합시키는 것을 보여주고 있다.

이 예제는 우리가 이전에 정의했던 트랜잭션 매니져에 대한 setter메소드를 가지고 있는  TransactionProxyFactoryBean을 사용하고 있다. TransactionProxyFactoryBean는 선언자를 사용한 트랜잭션 처리와 서비스객체를 다루는 기능을 갖고 있어 사용하기 편리하다. TransactionProxyFactoryBean을 사용하게 되면 트랜잭션이 적용될 메소드의 패턴을 정의하고 어떤 트랜잭션 룰이 그 메소드 패턴에 적용될 것인지를 결정하는 transactionAttributes프로퍼티를 통하여 어떻게 트랜잭션이 처리될 것인지를 정의할 수가 있다. 설정 격리 레벨와 트랜잭션 커밋,롤백에 대한 더 자세한 정보를 얻기를 원하면 TransactionAttributeEditor 를 참고하라.

TransactionProxyFactoryBean 클래스는 target프로퍼티에 대한 setter메소드를 가지고 있는데, target 프로퍼티는 orderTarget이라고 불리는 우리가 정의하는 비즈니스 서비스 객체가 레퍼런싱된다. orderTarget 빈은 어떤 비즈니스 서비스 클래스가 사용될 것인를 결정한다. 그리고 그것은 setOrderDAO() 같은 setter메소드를 통해 접근될 수 있는 프로퍼티를 가지고 있게 된다. 이 프로퍼티에는 우리의 퍼시스턴스 계층과 통신하는 orderDAO객체가 세팅된다.

스프링과 빈에 대해 알아야 할 한가지 사항이 더 있는데 그것은 빈은 두 가지 모드로 작동한다는 것이다. 이것은 싱글톤과 프로토타입으로써 정의된다. 기본 모드는 모든 곳에서 동일한 객체의 레퍼런스를 가지고 작업하게 되는 싱글톤 모드이다. 이것은 무상태 세션 빈(EJB에서의)이 제공하는 것과 같은 무상태 작업을 요구할 ??에 사용된다. 그리고 프로토타입 모드는 스프링 설정파일을 사용하여 결합되는 객체를 스프링에 요구할 때마다 새로운 인스턴스를 만들어 돌려주는 방식이다. 각각의 사용자가 작업 처리를 위해 각각의 객체 인스턴스를 필요로 하면 이 모드를 사용해야 한다.

서비스 로케이터(Service Locator) 제공

서비스 객체들이 DAO 객체들과 연결되어 이제 서비스 계층을 다른 계층과 결합할 수가 있게 되었다. 아래에서 보게 될 코드들은 일반적으로 스트럿츠나 스윙을 사용한 UI 계층에서 보게 될 코드들이다. 서비스 로케이터를 사용하여 스프링 컨텍스트에서 자원들을 빼내어 사용하는 것을 좀 더 쉽게 만들 수 있다. 물론 bean의 ID를 사용하여 스프링으로부터 직접 요청하여 자원을 빼내어 사용할 수도 있다.

여기에 스트럿츠 액션에 서비스 로케이터를 사용하는 예가 있다.(orderService라는 Action클래스의 Property로 서비스가 세팅된다.)

public abstract class BaseAction extends Action { private IOrderService orderService; public void setServlet(ActionServlet actionServlet) { super.setServlet(actionServlet); ServletContext servletContext = actionServlet.getServletContext(); WebApplicationContext wac = WebApplicationContextUtils. getRequiredWebApplicationContext( servletContext); this.orderService = (IOrderService) wac.getBean("orderService"); } protected IOrderService getOrderService() { return orderService; } }

UI 계층 설정

이번 예제에서 UI 계층 프레임웍으로 스트럿츠를 사용한다. 여기서 어플리케이션 계층화에 있어 스트럿츠와 관련된 내용에 대해 이야기해 볼 것이다. struts-config.xml 파일의 Action 설정을 살펴봄으로 시작해 보자.

<action path="/SaveNewOrder" type="com.meagle.action.SaveOrderAction" name="OrderForm" scope="request" validate="true" input="/NewOrder.jsp"> <display-name>Save New Order</display-name> <exception key="error.order.save" path="/NewOrder.jsp" scope="request" type="com.meagle.exception.OrderException"/> <exception key="error.order.not.enough.money" path="/NewOrder.jsp" scope="request" type="com. meagle. exception. OrderMinimumAmountException"/> <forward name="success" path="/ViewOrder.jsp"/> <forward name="failure" path="/NewOrder.jsp"/> </action>

SaveNewOrder 액션은 UI 계층에서 사용자가 전송한 order를 퍼시스턴스 상태로 저장하기 위하여 사용된다. 일반적인 스트럿츠 액션이라고 보여지나 이 액션에 대한 exception 설정은 주목해서 볼 필요가 있다. 이 exception들은 역시 applicationContext-hibernate.xml 같은 비즈니스 서비스 객체 생성을 위한 transactionAttributes 등의 프로퍼티를 설정하는 스프링 설정 파일에도 설정된다. 비즈니스 계층에서 이러한 예외가 발생하여 UI계층으로 그 예외를 돌려주면 우리는 UI계층에서 그것들을 적절히 처리할 수 있다. 첫번째 예외인 OrderException 은 order객체를 퍼시스턴스 계층에 저장시 실패할 때에 이 액션 객체에 의해 사용될 것이다. 이것은 트랜잭션을 rollback시키며 예외를 비즈니스 계층에서 UI계층, 즉 스트럿츠로 보낼 것이다. OrderMinimumAmountException 은 최소 order 총량에 맞지 않을 때 비즈니스 로직상에서 발생하며 역시 트랜잭션 레벨에서 처리된다. 이 예외가 발생하게 되면 또한 트랜잭션 rollbackwill 이 발생하게 되며 이 예외를 받게 될 UI 계층에서 알맞은 처리가 이루어질 것이다.

마지막 결합 과정은 프리젠테이션 계층과 비즈니스 계층을 엮는 것이다. 이것은 이전에 설명했던 서비스 로케이터를 사용함으로써 이루어진다. 서비스 계층은 우리의 비즈니스 계층과 퍼시스턴스 계층에 대한 인터페이스로써 동작하게 된다. 여기에 SaveNewOrder 액션이 비즈니스 메소드를 호출하기 위해 서비스 로케이터를 사용하는 것을 보여주고 있다.

public ActionForward execute( ActionMapping mapping, ActionForm form, javax.servlet.http.HttpServletRequest request, javax.servlet.http.HttpServletResponse response) throws java.lang.Exception { OrderForm oForm = (OrderForm) form; // 퍼시스턴스 계층에 저장될 Order객체를 생성하기 // 위해 폼을 사용한다. // 샘플 어플리케이션의 풀 소스를 참고하라. // 결합된 비즈니스 객체를 BaseAction에서 생성된 // 서비스 로케이터로부터 얻어 사용하면 된다. // Order 객체를 저장하기 위한 서비스 계층의 작업과 // 더 윗단에 있는 작업을 분리하고 있다. getOrderService().saveNewOrder(order); oForm.setOrder(order); ActionMessages messages = new ActionMessages(); messages.add( ActionMessages.GLOBAL_MESSAGE, new ActionMessage( "message.order.saved.successfully")); saveMessages(request, messages); return mapping.findForward("success"); }

결론

이 글은 기술과 구조의 측면에서 상당히 많은 넓은 부분을 다루고 있다. 기본 개념은 이렇다. 더 좋은 어플리케이션을 만들기 위해서는 유저 인터페이스 계층과 퍼시스턴스 계층과 또 다른 계층들을 분리해야만 한다는 것이다. 그런 것을 잘 하는 것은 코드량을 줄일 수가 있고 새 컴포넌트의 추가를 용이하게 만들며 차후에 있게 될 유지보수를 쉽게 만든다. 여기서 다루어진 기술들은 특별한 분야에 대한 문제를 잘 해결해 줄 수도 있다. 하지만,  이런 구조를 사용함으로써 각각의 어플리케이션 계층에 다른 기술을 사용할 수도 있다. 예를 들면 당신은 퍼시스턴스 계층의 구현을 위해 하이버네이트 프레임웍을 사용하고 싶지 않을지도 모른다. DAO 객체 구현에 있어 인터페이스를 사용하여 코딩하고 있기 때문에 거기에 다른 기술이나 프레임웍( iBATIS 등) 을 사용하는 법은 어떻게 보면 명백하리만큼 쉬운 것인지도 모른다. 아니면 UI 계층을 스트럿츠 말고 다른 프레임웍으로 교체하고 싶을지도 모른다. 단순히 UI 계층의 구현을 변경하는 것은 비즈니스 로직이나 퍼시스턴스 계층에 영향을 미치지 않는 것이 우리가 지금까지 설명했던 구조의 특징이다.  역시 퍼시스턴스 계층의 변경은 UI 계층이나 서비스 계층에 영향을 미치지 않을 것이다.  웹 어플리케이션을 제작한다는 것은 쉬운 일이 아니다. 하지만 어플리케이션의 각 계층들을 분리하여 제작하고 적절한 프레임웍을 사용하여 결합함으로써 어플리케이션을 제작하는 것이 그 쉽지 않은 작업들을 좀 더 편하게 만들어줄 수 있다.

마크 이글 은 Atlanta, GA.소재의 MATRIX Resources 의 선임 소프트뤠어 연구원으로 일하고 있다.