잊지 않겠습니다.

queryDSL이 4.x대로 변경이 되면서 package명에 큰 변화가 생겼습니다. 기존 com.mysema.querydsl 에서 com.querydsl로 package명이 바뀌어서 간략화 되었지요.

기존 querydsl 처리 부분에 대한 build.gradle을 변경한다면 다음과 같습니다.


dependencies {
compile("org.springframework.boot:spring-boot-starter-web") {
exclude module: "spring-boot-starter-tomcat"
}
compile 'com.graphql-java:graphql-java:2.1.0'
compile("org.springframework.boot:spring-boot-starter-jetty")
compile("org.springframework.boot:spring-boot-starter-actuator")
testCompile("junit:junit")
testCompile("org.springframework.boot:spring-boot-starter-test")
compile 'com.querydsl:querydsl-apt:4.1.4'
compile 'com.querydsl:querydsl-jpa:4.1.4'
compile 'org.springframework.data:spring-data-jpa:1.10.4.RELEASE'
compile group: 'org.hibernate', name: 'hibernate-core', version: '5.2.3.Final'
compile "org.hibernate:hibernate-entitymanager:5.2.3.Final"
compile 'com.h2database:h2:1.4.187'
compile group: 'org.aspectj', name: 'aspectjrt', version: '1.8.9'
compile group: 'org.aspectj', name: 'aspectjweaver', version: '1.8.9'
compileOnly "org.projectlombok:lombok:1.16.12"
}


sourceSets {
main {
java {
srcDirs 'src/main/java', 'src/main/generated'
}
}
}

task generateQueryDSL(type: JavaCompile, group: 'build', description: 'Generates the QueryDSL query types') {
file(new File(projectDir, "/src/main/generated")).deleteDir()
file(new File(projectDir, "/src/main/generated")).mkdirs()
source = sourceSets.main.java
classpath = configurations.compile + configurations.compileOnly
options.compilerArgs = [
"-proc:only",
"-processor", "com.querydsl.apt.jpa.JPAAnnotationProcessor"
]
destinationDir = file('src/main/generated')
}

compileJava {
dependsOn generateQueryDSL
}

clean.doLast {
file(new File(projectDir, "/src/main/generated")).deleteDir()
}


기존의 generateQueryDSL task를 유지할 필요 없이, java compile option으로 QClass 생성을 넣어주는것으로 script를 간략화시키는것이 가능합니다. 또한 Q class가 존재할 때, 기존 Q Class를 지워주지 않으면 compile 시에 에러를 발생하게 되기 때문에 지워주는 작업역시 doFirst에 같이 실행하고 있습니다.

기존의 build.gradle보다 간략해보여서 전 개인적으로 맘에 드네요.

저작자 표시 동일 조건 변경 허락
신고
Posted by xyzlast Y2K
TAG gradle, java, JPA
지금 저는 Gradle을 이용한 web application 배포시에 FTP를 사용하고 있습니다.

그런데, 이 부분에 조금 문제가 있는 것이… 기존 gradle의 ant ftp module의 경우 가끔씩 hang이 걸려서 FTP upload를 실패하는 경우가 왕왕 보입니다. 그리고 FTP upload 시에 아무런 로그가 표시되지 않고, hang이 걸려버려서 정상적으로 upload가 되고 있는지에 대한 확인이 불가능했습니다.

그래서, FTPClient를 이용해서 File upload를 한번 만들어봤습니다.

기본적으로 commons-net의 FTPClient를 이용해서 처리합니다. 먼저 buildscript에 common-net을 등록시킵니다.

buildscript {
    repositories {
        mavenCentral()
    }
    dependencies {
        classpath 'commons-net:commons-net:3.3'
        classpath 'commons-io:commons-io:2.4'
    }
}

다음은 FTPClient를 이용한 upload code입니다. groovy 스러운 문법은 전 아직 잘 안되더군요.;;


import java.io.File
import java.io.FileInputStream
import java.io.IOException
import java.io.InputStream
import java.io.PrintWriter
import org.apache.commons.net.PrintCommandListener
import org.apache.commons.net.ftp.FTP
import org.apache.commons.net.ftp.FTPClient
import org.apache.commons.net.ftp.FTPReply
import org.apache.commons.io.IOUtils


void ftpUpload(ftpUrl, ftpUsername, ftpPassword, targetPath) {
    def ftp = new FTPClient()
    ftp.connect(ftpUrl)
    int reply = ftp.getReplyCode()
    if(!FTPReply.isPositiveCompletion(reply)) {
        ftp.disconnect()
        throw new Exception("Exception in connecting to FTP Server")
    }
    ftp.login(ftpUsername, ftpPassword)
    ftp.setFileType(FTP.BINARY_FILE_TYPE)
    ftp.changeWorkingDirectory(targetPath)
    for(File f : file(getDistPath()).listFiles()) {
        upload(f, ftp)
    }
    ftp.disconnect()
}

void upload(File src, FTPClient ftp) {
    if (src.isDirectory()) {
        ftp.makeDirectory(src.getName())
        ftp.changeWorkingDirectory(src.getName())
        for (File file : src.listFiles()) {
            upload(file, ftp);
        }
        ftp.changeToParentDirectory();
    }
    else {
        InputStream srcStream = null;
        try {
            def uploadCompleted = false
            while(!uploadCompleted) {
                srcStream = src.toURI().toURL().openStream()
                println 'upload : ' + src.getName()
                uploadCompleted = ftp.storeFile(src.getName(), srcStream)
                if(!uploadCompleted) {
                    println 'upload failed : retry this file ' + src.getName()
                    IOUtils.closeQuietly(uploadCompleted)
                }
            }
        } catch(Exception ex) {
            println ex.getMessage()
        }
        finally {
            IOUtils.closeQuietly(srcStream);
        }
    }
}

java 코드 같은 느낌입니다. groovy를 사용하고 있으면 좀 더 groovy 스러운 코드여야지 될 것 같은데요. ^^;;


저작자 표시 동일 조건 변경 허락
신고
Posted by xyzlast Y2K

HikariCP 소개

Java 2014.08.21 17:10

HikariCP

BoneCP를 재치고 놀라운 속도를 자랑하는 DB Connection Pool입니다. BoneCP의 경우, Hibernate 4.x 버젼에서의 지원이 조금 애매해진 경향이 있습니다. (최신 버젼의 Hibernate에서는 에러가 발생합니다.) 반면에 HikariCP의 경우에는 Hibernate와의 통합 jar가 나오는 등, 계속해서 밀어주고 있다는 느낌이 강하게 듭니다.

GitHub page

https://github.com/brettwooldridge/HikariCP

Hibernate + HikariCP

build.gradle에 다음 dependency들을 추가합니다.

    compile 'org.slf4j:slf4j-api:1.7.5'
    compile 'com.zaxxer:HikariCP:2.0.1'
    compile 'org.javassist:javassist:3.18.2-GA'

hibernate.cfg.xml 파일에 다음 항목을 추가합니다.

<property name="connection.provider_class">com.zaxxer.hikari.hibernate.HikariConnectionProvider</property>
<property name="hibernate.hikari.dataSourceClassName">com.mysql.jdbc.jdbc2.optional.MysqlDataSource</property>
<property name="hibernate.hikari.dataSource.url">jdbc:mysql://localhost/test</property>
<property name="hibernate.hikari.dataSource.user">root</property>
<property name="hibernate.hikari.dataSource.password">qwer12#$</property>
<property name="hibernate.hikari.dataSource.cachePrepStmts">true</property>
<property name="hibernate.hikari.dataSource.prepStmtCacheSize">250</property>
<property name="hibernate.hikari.dataSource.prepStmtCacheSqlLimit">2048</property>
<property name="hibernate.hikari.dataSource.useServerPrepStmts">true</property>

Spring + HikariCP

@Configuration을 이용해서 HikariCP를 사용하는 법은 다음과 같습니다.

DataSource를 이용하는 경우 (ex: mssql server)
    @Bean(destroyMethod = "shutdown")
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();

        dataSource.setUsername(env.getProperty(CONNECT_USERNAME));
        dataSource.setPassword(env.getProperty(CONNECT_PASSWORD));

        dataSource.setDataSourceClassName("com.microsoft.sqlserver.jdbc.SQLServerDataSource");
        dataSource.addDataSourceProperty("url", env.getProperty(CONNECT_URL));

        int minConnection = Integer.parseInt(env.getProperty(CONNECT_MIN));
        dataSource.setMinimumIdle(minConnection);
        int maxConnection = Integer.parseInt(env.getProperty(CONNECT_MAX));
        dataSource.setMaximumPoolSize(maxConnection);

        return dataSource;
    }
Driver를 이용하는 경우 (ex: mssql server)
    @Bean(destroyMethod = "shutdown")
    public DataSource dataSource() {
        HikariDataSource dataSource = new HikariDataSource();

        dataSource.setUsername(env.getProperty(CONNECT_USERNAME));
        dataSource.setPassword(env.getProperty(CONNECT_PASSWORD));
        dataSource.setDriverClassName(env.getProperty(CONNECT_DRIVER));
        dataSource.setJdbcUrl(env.getProperty(CONNECT_URL));

        int minConnection = Integer.parseInt(env.getProperty(CONNECT_MIN));
        dataSource.setMinimumIdle(minConnection);
        int maxConnection = Integer.parseInt(env.getProperty(CONNECT_MAX));
        dataSource.setMaximumPoolSize(maxConnection);

        return dataSource;
    }

