프로그래밍/JAVA2015.06.30 21:59

ORM 솔루션을 사용했을 때 좋은 점은 애플리케이션 개발자로 하여금 SQL 같은 하부 데이터 구조에서 벗어나 보다 객체 중심으로 생각하고 코드를 작성할 수 있게 한다는 것이다. (그렇다고 ORM 솔루션을 사용한다고 해서 SQL을 잘 몰라도 된다는 것은 아니다. 오히려 ORM 솔루션을 통해 자동 생성되는 SQL을 이해하고, 이를 최적화 하기 위해서는 SQL RDBMS에 대한 깊은 이해가 필요하다.)


그러나 과거부터 현재까지 개발된 많은 애플리케이션이 꼭 ORM 솔루션을 사용하고 있는 것은 아니다. 오히려 MyBatis 같은 경량의 SQL Mapper 솔루션을 더욱 많이 사용해 왔고, 가까운 미래에도 이러한 추세가 크게 변하지 않을 것으로 보인다. (물론 이것은 어디까지나 우리나라 개발 환경에 대한 개인적인 판단이다. 해외의 경우 ORM 솔루션이 매우 활발하게 사용되고 있다.)


이 글에서는 우리 나라에서 매우 활발히(?) 사용되고 있는 Spring-MyBatis 조합 하에서 보다 객체중심으로 생각하고, ORM 솔루션을 사용할 때와 비슷한 코드를 작성하기 위한 방법과 제약 사항에 대해 설명할 것이다. MyBatis에 대한 상세한 설정 및 사용법에 대해서는 설명하지 않는다. MyBatis에 대한 상세한 설명은 https://mybatis.github.io/mybatis-3/ko/index.html 를 참고하도록 하자.



1. 객체 중심으로 생각하기

본격적으로 Spring-Mybatis를 활용한 간단한 애플리케이션을 작성하기에 앞서 해당 애플리케이션에서 사용하게 될 음악가, 앨범, 수록곡이라는 3개의 도메인 모델에 대해 생각해보자. 간단한 도메인 규칙은 다음과 같다.


I.      음악가는 0개 이상의 앨범을 출시할 수 있다.

II.     앨범은 반드시 특정 음악가를 지닌다.

III.   앨범은 0개 이상의 수록곡을 지닌다.

IV.   수록곡은 반드시 특정 앨범에 포함된다.


위 규칙을 간단하게 ERD로 그려보면 다음과 같다.



MyBatis를 통해 위 ERD를 반영할 때, SQL 실행 중심으로 생각한다면 각각의 테이블에 매핑되는 Class를 정의하고, 이들 Class의 멤버 속성은 각각 테이블의 컬럼을 1:1로 처리할 수 있도록 선언하면 된다. (@DataLombok 어노테이션으로 Class 멤버 속성에 대해 자동으로 getter/setter를 만들어 준다.)

@Data public class Artist {
    private Long seq;
    private String name;
    private Date debutDate;
}

@Data public class Album {
    private Long seq;
    private long artistSeq;
    private String title;
    private Stock stock;
    private Date issueDate;
}

@Data public class Song {
    private Long seq;
    private long albumSeq;
    private String name;
    private int playtime;
}

위와 같은 도메인 모델의 역할은 매우 단순하다. 도메인 모델은 단지 각각 테이블에 대한 VO(Value Object)로서 DAO, Serivce, View 레이어 사이에서 값을 전달하기 위해 사용된다. 이러한 도메인 모델은 별도의 비즈니스 로직을 포함하지 않는 것이 일반적이며 비즈니스 로직은 Service 레이어에 집중적으로 위치하게 될 것이다.


예를 들어 특정 Album의 모든 수록곡의 총 플레이 시간을 집계하는 기능을 만든다고 해보자. 아래와 비슷한 메소드를 Service 레이어에 추가하게 될 것이다.

@Transactional(readOnly=true)
public int selectTotalPlaytime(Long seq) {
    try {
        Album album = albumRepository.selectAlbumByPrimaryKey(seq);
        if (album != null) {
            List<Song> songs = songRepository.selectSongByAlbumKey(album.getSeq());
            return songs == null || songs.size() == 0 ? 0 : songs.stream().mapToInt(Song::getPlaytime).sum();
        }
        return 0;
    } catch (DataAccessException e) {
        logger.error(e.getMessage(), e);
        throw e;
    }
}

위 방법이 꼭 잘 못되었다고 할 수는 없지만, 한 가지 분명한 것은 개발자로 하여금 객체 중심으로 도메인을 분석하고 애플리케이션을 개발하도록 유도하지 않고 있으며, SQL 실행 중심으로 비즈니스 로직을 바라보게 한다는 것이다. 보다 객체 중심으로 생각하고 이를 애플리케이션 코드에 반영하기 위해서는 앞서 보여준 Class 코드는 다음과 같이 변경되어야 한다. (Artist Class Album 객체 목록을 멤버 속성으로 포함할 수 있지만 여기서는 생략한다.)

@Data public class Artist {
    private Long seq;
    private String name;
    private Date debutDate;
}

@Data public class Album {
    private Long seq;
    private Artist artist;
    private String title;
    private Stock stock;
    private Date issueDate;
    private List<Song> songs;

    public int getTotalPlaytime() {
        return (getSongs() == null || getSongs().size() == 0) ? 0 :
                getSongs().stream().mapToInt(Song::getPlaytime).sum();
    }
}

@Data public class Song {
    private Long seq;
    private Album album;
    private String name;
    private int playtime;
}

이제 Album 객체는 Artist 객체와 has one 관계에 있으며, Song 객체와는 has many 관계로 있다. 이에 따라 Album 객체는 멤버 속성으로 1개의 Artist 객체와, List에 담기는 복수의 Song 객체를 포함한다. 또한 Song 객체 List를 통해 해당 Album의 수록곡들의 총 플레이 시간을 계산할 수 있으므로 getTotalPlaytime() 메소드를 추가할 수 있다. (Service 레이어의 비즈니스 로직이 자연스럽게 도메인 모델로 옮겨져 왔다.) 그리고 Song 객체는 해당 Song 객체를 포함하고 있는 Album 객체를 멤버 속성으로 지닌다.


Hibernate같은 ORM 솔루션을 사용한다면 위와 같은 도메인 모델을 쉽고 자연스럽게 처리할 수 있다. Album 객체 입장에서 Artist 객체는 @ManyToOne 표현할 수 있으며, Song 객체는 @OneToMany로 표현할 수 있다. 이와 유사하게 Song 객체 입장에서 Album 객체는 @ManyToOne으로 표현할 수 있다. 이러한 도메인 모델을 구현하기 위해 올바른 RDBMS 설계만 필요할 뿐 별도의 SQL을 정의하거나 추가할 필요는 없다. 개발자가 SQL에 대한 고민이나 작성의 부담에서 벗어나 자연스럽게 객체들 사이의 관계에만 집중할 수 있도록 돕는 ORM 솔루션의 중요한 기능 중 하나인 것이다.


그렇다면 위와 같이 객체 중심으로 생각하고 도메인 모델을 작성하기 위해서 꼭 ORM 솔루션을 사용해야 하는 것일까? 그렇지 않다. 이 글의 목적이 바로 MyBatis를 사용하면서 위와 같은 도메인 모델을 작성하는 방법을 설명하는 것이다.



2. MyBatis로 작업하기

먼저 도메인 모델을 처리하는 Repository를 정의해 보자. (설명을 위한 최소한의 메소드를 지니 간단한 Repository를 선언한다.)

@Repository
public interface ArtistRepository {
    Artist selectArtistByPrimaryKey(Long seq);
}

@Repository
public interface AlbumRepository {
    Album selectAlbumByPrimaryKey(Long seq);
}

@Repository
public interface SongRepository {
    Song  selectSongByPrimaryKey(Long seq);
    List<Song> selectSongByAlbumKey(Long albumSeq);
}

계속해서 각각의 Repository에 대응되는 MyBatis XML에 대해 알아보자.


먼저 가장 간단한 ArtistRepositoryselectArtistByPrimaryKey 메소드에 대한 매핑 XML은 아래와 같다. Artist 객체 자체는 어떠한 연관 객체를 멤버 속성으로 지니지 않으므로 has one 또는 has many와 같은 연관 관계를 고려할 필요는 없다. (물론 이것은 필요에 따라 Artist 객체의 멤버 속성으로 Album List를 지니도록 수정될 수 있다. 이런 경우 매핑 XML 정의도 달라져야 하는데 이것은 다음에 설명할 AlbumRepository 매핑 XML 을 통해 확인할 수 있다.)

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.devop.test.core.repo.artist.ArtistRepository">
    <resultMap id="artistResultMap" type="com.devop.test.core.entity.artist.Artist">
        <id column="seq" property="seq" jdbcType="BIGINT"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <result column="debut_date" property="debutDate" jdbcType="TIMESTAMP"/>
    </resultMap>

    <select id="selectArtistByPrimaryKey" resultMap="artistResultMap" parameterType="java.lang.Long">
        select
            seq, name, debut_date
        from
            artists
        where
            seq = #{seq, jdbcType=BIGINT}
    </select>
</mapper>

AlbumRepository 매핑 XML은 다음과 같다. Album 객체를 기준으로 has one 관계에 있는 Artist 객체, has many 관계에 있는 Song 객체와 연관 되도록 SQL을 설정한다. 이러한 연관 설정은 MyBatis에서 제공하는 <association><collection>으로 정의할 수 있다.


먼저 has one 관계를 설정하기 위한 <association>에 대해 알아보자. 이것은 아래 매핑 XML에서 확인할 수 있듯이 <assocication> 태그로 정의할 수 있는데, 세부적으로는 또 한 번의 추가 Select 구문을 통해 데이터를 가져오는 방법과, Join을 통해 한번에 데이터를 가져오는 방법을 선택할 수 있다. MyBatis에서는 이들을 Nested Select, Nested Results라 한다. 아래 매핑 XML Album 객체와 has one 관계에 있는 Artist 객체를 추가 Select를 통해 가져오도록 설정한 것이다. 이를 위해 <assocication> 태그에 선언된 ‘select’ attribute를 통해 추가 Select SQL 구문을 가리키게 한다.


