본문 바로가기

spring

Spring + iBatis 프레임워크 구성시 오라클 LOB 타입 사용하기

Spring + iBatis 으로 프레임워크가 구성됐을 때 오라클의 LOB타입인 CLOB, BLOB을 이용하는 방법입니다.

테이블은 아래와 같이 구성되어 있습니다.

CREATE TABLE lob_test (

   key            NUMBER,

   clob           CLOB,

   blob           BLOB,

   dt             DATE DEFAULT SYSDATE

)

LOGGING

NOCOMPRESS

NOCACHE

NOPARALLEL

MONITORING;

 

CREATE UNIQUE INDEX lob_test_pk

   ON lob_test (key)

   LOGGING

   NOPARALLEL;

 

ALTER TABLE lob_test ADD (

  CONSTRAINT lob_test_pk

  PRIMARY KEY

  (key)

USING INDEX lob_test_pk);

 

HTML 대충 아래와 같이 구성합니다. 이전 브라우저에서는 .attr("encoding", "multipart/form-data")이렇게 해도 되더니.. 안되서 .attr("enctype", "multipart/form-data") 추가했습니다.

<%@ page language="java" pageEncoding="UTF-8"%>

<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt"%>

<!DOCTYPE html>

<html lang="ko">

<head>

<meta charset="utf-8">

<title></title>

<link rel="stylesheet" href="">

<style type="text/css">

body{

       font-family:"맑은 고딕", "굴림", "굴림체", "Arial", "Verdana";

       font-size:12px;

}

</style>

<script type="text/javascript" src="http://ajax.microsoft.com/ajax/jquery/jquery-1.7.1.min.js" charset="utf-8"></script>

<script type="text/javascript" charset="UTF-8">

 

$(document).ready(function(){

      

       $("#insertHandler").click(function(){

              //alert("insert");

             

              var form = $("form");

              form

              .attr("encoding", "multipart/form-data")

              .attr("enctype", "multipart/form-data")

              .attr("method","post")

              .attr("action","<c:url value='/insert.do'/>")

              .submit();          

             

       });  

});

</script>

</head>

<body>

<!-- <form method="post" enctype="multipart/form-data"> -->

<form>

<div>

       <h1>CLOB, BLOB TEST</h1>

</div>

<hr/>

<div>

       <h2>INSERT</h2>

       <textarea id="clob" name="clob" style="width:300px; height:100px;"></textarea>

       <br/>

       <input type="file" id="blob" name="blob" style="width:300px;" />

       <br/>

       <input type="button" id="insertHandler" name="insertHandler" value="insert" />

</div>

<hr/>

<div>

       <h2>LIST</h2>

       <table border="1" width="100%">

              <tr>

                     <th width="5%">NO.</th>

                     <th width="5%">KEY</th>

                     <th width="20%">CLOB</th>

                     <th width="10%">BLOB</th>

                     <th width="10%">DATE</th>

                     <th width="35%">ORIGINAL IMAGE</th>

                     <th width="15%">RESIZING IMAGE</th>

              </tr>

      

       <c:forEach items="${list}" var="result" varStatus="i">

              <tr>

                     <td>${i.count}</td>

                     <td>${result.key}</td>

                     <td>${result.clob}</td>

                     <td>${result.blob}</td>

                     <td>${result.dt}</td>

                     <td align="center"><img src="<c:url value='/image.do'/>?key=${result.key}&resize=false" /></td>

                     <td align="center"><img src="<c:url value='/image.do'/>?key=${result.key}&resize=true" /></td>

              </tr>

       </c:forEach>

       </table>

</div>

</form>

</body>

</html>

 

Spring controller를 아래와 같이 구성합니다.

package com.tistory.aircook.controller;

 

import java.util.Enumeration;

import java.util.HashMap;

import java.util.Map;

 

import javax.servlet.http.HttpServletRequest;

 

import com.tistory.aircook.service.LobService;

 

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import org.springframework.beans.factory.annotation.Autowired;

import org.springframework.stereotype.Controller;

import org.springframework.ui.ModelMap;

import org.springframework.web.bind.ServletRequestUtils;

