Android 에서 Restlet Net extension HTTP client connector 사용 시 timeout 설정

 결론부터 말하자면 라이브러리 버그랄까…

안드로이드에서는 net extension의 HTTP client connector를 사용하면
connection timeout 과 read timeout을 설정해도 적용이 안된다.

Resetlet 문서를 보면 Internal Connector의 HTTP connector는
development only 를 추천한다고 당당히 써 있기도 한데다가
서버 다운으로 접속이 안될 때 자꾸 문제를 있으켜서 Net extention의 HTTP connector를 쓰기로 했다.

안드로이드에서는 자동으로 쓸수 있게 하는게 안된다는거 같고.. 다음과 같이 해줘야 한다.

final Engine engine = Engine.getInstance();
engine.getRegisteredClients().clear();
engine.getRegisteredClients().add(new HttpClientHelper(null));

그리고, read timeout 과 connection timeout을 설정하고, ClientResource에서
해당 client를 사용하도록 설정해 준다.

final Context context = new Context();

final Series<Parameter> parameters = context.getParameters();
parameters.add(“readTimeout”, Integer.toString(READ_TIMEOUT));

final Client client = new Client(context, Protocol.HTTP);
client.setConnectTimeout(CONNECTION_TIMEOUT);

final ClientResource cr = new ClientResource(requestUrl);
cr.setNext(client);

그런데, timeout이 전혀 안먹는다=_=
설정하는 방법이 잘못됐나 싶어 온갖 삽질을 동원하며 이리저리 테스트 해도 답이 나오질 않는다..
그래서, 결국 Restlet 소스를 뒤지기 시작했다.
(라이브러리 소스나 뒤지는 신세라니… OTL)

마침내 찾아낸 부분은… org/restlet/ext/net/internal/HttpUrlConnectionCall.java 파일의

            // These properties can only be used with Java 1.5 and upper
            // releases
            int majorVersionNumber = SystemUtils.getJavaMajorVersion();
            int minorVersionNumber = SystemUtils.getJavaMinorVersion();
            if ((majorVersionNumber > 1)
                    || ((majorVersionNumber == 1) && (minorVersionNumber >= 5))) {
                this.connection.setConnectTimeout(getHelper()
                        .getConnectTimeout());
                this.connection.setReadTimeout(getHelper().getReadTimeout());
            }

JDK 버전을 체크해서 1.5 이상 일때만 timeout을 설정하도록 하는 부분이었다.
먼가 의심스러워서, 안드로이드에서 버전값을 찍어 봤다.
… 둘다 0 이 나온다..=_=
이건 내가 잘못한건지 쟤들이 안드로이드용이라면서 안드로이드 고려를 못한건지…
도저히 판단이 안서는 상황이지만, 구국의 결단을 내려 라이브러리 소스를 직접 컴파일해 쓰기로 결정;;

            this.connection.setConnectTimeout(getHelper()
                    .getConnectTimeout());
            this.connection.setReadTimeout(getHelper().getReadTimeout());

요렇게 고쳐서 해결을 봐버렸다=_=

Spring, DbUnit 을 이용한 rollback test

Spring으로 개발 중에 DbUnit을 이용해서 DAO 코드를 테스트 하고 있다.
(Spring 3.1.0, DbUnit 2.4.8 사용 중이다)

책을 보다보니 rollback 테스트라는게 있어서 적용해 봤는데 잘 안되서 삽질 좀 했다=_=

==

rollback 테스트라는 개념은 테스트를 시작하기 전에 트랜잭션을 시작해 주고,
테스트 종료 시 rollback 시켜 버리는 것이다.
마지막에 rollback을 해버리니 테스트 중에 무슨 짓을 해버려도 상관없다. 심지어 데이터를 다 날려도..
따로 테스트 DB를 사용하지 않아도 되고, 같은 DB로 여러명이 동시에 테스트를 돌려도 서로
영향을 받지 않으니 꽤 유용할 거 같아서 적용을 해보기로 했다.

트랜잭션은 @Transactional 어노테이션을 사용해서 걸어준다.
우선 컨텍스트 설정 파일에 다음을 추가해 준다.

<tx:annotation-driven />

그리고, Test 클래스에 @Transactional 을 붙여 준다.