has many 관계를 설정하기 위한 <collection>역시 <association>과 마찬가지로 추가 Select 구문을 통해 데이터를 가져오는 방법과, Join을 통해 한번에 데이터를 가져오는 방법 중에 선택할 수 있다. (has many 관계에서 left outer join을 통해 한 번에 데이터를 가져오는 경우 left측의 데이터가 우측에 나타나는 데이터의 수만큼 반복되어 나타나는 문제가 있기 때문에 데이터를 한번 더 정제하기 위한 절차가 필요하다.) 아래 매핑 XML Album 객체와 has many 관계에 있는 Song 객체를 추가 Select를 통해 가져오도록 설정한 것이다. 이를 위해 <collection> 태그에 선언된 ‘select’ attribute를 통해 추가 Select SQL 구문을 가리키게 한다.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.devop.test.core.repo.album.AlbumRepository">
    <resultMap id="songResultMap" type="com.devop.test.core.entity.song.Song">
        <id column="seq" property="seq" jdbcType="BIGINT"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <result column="playtime" property="playtime" jdbcType="INTEGER"/>
    </resultMap>

    <resultMap id="artistResultMap" type="com.devop.test.core.entity.artist.Artist">
        <id column="seq" property="seq" jdbcType="BIGINT"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <result column="debut_date" property="debutDate" jdbcType="TIMESTAMP"/>
    </resultMap>

    <resultMap id="albumResultMap" type="com.devop.test.core.entity.album.Album">
        <id column="seq" property="seq" jdbcType="BIGINT"/>
        <result column="title" property="title" jdbcType="VARCHAR"/>
        <result column="stock" property="stock" jdbcType="INTEGER" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
        <result column="issueDate" property="issueDate" jdbcType="TIMESTAMP"/>
        <association column="artist_seq" property="artist" select="selectArtistByPrimaryKey"/>
        <collection column="seq" property="songs" select="selectSongByAlbumKey"/>
    </resultMap>

    <select id="selectSongByAlbumKey" resultMap="songResultMap" parameterType="java.lang.Long">
        select
            seq, album_seq, name, playtime
        from
            songs
        where
            album_seq = #{seq, jdbcType=BIGINT}
    </select>

    <select id="selectArtistByPrimaryKey" resultMap="artistResultMap" parameterType="java.lang.Long">
        select
            seq, name, debut_date
        from
            artists
        where
            seq = #{seq, jdbcType=BIGINT}
    </select>

    <select id="selectAlbumByPrimaryKey" resultMap="albumResultMap" parameterType="java.lang.Long">
        select
            seq, artist_seq, title, stock, issue_date
        from
            albums
        where
            seq = #{seq, jdbcType=BIGINT}
    </select>
</mapper>

마지막으로 SongRepository 매핑 XML을 보자. 주의해서 볼 부분은 songResultMap에서 Song 객체와 has one 관계에 있는 Album 객체를 가져오기 위한 <assocication> 태그 부분이다. 2개의 <association> 태그가 정의되어 있는데 하나는 Join을 통해 한번에 데이터를 가져오는 방법 다른 하나는 추가 Select를 통해 데이터를 가져오는 방법을 정의한 것이다. (2개의 <association> 정의를 동시에 사용할 수는 없으므로 1개는 주석처리 해야한다.)


Join을 통해 한번에 데이터를 가져오는 경우 <association> 태그 내에 ‘resultMap’ attribute가 선언되는데 이것은 Join을 사용한 SQL 실행 결과를 어떻게 Song 객체에 바인딩 시킬지 가리키게 된다. 이것은 전체 결과 컬럼 중에서 Albums 테이블 컬럼에 대해 별칭을 부여하고, Song 객체의 Album 객체 멤버 속성에 결과가 바인딩 되도록 처리한 것이다.


추가 Select를 통해 데이터를 가져오는 경우 앞에서와 마찬가지로 <assocication> 태그에 선언된 ‘select’ attribute를 통해 추가 Select SQL 구문을 가리키게 한다.

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.devop.test.core.repo.song.SongRepository">
    <resultMap id="albumResultMap" type="com.devop.test.core.entity.album.Album">
        <id column="album_seq" property="seq" jdbcType="BIGINT"/>
        <result column="album_title" property="title" jdbcType="VARCHAR"/>
        <result column="album_stock" property="stock" jdbcType="INTEGER" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
        <result column="album_issue_date" property="issueDate" jdbcType="TIMESTAMP"/>
    </resultMap>

    <resultMap id="albumResultMap2" type="com.devop.test.core.entity.album.Album">
        <id column="seq" property="seq" jdbcType="BIGINT"/>
        <result column="title" property="title" jdbcType="VARCHAR"/>
        <result column="stock" property="stock" jdbcType="INTEGER" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
        <result column="issue_date" property="issueDate" jdbcType="TIMESTAMP"/>
    </resultMap>

    <resultMap id="songResultMap" type="com.devop.test.core.entity.song.Song">
        <id column="seq" property="seq" jdbcType="BIGINT"/>
        <result column="name" property="name" jdbcType="VARCHAR"/>
        <result column="playtime" property="playtime" jdbcType="INTEGER"/>
        <association property="album" resultMap="albumResultMap"/>
        <!--association column="album_seq" property="album" select="selectAlbumByPrimaryKey"/-->
    </resultMap>

    <select id="selectSongByPrimaryKey" resultMap="songResultMap" parameterType="java.lang.Long">
        select
            s.seq, s.album_seq, s.name, s.playtime,
            a.seq as album_seq,
            a.artist_seq as album_artist_seq,
            a.title as album_title,
            a.stock as album_stock,
            a.issue_date as album_issue_date
        from
            songs s left outer join albums a on s.album_seq = a.seq
        where
            s.seq = #{seq, jdbcType=BIGINT}
    </select>

    <select id="selectSongByAlbumKey" resultMap="songResultMap" parameterType="java.lang.Long">
        select
            seq, album_seq, name, playtime
        from
            songs
        where
            album_seq = #{albumSeq, jdbcType=BIGINT}
    </select>

    <select id="selectAlbumByPrimaryKey" resultMap="albumResultMap2" parameterType="java.lang.Long">
        select
            seq, artist_seq, title, stock, issue_date
        from
            albums
        where
            seq = #{seq, jdbcType=BIGINT}
    </select>
</mapper>

이로서 음악가, 앨범, 수록곡에 대한 Repository 작성을 완료 했다. 이제 이들 RepositoryDI 받아 사용할 Service를 정의한다.

@Service
public class ArtistService {
    private Logger logger = LoggerFactory.getLogger(getClass());
    @Autowired private ArtistRepository artistRepository;

    @Transactional(readOnly=true)
    public Artist selectArtistByPrimaryKey(Long seq) {
        try {
            return artistRepository.selectArtistByPrimaryKey(seq);
        } catch (DataAccessException e) {
            logger.error(e.getMessage(), e);
            throw e;
        }
    }
}

@Service
public class AlbumService {
    private Logger logger = LoggerFactory.getLogger(getClass());
    @Autowired private AlbumRepository albumRepository;

    @Transactional(readOnly=true)
    public Album selectAlbumByPrimaryKey(Long seq) {
        try {
            return albumRepository.selectAlbumByPrimaryKey(seq);
        } catch (DataAccessException e) {
            logger.error(e.getMessage(), e);
            throw e;
        }
    }
}

@Service
public class SongService {
    private Logger logger = LoggerFactory.getLogger(getClass());
    @Autowired private SongRepository songRepository;

    @Transactional(readOnly=true)
    public Song selectSongByPrimaryKey(Long seq) {
        try {
            return songRepository.selectSongByPrimaryKey(seq);
        } catch (DataAccessException e) {
            logger.error(e.getMessage(), e);
            throw e;
        }
    }

    @Transactional(readOnly=true)
    public List<Song> selectSongByAlbumKey(Long albumSeq) {
        try {
            return songRepository.selectSongByAlbumKey(albumSeq);
        } catch (DataAccessException e) {
            logger.error(e.getMessage(), e);
            throw e;
        }
    }
}

계속해서 사용자 인터페이스 요청을 처리할 간단한 SpringMVC 컨트롤러를 추가해보자.

@Controller
@RequestMapping(value="album")
public class AlbumController {
    @Autowired private AlbumService albumService;

    @RequestMapping(value="index")
    public ModelAndView selectAlbum(@RequestParam(required=true) Long seq) {
        Album album = albumService.selectAlbumByPrimaryKey(seq);
        ModelAndView mav = new ModelAndView();
        mav.addObject("album", album);
        return mav;
    }
}

AlbumController에 의해 최종적으로 사용자에게 출력될 결과물을 담고 있는 ViewJSP를 사용한다. JSP 페이지에서는 Album 객체와 연관 관계에 있는 Artist 객체와 Song 객체 목록을 사용한다.

<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<title>:: 앨범 ::</title>
</head>
<body>
    <h1>앨범정보</h1>

    <h2>앨범명</h2>
    ${album.title}<br/>

    <h2>재생시간</h2>
    ${album.totalPlaytime}<br/>

    <h2>음악가</h2>
    ${album.artist.name}<br/>
</body>
</html>

출력결과는 다음 그림과 같이 Album 객체를 통해 로드 된 Artist 정보와 Song 목록 정보를 사용해 정상적으로 모든 정보가 출력되고 있는 것을 확인할 수 있다.



