먹고 사는 이야기/프로그래밍

[toby의스프링] 11장 - 데이터 액세스 기술

Kunner 2011. 11. 10. 16:24

예전에.. 적어도 "웹프로그램" 이란 것은 DB 기술에 의존하는 부산물 같은 것이라고 생각했다. 실제로 중요한 것은 DB 기술이고, 웹프로그램에서 제어하는 것은 프레젠테이션 수준의 것이라고 생각했던 것이다. 사실 90년대 말에서 2000년 대 초반까지 주류를 이루던 asp니 jsp니 php니 하는 것들에 대해서는 크게 틀린 말도 아니었을 것이다. 



물론 지금도 일반적인 의미에서 "웹프로그램"이 하는 주요 역할은 DB(뿐 아니라 어떤 형태든)의 데이터를 특정한 방법으로 가공해 사용자의 브라우저에 표시해주고, 다시 사용자의 입력을 받아 DB에 저장하는 일련의 상호작용일 것이다.

그렇지만 최근 들어 각광받고 있는 여러 ORM 기술들 덕분에 개발에서 차지하는 DB제어 기술의 비중이 예전의 그것과 사뭇 달라진 것 같다. 꼭 그거 아니더라도, iBatis만 놓고 봐도 예전에 소스에다 컨트롤 C 컨트롤 V 하면서 갖다 박았던 것과는 정말 많이 다르다. 세상은 점점 편해져 간다 - 이렇게 프로그램 개발에서조차도.


웹프로그램에서 DB와 연동하는데는 크게 몇가지 중요한 개념이 있다.

1. DB 연결.
2. DB 조작/제어.
3. 트랜잭션

예전 같으면 DB를 연결하고, DB를 조작/제어 하고 여기에 트랜잭션 기술을 걸고 하는데 참 많은 손이 갔다. 특히 똑같은 코드를 페이지마다 계속 반복적으로 갖다 붙여야 했던 점, 아주 간단한 기능에도 코드가 주르륵 다 따라 붙어야 했던 점이나, 트랜잭션과 같은 부분에서 안정성과 유연성을 동시에 보장하기 어려웠던 점 등 불편했던 점이 한 두가지가 아니다.

그런데 스프링에서는 스프링 자체에서 지원하는 기술과 ORM 등 외부 기술과의 연계를 통해 이런 과정을 획기적으로 편리하게 해 준다.

DB를 연결하고 트랜잭션을 거는 일 따위를 환경설정 몇 줄로 다 끝낼 수 있다. 한번 제대로 설정해 놓으면, 그 다음부터는 딱히 신경 쓸 필요가 없다. 그리 어렵지도 않다. 놀라운 일이다.

비록 DB데이터를 조작하고 제어하는 일은 여전히 손이 많이 가는 일이고, 메소드마다 다 제각각일 수 밖에 없는 건 매한가지지만.. 그 적용 방법은 ORM 기술을 통해 획기적으로 편리해졌다. 감탄하는 식의 얘기가 계속되는데.. 예전의 노가다식 프로그램을 생각해 보면 이렇게 감탄하지 않을 수가 없다. 

개발을 하다보면 반복적이고 기계적인 적용이 필요한 곳들을 발견하게 된다. 그게 한번이 되고, 두 번이 되고 세번이 되면.. 어떻게 하면 이런 반복을 줄일 수 있을까 고민한다. 때문에 어지간한 개발자들은 완성도가 어떻든 저마다 일종의 프레임워크를 만들어 사용하게 된다. 그러니 이런 개념은 사실 완전히 새롭거나 완전히 획기적인 것은 아니다. 그렇지만 프레임워크의 완성도나 적용의 용이함, 그리고 실제 개발 시 시스템 전체를 아우르는 프레임워크의 커버리지 등은 정말 놀랍다. 이런거 진작 좀 나오지, 하는 생각 안 들 수가 없다. 쩝...