HikariCP property

HikariCP의 property 설정은 다음과 같습니다.

autoCommit (default : true)

connection이 종료되거나 pool에 반환될 때, connection에 속해있는 transaction을 commit 할지를 결정합니다.

readOnly (default : false)

database connection을 readOnly mode로 open합니다. 이 설정은 database에서 지원하지 않는다면 readOnly가 아닌 상태로 open되기 때문에, 지원되는 database 목록을 확인해보고 사용해야지 됩니다.

transactionIsolation (default : none)

java.sql.Connection 에 지정된 Transaction Isolation을 지정합니다. 지정된 Transaction Isoluation은 다음과 같습니다.

  • Connection.TRANSACTION_NONE : transaction을 지원하지 않습니다.
  • Connection.TRANSACTION_READ_UNCOMMITTED : transaction이 끝나지 않았을 때, 다른 transaction에서 값을 읽는 경우 commit되지 않은 값(dirty value)를 읽습니다.
  • Connection.TRANSACTION_READ_COMMITTED : transaction이 끝나지 않았을 때, 다른 transaction에서 값을 읽는 경우 변경되지 않은 값을 읽습니다.
  • Connection.TRANSACTION_REPEATABLE_READ : 같은 transaction내에서 값을 또다시 읽을 때, 변경되기 전의 값을 읽습니다. TRANSACTION_READ_UNCOMMITTED 와 같이 사용될 수 없습니다.
  • Connection.TRANSACTION_SERIALIZABLE : dirty read를 지원하고, non-repeatable read를 지원합니다.

기본값을 각 Driver vendor의 JDBCDriver에서 지원하는 Transaction Isoluation을 따라갑니다. (none으로 설정시.)

category (default : none)

connection에서 연결할 category를 결정합니다. 값이 설정되지 않는 경우, JDBC Driver에서 설정된 기본 category를 지정하게 됩니다.

connectionTimeout(default: 30000 - 30 seconds)

connection 연결시도시 timeout out값을 설정합니다. 이 시간내로 connection을 연결하는데 실패하면 SQLException을 발생합니다.

idleTimeout(default : 600000 - 10 minutes)

connection Pool에 의하여 확보된 connection의 maximum idle time을 결정합니다. connection Pool에 의하여 확보된 connection이 사용되지 않고, Pool에 의해서만 이 시간동안 관리된 경우, connection을 DB에 반환하게 됩니다. 값을 0으로 설정하는 경우, 확보된 connection을 절대 반환하지 않습니다.

maxLifetime(default : 1800000 - 30 minutes)

connection Pool에 의하여 확보된 connection의 최대 생명주기를 지정합니다. connection을 얻어낸지, 이 시간이상되면 최근에 사용하고 있던 connection일지라도, connection을 close시킵니다. 사용중에 있던 connection은 close 시키지 않습니다. (사용이 마쳐지면 바로 close 됩니다.) HikariCP에서는 이 값을 30~60 minutes 사이의 값을 설정하라고 강력권고합니다. 값을 0로 설정하는 경우 lifetime은 무제한이 됩니다.

leakDetectionThreshold (default : 0)

connectionPool에서 반환된 connection의 올바른 반환이 이루어졌는지를 확인하는 thread의 갯수를 지정합니다. 이 값을 0로 지정하는 경우, leak detection을 disable 시키게 됩니다. 만약에 또다른 connection pool을 사용하고 있다면, 다른 connection pool에서 만들어진 connection을 leak으로 판단하고 connection을 닫아버릴 수 있습니다.

jdbc4ConnectionTest (default : true)

connection을 맺은다음, Connection.isValid() method를 호출해서 connection이 정상적인지를 확인합니다. 이 property는 다음에 나올 connectionTestQuery에 매우 밀접한 영향을 받습니다.

connectionTestQuery (default : none)

Connection.isValid() method를 지원하지 않는 ‘legacy’ database를 위한 빠른 query를 지정합니다. (ex: VALUES 1) jdbc4ConnectionTest가 더 유용하기 때문에 사용하지 않는 것이 좋습니다.

connectionInitSql (default : none)

새로운 connection이 생성되고, Pool에 추가되기 전에 실행될 SQL query를 지정합니다.

dataSourceClassName (default : none)

JDBC driver에서 지원되는 dataSourceClassName을 지정합니다. 이 값은 driverClassName이 지정된 경우, 지정할 필요가 없습니다.

dataSource (default : none)

사용자가 만든 dataSource를 Pool에 의하여 wrapped하는 것을 원하는 경우, 이 값을 지정하여 사용합니다. HikariCP는 이 문자열을 이용해서 reflection을 통해 dataSource를 생성합니다. 이 값이 설정되는 경우, dataSourceClassName, driverClassName 과 같은 값들은 모두 무시 됩니다.

driverClassName

HikariCP에서 사용할 DriverClass를 지정합니다. 이 값이 지정되는 경우, jdbcUrl이 반드시 설정되어야지 됩니다.

jdbcUrl

jdbcUrl을 지정합니다. driverClassName이 지정된 경우, jdbcUrl을 반드시 지정해줘야지 됩니다.

minimumIdle (default : maximumPoolSize)

connection Pool에서 유지할 최소한의 connection 갯수를 지정합니다. HikariCP에서는 최고의 performance를 위해 maximumPoolSize와 minimumIdle값을 같은 값으로 지정해서 connection Pool의 크기를 fix하는 것을 강력하게 권장합니다.

# maximumPoolSize

connection Pool에서 사용할 최대 connection 갯수를 지정합니다. 이 부분은 운영환경과 개발환경에 매우 밀접하게 연결되는 부분으로, 많은 테스트 및 운영이 필요합니다.

username

Connection을 얻어내기 위해서 사용되는 인증 이름을 넣습니다.

password

username과 쌍이 되는 비밀번호를 지정합니다.

poolName (default : auto-generated)

logging과 JMX management에서 지정할 pool의 이름을 지정합니다.

registerMbeans (default : false)

JMX management Beans에 등록되는 될지 여부를 지정합니다.

저작자 표시 비영리 변경 금지
신고
Posted by xyzlast Y2K

Hibernate에서의 양방향 @OneToOne의 이용

Domain Model을 만들때, @OneToOne은 많은 의미를 갖습니다. Master-Detail 구조에서 Detail을 의미하기도 하고, Query를 만들어서 처리할 때, 많은 데이터를 한번에 읽기보다는 작은 데이터를 먼저 읽어서 표시하는데에 사용하는 등의 어찌보면 역정규화를 이용할 때 주로 사용됩니다. (개인적으로는 하나의 Big Table로 처리하는 것이 개발 상에서는 가장 편할 지도 모른다. 그런데 이렇게 하는 경우, Query를 사용해서 처리할 때 Query양이 많아지고 Table이 너무나 커져버리는 단점이 있습니다.) 또한, @OneToOne Relation은 Table을 이용한 BL의 확장에 큰 영향을 주게 됩니다. 새로운 BL Process가 생성되었을 때, 기존 Table을 변경하지 않는 방향으로 BL을 확장할 수 있기 때문에 현업에서는 자주 사용되는 방법입니다.

그런데, 여기에 문제가 있는 것이, 기본적으로 JPA에서는 @OneToOne은 모두 Early Loading으로만 처리가 가능하다는 점입니다. 이를 Lazy Loading으로 전환하기 위해서는 단방향처리만이 가능하게 처리해야지 됩니다. Parent에서 Key를 갖는 형식으로 말이지요.

그런데, 이와 같은 방법 역시 문제를 가지고 있습니다. 단방향처리만이 가능하다는 것은 기본적으로 처리하는 parent entity에서 FK를 가져야지 된다는 점입니다. Table 추가를 통한 Domain Logic의 확장에 있어서, 이는 기존 Table을 변경해야지 되는 큰 약점을 가지게 됩니다.

따라서, @OneToOne을 양방향으로 사용할 수 있는 방법을 반드시 제시할 수 있어야지 Domain Logic을 생성하는데 ORM을  사용하는 것이 될 수 있을 것입니다. 양방향(bidirection)으로 @OneToOne을 Lazy Loading으로 사용하기 위한 방법은 다음 3가지가 제시됩니다.

@OneToMany, @ManyToOne을 이용하는 방법

객체를 @OneToOne으로 사용하지 않고, Lazy Loading을 사용하기 위해서 다른 Relation으로 정의하고, Code상에서는 @OneToOne 처럼 사용하는 것입니다. 개인적으로 가장 싫어하는 방법입니다. 객체간의 관계를 객체만으로 명확히 보이는 ORM의 장점을 모두 무시하는 방법이라고 생각합니다. 또한 QueryDSL이나 HQL의 작성시에 매우 어렵게 되며, 생성되는 Query 역시 효율성이 떨어지는 Query를 생성하게 됩니다.

byte code instrument를 이용하는 방법

: 이는 CTW(compile time weaver)를 이용해서 처리하는 것입니다. Hibernate에서 제공하는 build time instrument를 이용해서 compile 시에 처리하는 방법입니다. 이에 대한 설명은 다음 Hibernate의 문서를 참고하는 것이 좋습니다. (ant 등 build Task가 별도로 필요합니다. ) -http://docs.jboss.org/hibernate/core/3.3/reference/en/html/performance.html#performance-fetching-lazyproperties
: 이 부분에 대한 설명은 Hibernate 4.x 대에서 제거되었습니다. 4.x에서의 byte code instrument가 지원되는지는 추가 확인이 필요합니다.
: Proxy를 사용하지 않기 때문에 다음과 같은 NoProxy 설정이 필요합니다.