@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations={“/z/y/x/test-context.xml”})
@Transactional
public class AppDaoTest {

그냥 요렇게만 붙여 주면, 마지막에 그냥 rollback이 된다고 한다.

트랙잭션 시작 -> @Before 매소드 실행 -> @Test 매소드 실행 -> @After 매소드 실행 -> rollback

요런 순서로 진행이 되므로, @Before가 붙은 매소드에서 데이터 초기화 등을 해주면 된다.
AOP로 트랜잭션을 쓸때는 괜찮았는데, 어노테이션으로 하니가 CGLIB가 필요해서 maven에 추가해줬다.

<dependency>
<groupId>cglib</groupId>
<artifactId>cglib</artifactId>
<version>2.2.2</version>
</dependency>

다음으로, DbUnit 에서 Spring 에서 트랜잭션을 시작한 것과 동일한 커넥션을 사용하도록 해주는 것이 핵심인데,
여기서 문제가 발생해서 고생을 좀 했다.=_=
원래 IDatabaseTester 를 사용해서 테스트 중인데, 참고한 책이나 구글신께 물어 봐도 이거 말고 다른걸 사용한
예제가 많이 나온다. 난 그걸로 하니까 먼가 문제가 자꾸 생겨서 이걸로 테스트 중이다.
어쨌든, 중요한건 동일한 커넥션을 쓰면 되니까..

 …

@Inject
private DataSource dataSource;

private IDatabaseTester databaseTester;

@Before

public void setUp() {

final Connection conn = DataSourceUtils.getConnection(dataSource);
final DatabaseConnection dbConn = new DatabaseConnection(conn);

databaseTester = new DefaultDatabaseTester(dbConn);

databaseTester.setDataSet(getDataSet());
databaseTester.onSetup();
 }

 …

dataSource 를 inject 받은 다음에 connection을 외부에서 받아서 쓰는 DefaultDatabaseTester 객체를 만들었다.
DefaultDatabaseTester 는 커넥션을 만드는 방법은 모르고, 단지 외부에서 받은 커넥션을 사용할 뿐이다.

자.. 이제 테스트를 돌리면… 전부 실패=_=
이런..도대체 머가 문제지? 메시지를 보면 죄다 커넥션이 이미 close 됐다는 에러다.
헉.. 도저히 알 수 없는 상황;;;;

이런 저런 삽질하다가, 메시지를 자세히 살펴보니… setUp()은 정상적으로 진행되고, 그 다음에 에러가 나는거 같다.
그럼, setUp() 후에 커넥션이 닫혔다는 말인데… 도저히 이해가 안되는 상황…
DefaultDatabaseTester  를 쓰면 안되는가 싶어서 다른 것도 써보고, 이래저래 찾아 봤지만.. 미궁 속이다.

결국 log4jdbc 디버그 메시지를 찍고 DbUnit 소스까지 뒤져서 알아낸 사실은…

DefaultDatabaseTester.onSetup() 호출 마지막에 커넥션을 닫아 버린다는것=_=;;;

DefaultDatabaseTester는 AbstractDatabaseTester를 상속하고, AbstractDatabaseTester에서는
IOperationListener 를 사용해서 커넥션을 가져온 후, onSetUp() 호출 후, onTearDown() 호출 후 작업을 처리한다.
여기서 디폴트로 쓰는 리스너가 DefaultOperationListener 인데, 얘는, onSetUp(), onTearDown() 후 무조건
커넥션을 종료하도록 되어 있는 것이다;;

외부에서 커넥션을 받아쓰는 DefaultDatabaseTester 에서 이 동작을 오버라이딩 안 했다는건
좀 이해하기 어려운데.. 버그인지 먼지 모르겠지만.. 어쨌든 안되면 바꿔야지=_=
다행히(?) IOperationListener는 변경이 가능하다.

그래서, 아무것도 안하는 DummyOperationListener를 정의해 주고,

private class DummyOperationListener implements IOperationListener {
@Override
public void connectionRetrieved(IDatabaseConnection connection) {
// do nothing
}

@Override
public void operationSetUpFinished(IDatabaseConnection connection) {
// do nothing
}

@Override
public void operationTearDownFinished(IDatabaseConnection connection) {
// do nothing
}
}

IDatabaseTester 객체 생성 후, operation listener로 설정해 줬다.

 …

@Inject
private DataSource dataSource;
private IDatabaseTester databaseTester;


@Before
public void setUp() {

final Connection conn = DataSourceUtils.getConnection(dataSource);
final DatabaseConnection dbConn = new DatabaseConnection(conn);

databaseTester = new DefaultDatabaseTester(dbConn); databaseTester.setOperationListener(new DummyOperationListener());

databaseTester.setDataSet(getDataSet());
databaseTester.onSetup();
 }