import org.springframework.web.bind.annotation.RequestMapping;

import org.springframework.web.multipart.MultipartFile;

import org.springframework.web.multipart.MultipartHttpServletRequest;

 

/**

 * <p>

 * LobController 클래스

 * </p>

 * <p>

 * Copyright(c) 2012 aircook.tistory.com. All rights reserved.

 * </p>

 *

 * @author Francis Lee

 * @since 2012. 2. 20.

 * @version 1.0

 */

@Controller

public class LobController {

 

       private final Log logger = LogFactory.getLog(getClass());

 

       @Autowired

       private LobService lobService;

 

       @RequestMapping("/list.do")

       public String list(HttpServletRequest request, ModelMap model) throws Exception {

 

              Map params = new HashMap();

 

              model.addAttribute("list", lobService.getList(params));

 

              return "/list";

       }

 

       @RequestMapping("/insert.do")

       public String insert(HttpServletRequest request, ModelMap model) throws Exception {

              // 테스트코드 시작

              Enumeration p = request.getParameterNames();

              int z = 1;

              while (p.hasMoreElements()) {

                     String name = (String) p.nextElement().toString();

                     String value = request.getParameter(name);

                     if (logger.isDebugEnabled()) {

                           logger.debug("[" + (z++) + "]" + name + " : " + value);

                     }

              }

              // 테스트코드 끝

 

              String clob = ServletRequestUtils.getStringParameter(request, "clob", "");

 

              MultipartHttpServletRequest multipartHttpServletRequest = (MultipartHttpServletRequest) request;

 

              MultipartFile multipartFile = multipartHttpServletRequest.getFile("blob");

 

              if (logger.isDebugEnabled()) {

                     logger.debug("multipartFile.isEmpty() : " + multipartFile.isEmpty());

                     logger.debug("multipartFile.getContentType() : " + multipartFile.getContentType());

                     logger.debug("multipartFile.getName() : " + multipartFile.getName());

                     logger.debug("multipartFile.getOriginalFilename() : " + multipartFile.getOriginalFilename());

                     logger.debug("multipartFile.getSize() : " + multipartFile.getSize());

              }

 

              Map params = new HashMap();

              params.put("clob", clob);

              params.put("blob", multipartFile.getBytes());

 

              lobService.setInsert(params);

 

              return "redirect:/list.do";

       }

      

       @RequestMapping("/image.do")

       public String image(HttpServletRequest request, ModelMap model) throws Exception {

 

              String key = ServletRequestUtils.getStringParameter(request, "key", "");

              String resize = ServletRequestUtils.getStringParameter(request, "resize", "");

 

              Map map = (HashMap) lobService.getImage(key);

              byte[] bytes = (byte[]) map.get("blob");

 

              model.addAttribute("resize", resize);

              model.addAttribute("bytes", bytes);

 

              return "blobView";

 

       }     

}

 

Service, Dao 클래스는 대충 알아서 만들고, ibatis mapping파일을 아래와 같이 구성합니다.

DTO 클래스를 생성안했기 때문에, resultMap을 그냥 HashMap을 이용해서 구성했습니다.

Insert 할때도 parameterMap을 구성해도 되나 귀찮아서 :CLOB :BLOB를 이용했습니다