위 화면을 출력하기 위해 실행 된 SQL은 총 3개이다. 첫 번째는 Album 정보를 조회하기 위한 SQL이며, 두 번째는 조회한 Album Artist 정보를 조회하기 위한 SQL 그리고 마지막으로 조회한 AlbumSong 정보를 조회하기 위한 SQL이다. (이들 SQL은 앞서 설명한 AlbumReposiroty 매핑 XML에 모두 정의되어 있다.)


그런데 이것으로 모두 끝난 것일까? 그렇지 않다. Album 하나를 조회하는데 총 3번의 SQL이 실행된다는 것은 너무 과하다. 만약 100개의 Album을 동시에 조회하는 페이지가 추가된다면 어떻게 될까? 100개의 Album 정보를 가져오기 위한 SQL1회 실행되고 각각의 Album에 대해 Artist 정보와 Song 정보를 가져오기 위해 2회씩 SQL이 실행되면 총 201번의 SQL이 실행될 것이다. (이것이 바로 유명한 N+1 Select 문제이다.)


 N+1 Select 문제

    I. 레코드의 목록을 가져오기 위해 하나의 SQL 구문을 실행한다. (+1 에 해당)

    II. 리턴된 레코드별로, 각각의 상세 데이터를 로드하기 위해 Select 구문을 실행한다. (N 에 해당)


이 문제를 해결하기 위한 방법은 2가지가 있다.


2.1. 지연 로딩(Lazy Loading)

먼저 간단한 설정을 통해 적용할 수 있는 지연 로딩(Lazy Loading)에 대해 알아보자. 지연 로딩은 어떤 추가 정보가 있을 때 그것을 사용하기 직전에 로드 하는 방법을 의미한다. , 앞서 Album 객체에서 Artist 객체와 Song 객체가 사용되지 않는다면 그것들을 사전에 로드 하지 않고 getAlbum(), getSongs() 메소드를 통해 접근이 발생할 때 로드 함으로써 불필요한 SQL 실행을 방지하는 방법이다. 앞서 AlbumController를 통해 출력되는 JSP 페이지에서 재생시간, 음악가 정보를 출력할 필요가 없다면 지연 로딩 기법을 적용해 N+1 Select 문제를 회피할 수 있다.


Hibernate같은 ORM 솔루션에서 지연 로딩은 객체 생명 주기를 포함하는 영속성 컨텍스트(Persistence Context), OSIV(Open Session In View Filter) 등 많은 개념을 이해하고 조심스럽게 사용해야 한다는 어려움이 있다. 하지만 MyBatis에서는 ORM 솔루션에서처럼 복잡한 개념들을 요구하지 않는다. MyBatis에서 지연 로딩을 적용하는 방법은 간단하게 설정 항목 중 lazyLoadingEnabled를 활성화시키고 aggressiveLazyLoading를 비활성화 시키면 된다. aggressiveLazyLoading를 활성화 시키면 지연 로딩이 제대로 작동하지 않는다. (aggressiveLazyLoading는 객체의 일반 getter 메소드가 실행되어도 해당 객체의 모든 연관 객체가 사용될 것이라고 판단하고 미리 추가 정보를 로드해 버린다.) 이와 같은 설정은 MyBatis 전역에 걸쳐 영향을 끼치게 된다. 만약 3.2.6 이상 버전의 MyBatis를 사용한다면 <association> 태그 내에 ‘fetchType’ attribute를 통해 지연 로딩을 활성화할 수 있다. 이 설정은 전역 lazyLoadingEnabled를 대체한다.


이처럼 MyBatis에서 지연 로딩은 직관적이고 설정도 쉽다. 그러나 지연 로딩이 모든 문제를 해결해주지는 못한다는 것을 이해하고 있어야 한다. 지연 로딩은 불필요한 정보를 사전에 로드 하는 것을 방지하는 것일 뿐 어떤 정보가 반드시 필요하다면 해당 정보는 결국 로드 될 것이다. 예를 들어 앞서 살펴본 JSP 페이지에서 재생시간, 음악가 정보가 반드시 필요하다면 N+1 Select 문제는 지연 로딩으로는 해결할 수 없게 된다.


2.2. 관계를 위한 내포된 결과(Nested Results)

지연 로딩을 대신할 수 있는 다른 방법은 Join을 이용해 필요한 데이터를 한번에 모두 가져오는 것이다. (MyBatis에서 이것을 Nested Results라 한다.) 이 방법은 특히 has one 관계에 놓여 있는 2개의 객체 사이에서 유용하게 사용할 수 있다. 예를 들어 Song 객체와 has one 관계에 있는 Album객체가 여기에 해당된다. Song 정보가 사용될 때 Album정보도 함께 사용된다고 하면 추가 Select를 통해 Album정보를 로드 하는 것보다 Song 정보를 로드 할 때 Album 정보도 같이 한번에 로드 하는 것이 효율적이다. 이것을 위해 Song left측에 두고 left outer join을 걸어 SQL 구문을 작성할 수 있을 것이다. 앞서 살펴본 SongRepository 매핑 XML을 보면 2개의 <association> 태그가 정의되어 있는데 <association> 태그 내에 ‘resultMap’ attribute가 선언되어 있는 것이 바로 Join을 통해 Song 정보와 Album 정보를 한번에 가져오기 위한 방법을 보여준다.


Nested Resultshas many 관계에 있는 객체들 사이에서도 사용이 가능하다. 사용 방법은 <association> 때와 일치한다. 다만, 한 가지 주의할 점은 has many 관계에서 left outer join을 통해 한 번에 데이터를 가져오는 경우 left측의 데이터가 우측에 나타나는 데이터의 수만큼 반복되어 나타난다는 것을 이해하고 있는 것이다. 예를 들어 Album 정보와 Song 정보를 한번에 가져오는 SQL은 다음과 같다.

select 
	a.seq, a.artist_seq, a.title, a.stock, a.issue_date,
	s.seq as songs_seq,
	s.album_seq,
	s.name,
	s.playtime
from 
	albums a left outer join songs s on a.seq = s.album_seq
where
	a.seq = ?

그리고 위 SQL이 실행 된 결과는 아래와 같이 Album 정보가 Row 마다 반복해서 나타나게 되는 것을 확인할 수 있다.



최종적으로 위 실행 결과는 <association> 태그 내에 선언된 ‘resultMap’ attribute가 가리키는 바인딩 전략에 따라 처리되고, 하나의 Album 객체와 List에 담기는 복수의 Song 객체에 바인딩 될 것이다.



3. 제한 사항

지금까지 알아본 것처럼 MyBatis에서 제공하는 <association><collection>을 이해하고 있으면 ORM 솔루션을 사용할 때처럼 객체 중심으로 생각하고 애플리케이션을 개발하는데 큰 도움을 얻을 수 있다. 그러나 근본적으로 MyBatis ORM이 아닌 SQL Mapper이기 때문에 여기에는 몇 가지 제한 사항이 따른다.


3.1. resultMap 및 SQL 정의 중복

AlbumRepository 매핑 XML SongRepository 매핑 XML을 비교해보자. resultMap 정의뿐만 아니라 비슷하거나 혹은 완전히 똑같은 SQL이 반복해서 나타나는 것을 확인할 수 있다. 연관 관계 놓여 있는 객체들 사이의 매핑 XML에서 <association><collection> 정의를 추가하기 위해 resultMap SQL 정의가 중복되는 것은 피할 수 없다. 이것은 결국 데이터베이스 스키마 변경에 따른 매핑 XML 수정 작업을 더욱 복잡하고 어렵게 한다.


3.2. 일관적이지 않은 객체 그래프

MyBatis<association><collection>를 통한 객체 모델링에서 나타날 수 있는 중요한 제약 사항 중 하나로 일관적이지 않은 객체 그래프 문제를 꼽을 수 있다. Album 객체와 Song 객체 사이의 관계를 통해 이 문제를 자세히 알아보자.


Album 객체는 앞서 설명한 대로 has many 관계에 놓여 있는 복수의 Song 객체를 List에 담고 있고, 이 관계는 <collection>를 통해 정의했다. 그리고 Album 객체는 getTotalPlaytime() 이라는 메소드를 통해 해당 Album 객체가 포함하고 있는 Song 객체들의 플레이 시간 총 합을 구할 수 있다. 만약 지연 로딩이 설정 되어 있다면 Song 객체 List에 대한 접근이 최초 발생할 때 추가 Select SQL을 통해 Song 객체 정보를 로드 하게 될 것이고, 결과적으로 getTotalPlaytime() 메소드는 올바른 값을 반환하게 된다그런데 Album 객체가 Song 객체를 통해 로드 되었다면 (앞서 살펴본 것처럼 Song 객체는 has one 관계에 있는 Album 객체에 대해 <association>를 통해 관계를 정의하고 정보를 로드 할 수 있다.) getTotalPlaytime() 메소드는 0을 반환할 것이다. 이것은 실제 Song 객체 List의 플레이 시간 총 합이 0이기 때문이 아니라 Album 객체와 has many 관계에 놓여 있는 Song 객체 List가 올바르게 로드 되지 않았기 때문이다. (이와 유사하게 Song 객체를 통해 로드 된 Album 객체는 Artist 객체를 로드 할 수 없다.)


결과를 통해 알 수 있는 것은 AlbumRepository를 통해 독립적으로 로드 된 Album 객체와 Song 객체를 통해 간접적으로 로드 된 Album 객체는 서로 다른 객체 그래프를 지니고 있다는 사실이다. , Song 객체를 통해 간접적으로 로드 된 Album 객체는 Song 객체 List를 로드 할 수 있는 능력이 없다. (Artist 객체 또한 로드 할 수 없다. 즉 연관 관계에 놓여있는 객체들 모두 로드 할 수 있는 능력이 없다.) 이것은 SongRepository의 매핑 XML에서 albumResultMap 정의를 살펴보면 그 이유를 알 수 있다. (아래 SongRepository의 매핑 XML 내용의 일부는 Join을 통해 Song 객체와 함께 한번에 Album 객체를 로드 할 때 사용되는 albumResultMap과 추가 Select SQL을 통해 Album 객체를 로드 할 때 사용되는 albumResultMap2 정의를 보여준다.)

