[Mybatis] 프로시져(procedure) 실행하기

Published on: 2013. 11. 14. 14:09 by louis.dev
ibatis 시절에는 procedure라는 테그가 있어서 해당 테그를 이용해 프로시져를 수행했다면 myBatis에서는 일반 쿼리문 처럼 insert, update, select 문을 통하여 프로시져를 수행할수 있다. insert, update, select 중 어떠한 테그를 사용해도 프로시저를 호출할수 있다. 대신 statementType="CALLABLE" 이라는 속성을 추가해 줘야 프로시져로 인식하여 프로시져를 수행한다.
<update id="execureProcedure" statementType="CALLABLE">
	{
		call procedure_name(#{id}, #{name})
	}
</update>

[Mybatis] 쿼리 파라미터 null 처리방법

Published on: 2013. 11. 14. 14:08 by louis.dev
myBatis와 iBatis에서 쿼리를 실행할때 PrepareStatement 방식으로 작동을 하게 되고 이때 쿼리로 전달값은 값을 각각 아래처럼 적용한다.
<!-- ibatis 방식-->
<insert id="insertQuery" parameterType="java.util.Map">
	INSERT INTO table (id , name, title) VALUES (#id#, #name#, #title#)
</select>

<!-- mybatis 방식 -->
<insert id="insertQuery">
	INSERT INTO table (id , name, title) VALUES (#{id}, #{name}, #{title})
</insert>
그런데 이때 insert 하려는 값 중에 null이 전달되었을 경우 오라클에서는
uncategorized SQLException for SQL []; SQL state [99999]; error code [17004]; 부적합한 열 유형: 1111; nested exception is java.sql.SQLException: 부적합한 열 유형: 1111
이런 에러를 발생시키게 된다. Spring을 사용했을 경우는 이런에러를 발생시킨다.
Caused by: org.springframework.jdbc.UncategorizedSQLException: Error setting null parameter.  Most JDBC drivers require that the JdbcType must be specified for all nullable parameters. Cause: java.sql.SQLException : 부적합한 열 유형: 1111
그렇다고 항상 쿼리에서 null을 체크하는 방식을 사용하는것은 너무나도 귀찮은 일!! 그래서 if(myBatis)문 혹은 isNotEmpty(ibatis)를 사용하지 않는 방법이 있다. (자세히 보면 Spring Framework에서 발생시키는 에러에 답이있다.)  
<!-- ibatis 방식-->
<insert id="insertQuery" parameterType="java.util.Map">
	INSERT INTO table (id , name, title) VALUES (#id:VARCHAR#, #name:VARCHAR#, #title:VARCHAR#)
</insert>

<!-- mybatis 방식 -->
<insert id="insertQuery">
	INSERT INTO table (id , name, title) VALUES (#{id, jdbcType=VARCHAR}, #{name, jdbcType=VARCHAR}, #{title, jdbcType=VARCHAR})
</insert>
위와 같이 전달되는 파라미터 값의 jdbcType이 무엇인지를 정해 주면 쿼리는 자연스럽게 null로 변경되어 insert query가 수행된다.(물론 해당 column이 NULL값을 저장가능한 column 이어야 한다) 지원하는 jdbcType은 아래와 같다.
  1. BIT
  2. FLOAT
  3. CHAR
  4. TIMESTAMP
  5. OTHER
  6. UNDEFINED
  7. TINYINT
  8. REAL
  9. VARCHAR
  10. BINARY
  11. BLOB
  12. NVARCHAR
  13. SMALLINT
  14. DOUBLE
  15. LONGVARCHAR
  16. VARBINARY
  17. CLOB
  18. NCHAR
  19. INTEGER
  20. NUMERIC
  21. DATE
  22. LONGVARBINARY
  23. BOOLEAN
  24. NCLOB
  25. BIGINT
  26. DECIMAL
  27. TIME
  28. NULL
  29. CURSOR
쿼리마다 적당이 jdbcType을 지정해서 사용하자.

[Mybatis]resultMap을 이용한 1:N select시 주의 점

Published on: 2013. 11. 14. 14:07 by louis.dev
mybatis에서 테이블간의 1:N관계를 select 할때 resultMap을 통한 일종의 서브쿼리 형식으로 데이터를 가져올수 있다. 예를들어 게시판(BOARD)라는 테이블과 댓글(COMMENT)라는 테이블이 있고, 하나의 게시글에는 여러개의 댓글이 생성될수 있음으로 게시판과 댓글의 관계는 1:N관계이다. 이런 구조를 자바 코드로 클래스를 만들어 보면 다음과 같다.
class  Board{
	private String board;
	private String title;
	private String content;
	private String writer;
	private List<Comment> comments;

	//getter, setter 생략
}
소스에서도 볼수 있듯이 Board라는 클래스는 comment의 List형태인 comments라는 프로퍼티를 가지고 있게된다. 이렇게 1:N의 구조일때 Mybatis에서는 쿼리를 저장하는 xml파일(iBatis에서는 sqlmap으로 불렀으나 mybatis에서는 mapper라고 부름)에 resultMap 엘리먼트로 다음과 같이 설정할 수 있다.
<resultMap id="boradResult" type="net.krespo.mybatis.Board">
	<result property="boardid" column="BOARDID"/>
	<result property="title" column="TITLE"/>
	<result property="content" column="CONTENT"/>
	<collection property="comments" column="BOARDID" javaType="java.util.ArrayList" ofType="net.krespo.mybatis.Comment" select="getCommentListById"/>
</resultMap>

<select id="getBoardById" resultMap="boardResult">
	SELECT boardid, title, content FROM board WHERE boardid = #{boardid}
</select>
<select id="getCommentListById" resultType="net.krespo.mybatis.Comment">
	SELECT commentid, boardid, writer, content FROM comment WHERE boardid = #{boardid}
</select>
위와같이 getBoardById라는 쿼리는 게시글을 읽어올때 실행되는 쿼리이다. 이때 getBoardByIdresultMap으로 boardResult를 지정하고 있고 boardResultcollection 선언을 통해(select="getCommentListByBoardId" 쿼리를 실행할때 parameter는 column으로 선언된 boardid를 가지고) 하위 댓글 리스트를 가져온다. 만약 댓글을 가져올때 자기가 쓴글에 자기가 쓴 댓글을 가져오려면 어떻게 해야할까?  그럴때는 boardid와 writer를 getCommentListById로 넘겨주어야 한다. 이처럼 여러개의 파라미터를 collection에서 넘길때는
column="{prop1=COLUMN1, prop2=COLUMN2}"
로 쓰면된다. . 즉
<collection property="comments" column="{boardid=BOARDID,writer=WRITER}" javaType="java.util.ArrayList" ofType="net.krespo.mybatis.Comment" select="getMyCommentListById"/>

<select id="getMyCommentListById" parameterType="java.util.Map" resultType="net.krespo.mybatis.Comment">
	SELECT id, writer, content FROM comment WHERE boardid = #{boardid} AND writer = #{writer}
</select>
로 쓰면 된다. 이때 select문에서 사용하는 파라미터명과 collection에서 선언한 column의 prop1, prop2명이 반드시 동일해야 한다.(column="{prop1=COLUMN1, prop2=COLUMN2}", #{prop1}, #{prop2}) 그리고 또 반드시 주의 해야 할점은 파라미터가 한개만 전달할때는(맨 첫번째 예제인 게시글에 댓글을 가져올때) getCommentListById에 parameterType을 지정하지 않아도 됐었다. 그러나 위와 같이 collection으로 파라미터를 여러개 전달해야 할때는 반드시 parameterType="java.util.Map"을 지정해 줘야 한다. 그렇지 않으면 다음과 같은 에러를 내뿜는다.
Caused by: org.apache.ibatis.reflection.ReflectionException: There is no setter for property named 'boardid' in 'class java.lang.Object'

[Maven] Maven 빌드가 느리게 되는 경우 처리방법

Published on: 2013. 11. 14. 13:21 by louis.dev
maven 빌드가 느려지는 경우는 다양한 경우가 있었지만, 내 경우의 경험을 써보고자 한다. 

 이전에 Maven을 통해 war 파일로 패키징을 진행할때 시간이 15분 이상 넘게 걸리는 경우가 있었다. 이때 webistrano를 이용해 빌드하고 배포를 진행했기 때문에 webistrano문제이겠거니 하고 대수롭지 않게 넘겼다. 
그런데 프로젝트 후반으로 갈수록 배포할 일이 많아졌고 그때마다 15분이나 되는 시간을 기다려야했기 때문에 결국 빌드가 느리게 되는 현상을 찾아보기로 하였다. 

이것저것 로그를 보다가 콘솔에서 직접 Maven 빌드를 진행해 보았는데 Maven 빌드 자체가 느리게 되는 문제를 확인하였다. 여기서 문제는 maven-metadata.xml과 같은 version 정보를 담고있는 xml파일을 다운로드 하는데 너무 오랜 시간이 걸리기 때문이라는 결론을 얻게 되었다. 

 회사에서는 사내에서 사용할 Nexus서버를 구축하였고, 프로젝트의 pom.xml파일에 사내 Nexus를 repository로 추가한 상태였다. 그렇기 때문에 maven-metadata.xml을 사내 repository에서 다운로드 받고 있었다.(빌드시 로그도 사내 repository에서 다운받고 있었다) 결국 문제는 네트워크 구조상의 문제였다. 


배포하려는 서버는 외부에서 들어오는 요청은 막혀있는 상태(보안을 위해)의 서버였다. 그렇기 때문에 wget 명령으로 외부페이지의 데이터를 긁어 온다거나 하는 작업을 수행할 수가 없었다. 


뜬금없지만 Maven의 구조에 대해서 먼저 말해보겠다. Maven의 설정파일인 pom.xml파일은 상속구조로 되어있다. Maven core안에 pom.xml이 기본적인 설정사항을 가지고 있고, 각 프로젝트마다 저장한 pom.xml파일이 상위의(Core의) pom.xml 파일을 상속받아(눈에보이지 않지만) 구현하고 있는 구조로 되어있다. 

이때 상위 pom.xml파일을 확인해 보면 해당 파일에 repository설정으로 Maven 중앙저장소가 세팅이 되어있고, 그렇기 때문에 우리는 각 프로젝트의 pom.xml에 repository 설정을 하지 않아도 library, maven-metadata.xml파일과 같은 파일을 자동으로 다운로드 받을수 있었던 것이다. Maven이 위와같은 구조로 되어있기 때문에 배포하려는 프로젝트는 2개의 repository(Maven에서 지원하는 중앙저장소, 사내에서 사용하는 Nexus)를 가지고 있게 되었다. 

따라서 library등을 다운로드할때 먼저 Maven 중앙저장소에서 라이브러리를 로컬에 다운로드 받고 만약 중앙저장소에 없는 라이브러리일 경우 사내 저장소에서 찾는 구조가 되었다. 이때 Maven 중앙저장소로 라이브러리와 버전정보 xml파일을 다운로드 받으려면 외부 네트워크로 트래픽이 나가야 하는데 네트워크 구조상 외부로 트래픽이 나갈수가 없어 계속 중앙저장소의 library, xml파일을 timeout 이 날때까지 계속 요청하다가 결국 받지 못하게 되고, 그 이후에 사내 저장소로 요청을하여 전체적으로 빌드 타임이 길어지는 문제가 있었다. 위와같은 문제는 maven의 global setting파일인 settings.xml파일에

중앙저장소 말고 특정한 저장소를 사용하겠다
라고 하는 세팅을 해주면 된다.  리눅스는 ${home}/.m2 디렉토리이고 윈도우는 C:사용자${사용자명}.m2 디렉토리안에 settings.xml파일을 생성해 주면 된다. 그리고 그 파일 안에 다음과 같이 설정하면 중앙저장소가 아닌 사내 저장소에서만 라이브러리를 다운로드 받을수 있게 된다.
<settings xmlns="http://maven.apache.org/SETTINGS/1.0.0"
          xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
          xsi:schemaLocation="http://maven.apache.org/SETTINGS/1.0.0 http://maven.apache.org/xsd/settings-1.0.0.xsd">
  <mirrors>
    <mirror>
      <id>${아이디지정}</id>
      <mirrorOf>*</mirrorOf>
      <url>${사용하고자하는 maven repository 설정}</url>
    </mirror>
  </mirrors>

  <profiles>
    <profile>
      <id>public</id>
      <repositories>
        <repository>
          <id>central</id>
          <url>http://central</url>
          <releases><enabled>true</enabled></releases>
          <snapshots><enabled>true</enabled></snapshots>
        </repository>
      </repositories>
      <pluginRepositories>
      <pluginRepository>
        <id>central</id>
        <url>http://central</url>
        <releases><enabled>true</enabled></releases>
        <snapshots><enabled>true</enabled></snapshots>
      </pluginRepository>
      </pluginRepositories>
    </profile>
  </profiles>

  <activeProfiles>
    <activeProfile>public</activeProfile>
  </activeProfiles>
</settings>
위와같이 설정하면 maven의 중앙저장소를 원하는 위치로 변경할 수 있다.



[Spring Framework] @Transactional로 구현한 트랜젝션에서 수동 rollback 하기

Published on: 2013. 11. 14. 13:13 by louis.dev
스프링에서 아주 간단하게 트랜젝션을 구현하려면 @Transactional 어노테이션을 통해서 쉽게 트랜젝션을 설정할 수 있습니다. (따로 해당 어노테이션을 사용하기 위한 설정 설명은 하지 않겠습니다.)
@Transactional
public int saveArticle(String title, String content) {
	// 게시글등록 테이블에 데이터를 저장합니다.
	articleDao.setArticle(title, content);
	// 최신글 테이블에 데이터를 저장합니다.
	newArticleDao.setNewArticle(title, content);

	//상태코드 200은 처리완료를 나타냅니다.
	return 200;
}
위의 코드와 같이 메소드 위에 @Transactional을 사용함으로서 saveArticle에 트랜젝션을 걸수가 있습니다.   그런데 위와같은 코드는 한가지 문제가 있습니다.   만약 저장되지 않았을때 상태코드를 500(익셉션 발생)을 내보내고 싶다면 어떻게 해야할까요?
@Transactional
public int saveArticle(String title, String content) {
	try {
		// 게시글등록 테이블에 데이터를 저장합니다.
		articleDao.setArticle(title, content);
		// 최신글 테이블에 데이터를 저장합니다.
		newArticleDao.setNewArticle(title, content);

		//상태코드 200은 처리완료를 나타냅니다.
		return 200;
	} catch(Exception e) {
		e.printStackTrace();
		return 500;
	}
}
위와같이 코딩할수 있습니다.  하지만 이때는 익셉션이 발생하면 try catch로 처리되기 때문에 익셉션이 메소드 밖으로 throw되지 않아 트랜잭션이 실행되지 않습니다. 즉 articleDao.setArticle(title, content); 가 저장되고 난 뒤 newArticleDao.setNewArticle(title, content); 에서 에러가 나면 articleDao.setArticle(title, content);로 저장된 글이 롤백이 되지 않아 article에는 저장되고 newArticle에는 저장이 되지 않게 됩니다. 이때는 Spring Framework의 TransactionAspectSupport 클래스를 통해 rollback을 수동으로 작업해 주면됩니다.
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
public int saveArticle(String title, String content) {
	try {
		// 게시글등록 테이블에 데이터를 저장합니다.
		articleDao.setArticle(title, content);
		// 최신글 테이블에 데이터를 저장합니다.
		newArticleDao.setNewArticle(title, content);

		//상태코드 200은 처리완료를 나타냅니다.
		return 200;
	} catch(Exception e) {
		e.printStackTrace();
  	        TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
		return 500;
	}
}
이렇게 작성해 주면 익셉션 발생시 해당메소드를 rollback 시킬수 있게 됩니다. 참고로 TransactionAspectSupport.currentTransactionStatus().setRollbackOnly() 가 실행될때 바로 rollback이 진행되지는 않습니다. setRollbackOnly 메소드는 속성만 변경을 하는 것이기 때문에 실제 rollback이 일어나는 시점은 commit이 되기 직전에 수행되게 됩니다.