 …

이제 테스트를 돌려보면… 짜잔~ 성공.

Jenkins + Sonar 사용 시 Unit test 실패

Spring MVC 어플리케이션을 위해 Jenkins를 이용해 CI 서버를 구축하고
Jenkins의 sonar 플러그인을 사용해 Sonar와 연결해 사용 중인데, sonar에서 실행하는
Unit test가 알수 없는 이유로 실패를 한다.
정확히는 sonar 에서 unit test를 위해 돌리는 cobertura 라는 놈이 문제인 듯… 한데…

java.lang.IllegalStateException: Failed to load ApplicationContext
Expecting a stackmap frame at branch target 47 in method z.y.x.controller.TestController.list(Lorg/springframework/ui/Model;)Ljava/lang/String; at offset 17
등의 에러가 발생한다. Jenkins에서 자체적으로 돌리는 Unit test는 문제가 없는걸로 봐서
Jenkins나 JUnit의 문제는 아닌 듯하고..
어째든 구글신께 열심히 물어본 결과, maven plugin 중에 surefire 라는 놈이 관련이 있는 듯하다.
maven의 pom.xml에 다음을 추가해 주고 빌드를 돌리니 문제가 없다.
<plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-surefire-plugin</artifactId> <version>2.12</version> <configuration> <argLine>-XX:-UseSplitVerifier</argLine> </configuration> </plugin>

Spring + Mybatis + tomcat 사용 중 “심각: Error listenerStar” 오류

아아 삽질의 연속 ㅠ.ㅜ

JDK 7, Spring 3.1, Mybatis, 3.1, tomcat 7, Ubuntu 12.04 사용 중이다.
로컬에서 Eclipse+tomcat 으로 개발 하던 어플리케이션을 서버에 배치해서 테스트 하려고 작업을 했다.
서버 설치도 다 됐고, 서버에 war 파일 올렸는데, 어플리케이션 시작이 안된다=_=
tomcat 로그에 찍히는 에러 메시지는 다음과 같이 달랑 한줄;;

2012. 6. 4 오후 11:06:18 org.apache.catalina.startup.HostConfig deployWAR
정보: Deploying web application archive /var/lib/tomcat7/webapps/app.war
2012. 6. 4 오후 11:06:20 org.apache.catalina.core.StandardContext startInternal
심각: Error listenerStart
2012. 6. 4 오후 11:06:20 org.apache.catalina.core.StandardContext startInternal
심각: Context [/app] startup failed due to previous errors
2012. 6. 4 오후 11:06:20 org.apache.catalina.loader.WebappClassLoader clearReferencesJdbc

도저히 이유를 알 수 없는 난감한 상황인데.. 구글신께 물어보니 spring 설정이 잘 못된 경우 저 에러가
날 수 있단다. 로컬에서는 잘됐는데 설정이 이상할리가.. 라고 생각했지만…
결국 설정을 하나씩 지워가며 테스트 했는데, mybatis 쪽 설정 의심되는 상황.
정확한 오류를 알 수 없어 전전긍긍하면서, 로그도 제대로 안 찍어 주는 tomcat를 원망하다 문득 깨달았다.
spring mvc template으로 프로젝트를 생성하면 log4j 설정이 console 로 로그를 출력하도록 한다는걸=_=
그래서 즉시 파일로 출력하도록 설정을 수정했다.

<?xml version=”1.0″ encoding=”UTF-8″?>

<!DOCTYPE log4j:configuration PUBLIC “-//APACHE//DTD LOG4J 1.2//EN” “log4j.dtd”>
<log4j:configuration xmlns:log4j=”http://jakarta.apache.org/log4j/”>

        <!– Appenders –>
        <appender name=”fileLog” class=”org.apache.log4j.DailyRollingFileAppender”>
                <param name=”File” value=”/var/lib/tomcat7/webapps/app/log/app.log” />
                <param name=”Append” value=”true” />
                <layout class=”org.apache.log4j.PatternLayout”>
                        <param name=”ConversionPattern” value=”%-5p: %c – %m%n” />
                </layout>
        </appender>

        <!– Application Loggers –>
        <logger name=”z.y.x”>
                <level value=”debug” />
        </logger>

        <!– 3rdparty Loggers –>
        <logger name=”org.springframework.core”>
                <level value=”debug” />
        </logger>

        <logger name=”org.springframework.beans”>
                <level value=”debug” />
        </logger>