고로 아래 캐시 설정중 는 flushOnExecute동작안합니다. 이유는 아직 못찾았습니다. Spring 설정시 mappingLocations를 사용하지 않고 iBatis 설정파일의 <sqlMap resource="com/tistory/aircook/dao/Lob.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="Lob">

        <!-- 캐시모델 설정 -->

       <cacheModel type="MEMORY" id="defaultCache">

              <flushInterval minutes="5"/>

              <flushOnExecute statement="Lob.setInsert"/>

              <property name="reference-type" value="SOFT" />

       </cacheModel>

        <typeAlias alias="caseInsensitiveMap" type="org.apache.commons.collections.map.CaseInsensitiveMap"/>

        <resultMap id="listMap" class="java.util.HashMap">

              <result property="key" column="key" javaType="java.lang.Integer" jdbcType="NUMBER" />

              <result property="clob" column="clob" javaType="java.lang.String" jdbcType="CLOB" />

              <result property="blob" column="blob" javaType="[B" jdbcType="BLOB" />

              <result property="dt" column="dt" javaType="java.util.Date" jdbcType="DATE" />

       </resultMap> 

       <!-- <select id="getList" parameterClass="java.util.HashMap" resultClass="caseInsensitiveMap"> -->

       <!-- <select id="getList" parameterClass="java.util.HashMap" resultClass="java.util.HashMap"> -->

       <select id="getList" parameterClass="java.util.HashMap" resultMap="listMap" cacheModel="defaultCache">

       <![CDATA[

              SELECT key, clob, blob, dt FROM lob_test

              ORDER BY key

       ]]>

       </select>

        <resultMap id="imageMap" class="java.util.HashMap">

              <result property="blob" column="blob" javaType="[B" jdbcType="BLOB" />

       </resultMap> 

       <select id="getImage" parameterClass="java.lang.String" resultMap="imageMap" cacheModel="defaultCache">

              SELECT blob FROM lob_test

              WHERE key = #key#

       </select>

        <insert id="setInsert" parameterClass="java.util.HashMap">

              INSERT INTO lob_test

                          (key,

                           clob,

                           blob

                          )

              VALUES      ((SELECT NVL (MAX (key), 0) + 1 FROM lob_test),

                           #clob:CLOB#,

                           #blob:BLOB#

                          )             

       </insert>

 </sqlMap>

 

BLOB의 경우 이미지를 넣고 화면에 표현하기 위해 Controller Request Mapping(@RequestMapping("/image.do"))을 걸었는데 이것에 대한 View를 아래와 같이 따로 정의하였습니다.

package com.tistory.aircook.common.view;

 

import java.io.ByteArrayInputStream;

import java.io.InputStream;

import java.net.URLConnection;

import java.util.Map;

 

import javax.servlet.ServletOutputStream;

import javax.servlet.http.HttpServletRequest;

import javax.servlet.http.HttpServletResponse;

 

import com.tistory.aircook.common.util.ImageUtil;

 

import org.springframework.util.FileCopyUtils;

import org.springframework.web.servlet.view.AbstractView;

 

/**

 * <p>

 * blob view

 * </p>

 * <p>

 * Copyright(c) 2012 aircook.tistory.com. All rights reserved.

 * </p>

 *

 * @author Francis Lee

 * @since 2012. 4. 19.

 * @version 1.0

 */

public class BlobView extends AbstractView {

 

       /*

        * public BlobView(){ setContentType(contentType); }

        */

 

       @Override

       protected void renderMergedOutputModel(Map model, HttpServletRequest request, HttpServletResponse response)

                     throws Exception {

 

              // 이미지 resize

              double xyRatio = 1.0;

              int maxWidth = 100;

 

              byte[] bytes = (byte[]) model.get("bytes");

              boolean isResize = Boolean.parseBoolean((String) model.get("resize"));

 

              if (bytes.length > 0) {

 

                     // byte --> InputStream 타입으로 변환

                     InputStream is = new ByteArrayInputStream(bytes);

 

                     String contentType = URLConnection.guessContentTypeFromStream(is);

                     if (logger.isDebugEnabled()) {

                           logger.debug("contentType : " + contentType);

                     }

 

                     // 이미지일때만

                     if (contentType.equals("image/jpeg") || contentType.equals("image/png") || contentType.equals("image/gif")) {

 

                           if (logger.isDebugEnabled()) {

                                  logger.debug("isResize : " + isResize);

                           }

 

                           // 이미지를 리사이징해야된다면

                           if (isResize) {

                                  bytes = ImageUtil.generateImage(bytes, maxWidth, xyRatio);

                                  is = new ByteArrayInputStream(bytes);

                           }

 

                           response.setContentType(contentType);

                           response.setContentLength((int) bytes.length);

 

                           // OutputStream 으로 출력

                           ServletOutputStream os = response.getOutputStream();

 

                           FileCopyUtils.copy(is, os);

 

                           os.flush();

                     }

              }

        }

}

 