<resultMap id="albumResultMap" type="com.devop.test.core.entity.album.Album">
        <id column="album_seq" property="seq" jdbcType="BIGINT"/>
        <result column="album_title" property="title" jdbcType="VARCHAR"/>
        <result column="album_stock" property="stock" jdbcType="INTEGER" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
        <result column="album_issue_date" property="issueDate" jdbcType="TIMESTAMP"/>
</resultMap>

<resultMap id="albumResultMap2" type="com.devop.test.core.entity.album.Album">
    <id column="seq" property="seq" jdbcType="BIGINT"/>
    <result column="title" property="title" jdbcType="VARCHAR"/>
    <result column="stock" property="stock" jdbcType="INTEGER" typeHandler="org.apache.ibatis.type.EnumOrdinalTypeHandler"/>
    <result column="issue_date" property="issueDate" jdbcType="TIMESTAMP"/>
</resultMap>

Join SQL 결과 reusltMap (albumResultMap), 추가 Select SQL resultMap (albumResultMap2) 모두 단순히 Album 객체의 일반 속성 항목에 대한 바인딩 규칙 만 있을 뿐 Album 객체와 has many 관계에 있는 Song 객체에 대한 <collection> 정의는 찾아볼 수 없다. 따라서 당연히 Song 객체를 통해 로드 된 Album 객체는 Song 객체 List에 대한 초기화가 불가능하게 된다.


일관적이지 않은 객체 그래프는 분명 매우 심각한 문제이다. 어떤 Album 객체가 주어졌을 때 Album 객체가 어떻게 로드 되었는지에 따라서 메소드 실행 결과값이 달라진다면 OOP의 원칙을 심각하게 위배하고 있음은 물론이고, 그 어떤 개발자도 올바르게 작동하는 애플리케이션을 개발할 수 없을 것이기 때문이다.


당장 이 문제를 해결하기 위해서는 SongRepository 매핑 XMLalbumResultMap 정의에 Album 객체와 has many 관계에 있는 Song 객체 List를 처리하기 위한 <collection>을 정의를 추가해야 한다하지만 이 방법은 연관 관계에 놓여 있는 모든 객체들의 객체 그래프의 동일함을 보장하기 위해 관련된 모든 매핑 XML 파일에 <assocication> 또는 <collection>정의를 추가해야 한다는 것을 의미한다. 연관 관계에 놓여 있는 객체가 서로 순환 참조하고 있을 때는 매핑 XML 정의를 더욱 복잡하게 만든다. 이것은 결국 앞서 설명한 SQL resultMap 정의 중복문제를 더욱 심각하게 하는 것에 지나지 않는다.


따라서 이 문제를 보다 효율적으로 해결하기 위해서는 다른 접근 방식을 생각해 보아야 한다. 다음과 같은 질문들을 던져본다.


I.       애플리케이션 개발 과정에서 Song 객체를 통해 Album 객체를 로드 하는 패턴이 얼마나 빈번하게 발생하는가?

II.      그러한 접근 패턴이 애플리케이션 성능을 향상 시키고 있는가?

III.    그러한 접근 패턴이 올바른 사용자 경험을 제공하는가?


만약 이러한 질문을 통해 Song 객체가 has one 관계에 놓여 있는 Album 객체를 반드시 포함할 필요가 없다고 판단되면 Song Class 멤버 속성에서 Album 객체 멤버 속성을 제거한다. 어쩔 수 없는 이유로 반드시 포함해야 한다면 객체 그래프의 동일성을 보장하기 위해 SongRepository 매핑 XML을 수정한다. 중복 정의 문제가 발생하는 것은 피할 수 없지만 일관성 없는 객체 그래프로 인하여 발생할 수 있는 버그가 더욱 치명적이다.



4. 결론

MyBatis는 완전한 ORM 솔루션이 아니다. MyBatis에서 제공하는 <association><collection>을 통해 ORM 솔루션을 사용할 때와 유사한 효과를 가져올 수 있으나 여기에는 몇 가지 치명적인 문제점이 존재하고 있음을 확인했다. 이러한 문제점을 완화 시키기 위해 연관 객체들 사이의 상호 참조 및 포함 관계를 제한할 수 있으나 이것이 근본적으로 올바른 해결 방법은 아니다. 이러한 고려 사항 자체가 이미 데이터 구조에서 벗어난 순수한 객체 모델링을 방해하고 있는 셈이기 때문이다그러나 이러한 특징들이 MyBatisORM 솔루션보다 나쁘다는 것을 의미하지는 않는다. MyBatisMyBatis 대로 오랜 시간 동안 충분히 검증된 훌륭한 SQL Mapper 솔루션이며, 그에 걸맞은 사용법이 있기 때문이다. 앞에서 언급한 문제점들은 SQL Mapper 솔루션으로서 MyBatisORM 솔루션처럼 사용하려 했기 때문에 발생했던 부작용들이다.


저작자 표시 비영리 변경 금지
신고
Posted by devop
프로그래밍/JAVA2013.02.01 11:40

포함된 메소드


  • Date getNextYear(Date date) - 1년 후의 날을 구한다.
  • Date getPreviousYear(Date date) - 1년 전의 날을 구한다.
  • Date getNextMonth(Date date) - 한달 후의 날을 구한다.
  • Date getPreviousMonth(Date date) - 한달 전의 날을 구한다.
  • Date getNextWeek(Date date) - 7일 후의 날을 구한다.
  • Date getPreviousWeek(Date date) - 7일 전의 날을 구한다.
  • Date getNextDate(Date date) - 다음 날을 구한다.
  • Date getPreviousDate(Date date) - 하루 전 날을 구한다.
  • Date getFirstDateOfWeek(Date date) - 해당 주의 첫번째 날을 구한다.
  • Date getLastDateOfWeek(Date date) - 해당 주의 마지막 날을 구한다.
  • Date getFirstDateOfMonth(Date date) - 해당 연도 달의 첫번째 날을 구한다.
  • Date getFirstDateOfMonth(int year, int month) - 해당 연도 달의 첫번째 날을 구한다.
  • Date getLastDateOfMonth(Date date) - 해당 연도 달의 마지막 날을 구한다.
  • Date getLastDateOfMonth(int year, int month) - 해당 연도 달의 마지막 날을 구한다.
  • Date minimized(Date date) - 시,분,초를 모두 최소치로 초기화한다.
  • Date maximize(Date date) - 시,분,초를 모두 최대치로 초기화한다.

소스코드
import java.util.Calendar;
import java.util.Date;

import org.apache.commons.lang.time.DateUtils;

public class DateUtilsEx {

	public static Date getDate(int year, int month, boolean truncate) {
    	Calendar cal = Calendar.getInstance();
    	cal.set(Calendar.YEAR, year);
    	cal.set(Calendar.MONTH, month);
    	
    	if( truncate ) {
    		cal = DateUtils.truncate(cal, Calendar.MONTH);
    	}
    	
    	return cal.getTime();
    }

    public static Date getDate(int year, int month) {
    	return getDate(year, month, false); 
    }
    
    /**
	 * 1년 후의 날을 구한다.
	 */
	public static Date getNextYear(Date date) {
    	Calendar cal = Calendar.getInstance();
    	cal.setTime(date);
    	
    	cal.add(Calendar.YEAR, 1);
    	
    	return cal.getTime();
    }

	/**
	 * 1년 전의 날을 구한다.
	 */
    public static Date getPreviousYear(Date date) {
    	Calendar cal = Calendar.getInstance();
    	cal.setTime(date);
    	
    	cal.add(Calendar.YEAR, -1);
    	
    	return cal.getTime();
    }
    
    /**
	 * 한달 후의 날을 구한다.
	 */
    public static Date getNextMonth(Date date) {
    	Calendar cal = Calendar.getInstance();
    	cal.setTime(date);
    	
    	cal.add(Calendar.MONTH, 1);
    	
    	return cal.getTime();
    }
    
    /**
	 * 한달 전의 날을 구한다.
	 */
	public static Date getPreviousMonth(Date date) {
    	Calendar cal = Calendar.getInstance();
    	cal.setTime(date);
    	
    	cal.add(Calendar.MONTH, -1);
    	
    	return cal.getTime();
    }
	
	/**
	 * 7일 후의 날을 구한다.
	 */
	public static Date getNextWeek(Date date) {
    	Calendar cal = Calendar.getInstance();
    	cal.setTime(date);
    	
    	cal.add(Calendar.DATE, 7);
    	
    	return cal.getTime();
    }
    
	/**
	 * 7일전의 날을 구한다.
	 */
	public static Date getPreviousWeek(Date date) {
    	Calendar cal = Calendar.getInstance();
    	cal.setTime(date);
    	
    	cal.add(Calendar.DATE, -7);
    	
    	return cal.getTime();
    }
	
	/**
	 * 다음 날을 구한다.
	 */
	public static Date getNextDate(Date date) {
    	Calendar cal = Calendar.getInstance();
    	cal.setTime(date);
    	
    	cal.add(Calendar.DATE, 1);
    	
    	return cal.getTime();
    }
    
	/**
	 * 하루 전 날을 구한다.
	 */
	public static Date getPreviousDate(Date date) {
    	Calendar cal = Calendar.getInstance();
    	cal.setTime(date);
    	
    	cal.add(Calendar.DATE, -1);
    	
    	return cal.getTime();
    }
	
	/**
	 * 해당 주의 첫번째 날을 구한다.
	 */
	public static Date getFirstDateOfWeek(Date date) {
		Calendar cal = Calendar.getInstance();
    	cal.setTime(date);
    	
    	int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
    	cal.add(Calendar.DATE, (dayOfWeek-1)*-1);
    	
    	return cal.getTime();
	}
	