@OneToOne(fetch = FetchType.LAZY, optional = true) @LazyToOne(LazyToOneOption.NO_PROXY) @JoinColumn(name = "customerPetitId", nullable = true) private CustomerPetit customerPetit;
runtime byte code instrument를 이용하는 방법

: 이 방법은 LTW(load time weaver)를 이용해서 처리하는 방법입니다. 이 방법을 사용하기 위해서는 full-blown JEE environment에서 구성되어야지 됩니다. tomcat, jboss, jetty와 같은 JEE 환경에서만 사용이 가능하다는 단점을 갖습니다. (JUnit Test 환경에서는 동작하지 않습니다.)
: hibernate.ejb.use_class_enhancer 설정을 true로 해주는 것으로 설정이 가능합니다. 이 방법 역시 @LazyToOne(LazyToOneOption.NO_PROXY) annotation이 필요합니다.
: Spring 환경에서도 사용이 가능합니다. 단, Lazy Loading시의 performance 이슈는 해결되어야지 됩니다.

위 3가지 방법이 JBoss에서 문서화된 방법들입니다. 이러한 방법 이외에 다른 방법을 하나 더 소개하고자 합니다. 이 방법은 @OneToMany, @ManyToOne 방법과 같이 HQL의 변경을 가지고 오지도 않고, build 시에 새로운 task를 생성할 필요가 없으며, LTW와 같이 성능 저하나 환경을 따르지도 않습니다.

FieldHandler, FieldHandled를 이용한 Bi-Direction @OneToOne

이 방법은 Hibernate의 내부에 이미 구현된 FieldHandler를 사용하는 방법입니다. FieldHandler는 객체에 값을 주입할 때, Hibernate core에서 사용되는 객체입니다. 또한 Hibernate는 FieldHandled interface를 구현한 객체의 경우, Hibernate에서 load될 때, 자동으로 주입시켜주기 때문에 우리가 별도로 개발할 필요는 없습니다.

구현하기 위한 조건은 다음 2가지입니다.

  1. @LazyToOne(LazyToOneOption.NO_PROXY) annotation
  2. Entity 객체에 FieldHandled interface 구현

구현되는 코드는 다음과 같습니다. (양방향이기 때문에 Parent, Child Entity에 모두 구현해야지 됩니다.)

public class OperationResult extends AbstractInsertUpdateEntity implements Serializable, FieldHandled {
    @OneToOne(fetch = FetchType.LAZY, optional = true)
    @LazyToOne(LazyToOneOption.NO_PROXY)
    @JoinColumn(name = "operationId", nullable = true)
    private OperationPlan plan;

    public void setPlan(OperationPlan plan) {
        if(fieldHandler != null) {
            this.plan = (OperationPlan) fieldHandler.writeObject(this, "plan", this.plan, plan);
        } else {
            this.plan = plan;
        }
    }

    public OperationPlan getPlan() {
        if(fieldHandler != null) {
            this.plan = (OperationPlan) fieldHandler.readObject(this, "plan", plan);
        }
        return this.plan;
    }

    private FieldHandler fieldHandler;

    @Override
    public void setFieldHandler(FieldHandler handler) {
        this.fieldHandler = handler;
    }

    @Override
    public FieldHandler getFieldHandler() {
        return fieldHandler;
    }
}

이 구현자체는 byte instrument에서 compile/runtime 시에 동작하는 원리와 완전히 동일하게 동작합니다. byte code instrument를 이용한 compile/runtime 시의 동작이 위의 get/set method에 대한 AOP 동작을 주입하는 것이기 때문입니다.

개인적으로는 저는 이 방법을 추천합니다. 이 방법의 경우 다음과 같은 장점이 있습니다.

  1. @OneToMany, @ManyToOne과 같은 HQL을 변경시키는 Relation을 쓰지 않아도 됩니다.
  2. LTW와 같은 성능저하가 작습니다.
  3. CTW와 같이 build시에 따로 처리할 필요가 없습니다.

reference


저작자 표시 비영리 변경 금지
신고
Posted by xyzlast Y2K

Hibernate @OneToOne

Java 2014.08.19 01:50

Hibernate @OneToOne

  • 기본적으로 @OneToOne을 사용하지 않는 것이 좋다. @OneToOne의 경우에는 Lazy Loading에 심각한 문제가 있고, 이는 전체 객체에 대한 어마어마한 로딩을 가지고 오는 결과를 가지고 온다.
  • @SecondaryTable로 해결할수도 있으나, BL에 따라서 생각하는 것이 좋다.

기본적으로 다음 기준을 따른다.

  • parent가 되는 entity를 결정하고, 그 entity가 나중에 insert되는 senerio를 택한다.
  • 여러 table의 집합 정보를 가지게 된다면 그 table은 child로 구성한다.

예시

예를 들어 다음과 같은 BL이 존재한다면 Entity는 다음과 같이 구성되어야지 된다.

  • Book과 Note가 존재하고, 둘의 Summary를 지정한다.
  • Book, Note와 Summary는 @OneToOne 관계를 가지게 된다.

이럴때, Entity code의 구성은 다음과 같다.

@Entity
public class Book {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    private String title;

    @OneToOne(fetch = FetchType.LAZY, optional = false, cascade = CascadeType.ALL)
    @JoinColumn(name = "bookId", nullable = false, unique = true)
    private Summary summary;
}


@Entity
public class Note {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    private String title;

    @OneToOne
    @JoinColumn(name = "noteId")
    private Summary summary;
}

@Entity
public class Summary {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @Column
    private String context;
}

위 entity로 Book을 추가하는 code는 다음과 같이 구성된다.

@Override
public Book add(String name) {
    SessionFactory sessionFactory = SessionUtils.build();
    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    try {
        Book book = new Book();
        book.setTitle(name);

        Summary summary = new Summary();
        summary.setContext("CONTEXT FROM BOOK");
        book.setSummary(summary);
        session.save(book);
        transaction.commit();

        return book;
    } catch(Exception ex) {
        transaction.rollback();
        throw ex;
    } finally {
        session.close();
    }
}

위 코드가 실행되면, 다음과 같은 Query 결과를 보여준다.

Hibernate: insert into Summary (context) values (?)
Hibernate: insert into Book (bookId, title) values (?, ?)
Hibernate: select this_.id as id1_0_0_, this_.bookId as bookId3_0_0_, this_.title as title2_0_0_ from Book this_

DB로 생각하면, 먼저 insert될 정보가 main, parent가 되어야지 되고, child는 나중에 insert가 되어야지 된다고 생각하기 쉽다. 그렇지만, 이는 @OneToMany로 지정된 parent-child 구조에서 이렇게 되는 것이고, @OneToOne의 경우에는 child가 먼저 저장이 되어야지 되는 것을 명심하자. 이는 DB Table의 구조에 지대한 영향을 미치게 된다.

DB 구조

  • @OneToMany의 경우, child에서 parent PK를 갖는 구조가 되어야지 된다.
  • @OneToOne의 경우, parent에서 child PK를 갖는 구조가 되어야지 된다.

DB의 구조는 BL을 따라가기 때문에, 어떤 기준으로 검색을 해야지 되는지에 따라서 Table구조가 바뀐다면 위 원칙만을 기억하고 처리하면 가장 좋을 것 같다.

@OneToOne에서의 Lazy 문제

기본적으로 @OneToOne은 Early Loading을 하게 된다. 그 이유는 null 값이 가능한 OneToOne child를 Proxy화 할 수 없기 때문이다. (null이 아닌 proxy instance를 return하기 때문에 DB값의 null을 표현하는 것이 불가능하다.) 따라서 JPA 구현체는 기본적으로 @OneToOne에서 Lazy 를 허용하지 않고, 즉시 값을 읽어 들인다. Lazy를 설정할 수 있지만, 동작하지 않는다.

@OneToOne에서 Lazy Loading을 가능하게 하기 위해서는 다음과 같은 처리가 필요하다.

  • nullable이 허용되지 않는 @OneToOne 관계. (ex: Plan과 PlanResult)
  • 양방향이 아닌, 단방향 @OneToOne 관계. (parent -> child)
  • @PrimaryKeyJoin은 허용되지 않음.

위 3가지 조건을 모두 만족하는 code는 다음과 같다.

    @OneToOne(fetch = FetchType.LAZY, optional = false, cascade = CascadeType.ALL)
    @JoinColumn(name = "bookId", nullable = false, unique = true)

양방향 @OneToOne Lazy loading entity

기본적으로 Lazy Loading을 위해서는 양방향 으로는 되지 않는다. 되게 하기 위해서는 다음 site들을 참고하길 바란다.

Summary

  • Hibernate에서 @OneToOne은 피할수 있으면 최대한 피하라. (@SecondTable과 같은 방법이 있다.)
  • @OneToMany와 @OneToOne은 parent, child의 저장 순서가 다르다.
    • @OneToOne에서는 child가 먼저 저장이 되어야지 되고, @OneToMany는 parent가 먼저 저장이 되어야지 된다.
  • @OneToOne을 Lazy loading하고자 하면, 반드시 다음 3가지 조건을 지켜야지 된다.
    • nullable이 허용되지 않는 @OneToOne 관계만이 허용된다. (ex: Plan과 PlanResult)
    • 양방향이 아닌, 단방향 @OneToOne 관계만이 허용된다. (parent -> child)
    • @PrimaryKeyJoin은 허용되지 않는다.


