Java2014. 11. 17. 00:51
반응형

 자바(Java)로 프로그램을 작성하다 보면 Base64인코딩/디코딩 코드를 작성할 필요가 있습니다. 이 경우 자바는 다른 언어와는 다르게 표준API상에 관련 API가 없는 것을 알게 됩니다. 그러나 JVM내부적으로 필요한 경우도 있어서 각 JDK별로 별도의 구현체가 존재하는 경우도 있습니다. 예를 들어 썬마이크로시스템(Sun/Oracle)의 JDK의 경우에는  패키지 sun.misc에 BASE64Encoder/BASE64Decoder 구현체가 존재합니다. 이러한 JDK별로 존재하는 구현체를 사용하는 것은 지양해야 하므로 일반적으로 Apache재단의 commons-codec라이브러리를 이용하게 됩니다. 이번 포스팅에서는 해당 라이브러리의 사용법에 대해서 간단하게 알아보도록 하겠습니다.


 먼저 관련해서 필요한 라이브러리를 아래와 같이 MAVEN이용해서 추가하거나 다운로드해서 빌드패스에 추가하도록 합니다.


commons-codec

<dependency>
    <groupId>commons-codec</groupId>
    <artifactId>commons-codec</artifactId>
    <version>1.9</version>
</dependency>


commons-io

<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.4</version>
</dependency>



※사실 인코딩 자체는 commons-codec라이브러리만 있으면 수행 할 수 있습니다. 구현상의 편의를 위해 유틸리티 라이브러리로 commons-io을 추가하였습니다.







파일을 Base64 문자열로 변환

public static String fileToBase64String(final File target)
throws IOException {

	BufferedInputStream bis = null;
	try {
		bis = new BufferedInputStream(new FileInputStream(target));
		return Base64.encodeBase64String(IOUtils.toByteArray(bis));
	} finally {
		try {
			bis.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}
}


Base64 문자열을 파일로 변환

public static void base64StringToFile(final String encoded, final File output)
throws IOException {

	BufferedOutputStream bos = null;
	try {
		output.createNewFile();
		bos = new BufferedOutputStream(new FileOutputStream(output));
		bos.write(Base64.decodeBase64(encoded));
	} finally {
		try {
			bos.close();
		} catch (IOException e) {
			e.printStackTrace();
		}
	}



  위의 소스코드는 특정 파일을 Base64인코딩하여 문자열로 바꾸고 이를 다시 디코딩하여 파일로 변환하는 코드입니다. 위처럼 단순히 문자열로 바꾸는 것 이외에 URL Safe인코딩(URL이나 파일명등을 인코딩할 경우)타입이나 메일 관련한 MIME인코딩(76문자마다 개행)의 경우 아래의 메소드를 이용해서 인코딩/디코딩을 할 수 있다.



Base64#encodeBase64(final byte[] binaryData, final boolean isChunked, final boolean urlSafe)



 위의 메소드에 isChunked의 인자로 true를 지정할 경우 MIME인코딩, urlSafe의 인자로 true를 지정할 경우 URL Safe타입의 인코딩을 수행할 수 있습니다. 필요에 따라서 지정해서 호출하도록 합시다.



 Base64구현 관련하여 commons-codec라이브러리에 대해서 소개했습니다만 사실 자바8(Java8)부터 드디어(이제서야) 표준API에 Base64관련 API가 제공되기 시작했습니다. 어쩌면 위의 코드는 어느 사이에 사용되지 않을지도 모르겠습니다.


'Java' 카테고리의 다른 글

apache httpclient 요청 디버깅  (0) 2014.11.27
Java8 Base64 Encoder/Decoder  (0) 2014.11.19
apache httpclient applcation/json, multipart/mixed 요청  (2) 2014.10.26
Eclipse java Access restriction rt.jar  (0) 2013.12.04
Java7 zip 파일 압축  (0) 2013.11.26
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