	/**
	 * 해당 주의 마지막 날을 구한다.
	 */
	public static Date getLastDateOfWeek(Date date) {
		Calendar cal = Calendar.getInstance();
    	cal.setTime(date);
    	
    	int dayOfWeek = cal.get(Calendar.DAY_OF_WEEK);
    	cal.add(Calendar.DATE, 7-dayOfWeek);
    	
    	return cal.getTime();
	}
	
	/**
	 * 해당 연도 달의 첫번째 날을 구한다.
	 */
	public static Date getFirstDateOfMonth(Date date) {
		Calendar cal = Calendar.getInstance();
    	cal.setTime(date);
    	
    	cal.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.getActualMinimum(Calendar.DAY_OF_MONTH));
		return cal.getTime();
	}
	
	/**
	 * 해당 연도 달의 첫번째 날을 구한다.
	 */
	public static Date getFirstDateOfMonth(int year, int month) {
		Calendar cal = Calendar.getInstance();
    	
		cal.set(Calendar.YEAR, year);
    	cal.set(Calendar.MONTH, month-1);
    	cal.set(Calendar.DATE, cal.getActualMinimum(Calendar.DAY_OF_MONTH));
		
		return minimized(cal.getTime());
	}
	
	/**
	 * 해당 연도 달의 마지막 날을 구한다.
	 */
	public static Date getLastDateOfMonth(Date date) {
		Calendar cal = Calendar.getInstance();
    	cal.setTime(date);
    	
    	cal.set(cal.get(Calendar.YEAR), cal.get(Calendar.MONTH), cal.getActualMaximum(Calendar.DAY_OF_MONTH));
		return cal.getTime();
	}
	
	/**
	 * 해당 연도 달의 마지막 날을 구한다.
	 */
	public static Date getLastDateOfMonth(int year, int month) {
		Calendar cal = Calendar.getInstance();
    	
		cal.set(Calendar.YEAR, year);
    	cal.set(Calendar.MONTH, month-1);
    	cal.set(Calendar.DATE, cal.getActualMaximum(Calendar.DAY_OF_MONTH));
		
		return maximize(cal.getTime());
	}
	
	/**
	 * 시,분,초를 모두 최소치로 초기화한다.
	 */
	public static Date minimized(Date date) {
		return DateUtils.truncate(date, Calendar.DATE);
	}
	
	/**
	 * 시,분,초를 모두 최대치로 초기화한다.
	 */
	public static Date maximize(Date date) {
		Calendar cal = Calendar.getInstance();
    	cal.setTime(date);
    	
    	cal.set(Calendar.HOUR_OF_DAY, 23);
    	cal.set(Calendar.MINUTE, 59);
    	cal.set(Calendar.SECOND, 59);
		
		return cal.getTime();
	}
	
}


저작자 표시 비영리 변경 금지
신고
Posted by devop
프로그래밍/JAVA2013.01.30 13:57

oval는 java object를 어노테이션 기반으로 손쉽게 검증할 수 있도록 하는 validation framework입니다.


OVal is a pragmatic and extensible validation framework for any kind of Java objects (not only JavaBeans). Constraints can be declared with annotations (@NotNull, @MaxLength), POJOs or XML. Custom constraints can be expressed as custom Java classes or by using scripting languages such as JavaScript, Groovy, BeanShell, OGNL or MVEL. Besides field/property validation OVal implements Programming by Contract features by utilizing AspectJ based aspects. This for example allows runtime validation of method arguments.


maven을 통한 설치

<dependency>
	<groupId>net.sf.oval</groupId>
	<artifactId>oval</artifactId>
	<version>1.81</version>
</dependency>

또는 아래 링크에서 라이브러리 및 문서를 다운로드할 수 있습니다.


http://oval.sourceforge.net/


OVal를 이용해 java object validation을 수행하기 위한 첫 번째 단계는 아래 예제 처럼 class field에 어노테이션을 사용해 제약조건을 설정하는 것입니다. 제약조건은 class field외에도 getter 메소드에 사용할 수 있습니다.)


OVal는 아래 예제에서 사용된 제약조건외에도 pre-built 된 다양한 제약조건을 제공합니다.

(사용가능한 제약조건 어노테이션은 net.sf.oval.constraint package에서 확인할 수 있습니다.)

public class Person {

	@NotNull
	@NotEmpty
	@Length(max=10)
	private String name;
	
	@NotNull
	@Digits
	private Integer age;
	
	@NotNull
	@NotEmpty
	@Length(max=10)
	private String city;

	public String getName() {
		return name;
	}

	public void setName(String name) {
		this.name = name;
	}

	public Integer getAge() {
		return age;
	}

	public void setAge(Integer age) {
		this.age = age;
	}

	public String getCity() {
		return city;
	}

	public void setCity(String city) {
		this.city = city;
	}
}

제약조건들의 AND 또는 OR와 같은 logical한 제약조건 검사를 실행하기 위해 expression language를 사용할 수도 있습니다.


 @Assert(expr = "_value ==_this.deliveryAddress || _value == _this.invoiceAddress", lang = "jexl")


  • _value : contains the value to validate (field value or getter return value)
  • _this : is a reference to the validated object


위 예제에서 age의 경우 항상 0보다 커야 하므로 @Assert를 다음과 같이 활용할 수 있습니다.


@Assert(expr="_value>0", lang="jexl")
private Integer age;


사용가능한 expression language는 


  • bsh or beanshell for BeanShell
  • groovy for Groovy
  • jexl for JEXL
  • js or javascript for JavaScript (via Mozilla Rhino)
  • mvel for MVEL
  • ognl for OGNL
  • ruby or jruby for Ruby (via JRuby)

위와 같으며, 해당 language의 라이브러리가 필요합니다.
필자의 경우 apache commons의 jexl을 주로 사용합니다.

<dependency>
    	<groupId>org.apache.commons</groupId>
	<artifactId>commons-jexl</artifactId>
	<version>2.1.1</version>
</dependency>

자 이제 제약조건을 설정한 object를 실제 validation 하는 방법은 다음과 같습니다.

Validator validator = new Validator();

Person p = new Person();

// collect the constraint violations
List<ConstraintViolation> violations = validator.validate(p);

if( violations.size() > 0 ) {
	for(ConstraintViolation violation : violations) {
		System.out.println(violation.getMessage());
	}
}

object에 제약조건에 위반되는 field값이 있다면 validator.validate 메소드의 실행 결과 리스트 사이즈는 0보다 큽니다. 


위 코드에서 Person object는 name, age, city filed에 대한 값이 null 이기 때문에 위 예제에서 설정한 제약조건을 위반하였고, 다음과 같은 메세지를 확인할 수 있습니다.


  • Person.name cannot be null
  • Person.age cannot be null
  • Person.age does not satisfy condition: _value>0
  • Person.city cannot be null

OVal는 위에 설명한 기능외에도 다양한 방식을 통해 object validation을 수행할 수 있고, Spring 등과 같은 프레임워크에 통합도 가능합니다. 보다 자세한 정보는 http://oval.sourceforge.net/ 에서 확인할 수 있습니다.

저작자 표시 비영리 변경 금지
신고
Posted by devop
프로그래밍/JAVA2012.12.17 14:11

ComputeFireTimesBetween 메소드를 통해 cronExpression에 정의된 규칙에 따라 trigger가 활성화되는 시각을 미리 확인할 수 있다.


ComputeFireTimesBetween Method :

Returns a list of Dates that are the next fire times of a Trigger that fall within the given date range. The input trigger will be cloned before any work is done, so you need not worry about its state being altered by this method. NOTE: if this is a trigger that has previously fired within the given date range, then firings which have already occured will not be listed in the output List.


다음은 ComputeFireTimesBetween 메소드의 예제 소스이다.

CronTriggerImpl cron = new CronTriggerImpl();

cron.setStartTime(new Date());
cron.setCronExpression("0/10 0 * * * ?"); /* 10초 마다 실행 */

BaseCalendar calendar = new BaseCalendar();
List<Date> result = TriggerUtils.computeFireTimesBetween(cron, calendar, 
new Date(), DateUtils.add(new Date(), Calendar.DATE, 1));

for (Date date : result) {
     System.out.println(date);
}
quartz에 대한 보다 자세한 정보는 아래 링크 참조


저작자 표시 비영리 변경 금지
신고
Posted by devop
프로그래밍/JAVA2011.02.14 15:23
1. Jersey
Jersey는 JAX-RS, 즉 RESTful 웹 서비스용 자바 API(JSR-311)의 오픈 소스 구현체 입니다. JAX-RS는 자바 어노테이션 API로 REST 아키텍처 스타일을 따르는 자바 기반 RESTful 웹 서비스를 손쉽게 작성하도록 지원합니다.



2. Jersey 사용하기
Jersey를 이용해 RESTful 웹 서비스를 구축하기 위해 다음과 같은 파일을 위 사이트에서 다운로드 합니다. 
(이 글을 쓰는 시점에서 jersey의 다운로드 버전은 1.5 입니다.)


  • asm-3.1.jar 
  • jersey-core-1.5.jar
  • jersey-server-1.5.jar
  • jersey-json-1.5.jar
  • jersey-spring-1.5.jar

jersey-json-1.5.jar와 jersey-spring-1.5.jar는 필수 요소는 아니지만 가각, JSON 객체의 JAXB 지원과 Jersey와 Spring의 통합을 위해 필요한 파일 입니다.

그 외 Spring3와 필요한 파일을 다운받아 다음 그림과 같이 라이브러리에 추가합니다.


위 그림에서는 Jersey와 Spring외에 log4j, iBatis, Apache-Commons 파일이 추가되어 있습니다.