저작자 표시 비영리 변경 금지
신고
Posted by xyzlast Y2K

Dependency Injection

DI란 사용될 Object들이 의존성을 갖는 여러 객체들을 직접 자신이 얻어내는 것을 의미한다. DI는 여러분의 code를 느슨한 결합상태로 만들고, 테스트 하기 쉽게 만들며, 읽기 쉽게 만들어준다.

Java의 Official standard인 JSR-330에서 정의된 DI에 대해서도 역시 알아볼 것이다.

Inject some knowledge - understanding IoC and DI

IoC와 DI의 개념은 매우 헛갈리며, 이 둘을 섞어서 이야기하는 경우가 매우 많다. 이 둘에 대한 내용을 좀더 알아보기로 한다.

IoC

만약에 IoC를 사용하고 있지 않다면, program의 logic은 함수의 조합에 의하여 조절될 것이다. 매우 정밀한 design에 의해서 꾸며진 이 함수들은 여러 Object들에 의해서 재사용이 되어가며 사용되고 있을것입니다.
IoC를 사용한다면, 이에 대한 “central control” 이라는 개념자체를 뒤집게 됩니다. 호출자의 code에 의해 다뤄지는 program의 실행으로 구성이 되며 모든 Program의 Logic은 호출되는 subroutine에 의하여 encapsulated 되게 됩니다.
이는 Design Pattern 중 Hollywood Principal과 동일합니다. 여러분의 code가 호출하는 것이 아닌, 어떤 곳에서 여러분의 code를 호출하는 방식으로의 변경을 의미하게 됩니다.

Text 기반의 mud game이 있고, 이를 GUI Framework로 감싼 Version이 있다고 가정해보도록 합시다. GUI Framework는 어떠한 Logic도 가지고 있지 않습니다. “LEFT Click” 이라는 event가 호출이 되면, 이에 따른 GO LEFT 라는 command가 실행이 되는 것 뿐입니다. 이에 대한 실질적인 로직은 모두 Text 기반의 mud game안에 들어가 있습니다.

IoC에 대해서 이야기한다면, 다른 개념으로 생성자에 대한 접근을 볼 수 있습니다. 우리가 일반적인 개발방식으로 Program Logic을 생성한다면, 많은 생성자를 Call 하는 Code내에서 생성하게 되는 것을 볼 수 있습니다. 그런데, 이와 같은 방식을 뒤집에서 이미 생성되어 있는, 또는 생성할 방법이 결정나 있는 Factory Pattern을 통해 구성되는 객체들로 만들어진다면 이는 객체에 대한 Control을 객체에게 넘겨준 IoC가 적용된 상태라고 할 수 있습니다.

DI (Dependency Injection)

DI는 IoC의 일부입니다. 이는 여러분의 code안에 있는 객체의 dependency를 code 바깥에서 code가 생성되거나 실행될 때, 주입(Inject)하는 것에 촛점이 맞춰져있습니다.

IoC Container를 Spring에서 이야기한다면, ApplicationContext가 그 역활을 합니다.
DI의 경우, @Autowired가 그 일을 담당하게 됩니다.

DI를 구성하는 경우 다음과 같은 장점을 갖습니다.

Loose coupling

많은 코드들 내부에서 가지고 있는 new를 통한 객체의 생성을 하지 않기 때문에, 객체에 대한 coupling이 작아지게 됩니다. interface를 통한 결합을 하게 된다면, 의존되는 객체에 대한 의존성을 제거하고, 객체의 행위에만 집중할 수 있게 됩니다.

Testability

Loose coupling을 통해, 단순화된 객체들은 Test를 행하기 쉽습니다. Loose coupling이 되지 않은 객체들은 각각의 생성자 및 모든 method의 호출 방법의 차이에 따른 Test가 매우 힘듭니다.

Greater cohesion (높은 응집성)

생성된 code들은 객체에 대한 생성방법이나 여러 부가적 initialize를 할 필요가 없기 때문에, Logic에 대한 높은 응집성을 갖게 됩니다. 이는, code에 대한 가독성을 높여주는 장점을 가지고 있습니다.

Reusable components

loose coupling에 의한 결과로서, 다른 객체에서 사용하기 쉬운 상태로 만들어주고, 이는 객체에 대한 재 사용성을 높여주게 됩니다.

Lighter code

이는 높은 응집성에 의하여 나온 결과입니다. dependency 된 객체에 대한 추가 코드는 더이상 존재하지 않고, 사용자가 작성한 code가 직접적으로 호출하는 부분만이 남겨져 있는 상태입니다. 가독성이 높아지고, 버그가 생길 구석이 좀 더 줄어들수 있습니다.

DI의 표준화

2004년 google Guice의 Bob Lee와 SpringSource의 Rod Johnson은 JSR-330(javax.inject)에 대하여 합의하게 됩니다. 이 합의로 인하여, 요구되는 Framework내에서 표준화된 DI를 사용할 수 있게 되었습니다. 가벼운 Guice Framework를 사용하다가, 보다 많은 기능을 가진 Spring Framework로의 전환이 자유롭게 된 것을 의미합니다.

실질적으로 위의 말은 거의 불가능에 가깝습니다. Guice와 Spring Framework의 성격이 IoC와 DI를 제공하는 것이 사실이지만, Project가 진행되어가는데 있어서, 기본적인 javax.inject의 기능만이 아닌 다른 기능을 이용해서 처리하게 되는 것이 일반적입니다. 왜냐면 javax.inject는 Guice와 Spring의 가장 기본적인 기능중 공통점만을 가지고 있기 때문입니다.
지금, javax를 이용하는 경우는, JNDI를 이용한 Resource의 관리 이외에는 크게 사용되고 있지 않습니다. Spring, Guice 만을 이용하게 되는 것이 일반적이지요.

@Inject annotation

@Inject annotation은 삽입(Inject)될 dependency를 지정합니다. @Inject는 다음과 같은 곳에서 사용가능합니다.

  • Constructor
  • Methods
  • Fields

@Qualifier annotation

@Qualifier annotation은 객체들의 identify를 지정합니다. 이는 뒤에 나올 @Named와 밀접한 관계를 갖습니다.

@Named annotation

@Inject와 같이 사용됩니다. @Qualifier에 의하여 지정된 이름을 가진 객체를 @Named에 의해서 지정하여 삽입(Inject)하게 됩니다.

@Scope annotation

삽입될 객체의 생명주기를 나타냅니다. 이는 다음과 같습니다.

  • @Scope가 선언되지 않는 경우, 삽입(Inject)가 발생할 때마다, 객체를 새로 생성하게 됩니다.
  • @Scope가 선언되는 경우, 삽입(Inject)가 발생될 때, 기존에 생성된 객체가 있는 경우에는 그 객체를 사용하게 됩니다. 이는 thread-safe해야지 될 필요성을 갖게 됩니다.

@Scope annotation은 각 IoC Framework에서 새롭게 정의되어서 사용되는 것이 일반적이니다. 또한 @Singleton annotation이 생긴후, JSR-330에서는 @Singleton을 이용한 객체 선언을 주로 해주는 경우가 더 많습니다.

@Singleton annotation

@Scope가 선언된것과 동일하게 동작합니다. 이는 거의 모든 DI Framework의 기본 동작입니다.

Provide< T > interface

T 객체에 대한 구현이 아닌 Provide<T>에 대한 구현을 요구하는 경우가 있습니다. 이런 경우, 다음과 같은 목적으로 사용되게 됩니다.

  • 여러 instance를 생성해서 사용하는 경우.
  • 객체를 사용할 때, Lazy Loading을 이용해서 객체를 얻어낼 필요가 있을 때.
  • circular dependency를 회피할 목적으로.

Google Guice를 이용한 DI sample code

/**
 * Created by ykyoon on 14. 8. 5.
 * code에 @Inject가 될 대상
 */
public class AgentFinder {
    public void doSomeThing() {
        System.out.println("Print SomeThing");
    }
}

/**
 * Created by ykyoon on 14. 8. 5.
 * @Inject를 이용해서 서비스 받을 대상
 */
public class HollyWoodService {
    private AgentFinder agentFinder;

    @Inject
    public HollyWoodService(AgentFinder agentFinder) {
        this.agentFinder = agentFinder;
    }
}

/**
 * Created by ykyoon on 14. 8. 5.
 * AgentFinder에 대한 dependency될 객체 설정 - Spring의 @Configuration과 거의 동일
 */
public class AgentFinderModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(AgentFinder.class);
    }
}

위 코드에서 보면 알수 있듯이, AbstractModule을 이용한 Configuration이 행해진다는 것에 유의할 필요가 있다. DI를 사용하는 code를 JUnit test code로 작성하면 다음과 같다.

public class HollyWoodServiceTest {

    private Injector injector = null;

    @Before
    public void setUp() {
        injector = Guice.createInjector(new AgentFinderModule());
        assertThat(injector, is(not(nullValue())));
    }