        <logger name=”org.springframework.context”>
                <level value=”debug” />
        </logger>

        <logger name=”org.springframework.web”>
                <level value=”debug” />
        </logger>

        <!– Root Logger –>
        <root>
                <priority value=”debug” />
                <appender-ref ref=”fileLog” />
        </root>

</log4j:configuration>

엉엉.. 로그가 정말 잘 찍힌다ㅜ.ㅜ

ERROR: org.springframework.web.context.ContextLoader – Context initialization failed
org.springframework.beans.factory.CannotLoadBeanClassException: Error loading class [z.y.x.service.UserServiceImpl] for bean with name ‘userServiceImpl’ defined in file [/var/lib/tomcat7/webapps/hadpan_web/WEB-INF/classes/z/y/x/service/UserServiceImpl.class]: problem with class file or dependent class; nested exception is java.lang.UnsupportedClassVersionError: z/y/x/service/UserServiceImpl : Unsupported major.minor version 51.0 (unable to load class z.y.x.service.UserServiceImpl)
        at org.springframework.beans.factory.support.AbstractBeanFactory.resolveBeanClass(AbstractBeanFactory.java:1265)

음.. 예상과 달리 직접적으로 mybatis 관련된쪽은 아닌데… Unspported major.minor version 어쩌고 하는 메시지가 보인다.
또 구글신께 물어 봤지. 자바 버전 차이에 의해 발생할 수 있는 에러란다.
아.. 어플리케이션을 자바7으로 빌드했는데, tomcat7은 JRE6 에서 돌아가고 있었던 거다.

사용 중인 Ubuntu 서버의 tomcat7 실행 스크립트를 보니 다음과 같은 부분이 있다.