마지막으로 이미지 리사이징하는 클래스 인터넷 검색(http://blog.naver.com/PostView.nhn?blogId=ppant&logNo=70107766793)해서 참조(복사!)했습니다.

 package com.tistory.aircook.common.util;

 

import java.awt.Color;

import java.awt.Graphics2D;

import java.awt.RenderingHints;

import java.awt.image.BufferedImage;

import java.io.ByteArrayInputStream;

import java.io.ByteArrayOutputStream;

import java.io.IOException;

 

import javax.imageio.ImageIO;

 

public class ImageUtil {

 

       public static byte[] generateImage(byte[] imageContent, int maxWidth, double xyRatio) throws IOException {

              BufferedImage originalImg = ImageIO.read(new ByteArrayInputStream(imageContent));

 

              // get the center point for crop

              int[] centerPoint = { originalImg.getWidth() / 2, originalImg.getHeight() / 2 };

 

              // calculate crop area

              int cropWidth = originalImg.getWidth();

              int cropHeight = originalImg.getHeight();

 

              if (cropHeight > cropWidth * xyRatio) {

                     // long image

                     cropHeight = (int) (cropWidth * xyRatio);

              }

              else {

                     // wide image

                     cropWidth = (int) ((float) cropHeight / xyRatio);

              }

 

              // set target image size

              int targetWidth = cropWidth;

              int targetHeight = cropHeight;

 

              if (targetWidth > maxWidth) {

                     // too big image

                     targetWidth = maxWidth;

                     targetHeight = (int) (targetWidth * xyRatio);

              }

 

              // processing image

              BufferedImage targetImage = new BufferedImage(targetWidth, targetHeight, BufferedImage.TYPE_INT_RGB);

              Graphics2D graphics2D = targetImage.createGraphics();

              graphics2D.setBackground(Color.WHITE);

              graphics2D.setPaint(Color.WHITE);

              graphics2D.fillRect(0, 0, targetWidth, targetHeight);

              graphics2D.setRenderingHint(RenderingHints.KEY_INTERPOLATION, RenderingHints.VALUE_INTERPOLATION_BILINEAR);

              graphics2D.drawImage(originalImg, 0, 0, targetWidth, targetHeight, centerPoint[0] - (int) (cropWidth / 2),

                          centerPoint[1] - (int) (cropHeight / 2), centerPoint[0] + (int) (cropWidth / 2), centerPoint[1]

                                         + (int) (cropHeight / 2), null);

 

              ByteArrayOutputStream output = new ByteArrayOutputStream();

              ImageIO.write(targetImage, "png", output);

 

              return output.toByteArray();

       }

 

}

 

왜 이미지를 BLOB에 넣는지 이해는 안되지만.. 고객의 needs인지라ㅎㅎㅎ

사용한 라이브러리 버전은 다음과 같습니다.

aopalliance-1.0.jar / 4,467

aspectjrt.jar  /  116,219

aspectjweaver.jar  /  1,749,970

cglib-nodep-2.2.jar  /  322,362

commons-beanutils-1.8.3.jar  /  232,019

commons-codec-1.6.jar  /  232,771

commons-collections-3.2.1.jar  /  575,389

commons-fileupload-1.2.2.jar   /  59,590

commons-io-1.4.jar / 109,043

commons-lang-2.4.jar  /  261,809

commons-logging-1.1.1.jar  /  60,686

ibatis-2.3.4.726a.jar  /  380,731

jstl-api-1.2.jar  /  30,690

jstl-impl-1.2.jar  /  392,431

log4j-1.2.16.jar  /  481,534

org.springframework.aop-3.1.0.RELEASE.jar  /  331,474

org.springframework.asm-3.1.0.RELEASE.jar  /  53,079

org.springframework.beans-3.1.0.RELEASE.jar  /  589,253

org.springframework.context-3.1.0.RELEASE.jar  /  829,601

org.springframework.core-3.1.0.RELEASE.jar  /  442,400

org.springframework.expression-3.1.0.RELEASE.jar  /  176,283

org.springframework.jdbc-3.1.0.RELEASE.jar  /  401,762