    @Test
    public void getHollywoodService() {
        HollyWoodService ho = injector.getInstance(HollyWoodService.class);
        assertThat(ho, is(not(nullValue())));
    }
}

Google Guice의 AbstractModule을 보면 Spring의 @Configuration과 매우 유사합니다. 그리고 문법의 경우에는 .NET 진영의 ninject와 거의 유사합니다. (서로간에 영향을 받은건지, 아니면 ninject가 영향을 받은 건지는 잘 모르겠습니다.)

이번에는 객체의 생명주기를 결정하는 Scope입니다.

Scope를 이용한 객체 생명주기의 설정

AppModule내에서 객체의 생명주기를 결정 가능합니다.

public class AgentFinderModule extends AbstractModule {
    @Override
    protected void configure() {
        bind(AgentFinder.class)
                .annotatedWith(Names.named("primary"))
                .to(AgentFinderImpl.class)
                .in(Singleton.class);
        bind(HollyWoodService.class);
    }
}

위 구성으로 다음 테스트 코드를 실해하면 결과는 다음과 같습니다.

public class HollyWoodServiceTest {

    private Injector injector = null;

    @Before
    public void setUp() {
        injector = Guice.createInjector(new AgentFinderModule());
        assertThat(injector, is(not(nullValue())));
    }

    @Test
    public void getHollywoodService() {
        HollyWoodService ho1 = injector.getInstance(HollyWoodService.class);
        assertThat(ho1, is(not(nullValue())));
        HollyWoodService ho2 = injector.getInstance(HollyWoodService.class);
        System.out.println(ho1);
        System.out.println(ho2);
        assertThat(ho1 != ho2, is(true));

        System.out.println(ho1.getAgentFinder());
        System.out.println(ho2.getAgentFinder());
        assertThat(ho1.getAgentFinder() == ho2.getAgentFinder(), is(true));
    }
}
me.xyzlast.gg.hollywood.HollyWoodService@2b9627bc
me.xyzlast.gg.hollywood.HollyWoodService@65e2dbf3
me.xyzlast.gg.hollywood.AgentFinderImpl@7b49cea0
me.xyzlast.gg.hollywood.AgentFinderImpl@7b49cea0

Singleton으로 설정된 객체는 호출될 때, 기존의 객체가 있는 경우에는 그 객체를 계속해서 사용하게 됩니다. 그렇지만, 아무것도 설정되지 않은 객체의 경우에는 마치 new를 통해서 생성되는 객체와 동일한 패턴을 따르게 됩니다.


저작자 표시 비영리 변경 금지
신고
Posted by xyzlast Y2K
TAG Di, guice, IOC, java

querydsl 4.x대의 경우, 다음 링크 글을 참고해주세요.

http://netframework.tistory.com/entry/gradle-%EC%A0%95%EB%A6%AC-queryDSL-code-generate-v-4x


기존 Gradle을 이용한 개발환경구성글에서 다음이 변경되었습니다.

Gradle 2.0의 지원

Gradle 2.0에 호환되도록 Project를 변경했습니다. Gradle 2.0의 가장 큰 변화는 groovy 문법의 변경으로 인한 += operator가 변경된 점입니다. 기존 sourceSet에 추가 path를 할 때, 코드는

sourceDir += file('src/main/generated')

로 구성되었지만, groovy의 변경으로 += operator는 모두 같은 형태의 객체들에서만 사용되도록 변경되었습니다. 그래서 이 부분을 다음과 같이 수정하면 됩니다.

sourceDir += [ file('src/main/generated') ]

Java 8의 지원

java 8이 지원될 수 있도록 souceCompatibility와 targetCompatibility가 모두 변경되었습니다.

build.gradle의 간소화

기존에는 base.gradle, domain.gradle과 같이 여러개의 build.gradle 파일로 나뉜 상태를 간소화시켰습니다.
build.gradle과 각 module에서 필요한 gradle 파일을 각기 나누어 처리하도록 변경하였습니다.


web deploy container의 변경

기존 gradle tomcat plugin에서부터 gretty로 변경했습니다. gretty의 특징은 다음과 같습니다.

  • tomcat/jetty 지원
  • tomcat8, jetty9 지원
  • hot-deploy의 지원
  • jvmargs의 지원

무엇보다도 기존 gradle tomcat plugin보다 사용이 편리한 것이 장점입니다. web server를 실행시키는 명령어는 다음과 같습니다.

gradle :privateWeb:jettyRun   //Jetty9을 이용한 web server 실행
gradle :privateWeb:tomcatRun  //tomcat8을 이용한 web server 실행

git hub에 코드를 올려뒀습니다. 모두들 Happy coding~

https://github.com/xyzlast/gradle_multi_project_test

저작자 표시 비영리 변경 금지
신고
Posted by xyzlast Y2K

Spring을 사용할 때, Proxy mode에서의 @Transactional의 문제점에 대해서 전 글에서 알아봤습니다.

Proxy 모드에서의 가장 큰 문제는 @Transaction이 적용되지 않은 method 내부에서의 @Transaction의 호출입니다. 이 문제를 해결하기 위해서는 Proxy mode에서의 @Transaction이 아닌, AspectJ mode에서 @Transaction을 사용해줘야지 됩니다.

AspectJ mode는 2가지 방법을 제공합니다.

Load-Time Weaver

객체를 Load 할때, AspectJ에 의해서 wearving된 객체를 넘겨주는 방식입니다. 아래와 같은 방식으로 동작하게 됩니다.

  1. application context에 로드된 객체의 loading
  2. aspectj weaver에 의한 객체 weaving (@Transaction annotation이 있는 class, method에 대한 transaction 처리가 된 객체로 변경)
  3. 객체의 이용

위 순서를 보시면 아실 수 있듯이, 이는 객체의 사용에 대해서 약간의 performance의 하락을 가지고 오게 됩니다. application context에서 객체를 load 할 때, aspectj weaver에서 하는 또 다른 일들을 지정하게 됩니다.

Compile-Time Weaver

객체를 Load 할 때, 위와 같은 문제가 있기 때문에, compile 시에 aspectj에서 간섭해서 필요한 객체에 weaving을 시켜서 class를 만들어내는 방식입니다. 이렇게 되면, application context의 load시에 다른 절차가 없기 때문에, performance의 하락도 없는 거의 완벽한 방법으로 구성이 가능합니다.

LTW vs CTW

Load-Time Weaver의 단점은 다음과 같습니다.

  1. application context에 객체가 로드될 때, aspectj weaver와 spring-instrument에 의한 객체 handling이 발생하기 때문에 performance가 저하된다.
  2. web container의 실행시, LTW를 위한 설정이 필요하다.

반면에 Compile-Time Weaver의 단점은 다음과 같습니다.

  1. 개발환경의 구성이 어렵다.
  2. lombok과 같은 compile시에 간섭하는 여러 plugin들과의 매우 다채로운 충돌이 발생한다. 특히 lombok과는 같이 사용하지 못한다고 생각해도 과언이 아니다.

LTW는 운영상의 문제를 발생시킬 수 있고, CTW는 개발상의 문제를 발생시킬 수 있다는 생각이 듭니다. (이런 생각이 들면 무조건 CTW로 가야지 되긴 하는데….;;;)

LTW를 이용한 @Transaction의 처리

먼저 DomainConfiguration에 LTW를 이용한 @Transaction을 다음과 같이 지정해줍니다.

@EnableTransactionManagement(mode = AdviceMode.ASPECTJ, order = 0)

그리고, LTW를 이용하기 위해서 LTW를 활성화시켜야지 됩니다. LTW활성화는 다음과 같습니다.

@EnableLoadTimeWeaving

이것만으로 끝이 아닙니다. LTW는 반드시 다음 jvm option을 가져야지 됩니다. 위에서 서술했듯이, 객체가 로드 될 때, aspectj weaver와 spring-instrument가 각각 객체에 대한 처리를 해줘야지 됩니다. 먼저 aspectj weaver는 실질적으로 일을 하는 객체들이 모여있고, spring-instrument의 경우에는 aspectj weaver에 class loader를 위임하는 일을 맡아서 하게 됩니다. jvm argument에 다음 option을 추가시켜줍니다.

-javaagent:/fullpath/aspectjweaver-1.8.1.jar
-javaagent:/fullpath/spring-instrument-4.0.6.RELEASE.jar

aspectjweaver와 spring-instrument의 뒤에 붙는 버젼은 aspectj와 spring의 버젼과 동일합니다.

intelliJ

intelliJ를 사용하고 있고, JUnit test를 돌리는 경우에는 Run/Debug Configuration에 다음 설정을 추가해줘야지 됩니다.

VM Option에 jvm argument를 default로 넣어주고, 실행시 언제나 사용하도록 구성되어야지 됩니다.

gradle

gradle의 test 시에 역시 다음 jvmargs가 필요하기 때문에 다음과 같은 설정이 필요합니다.

    test {
        jvmArgs '-javaagent:/weavers/spring-instrument-4.0.6.RELEASE.jar ' +
                '-javaagent:/weavers/aspectjweaver-1.8.1.jar'
    }
tomcat

tomcat에서 LTW를 사용하기 위해서는 2가지 방법이 있습니다. 특정 application context에서만 사용할 수 도 있고, 모든 application context에서 사용할 수 있습니다.

특정 application context에서만 LTW를 이용

먼저, spring-instrument.jar파일을 tomcat의 lib 폴더 안에 copy시켜줍니다. 그 후, application context의 context.xml안에 다음 내용을 추가합니다.