여튼, 이번 장에서는 DB를 연결하고 트랜잭션을 관리하는 기술들을 소개한다.
(DB의 조작과 제어는 SQL이나 ORM 기술 등 스프링이 하는 일과는 조금 거리가 있기 때문에 제외되었다)


책에서는 전통적인 JDBC는 물론, iBatis, JPA, 하이버네이트, JTA 등 여러가지 기술을 다루고 있다.
DB를 연결하고 자바 코드에서 불러와 사용하는 방법, 트랜잭션을 관리하는 법 등 굉장히 세밀하게 기술하고 있는데, 그걸 일일히 다 읽고 외울 필요는 없을 것이다.

일단은 빈번하게 쓰이는 것들을 눈여겨 보고, 나중에 필요할 때 또 들춰 보는 식으로 익히는게 좋을 것 같다.


아래는 지난 10장에서 소개한 applicationContext.xml 샘플 중 DB 관련 내용이 있는 부분이다.

이 소스는 iBatis 와 연동해 개발된 것이다.
이해를 돕기 위해 아래 코드에 주석과 각주를 달아 보자.
서로 연결된 오브젝트는 같은 색깔로 되어 있다. 그 표시들을 주의깊게 보면 이해가 더욱 쉬울 것이다.


applicationContext.xml


<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"

xmlns:p="http://www.springframework.org/schema/p"

xmlns:aop="http://www.springframework.org/schema/aop"

xmlns:tx="http://www.springframework.org/schema/tx"

xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"

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-2.5.xsd

        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-2.5.xsd">


        <!-- ... 중략 ...  -->



        <!--트랜잭션 적용을 위한 AOP 설정 -->

<aop:config>

<aop:pointcut id="defaultServiceOperation"

expression="execution(* classpathRoot.*.service.*Service.*(..))" /> // 클래스패스 하위의 service 패키지에 있는 개체 중 이름이 Service로 끝나는 것들에 적용한다. 스프링에서는 보통 인터페이스에 적용해야 한다.[각주:1] 

<aop:advisor pointcut-ref="defaultServiceOperation"

advice-ref="defaultTxAdvice" order="2"/>
                 <!--
                 // 위 두 <aop:pointcut />과 <aop:advisor />를 아래처럼 하나의 태그로 합칠 수 있다. 
<aop:advisor advice-ref="defaultTxAdvice" order="2" pointcut="execution(* classpathRoot.*.service.*Service.*(..))"  />

                 --> 

</aop:config>


<!-- 실제 메소드에 트랜잭션 적용하기 -->

<tx:advice id="defaultTxAdvice" transaction-manager="transactionManager">

<tx:attributes>

<tx:method name="update*[각주:2]" propagation="REQUIRED"[각주:3] rollback-for="Exception"[각주:4]/>

<tx:method name="delete*" propagation="REQUIRED" rollback-for="Exception"/>

<tx:method name="insert*" propagation="REQUIRED" rollback-for="Exception"/>

<tx:method name="process*" propagation="REQUIRED" rollback-for="Exception"/>

<tx:method name="perform*" propagation="REQUIRED" rollback-for="Exception"/>

<tx:method name="get*" propagation="REQUIRED" read-only="true"[각주:5] />

<tx:method name="set*" propagation="REQUIRED" read-only="true" />

<tx:method name="find*" propagation="REQUIRED" read-only="true"/>

<tx:method name="*" propagation="REQUIRED" read-only="true" isolation="READ_COMMITTED"[각주:6] />

</tx:attributes>

</tx:advice> 


<!-- DB 연결 설정 -->

<bean id="nativeJdbcExtractor" class="org.springframework.jdbc.support.nativejdbc.SimpleNativeJdbcExtractor" lazy-init="true"/>


<bean id="oracleLobHandler" class="org.springframework.jdbc.support.lob.OracleLobHandler" lazy-init="true">

   <property name="nativeJdbcExtractor"><ref local="nativeJdbcExtractor"/></property>

  </bean>

