메뉴 바로가기 검색 및 카테고리 바로가기 본문 바로가기

한빛출판네트워크

IT/모바일

Spring으로 마이그레이션(Migration)(1)

한빛미디어

|

2007-05-14

|

by HANBIT

10,925

제공 : 한빛 네트워크
저자 : Ethan McCallum
역자 : 박찬욱
원문 : Migrating to Spring

나를 파티에 늦게 불러줘요. 그 파티는 Spring 프레임워크 파티 입니다. 늦게 참석하는 친구들을 위해서 Spring을 소개하자면, Spring은 Apache 2.0 라이센스 하에 배포되는 인프라스트럭처 코드의 라이브러리입니다. Spring의 핵심은 Inversion-of-Control 컨테이너이며, 개발팀은 추가적으로 반복적으로 사용되는 JDBC와 JMS 코드, 웹 MVC 프레임워크 등을 위한 템플릿을 만들어 두었습니다.

Spring의 성숙도와 유명세에도 불구하고, Spring 사용을 시도해보는 데에는 시간이 걸리기 때문에 나는 내가 파티에 늦을 거라고 말했습니다. 내 질문은 “Spring이 날 위해 무엇을 해줄 수 있는가?”입니다. 답을 찾기 위해서 나는 지금 참조하는 애플리케이션의 내용(gut)을 Spring 컴포넌트로 교체 했습니다. 오래 전에 Spring을 처음 사용하기 시작했을 때 느낀 점은 Spring이 제공하는 커스텀 헬퍼 코드 때문에 애플리케이션의 코드가 이전보다 훨씬 더 깔끔해졌고, 디버그와 확장이 더 쉬워졌고, 더 가벼워졌다는 점을 들 수 있습니다.

이번 글에서는 실험을 통해 내 생각과 내가 발견한 것을 공유할 것입니다. 특별히 참조하고 있는 애플리케이션의 싱글톤 레지스트리, JDBC 코드와 웹의 앞 뒤 레이어를 어떻게 Spring 컴포넌트로 변경했는지 설명할 것입니다. 또 한 가지 문제점을 묘사하고 어떻게 내가 그 문제점에 맞서고 해결했는지 설명할 것입니다.

다음을 따라 하기 위해서 Spring 전문 자료가 필요지만, 그래도 나중을 위해서 Spring 참고 자료의 링크를 제공할 것 입니다.

샘플 코드는 Spring 1.2.x와 유닉스에서 썬의 JDK 1.5.0_07으로 테스트했습니다.

기존 코드

테스트 대상이 되는 애플리케이션 제품을 위해 새로운 실험을 원하지 않기 때문에, 내가 이전에 썼던 다른 글 http://today.java.net/pub/a/today/2005/11/17/app-managed-datasources-with-commons-dbcp.html 에서 사용했던 테스트 애플리케이션을 가져왔습니다. 테스트 애플리케이션은 진입점에서 두 개의 서블릿 페이지 컨트롤러를 갖는 간단한 자바 웹 애플리케이션입니다. 이 서블릿은 데이터 접근 객체(Data Access Object : DAO)를 사용해서 데이터베이스에 접근하며, 로컬 데이터 소스에서 데이터베이스 커넥션을 가져옵니다. 관련된 객체는 다른 객체를 찾아내기 위해서 싱글톤 레지스트리를 호출합니다. 특별히 몇 개를 알아보면 :
SimpleDAO : 객체를 데이터베이스로 전송함
DBUtil : JDBC ResultSet, Connection 들과 작업할 때 필요한 일반적인 작업을 편리하게 해줌
ObjectRegistry : 객체를 찾을 수 있는 싱글톤 레지스트리.
SetupDataSourceContextListener: JDBC 데이터소스 설정.
SetupDBContextListener: (내장되어 있는) 데이터베이스 준비함.
GetDataServlet: 데이터를 보여주기 위한 페이지 컨트롤러
PutDataServlet: 데이터를 저장하기 위한 페이지 컨트롤러
매우 간단한 웹 애플리케이션이지만, 자체적인 컨테이너를 갖고 있으며, N-티어와 같은 더 큰 동작을 나타낼 수 있습니다. 그러므로 이 매우 작은 실험의 결과 일부를 실제 프로젝트로 전환이 가능합니다.

내부 구조 변경하기 : Object Registry

자세히 들여볼 첫 번째 클래스는 관련된 객체들 사이의 레이어에 위치해 있는 ObjectRegistry입니다.
ackage pool_test.util ;

public class ObjectRegistry {

  private static ObjectRegistry _instance =
    new ObjectRegistry() ;

  public static ObjectRegistry getInstance(){
    return( _instance ) ;
  }