<Context path="/ltwdemo">
    <Loader loaderClass="org.springframework.instrument.classloading.tomcat.TomcatInstrumentableClassLoader"/>
</Context>
모든 application context에서 LTW를 이용

tomcat 시작시, jvmargs에 -javaagent:/weavers/spring-instrument-4.0.6.RELEASE.jar를 추가해주면 됩니다.

CTW를 이용한 @Transaction의 처리

CTW를 이용하는 경우, 이는 compile시에 처리하는 것이기 때문에 code상의 변화는 거의 없습니다. @EnableLoadTimeWeaver만을 제거시켜주고, mode를 AspectJ로 설정해주면 됩니다.

@EnableTransactionManagement(mode = AdviceMode.ASPECTJ, order = 0)

우리가 개발을 할때, compile을 하는 도구는 거의 2가지입니다. IDE와 build tool(maven, gradle, ant)입니다.

IntelliJ

setting의 compile option을 다음과 같이 변경합니다.

  • Use compiler : Ajc
  • Path to Ajc compiler : AspectJtools.jar 위치 지정
  • Command line parameters : -1.8 (JavaVersion 설정)
gradle

CTW에 대해서 open source로 plugin이 존재합니다. (https://github.com/eveoh/gradle-aspectj)
사용방법이 조금 까다롭습니다. 주의점은 다음과 같습니다.

  • ext.aspectjVersion property가 apply plugin:aspectj 보다 먼저 선언되어야지 됩니다. (파일 위치상에서 line이 더 위여야지 됩니다.)
  • spring-aspectj component가 아래와 같이 두번 선언되어야지 됩니다.
      aspectpath "org.springframework:spring-aspects:${rootProject.ext.springVersion}"
      compile "org.springframework:spring-aspects:${rootProject.ext.springVersion}"
    

다음은 CTW가 적용된 build.gradle의 전체 내용입니다.

apply plugin: 'java'

sourceCompatibility = 1.8
targetCompatibility = 1.8

version = '1.0'
buildscript {
    repositories {
        maven {
            url "https://maven.eveoh.nl/content/repositories/releases"
        }
    }
    dependencies {
        classpath "nl.eveoh:gradle-aspectj:1.4"
    }
}

repositories {
    mavenCentral()
}

ext {
    javaVersion = "1.8"
    springVersion = "4.0.6.RELEASE"
    springjpaVersion = "1.6.0.RELEASE"
    querydslVersion = "3.3.2"
    hibernateVersion = "4.3.4.Final"
    springsecurityVersion = "3.2.4.RELEASE"
    aspectjVersion = '1.8.1'
}

apply plugin: 'aspectj'
dependencies {
    compile 'org.slf4j:slf4j-api:1.7.6'
    compile 'org.slf4j:jcl-over-slf4j:1.7.6'
    compile 'ch.qos.logback:logback-classic:1.0.13'
    compile 'ch.qos.logback:logback-core:1.0.13'

    compile "org.springframework:spring-context:${rootProject.ext.springVersion}"
    aspectpath "org.springframework:spring-aspects:${rootProject.ext.springVersion}"
    compile "org.springframework:spring-aspects:${rootProject.ext.springVersion}"
    compile "org.springframework.data:spring-data-jpa:$rootProject.ext.springjpaVersion"
    compile group: 'org.apache.httpcomponents', name: 'httpclient', version:'4.2.5'
    compile 'org.apache.commons:commons-lang3:3.3.2'

    compile 'org.aspectj:aspectjrt:1.8.1'
    compile 'org.aspectj:aspectjtools:1.8.1'
    compile 'org.aspectj:aspectjweaver:1.8.1'

    testCompile "junit:junit:4.11"
    testCompile 'org.mockito:mockito-core:1.9.5'
    testCompile 'org.hamcrest:hamcrest-all:1.3'
    testCompile "org.springframework:spring-test:${rootProject.ext.springVersion}"

    compile 'mysql:mysql-connector-java:5.1.31'
}

Summary

LTW와 CTW를 이용한 @Transaction에 대해서 정리해봤습니다. 이상하게 인터넷에서 대부분의 코드가 @EnableTransactionManagement에서 mode를 바꾸면 된다. 식의 글만 있고, 명확히 어떤 일들을 해줘야지 되는지 적혀 있지 않아서 한번 정리해볼 필요성을 느껴서 작성하게 되었습니다. CTW가 모든 면에서 우월성을 가지고 있지만, 저는 lombok없는 개발은 어떻게 할지 잘 모르겠다는 생각까지 들 정도로 중독되어서… 걱정중입니다.; lombok과 CTW를 같이 사용할 방법에 대한 고민이 좀 더 필요할 것 같습니다.

저작자 표시 비영리 변경 금지
신고
Posted by xyzlast Y2K

Spring @Transactional에 대하여.

DB에 대한 Transaction의 경계를 잡는데에 유용하게 쓰이는 Spring의 @Transaction은 Proxy mode와 AspectJ mode로 동작합니다. 일반적으로 많이 사용하는 것은 Proxy mode를 주로 사용하는데, Proxy mode를 사용하게 되는 경우, 다음과 같은 주의점이 필요합니다.

private method의 @Transaction 미적용 문제

이는 어찌보면 당연한 결과입니다. 우리는 Service 객체를 Proxy를 통해서 얻어오고, 얻어온 객체를 접근할 때 Proxy에 의하여 Transaction이 시작되기 때문에 당연히 이런 상황에서는 Transaction이 적용되지 못합니다.
이 문제는 큰 문제가 될 수 없습니다. 다만, 다음 문제는 조금 생각해봐야지 되는 문제가 발생하게 됩니다.

Transaction이 적용되지 않은 method에서 Transaction이 적용된 method를 호출할때.

소위 말하는 self call의 경우 입니다. 이 경우는 어떻게 될까요? 다음 code를 통해서 확인해보도록 하겠습니다.

먼저, 2개의 public method를 준비합니다.

public interface BookService {
    void doNotTransactionAction();
    void doTransactionAction();
}

그리고, 이에 대한 서비스 구현 코드를 다음과 같이 구성합니다.

    @Override
    public void doNotTransactionAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.isNull(TransactionSynchronizationManager.getCurrentTransactionName());
    }

    @Transactional(readOnly = false)
    @Override
    public void doTransactionAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.notNull(TransactionSynchronizationManager.getCurrentTransactionName());
    }

코드 내용은 매우 단순합니다. method가 호출되면 TransactionSynchronizationManager에서 현 Transaction의 이름을 확인하는 코드입니다. 첫번째 doNotTrancationAction에서는 당연히 Transaction이 없어야지 되고, 두번째 doTransactionAction에서는 당연히 Transaction이 있어야지 될 것으로 생각할 수 있습니다. 이제 이 두 코드를 호출하는 테스트 코드를 작성해서 시험해보면 다음과 같은 결과를 보입니다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = DomainConfiguration.class)
public class BookServiceImplTest {
    @Autowired
    private BookService bookService;

    @Before
    public void setUp() {
        assertThat(bookService, is(not(nullValue())));
    }

    @Test
    public void doNotTransactionAction() {
        bookService.doNotTransactionAction();
    }

    @Test
    public void doTransactionAction() {
        bookService.doTransactionAction();
    }
}

매우 당연한 결과이지만, 이 결과를 이제 수정해서 다음 코드로 바꾸어보도록 하겠습니다. Transaction이 적용되지 않은 code에서 Transaction이 적용된 method를 호출하는 경우에 어떻게 진행되는지 보도록 하겠습니다.

    @Override
    public void doNotTransactionAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.isNull(TransactionSynchronizationManager.getCurrentTransactionName());
        doTransactionAction();
    }

결과는 다음과 같이 Assert Error가 나타나게 됩니다.

java.lang.IllegalArgumentException: [Assertion failed] - this argument is required; it must not be null
    at org.springframework.util.Assert.notNull(Assert.java:112)
    at org.springframework.util.Assert.notNull(Assert.java:123)
    at me.xyzlast.bookstore.services.BookServiceImpl.doTransactionAction(BookServiceImpl.java:54)
    at me.xyzlast.bookstore.services.BookServiceImpl.doNotTransactionAction(BookServiceImpl.java:47)
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method)
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:62)
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43)

