Java/Spring2015. 2. 9. 01:09
반응형

 Spring Boot를 이용하여 웹 어플리케이션을 작성할 경우 일반적인 웹 어플리케이션과 같이 이미 구동된 WAS(Web Application Server)에 반영(Deploy)하는 절차를 통해서 구동되는 것과는 달리 어플리케이션 자체에 WAS를 내장하고 구동하는 형태를 띠고 있기 때문에 수정된 내용을 반영하기 위해서는 가상머신(JVM)을 재기동할 필요가 있었습니다. 이런 경우 Spring에서 제공하고 있는 Spring Loaded를 이용하면 가상머신을 재기동 하지 않고 개발을 진행할 수 있어 개발 생산성을 높일 수 있을 듯 합니다. Spring 레퍼런스 문서상에는 이러한 기능을 Hot Swapping이라는 타이틀로 기술하고 있습니다. 물론 Spring Loaded가 웹 어플리케이션만을 위한 기술은 아닙니다. 데몬과 같이 상주형 어플리케이션을 개발할 경우에도 유용하게 사용할 수 있습니다.


※Hot Swap이란 Java 1.4에서 추가된 Java Platform Debugger Architecture (JPDA)의 기능의 하나입니다.



 먼저 git저장소(https://github.com/spring-projects/spring-loaded)에서 라이브러리를 다운로드 받아서 적당한 위치에 저장합니다.



커맨드 라인 실행

java -javaagent:<path>/springloaded-{버전}.jar -noverify JavaEntryClass

※JavaEntryClass는 main메소드가 있는 진입점 클래스를 의미합니다.


 위의 실행 명령어를 입력하여 Spring Boot 어플리케이션을 실행하면 소스코드 수정후 저장하면 즉시 소스 코드가 반영됩니다. 정확한 내용은 소스코드를 분석해 보지 않아서 모르겠지만 기반이 되는 기술은 위에서 언급한 JPDA인듯하고 실제 반영되는 부분은 Grails2를 이용해서 수행하고 있는듯 합니다. 사실 JPDA에서 제공하는 Hot Swap은 클래스 구조에 영향을 끼치지 않는 범위(메소드 바디 수정)에서만 동작하는데 Spring Loaded의 경우는 메소드/필드/생성자 추가 삭제가 가능합니다. 동작원리는 본 포스팅의 범위를 벗어나므로 따로 분석하지 않겠습니다.(사실은 능력이...) 관심있는 분들은 소스코드를 분석해 보는것도 좋은 공부가 되지 않을지 싶습니다.

 사실 일반적인 프로젝트에서 처럼 WAS에서 어플리케이션 단위로 디플로이 되는 것이 아닌 런타임에 Java 소스 코드 반영이 프로덕트 환경에서 재기동 없는 Hot Swapping이 정확한 동작을 보증할지 의문이 생기기도 하고 위의 커맨드 라인 실행에서 추가한 -noverify 옵션은 바이트 코드의 검증을 생략한다는 무시무시한 설명만 있어서 먼가 꺼림직 하기도 합니다.



IDE환경에서의 실행

 사실 일반적으로 개발을 진행할 경우 커맨드 라인 환경에서 어플리케이션을 실행하는 경우는 드물다고 생각되므로 IDE(통합 개발 환경)환경하에서 실행하는 방법에 대해서 자세히 알아보도록 하겠습니다.


※본 포스팅에서는 일반적으로 많이 사용하는Eclipse(이클립스)를 기준으로 설명하도록 하겠습니다.



Run > Run Configuration 메뉴에서 가상머신 인자로 위와 같이 지정해서 실행하면 위에서 소개한 커맨드라인 실행과 동일하게 작동가능합니다. Main탭에서 엔트리 클래스를 별도로 지정하므로 인자에 엔트리 클래스 명을 기술할 필요가 없다는 점이 다릅니다.



 위에서 소개한 Run Configuration상에 지정하는 방법은 각 개발자가 개별로 설정을 진행해야 하기때문에 프로젝트를 진행하는 관점에서 생각하면 아직 불편합니다. 전체 개발자의 개발환경을 동일하게 구성하기 위해서는 역시 빌드 툴을 통해서 작동할 수 있도록 해주어야 합니다. Maven이용해서 Spring Loaded를 적용하는 방법을 살펴보도록 합시다.


Maven 이용하여 실행하기


	<build>
		<plugins>
			<plugin>
				<groupid>org.springframework.boot</groupid>
				<artifactid>spring-boot-maven-plugin</artifactid>
				<dependencies>
					<dependency>
						<groupid>org.springframework</groupid>
						<artifactid>springloaded</artifactid>
						<version>1.2.1.RELEASE</version>
					</dependencies>
			</plugin>
		</plugins>
	</build>



 위처럼 pom.xml에 플러그인 설정을 추가하고 Maven Build를 이용하여 실행하면 적용됩니다. Maven Build의 Goals에 spring-boot:run 을 지정하여 빌드를 실행하면 Spring Loaded가 적용된 상태로 어플리케이션이 기동됩니다. Gradle의 경우에는 Intellij의 플러그인을 통해서 Spring Loaded가 적재된 상태로 가능한 듯 합니다.



 다음으로 알아볼 내용은 정적 리소스(엄밀한 의미에서 정적 리소스가 아닌 템플릿 엔진의 템플릿 파일을 의미함)의 재로딩 방법입니다. Spring Boot에서는 다양한 템플릿 엔진을 기본적으로 지원하고 있는데 Spring Boot의 기본 설정 파일인 'application.properties'에 아래와 같은 간단한 설정만으로 재로딩이 가능합니다.


 Thymeleaf

spring.thymeleaf.cache=false



 FreeMarker

spring.freemarker.cache=false



 Groovy

spring.groovy.template.cache=false



 Velocity

spring.velocity.cache=false



 본 포스팅에서 소개한 Spring Loaded를 이용하여 Spring Boot를 이용한 개발에 생산성을 더해보도록 합시다. Spring Boot자체가 개발의 편의성을 위해서 나왔는데 이를 좀 더 편리하게 하는 무엇인가가 필요하다는게 좀 아이러니 하기도 합니다.


Posted by Reiphiel
Java/Spring2014. 11. 5. 23:24
반응형

 스프링(Spring Framework)의 중첩 트랜잭션(정확히는 중첩 트랜잭션 전파레벨)에서 예외 로그 출력에 관한 포스팅을 하면서 이왕이면 스프링이 제공하는 트랜잭션 관리 기능에 대해서 구체적으로 알아보는 것이 좋겠다고 생각이 들었습니다.


 일단 트랜잭션이란 무엇인지에 대해서 간단하게 언급하고 지나가도록 하겠습니다. 잘알고 계시듯 일반적으로 트랜잭션이란 더이상 쪼갤수 없는 어떤 논리적인 작업단위라고 할 수 있습니다. DBMS(데이터베이스 관리 시스템)에서는 의미를 명확히 하기 위해서 작업단위 자체를 나타내는 용어를 데이터베이스 트랜잭션(Database Transaction)이라고 하고 흔히 트랜잭션이라고 말하면 떠올리는 데이터베이스 트랜잭션에 관련된 조작을 트랜잭션 처리(Transaction Processing)라고 합니다. 이러한 트랜잭션(특별히 언급하지 않는한 트랜잭션은 트랜잭션 처리를 의미함)을 사용하는 주된 목적은 데이터의 완전무결성(integrity)을 보장하기 위해서 입니다.



 스프링의 트랜잭션 관리 기능에 대해서 알아보기 전에 순수하게 JDBC 인터페이스만을 이용하여 트랜잭션을 처리하는 단순한 소스코드를 살펴보도록 하자.



소스코드1

public void methodNameA() {
    ...
    conn = ds.getConnection();
    conn.setAutoCommit(false);

    stmt = conn.createStatement();

    stmt.executeUpdate("INSERT INTO TABLE_NAME (COLUMN_NAME) VALUES ('VALUE')");

    conn.commit();
    ...
}

public void methodNameB() {
    ...
    conn = ds.getConnection();
    conn.setAutoCommit(false);

    stmt = conn.createStatement();

    stmt.executeUpdate("UPDATE TABLE_NAME SET COLUMN_NAME = 'NEW_VALUE'");

    conn.commit();
    ...
}


 JDBC를 이용한 단순한 트랜잭션 처리는 위와같이 구현될 수 있는데 위와 같이 순수한 JDBC를 이용한 소스코드의 문제점은 무엇일지 아래의 코드를 보면서 생각해보자.



소스코드2

public void methodNameC() {

    methodNameA();
    methodNameB();

}



  소스코드1처럼 단순히 메소드 단위로 호출이 될 경우에는 트랜잭션이 경계가 확실히 구분되면서 잘 작동하겠지만 소스코드2처럼 별도의 트랜잭션 처리를 하는 메소드 2개를 제3의 메소드에서 호출한다고 할 경우 methodNameA의 처리가 완료된 시점에 commit이 실행되어 하나의 트랜잭션이 완결되므로 A처리후 B처리중에 실패한다고 하더라도 A의 트랜잭션을 롤백할 수 없게 되어 버립니다. 결과적으로 위의 소스코드는 재사용성이 극히 떨어지는 상태가 되며 동일한 처리를 반복해서 작성해야 합니다. 또한 위와 같은 명시적인 커밋, 롤백 등등의 처리를 단순히 Copy & Paste 로 입력하다보니 빠지는 경우도 발생하게 됩니다.



 위와 같은 트랜잭션 처리상의 어려움 해결하기 위한 방법은 크게 2가지가 있을듯 합니다.

  1. 트랜잭션의 상태를 각 모듈에 직접 전파한다.
  2. 트랜잭션의 상태를 공용 저장공간을 이용하여 간접적으로 전파한다.



 언뜻 이해하기 힘들 수 있겠지만 간단하게 말해서 첫번째는 트랜잭션의 상태를 호출하는 메소드에 인자로 직접적으로 전달하는 것을 말하며 두번째는 메소드 내부에서 공용공간을 참조하여 간접적으로 상태를 확인하는 방법입니다.(일반적으로 ThreadLocal이 이용된다.)



 첫번째에 해당하는 해결책은 사실 메소드 시그니쳐에 영향을 끼치기 때문에 특정 벤더의 솔루션 내부적으로 템플릿화 해서 구현하는 경우가 일반적이며 범용적인 트랜잭션 관리 구현체는 두번째 방법을 이용해서 구현되어 있습니다. 스프링에서도 마찬가지로 트랜잭션을 포괄적으로 관리할 수 있는 기능을 제공합니다. 스프링에서 제공하는 트랜잭션 관련 기능을 간단하게 살펴보면 아래와 같습니다.



  • 트랜잭션 관리를 위한 추상화 기능

 추상화된 트랜잭션 관리 기능을 제공하기 때문이 이종의(JTA, JDBC, Hibernate, JPA, JDO) 데이터베이스 접근API간에도 일관적인 프로그래밍이 가능합니다.

  • 선언적(declarative) 트랜잭션 관리

어토테이션 혹은 AOP를 통해서 프로그램 코드가 아니라 설정 혹은 선언만으로 트랜잭션 관리가 가능합니다.

  • 프로그램(직접적인 코드)에 의한 트랜잭션 관리

심플한 프로그램 코드에 의한 직접적인 트랜잭션 관리기능도 제공합니다.




 그러면 먼저 스프링의 트랜잭션 추상화 인터페이스인 org.springframework.transaction.PlatformTransactionManager의 소스코드를 살펴보자.


public interface PlatformTransactionManager {

    TransactionStatus getTransaction(TransactionDefinition definition)
        throws TransactionException;

    void commit(TransactionStatus status) throws TransactionException;

    void rollback(TransactionStatus status) throws TransactionException;
}


 코드상의 getTransaction메소드의 경우 트랜잭션의 상태를 반환하고 있다. 위에서 트랜잭션 관리를 위해서 트랜잭션의 상태를 전파할 필요가 있다고 언급했었는데 소스코드상에서 트랜잭션을 취득하면 트랜잭션 상태를 반환받으므로 이것을 전파하는 것으로 트랜잭션 관리할 수 있을 것으로 파악할 수 있다.



public interface TransactionStatus extends SavepointManager {

    boolean isNewTransaction();

    boolean hasSavepoint();

    void setRollbackOnly();

    boolean isRollbackOnly();

    void flush();

    boolean isCompleted();

}


 트랜잭션 매니저에서 트랜잭션 취득시에 반환받았던 트랜잭션 상태 클래스의 구조를 살펴보면 새 트랜잭션인가 세이브포인트를 가지고 있는가 롤백을 할것인가 등등의 정보를 저장하고 있다. 이를 이용해서 최종적으로 트랜잭션 종료 시점에 커밋, 롤백 등등의 처리를 진행 할 것임을 예측 할 수 있습니다.



설정예


  


 위의 설정예는 일반적인 프로젝트에서 가장 많이 사용하는 데이터 소스 기반의 트랜잭션 매니져 설정이다. 위에서 소개한 트랜잭션관련 인터페이스의 구현체를 스프링 빈으로 등록하여 사용하게 됩니다. 개념을 알고 보니 설정에 대해서 이해하기 한결 수월해진듯 합니다.




 스프링 트랜잭션 관리의 개념에 대해서 간단하게 알아보았습니다. 사실 스프링 트랜잭션 관리의 개념 자체는 스프링만의 독창것이라고 보기는 어려울지도 모릅니다. 하지만 예전부터 각자 구현해 왔던 트랜잭션 관리를 추상화 하여 다양한 플랫폼 아래서도 일관된 소스코드를 적용할 수 있게 해주는 점이 매력적이라고 할 수 있습니다.



참조 : http://docs.spring.io/spring/docs/3.2.12.RELEASE/spring-framework-reference/html/transaction.html


Posted by Reiphiel
Java/Spring2014. 11. 5. 01:47
반응형

 스프링 프레임워크를 이용하여 서비스 레이어 개발을 진행하던중 아래와 같은 로그를 발견하고 문제점을 찾아보기 시작했다. 디버그모드에서 출력되는 메시지인 관계로 무시해도 될 듯 했지만 스택 트레이스가 출력되고 있는 관계로 문제를 명확히 하기로 하고 찾아보기 시작했다. 일단 단순히 출력된 메시지만 확인한 결과 명시적으로 JDBC 세이브포인트를 릴리스 할수 없다는 내용이었습니다.


DEBUG o.s.j.d.JdbcTransactionObjectSupport - Could not explicitly release JDBC savepoint
java.sql.SQLException: 지원되지 않는 기능입니다
    at oracle.jdbc.driver.PhysicalConnection.releaseSavepoint(PhysicalConnection.java:6356) ~[ojdbc6-11.2.0.4.jar:11.2.0.3.0]
    at sun.reflect.NativeMethodAccessorImpl.invoke0(Native Method) ~[na:1.7.0_51]
    at sun.reflect.NativeMethodAccessorImpl.invoke(NativeMethodAccessorImpl.java:57) ~[na:1.7.0_51]
    at sun.reflect.DelegatingMethodAccessorImpl.invoke(DelegatingMethodAccessorImpl.java:43) ~[na:1.7.0_51]
    at java.lang.reflect.Method.invoke(Method.java:606) ~[na:1.7.0_51]
    at org.apache.ibatis.datasource.pooled.PooledConnection.invoke(PooledConnection.java:246) ~[mybatis-3.2.7.jar:3.2.7]
    at com.sun.proxy.$Proxy92.releaseSavepoint(Unknown Source) ~[na:na]
    at org.springframework.jdbc.datasource.JdbcTransactionObjectSupport.releaseSavepoint(JdbcTransactionObjectSupport.java:142) ~[spring-jdbc-3.2.10.RELEASE.jar:3.2.10.RELEASE]
    at org.springframework.transaction.support.AbstractTransactionStatus.releaseHeldSavepoint(AbstractTransactionStatus.java:160) [spring-tx-3.2.10.RELEASE.jar:3.2.10.RELEASE]
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.processCommit(AbstractPlatformTransactionManager.java:749) [spring-tx-3.2.10.RELEASE.jar:3.2.10.RELEASE]
    at org.springframework.transaction.support.AbstractPlatformTransactionManager.commit(AbstractPlatformTransactionManager.java:724) [spring-tx-3.2.10.RELEASE.jar:3.2.10.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.commitTransactionAfterReturning(TransactionAspectSupport.java:475) [spring-tx-3.2.10.RELEASE.jar:3.2.10.RELEASE]
    at org.springframework.transaction.interceptor.TransactionAspectSupport.invokeWithinTransaction(TransactionAspectSupport.java:270) [spring-tx-3.2.10.RELEASE.jar:3.2.10.RELEASE]
    at org.springframework.transaction.interceptor.TransactionInterceptor.invoke(TransactionInterceptor.java:94) [spring-tx-3.2.10.RELEASE.jar:3.2.10.RELEASE]
    at org.springframework.aop.framework.ReflectiveMethodInvocation.proceed(ReflectiveMethodInvocation.java:172) [spring-aop-3.2.10.RELEASE.jar:3.2.10.RELEASE]
    at org.springframework.aop.framework.CglibAopProxy$DynamicAdvisedInterceptor.intercept(CglibAopProxy.java:633) [spring-aop-3.2.10.RELEASE.jar:3.2.10.RELEASE]
    ...
    ...



 중첩 트랜잭션이 내부적으로 세이브포인트를 사용하므로 중첩 트랜잭션 사용이 원인일 것으로 생각되었다. 로그의 좀더 살펴본 결과 중간 부분에 releaseSavepoint라는 메소드가 호출되고 있는 부분도 확인 되었다. 스택 트레이스를 따라서 호출한 메소드를 확인해 본 결과 역시나 해당 메소드의 트랜잭션 전파규칙은 중첩(NESTED)으로 선언되어 있었다.
※사실 이전의 세이브포인트 관련 포스팅이 중첩 트랜잭션을 사용하기전에 상세한 내용을 알아보기위해 작성한 글이다.


@Transactional(propagation=Propagation.NESTED)
public void methodName() {
    ......
}



 일단 releaseSavepoint 메소드 호출시 해당 예외가 발생하는 것을 확인였으므로 사용하는 JDBC인 오라클 관련해서 검색을 좀 해본결과 오라클 도큐멘트에서 아래와 같은 문구가 있었습니다.


public void releaseSavepoint(Savepoint savepoint) throws SQLException;

Not supported at this release. Always throws SQLException.


※출처 : http://docs.oracle.com/cd/B10501_01/java.920/a96654/jdbc30ov.htm







 오라클 JDBC3.0 구현체에서는 명시적으로 세이브포인트 해제 메소드 호출은 지원하지 않고있음에도 불구하고 스프링 프레임워크 내부적으로 아래와 같이 커밋 시점에 명시적으로 호출하기 때문에 예외가 발생하고 있음을 알게 되었습니다.


 org.springframework.transaction.support.AbstractPlatformTransactionManager 내부 커밋 구현

    private void processCommit(DefaultTransactionStatus status) throws TransactionException {
        try {
            boolean beforeCompletionInvoked = false;
            try {
                ...중략...
                if (status.hasSavepoint()) {
                    if (status.isDebug()) {
                        logger.debug("Releasing transaction savepoint");
                    }
                    status.releaseHeldSavepoint();
                }
                else if (status.isNewTransaction()) {
                    if (status.isDebug()) {
                        logger.debug("Initiating transaction commit");
                    }
                    doCommit(status);
                }
                 ...후략...



 실제 예외의 로그를 출력하는 부분의 소스코드도 살펴보았다. 아래처럼 단순히 예외를 캐치하고 로그를 출력하고만 있었다. 스프링 자체의 구현으로는 전후의 소스에 영향을 줄만한 부분은 없다는 것이 확인되었다.


 org.springframework.jdbc.datasource.JdbcTransactionObjectSupport 내부 릴리스 세이브포인트 구현

public void releaseSavepoint(Object savepoint) throws TransactionException {
        try {
            getConnectionHolderForSavepoint().getConnection().releaseSavepoint((Savepoint) savepoint);
        }
        catch (Throwable ex) {
            logger.debug("Could not explicitly release JDBC savepoint", ex);
        }
    }


 사실 로그만 출력되는 현상이었고 단위테스트상에서는 커밋 롤백모두 잘 실행되었으므로 문제가 있다고는 할 수 없었지만 일단 무엇인가 찜찜한 기분은 어쩔수 없어서 내부 구현을 확인해 보았다. 위와 같이 큰 문제가 없는 부분도 있지만 방치해 두면 알수없는 애매한 버그를 발생시키는 경우도 있으므로 명확하게 테스트를 진행하고 소스코드를 확인해 두는 것이 좋겠습니다. 프레임워크를 이용하으로 막연히 잘되겠지고 안이하게 생각한다면 뜻하지 않은 곳에서 어려움에 봉착할지도 모르므로 주의하도록 합시다.


Posted by Reiphiel