<bean id="sqlMapClient" class="org.springframework.orm.ibatis.SqlMapClientFactoryBean" >

<property name="configLocation">

<value>/WEB-INF/config/sqlmap-config.xml</value> //iBatis Sql Mapping 연결설정

</property>

<property name="dataSource">

<ref bean="dataSource" />

</property>

<property name="lobHandler" ref="oracleLobHandler" />

</bean>

<!-- jndi -->

<bean id="jnditemplate" class="org.springframework.jndi.JndiTemplate" >

    <property name="environment">

       <props>

          <prop key="java.naming.factory.initial">jeus.jndi.JNSContextFactory</prop>

          <prop key="java.naming.provider.url">localhost:9736</prop>

       </props>

    </property>

</bean>

<!-- Jeus Server use ConnectionPoolingDataSource -->

<bean id="transactionManager"

class="org.springframework.jdbc.datasource.DataSourceTransactionManager"

p:dataSource-ref="dataSource" />

<bean id="dataSource" class="org.springframework.jndi.JndiObjectFactoryBean">

<property name="jndiName" value="JNDI_NAME"/>

<property name="jndiTemplate" ref="jnditemplate"/>

</bean> 

</beans>



위 소스는 사실 어느 정도는 기계적인 적용이 가능하다.
JNDI와 데이터소스, 트랜잭션 매니저를 설정하는 부분은 해당 환경에 맞게 한번만 바꿔주면 되는 것이니 한번 샘플을 제대로 만들어 놓으면 쉽게 적용해 쓸 수 있을 것이다.

또 트랜잭션을 설정하는 부분 역시 프로그램마다 크게 다를리 없다.
데이터액세서란 결국 CRUD[각주:7] 안에서 움직이기 때문이다.
따라서 트랜잭션 설정 부분의 readOnly 애트리뷰트를 놓고 보면 언제나 Read 에서는 true, 그 외 CUD에서는 false가 될 것이다.

트랜잭션을 설정하는 방법은 이렇게 applicationContext에서 미리 설정해 두는 방법 외에도 자바 소스에서 메소드 앞에 @Transactional 애노테이션을 적용하는 방법이 있다.
애노테이션으로 설정된 트랜잭션은 applicationContext에서 정의한 트랜잭션에 우선한다. 이 애노테이션을 사용하고 싶으면 applicationContext에 다음과 같은 선언을 해 주면 된다.

<tx:annotation-driven />


@Transactional 애노테이션은 메소드 단위로 적용할 수 있다.
이때, 애노테이션을 클래스 앞에 붙여 놓으면 하위 메소드에 일괄 적용된다.

아래는 트랜잭션 설정에 관한 애트리뷰트의 종류와 설정값에 대한 정리이다.

트랜잭션 설정에 관한 애트리뷰트의 종류와 설정값

propagation

  - REQUIRED: 기본 속성. 모든 트랜잭션 매니져가 지원함. 이미 시작된 트랜잭션이 있으면 참여, 없으면 새로 생성. 
  - SUPPORTS: 이미 시작된 트랜잭션이 있으면 참여. 없으면 그냥 진행.
  - MANDATORY: 이미 시작된 트랜잭션이 있으면 참여. 없으면 예외 오류.
  - REQUIRES_NEW: 항상 새로운 트랜잭션 시작. 이미 시작된 트랜잭션은 일시중지됨.
  - NOT_SUPPORTED: 사용안함. 이미 시작된 트랜잭션은 일시중지됨.
  - NEVER: 트랜잭션을 사용하지 않음. 이미 시작된 트랜잭션이 있으면 예외 오류.
  - NESTED: 이미 진행중인 트랜잭션이 있으면 중첩 트랜잭션 실행. 일련의 처리 중 트랜잭션을 롤백시킬 만큼의 중대한 처리가 아닌 부분에 한시적으로 사용할 수 있음. ex) 로그 기록 중 오류가 나는 경우 업무 처리는 그대로 진행하도록 함. 단, 업무 처리에서 오류가 난 경우 해당 로그 기록도 함께 삭제됨.