다음으로 web.xml을 다음과 같이 편집합니다.


  sample
  Spring-Jersey sample project
  
  
  	contextConfigLocation
  	/WEB-INF/sample-context.xml
  
  
  	org.springframework.web.context.ContextLoaderListener
  
  
  	Jersey Spring Servlet
  	com.sun.jersey.spi.spring.container.servlet.SpringServlet
  	
  		com.sun.jersey.config.property.packages
  		org.itams.sample.rest;
  	
  
  
  	Jersey Spring Servlet
  	/rest/*
  
  
  	encodingFilter
  	org.springframework.web.filter.CharacterEncodingFilter
  	
  		encoding
		UTF-8
  	
  
  
  
    index.html
  

org.itams.sample.rest 패키지 이하의 클래스를 Resource를 사용할 것이며, applicationContext로 sample-context.xml를 사용합니다.

다음은 sample-context.xml의 내용입니다.




	
	
	
	
        
            
                classpath:jdbc.properties
            
        
    
    
    
		
        
        
        
        
    
    
    
    
    
     
    	
    	
    
	
	 
    	
    
    
    
    
    
		
    
    
    
    
    
	
		
	
    
    
    
    
	
		
			
		
	

Spring + iBatis를 DAO 구현은 다음과 같습니다.
package org.itams.sample.rest.hello.dao;

import org.itams.sample.rest.hello.Hello;

public interface HelloDAO {

	public Hello getHello();
	
}
package org.itams.sample.rest.hello.dao;

import org.itams.sample.rest.hello.Hello;
import org.springframework.orm.ibatis.support.SqlMapClientDaoSupport;

public class HelloIBatisDAO extends SqlMapClientDaoSupport implements HelloDAO {

	@Override
	public Hello getHello() {
		return (Hello)getSqlMapClientTemplate().queryForObject("selectHello");
	}

}
JAXB를 통해 XML 또는 JSON객체로 마샬링/언마샬링을 지원하는 POJO객체 Hello는 다음과 같습니다.
package org.itams.sample.rest.hello;

import javax.xml.bind.annotation.XmlRootElement;

@XmlRootElement(name="Hello")
public class Hello {

	private String message;

	public String getMessage() {
		return message;
	}

	public void setMessage(String message) {
		this.message = message;
	}
	
}

마지막으로  RESTful 웹 서비스를 URL을 통해 노출하는 인터페이스 클래스는 다음과 같습니다.
package org.itams.sample.rest.hello.service;

import org.itams.sample.rest.hello.Hello;

public interface HelloService {

	public Hello getHello(String accept);
	
}
package org.itams.sample.rest.hello.service;

import javax.ws.rs.GET;
import javax.ws.rs.HeaderParam;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;

import org.apache.log4j.Logger;
import org.itams.sample.rest.hello.Hello;

@Path("/hello")
public class HelloServiceImpl implements HelloService {

	private Logger logger = Logger.getLogger("org.itams.sample.service");

	private HelloServiceDelegate delegate;
	
	public HelloServiceImpl(HelloServiceDelegate delegate) {
		this.delegate = delegate;
	}

	@Override
	@GET
	@Produces({MediaType.APPLICATION_XML, MediaType.APPLICATION_JSON})
	public Hello getHello(@HeaderParam("Accept") String accept) {
		logger.info("Method: GET, Accept: "+accept);
		return delegate.getHello();
	}
	
}
package org.itams.sample.rest.hello.service;

import org.itams.sample.rest.hello.Hello;
import org.itams.sample.rest.hello.dao.HelloDAO;

public class HelloServiceDelegate {

	private HelloDAO dao;
	
	public HelloServiceDelegate(HelloDAO dao) {
		this.dao = dao;
	}
	
	public Hello getHello() {
		return dao.getHello();
	}
	
}
RESTClient를 통해 RESTful 서비스의 정상 구동을 확인합니다.


서버로부터 리턴받을 데이터의 mime-type을 Accept로 명시하여 사용할 수 있습니다.
Accept가 생략될 경우 데이터의 기본 mime-type은 application/xml로 처리됩니다.
신고
Posted by devop
프로그래밍/JAVA2010.04.30 10:42
Javadoc은 java 소스파일 내의 선언 및 주석을 해석해, 클래스, 인터페이스, 생성자 , 메소드, 및 필드에 도착해 기술한 HTML 페이지 세트를 생성하는 툴입니다. Javadoc의 주석은 /** 로 시작하여 */로 끝나야 하며 각 라인은 하나 이상의 *로 시작해야 하는 규칙을 가지고 있습니다. 또한 주석내에 HTML 태그를 포함할 수 있습니다.

JAutoDoc은 Javadoc을 이클립스에서 편리하게 사용하기 위한 플러그인 입니다.


이클립스에서 Help > Install New Softwere를 선택하고 다음과 같은 주소를 입력하면 JAutoDoc을 설치할 수 있습니다.

 http://jautodoc.sourceforge.net/update/


설치가 완료되면 이클립스를 재시작합니다. 

소스파일에 Javadoc주석을 생성하기 위해 다음과 같이 오른쪽 마우스를 클릭해 JAutoDoc > Add Javadoc을 선택합니다.


다음과 같이 소스파일에 Javadoc주석이 생성된 것을 확인할 수 있습니다.