  private Map _singletons ;

  public void put(
    final String key , final Object obj
  ){
    _singletons.put( key , obj ) ;
  }
  
  public Object get( final String key ){
    return( _singletons.get( key ) ) ;
  }

}
ObjectRegistry는 실제 String : Object의 한 쌍을 갖는 Map입니다. 특정 위치에서 put() 메소드를 사용해 저장소에 객체를 저장할 수 있고, 또 다른 위치에서 get() 메소드를 사용해서 저장한 것과 같은 객체를 받아 올 수 있습니다. 레지스트리를 사용하는 것은 객체의 일반적인 타입(상위 클래스나 인터페이스)과 룩업 키만 알고 있으면 되기 때문에 객체 의존성을 약하게 합니다. 이 특징-구현(implementation), 생성(instantiation), 구성(configuration)-은 객체를 저장하기 위해서 put() 메소드를 호출 할 때 코드에 남게 됩니다.

이 작업을 할 때, 그리고 이전에 작업 했던 것을 보면 완벽하지도 않으면서, 필요한 것 보다 더 큰 작업을 필요로 합니다. put() 메소드를 놓치거나, 잘못된 위치에 놓게 되면 null-pointer 에러나 stack overflow의 원인이 됩니다. 또한 아직 존재하지 않는 객체를 찾는 것을 시도하는지 확인하기 위해서 레지스트리에 저장된 객체를 순서에 따라서 추적해야 합니다. 작은 규모의 애플리케이션에서 생성 순서를 관리하기 위해서 ContextListeners-저도 여기서 사용 했습니다-를 사용한다지만, 더 큰 규모의 애플리케이션에서는 발생하는 문제를 피하기 위해서 그 외의 안전장치가 필요 합니다.

이전 싱글톤 레지스트리의 또 다른 문제점은 명시적으로 put() 오퍼레이션이 Java에서 호출한다는 것입니다. 이것은 저장된 객체-테스를 위한 스텁(testing stub)을 실제 데이터베이스와 연동된 DAO로 교체하길 원하는 경우-의 구현이 변경되면 재 컴파일이 필요하다는 것을 의미합니다. 체크하는 것을 한 번이라도 실수한다면, 실제 애플리케이션은 DAO 스텁을 사용하게 될 것입니다. 큰 규모의 애플리케이션에서 이것들이 코드에 묻혀 있기 때문에 다시 한 번 추적하는 것을 생각해 봐야 합니다.

Spring은 이와 같은 단점들을 다 없애 버렸습니다. 다음은 새로 작성된 레지스트리입니다.
package pool_test.util ;

import org.springframework....ApplicationContext ;
import org.springframework.
   ...ClasspathXMLApplicationContext ;

public class ObjectRegistry {

  private ApplicationContext _singletons ;
  
  private ObjectRegistry(){

    _singletons =
      new ClassPathXmlApplicationContext(
        new String[] { "spring-objectregistry.xml" }
      );
        
  }

  public Object get( final String key ){
    return( _singletons.getBean( key ) ) ;
  }

}
이전에 사용하던 Map을 Spring의 ApplicationContext로 변경한 것을 주목해야 합니다. Map과 비슷하게 ApplicationContext는 객체를 저장하고, 이름으로 그 객체들을 받아 올 수 있습니다. 비교하자면 ApplicationContext는 설정에 대한 정보를 밖으로 빼낸 XML 파일에서 객체 정보를 모두 불러옵니다. spring-objectregistry.xml에 정보가 변경되면 애플리케이션의 재시작이 필요하긴 하지만, 완전한 재 컴파일은 필요 없습니다.

spring-objectregistry.xml에서 발췌한 부분을 보면 다음과 같습니다.

  <-- 간단히 보여주기 위해서 생략 -->



  
    
  



  
    
               

XML 구성 요소(element)는 Reflection 호출에 대응합니다. 요소는 객체를 정의하고, 그 안에 있는 요소는 객체의 값을 부여하는 메소드를 호출합니다. 예를 들어, id가 DAO인 빈에서 Spring은 처음으로 SimpleDAO 타입의 객체를 생성한 다음에 setDataSource() 메소드를 호출합니다. setDataSource() 메소드를 위한 인자 값은 설정 파일에 정의되어 있는 DataSource 빈입니다.