isolation

  - DEFAULT: DB 드라이버의 기본 설정에 따른다.
  - READ_UNCOMMITTED: 가장 낮은 격리수준으로 트랜잭션 커밋 여부와 관계 없이 다른 트랜잭션에 노출.
  - READ_COMMITTED: 가장 많이 쓰임. 일반적으로 얘기하는 트랜잭션. 커밋되지 않은 정보는 다른 트랜잭션에서 보이지 않는다.
  - REPEATABLE_READ: 다른 트랜잭션에서 읽은 로우의 경우 수정 불가. 단, 새로운(다른 트랜잭션에서 읽히지 않은) 로우에 대해서는 수정 가능하다.
  - SERIALIZABLE: 읽기만 해도 수정 불가. 가장 강력하고 가장 낮은 성능. MSSQL의 쿼리어낼라이저에서 트랜잭션 걸어봤다면 확 감이 오겠지.


read-only, readOnly
  - true / false: 추가 설명이 필요 없을 것 같다.

트랜잭션 롤백 예외
  - rollback-for, rollbackForClassName, rollbackFor: 역시 추가 설명이 필요 없을 것 같다.

트랜잭션 커밋 예외
  - no-rollback-for, noRollbackForClassName, noRollbackFor : 마찬가지로 추가 설명이 필요 없을 것 같다.




이제 실제 java 소스코드에서 어떻게 쓰이는지 보자.

먼저 iBatis 를 쓰기 위해 임포트하자.

import com.ibatis.sqlmap.client.SqlMapClient;



다음은 각 DAO 에서 SqlMapClient를 구현한다.

private SqlMapClientTemplate sqlMapClientTemplate;

public void setSqlMapClient(SqlMapClient sqlMapClient) {

sqlMapClientTemplate = new SqlMapClientTemplate(sqlMapClient);

}


// 아래는 DAO에서 구현한 메소드이다. 
public void insert(Member m) { sqlMapClientTemplate.insert("insertMember", m); }

public void deleteAll() { sqlMapClientTemplate.delete("deleteMemberAll"); }

public Member select(int id) { return (Member)sqlMapClientTemplate.queryForObject("findMemberById", id); }

public List<Member> selectAll() { return sqlMapClientTemplate.queryForList("findMembers"); }


여기서 주의깊게 볼 것은 sqlMapClientTemplate의 사용법이다. 좀 더 자세히 살펴 보자.

sqlMapClientTemplate 사용법

1. 등록/수정/삭제

insert()

  - Object insert(String statementName): 여기서 statementName은 XML 매핑파일에서 규정된 쿼리의 id 값이다. 이하 동일함.
  - Object insert(String statementName, Object parameterObject): parameterObject는 넘겨 줄 파라미터 값이다. SQL 매핑 파일에서 #파라미터#와 연동된다. 이하 동일함.


update()
  - int update(String statementName): 반환되는 int 값은 결과행의 수이다. 이하 동일함.
  - int update(String statementName, Object parameterObject)
  - void update(String statementName, Object parameterObject, int requiredRowsAffected): 반환값이 없는 대신 예상 결과행 수와 맞지 않으면 예외 오류 발생.

delete()
  - int delete(String statementName): 반환되는 int 값은 결과행의 수이다. 이하 동일함.
  - int delete(String statementName, Object parameterObject)
  - void delete(String statementName, Object parameterObject, int requiredRowsAffected): 반환값이 없는 대신 예상 결과행 수와 맞지 않으면 예외 오류 발생.


2. 조회

단일 로우 조회: queryForObject() - 실행된 결과가 단일행인 경우 사용한다.
  - Object queryForObject(String statementName)
  - Object queryForObject(String statementName, Object parameterObject)
  - Object queryForObject(String statementName, Object parameterObject, Object resultObject): 특정 오브젝트로 결과값을 반환할 경우 resultObject로 정의한다.

