프로그래밍/Vert.x2015.07.19 17:17

Vert.x 3.0이 지난 2015년 6월 24일 릴리즈 되었다.



Vert.x 2.x 에서 Vert.x 3.0 으로 넘어오며 많은 변화가 생겼다. 몇 가지 큰 변화를 살펴보면 다음과 같다.

(아래 목록을 포함한 다양한 기능 소개 및 문서는 http://vertx.io/docs/ 에서 확인할 수 있다.)


  1. Node.js의 Express 같은 강력한 웹 프레임워크 기능 제공
  2. MySQL, MongoDB, Redis를 위한 DataAccess 기능 제공
  3. 비동기 테스팅을 지원하기 위한 Vert.x Testing 제공
  4. Apache Shiro, JWT 등 다양한 인증 기능 제공
  5. RxJava 통합 기능 제공


그러나 Vert.x 3.0에서 제공하는 다양한 기능의 혜택을 누리기 위해서는 기존 Vert.x 2.x에서 Vert.x 3.0으로 마이그레이션 작업이 필요하다. 패키지 구조 및 의존성 변경, 빌드 방식에 변경에 있기 때문에 마이그레이션 작업이 생각보다 간단치 않다는 것을 염두해두자. 따라서 Vert.x 3.0에서 제공하는 기능을 꼭 사용하기 위해 기존 Vert.x 2.x 기반 애플리케이션을 반드시 업그레이드 해야겠다는 것이 아니면 기존 Vert.x 애플리케이션은 2.1.6 버전으로 유지하고 (2.1.6 버전은 2015년 6월 30일에 발표되었다. 몇 가지 버그 픽스가 이루어진 것으로 보인다. http://vertx.io/blog/vert-x-2-1-6-released/ 를 참고하자.) 앞으로 새롭게 개발할 Vert.x 애플리케이션에 3.0 을 적용해보는 것이 좋을 것 같다. 아래 링크는 Vert.x 3.0으로 마이그레이션 포인트에 대해 잘 설명하고 있다. 



특히 Vert.x Module 체계를 fat-jar 방식으로 변경함에 따라 기존 Maven Archetype 및 Plug-In이 완전히 소용없게 되어버렸다. (앞으로 Vert.x 3.0을 지원하기 위한 Maven Archetype, Plug-In이 나올지는 미지수이다. 아마 안나올듯...) 이것은 필자 처럼 IntelliJ나 Eclipse 같은 IDE 환경에서 Maven Archetype, Plug-In + Java 언어 조합으로 개발하던 개발자들에게 Vert.x 개발 환경을 다시 세팅해야 한다는 것을 의미하기도 한다(...)

따라서 본 포스팅에서는 Vert.x 3.0 개발 환경을 IntelliJ에 구축하는 것을 설명하도록 하겠다.

1. Vert.x 3.0 다운로드
일단 Vert.x 3.0 실행 바이너리를 다운로드 한다. (JDK8버전이 필요하다. Vert.x 3.0에서는 람다표현식을 매우 빈번하게 사용하고 있다.) 적당한 곳에 압출을 풀고 $PATH에 (압축해제 경로)/bin을 추가해두자. 잘 설치되었는지 확인해보기 위해 콘솔을 하나 열고 vertx -version을 입력해보자. 3.0.0이 출력된다면 설치가 완료된 것이다.

 PS C:\Users\home> vertx -version

 3.0.0 



2. IntelliJ 개발 환경 설정

일단 IntelliJ를 실행시키고, Maven기반 모듈을 하나 만든다. (archetype은 선택하지 않는다.) 적당한 groupId와 artifactId,version을 입력한다. 모듈이 생성되면 아래 그림과 같은 구조가 된다.



pom.xml의 내용도 아래 처럼 매우 간단하다.

<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.devop.vertx3</groupId> <artifactId>vertx3-test</artifactId> <version>1.0-SNAPSHOT</version> </project>

위 pom.xml의 내용을 https://github.com/vert-x3/vertx-examples/blob/master/maven-simplest/pom.xml를 참고하여 다음과 같이 수정한다. 편집한 pom.xml 파일은 Vert.x 3.0 필수 의존 라이브러리 관리 및 실행 가능한 fat-jar를 빌드하기 위한 내용을 담고 있다. (properties 항목의 main.verticle 값은 fat-jar가 실행될 때 main 함수 역할을 하는 entry class를 지정한다.)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">

    <modelVersion>4.0.0</modelVersion>
    <groupId>com.devop.vertx3</groupId>
    <artifactId>vertx3-test</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <!-- the main verticle class name -->
        <main.verticle>com.devop.vertx3.TestVerticle</main.verticle>
        <vertx.version>3.0.0</vertx.version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-core</artifactId>
            <version>${vertx.version}</version>
        </dependency>
        <dependency>
            <groupId>io.vertx</groupId>
            <artifactId>vertx-hazelcast</artifactId>
            <version>${vertx.version}</version>
        </dependency>
    </dependencies>

    <build>
        <pluginManagement>
            <plugins>
                <!-- We specify the Maven compiler plugin as we need to set it to Java 1.8 -->
                <plugin>
                    <artifactId>maven-compiler-plugin</artifactId>
                    <version>3.1</version>
                    <configuration>
                        <source>1.8</source>
                        <target>1.8</target>
                    </configuration>
                </plugin>
            </plugins>
        </pluginManagement>

        <!-- You only need the part below if you want to build your application into a fat executable jar.-->
        <!-- This is a jar that contains all the dependencies required to run it, so you can just run it with  java -jar -->
        <plugins>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-shade-plugin</artifactId>
                <version>2.3</version>
                <executions>
                    <execution>
                        <phase>package</phase>
                        <goals>
                            <goal>shade</goal>
                        </goals>
                        <configuration>
                            <transformers>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
                                    <manifestEntries>
                                        <Main-Class>io.vertx.core.Starter</Main-Class>
                                        <Main-Verticle>${main.verticle}</Main-Verticle>
                                    </manifestEntries>
                                </transformer>
                                <transformer implementation="org.apache.maven.plugins.shade.resource.AppendingTransformer">
                                    <resource>META-INF/services/io.vertx.core.spi.VerticleFactory</resource>
                                </transformer>
                            </transformers>
                            <artifactSet>
                            </artifactSet>
                            <outputFile>${project.build.directory}/${project.artifactId}-${project.version}-fat.jar</outputFile>
                        </configuration>
                    </execution>
                </executions>
            </plugin>
        </plugins>
    </build>

</project>

이제 테스트를 위해 간단한 Verticle을 하나 작성해보자. 아래 Verticle은 logger를 통해 hello vertx3을 출력하는 아주 간단한 Verticle이다.

package com.devop.vertx3;

import io.vertx.core.AbstractVerticle;
import io.vertx.core.logging.Logger;
import io.vertx.core.logging.LoggerFactory;

public class TestVerticle extends AbstractVerticle {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Override
    public void start() throws Exception {
        logger.info("hello vertx3 ");
    }
}

위 TestVerticle을 IntelliJ에서 실행하기 위한 방법은 2가지가 있다. 


첫 번째 방법은 Vert.x 3.0에서 제공하는 io.vertx.core.Starter를 직접 사용해 Verticle을 실행하는 것이다. (io.vertx.core.Starter는 콘솔에서 vertx 명령을 입력했을 때 실행되는 class이다. 위 pom.xml 파일에서 main.verticle 값으로 지정한 entry class를 실행하는데에도 io.vertx.core.Starter가 사용되고 있음을 확인할 수 있다.)



위 그림 처럼 Run/Debug 설정의 Main class 항목에 io.vertx.core.Starter를 입력하고, Program arguments 항목에는 run 명령과 함께 실행할 Verticle class를 입력한다. 필요한 경우 run com.devop.vertx3.TestVerticle -conf conf.json 처럼 Verticl을 실행하는데 필요한 설정 파일을 함께 입력할 수 있다. 즉, 콘솔에서 vertx 명령을 실행할 때 입력할수 있는 모든 옵션은 Program arguments 항목에 입력할 수 있는 것이다. (io.vertx.core.Starter 자체가 vertx 명령을 의미한다.)


이제 IntelliJ에서 모듈을 실행해보면 콘솔에 'hello vertx3'이 출력되는 것을 확인할 수 있다.


두 번째 방법은 Verticle을 Embedded 방식으로 실행하는 것인데 이를 위해 아래 처럼 main() 함수를 포함하는 새로운 class를 하나 작성한다.

import io.vertx.core.Vertx;

public class TestLauncher {

    public static  void main(String[] args) {
        Vertx.vertx().deployVerticle("com.devop.vertx3.TestVerticle");
    }
}

TestLauncher와 TestVerticle을 포함하는 모듈 전체 구조는 아래 그림과 같다.



그리고 Run/Debug 설정을 아래 그림 처럼 변경한다. Main class 항목에 io.vertx.core.Starter대신 main() 함수를 포함하는 Launcher class를 입력하고 Program arguments 항목 값을 삭제한다.



앞에서와 마찬가지로 IntelliJ에서 모듈을 실행해보면 콘솔에 'hello vertx3'이 출력되는 것을 확인할 수 있다.


첫 번째, 두 번 째 방법 모두 브레이크 포인트를 설정하고, 디버그 모드로 실행해보면 프레이크 포인트에서 실행이 멈추고 정상적으로 디버깅 작업이 가능한 것을 확인할 수 있다. 한 가지 참고할 점은 Vert.x 3.0에서는 이벤트 루프 스레드가 오랜시간 블록되면 예외를 발생시키는데, 아래와 같이 StackTrace와 스레드가 블록된 시간 정보를 확인할 수 있다.


 경고: Thread Thread[vert.x-eventloop-thread-2,5,main] has been blocked for 18730 ms, time limit is 2000

 io.vertx.core.VertxException: Thread blocked

at java.lang.invoke.DirectMethodHandle.makePreparedLambdaForm(DirectMethodHandle.java:212)

at java.lang.invoke.DirectMethodHandle.preparedLambdaForm(DirectMethodHandle.java:188)

at java.lang.invoke.DirectMethodHandle.preparedLambdaForm(DirectMethodHandle.java:177)

at java.lang.invoke.DirectMethodHandle.make(DirectMethodHandle.java:81)

at java.lang.invoke.MethodHandles$Lookup.getDirectMethodCommon(MethodHandles.java:1656)

at java.lang.invoke.MethodHandles$Lookup.getDirectMethodNoSecurityManager(MethodHandles.java:1613)

at java.lang.invoke.MethodHandles$Lookup.getDirectMethodForConstant(MethodHandles.java:1798)

at java.lang.invoke.MethodHandles$Lookup.linkMethodHandleConstant(MethodHandles.java:1747)

at java.lang.invoke.MethodHandleNatives.linkMethodHandleConstant(MethodHandleNatives.java:477)

at io.vertx.core.impl.DeploymentManager.lambda$doDeploy$169(DeploymentManager.java:408)

at io.vertx.core.impl.DeploymentManager$$Lambda$4/992768706.handle(Unknown Source)

at io.vertx.core.impl.ContextImpl.lambda$wrapTask$15(ContextImpl.java:314)

at io.vertx.core.impl.ContextImpl$$Lambda$5/726281927.run(Unknown Source)

at io.netty.util.concurrent.SingleThreadEventExecutor.runAllTasks(SingleThreadEventExecutor.java:357)

at io.netty.channel.nio.NioEventLoop.run(NioEventLoop.java:357)

at io.netty.util.concurrent.SingleThreadEventExecutor$2.run(SingleThreadEventExecutor.java:111)

at java.lang.Thread.run(Thread.java:745) 


지금까지 IntelliJ에서 Vert.x 3.0 개발 환경 설정하는 방법에 대해 설명했지만, io.vertx.core.Starter의 역할만 이해하고 있다면 어렵지 않게 Eclipse에서도 Vert.x 3.0 개발 환경을 만들 수 있을 것이다.



3. fat-jar 패키징

Vert.x 3.0 기반 애플리케이션 개발을 완료 했다면, 해당 응용 프로그램을 어딘가에 배포해야 할 것이다. 이때 수 많은 class와 의존 라이브러리를 하나하나 관리하기는 번거로움이 있으므로, 하나의 모듈로 묶어서 관리할 수 있으면 매우 편리할 것이다. 기존 Ver.tx 2.x에서는 관련 class와 리소스를 묶어 Vert.x Module로 관리할 수 있었다.


그러나 Vert.x 3.0에서는 더 이상 이런 Vert.x Module을 사용하지 않는다. 대신 실행 가능한 far-jar 형태로 java -jar 명령을 통해 바로 실행 가능한 바이너리로 패키징한다. 앞서 살펴본 pom.xml 파일의 대부분의 내용이 fat-jar 패키징을 위한 내용이었다. 이를 위해 mvn clean package 명령을 실행하면 빌드가 실행되고, fat-jar가 생성된다. (Vert.x Module Repository에 등록되어 있는 모듈들은 어쩔 것인가!! 물론 쓸만한 모듈이 많지는 않다만은...)



아래는 java -jar 명령을 통해 fat-jar를 실행한 결과이다.


PS C:\Dev\intellij\my-work\vertx3-test\target> java -jar .\vertx3-test-1.0-SNAPSHOT-fat.jar

7월 19, 2015 5:12:19 오후 com.devop.vertx3.TestVerticle

정보: hello vertx3

7월 19, 2015 5:12:19 오후 io.vertx.core.Starter

정보: Succeeded in deploying verticle 


한 가지 흥미로운 것은 fat-jar에는 아래 그림 처럼 Vert.x 3.0 Core 라이브러리 등 실행에 필요한 모든 리소스가 함께 패키징 되기 때문에 fat-jar 파일 용량이 생각보다 크다는 것과 따라서 far-jar를 실행하는 시스템에서 반드시 Vert.x 3.0 바이너리를 설치하지 않아도 된다는 것이다.


저작자 표시 비영리 변경 금지
신고

'프로그래밍 > Vert.x' 카테고리의 다른 글

Vert.x 3.0 개발 환경 설정  (0) 2015.07.19
Posted by devop
프로그래밍/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
프로그래밍/C#/WPF2015.06.21 17:51

1. 서로 다른 데이터 타입 사이에서의 데이터 바인딩

앞의 예제(http://lyb1495.tistory.com/108)에서 로그인 처리를 위해 사용자의 아이디와 비밀번호를 TextBoxText 속성을 통해 입력 받고, 이를 LoginViewModelLoginID, LoginPasswd 프로퍼티에 바인딩하는 방법에 대해 알아보았다사실 지금까지 별도의 언급은 없었지만 바인딩 대상의 속성 타입과(TextBoxText 속성), 바인딩 소스의 데이터 타입(LoginViewModelLoginID, LoginPasswd 프로퍼티)이 동일한가의 여부는 중요한 문제다.


앞의 예제에서는 TextBoxText 속성 타입과 LoginID, LoginPasswd 프로퍼티 타입 모두 string으로 동일한 데이터 타입을 지니고 있기 때문에 별다른 문제 없이 데이터 바인딩이 가능했던 것이다. 그렇다면 바인딩 대상의 속성과 바인딩 소스의 데이터 타입이 다르다면 과연 데이터 바인딩은 어떻게 해야 할까?


예를 들어 다음과 같은 상황을 가정해보자ViewModel에서는 bool 타입의 특정 프로퍼티를 제공하고 있고, 이 프로퍼티에 UI 요소의 화면 출력 여부를 결정하는 Visibility 속성을 바인딩 하려고 한다. bool 타입과 Visibility 타입은 명백히 다른 데이터 타입을 지닌다. (Visibility에 대해 좀 더 자세히 설명하자면, UI 요소의 화면 출력을 결정하기 위한 enum 데이터 타입이다.)

namespace System.Windows
{
    // 요약:
    //     요소의 표시 상태를 지정합니다.
    public enum Visibility
    {
        // 요약:
        //     요소를 표시합니다.
        Visible = 0,
        //
        // 요약:
        //     요소를 표시하지 않지만 레이아웃에 요소를 위한 공간을 남겨 둡니다.
        Hidden = 1,
        //
        // 요약:
        //     요소를 표시하지 않고 레이아웃에 요소를 위한 공간을 남겨 두지 않습니다.
        Collapsed = 2,
    }
}

먼저 다음과 같이 버튼3개를 지니는 간단한 XAML을 작성해 보자. 각 버튼은 버튼의 화면 출력여부를 결정하기 위해 Visibility 속성에 ViewModel에서 제공하는 bool 타입의 프로퍼티를 바인딩 한다. (참고로 TextBoxVisibility는 기본값으로 OneWay 바인딩을 사용한다.)

<Window x:Class="WpfApplication1.VisibilityBidingWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="VisibilityBidingWindow" Height="300" Width="400">

    <StackPanel>
        <Button Content="Button1" Visibility="{Binding VisibilityForButton1}"/>
        <Button Content="Button2" Visibility="{Binding VisibilityForButton2}"/>
        <Button Content="Button3" Visibility="{Binding VisibilityForButton3}"/>
    </StackPanel>

</Window>

그리고 위 XAMLCode Behind는 다음과 같이 간단하게 DataContextViewModel을 설정하는 것으로 끝이 난다.

using System.Windows;
using WpfApplication1.ViewModels;

namespace WpfApplication1
{
    public partial class VisibilityBidingWindow : Window
    {
        public VisibilityBidingWindow()
        {
            InitializeComponent();
            this.DataContext = new VisibilityBidingViewModel(true, false, false);
        }
    }
}

계속해서 VisibilityBidingViewModel의 코드를 확인해보자. VisibilityBidingViewModel은 각각의 버튼 Visibility 속성에 바인딩 될 3개의 bool 타입의 프로퍼티를 지닌다. VisibilityBidingViewModel의 생성자는 이들 3개의 bool 타입 프로퍼티를 초기화하기 위한 매개변수를 받도록 한다. (위에서 살펴본 XAMLCode Behind는 매개변수로 true, false, false를 입력하고 있으므로 첫 번째 버튼만 화면에 출력되고 나머지 2개 버튼은 화면에 출력되지 않는 것이 올바른 결과가 된다.)


마지막으로 INotifyPropertyChanged 인터페이스 구현을 추가해 OneWay 또는 TwoWay 바인딩 방식에서 바인딩 소스(ViewModel)의 변화를 바인딩 대상(UI 요소)으로 올바르게 전파하도록 한다.

using System.ComponentModel;

namespace WpfApplication1.ViewModels
{
    public class VisibilityBidingViewModel : INotifyPropertyChanged
    {
        private bool _visibilityForButton1;
        public bool VisibilityForButton1
        {
            get { return _visibilityForButton1; }
            set
            {
                _visibilityForButton1 = value;
                OnPropertyUpdate("VisibilityForButton1");
            }
        }

        private bool _visibilityForButton2;
        public bool VisibilityForButton2
        {
            get { return _visibilityForButton2; }
            set
            {
                _visibilityForButton2 = value;
                OnPropertyUpdate("VisibilityForButton2");
            }
        }

        private bool _visibilityForButton3;
        public bool VisibilityForButton3
        {
            get { return _visibilityForButton3; }
            set
            {
                _visibilityForButton3 = value;
                OnPropertyUpdate("VisibilityForButton3");
            }
        }

        public VisibilityBidingViewModel(bool visibilityForButton1, bool visibilityForButton2, bool visibilityForButton3)
        {
            _visibilityForButton1 = visibilityForButton1;
            _visibilityForButton2 = visibilityForButton2;
            _visibilityForButton3 = visibilityForButton3;
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyUpdate(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

소스 코드를 빌드하고 실행해보면 아래와 같은 화면을 볼 수 있을 것이다.



Code Behind는 매개변수로 true, false, false를 입력하고 있으므로 첫 번째 버튼만 화면에 출력되고 나머지 2개 버튼은 화면에 출력되지 않아야 하지만 3개의 버튼이 모두 보인다. 바인딩에 무엇인가 문제가 있다는 뜻 이다. 디버그 모드에서 출력되는 정보를 확인해보면 다음과 같은 메시지를 확인할 수 있다.


 System.Windows.Data Error: 1 : Cannot create default converter to perform 'one-way' conversions between types 'System.Boolean' and 'System.Windows.Visibility'. Consider using Converter property of Binding. BindingExpression:Path=VisibilityForButton1; DataItem='VisibilityBidingViewModel' (HashCode=46591244); target element is 'Button' (Name=''); target property is 'Visibility' (type 'Visibility')


System.Windows.Data Error: 5 : Value produced by BindingExpression is not valid for target property.; Value='True' BindingExpression:Path=VisibilityForButton1; DataItem='VisibilityBidingViewModel' (HashCode=46591244); target element is 'Button' (Name=''); target property is 'Visibility' (type 'Visibility')


, bool 타입과 Visibility 타입이 서로 다르기 때문에 바인딩을 정상적으로 처리할 수 없다는 메시지 이다.



2. IValueConverter

서로 다른 데이터 타입 사이의 데이터 바인딩을 처리하기 위해 사용할 수 있는 것이 바로 IValueConverter이다. IvalueConverterConvert()ConvertBack() 이라는 2개의 메소드를 제공하는데 OneWay 바인딩 방식에서는 Convert() 메소드만 사용한다. , Convert() 메소드는 바인딩 소스의 데이터 타입으로부터 바인딩 대상 속성의 데이터 타입으로 변환을 처리하며, ConvertBack() 메소드는 이와 반대로 바인딩 대상 속성 데이터 타입으로부터 바인딩 소스 데이터 타입으로 변환을 처리한다. (ConvertBack() 메소드의 구현은 Convert() 메소드 구현의 역 과정이라고도 할 수 있다.)

Object Convert(
	Object value, // 바인딩 소스에서 생성 되는 값입니다
	Type targetType, // 바인딩 대상 속성의 형식입니다.
	Object parameter, // 사용할 변환기 매개 변수입니다.
	CultureInfo culture // 변환기에서 사용할 문화권입니다.
)

Object ConvertBack(
	Object value, // 바인딩 대상에서 생성 되는 값입니다.
	Type targetType, // 변환할 대상 형식입니다.
	Object parameter, // 사용할 변환기 매개 변수입니다.
	CultureInfo culture // 변환기에서 사용할 문화권입니다.
)

그럼 IvalueConverter를 이용해 bool 타입과 Visibility 타입 사이의 데이터 바인딩을 위한 타입 변환기를 작성해 보자. 먼저 바인딩 소스(ViewModel)에서 제공하는 데이터 타입 boolUI 요소의 Visibility 속성에 적용 시키기 위해 Convert() 메소드를 작성한다. 간단하게 bool 변수 값이 true 라면 Visibility.Visible를 반환해 화면에 UI 요소가 출력되도록 하고, false 라면 Visibility.Hidden를 반환해 UI 요소가 출력되지 않도록 한다.

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace WpfApplication1.Converters
{
    public class BooleanToVisibilityConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            bool flag = (value is bool) ? (bool)value : false;
            return flag ? Visibility.Visible : Visibility.Hidden;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }
}

UI 요소의 Visibility 속성은 OneWay 바인딩 방식을 사용하므로 ConvertBack() 메소드는 사용하지 않는다.


마지막으로 XAML에 위에서 작성한 Converter를 선언하고 바인딩 구문을 다음과 같이 변경한다.

<Window x:Class="WpfApplication1.VisibilityBidingWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        xmlns:cvts="clr-namespace:WpfApplication1.Converters"
        Title="VisibilityBidingWindow" Height="300" Width="400">

    <Window.Resources>
        <cvts:BooleanToVisibilityConverter x:Key="boolToVisibility"/>
    </Window.Resources>
    <StackPanel>
        <Button Content="Button1" Visibility="{Binding VisibilityForButton1, Converter={StaticResource boolToVisibility}}"/>
        <Button Content="Button2" Visibility="{Binding VisibilityForButton2, Converter={StaticResource boolToVisibility}}"/>
        <Button Content="Button3" Visibility="{Binding VisibilityForButton3, Converter={StaticResource boolToVisibility}}"/>
    </StackPanel>

</Window>

애플리케이션을 다시 빌드 한 후 실행해보면 첫 번째 버튼만 화면에 출력되는 것을 확인할 수 있다.



3. 데이터 변환 파라미터 사용

XAMLCode Behind에서 VisibilityBidingViewModel를 생성할 때 bool 값을 true, false, true 순으로 지정해보자. 이것은 아래 그림처럼 두 번째 버튼을 제외하고 첫 번째와 세 번째 버튼만 화면에 출력하도록 한다.



두 번째 버튼의 공간이 비어있는 것을 확인할 수 있는데, 이것은 BooleanToVisibilityConverter 구현시 bool 값이 false라면 Visibility.Hidden를 반환하도록 했기 때문이다. (Visibility.HiddenUI 요소가 화면에 출력되지는 않지만 공간을 점유한다. 화면에 출력되지 않고 공간도 차지하지 않게 하려면 Visibility.Collapsed를 반환하도록 한다.) 필요에 따라 bool 값이 false라면 Visibility.Hidden이 아닌 Visibility.Collapsed를 반환하도록 하려면 어떻게 해야 할까? 한 가지 방법은 Convert(), ConvertBack() 메소드의 매개 변수 중 하나인 parameter를 활용하는 것이다.

using System;
using System.Globalization;
using System.Windows;
using System.Windows.Data;

namespace WpfApplication1.Converters
{
    public class BooleanToVisibilityConverter : IValueConverter
    {
        public object Convert(object value, Type targetType, object parameter, CultureInfo culture)
        {
            bool flag = (value is bool) ? (bool)value : false;
            bool collapsed = (parameter as string) == "collapsed";
            if (flag)
                return Visibility.Visible;
            return collapsed ? Visibility.Collapsed : Visibility.Hidden;
        }

        public object ConvertBack(object value, Type targetType, object parameter, CultureInfo culture)
        {
            throw new NotSupportedException();
        }
    }
}

변경 된 Convert() 메소드를 보면 bool 값이 false일때, 매개변수 parameterstring 타입이고 그 값이 “collapsed” 라면 반환 값으로 Visibility.Collapsed를 지정했다. 그 외에는 이전 구현 대로 Visibility.Hidden를 반환하도록 한다그리고 XAML의 바인딩 구문을 아래와 같이 변경한다. 이것은 ConverterParameter를 통해 Convert() 메소드의 매개변수 중 하나인 parameter에 “collapsed” 값을 지니는 string을 넘기는 방법을 보여준다.


<Button Content="Button2" Visibility="{Binding VisibilityForButton2, Converter={StaticResource boolToVisibility}, ConverterParameter=collapsed}"/>




애플리케이션을 다시 빌드 하고 실행하면 아래 그림처럼 두 번째 버튼의 공간까지 사라지는 것을 확인할 수 있다.




4. 다중 입력값 처리를 위한 IMultiValueConverter

IMultiValueConverterMultiBinding을 통해 2개 이상의 입력 값이 존재하고 이들을 특정 데이터 타입으로 변환해야 할 때 사용할 수 있다. IMultiValueConverterIValueConverter처럼 Convert()ConvertBack() 이라는 2개의 메소드를 제공하며 사용법은 IvalueConverter와 큰 차이가 없다. 다만 Convert(), ConvertBack() 메소드 시그니처가 IValueConverter와는 조금 다르다는 것에 주의하도록 한다.

Object Convert(
	Object[] values, // 배열 값의 소스 바인딩에 MultiBinding 생성 합니다. 값 UnsetValue 소스 바인딩 값에 대 한 변환을 제공 합니다 있음을 나타냅니다. 
	Type targetType, // 바인딩 대상 속성의 형식입니다.
	Object parameter, // 사용할 변환기 매개 변수입니다.
	CultureInfo culture // 변환기에서 사용할 문화권입니다.
)

Object[] ConvertBack(
	Object value, // 바인딩 대상에서 생성 하는 값입니다.
	Type[] targetTypes, // 배열 형식으로 변환 합니다. 배열 길이 수와의 반환 방법에 대해 제안 된 값을 나타냅니다. 
	Object parameter, // 사용할 변환기 매개 변수입니다.
	CultureInfo culture // 변환기에서 사용할 문화권입니다.
)


5. 참고

https://msdn.microsoft.com/ko-kr/library/system.windows.data.ivalueconverter.convertback(v=vs.110).aspx

https://msdn.microsoft.com/ko-kr/library/system.windows.data.imultivalueconverter.convertback(v=vs.110).aspx


저작자 표시 비영리 변경 금지
신고

'프로그래밍 > C#/WPF' 카테고리의 다른 글

WPF 데이터 바인딩의 기초 - 2  (0) 2015.06.21
WPF 데이터 바인딩의 기초 - 1  (1) 2015.06.16
Posted by devop
프로그래밍/C#/WPF2015.06.16 17:56

1. 데이터 바인딩 배경

WPF에서 데이터 바인딩이란 XAML로 표현되는 UI 요소와 ViewModel로 표현되는 데이터 사이에 관계를 맺는 기술을 의미한다. 이를 통해 UI 부분과 데이터 부분을 서로 독립적으로 다룰 수 있으며, 이것은 곧 UI 디자이너와 개발자의 역할을 보다 분명하게 정의하고 협업을 효율적으로 할 수 있게 한다.

또한, 굳이 UI 디자이너와 개발자 사이의 협업 관계를 고려하지 않는다 해도 애플리케이션 로직(데이터)과 프레젠테이션(UI) 부분을 분명하게 분리한다는 것은 개발 생산성이나 추후 유지보수성을 고려해보았을 때 매우 바람직한 시도라 할 수 있다. 실제로 이러한 시도의 일환으로 과거부터 존재했던 수 많은 MVC 프레임워크를 일일이 논하지 않아도 많은 개발자들은 이미 애플리케이션 로직과 프레젠테이션 부분을 분리하는데 많은 관심과 노력을 기울이고 있다는 것을 알 수 있다.



2. 데이터 바인딩 구조

WPF 데이터 바인딩을 구성하는 요소는 크게 3가지로 생각해 볼 수 있다. UI 요소를 의미하는 바인딩 대상(보다 정확하게는 UI요소의 DependencyObject 속성), 데이터를 의미하는 바인딩 소스 그리고 이 둘 사이의 관계를 맺어주는 바인딩 개체가 바로 그것이다.

바인딩 개체가 제공하는 바인딩 방식은 4가지가 있다.


첫 번째, OneWay방식은 바인딩 소스에서 바인딩 대상 방향으로만 데이터 바인딩을 제공한다. 예를 들어 TextBox의 Text 속성(바인딩 대상)에 바인딩 된 string 객체(바인딩 소스)가 있다면, string 객체를 수정했을 때 수정 된 값이 TextBox에 반영된다. 그러나 반대로 TextBox의 Text속성이 변경되어도 여기에 바인딩 된 string 객체는 변경되지 않는 않는다.


두 번째, TwoWay는 바인딩 소스와 바인딩 대상 양방향 모두 데이터 바인딩을 제공한다. 즉, OneWay에서 TextBox의 Text 속성을 변경하면 여기에 바인딩 된 string 객체의 값도 함께 변경된다는 것을 의미한다. 대부분의 UI 요소는 기본값으로 OneWay 바인딩을 사용하지만 TextBox의 Text속성, CheckBox의 IsChecked 속성 등은 TwoWay 바인딩을 기본값으로 한다.


세 번째, OneWayToSoruce는 OneWay방식의 반대로 동작한다.

네 번째, 아래 그림에서는 표현되어 있지 않지만 OneTime 방식으로 최초 바인딩 소스 값이 바인딩 대상 속성 값을 초기화 하지만 그 이후는 어떤 변환도 바인딩 대상, 바인딩 소스 모두에 반영되지 않는 방식이다.


위 4가지 데이터 바인딩 방식에서 조금 더 관심을 가져야 하는 부분은 TwoWay 바인딩 방식이다.


TwoWay 바인딩은 바인딩 대상의 속성 값이 변경되면, 해당 변경 내용을 다시 바인딩 소스로 전파하는데 이 과정을 UpdateSourceTrigger라 한다. (OneWayToSource 바인딩도 UpdateSourceTrigger를 정의할 수 있다.)


UpdateSourceTrigger

설명

LostFocus

UI 요소가 포커스를 잃었을 때 바인딩 소스를 업데이트한다.

) TextBox가 포커스를 잃었을 때 TextBoxText 속성 값을 여기에 바인딩 된 string 객체로 전파한다.

PropertyChanged

UI요소의 바인딩 된 속성값이 변경될 때 바인딩 소스를 업데이트한다.

) TextBoxText 속성값이 변경되면 여기에 바인딩 된 string 객체로 전파한다.

Explicit

애플리케이션에서 명시적으로 UpdateSource를 호출할 때

) 사용자가 특정 버튼을 클릭했을 때 UpdateSoruce를 실행해 TextBoxText 속성값을 여기에 바인딩 된 string 객체로 전파한다.



3. 예제

먼저 데이터 바인딩을 사용하지 않을 경우 나타날 수 있는 애플리케이션 코드를 살펴보자.


아래  XAML은 사용자 로그인을 처리하기 위해 아이디와 비밀번호를 입력 받고, 로그인 처리를 실행하기 위한 1개의 버튼을 정의하고 있다. 2개의 TextBox와 1개의 버튼은 x:Name을 통해 고유 식별자를 정의하고 있는데 이것은 XAML의 Code Behind(XAML파일명에 .cs확장자를 더한 클래스 파일이다. 예를 들어 MainWindow.xaml은 MainWindow.xaml.cs라는 Code Behind 파일을 지니게 된다.)에서 해당 UI 요소를 접근하는데 사용된다.

<Window x:Class="WpfApplication1.MainWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="MainWindow" Height="300" Width="400">

    <StackPanel>
        <StackPanel.Resources>
            <Style TargetType="StackPanel">
                <Setter Property="Margin" Value="5"/>
            </Style>
            <Style TargetType="Label">
                <Setter Property="Width" Value="80"/>
                <Setter Property="VerticalAlignment" Value="Center"/>
            </Style>
            <Style TargetType="TextBox">
                <Setter Property="Width" Value="120"/>
            </Style>
            <Style TargetType="Button">
                <Setter Property="Width" Value="50"/>
                <Setter Property="Height" Value="25"/>
            </Style>
        </StackPanel.Resources>

        <StackPanel Orientation="Horizontal">
            <Label Content="아이디"/>
            <TextBox x:Name="ID"/>
        </StackPanel>

        <StackPanel Orientation="Horizontal">
            <Label Content="비밀번호"/>
            <TextBox x:Name="Passwd"/>
        </StackPanel>

        <StackPanel Orientation="Horizontal">
            <Button x:Name="LoginBtn" Content="로그인"/>
        </StackPanel>
    </StackPanel>

</Window>

아래 코드는 위 XAML의 Code Behind 내용 이다. x:Name으로 명명된 식별자를 통해 TextBox의 Text 속성의 값을 읽거나 설정할 수 있다. Button 역시 x:Name으로 명명된 식별자를 통해 Click 이벤트 등을 제어할 수 있다.


로그인 화면 구성이 비교적 간단하고, 입력 값 검증 규칙이 복잡하지 않기 때문에 Code Behind에 UI 요소와 데이터를 처리하는 로직이 뒤섞여 있어도 불편함이 크게 들어나지는 않는다. 그러나 화면을 구성하는 UI 요소의 개수가 증가하고, 입력 값 검증 로직이 복잡해짐에 따라 Code Behind의 복잡도도 크게 증가하게 될 것을 쉽게 예상할 수 있다.

using System.Windows;
using System.Windows.Input;

namespace WpfApplication1
{
    public partial class MainWindow : Window
    {
        private string LoginID { get; set; }
        private string LoginPasswd { get; set; }

        public MainWindow()
        {
            InitializeComponent();
            this.Loaded += OnLoaded;
            this.LoginBtn.Click += LoginButtonClick;
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            Keyboard.Focus(this.ID);
        }

        private void LoginButtonClick(object sender, RoutedEventArgs e)
        {
            LoginID = this.ID.Text;
            LoginPasswd = this.Passwd.Text;
            if (string.IsNullOrEmpty(LoginID))
            {
                MessageBox.Show("아이디를 입력해주세요.");
                Keyboard.Focus(this.ID);
                return;
            }
            if (string.IsNullOrEmpty(LoginPasswd))
            {
                MessageBox.Show("비밀번호를 입력해주세요.");
                Keyboard.Focus(this.Passwd);
                return;
            }
            doLogin();
        }

        private bool doLogin()
        {
            //-- 로그인 처리
            MessageBox.Show(string.Format("아이디={0}, 비밀번호={1}", LoginID, LoginPasswd));
            return true;
        }
    }
}

위 코드를 데이터 바인딩을 적용해 UI 부분과 데이터 부분을 유연하게 분리해 보자.


먼저 변경 된 XAML은 아래와 같다앞서 살펴본 XAML과는 크게 차이가 나지 않지만 TextBoxText 속성에 바인딩 구문이 사용되고 있음을 주목하자.

<Window x:Class="WpfApplication1.LoginWindow"
        xmlns="http://schemas.microsoft.com/winfx/2006/xaml/presentation"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Title="LoginWindow" Height="300" Width="400">

    <StackPanel>
        <StackPanel.Resources>
            <Style TargetType="StackPanel">
                <Setter Property="Margin" Value="10"/>
            </Style>
            <Style TargetType="Label">
                <Setter Property="Width" Value="80"/>
                <Setter Property="VerticalAlignment" Value="Center"/>
            </Style>
            <Style TargetType="TextBox">
                <Setter Property="Width" Value="120"/>
            </Style>
            <Style TargetType="Button">
                <Setter Property="Width" Value="50"/>
                <Setter Property="Height" Value="25"/>
            </Style>
        </StackPanel.Resources>

        <StackPanel Orientation="Horizontal">
            <Label Content="아이디"/>
            <TextBox x:Name="ID" Text="{Binding LoginID, UpdateSourceTrigger=PropertyChanged}"/>
        </StackPanel>

        <StackPanel Orientation="Horizontal">
            <Label Content="비밀번호"/>
            <TextBox x:Name="Passwd" Text="{Binding LoginPasswd, UpdateSourceTrigger=PropertyChanged}"/>
        </StackPanel>

        <StackPanel Orientation="Horizontal">
            <Button x:Name="LoginBtn" Content="로그인"/>
        </StackPanel>
    </StackPanel>

</Window>

Code Behinde의 내용은 다음과 같다. this.DataContext에 LoginViewModel을 설정하고 있는 부분이 앞서 살펴본 Code Behinde와 차이가 있음을 알 수 있다. DataContext는 XAML의 UI 요소의 바인딩 구문에 사용될 수 있는 데이터를 가리킨다. 예를 들어 x:Name이 “ID”인 TextBox의 Text는 LoginID라는 항목에 바인딩 되어 있는데 이것은 바로 DataContext로 지정한 LoginViewModel의 LoginID 프로퍼티를 가리키는 것이다. 이와 비슷하게 LoginPasswd는 LoginViewModel의 LoginPasswd 프로퍼티를 가리킨다. 앞에서 알아본 내용에 따르면 TextBox의 Text 속성은 기본값으로 TwoWay 바인딩을 사용하므로 LoginViewModel의 LoginID 값을 변경하면 변경 된 값이 TextBox의 Text속성에 반영이 되거나 또는 사용자가 TextBox에 직접 타이핑해 넣은 값이 LoginViewModel의 LoginID에 저장될 것을 기대할 수 있다.

using System.Windows;
using System.Windows.Input;
using WpfApplication1.ViewModels;

namespace WpfApplication1
{
    public partial class LoginWindow : Window
    {
        public LoginWindow()
        {
            InitializeComponent();

            this.Loaded += OnLoaded;
            this.LoginBtn.Click += LoginButtonClick;
            this.DataContext = new LoginViewModel();
        }

        private void OnLoaded(object sender, RoutedEventArgs e)
        {
            Keyboard.Focus(this.ID);
        }

        private void LoginButtonClick(object sender, RoutedEventArgs e)
        {
            var viewModel = this.DataContext as LoginViewModel;
            if (string.IsNullOrEmpty(viewModel.LoginID))
            {
                MessageBox.Show("아이디를 입력해주세요.");
                Keyboard.Focus(this.ID);
                return;
            }
            if (string.IsNullOrEmpty(viewModel.LoginPasswd))
            {
                MessageBox.Show("비밀번호를 입력해주세요.");
                Keyboard.Focus(this.Passwd);
                return;
            }
            doLogin();
        }

        private bool doLogin()
        {
            //-- 로그인 처리
            var viewModel = this.DataContext as LoginViewModel;
            MessageBox.Show(string.Format("아이디={0}, 비밀번호={1}", viewModel.LoginID, viewModel.LoginPasswd));
            return true;
        }
    }
}

실제로 아이디와 비밀번호에 값을 입력하고 로그인 버튼을 클릭하면 아래 그림과 같은 결과를 확인할 수 있다. 즉, 바인딩 대상(UI 요소)의 변경이 바인딩 소스(ViewModel)로 올바르게 전파되고 있는 것을 확인한 것이다. 이러한 변경의 전파는 UpdateSourceTrigger 값으로 PropertyChanged를 사용하고 있기 때문에 TextBox의 Text 속성 값이 변경될 때마다 발생하게 된다.


그럼 반대로 바인딩 소스(ViewModel)의 변경이 바인딩 대상(UI 요소)으로 전파되는지도 확인해보자. 이를 위해 간단하게 로그인 버튼 옆에 자동입력 버튼을 하나 추가한다.


 <StackPanel Orientation="Horizontal">

   <Button x:Name="AutoBtn" Content="자동입력"/>

   <Button x:Name="LoginBtn" Content="로그인" Margin="8,0,0,0"/>

 </StackPanel>


그리고 Code Behinde에는 다음과 같은 자동입력 버튼을 클릭했을 때 실행 될 핸들러를 하나 추가한다.


 private void AutoButtonClick(object sender, RoutedEventArgs e)

 {

     var viewModel = this.DataContext as LoginViewModel;

     viewModel.LoginID = "myid2";

     viewModel.LoginPasswd = "mypassword2";

 }


애플리케이션을 다시 빌드하고 실행한다. 그리고 자동입력 버튼을 클릭한 다음 바로 로그인 버튼을 클릭해보자. 그러면 아래 그림과 같이 조금은 이상한 결과를 얻게 된다.



AutoButtonClick 핸들러를 통해 LoginViewModel의 LoginID와 LoginPasswd 프로퍼티의 값을 변경 했으나 변경의 전파가 UI 요소로 올바르게 이루어 지지 않았다는 것을 알 수 있다. (Alert 윈도우를 통해 LoginViewModel의 LoginID와 LoginPasswd 프로퍼티는 값이 올바르게 변경되었다는 것을 확인할 수 있다.)


사실 이것은 ViewModel을 구현하는데 있어서 INotifyPropertyChanged 인터페이스의 필요성을 설명하기 위해 의도한 결과이다. OneWay나 TwoWay 바인딩 방식에서 바인딩 소스(ViewModel)의 변화를 바인딩 대상(UI 요소)으로 올바르게 전파하기 위해서는 INotifyPropertyChanged 인터페이스 구현이 반드시 필요하다.

using System.ComponentModel;

namespace WpfApplication1.ViewModels
{
    public class LoginViewModel : INotifyPropertyChanged
    {
        private string _loginID;
        public string LoginID
        {
            get { return _loginID; }
            set
            {
                _loginID = value;
                OnPropertyUpdate("LoginID");
            }
        }

        private string _loginPasswd;
        public string LoginPasswd
        {
            get { return _loginPasswd; }
            set
            {
                _loginPasswd = value;
                OnPropertyUpdate("LoginPasswd");
            }
        }

        public event PropertyChangedEventHandler PropertyChanged;

        private void OnPropertyUpdate(string propertyName)
        {
            if (PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyName));
        }
    }
}

INotifyPropertyChanged 인터페이스는 PropertyChangedEventHandler라는 이벤트 객체를 하나 포함하고 있는데, 이 이벤트 객체를 통해 프로퍼티 값이 변경되었다는 것을 UI 요소에 알리게 된다. 이제 다시 애플리케이션을 빌드하고 실행해보자. 그리고 자동입력 버튼을 클릭하면 아이디와 비밀번호 TextBox에 LoginViewModel을 통해 변경된 값이 반영되는 것을 확인할 수 있다.



참고

https://msdn.microsoft.com/ko-kr/library/ms752347(v=vs.110).aspx



저작자 표시 비영리 변경 금지
신고

'프로그래밍 > C#/WPF' 카테고리의 다른 글

WPF 데이터 바인딩의 기초 - 2  (0) 2015.06.21
WPF 데이터 바인딩의 기초 - 1  (1) 2015.06.16
Posted by devop
프로그래밍/기타2015.06.12 15:45

나이브 베이지안 알고리즘에 대해 생각해 볼 일이 생겼다.

일단 대체 이게 어디에 쓰이는 알고리즘인지 알아보자.


구글 검색을 해보니 역시 조대협님의 블로그에서 잘 정리된 정보를 찾을 수 있었다. (역시 보물창고)

http://bcho.tistory.com/1010


한 줄 요약하면, 머신 런닝 분야에서 분류 알고리즘으로서 널리 쓰이고 있으며, 이 알고리즘을 통해 문서 분류기 같은 것을 만들 수 있다. 예를 들어 어떤 메일이 있을 때 이 메일이 스팸이냐 아니냐를 분류하거나, 어떤 뉴스 기사가 있을 때 해당 기사가 경제 기사냐, 스포츠 기사냐를 분류한다.


자세한 수학적 이론과 예제는 위에 조대협님 블로그에서 참고하기로 하고 여기에서는 나이브 베이지안 알고리즘을 적용한 문서 분류기 코드를 작성해 보자.


문제) 다음과 같이 5개의 학습 문서가 존재하고, 분류가 comedy(코메디 영화), action(액션 영화) 두개가 존재한다고 하자. 이제 어떤 문서에 fun, furious, fast 라는 3개의 단어만 있는 문서가 있을 때, 이 문서가 코메디인지 액션 영화인지 분리를 해보자. (문제 예시는 조대협님의 블로그 예시를 그대로 가져옴)


 영화 

 단어

 분류 

 1

 fun, couple, love, love

 Comedy

 2

 fast, furious, shoot

 Action

 3

 couple, fly, fast, fun, fun

 Comedy

 4

 furious, shoot, shoot, fun

 Action

 5

 fly, fast, shoot, love

 Action


자 일단 전체 코드 부터 보자.

import java.util.Arrays;
import java.util.HashMap;
import java.util.Map;

public class NaiveBayesianClassification {

    private String[] dataSet;
    private Map<String, Long> classifies = new HashMap<>();
    private Map<String, Map<String, Long>> counter = new HashMap<>();

    public NaiveBayesianClassification(String[] dataSet) {
        if (dataSet == null || dataSet.length == 0)
            throw new IllegalArgumentException("Empty dataSet");
        this.dataSet = dataSet;
    }

    private String getClassify(String input) {
        int divide = input.indexOf('|');
        return divide > - 1 ? input.substring(0, divide) : null;
    }

    private String[] getWords(String input) {
        int divide = input.indexOf('|');
        return divide > -1 ? input.substring(divide+1).split(",") : null;
    }

    public void training() {
        Arrays.stream(dataSet).forEach(data -> {
            String classify = getClassify(data);
            String[] words = getWords(data);
            //-- 분류명과 분류명이 나타난 횟수를 classifies에 저장한다.
            if (classify != null) {
                Long count = classifies.get(classify);
                if (count == null)
                    count = 1L;
                else
                    count++;
                classifies.put(classify, count);
                //-- 각 분류명에 대해 특정 단어가 나타난 횟수를 couner에 저장한다.
                if (words != null) {
                    Arrays.stream(words).forEach(word -> {
                        Map<String, Long> wordCounter = counter.get(classify);
                        if (wordCounter == null) {
                            wordCounter = new HashMap<>();
                            counter.put(classify, wordCounter);
                        }
                        Long wordCount = wordCounter.get(word);
                        if (wordCount == null)
                            wordCount = 1L;
                        else
                            wordCount++;
                        wordCounter.put(word, wordCount);
                    });
                }
            }
        });
    }

    public String judgment(String[] words) {
        Map<String, Double> results = new HashMap<>();
        long classifiesTotalCount = classifies.values().stream().mapToLong(Long::longValue).sum();
        classifies.forEach((classify, count) -> {
            double[] points = Arrays.stream(words).mapToDouble(word -> {
                Map<String, Long> wordCounter = counter.get(classify);
                if (wordCounter == null)
                    return 0.0f;
                Long wordCount = wordCounter.get(word);
                if (wordCount == null)
                    return 0.0f;
                long wordTotalCount = wordCounter.values().stream().mapToLong(Long::longValue).sum();
                return (double)wordCount / wordTotalCount;
            }).toArray();
            double total = (double)classifies.get(classify) / classifiesTotalCount;
            total = Arrays.stream(points).reduce(total, (x, y) -> x * y);
            results.put(classify, total);
        });
        results.entrySet().forEach(entry ->
                System.out.println(String.format("%s : %f", entry.getKey(), entry.getValue())));
        return results.entrySet().stream().max(Map.Entry.comparingByValue(Double::compareTo)).get().getKey();
    }

    public static void main(String[] args) throws Exception {
        //-- 학습 데이터
        String[] dataSet = {
                "Comedy|fun,couple,love,love",
                "Action|fast,furious,shoot",
                "Comedy|couple,fly,fast,fun,fun",
                "Action|furious,shoot,shoot,fun",
                "Action|fly,fast,shoot,love"
        };
        //-- 테스트 데이터
        String[] words = {"fun", "furious", "fast"};

        NaiveBayesianClassification classifier = new NaiveBayesianClassification(dataSet);
        classifier.training();
        String classify = classifier.judgment(words);
        System.out.println(classify);
    }

}

trainning 메소드는 dataSet을 통해 문서 분류기를 학습 시키는 역할을 한다. 먼저 데이터로부터 분류와 단어들을 추출하고, classifies에는 분류명과 해당 분류명의 나타난 횟수를 기록한다. counter는 분류별로 특정 단어가 나타난 횟수를 기록하는데 사용한다.


judgment 메소드는 단어들이 주어졌을 때, 주어진 단어를 통해 해당 문서가 어떤 분류에 속할지 계산한다. 이를 위헤 classifies에 포함된 모든 분류들에 대해 확률값을 계산한 후 그 중 가장 큰 확률값을 지닌 분류를 선택한다.


위 분류기에 의한 결과값은 Action : 0.001803, Comedy : 0.000000 으로 "fun", "furious", "fast" 단어들을 포함하는 문서는 Action 영화으로 분류된다.

저작자 표시 비영리 변경 금지
신고
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

spring-data는 이름에서 느낄수 있는 그대로 데이터 접근에 필요한 기능을 제공해준다.

spring-data는 하나의 큰 카테고리로 생각할 수 있으며, 그 하위의 sub-project들은 각각 big- data, http, redis, mongoDB 등 다양한 형태로 제공되는 데이터에 접근할 수 있는 기능을 구현한다.


spring-data에서 제공되는 데이터 접근 기술은 다음과 같다.



spring-data 관련 정보는 여기서 확인하자 : http://www.springsource.org/spring-data


본 포스팅에서는 spring-data에서 redis를 사용하는 방법을 다룬다.


먼저 redis에 대해 간략한 설명은 다음과 같다.


Redis는 "REmote DIctionary System"의 약자로 메모리 기반의 Key/Value Store 이다. 
Cassandra나 HBase와 같이 NoSQL DBMS로 분류되기도 하고, memcached와 같은 In memory 솔루션으로 분리되기도 한다. 성능은 memcached에 버금가면서 다양한 데이타 구조체를 지원함으로써 Message Queue, Shared memory, Remote Dictionary 용도로도 사용될 수 있으며, 이런 이유로 인스탄트그램, 네이버 재팬의 LINE 메신져 서비스, StackOverflow,Blizzard,digg 등 여러 소셜 서비스에 널리 사용되고 있다.

BSD 라이센스 기반의 오픈 소스이며 최근 VMWare에 인수되어 계속해서 업그레이드가 되고 있다. 16,000 라인정도의 C 코드로 작성되었으며, 클라이언트 SDK로는 Action Script,C,C#,C++,Clojure,Erlang,Java,Node.js,Objective-C,Perl,PHP,Python,Smalltalk,Tcl등 대부분의 언어를 지원한다.


필자는 일반적으로 RDBMS의 SQL 질의 결과를 캐시하는 용도로서 redis를 사용하고 있으나, 위 소개글 처럼 메모리 기반 NoSQL 솔루션으로 사용될 수도 있다. 


redis에 대한 정보는 여기서 확인하자 : http://www.redis.io/clients


redis는 다양한 언어의 클라이언트를 지원한다. 우리는 java언어를 사용하기 때문에 redis 클라이언트로 jedis를 사용해야 한다.


jedis : https://github.com/xetorthio/jedis


빌드툴로 maven을 사용하고 있다면 spring-data와 jedis 설치는 다음과 같다. 

버전은 알아서 최신 버전으로 바꿔주자. (jedis는 2.0 이상 버전을 사용해야 한다.)

<!-- jedis -->
<dependency> 
    <groupId>redis.clients</groupId>
	<artifactId>jedis</artifactId>
	<version>2.1.0</version>
</dependency> 
<!-- jedis -->

<!-- spring data for jedis -->
<dependency>
	<groupId>org.springframework.data</groupId>
  	<artifactId>spring-data-redis</artifactId>
  	<version>1.0.2.RELEASE</version>
</dependency> 
<!-- spring data for jedis -->

다음으로 spring-context에 다음과 같이 bean 설정을 추가한다. 

<!-- Redis Source -->
<bean id="connectionFactory" class="org.springframework.data.redis.connection.jedis.JedisConnectionFactory">
	<property name="hostName" value="${redis.host}"/>
	<property name="port" value="${redis.port}"/>
	<property name="password" value="${redis.password}"/>
</bean>
    
<bean id="redisTemplate" class="org.springframework.data.redis.core.StringRedisTemplate">
   	<property name="connectionFactory" ref="connectionFactory"/>
</bean>
<!-- Redis Source -->

위 connectionFactory 샘플에서는 가장 기본적인 host, port, password 설정만 있다.

실제는 connection pooling 사용 등 다양한 설정을 처리할 수 있음을 참고한다.


StringRedisTemplate는 RedisTemplate를 상속하는데, RedisTemplate는 spring-data에서 redis로의 데이터 접근 및 처리 명령을 제공하는 helper class이다. redis의 대부분의 연산들이 string 처리에 기반하기 때문에 StringRedisTemplate는 가장 기초적이고 빈번하게 사용된다.


redis에서는 string, hash, list, set, sorted set 자료구조를 제공하는데 spring-data에서는 각각 자료구조에 해당하는 operation을 추상화한 클래스를 제공한다.


 sting

 ValueOperations<K, V>

 list

 ListOperations<K, V>

 hash

 HashOperations<K, HK, HV>

 set  SetOperations<K, V>
 sorted set  ZSetOperations<K, V>


이들 operation 클래스 외에도 주어진 key에 operation를 바인딩한 bounnd***Ops 형태의 이름을 가지는 클래스를 제공하기 도 한다. 이들 클래스는 내부적으로 각각 자료구조에 일치하는 operation 클래스를 사용하고 있기 때문에, 실질적으로위 표에 나열된 클래스를 통해 redis 연산이 수행된다는 것이다. 


각각의 operation 클래스는 jedis에서 제공하는 명령을 그대로 wrapping 하고 있기 때문에 사용법 자체는 그리 어렵지 않다.


redis 서버가 특정 key를 포함하고 있는지 확인하는 메소드는 다음과 같다.


 Boolean RedisTemplate::hasKey(String key)


다음의 예제코드는 BoundHashOperations를 통해 redis 서버에서 hash 구조로 데이터를 저장하고 읽어오는 예이다.

BoundHashOperations은 이미 특정 key에 바인딩되어 있으므로, 데이터를 읽거나 쓸때 key를 지정하지 않는다. (여기서 말하는 key는 hash의 key가 아님. hash 데이터 자체가 바인딩된 key를 의미)

// redis서버에서 hash형태의 데이터를 읽어온다.
public void restore(BoundHashOperations<String, String, String> hashOps) {
	refKey 		 = hashOps.get("refKey");
	refName 	 = hashOps.get("refName");
	name 		 = hashOps.get("name");
	basePath 	 = hashOps.get("basePath");
	relativePath 	 = hashOps.get("relativePath");
	srcName 	 = hashOps.get("srcName");
	srcExt 		 = hashOps.get("srcExt");
	size 		 = NumberUtils.toLong(hashOps.get("size"));
	width 		 = NumberUtils.toInt(hashOps.get("width"));
	height 		 = NumberUtils.toInt(hashOps.get("height"));
	orderNum 	 = NumberUtils.toInt(hashOps.get("orderNum"));
	try { regdate = fmt.parse(hashOps.get("regdate")); } catch(Exception ignore) {/**/}
}
	
// redis서버로 hash형태의 데이터를 저장한다.
public void cache(BoundHashOperations<String, String, String> hashOps) {
	if( StringUtils.isNotEmpty(refKey) ) 		hashOps.put("refKey", refKey);
	if( StringUtils.isNotEmpty(refName) ) 		hashOps.put("refName", refName);
	if( StringUtils.isNotEmpty(name) ) 		hashOps.put("name", name);
	if( StringUtils.isNotEmpty(basePath) ) 		hashOps.put("basePath", basePath);
	if( StringUtils.isNotEmpty(relativePath) ) 	hashOps.put("relativePath", relativePath);
	if( StringUtils.isNotEmpty(srcName) ) 		hashOps.put("srcName", srcName);
	if( StringUtils.isNotEmpty(srcExt) ) 		hashOps.put("srcExt", srcExt);
	if( size != null ) 				hashOps.put("size", String.valueOf(size));
	if( width != null ) 				hashOps.put("width", String.valueOf(width));
	if( height != null ) 				hashOps.put("height", String.valueOf(height));
	if( orderNum != null ) 				hashOps.put("orderNum", String.valueOf(orderNum));
	if( regdate != null ) 				hashOps.put("regdate", fmt.format(regdate));
}		

redis에서 제공하는 list, set 등의 자료구조로의 연산도, redis에서 제공하는 명령셋을 이해하면 어렵지 않게 사용할 수 있다.


마지막으로 redis에 대한 좋은 소개글 링크를 첨부한다.


http://dev.kthcorp.com/2011/07/28/redis-buildingfast-lightweight-webapp/

저작자 표시 비영리 변경 금지
신고

'프로그래밍 > 오픈소스' 카테고리의 다른 글

spring-data에서 redis 사용하기  (0) 2012.12.06
Hibernate @NotFound 어노테이션  (0) 2012.12.05
이클립스에 GlassFish 설치하기  (0) 2011.02.17
SQLite with Eclipse  (1) 2010.05.06
SWF파일 분석 도구  (0) 2010.04.13
boost library 설치  (0) 2010.03.17
libcurl 사용하기  (0) 2010.03.15
SQLite  (3) 2009.09.11
ACE(ADAPTIVE Communication Environment)  (0) 2009.09.03
MySQL++  (0) 2009.08.21
C++ 암복호화 라이브러리 Crypto++  (0) 2009.08.20
Posted by devop

다음과 같이 Parent, Child 두개의 테이블이 존재하고 Parent와 Child가 1:N의 관계를 형성한다고 할때, Parent 테이블의 PK가 Child 테이블에서 FK로 사용되어진다. 아래 그림을 좀 더 자세히 보면 Parent는 0개 이상의 Child와 관계할 수 있고, Child는 반드시 1개의 Parent와 관계해야 하는 제약을 지니는데 이는 즉, parent_seq가 null이 될 수 없음을 의미한다.


 

위 내용은 관계형 데이터베이스 논리 모델링에서 나오는 아주 기초적인 내용이다.


그런데 실제 프로젝트를 진행하다보면 위와 같은 제약조건들을 제대로 지키지 않아 데이터 무결성이 훼손된 경우를 많이 보게 된다. 물론 시스템 퍼포먼스를 위해 의도적으로 비정규화를 수행하는 경우도 많지만, 심지어 모든 테이블에 PK만 존재하고 FK가 전혀 존재하지 않는 경우도 봐왔다. (물론 논리 모델에서는 FK가 존재하지만, 실제 물리 모델에는 FK를 전혀 반영하지 않은 경우이다) 필자는 아직 경험이 일천해 모든 관계형 데이터베이스 전문 서적에서 FK 제약조건에 대해 지켜야 한다고 나와있지만 , 실제 현업에서는 이런식의 데이터베이스 설계가 존재하는지 정확한 이유는 모른다. 아마도 제약조건에 따른 데이터 관리의 까다로움과 퍼포먼스 저하에 대한 우려 때문에 그런것이 아닌가 하고 추측만할 뿐이다.


어찌되었든 제약조건이 정상적으로 걸어져 있다면 Child 테이블에 parent_seq가 0을 가지는 row는 절대 insert될수가 없다. 

그러나 많은 데이터베이스 설계들이 이런 제약조건을 제대로 지키지 않고 있기 때문에 parent_seq가 0인 row가 insert되고 해당 Child의 row로부터 seq가 0인 row를 Parent 테이블에서 select 할 수 있는 가능성의 여지를 남겨두게 된다.


자 그럼 본론으로 들어가서 Hibernate를 통해 Child와 Parent Entity를 @ManyToOne 관계로 매핑하고 FetchType을 EAGER로 설정해 join 연산을 수행하게 되면 아래와 비슷한 오류 메세지를 만나게 된다.


org.hibernate.ObjectNotFoundException: No row with the given identifier exists: [com.orderme.admin.entity.legacy.shop.ShopInfo#0] 


위와 같은 문제를 해결하기 위해 @NotFound 어노테이션을 사용할 수 있다.


 @NotFound(action=NotFoundAction.IGNORE)


식별자 값이 유효하지 않더라도 org.hibernate.ObjectNotFoundException 를 발생시키지 않고 무시한다. 


하지만 이게 과연 좋은 방법인가에 대해서는 고민을 해봐야한다. 위와 같은 조치를 통해 예외를 무시할 경우 데이터 무결성을 훼손시키는 데이터가 어딘가에 존재하고 있는데 그것을 인지할 수 있는 수단을 무시하는 것과 같기 때문이다.


커뮤니티에서도 이와 같은 문제를 지적하는 글을 찾을 수 있었다. 

https://forum.hibernate.org/viewtopic.php?f=1&t=1007085


현재 사용중인 Hibernate 버전은 3.6.10.Final 인데 아직 NotFoundAction의 옵션으로 IGNORE, EXCEPTION 2개만 제공하고 있는 것으로 보이며, 아직까지 해당 이슈에 대한 어떠한 조치는 없는 것 같다.

저작자 표시 비영리 변경 금지
신고

'프로그래밍 > 오픈소스' 카테고리의 다른 글

spring-data에서 redis 사용하기  (0) 2012.12.06
Hibernate @NotFound 어노테이션  (0) 2012.12.05
이클립스에 GlassFish 설치하기  (0) 2011.02.17
SQLite with Eclipse  (1) 2010.05.06
SWF파일 분석 도구  (0) 2010.04.13
boost library 설치  (0) 2010.03.17
libcurl 사용하기  (0) 2010.03.15
SQLite  (3) 2009.09.11
ACE(ADAPTIVE Communication Environment)  (0) 2009.09.03
MySQL++  (0) 2009.08.21
C++ 암복호화 라이브러리 Crypto++  (0) 2009.08.20
Posted by devop