왜 이런 결과를 나타내게 될까요? 이는 Spring Transaction이 Proxy로 동작하기 때문입니다. Public으로 Service interface에 접근할 때, Transaction이 동작하기 때문에 발생하는 에러입니다. 이 문제는 보다 더 복잡한 문제를 야기할 수 있습니다. 다음과 같은 코드를 확인해보도록 하겠습니다.

    @Transactional(readOnly = false)
    @Override
    public void doTransactionAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.notNull(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.isTrue(!TransactionSynchronizationManager.isCurrentTransactionReadOnly());
    }

    @Transactional(readOnly = true)
    @Override
    public void doTransactionReadonlyAction() {
        System.out.println(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.notNull(TransactionSynchronizationManager.getCurrentTransactionName());
        Assert.isTrue(TransactionSynchronizationManager.isCurrentTransactionReadOnly());
        doTransactionAction();
    }

2개의 Transaction action이 있습니다. 하나는 ReadOnly로 동작하고, 나머지 하나는 Not ReadOnly입니다. 그런데 ReadOnly method에서 Not ReadOnly method를 내부에서 호출하게 되면 어떻게 될까요. 결과는 Not ReadOnly method 내부에서도 Transaction은 ReadOnly로 동작하게 됩니다. 이는 매우 심각한 Transaction문제를 가지고 오게 됩니다.

Summary

Spring의 @Transactional annotation은 매우 훌륭합니다. 다만 이를 PROXY mode에서 사용할 때는 다음과 같은 원칙을 알고 사용해야지 됩니다.

  1. private method에서는 동작하지 않음
  2. transaction이 적용되지 않은 public method 내부에서 transaction이 적용된 public method를 호출하는 경우, transaction이 동작하지 않는다.
  3. readonly transaction이 적용된 public method 내부에서 not-readonly transaction이 적용된 public method를 호출하는 경우, 모든 method는 readonly transaction으로 동작하게 된다.
  4. (3)의 경우는 반대로도 적용된다.


저작자 표시 비영리 변경 금지
신고
Posted by xyzlast Y2K

WebSocket

Spring 4.0에서부터 지원하는 기능중 가장 눈여겨봐야지 될 내용은 WebSocket입니다.

WebSocket은 RFC6455에서 정의한 기능으로 다음과 같은 특징을 가지고 있습니다.

  • Real-time full duplex communication over TCP
  • Uses port 80 / 443
  • URL scheme : ws, wss (SSL)
  • Small overhead for text message (frame base) - 0x00 ~ 0xFF
  • Ping/Pong frames for staying alive

WebSocket을 이용해서 접근하게 되면 다음과 같은 Request와 Server Response가 나타납니다.

  • Request

    GET /echo HTTP/1.1
    Host: localhost:8080
    Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
    Sec-WebSocket-Version: 13
    Origin: http://localhost:9000
    Sec-WebSocket-Key: HfYHmSeeKJWzbQv/K5yBVQ==
    Cookie: _ga=GA1.1.18456469.1406047636
    Connection: keep-alive, Upgrade
    Upgrade: websocket
    
  • Response

    HTTP/1.1 101 Switching Protocols
    Server: Apache-Coyote/1.1
    Upgrade: websocket
    Connection: upgrade
    Sec-WebSocket-Accept: i2iJBvVoYvXsVlrnYki5BNeKjew=
    Date: Wed, 23 Jul 2014 17:56:03 GMT
    

WebSocket의 javascript client code는 다음과 같이 구성될 수 있습니다.

  $scope.ws = new WebSocket('ws://localhost:8080/echo');
  $scope.ws.onopen = function() {
    console.log('websocket opened');
  };
  $scope.ws.onmessage = function(message) {
    console.log(message);
    console.log('receive message : ' + message.data);
  };
  $scope.ws.onclose = function(event) {
    console.log(event);
    console.log('websocket closed');
  };

이제 Spring 4.0에서 지원하는 WebSocket을 구성해보도록 하겠습니다. WebSocket을 지원하는 서버를 개발하기 위해서는 다음과 같은 절차로서 구성되어야지 됩니다.

1) MessageHandler의 구성
2) Spring Configuratio에 ConnectionEndPoint 설정

이제 한 단계식 구성을 따라가보도록 하겠습니다.

MessageHandler의 구성

Client에서 전송되는 message를 처리하는 MessageHandler입니다. MessageHandler는 크게 2가지로 나눌 수 있습니다. TextWebSocketHandler와 BinaryWebSocketHandler가 그것입니다. 이 두가지는 이름으로 알 수 있듯이, Text Message/Binary Message를 각각 처리하는데 사용됩니다.

먼저, Input Text Message에 ECHO:를 붙여주는 매우 단순한 TextMessageHandler를 구성해보도록 하겠습니다. TextMessgeHandler의 경우 매우 중요합니다. json data의 경우에도 역시 TextMessageHandler를 통해서 처리가 되는 것이 일반적이기 때문에 큰 Binary를 처리하거나, 아니면 암호화가 된 특정 이진데이터를 처리하는 것이 아니라면 대부분의 WebSocket 서버에서 사용될 내용은 TextMessageHandler를 기반으로 구성이 가능합니다.

단순한 EchoTextMessageHandler는 다음과 같이 구성될 수 있습니다.

@Component
public class EchoHandler extends TextWebSocketHandler {
    @Override
    protected void handleTextMessage(WebSocketSession session, TextMessage message) throws Exception {
        TextMessage echoMessage = new TextMessage("ECHO :" + message.getPayload());
        session.sendMessage(echoMessage);
    }
}

ConnectionEndPoint의 구성

WebSocket을 이용한 client code를 보시면 쉽게 아실 수 있는 것이, connection의 endpoint가 존재합니다. 이 endpoint를 통해서 데이터를 보내고, 받는 작업을 할 수 있습니다. 이런 endpoint를 지정하는 작업은 다음과 같이 진행할 수 있습니다.

ConnectionEndPoint를 구성하기 위해서 Spring은 @EnableWebSocket annotation과 WebSocketConfigurer interface를 제공하고 있습니다. 여기서 중요한 것은 WebSocketConfigurer interface입니다. interface는 다음과 같이 구성됩니다.

public interface WebSocketConfigurer {
    void registerWebSocketHandlers(WebSocketHandlerRegistry registry);
}

registerWebSocketHandlers 라는 method 하나만이 존재하고, 이 method안에 connection endpoint를 다음과 같이 구성할 수 있습니다.

    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(echoHandler, "/echo");
    }

/echo를 connection endpoint로 만들어주고, 연결된 connection에 대한 Handler를 지정해줍니다. DI를 위해 방금 만든 EchoHandler를 Bean으로 등록하고, 등록된 Bean을 이용하는 전체 @Configuration code는 다음과 같습니다.

@Configuration
@EnableWebSocket
@EnableWebMvc
@ComponentScan(basePackages = {
        "me.xyzlast.controllers",
        "me.xyzlast.handlers"
})
public class ControllerConfiguration extends WebMvcConfigurerAdapter implements WebSocketConfigurer {
    @Autowired
    private EchoHandler echoHandler;

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(echoHandler, "/echo");
    }
}

위 설정은 WebSocket 서버와 WebMVC를 모두 다 지원하기 위해서 구성한 결과입니다.

이제 WebSocket을 이용한 Echo Server 구성이 모두 마쳐졌습니다. tomcat 7.x 이상이 필요하며, angularJS를 이용한 client page는 다음과 같이 구성할 수 있습니다.

angular.module('websocketClientApp').controller('WsCtrl', function ($scope, $timeout) {
//STARTOFCONTROLLER
$scope.message = '';
$scope.ws;
$scope.echoMessages = [];

$scope.init = function() {
  $scope.ws = new WebSocket('ws://localhost:8080/echo');
  $scope.ws.onopen = function() {
    console.log('websocket opened');
  };
  $scope.ws.onmessage = function(message) {
    console.log(message);
    console.log('receive message : ' + message.data);
    $scope.echoMessages.unshift(message.data);
    $timeout(function() {
      $scope.$apply('echoMessages');
    })
  };
  $scope.ws.onclose = function(event) {
    console.log(event);
    console.log('websocket closed');
  };
};

$scope.send = function() {
  $scope.ws.send($scope.message);
};

$scope.init();
//ENDOFCONTROLLER
});
<h3>WebSocket Test Page</h3>

<input type="text" ng-model="message"/>
<button ng-click="send()">SEND</button>

<ul ng-repeat="echo in echoMessages">
  <li>{{echo}}</li>
</ul>

결과는 다음과 같습니다.

SocketJS

WebSocket은 지금까지 사용되고 있던 Ajax에 비하여 매우 훌륭한 방법입니다. 그렇지만, WebSocket이 Spring 4.0에서나 지원이 된 이유는 약간 발전이 너무나도 느렸습니다. WebSocket을 지원하는 Browser의 종류는 다음과 같습니다.

WebSocket의 경우, IE 10이상에서 지원하고 있기 때문에, 현실상 지원하기 힘듭니다. 이 문제를 해결하기 위해서, socket.io와 같은 websocket을 지원하지 않는 browser에서 websocket과 같이 접근하기 위한 방법들이 계속해서 나왔는데, SockJS도 그 방법들중 하나입니다.

SockJS는 기본적으로 WebSocket을 이용하고 있고, interface가 동일합니다. 다만 WebSockeet과는 다음과 같은 차이점을 가지고 있습니다.

  1. schema가 ws가 아닌 http
  2. WebSocket과 같이 Browser에서 제공되는 library가 아닌, 외부 library 사용
  3. IE 6이상부터 지원합니다.
  4. Server 측에서도 websocket이 아닌 SockJS server side library를 사용

websocket과 sockjs의 client code는 다음과 같습니다. 비교해보시면 schema만을 제외하면 완전히 동일한 것을 알 수 있습니다.

WebSocket

var ws = new WebSocket('ws://domain/endpoint');
ws.onopen = function() {
  console.log('open socket');
}
ws.onmessage = function(message) {
  console.log('message', message.data);
}
ws.onclose = function(e) {
  console.log('close');
}

SockJS

var sock = new SockJS('http://domain/endpoint');
sock.onopen = function() {
  console.log('open socket');
}
sock.onmessage = function(message) {
  console.log('message', message.data);
}
sock.onclose = function(e) {
  console.log('close');
}