다중 로우 조회: queryForList() - 실행된 결과가 한건 이상일 경우 List로 반환
  - List queryForList(String statementName)
  - List queryForList(String statementName, Object parameterObject)
  - List queryForList(String statementName, int skipResults, int maxResults): 몇번째 행부터, 최대 몇개 행을 가져 올 것인지 규정할 수 있다.
  - List queryForList(String statementName, Object parameterObject, int skipResults, int maxResults)

다중 로우 조회: queryForMap() - JDBC에서의 querForMap과는 전혀 다르므로 주의해야 한다.
  ※ JDBC에서는 하나의 Row를 대상으로 각 필드명을 key로 하여 필드 데이터를 맵에 담는데 반해, iBatis에서는 여러개의 Row를 대상으로 하여 지정된 컬럼을 key로 하여 각 로우의 모든 데이터를 맵에 담는다.
  - Map queryForMap(String statementName, Object parameterObject, String keyProperty): keyProperty는 key값이 될 필드를 말한다. 이하 동일함.
  - Map queryForMap(String statementName, Object parameterObject, String keyProperty, String valueProperty): valueProperty는 로우 전체가 아닌 특정 필드값만 맵에 담고 싶을 때 적어준다.

다중 로우 조회: queryWithRowHandler() - JDBC의 RowMapper와 비슷한 기능을 한다.
  ※ SQL 결과를 루프를 돌면서 각 로우마다 콜백 오브젝트를 호출한다.
  - void queryWithRowHandler(String statementName, RowHandler rowHandler): 각 로우마다 호출될 rowHandler는 미리 구성해 놓아야 한다.
  - void queryWithRowHandler(String statementName, Object parameterObject, RowHandler rowHandler)

rowHandler 구성 방법 예시[각주:8]

// RowHandler import
import com.ibatis.sqlmap.client.event.RowHandler;

// row를 받아 오기 위해 Entity import
import com.lg.g4c.capp.lib.entity.Entity;

//  실제 rowHandler 구현

public class ResultRowHandler implements RowHandler{

private List<Entity> returnList = new ArrayList<Entity>() ;


public ObjectRowHandler(){

}


public void handleRow(Object rowObject){

Entity result = new Entity() ;

try{

result.parseResultSet((ResultSet)rowObject) ;

returnList.add(result) ;

}catch(Exception e){

e.printStackTrace();

}

}

public List<Entity> getReturnList(){

return returnList ;

}




마지막으로 iBatis SQL 매핑 파일 및 SQL 파일을 보자.


sqlmap-config.xml

<?xml version="1.0" encoding="UTF-8" ?>


<!DOCTYPE sqlMapConfig

PUBLIC "-//ibatis.apache.org//DTD SQL Map Config 2.0//EN"

"http://ibatis.apache.org/dtd/sql-map-config-2.dtd">


<sqlMapConfig>

<settings enhancementEnabled="true"[각주:9] useStatementNamespaces="true"[각주:10] />


<!-- common sqlMap -->

<sqlMap resource="xmlfile.xml" />[각주:11]

... (필요한 수 만큼 반복) ... 

</sqlMapConfig> 




xmlfile.xml

<?xml version="1.0" encoding="UTF-8"?>


<!DOCTYPE sqlMap PUBLIC "-//ibatis.apache.org//DTD SQL Map 2.0//EN"