Javadoc의 주요 주석태그는 다음과 같습니다.

  • @author : 클래스나 인터페이스의 제작자 표시
  • @version : 버전정보
  • @return : 메소드가 void형이 아닌경우 return value type을 기술
  • @exception : 메소드가 발생 시킬수 있는 예외를 기술
  • @throws : @exception Tag와 동일
  • @deprecated : 다음버젼에서 폐기된 메소드를 알림
  • @param : 매개변수에 대한 설명(@param 변수 설명의 형태)
  • @serial : 기본적으로 직렬화 할 수 있는 클래스의 멤버를 설명
  • @see class_name#member  : 클래스 , 인터페이스, 메소드, 생성자 혹은 URL에 대한 전후참조표시
  • @since : Tag를 가진 객체가 언제 추가 되었는지 명시
  • {@link class_name#member label} : 메소드나 필드의 상호 참조에 대한 링크를 표시

* @see의 경우 Javadoc의 See Also: 문장과 링크가 만들어지나, @link의 경우 주석내에 링크를 생성합니다.

Javadoc의 주석 작성법은 다음과 같습니다.

  • method 상단에 /** .. */ 주석을 추가하면, Javadoc의 대상이된다.
  • comment 는 반드시 /** 으로 시작해야한다. /* 은 Javadoc의 대상이 아니다.
  • method 설명은 html 형식으로 출력된다. 즉, 줄바꿈을 하려면 <br/> 태그를 사용해야한다.
  • @param, @return, @throws 는 method 에서 필수속성이다.
  • @throws는 사용자가 처리해야할 예외이다. 따라서 이 예외가 언제 발생하는지에 대해 반드시 설명이 필요하다.
  • @return 에서 반환타입에 대해서 기술할 필요가 없다.
  • interface가 존재한다면, 구현체에서는 주석을 생성할 필요가 없다.
    interface에서 정의된 method의 의도대로 구현을 한 것이므로, interface에서의 주석만으로 충분하다.
    단, interface 에 정의되어 있지 않은 구현체의 method 는 주석을 필요로 한다.
  • 주석 작성시, 다른 클래스를 참조할 경우에는 {@link } 를 사용한다.
    이 것은 Javadoc에서 하이퍼링크를 생성한다.
  • 주석 작성시, code를 기술할 경우엔, <pre> 태그를 사용한다.

보다 자세한 내용은 http://java.sun.com/j2se/javadoc/index.jsp 에서 확인할 수 있습니다.

이클립스에서 프로젝트 > Export > Java > Javadoc을 통해 Javadoc를 생성할 수 있습니다.
한글처리를 위헤 VM option으로 다음을 입력합니다.

 -locale ko_KR -encoding UTF-8 -charset UTF-8 -docencoding UTF-8
신고
Posted by devop
프로그래밍/JAVA2010.04.22 11:03
이클립스에는 JUnit이 기본적으로 포함되어 있기 때문에 쉽게 테스트 케이스를 작성하고, 테스트를 해볼 수 있습니다.

아래 그림과 같이 Java 프로젝트를 생성하고 Number라는 클래스를 작성합니다.


Number클래스는 JUnit을 통해 테스트를 실행하게 될 클래스이며, test는 테스트 클래스들이 들어갈 패키지 입니다.(패키지명은 자유롭게 지정할 수 있습니다.)

Number 클래스의 코드는 다음과 같습니다.
public class Number {

	private int value;
	
	public Number() {
		this(0);
	}
	
	public Number(int value) {
		this.value = value;
	}
	
	public int add(int rhs) {
		return value += rhs;
	}
	
	public int minus(int rhs) {
		return value -= rhs;
	}
	
	public int multiply(int rhs) {
		return value *= rhs;
	}
	
	public int divide(int rhs) {
		return value /= rhs;
	}
	
	public int getValue() {
		return value;
	}
	
}
테스트 클래스를 작성하기 위해 프로젝트에 JUnit을 추가해야 합니다.
프로젝트의 Properties를 열고 Java Build Path에서 Add Library 버튼을 클릭해 JUnit을 프로젝트에 추가할 수 있습니다.



선택할 수 있는 JUnit 은 버전3과 버전4가 있는데 본 예제에서는 4를 선택하도록 하겠습니다.


아래 그림과 같이 JUnit 라이브러리가 프로젝트에 추가된 것을 확인할 수 있습니다.


Number 클래스에서 오른쪽 마우스를 클릭해 New - JUnit Test Case를 클릭합니다.

아래 그림과 같이 Package를 Number.test로 지정하고 Next를 클릭합니다.


아래 그림과 같이 테스트를 수행할 Number 클래스를 체크하고 Finish 버튼을 클릭하면 테스트 클래스가 생성됩니다.


테스트 코드는 다음과 같습니다.
package Number.test;

import static org.junit.Assert.*;

import org.junit.Test;

import Number.Number;

public class NumberTest {

	@Test
	public void testNumber() {
		Number num = new Number();
		assertEquals(0, num.getValue());
	}

	@Test
	public void testNumberInt() {
		Number num = new Number(10);
		assertEquals(10, num.getValue());
	}

	@Test
	public void testAdd() {
		Number num = new Number(10);
		assertEquals(20, num.add(10));
	}

	@Test
	public void testMinus() {
		Number num = new Number(10);
		assertEquals(5, num.minus(5));
	}

	@Test
	public void testMultiply() {
		Number num = new Number(5);
		assertEquals(25, num.multiply(5));
	}

	@Test
	public void testDivide() {
		Number num = new Number(20);
		assertEquals(2, num.divide(10));
	}

	@Test
	public void testGetValue() {
		Number num = new Number(20);
		assertEquals(20, num.getValue());
	}

}
테스트를 실행하기 위해 NumberTest 클래스에서 오른쪽 마우스를 클릭해 Run as - JUnit Test를 클립합니다.

테스트가 실행되고 아래 그림과 같이 모든 테스트가 성공했음을 확인할 수 있습니다.


만약 테스트가 실패하면 아래 그림과 같이 실패한 테스트와 메세지를 확인할 수 잇습니다.


신고
Posted by devop
TAG java, JUnit, TDD
프로그래밍/JAVA2010.04.21 15:56
JDK 5.0 릴리즈에는 어노테이션(annotation)이라 불리는 메타데이터 기능이 도입되었다. 어노테이션은 코드 조각의 작성자 명이나 컴파일러가 특정 오류를 억제하도록 지시하는 것과 같이 프로그램의 일부가 아닌 프로그램에 관한 데이터를 제공해 준다. 어노테이션은 코드가 어떻게 수행되는 것에는 아무런 영향을 주지 않는다.


1. 어노테이션의 기초
어노테이션은 @표시 뒤에 어노테이션 이름을 붙이며 클래스, 필드, 메소드 등과 같은 프로그램의 선언부에 적용할 수 있다. 
어노테이션은 가장 처음으로 그리고 종종(관례상) 그 줄에 나타나며 임의의 인수를 포함할 수 있다.

어노테이션은 세 가지 기본 범주로 나눌수 있다.

  • Marker 어노테이션은 변수가 없다. 이 어노테이션은 이름으로 구분되며 추가 데이터 없이 나타난다. 예를 들어, @MarkerAnnotation은 marker 어노테이션이다. 데이터가 없으며 단지 어노테이션 이름만 있을 뿐이다. 
  • Single-value 어노테이션은 marker와 비슷하지만 데이터를 제공한다. 싱글 비트 데이트를 제공하기 때문에 간단한 신택스를 사용할 수 있다. (단, 어노테이션 유형이 이 문법을 수용해야 함): @SingleValueAnnotation("my data")이는 @표시만 제외하고는 일반적인 자바 메소드 호출과 비슷하다. 
  • Full 어노테이션은 다중 데이터 멤버를 갖고 있다. 결과적으로 전체 신택스를 사용해야 한다. (그리고 어노테이션은 일반 자바 메소드와 더 이상 비슷하지 않다): @FullAnnotation(var1="data value 1", var2="data value 2", var3="data value 3")

JDK 5.0에서는 3가지 빌트인 어노테이션이 제공되며 직접 작성할 수 있는 커스텀 어노테이션 또한 지원된다.


2. Override 어노테이션
Override는 상위 클래스에서 선언된 한 메소드를 오버라이드 할 것임을 컴파일러에게 알려준다. 따라서 Override는 메소드에 대해서만 사용되어야 한다. (클래스, 패키지 선언, 기타 구조체는 안된다.) Override로 표시된 한 메소드가 상위 클래스에 있는 메소드를 오버라이드하는 데 실패할 경우 컴파일러는 에러를 발생시킨다.
public class OverrideTester {

  public OverrideTester() { }

  @Override
  public String toString() {
    return super.toString() + " [Override Tester Implementation]";
  }

  @Override
  public int hashCode() {
    return toString().hashCode();
  }

}
위 예제에서 @Override어노테이션은 두 개의 메소드, toString()과 hashCode()가 OverrideTester 클래스의 수퍼클래스 (java.lang.Object)에서 메소드의 버전을 오버라이드 한다는 것을 나타내고 있다. 
언뜻 보기에는 사소한 것 같지만 매우 좋은 기능이다. 이들 메소드를 오버라이딩 하지 않고는 클래스를 컴파일 할 수 없다.


3. Deprecated 어노테이션
Deprecated가 표시된 메소드가 더 이상 사용되지 말아야 함을 가리킨다. 컴파일러는 프로그램이 비추천(deprecated) 메소드나 클래스 혹은 변수를 사용할 때마다 경고를 발생시킨다.


4. SuppressWarnings 어노테이션
SuppressWarnings 어노테이션은 컴파일러가 다르게 발생될 특정 경고를 억제하도록 해준다.
제네릭를 생각해보자. 제네릭은 모든 유형의 새로운 type-safe 작동을 만든다. 특히 자바 컬렉션의 경우 더욱 그렇다. 하지만 generics 때문에 컴파일러는 컬렉션이 type-safety 없이 사용될 때 경고를 던진다.
@SuppressWarnings("unchecked")
public void nonGenericsMethod() {
  List wordList = new ArrayList();    // no typing information on the List
  wordList.add("foo");                // causes error on list addition
}
위 예제는 제네릭이 나오기 전에 작성된 레거시 코드에 대한 경고를 억제해준다.
신고

'프로그래밍 > JAVA' 카테고리의 다른 글

OVal - Object Validation Framework for java  (0) 2013.01.30
Quartz cronExpression 테스트  (0) 2012.12.17
Jersey와 Spring으로 RESTful 웹 서비스 구축하기  (0) 2011.02.14
Javadoc  (1) 2010.04.30
이클립스에서 JUnit 사용하기  (0) 2010.04.22
Java 어노테이션(Annotation)  (0) 2010.04.21
Java Decompiler jad  (2) 2010.03.17
Apache Xml Security을 이용한 XML 전자서명  (1) 2010.03.17
MSSQL with JDBC  (0) 2010.02.03
JavaMail 첨부파일 읽기  (0) 2010.01.28
JavaMail with IMAP  (0) 2010.01.27
Posted by devop
프로그래밍/JAVA2010.03.17 17:46
C와 같은 언어는 소스파일을 컴파일하면 해당 시스템에 적합한 바이너리 코드를 생성합니다.
바이너리 코드를 역컴파일 하는 것이 불가능 한것은 아니나, 이것은 지루하고 복잡한 작업입니다.
흔히들 이와같은 작업을 역공학(리버스 엔지니어링)이라 하며, 소프트웨어 보안과 라이센스 정책에 크게 위협이 될수 있는 분야이기도 합니다.

Java의 경우 C와 같은 언어와 달리 바이트 형태의 class파일을 생성하는데 'Write Once, Run Anywhere"라는 Java의 패러다임에서 알수 있듯이, JavaVM이 존재하는 모든 시스템에서 실행될수 있는 시스템 독립적인 코드입니다.
따라서, 바이트 코드와 같은 경우는 바이너리 코드와 달리 손쉽게 역컴파일이 가능합니다.

jad는 바로 Java의 class파일을 디컴파일 해주는 프로그램입니다.
즉, java소스 파일은 없고 class파일만 존재하고 있을때, jad를 이용해 class파일을 java파일로 변환할 수 있습니다.


jad를 Eclipse IDE에 통합하여 편리하게 사용할수 있게 도와주는 plugin이 있습니다.
(Eclipse 3.4에 jadclipse3.3 설치가 가능합니다.)



1. 설치하기

아래 그림과 같이 다운로드 받은 jad파일을 Eclipse디렉토리에 복사합니다.


JadClipse파일(net.sf.jadclipse_3.3.0.jar)을 Eclipse plugins디렉토리에 복사합니다.


Eclipse를 재식하고 Window > Preference > Java > JadClipse에서 아래 그림과 같이 Path to decompiler을 입력합니다.


한글이 깨지는 것은 방지하기 위해 아래 그림과 같이 마지막 항목을 체크합니다.




2. class파일 디컴파일

디컴파일하고자 하는 class파일을 더블클릭하면, 아래 그림과 같이 디컴파일된 java파일이 나타납니다.




3. 난독처리

Java 프로그램은 컴파일된 코드만 배포한다고 하여도(바이트 코드) 이로부터 소스코드를 쉽게 만들어 낼 수 있으므로 소프트웨어 보호라는 측면에서 많은 문제점을 가지고 있습니다. 이와 같은 위험으로부터 소스코드를 보호하기 위해 소스코드를 난독화 할수 있습니다. 난독화는 는프로그램 코드를 변환하는 방법의 일종으로, 프로그램에 사용된 변수명을 아무 의미없는 이름으로 변환하는 등 코드를 지저분하게 하여 사람이 읽기 어렵게 만들어주는 기술입니다.

* Java 역컴파일 방지 툴: http://proguard.sourceforge.net/

신고

'프로그래밍 > JAVA' 카테고리의 다른 글

Quartz cronExpression 테스트  (0) 2012.12.17
Jersey와 Spring으로 RESTful 웹 서비스 구축하기  (0) 2011.02.14
Javadoc  (1) 2010.04.30
이클립스에서 JUnit 사용하기  (0) 2010.04.22
Java 어노테이션(Annotation)  (0) 2010.04.21
Java Decompiler jad  (2) 2010.03.17
Apache Xml Security을 이용한 XML 전자서명  (1) 2010.03.17
MSSQL with JDBC  (0) 2010.02.03
JavaMail 첨부파일 읽기  (0) 2010.01.28
JavaMail with IMAP  (0) 2010.01.27
JavaMail을 이용하며 메일전송  (0) 2010.01.15
Posted by devop
프로그래밍/JAVA2010.03.17 15:28
다음과 같은 XML 파일을 Apache Xml Security를 이용하여 Eveloped-Signature를 생성하는 과정은 다음과 같다.


1. 원본 XML의 내용
<?xml version="1.0" encoding="UTF-8"?>
<Book>
	<Title>XML 전자서명</Title>
	<Author>이연복</Author>
	<Price>10000</Price>
	<Publisher>SNUT</Publisher>
</Book>

2. 전자서명 코드
package org.sopt.dev;

import java.security.PrivateKey;
import java.security.cert.X509Certificate;

import javax.xml.XMLConstants;

import org.apache.xml.security.exceptions.XMLSecurityException;
import org.apache.xml.security.signature.XMLSignature;
import org.apache.xml.security.transforms.Transforms;
import org.w3c.dom.Document;
import org.w3c.dom.Element;
import org.w3c.dom.NodeList;

public class XMLSigner {

	private Document document = null;
	
	private PrivateKey privatekey = null;
	
	private X509Certificate x509Cert = null;
	
	public XMLSigner(Document document, PrivateKey privatekey, X509Certificate x509Cert) {
		this.document = document;
		this.privatekey = privatekey;
		this.x509Cert = x509Cert;
	}
	
	public Document sign() throws XMLSecurityException {
		XMLSignature sig = new XMLSignature(document, 
				XMLConstants.XMLNS_ATTRIBUTE_NS_URI,
				"http://www.w3.org/2000/09/xmldsig#rsa-sha1",
				"http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
		
		NodeList nodeList = document.getElementsByTagName("Book");
		Element bookElement = (Element)nodeList.item(0);
		nodeList = document.getElementsByTagName("Title");
		bookElement.insertBefore(sig.getElement(), (Element)nodeList.item(0));
		
		Transforms transforms = new Transforms(document);
		transforms.addTransform("http://www.w3.org/2000/09/xmldsig#enveloped-signature");
		transforms.addTransform("http://www.w3.org/TR/2001/REC-xml-c14n-20010315");
		sig.addDocument("", transforms, "http://www.w3.org/2000/09/xmldsig#sha1");
		
		sig.addKeyInfo(x509Cert);
		sig.sign(privatekey); 
		
		return document;
	}
	
}

3. 전자서명 결과
<?xml version="1.0" encoding="UTF-8"?>
<Book>
<ds:Signature xmlns:ds="http://www.w3.org/2000/09/xmldsig#">
<ds:SignedInfo>
<ds:CanonicalizationMethod Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></ds:CanonicalizationMethod>
<ds:SignatureMethod Algorithm="http://www.w3.org/2000/09/xmldsig#rsa-sha1"></ds:SignatureMethod>
<ds:Reference URI="">
<ds:Transforms>
<ds:Transform Algorithm="http://www.w3.org/2000/09/xmldsig#enveloped-signature"></ds:Transform>
<ds:Transform Algorithm="http://www.w3.org/TR/2001/REC-xml-c14n-20010315"></ds:Transform>
</ds:Transforms>
<ds:DigestMethod Algorithm="http://www.w3.org/2000/09/xmldsig#sha1"></ds:DigestMethod>
<ds:DigestValue>eJGWkV4mDEzNaQebWXTmAGFP20Q=</ds:DigestValue>
</ds:Reference>
</ds:SignedInfo>
<ds:SignatureValue>
vURLd5SJnYi85UDMGutu9Czmh4SuxR56iC7Wv5znKlev0ubUCqhvZrN+9mp3H10IaW1ZJxaGu0V1
/Rg576FwVR1GVr6d3at2SLkbUZDN2DdtNPYIvBGlmRNVmdSDtDB6QhG1lhv3WmStI6Huo4lk75Ig
yyDz5PJy8OlH+R5nk+k=
</ds:SignatureValue>
<ds:KeyInfo>
<ds:X509Data>
<ds:X509Certificate>
MIIFSTCCBDGgAwIBAgIECkDV5DANBgkqhkiG9w0BAQUFADBKMQswCQYDVQQGEwJrcjEQMA4GA1UE
CgwHeWVzc2lnbjEVMBMGA1UECwwMQWNjcmVkaXRlZENBMRIwEAYDVQQDDAl5ZXNzaWduQ0EwHhcN
MDkxMTE3MTUwMDAwWhcNMTAxMTE4MTQ1OTU5WjCBqTELMAkGA1UEBhMCa3IxEDAOBgNVBAoMB3ll
c3NpZ24xEzARBgNVBAsMCnhVc2U0RXNlcm8xDDAKBgNVBAsMA0tNQjEcMBoGA1UECwwTU0FNSlVO
RyBEQVRFU0VSVklDRTFHMEUGA1UEAww+7IK87KCV642w7J207YOA7ISc67mE7IqkKChTQU1KVU5H
IERBVEVTRVJWSUNFKTAwMDQ2ODg3MDA0MDY2NDMwgZ8wDQYJKoZIhvcNAQEBBQADgY0AMIGJAoGB
AMHbMPCKVTKMVkZe4s1tW/wTDzqijdU7Oe2rGlfOuKNcdAjDxkLj77IOgLou/DIwJ0imN6xWcfd1
1stHDfl3Yi6Ehz91gaQCXENXhyhsOFP7Gie06emxstSE3tn725+U6A2w1s7Ty311k0l7U4Bf1LNJ
Db8Imp5XAaPWg+zk2U7LAgMBAAGjggJZMIICVTCBjwYDVR0jBIGHMIGEgBRK+70zLYux0YyUa//g
QjZfHJHLCKFopGYwZDELMAkGA1UEBhMCS1IxDTALBgNVBAoMBEtJU0ExLjAsBgNVBAsMJUtvcmVh
IENlcnRpZmljYXRpb24gQXV0aG9yaXR5IENlbnRyYWwxFjAUBgNVBAMMDUtJU0EgUm9vdENBIDGC
AidgMB0GA1UdDgQWBBTH8Zj4a7Qm0qa80rc6wvATAZ694jAOBgNVHQ8BAf8EBAMCBsAwegYDVR0g
AQH/BHAwbjBsBgoqgxqMmkUBAQYIMF4wLgYIKwYBBQUHAgIwIh4gx3QAIMd4yZ3BHLKUACCs9cd4
x3jJncEcACDHhbLIsuQwLAYIKwYBBQUHAgEWIGh0dHA6Ly93d3cueWVzc2lnbi5vci5rci9jcHMu
aHRtMGgGA1UdEQRhMF+gXQYJKoMajJpECgEBoFAwTgwZ7IK87KCV642w7J207YOA7ISc67mE7Iqk
KDAxMC8GCiqDGoyaRAoBAQEwITAHBgUrDgMCGqAWBBSlWn1wqt/tN4hKoAVB0RQKp7gj/jByBgNV
HR8EazBpMGegZaBjhmFsZGFwOi8vZHMueWVzc2lnbi5vci5rcjozODkvb3U9ZHAzcDM3OTgzLG91
PUFjY3JlZGl0ZWRDQSxvPXllc3NpZ24sYz1rcj9jZXJ0aWZpY2F0ZVJldm9jYXRpb25MaXN0MDgG
CCsGAQUFBwEBBCwwKjAoBggrBgEFBQcwAYYcaHR0cDovL29jc3AueWVzc2lnbi5vcmc6NDYxMjAN
BgkqhkiG9w0BAQUFAAOCAQEAB9Im0fBWHNmU8ROrlGnsAPINOvBpOu1IG7CSfxgsrSqW4DVnjuT+
+ShEccR0+IpHuBDp1a/IYzWMln69Fh7Uso+Ac60btXoPKJh8+HtlkQfaVgfdAk2zihpuB05Gg1p2
qJIk6wQgkAgreDRYMCKz00je7DD2v4tNfBmslJ4CkMXO3IVHdyudoHChDoLjuYBWDKu0JIlUiO0m
ydvyKT48SrVgNxVCA52BOsTc5XDT0f/lVRIDQ90KYA6UA1l8oZ5P6GEIwWNDnH97NGblobyhoFfT
s5ERFjKsZjygZxAdQroBWH8TNFZvLxMoOionBy/A3SGyMkmrWcr+rb/u1wC5YA==
</ds:X509Certificate>
</ds:X509Data>
</ds:KeyInfo>
</ds:Signature>

	<Title>XML 전자서명</Title>
	<Author>이연복</Author>
	<Price>10000</Price>
	<Publisher>SNUT</Publisher>
</Book>
위 코드는 입력된 XML파일의 모든 노드셋을 대상으로 c14n 정규화 알고리즘을 적용한 후 sha1 알고리즘으로 digest value를 생성한다. 이후 CanonicalizationMethod와 SignatureMethod에 명시된 알고리즘에 따라 SignedInfo를 정규화하고, 서명을 수행한다.

 KeyInfo 노드는 서명을 제3 자가 서명을 검증할수 있도록 검증키(공개키)를 포함한다.

XMLSigner의 privatekey와 x509Cert는 각각 공인인증서의 개인키(signPri.key)와 공개키(signCert.der)이다.

신고

'프로그래밍 > JAVA' 카테고리의 다른 글

Jersey와 Spring으로 RESTful 웹 서비스 구축하기  (0) 2011.02.14
Javadoc  (1) 2010.04.30
이클립스에서 JUnit 사용하기  (0) 2010.04.22
Java 어노테이션(Annotation)  (0) 2010.04.21
Java Decompiler jad  (2) 2010.03.17
Apache Xml Security을 이용한 XML 전자서명  (1) 2010.03.17
MSSQL with JDBC  (0) 2010.02.03
JavaMail 첨부파일 읽기  (0) 2010.01.28
JavaMail with IMAP  (0) 2010.01.27
JavaMail을 이용하며 메일전송  (0) 2010.01.15
RSA 암호화  (0) 2010.01.15
Posted by devop