Spring 4.0은 SockJS 서버를 지원하고 있으며, 지원 방법은 WebSocket 서버와 거의 유사합니다. 아니 동일한 수준입니다.
SockJS 서버의 구축 방법은 WebSocket 서버의 구축 방법과 동일합니다.

1) MessageHandler의 구성
2) Spring Configuratio에 ConnectionEndPoint 설정

MessageHandler의 구성

MessageHandler는 WebSocket과 완전히 동일합니다.

Connection EndPoint의 구성

Connection EndPoint의 경우에도 거의 유사합니다. 다음은 WebSocket의 endpoint와 SockJS의 endpoint 비교입니다.

    @Override
    public void registerWebSocketHandlers(WebSocketHandlerRegistry registry) {
        registry.addHandler(echoHandler, "/echo");
        registry.addHandler(echoHandler, "/echojs").withSockJS();
    }

주의할점은 SockJS와 WebSocket의 endpoint를 동일하게 넣어서는 안된다는 점입니다. 같은 port를 이용하고, schema만이 다르기 때문에 접근되는 endpoint가 동일한 경우, 오동작을 발생시키면서 connection이 종료됩니다.

SockJS client의 구성

SockJS의 경우, bower에서 지원하고 있습니다. 다음 command를 통해서 bower로 install을 할 수 있습니다.

bower install sockjs --save

angularjs + yeoman을 통해서 구성한 sockjs client code는 다음과 같습니다.

angular.module('websocketClientApp').controller('SockCtrl', function ($scope) {
//STARTOFCONTROLLER
$scope.message = '';
$scope.sock;

$scope.init = function() {
  $scope.sock = new SockJS('http://localhost:8080/echojs');
  $scope.sock.onopen = function() {
    console.log('websocket opened');
  };
  $scope.sock.onmessage = function(message) {
    console.log(message);
    console.log('receive message : ' + message.data);
  };
  $scope.sock.onclose = function(event) {
    console.log(event);
    console.log('websocket closed');
  };
};

$scope.send = function() {
  $scope.sock.send($scope.message);
};

$scope.init();
//ENDOFCONTROLLER
});
<script src="bower_components/sockjs/sockjs.js"></script>

<h3>SockJS Test Page</h3>

<input type="text" ng-model="message"/>
<button ng-click="send()">SEND</button>

STOMP

STOMP는 text 지향의 message protocol입니다. websocket을 이용한 message handling을 보다 더 쉽게 만들어줍니다. WebSocket이나 SockJS의 경우 onmessage function에서 받는 메세지를 모두 handling해야지 되는 단점을 가지고 있습니다. 이러한 단점을 구독(subscription)과 사용자 구독(user)를 통해서 처리할 수 있다는 장점을 가지고 있습니다.

STOMP를 이해하기 위해서는 Client code의 흐름을 보는 것이 좀 더 이해가 편합니다.

$scope.client;
$scope.name = '';
$scope.message = '';

$scope.init = function() {
  var socket = new SockJS('http://localhost:8080/endpoint'); //SockJS endpoint를 이용
  $scope.client = Stomp.over(socket); //Stomp client 구성
  $scope.client.connect({}, function(frame) {
    console.log('connected stomp over sockjs');
    // subscribe message
    $scope.client.subscribe('/subscribe/echo', function(message) {
      console.log('receive subscribe');
      console.log(message);
    });
  });
};

// send message
$scope.send = function() {
  var data = {
    name: $scope.name,
    message: $scope.message
  };
  $scope.client.send('/app/echo', {}, JSON.stringify(data));
};

STOMP는 기본적으로 subscribe를 통해서 데이터를 전달받습니다. 따라서, 이 message가 어떠한 method에 의해서 전송을 받았는지를 좀 더 명확히 할 수 있습니다. 기존의 WebSocket이나 SockJS를 이용하게 되는 경우에는 전달받는 message의 범위가 매우 광범위하기 때문에, 전달되는 메세지에 message key값을 넣어서 send/receive에 대한 처리를 따로 해줘야지 되는 것이 일반적입니다. 그렇지만, STOMP의 경우에는 접근하는 방식이 send point와 subscribe point가 각기 다르기 때문에 처리를 좀 더 자유롭게 해줄 수 있습니다.

STOMP에서는 subscribe와 user/** 두개로 subscribe의 target을 나눌수 있는데, 각각의 차이는 전자는 전역이고, 후자는 호출된 사용자에게만 던져진다는 차이를 가지고 있습니다.

이제 Spring을 이용한 STOMP 서버 구축에 들어가보도록 하겠습니다.

Dependency 설정

STOMP를 지원하기 위해서 Spring은 spring-message에 대한 dependency를 필요로 합니다.

compile "org.springframework:spring-messaging:${rootProject.ext.springVersion}"

@Configuration 설정

STOMP를 지원하기 위해서는 @EnableWebSocketMessageBroker과 @Configuration에서는 AbstractWebSocketMessageBrokerConfigurer를 상속받거나, WebSocketMessageBrokerConfigurer 인터페이스를 구현해야지 됩니다.

@Configuration에서 할 일은 2가지입니다.

1) EndPoint의 설정
2) (option) STOMP URL에 대한 전역 prefix 설정
3) (option) send에 대한 subscribe url의 prefix 설정
4) @EnableWebSocketMessageBroker 설정

EndPoint 설정

EndPoint의 경우, SockJS를 사용할지, WebSocket을 사용할지에 따라 다른 설정을 해주게 됩니다. STOMP를 이용하는 것은 대부분 SockJS를 이용해서 Browser의 적합성을 높이는 경우가 많기 때문에, SockJS를 이용하는 것이 일반적입니다. WebSocketMessageBrokerConfigurer 인터페이스의 registerStompEndpoints method를 다음과 같이 정의합니다.

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/endpoint").withSockJS();
    }
prefix 설정

Send Url에 대한 전역 prefix와 subscribe에 대한 전역 prefix를 설정할 수 있습니다. 이는 WebSocketMessageBrokerConfigurer 인터페이스의 configureMessageBroker method 에서 수정하면 됩니다. (아래는 모든 send prefix를 app으로 지정하고, subscribe의 기본 prefix를 subscribe로 지정했습니다.)

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/subscribe");
        registry.setApplicationDestinationPrefixes("/app");
    }
@EnableWebSocketMessageBroker 설정

모든 설정이 완료된 @Configuration code는 다음과 같습니다.

@Configuration
@EnableWebSocketMessageBroker
@ComponentScan(basePackages = {
        "me.xyzlast.controllers"
})
public class StompConfigurer extends AbstractWebSocketMessageBrokerConfigurer {

    @Override
    public void registerStompEndpoints(StompEndpointRegistry registry) {
        registry.addEndpoint("/endpoint").withSockJS();
    }

    @Override
    public void configureMessageBroker(MessageBrokerRegistry registry) {
        registry.enableSimpleBroker("/subscribe");
        registry.setApplicationDestinationPrefixes("/app");
    }
}

@Controller 구성

STOMP의 @Controller는 Spring MVC의 @Controller와 같은 @annotation을 사용하고, STOMP를 제공하기 위한 다음 @annotation들을 제공합니다.

@annotationdescription
@MessageMappingSTOMP client의 send에 대한 target url입니다.
@SendToSTOMP client의 subscribe에 대한 target url을 지정합니다. (지정되지 않은 경우, @MessageMapping에 지정된 URL + @Configuration에서 설정된 prefix URL을 이용합니다. )
@SubscribeEventclient에서 subscribe 할 수 있는 url을 지정합니다. 특정 message가 발생하거나 event가 발생했을 때, Client에 값을 전송하는데 사용합니다.

annotation이 적용된 @Controller code는 다음과 같이 구성될 수 있습니다.

@Controller
public class EchoController {
    @MessageMapping("/echo")
    @SendTo("/subscribe/echo")
    public Hello sendEcho(Hello hello) throws Exception {
        System.out.println("receive message : " + hello.toString());
        Hello echoHello = new Hello();
        echoHello.setMessage(hello.getName());
        echoHello.setName(hello.getMessage());
        return echoHello;
    }
}

위 @Controller의 sendEcho methos는 /echo url을 통해 데이터를 전달받고, /subscribe/echo를 통해 데이터를 전달하게 됩니다.

기존의 SockJS와는 다르게 MessageHandler를 따로 구현하지 않습니다. 기본적으로 @Controller를 이용해서 처리하기 때문에 기존의 WebMvc와 거의 유사한 형태의 개발을 진행할 수 있습니다.

User Destination

STOMP에서는 특정 사용자들에게 메세지를 보낼 수 있습니다. 기본값으로 설정되어 있는 /user로 시작되는 subscribe의 경우에는 각 사용자들이 받을 수 있는 url을 제공합니다. 이는 사용자들의 특화된 message를 보내거나 error handling시에 주로 사용됩니다.

client.subscribe('/user/queue/private-message') = function(messaage) {

}

이러한 User subscribe는 Spring에서 @SendToUser annotation으로 기능이 따로 제공되고 있습니다.

    @MessageMapping("/message")
    @SendToUser
    public String sendMessage(String message) {
        return message.toUpperCase();
    }

    @MessageExceptionHandler
    @SendToUser("/queue/errors")
    public String handlingException(IllegalStateException ex) {
        return ex.getMessage();
    }


저작자 표시 비영리 변경 금지
신고
Posted by xyzlast Y2K


티스토리 툴바