    "http://ibatis.apache.org/dtd/sql-map-2.dtd">

<sqlMap namespace="BoardFile">

<typeAlias alias="fileVo" type="projectName.model.FileVo"/> //조회된 값과 연결될 오브젝트

<typeAlias alias="map" type="java.util.Map"/>

<resultMap id="_resultMap" class="fileVo" >

<result column="FILE_NM" property="fileNm" />

<result column="FILE_PATH" property="filePath" />

</resultMap>

<select id="fileInfo" resultMap="_resultMap" parameterClass="fileVo"> // 파라미터와 결과값에 대한 정의를 설정한다.

SELECT *

FROM

TABLE_NAME

WHERE

SEQ = #seq#

<isNotEmpty property="requestParameter"> // 넘겨 받은 값에 의한 판별

AND MENU_CD = #requestParameter#  // #~#로 된 부분이 파라미터와 연결되는 부분이다. 이름이 같으면 자동 연결된다.

</isNotEmpty>

</select>

  
<!-- 반복되는 코드는 아래와 같이 include 해서 쓸 수 있다. -->
<sql id="page-header">

SELECT PAGINGSELECT.*

FROM (SELECT TMP.*,

CEIL(ROWNUM/#pageSize#) AS TMPPAGE

FROM (

</sql>

<sql id="page-footer">

) TMP

) PAGINGSELECT

WHERE PAGINGSELECT.TMPPAGE = #pageNo#

</sql>
<!-- 반복되는 코드 정의 -->

  <select id="selectInfoDivList" resultMap="infoResult">

<include refid="page-header"/> // 미리 정의해 둔 코드 인클루드

SELECT PRT_ORDER

,BBS_DIV_CD

,BBS_NM

,USE_YN

FROM INFOVIO_DIV

WHERE INSTR(BBS_DIV_CD, 'COM') = 0

<isNotEmpty prepend="AND" property="searchType">

USE_YN = #searchType#

</isNotEmpty>

ORDER BY BBS_DIV_CD, PRT_ORDER

<include refid="page-footer"/> // 미리 정의해 둔 코드 인클루드

</select>

 </sqlMap> 




코드만 봐도 참 쉽지 않은가?

이거 원.. 밥로스 라도 소환해야 할 것 같다.





- JPA와 하이버네이트, JTA 에 대한 내용은 책에 나온 내용이 좀 부실하여 따로 공부를 좀 더 해야 할 것 같다.





 
  1. 만약 클라이언트에서 인터페이스가 바라보는 실제 메소드를 직접 호출한 경우 트랜잭션 적용이 안 될 수 있다. 이런 경우에는 트랜잭션 전파 범위 설정을 클래스 단위로도 할 수 있지만, 이는 스프링이 지향하는 개발 방식과 배치되므로 주의해야 한다. 일단은 인터페이스에 적용하고 인터페이스를 불러다 쓰는 것이 좋다는 정도로 정리하자. (클래스 프록시: 본문 p988 ~) [본문으로]
  2. 이름이 update로 시작되는 모든 메소드에 적용한다는 뜻이다. [본문으로]
  3. 트랜잭션의 전파 범위를 설정한다. [본문으로]
  4. 롤백 예외: 롤백이 필요한 경우에 대한 설정. rollback-for, rollbackFor, rollbackForClassName이 있다. rollback-for, rollbackForClassName에서는 예외 종류를, rollbackFor에서는 클래스를 지정해 준다. 반대 개념으로 no-rollback-for, noRollbackFor, noRollbackForClassName 애트리뷰트가 있다. [본문으로]
  5. 읽기 전용 속성을 설정한다. read-only, readOnly 애트리뷰트에 true/false 를 설정한다. 기본은 false다. [본문으로]
  6. 트랜잭션 격리수준을 말한다. 자세한 설명은 아래 따로 정리한다. [본문으로]
  7. Create / Read / Update / Delete [본문으로]
  8. http://blog.naver.com/brainkorea/150099231928 참조 [본문으로]
  9. 바이트코드의 기능 향상. [본문으로]
  10. SQL 매핑 정보에서 nameSpace를 사용할 것인지 여부. 만약 true인데 SQL 파일에는 namespace 정보가 없으면 오류가 나므로 주의. [본문으로]
  11. 관리의 편의를 위해 xml 파일을 여러개로 쪼갠 경우 개수만큼 행을 반복한다. [본문으로]