Spring 2.1로 줄곧 개발하고 있었는데, 항상 느끼던게 지나치도록 복잡한 설정이었다. 진입장벽이 높다고 했던가? 아마도 SpringFramework의 초기 진입시 가장 어려운 점은 무엇보다도 설정 이 아닌가 생각되었다.
Spring 2.1에서의 복잡한 설정(물론 몇 번 해보면 익숙해지면 금방 copy & paste 가 가능하다.)은 Spring 2.5에서 어노테이션이라는 강력한 기능으로 중앙집중적인 설정에서 지방분권적인 설정을 가능하게 만들었다.
(xml에 의한 설정이 아닌 개별 java 파일로 설정이 옮겨감)
공실장 : 어이 김과장, 내가 이번에 Spring을 공부 해보고 싶은데 어떤 책을 봐야하지? Spring in Action은 한번 죽 훓어 봤는데, 다른 좀 쉬운 책 그런거 없나?어이없이 들릴지 몰라도, 처음 Spring을 접하는 사람이라면 Hello World 조차도 왠지 멀게만 느껴질 것이다. 위의 예처럼 설정만 누군가 해준다면, 자바 소스는 짤 수 있겠는데... 라는 말이 나오겠지만, 무엇보다도 설정을 할 줄 아는게 중요하겠다.
김과장 : ... Hello World는 찍어보셨나요?
공실장 : 허허허, 나보고 Hello World나 찍어보라는건가? 내가 그런걸... 허허허
(며칠 뒤)
김과장 : 공실장님, Spring 은 어떻게 잘 되고 계세요?
공실장 : 나 Spring은 포기 했네, 설정이 어려워서 그런지 Hello World가 도무지 실행이 안되던데...
하지만, 개발기간이 초초 단기라면, 한달 이내에 분석부터 구현까지 모두 끝내야하는 것이라면 Step by Step으로 설정부터 배워나가서 Spring을 구성하기가 어려울 것이다. 아마, Spring은 배제될 것이 뻔하다. 단순히 jsp 페이지만으로 구성해서 빨리 마치고자 노력할 것이다. 온통 Copy & Paste 범벅이 될 게 자명하다. 누군가 Copy & Paste도 실력이라는 진리를 이야기 해 준적이 있지만, 생각없는 Copy & Paste는 내 프로그램의 사명이라고 생각되지 않는다면 다른 길을 찾아야 하지 않을까?
몇 번의 Copy & Paste 로 만든 사이트가 지긋지긋하다면, 혹은 비즈니스 로직에만 집중하라는 만고의 진리를 느껴보고 싶다면, Utility를 만들어 보는 것도 나쁜 생각은 아닌 것 같다. 초기 진입을 낮춰 주는 무언가가 있다면 난 정말 비즈니스 로직은 환상적으로 짤 수 있을거야라는 정말 Lazy 한 생각이라면 Utility로 간단히 해결하자.
PCube(회사 내 서브 프로젝트명으로 Spring 2.5 기반 설정 파일 자동화 Utility의 이름) 의 시작은 프로젝트 시간이 촉박한 개발자를 위해 시작한 사내 프로젝트이다. Spring을 이용하되, 설정을 쉽게 도와줌으로써 "시작" 할 수 있는 여건을 마련해 준다. 이제 한 컷 씩 따라하면서 "Spring을 누군가 설정만 해준다면..." 라는 Lazy한 개발자가 되보자.
위의 2개 파일을 일단 다운로드 받도록 한다.
각 파일의 설명은 아래와 같다.
pcube-1.0.1.jar | Generate 해주는 클래스 실행 파일 모음 | |
project.pcube | Generate 해야 할 각족 설정이 담긴Seed 파일 |
project.pcube 에 설정된 값들을 가지고, pcube-1.0.1.jar 파일로 설정 파일을 Generate 한다. 특별히, project.pcube 파일은 JSON 파일 형식으로 되어 있어서 실제로 열어보면 간단한 중첩 구조로 되어 있음을 알 수 있다.
샘플을 보면서 실제로 어떤 설정을 해야 하는지 살펴보자.
project:
{
name:'Test Project generate by Utility(PCube)',
directory:'C:/eclipse_spring/workspace/TestProject/WebContent',
package:'com.company.project.package',
author:'Hyoseok Kim',
email:'toriworks@gmail.com',
comment:'프로젝트의 설명을 넣어주세요.',
schema:
{
kind:'MySql',
version:'5.0',
connects:
[
{id:'jdbc/JNDIName', url:'localhost', db:'testDB', uid:'root', pwd:'123456'}
],
tables:
[
{
name:'User',
lang:'UTF8',
fields:
[
{fname:'user_id',type:'varchar(20)',jtype:'String',auto:'no',key:'yes',nil:'no',default:''},
{fname:'user_name',type:'varchar(20)',jtype:'String',auto:'no',key:'no',nil:'no',default:''},
{fname:'auth_code',type:'int',jtype:'int',auto:'no',key:'no',nil:'yes',default:''},
{fname:'last_login',type:'datetime',jtype:'String',auto:'no',key:'no',nil:'no',default:'now()'},
{fname:'login_count',type:'int',jtype:'int',auto:'no',key:'no',nil:'no',default:'0'},
{fname:'email_address',type:'varchar(150)',jtype:'String',auto:'no',key:'no',nil:'no',default:''},
{fname:'contact1',type:'varchar(50)',jtype:'String',auto:'no',key:'no',nil:'no',default:''},
{fname:'contact2',type:'varchar(50)',jtype:'String',auto:'no',key:'no',nil:'yes',default:''},
{fname:'title_code',type:'varchar(20)',jtype:'String',auto:'no',key:'no',nil:'yes',default:''},
{fname:'title',type:'varchar(20)',jtype:'String',auto:'no',key:'no',nil:'no',default:''},
]
},
{
name:'Manager',
lang:'UTF8',
fields:
[
{fname:'manager_id',type:'varchar(20)',jtype:'String',auto:'no',key:'yes',nil:'no',default:''},
{fname:'manager_name',type:'varchar(20)',jtype:'String',auto:'no',key:'no',nil:'no',default:''},
{fname:'last_login',type:'datetime',jtype:'String',auto:'no',key:'no',nil:'no',default:'now()'},
{fname:'login_count',type:'int',jtype:'int',auto:'no',key:'no',nil:'no',default:'0'},
{fname:'email_address',type:'varchar(150)',jtype:'String',auto:'no',key:'no',nil:'no',default:''},
]
}
]
}
}
}
데이터베이스에 대한 설계가 모두 끝난 다음에 PCube를 이용하는게 가장 best practice라고 생각이 되는데, 왜냐하면 위의 Seed 파일의 내용을 보면 데이터베이스의 엔티티 정보가 그대로 들어가 있기 때문이다. 이 정보를 바탕으로 iBatis 쿼리를 만들어 내도록 되어 있기 때문임을 이해해 줬으면 한다.
Seed 파일의 첫 문장은 project: 로 시작하고, 기본적인 프로젝트 정보를 담는 부분과 데이터베이스 엔티티 부분으로 크게 나눌 수가 있다. 기본적인 프로젝트 정보를 담는 부분은 나중에 Java 소스의 주석부분으로 옮겨지게 된다. 중요한 점은 package: 인데, package: 부분에 쓰인 대로 Java 소스에 Package 정보가 부여된다.
패키지명이 Seed 파일에 기술된 대로(com.company.project.package) 구성되었음을 확인 해 볼 수 있다.
옆에 보여지는 이미지가 자동생성 된 이후에 구성된 Java package 인데, 실제로 controller, dao, domain, service, test, utils, validation 총 7가지의 파일을 자동적으로 생성해 낸다.
domain package 의 파일을 살펴보면 아래와 같다.
대강 살펴보면, Seed 파일에서 작성한 내용이 그대로 옮겨졌다는 걸 알 수가 있다.
domain 클래스만 살펴보았지만, 기타 다른 package를 보면 규칙에 따라서 파일이 생성됨을 알 수가 있다.
File을 누르고, Dynamic Web Project 를 선택하자.
Project 명으로 TestProject 라고 입력하고, 하단의 Finish 버튼을 누른다.(Project contents, Target Runtime, Dynamic Web Module version, Configuration 등의 설정은 필요에 따라 설정하자.)
lib 폴더를 펼친 후에, 아까 다운로드 받아놓은 pcube-1.0.1.jar 파일을 lib 폴더에 복사한다.
그리고, 2개의 폴더를 새로 만든다.
input 과 output 폴더를 만든다.
위 과정까지 마친 후의 폴더 모습은 다음과 같다.
input 폴더와 output 폴더가 추가되었고, WEB-INF 밑의 lib 폴더에 pcube-1.0.1.jar 파일이 추가되었음을 알 수가 있다.
그리고, input 폴더에 위에서 받아 놓은 Seed 파일인 project.pcube 파일을 복사 해 둔다.
input 폴더에 project.pcube 파일을 읽어 들여서, output 폴더로 파일들이 생성될 예정이다.
이제 파일들을 Generate 하기 위해서, 클래스 파일 하나를 만들어 보자.
클래스명에 GenerateFilesByAutomation 이라 입력하고, main 함수를 만들 예정이므로 public static void main(String[] args) 에 체크를 했다. Finish 버튼을 누르면, 파일 작성을 하기 위해 파일이 열린다.
GenerateFilesByAutomation.java 에 이렇게 내용을 기록하자.
import java.io.IOException;
import org.json.JSONException;
import com.cs.pcube.repository.start.GenerateFiles;
/**
* @author Hyoseok Kim(toriworks@gmail.com)
*
*/
public class GenerateFilesByAutomation {
/**
* @param args
* @throws JSONException
* @throws IOException
*/
public static void main(String[] args) throws IOException, JSONException {
// 파일을 자동으로 만들어보자.
GenerateFiles generator = new GenerateFiles();
generator.generateAllFiles(0);
}
}
수기로 작성해야 하는 부분은 위에처럼 파란색 굵은 글씨로 표시를 했다. 이렇게 작성을 했으면, Java 파일을 실행 시켜보자.
Shift + Alt + X 누른 후에, J 버튼을 눌러서 Java application을 실행 시켜보자.
실행 후에, output 폴더를 새로 고침을 눌러서 갱신을 해보자.(처음에는 생성된게 잘 보이지 않는다. 꼭 새로 고침을 눌러보자.)
새로 고침을 누르면, 크게 2부분으로 파일들이 나눠져 있음을 알 수가 있다.
첫째는 .java 파일들이고, 둘째는 Tomcat 쪽 파일과 .xml 파일들이다. 생성되어진 모든 파일을 한번 살펴보자.
생성된 디렉토리와 파일들을 Full로 펼쳐 보이면 이 정도 구조를 가지는 파일들이 생성되었음을 알수 있다.
com 밑의 폴더들은 기본적인 .java 파일과 iBATIS 용 .xml 파일들이 보이고, etc 밑의 폴더에서는 tomcat 용 설정파일과 WebContent에 위치해야 할 .jsp 파일과 .xml 파일들이 있음을 볼 수가 있다.
또한 SiteMesh를 이용하고 있기 때문에 이에 따르는 decorator.xml 이 폴더 구조에서 보여지고 있다.
이후 작업은 단순히 폴더를 복사해서, 폴더 구조에 맞게끔 복사를 해주면 그만이다. 예를 들어서, WebContent 폴더는 프로젝트의 WebContent 밑에 통째로 복사해 주면 된다.
일단 Spring 2.5에서 사용할 설정 파일에 대해 살펴보자. 3개의 파일을 생성해 주고 있는데, 프로젝트명-data.xml, 프로젝트명-service.xml, 프로젝트명-servlet.xml 이다. 각 각의 파일에 대해 살펴보자.
<?xml version="1.0" encoding="UTF-8"?>
<!-- 이 파일은 XML Schema 기반으로 Spring 설정을 구성합니다. -->
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<!-- dataSource 설정 설정 -->
<!-- (주의) dataSource가 여러개 일 경우 bean id 값을 조정하셔야 합니다. -->
<!-- 1번째 dataSource 설정 -->
<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean"
p:jndiName="java:comp/env/jdbc/JNDIName" />
<!-- 트랜잭션 부분 코드를 삽입한다. -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:annotation-driven transaction-manager="txManager"/>
<!-- SqlMap와 dataSource를 연결해준다. -->
<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean"
p:configLocation="/WEB-INF/resources/ibatis/sql-map-config.xml"
p:dataSource-ref="dataSource" />
<!-- 이 곳에 하위로 사용자와 관리자용 Spring 설정 파일 설정 부분 import 태그를 이용해 입력합니다.
또는 이 예시에 주어진 대로 파일을 생성 하고 아래 내용을 복사하세요.
예시
<import resource="resources/operation/sample-manager-data.xml" />
<import resource="resources/operation/sample-user-data.xml" /> -->
<!-- User DAO 선언 -->
<bean id="userDao" class="com.company.project.package.dao.ibatis.UserDaoImpl">
<property name="sqlMapClient" ref="sqlMapClient" />
</bean>
<!-- Manager DAO 선언 -->
<bean id="managerDao" class="com.company.project.package.dao.ibatis.ManagerDaoImpl">
<property name="sqlMapClient" ref="sqlMapClient" />
</bean>
</beans>
수작업으로 할일은 거의 없다. 자동으로 파일을 만들었으니, 그냥 설정이 이렇게 구성되는구나 라고 알고 있으면 되겠다. 하지만, 설정을 보면서 Spring 책을 꼭 찾아서 봄 직하다.
다음은 프로젝트명-service.xml을 살펴보자. 크게 어렵지 않다.
<?xml version="1.0" encoding="UTF-8"?>
<!-- 이 파일은 XML Schema 기반으로 Spring 설정을 구성합니다. -->
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<!-- 빈 스캐닝 부분을 추가한다. -->
<context:annotation-config />
<context:component-scan base-package="com.company.project.package" />
<!-- 이 곳에 하위로 사용자와 관리자용 Spring 설정 파일 설정 부분 import 태그를 이용해 입력합니다. -->
<!-- 예시 -->
<!-- <import resource="resources/operation/sample-manager-service.xml" /> -->
<!-- <import resource="resources/operation/sample-user-service.xml" /> -->
</beans>
service 는 더 간단하다. 왜냐하면, 빈 스캐닝을 통해서 굳이 .xml 파일에 설정을 넣을 필요가 없어졌기 때문이다. 그렇다면, Service 레이어를 담당하는 .java 파일을 함께 살펴보자.(좀 길게 되어 있으나, 한번 쯤은 꼭 봐야 하기에 전체 소스를 옮긴다.)
/**
* Test Project generate by Utility(PCube)
* This file is generate by PCUBE system automatically.
* Generated by Sun Feb 01 18:52:46 KST 2009
*/
package com.company.project.package.service.impl;
// << Please import packages >>
// TODO : import package name if you want to add
import org.springframework.stereotype.Service;
import org.springframework.dao.DataAccessException;
import com.company.project.package.dao.IManagerDao;
import com.company.project.package.domain.Manager;
import com.company.project.package.service.IManagerService;
import org.springframework.beans.factory.annotation.Autowired;
import java.util.*;
// << Please insert import packages (for example, import java.util.List; ) >>
/**
* @author Hyoseok Kim (toriworks@gmail.com)
*
*/
@Service
public class ManagerServiceImpl implements IManagerService {
/**
* Constructor
*/
public ManagerServiceImpl() {}
// Setter injection 대신에 @Autowired와 @Qualifier annotation 사용으로 injection 수행
@Autowired
private IManagerDao managerDao = null;
public void setManagerDao(IManagerDao managerDao) {
this.managerDao = managerDao;
}
/**
* C of CRUD - 새로운 정보를 데이터베이스에 입력하기 위해서 DAO 객체에 역할을 위임합니다.
*/
@Override
public void create(Manager manager) throws DataAccessException {
managerDao.create(manager);
}
/**
* U of CRUD - 기존에 등록된 정보를 수정하기 위해서 DAO 객체에 역할을 위임합니다.
*/
@Override
public void update(Manager manager) throws DataAccessException {
managerDao.update(manager);
}
/**
* D of CRUD - 기존에 등록된 정보를 삭제하기 위해서 DAO 객체에 역할을 위임합니다.
*/
@Override
public void delete(Object parameter) throws DataAccessException {
managerDao.delete(parameter);
}
/**
* R of CRUD - DAO 객체에 위임하여 요청받은 목록을 반환합니다.
*/
@SuppressWarnings("unchecked")
@Override
public List<Manager> lists(Object parameter) throws DataAccessException {
return managerDao.lists(parameter);
}
/**
* DAO 객체에 위임하여 목록 개수를 반환합니다.
*/
@Override
public Integer listsCount(Object parameter) throws DataAccessException {
return (Integer) managerDao.listsCount(parameter);
}
/**
* R of CRUD - DAO 객체에 위임하여 요청받은 자료의 상세정보를 반환합니다.
*/
@Override
public Manager detail(Object parameter) throws DataAccessException {
return managerDao.detail(parameter);
}
} // class end
CRUD 에 관한 메소드가 선언되어 있고, 특이점으로 파일 상단에 @Service 라고 어노테이션이 선언되어 있다. @Service 어노테이션 덕분에 빈 스캐닝을 통해 자동적으로 빈이 컨테이너에 올라갈 수 있다.
마지막으로 프로젝트명-servlet.xml 파일을 살펴보자.
<?xml version="1.0" encoding="UTF-8"?>
<!-- 이 파일은 XML Schema 기반으로 Spring 설정을 구성합니다. -->
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:p="http://www.springframework.org/schema/p" xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<!-- @RequestMapping 을 사용하기 위한 빈 설정 2개 추가한다. -->
<bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
<bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />
<!-- 빈 스캐닝 부분을 추가한다. -->
<context:annotation-config />
<context:component-scan base-package="com.company.project.package" />
<!-- 뷰 리졸버를 선언합니다. -->
<bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver"
p:prefix="/WEB-INF/jsp" p:suffix=".jsp">
</bean>
<!-- 에러 뷰 리졸버를 선언합니다. -->
<bean id="exceptionResolver" class="org.springframework.web.servlet.handler.SimpleMappingExceptionResolver">
<property name="exceptionMappings">
<props>
<prop key="java.lang.Exception">errorPage</prop>
<prop key="org.springframework.dao.DataAccessException">errorPage</prop>
</props>
</property>
</bean>
<!-- 업로드 리졸버를 선언합니다. -->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="maxUploadSize" value="100000000" />
<property name="defaultEncoding" value="utf-8" />
<property name="uploadTempDir" ref="uploadDirResource" />
</bean>
<!-- 업로드 임시 공간을 선언합니다. -->
<bean id="uploadDirResource" class="org.springframework.core.io.FileSystemResource">
<constructor-arg>
<value>C:/eclipse_spring/workspace/TestProject/WebContent/respository_temp/</value>
</constructor-arg>
</bean>
<!-- 이 곳에 하위로 사용자와 관리자용 Spring 설정 파일 설정 부분 import 태그를 이용해 입력합니다. -->
<!-- 예시 -->
<!-- <import resource="resources/operation/sample-manager-servlet.xml" /> -->
<!-- <import resource="resources/operation/sample-user-servlet.xml" /> -->
</beans>
@RequestMapping 을 사용하기 위해서 추가된 2개의 태그가 있다는 것만 기억하면 될 것 같다.
그렇다면, 실제로 Controller에서 @RequestMapping이 사용되는 걸 보자.
package com.company.project.package.controller;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;
/**
* @author Hyoseok Kim (toriworks@gmail.com)
*
*/
@Controller
public class IndexController {
@RequestMapping("/index.html")
public ModelAndView viewDefaultIndex() {
ModelAndView mav = new ModelAndView();
mav.setViewName("index");
return mav;
}
}
@RequestMapping 어노테이션의 사용법에 대해서는 차후 좋은 블로그를 찾아서 소개를 하던, 아니면 다뤄보도록 하겠다.
작은 귀찮음에서 시작한 거였지만, 그럭저럭 설정에 애 먹고 있는 사람들에게 요긴했으면 한다. 혹 SpringFramework 3.0에는 이런 기능도 함께 있으면 싶기도 하다. 부족하나마, 잘 따라해 주기만 한다면, 데이터베이스 엔티티가 많으면 많은 수록 프로젝트의 기간은 더 많이 단축할 수 있으리라 생각된다. 위의 예에서는 단순히 2개의 엔티티만을 대상으로 했지만, 그 수가 많으면 많을 수록 빛을 발하리라 기대해 본다.
출처 : http://toriworks.tistory.com/106