  • /etc/init.d/tomcat7

JDK_DIRS=”/usr/lib/jvm/default-java ${OPENJDKS} /usr/lib/jvm/java-6-openjdk /usr/lib/jvm/java-6-sun”

# Look for the right JVM to use
for jdir in $JDK_DIRS; do
    if [ -r “$jdir/bin/java” -a -z “${JAVA_HOME}” ]; then
        JAVA_HOME=”$jdir”
    fi
done
export JAVA_HOME

/usr/lib/jvm/default-java 라는 디렉토리가 있으면 젤 먼저 거기를 JAVA_HOME으로 지정하게 되어있다.
해당 파일을 확인해 보니 역시나 버전이 6이다.

$ ls -l default-java
lrwxrwxrwx 1 root root 23  6월  4 23:10 default-java -> java-1.6.0-openjdk-i386

버전 7를 사용하도록 심볼릭 링크를 바꿔줬다.

$ rm default-java
$ ln -s java-1.7.0-openjdk-i386 default-java

그리고 tomcat 재실행.

$ service tomcat7 restart

멀쩡하게 실행이 되는 구나 ㅜ.ㅠ

Spring 에서 Transaction 설정 시 Service 레벨에서 적용 안되는 문제

요즘 뒤늦게 spring을 쓴다고 삽질에 여념이 없다=_=
어제는 Transaction 설정을 추가했는데, 이럴수가 적용이 안되는 것이다;;;
STS에서 transaction 표시도 잘 되고, 설정은 몇 번이나 검토해도 틀린게 없는데 말이다.

maven을 위해 pom.xml에 다음과 같이 추가해 주고

<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-tx</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.5.4</version>
</dependency>

다음과 같이 spring 에 data source, transaction manager 빈을 추가하고,
service 클래스에서 transaction 이 사용하도록 설정을 했다.

<!– data source –>
<bean id=”dataSource” class=”org.apache.commons.dbcp.BasicDataSource”>
<property name=”driverClassName” value=”net.sf.log4jdbc.DriverSpy” />
<property name=”url” value=”jdbc:log4jdbc:postgresql://192.168.0.12:5432/db” />
<property name=”username” value=”user” />
<property name=”password” value=”passwd” />
<property name=”initialSize” value=”3″ />
<property name=”maxActive” value=”10″ />
<property name=”maxWait” value=”3000″ />
</bean>

<!– transaction manager –>
<bean id=”transactionManager”
class=”org.springframework.jdbc.datasource.DataSourceTransactionManager”>
<property name=”dataSource” ref=”dataSource” />
</bean>

<!–  transaction setting –>
<tx:advice id=”txAdvice” transaction-manager=”transactionManager”>
<tx:attributes>
<tx:method name=”get*” read-only=”true” />
<tx:method name=”list*” read-only=”true” />
<tx:method name=”find*” read-only=”true” />
<tx:method name=”*” />
</tx:attributes>
</tx:advice>

<aop:config>
<aop:advisor advice-ref=”txAdvice”
                                     
pointcut=”execution(* com.acroem.hadpan.service.*Service.*(..))” />
</aop:config>

그리고, MyBatis와 연동해서 쓰는지라 다음과 같이 MyBatis 설정도 추가했다.

<bean id=”sqlSessionFactory” class=”org.mybatis.spring.SqlSessionFactoryBean”>
<property name=”dataSource” ref=”dataSource” />
<property name=”typeAliasesPackage” value=”z.y.x.bean” />
</bean>

<bean id=”sqlSession” class=”org.mybatis.spring.SqlSessionTemplate”>
<constructor-arg ref=”sqlSessionFactory” />
</bean>

<bean class=”org.mybatis.spring.mapper.MapperScannerConfigurer”>
<property name=”basePackage” value=”z.y.x.dao” />
</bean>

그러나 당최가 transaction이 동작을 하지 않는다=_= service 쪽의 매소드에 일부러 성공하는 SQL과 실패하는 SQL을
연속으로 실행 했는데, rollback 되지 않고 첫 번째 결과가 무조건 commit 이 되는 것이다;;
이래저래 설정을 바꿔보고, 이것 저것 실험을 해봐서 얻은 결론은 DAO 레벨에서는 transaction이 작동하는데,
service 레벨에서는 안한다는것.
삽질 끝에 service 클래스에서 @Service 어노테이션을 제거하고, 설정 파일에 bean 설정으로 추가 했더니..
동작한다! 먼가 해결의 실마리가 보이는 듯하지만 도대체가 이유를 알 수 없는 상황…-_-

다시 이래저래 설정을 바꿔가면서 실험해보고, 관련 글들을 찾아서 읽어 봤는데…
결론은 component scan의 과용과 spring context 설정에 대한 무지로 인한 설정 오류… 랄까..-_-a

STS에서 Spring MVC Template 프로젝트를 사용하여 프로젝트를 생성했는데, 이렇게 하면
기본적으로 계층 구조를 갖는 두 개의 context가 생성된다.
root context
 |- servlet context

그리고, root context의 설정 파일은 /WEB-INF/spring/root-context.xml 에,
servlet context 의 설정파일은 /WEB-INF/spring/appServlet/servlet-context.xml 에 저장된다.

참고로, root context의 빈은 servlet context 에서 참조 가능하나, 그 반대는 안되고,
root context와 servlet context에 동일한 빈이 있으면, root context의 빈은 무시될 수 있다고 한다.

여기서 servlet context 설정 파일에 <context:component-scan basePackage=”z.y.x” /> 를 넣은 것이 화근.
위의 data source, transaction, mybatis 관련 설정은 모두 root context의 설정에 추가 되어 있으며,

root context 설정 파일에도
<context:component-scan basePackage=”z.y.x” /> 가 추가되어있다.

여기서 문제는 AOP 설정은 다른 context에는 영향을 미칠 수 없다는 것과
root context와 servlet context에 동일한 빈이 있으면, root context의 빈은 무시될 수 있다는 것.

servlet context에서  <context:component-scan basePackage=”z.y.x” /> 을 해버렸기 때문에,

@Component, @Control, @Service, @Repository 가 붙은 모든 빈은 root context에서 무시될 가능성이 높다.
거기다 transaction 경계를 설정 하는 AOP 설정이 root context에 있으니…
root context 설정에서 component scan 했다고 하더라도 모조리 무시되니 root context 내에서는 AOP 설정이 안되고,
AOP 설정이 servlet context에는 적용되지 않으니, 결국 어디에도 transaction 설정이 되지 않는 것이지=_=

DAO 같은 경우에는 root context에 MyBatis 설정이 있으니, component scan에는 걸리지 않고
root context의 빈으로 등록되어 transaction이 잘 적용됐던 것이지. 덴장 OTL
망할놈의 STS는 context 간의 관계를 알리가 없으니 설정에 있으면 transaction 표시를 해줬던 것이고;

머.. 암튼 root context 설정에서는 요렇게,

<context:component-scan basePackage=”z.y.x.service” />

servlet context 설정에서는 요렇게,

<context:component-scan basePackage=”z.y.x.controller” />

해주는 것으로 모든 문제 해결!