이 작업의 배후에서 Spring은 DataSource를 생성(congfigure)하고, DAO에 할당합니다. Spring에 의해서 관리되는 객체는 단지 빈의 이름을(DAO) 호출해서 DAO를 참조하며, 빈에 대한(여기서는 ”SimpleDAO") 구현의 변화에 대해서는 알지 못 합니다.

이제 Spring은 객체를 관리하고 있고, ObjectRegistry는 클라이언트를 위해서 읽기 전용의 메소드만 제공합니다. ObjectRegistry 클래스에 있던 put() 메소드를 삭제할 수 있었고, 또한 명시적으로 다른 클래스에서 put() 메소드를 호출하는 것도 제거했습니다. 예를 들어, SetupDataSourceContextListener는 지금 초기화된 커넥션의 풀의 역할을 하고 있습니다.

이제 web.xml 배치 기술자에 몇 가지 중요한 점을 찾을 수 있습니다. 예를 들어, 몇몇의 컨텍스트 인자는 JDBC와 관련된 정보를 위해서 로컬 프라퍼티 파일(local properties file)을 참조하고 있습니다. Spring은 이 프라퍼티 파일과 파일에 할당된 값을 사용해서 객체를 만듭니다.

Spring은 또한 spring-objectregistry.xml에 있는 객체들 간의 의존성 확인을 도와줍니다. 이전 코드에서는 제가 스스로 작업해야 했었습니다. 하지만 이제 이 애플리케이션에서 클라이언트 코드에서 객체를 사용하기 전에, 참조되는 객체가 적절한 순서로 생성이 되었는지를 확인해주기 때문에, 의존성 확인을 좀 더 편리하게 사용할 수 있습니다. 이것은 Spring이 코드를 더 깨끗하게 해주는 것에 더해서, 기록해야 하는(bookkeeping) 몇몇 작업의 짐 또한 덜어주었다는 걸 의미합니다.

한 가지 논쟁거리가 되는 것은 “pure" IoC 접근법은 명시적으로 ObjectRegistry를 호출하는 것의 필요성을 제거했다는 것과 Spring이 실행 시에 객체간의 관계를 관리해준다는 점입니다. 리팩토링이 요구되는 시점입니다. 나중에 문제 발생 소지가 되겠지만, 여전히 지금은 레지스트리가 필요 합니다.

데이터 티어 변경하기 : Spring JDBC

커스텀 DataSource 구성은 환상적인 XML 작업으로 해결됩니다. 또 Spring은 반복적인 JDBC 코드를 제거한 기본 DAO 클래스를 제공합니다. 이 말은 Spring은 커넥션 관리와 ResultSet과 PreparedStatements를 닫아주는 작업을 도와준다는 뜻입니다. 그럼 애플리케이션 코드에는 무엇이 남을까요?

새로운 DAO는 이전 DAO와 같은 인터페이스를 갖고 있습니다.
package pool_test.data.jdbc ;

public class SimpleDAO {
  public void setupTable() ;
  public void putData() ;
  public Collection getData() ;
  public void destroyTable() ;
}
그렇지만 이 덮개 안에는 다른 동물이 들어있습니다. 이전 버전의 DAO는 클래스 내에 많은 JDBC 코드를 갖고 있었던 반면에, 새로운 버전의 DAO는 Spring을 사용함으로서 많은 짐을 덜게 되었습니다.
package pool_test.data.jdbc ;

public class SimpleDAO extends JdbcDaoSupport {

  private GetDataWorker _getDataWorker ;
  private PutDataWorker _putDataWorker ;
  private CreateTableWorker _createTableWorker ;
  private DestroyTableWorker _destroyTableWorker ;

  // 생성자는 선언되지 않았습니다.

  protected void initDao() throws Exception {
    
    super.initDao() ;
    
    _getDataWorker =
      new GetDataWorker( getDataSource() ) ;

    _putDataWorker =
      new PutDataWorker( getDataSource() ) ;

    _createTableWorker =
      new CreateTableWorker( getDataSource() ) ;

    _destroyTableWorker =
      new DestroyTableWorker( getDataSource() ) ;

    return ;
    
  } // initDao()

  public void setupTable() {
    _createTableWorker.update() ;
  }

  public Collection getData() {
    return( _getDataWorker.execute() ) ;
  }

  // ... destroyTable()과 getData()
  //   비슷한 규약(Convention)을 따릅니다 ...

}
첫 번째 변화는 상위 클래스에 있습니다. SimpleDAO는 데이터베이스 작업에 필요한 메소드와 내부 클래스를 갖고 있는 Spring의 JdbcDaoSupport를 상속합니다. 도움을 주는 메소드의 첫 번째로 객체에 JDBC DataSource를 할당해주는 setDataSource()가 있습니다. 하위클래스에서는 객체를 받아오기 위해서 getDataSource()를 호출합니다.

initDao()는 JdbcDaoSupport에서 상속받는 다음 메소드입니다. 부모 클래스는 한 번만 실행되는 초기화 코드를 실행시키길 수 있는 기회를 자식 클래스에게 주기 위해서 이 메소드를 호출합니다. 여기서 SimpleDao는 멤버 변수에 값을 할당받게 됩니다. 멤버 변수 역시 생성됩니다. Spring JDBC로 사용하는 것은 GetDataWorker나 PutDataWorker와 같은 특별한 내부 클래스에 있는 SimpleDAO의 기능-데이터를 저장하고 받아오는-을 사용하는 것을 의미합니다. 예를 들어, 데이터를 저장하는 것은 PutDataWorker에 의해서 해결됩니다.
package pool_test.data.jdbc ;

import org.springframework ... SqlUpdate ;

public class SimpleDAO {

 ...
   private class PutDataWorker extends SqlUpdate {
    
     public PutDataWorker( final DataSource ds ){
      
       super( ds , SQL_PUT_DATA ) ;
    
       declareParameter(
             new SqlParameter( Types.VARCHAR ) ) ;
       declareParameter(
             new SqlParameter( Types.INTEGER ) ) ;

     }

     // 실제 애플리케이션에서는 외부 소스에서 SQL 문을 불러오게 됩니다.
     private static final String SQL_PUT_DATA =
       "INSERT INTO info VALUES( ? , ? )" ;

   }
   ...
}
PutDataWorker는 SQL INSERT와 UPDATE 호출 작업을 관리하는 Spring 템플릿 클래스인 SqlUpdate를 상속합니다. decalreParameter() 메소드는 SQL 문에서 사용할-문자열이나 숫자 각각- 데이터 타입을 Spring에게 말해주기 위해 호출합니다.

PutDataWorker는 간결한 클래스임을 강조하고 싶습니다. super() 메소드는 DataSource와 SQL 문을 상위 클래스로 전달하며 decalreParameter()는 쿼리를 작성합니다. SqlUpdate는 JDBC 관련 객체와의 실제 상호 작용을 통한 작업을 관리하고, 커넥션을 종료시킵니다. 이번에는 SimpleDao.putData() 메소드를 다듬어 보겠습니다.
public class SimpleDAO {

  public void putData() {

    for( ... ){

      // ... "nameParam" and "numberParam"는 지역 변수

      Object[] params = {
        nameParam , // 문자열 변수
        numberParam // 숫자형 변수
      } ;

      _putDataWorker.update( params ) ;
    }
  }
}
putData() 메소드는 크게 의미없는 데이터 몇 개를 데이터베이스에 전송합니다. 이 메소드는 작업하는 클래스를 대표하고 있습니다. 특별히 작업하는 클래스에는 상속 받은 메소드가 있습니다. SqlUpdate.update()는 데이터를 받아 오는 것을 도와주고, JDBC Connection과 관련된 Statement 객체를 닫아줍니다. 이것은 내가 작성한 커스텀 JDBC 코드의 많은 부분이 외부로 이동하게 되며, 심지어 Connection, Statement, RsultSet을 종료하기 위해 도움이 되는 메소드(Convenient method)를 갖고 있는 옛날 DBUtil과 같은 클래스 전체가 없어지기도 합니다.

SqlUpdate는 업데이트를 호출하기 위해서, Spring의 MappingSqlQuery가 쿼리를 사용하게 됩니다. GetDataWorker에 있는 mapRow() 메소드를 보겠습니다.
package pool_test.data.jdbc ;

import org.springframework ... MappingSqlQuery ;


// SimpleDAO 안에 있는 클래스 ...

private class GetDataWorker
  extends MappingSqlQuery {

  // PutDataWorker와 비슷한 생성자를 갖고 있음 ...
  
  protected Object mapRow( final ResultSet rs ,
       final int rowNum ) throws SQLException
  {
      
      final SimpleDTO result = new SimpleDTO(
        rs.getString( "names" ) ,
        rs.getInt( "numbers" )
      ) ;

      return( result ) ;
      
   } // mapRow()

}
이 메소드는 표 형식의 ResultSet 데이터를 한 번에 한 행씩 작업 가능한 SimpleDTO 객체로 변경하는 것을 도와줍니다. Spring은 ResultSet의 매 행마다 이 메소드를 호출합니다. mapRow()은 ResultSet 객체와 상호 작업을 하는 반면에, close() 하는 메소드에 책임을 갖거나, 프로세스에 어떤 행들이 남아 있는지를 체크하지는 않습니다.
역자 박찬욱님은 현재 학생으로, 오픈 소스에 많은 관심을 가지고 있습니다. Agile Java Network에서 커뮤니티 활동을 열심히 하고 있고, 블로그(chanwook.tistory.com)를 재미있게 운영하고 있습니다.
TAG :
댓글 입력
자료실

최근 본 책0