출처 : http://stackoverflow.com/questions/5152686/self-injection-with-spring


@Service
public class UserService implements Service {

    @Autowired
    private ApplicationContext applicationContext;

    private Service self;

    @PostConstruct
    private void init() {
        self = applicationContext.getBean(UserService.class);
    }
}


출처 : http://slipp.net/questions/123

오늘 애플리케이션 개발할 때 retry는 어떤 식으로 구현하고 있나? (http://www.slipp.net/questions/122)에에) 대한 질문을 올렸다. 이 질문을 올린 후에 곰곰히 생각해 봤다. 지금까지 애플리케이션을 구현하면서 retry를 구현하는 경우가 많지 않았는데 유독 slipp.net 소스에 retry 구현이 많지 않은가? 분명 어딘가 문제가 있다는 생각이 들었다.

그 래서 retry를 이렇게 많이 사용하게된 원인을 파악해 봤다. 그랬더니 페이스북으로 글을 전송할 때 가끔씩 정상적으로 동작하지 않아 문제를 해결하려다보니 retry를 무분별하게 사용하게 되었다는 것이 떠올랐다. 그렇다면 왜 페이스북으로 글을 전송할 때 문제가 발생했을까? 원인을 찾아보니 페이스북 전송에 문제가 있었던 것이 아니라 Async로 처리하는 부분에서 데이터베이스에 저장된 데이터가 정상적으로 조회되지 않는다는 것이 가장 이슈였다. 원인은 다음 상황에서 발생하고 있었다.

@Transactional
public class QnaService {
    public Question createQuestion(SocialUser loginUser, QuestionDto questionDto) {
        [...]


        if (questionDto.isConnected()) {
             facebookService.sendToQuestionMessage(loginUser, savedQuestion.getQuestionId());
        }
        return savedQuestion;
    }
}

위 소스 코드와 같이 글을 쓰는 시점에 페이스북으로 전송 상태이면 facebookService.sendToQuestionMessage()를 호출해 페이스북으로 글을 전송한다. facebookService.sendToQuestionMessage() 내부를 살펴보면 다음과 같다.

@Service
@Transactional
public class FacebookService {
    @Async
    public void sendToQuestionMessage(SocialUser loginUser, Long questionId) {
        Question question = questionRepository.findOne(questionId);
        if (question == null) {
            question = retryFindQuestion(questionId);
        }


        [...]
    }


}

위 소스 코드를 보면 알 수 있듯이 sendToQuestionMessage()는 Async로 동작하도록 구현하고 있다. 이 때 문제가 발생하는 부분은 questionRepository.findOne(questionId)를 호출할 때 Question 데이터가 null이 되는 경우가 가끔 발생한다는 것이 문제의 원인이었다. 이 문제에 대한 해결책으로 retry를 선택한 것이다. 하지만 위와 같이 retry를 하더라도 문제는 완전히 해결되지 않았다. 왜 이와 같은 현상이 발생하는지 살펴보자.

먼저 위와 같이 구현할 경우의 데이터 처리 상태를 파악해 보자.

QnaService.createQuestion()에서 Transaction이 시작된다.

데이터베이스에 Question 데이터를 저장한다. 하지만 아직까지 commit이 완료되지 않았기 때문에 데이터베이스에 완전히 저장된 것은 아니다.

facebookService.sendToQuestionMessage() 메소드를 호출해 페이스북에 메시지를 전송한다.

QnaService.createQuestion() 메소드가 종료되면서 commit을 한다.

위 과정에서 문제의 원인이 된 부분은 4번 과정이 완료되지 않은 상태에서 3번 과정이 먼저 실행되면서 4번에서 commit 후에 저장해야 될 Question 데이터를 조회하기 때문에 조회할 수 없는 상황이 발생한 것이다. 3번과 4번의 실행 순서가 어떻게 되느냐에 따라 기능이 정상 동작하거나 동작하지 않는 상황이 발생한 것이다.

이에 대한 문제를 인지하고 자료를 찾아보니 몇 일 전에 올라온 따끈따끈한 자료가 있어 참고해서 문제를 해결할 수 있었다.

http://architects.dzone.com/articles/synchronizing-transactions : 이틀 전에 올라온 글이다. ㅋㅋ

이 글을 참고해서 다음과 같이 코드를 수정했다.

public class QnaService {
    public Question createQuestion(final SocialUser loginUser, QuestionDto questionDto){
        Set<Tag> tags = tagService.processTags(questionDto.getPlainTags());


        Question newQuestion = new Question(loginUser, questionDto.getTitle(), questionDto.getContents(), tags);
        final Question savedQuestion = questionRepository.saveAndFlush(newQuestion);


        if (questionDto.isConnected()) {
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronizationAdapter() {
                public void afterCommit() {
                    facebookService.sendToQuestionMessage(loginUser, savedQuestion.getQuestionId());
                }
            });
        }
        return savedQuestion;
    }
}

위 소스 코드와 같이 TransactionSynchronizationManager.registerSynchronization()를 활용해 구현해 봤다. 아직까지 모든 문제가 완료된 것인지는 확인하지 못했다. 당분간 운영해 보면 문제가 정상적으로 해결되었는지 확인해 볼 수 있을 듯하다. 이 이슈를 경험하면서 Async 기능을 구현할 때는 고려해야할 부분이 많다는 것을 다시 한번 느꼈다. 한 가지 기능에 대해 여러 개의 Thread가 동작하는 경우 Transaction이 어떻게 동작하는지 정확하게 이해하고 사용할 필요가 있을 것으로 생각한다.


출처 : http://blog.outsider.ne.kr/870
참고 : http://whiteship.me/?tag=%ED%8A%B8%EB%9E%9C%EC%9E%AD%EC%85%98&paged=2

11.5.4 다른 빈에 다른 트랜잭션의 의미를 설정하기
다수의 서비스계층 객체가 있고 각각에 완전히 다른 트랜잭션 설정을 적용하고자 하는 시나리오를 생각해 보자. 이러한 경우 다른 pointcut과 advice-ref 속성값을 가진 별개의 <aop:advisor/> 요소를 정의할 수 있다.

약 간의 차이점있지만 우선 모든 서비스 계층의 클래스는 x.y.service 패키지 루트에 정의되어 있다고 가정한다. 모든 빈을 이 패키지(혹은 그 하위 팩키지)에 정의된 클래스의 인스턴스로 만들고 Service로 이름이 끝나는 모든 빈이 기본 트랜잭션 설정을 가지도록 하려면 다음과 같이 작성해야 한다.

<?xml version="1.0" encoding="UTF-8"?>
  xsi:schemaLocation="
 
  <aop:config>
 
    <aop:pointcut id="serviceOperation"
          expression="execution(* x.y.service..*Service.*(..))"/>
 
    <aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"/>
 
  </aop:config>
 
  <!-- 이 두 빈은 트랜잭션이 적용될 것이다... -->
  <bean id="fooService" class="x.y.service.DefaultFooService"/>
  <bean id="barService" class="x.y.service.extras.SimpleBarService"/>
 
  <!-- ... 그리고 이 두 빈은 트랜잭션이 적용되지 않는다 -->
  <bean id="anotherService" class="org.xyz.SomeService"/> <!-- (적합한 패키지에 있지 않다) -->
  <bean id="barManager" class="x.y.service.SimpleBarManager"/> <!-- (이름이 'Service'로 끝나지 않는다) -->
 
  <tx:advice id="txAdvice">
    <tx:attributes>
      <tx:method name="get*" read-only="true"/>
      <tx:method name="*"/>
    </tx:attributes>
  </tx:advice>
 
  <!-- PlatformTransactionManager와 같은 다른 트랜잭션 인프라스트럭처 빈은 생략했다... -->
 
</beans>

다음 예제는 환전히 다른 트랜잭션 설정으로 별도의 두 빈을 설정하는 방법을 보여준다.

<?xml version="1.0" encoding="UTF-8"?>
  xsi:schemaLocation="
 
  <aop:config>
 
    <aop:pointcut id="defaultServiceOperation"
          expression="execution(* x.y.service.*Service.*(..))"/>
 
    <aop:pointcut id="noTxServiceOperation"
          expression="execution(* x.y.service.ddl.DefaultDdlManager.*(..))"/>
 
    <aop:advisor pointcut-ref="defaultServiceOperation" advice-ref="defaultTxAdvice"/>
 
    <aop:advisor pointcut-ref="noTxServiceOperation" advice-ref="noTxAdvice"/>
 
  </aop:config>
 
  <!-- 이 빈은 트랜잭션이 적용된다 ('defaultServiceOperation' 포인트컷을 참고해라) -->
  <bean id="fooService" class="x.y.service.DefaultFooService"/>
 
  <!-- 이 빈도 트랜잭션이 적용되지만 완전히 다른 트랜잭션 설정을 가진다 -->
  <bean id="anotherFooService" class="x.y.service.ddl.DefaultDdlManager"/>
 
  <tx:advice id="defaultTxAdvice">
    <tx:attributes>
      <tx:method name="get*" read-only="true"/>
      <tx:method name="*"/>
    </tx:attributes>
  </tx:advice>
 
  <tx:advice id="noTxAdvice">
    <tx:attributes>
      <tx:method name="*" propagation="NEVER"/>
    </tx:attributes>
  </tx:advice>
 
  <!-- PlatformTransactionManager와 같은 다른 트랜잭션 인프라스트럭처 빈은 생략했다... -->
</beans>

11.5.5 <tx:advice/> 설정
이번 색션에서는 <tx:advice/> 태그를 사용해서 지정할 수 있는 여러가지 트랜잭션 설정을 간략히 설명한다. 기본 <tx:advice/> 설정은 다음과 같다.

  • 전파(Propagation) 설정은 REQUIRED다.
  • 격리 수준(Isolation level)은 DEFAULT이다.
  • 트랜잭션은 읽기/쓰기이다.
  • 트랜잭션 타입아웃 기본값은 의존하는 트랜잭션 시스템의 기본 타입아웃이거나 타임아웃을 지원하지 않는다면 존재하지 않는다.
  • 모든 RuntimeException은 롤백을 발생시키고 모든 체크드 Exception은 롤백을 발생시키지 않는다.
이 기본 설정을 변경할 수 있다. <tx:advice/>와 <tx:attributes/>내에 중첩된 <tx:method/> 태그의 여러가지 속성은 아래에 정리되어 있다.

Table 11.1. <tx:method/> 설정

속성 필수여부? 기본값 설명
name Yes   연결된 트랜잭션 속성의 메서드 이름. 와일드카드 (*) 문자를 다수의 메서드를 가진 같은 트랜잭션 속성 설정과 연결하는데 사용할 수 있다. 예를 들어, get*, handle*, on*Event 등등 이다.
propagation No REQUIRED 트랜잭션 전파 동작.
isolation No DEFAULT 트랜잭션 격리 수준.
timeout No -1 트랜잭션 타임아웃 값 (초단위).
read-only No false 해당 트랜잭션이 읽기 전용인가?
rollback-for No   롤백을 일으키는 Exception(s). 콤마로 구분한다. 예를 들면 com.foo.MyBusinessException,ServletException.
no-rollback-for No   롤백을 일으키지 않는 Exception(s). 콤마로 구분한다. 예를 들어 com.foo.MyBusinessException,ServletException.
















11.5.6 @Transactional 사용하기
트 랜잭션 설정에 대한 XML에 기반한 선언적인 접근에 추가적으로 어노테이션 기반의 접근을 사용할 수 있다. 자바 소스코드에 직접 트랜잭션을 선언하는 것은 영향받는 코드에 훨씬 가깝게 선언을 둘 수 있다. 어쨌든 트랜잭션을 사용하는 코드는 거의 항상 이러한 방법으로 배포되기 때문에 과도한 커플링으로 인한 큰 위험은 없다.

@Transactional 어노테이션의 쉬운 사용방법은 예제로 설명하는 것이 가장 쉽고 예제후에 좀더 자세히 설명한다. 다음의 클래스 정의를 보자.

// 트랜잭션을 적용하고자 하는 서비스 클래스
@Transactional
public class DefaultFooService implements FooService {
 
  Foo getFoo(String fooName);
 
  Foo getFoo(String fooName, String barName);
 
  void insertFoo(Foo foo);
 
  void updateFoo(Foo foo);
}

위의 POJO가 스프링 IoC 컨테이너의 빈처럼 정의되었을 때 딱 한 줄의 XML 설정을 추가해서 빈 인스턴스에 트랜잭션을 적용할 수 있다.

<!-- 'context.xml' 파일에서 -->
<?xml version="1.0" encoding="UTF-8"?>
     xsi:schemaLocation="
   
  <!-- 트랜잭션을 적용하고자 하는 서비스 객체다 -->
  <bean id="fooService" class="x.y.service.DefaultFooService"/>
 
  <!-- 어노테이션에 기반한 트랜잭션 동작의 설정을 활성화한다. -->
  <tx:annotation-driven transaction-manager="txManager"/>
 
  <!-- a PlatformTransactionManager는 여전히 필요하다 -->
  <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
  <!-- (이 의존성은 어딘가에 정의되어 있다) -->
  <property name="dataSource" ref="dataSource"/>
  </bean>
   
  <!-- 다른 <bean/> 정의는 여기에 한다 -->
</beans> 


Tip
연 결하려는 PlatformTransactionManager의 빈 이름이 transactionManager인 경우에는 <tx:annotation-driven/> 태그에서 transaction-manager 속성을 생략할 수 있다. 의존성 주입하려는 PlatformTransactionManager 빈이 다른 이름을 가지고 있다면 앞의 예제처럼 transaction-manager 속성을 명시적으로 사용해야 한다.

인터페이스 정의, 인터페이스의 메서드, 클래스 정의, 클래스의 퍼블릭 메서드 앞에 @Transactional 어노테이션을 둘 수 있다. 하지만 단지 @Transactional 어노테이션의 존재만으로는 트랜잭션 동작을 활성화하기에 충분하지 않다. @Transactional 어노테이션은 단순히 @Transactional을 인지하는 몇몇 런타임 인프라스트럭처가 소비할 수 있는 메타데이터이고 적절한 빈에 트랜잭션 동작을 설정하는데 메타데이터를 사용할 수 있다. 앞의 예제에서 <tx:annotation-driven/> 요소가 트랜잭션 동작을 활성화한다.

메서드 가시성과 @Transactional
프 록시를 사용할 때는 public 가시성을 가진 메서드에만 @Transactional 어노테이션을 적용해야 한다. protected나 private, package-visible 메서드에 @Transactional 어노테이션을 붙히면 오류가 발생하지는 않지만 어노테이션이 붙은 메서드는 설정된 트랜잭션 설정에 나타나지 않는다. 퍼블릭이 아닌 메서드에 어노테이션을 붙혀야 한다면 AspectJ의 사용을 고려해봐라(아래 참고).

Tip
스 프링은 인터페이스에 어노테이션을 붙히는 것과는 반대로 구현(concrete) 클래스(그리고 구현 클래스의 메서드들)에만 @Transactional 어노테이션을 붙히기를 권장한다. 확실히 인터페이스(또는 인터페이스의 메서드)에도 @Transactional 어노테이션을 붙힐 수 있지만 인터페이스 기반의 프록시를 사용하는 경우에만 제대로 동작한다. 자바의 어노테이션이 인터페이스를 상속받지 않는다는 점은 클래스 기반의 프록시 (proxy-target-class="true")를 사용하거나 위빙기반의 관점 (mode="aspectj")을 사용할 때 프록시와 위빙 인프라스트럭처가 트랜잭션 설정을 인지하지 못하고 해당 객체가 트랜잭션이 적용된 프록시로 감싸지지 않는다는 것(아주 안좋다)을 의미한다.

Note
프 록시 모드에서(기본값이다) 프록시를 통한 외부 메서드 호출만을 가로챈다. 즉, 호출된 메서드가 @Transactional로 표시되어 있더라도 자기호출(self-invocation, 대상 객체의 다른 메서드를 호출하는 대상 객체내의 메서드)은 런타임시에 실제로 트랜잭션이 되지 않을 것이다.

자기호출이 트랜잭션으로 잘 감싸지길 원한다면 AspectJ 모드의 사용을 고려해 봐라.(아래 표의 mode 속성을 참고해라.) 우선 이 경우에 프록시가 없다. 대신, 모든 종류의 메서드에서 @Transactional을 런타임동작으로 바꾸기 위해 대상 객체는 위빙된 것이다.(즉 대상객체의 바이트코드가 수정될 것이다.)


Table 11.2. <tx:annotation-driven/> 설정

속성 기본값 설명
transaction-manager transactionManager 사용할 트랜잭션 관리자의 이름. 위의 예제처럼 트랜잭션 관리자의 이름이 transactionManager이 아닌 경우에만 필요하다.
mode proxy 기 본 모드인 "proxy"가 스프링의 AOP 프레임워크를 사용해서 프록시되는 어노테이션이 붙은 빈을 처리한다.(위에서 얘기한 다음의 프록시 의미는 프록시를 통한 메서드 호출에만 적용된다.) 다른 모드인 "aspectj"는 스프링의 AspectJ 트랜잭션 관점으로 영향받은 클래스를 대신 위빙해서 모든 종류의 메서드 호출에 적용하기 위해 대상객체의 바이트코드를 수정한다. AspectJ 위빙은 활성화된 로드타임 위빙(또는 컴파일타임 위빙)과 마찬가지로 클래스패스에 spring-aspects.jar를 필요로 한다.(로드타임 위빙을 설정하는 방법은 Section 8.8.4.5, “스프링 설정”를 참고해라.)
proxy-target-class false proxy 모드에만 적용된다. @Transactional 어노테이션이 붙은 클래스에 어떤 타입의 트랜잭션 프록시를 생성할 것인지 제어한다. proxy-target-class 속성을 true로 설정했다면 클래스기반의 프록시가 생성된다. proxy-target-class가 false이거나 이 속성을 생략하면 표준 JDK 인터페이스 기반 프록시가 생성된다. (다른 프록시 타입의 자세한 설명은 Section 8.6, “프록싱 메카니즘”를 참고해라.)
order Ordered.LOWEST_PRECEDENCE @Transactional 어노테이션이 붙은 빈에 적용되는 트랜잭션 어드바이스의 순서를 정의한다. (AOP 어드바이스의 순서와 관계된 규칙에 대한 내용은 Section 8.2.4.7, “어드바이스 순서”를 참고해라.) 순서를 지정하지 않으면 AOP 서브시스템이 어드바이스의 순서를 결정한다.



























Note
@Transactional 어노테이션이 붙은 클래스에 어떤 타입의 트랜잭션이 적용된 프록시를 생성할 것인 지를 <tx:annotation-driven/> 요소의 proxy-target-class 속성이 제어한다. proxy-target-class 속성을 true로 설정했으면 클래스기반의 프록시가 생성된다. proxy-target-class가 false이거나 이 속성을 생략하면 표준 JDK 인터페이스기반의 프록시가 생성된다. (다른 프록시 타입에 대한 내용은 Section 8.6, “프록싱 메카니즘”를 참고해라.)

Note
<tx:annotation- driven/>는 같은 어플리케이션 컨텍스트에 정의된 @Transactional가 붙은 빈만을 찾는다. 즉, DispatcherServlet을 위해서 WebApplicationContext에 <tx:annotation-driven/>를 두면 서비스가 아니라 컨트롤러에서만 @Transactional 빈을 확인한다. 자세한 내용은 Section 16.2, “The DispatcherServlet”를 참고해라.

메 서드에 트랜잭션 설정을 평가할 때 가장 깊은 위치(most derived location)의 설정을 우선시한다. 다음 예제에서 DefaultFooService 클래스는 읽기전용 트랜잭션 설정으로 클래스수준에 어노테이션을 붙혔지만 같은 클래스의 updateFoo(Foo) 메서드의 @Transactional 어노테이션이 클래스수준에 정의된 트랜잭션 설정보다 앞선다.


@Transactional(readOnly = true)
public class DefaultFooService implements FooService {
 
  public Foo getFoo(String fooName) {
    // 어떤 작업을 한다
  }
 
  // 이 메서드에는 이 설정이 우선시된다
  @Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
  public void updateFoo(Foo foo) {
    // 어떤 작업을 한다
  }
}


11.5.6.1 @Transactional 설정
@Transactional 어노테이션은 인터페이스, 클래스, 메서드가 트랜잭션이 되어야 한다는 것을 지정하는 메타데이터이다. 예를 들어 “해당 메서드가 호출될 때 기존의 존재하는 트랜잭션은 중지시키고 읽기 전용의 새로운 트랜잭션을 시작해야 한다는 식이다.” 기본 @Transactional 설정은 다음과 같다.

  • 전파 설정은 PROPAGATION_REQUIRED 이다.
  • 격리 수준은 ISOLATION_DEFAULT 이다.
  • 트랜잭션은 읽기/쓰기가 가능하다.
  • 트랜잭션 타임아웃의 기본값은 의존하는 트랜잭션 시스템의 기본 타임아웃값이거나 타임아웃이 지원되지 않는다면 타임아웃이 없다.
  • 모든 RuntimeException은 롤백을 실행하고 체크드 Exception은 롤백하지 않는다.
이 기본설정은 변경할 수 있다. @Transactional 어노테이션의 다양한 프로퍼티는 다음 표에 정리해 놨다.


Table 11.3. @Transactional 프로퍼티

프로퍼티 타입 설명
value String 사용할 트랜잭션 관리자를 지정하는 선택적인 제한자(qualifier)
propagation enum: Propagation 선택적인 전파 설정.
isolation enum: Isolation 선택적인 격리수준.
readOnly boolean 읽기/쓰기 트랜잭션인가? 읽기전용 트랜잭션인가?
timeout int (in seconds granularity) 트랜잭션 타임아웃.
rollbackFor Throwable에서 얻어져야하는 Class 객체들의 배열. 반드시 롤백해야 하는 예외 클래스의 선택적인 배열.
rollbackForClassname 클래스 이름의 배열. Throwable에서 얻어져야 하는 클래스들. 반드시 롤백해야 하는 예외 클래스 이름의 선택적인 배열.
noRollbackFor Throwable에서 얻어져야 하는 Class 객체들의 배열. 반드시 롤백하지 않아야 하는 예외 클래스의 선택적인 배열.
noRollbackForClassname Throwable에서 얻어져야 하는 클래스 이름 String의 배열. 반드시 롤백하지 않아야 하는 예외 클래스 이름의 선택적인 배열.




















지금은 트랜잭션의 이름으로 명시적인 제어를 할 수 없다. 여기서 '이름'은 가능한 경우 트랜잭션 모니터(예를 들면 웹로직의 트랜잭션 모니터)와 로깅 출력에 나올 트랜잭션의 이름을 의미한다. 선언적인 트랜잭션에서 트랜잭션 이름은 항상 정규화된 클래스명 + "." + 트랜잭션하게 어드바이즈된 클래스의 메서드명이다. 예를 들어 BusinessService 클래스의 handlePayment(..) 메서드가 트랜잭션을 시작하면 트랜잭션의 이름은 com.foo.BusinessService.handlePayment가 된다.


11.5.6.2 @Transactional과 여러 트랜잭션 관리자
대 부분의 스프링 어플리케이션은 딱 하나의 트랜잭션 관리자만 필요로 하지만 하나의 어플리케이션에서 여러 개의 독립적인 트랜잭션 관리자가 필요한 상황이 있을 것이다. @Transactional 어노테이션의 value 속성은 사용할 PlatformTransactionManager의 식별자를 선택적으로 지정하는데 사용할 수 있다. 이는 트랜잭션 관리자 빈의 이름이나 제한자(qualifier) 값이 될 수 있다. 예를 들어 제한자(qualifier) 표기법을 사용한 다음의 자바코드는

 public class TransactionalService {

   
  @Transactional("order")
  public void setSomething(String name) { ... }
 
  @Transactional("account")
  public void doSomething() { ... }
}

어플리케이션 컨텍스트에서 다음의 트랜잭션 관리자 빈 선언과 섞을 수 있다.

<tx:annotation-driven/>
 
  <bean id="transactionManager1" class="org.springframework.jdbc.DataSourceTransactionManager">
    ...
    <qualifier value="order"/>
  </bean>
 
  <bean id="transactionManager2" class="org.springframework.jdbc.DataSourceTransactionManager">
    ...
    <qualifier value="account"/>
</bean> 


이 경우에 TransactionalService의 두 메서드는 "order"와 "account" 제한자로 구분된 별도의 트랜잭션 관리자에서 실행될 것이다. 적합한 PlatformTransactionManager 빈을 특별히 찾아내지 못한다면 transactionManager 빈 이름을 가리키는 기본 <tx:annotation-driven>를 여전히 사용할 것이다.


11.5.6.3 커스텀 단축 어노테이션
여러 메서드의 @Transactional에 같은 속성을 반복적으로 사용하고 있다면 스프링 메타어노테이션 지원을 이용해서 커스텀 단축 어노테이션을 정의할 수 있다. 예를 들면 다음과 같은 어노테이션을 정의해서

@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(value="order", propagation=Propagation.REQUIRED_NEW, rollbackFor=Exception.class)
public @interface OrderTx {
}
 
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional("account")
public @interface AccountTx {
}


다음과 같이 이전 섹션의 예제를 작성할 수 있다.

public class TransactionalService {
   
  @OrderTx
  public void setSomething(String name) { ... }
 
  @AccountTx
  public void doSomething() { ... }
}

여기서는 트랜잭션 관리자 제한자를 정의하는 문법을 사용했지만 전파 동작, 롤백 규칙, 타임아웃 등도 사용할 수 있다.


11.5.7 트랜잭션 전파
이번 섹션에서는 스프링에서 트랜잭션 전파의 의미를 설명한다. 이번 섹션이 트랜잭션 전파의 적절한 소개가 아니라는 것을 유념해야 한다. 오히려 스프링에서 트랜잭션 전파에 관련된 몇 가지 의미를 설명한다.

스프링이 관리하는 트랜잭션에서sms 물리적인 트랜잭션과 논리적인 트랜잭션간의 차이점과 이 차이점에 전파설정을 적용하는 방법을 알아야 한다.


11.5.7.1 Required

PROPAGATION_REQUIRED

PROPAGATION_REQUIRED

전파 설정이 PROPAGATION_REQUIRED인 경우 설정이 적용된 각 메서드마다 논리적인 트랜잭션의 범위가 생성된다. 이러한 각각의 논리적인 트랜잭션 범위는 외부 트랜잭션 범위는 내부 트랜잭션 범위와 논리적으로 독립되어 개별적인 롤백전용(rollback-only) 상태를 결정한다. 물론 표준 PROPAGATION_REQUIRED 동작의 경우 이러한 모든 범위는 같은 물리적인 트랜잭션에 매핑될 것이다. 그래서 내부 트랜잭션 범위에 설정된 롤백전용 표시(marker)는 외부 트랜잭션이 실제로 커밋할 기회에 영향을 준다.(기대대로)

하지만 내부 트랜잭션 범위가 롤백전용으로 설정된 경우에 외부 트랜잭션 스스로 롤백을 결정하지 않으므로 롤백을(내부 트랜잭션 범위가 조용히 실행한다.) 예상하지 못한다. 이 때 대응되는 UnexpectedRollbackException이 던져진다. 이는 트랜잭션의 호출자가 커밋되지 않아야 하는 경우 커밋되었다고 가정하지 않도록 할 수 있는 예상된 동작이다. 그래서 내부 트랜잭션(외부 호출자가 알지 못하는)은 트랜잭션을 조용히 롤백전용으로 표시하고 외부 호출자는 여전히 커밋을 호출한다. 외부 호출자는 커밋이 아니라 롤백이 수행되었다는 것을 명확히 알도록 UnexpectedRollbackException를 받야야 한다.


11.5.7.2 RequiresNew
PROPAGATION_REQUIRES_NEW

PROPAGATION_REQUIRES_NEW

PROPAGATION_REQUIRED 와는 반대로 PROPAGATION_REQUIRES_NEW는 영향받은 각각의 트랜잭션 범위에 완전히 독릭적인 트랜잭션을 사용한다. 이러한 경우 의존하는 물리적인 트랜잭션이 다르므로 외부 트랜잭션이 내부 트랜잭션의 롤백상태에 영향을 받지 않고 독립적으로 커밋하거나 롤백할 수 있다.


11.5.7.3 Nested
PROPAGATION_NESTED 는 롤백할 수 있는 여러 세이브포인트(savepoint)를 가진 하나의 물리적인 트랜잭션을 사용한다. 이러한 부분 롤백으로 어떤 작업이 롤백되었더라도 외부 트랜잭션은 계속해서 물리적인 트랜잭션을 진행하면서 내부 트랜잭션 범위가 자신의 범위에서 롤백을 실행할 수 있게 한다. 이 설정은 보통 JDBC 세이브포인트에 매핑되므로 JDBC 리소스 트랜잭션에서만 동작할 것이다. 스프링의 DataSourceTransactionManager를 참고해라.


11.5.8 트랜잭션 작업 어드바이징하기
트랜잭션 작업과 몇가지 기본적인 프로파일링 어드바이스를 둘 다 실행한다고 생각해보자. <tx:annotation-driven/> 컨텍스트에서 이렇게 하려면 어떻게 해야하는가?

updateFoo(Foo) 메서드를 호출할 때 다음의 동작을 보기 원할 것이다.

  1. 설정된 프로파일링 관점 시작.
  2. 트랙잭션이 적용된 어드바이스 실행.
  3. 어드바이즈된 객체의 메서드 실행.
  4. 트랜잭션 커밋.
  5. 프로파일링 관점이 전체 트랜잭션 메서드 호출의 정확한 실행시간을 리포팅함.
Note
이번 장에서는 AOP의 자세한 내용은 설명하지 않는다.(트랜잭션을 적용하는 것과 관련된 것은 제외하고) 일반적인 다음의 AOP와 AOP의 설정에 대한 자세한 내용은 Chapter 8, 스프링의 관점 지향 프로그래밍를 참고해라.

다음은 앞에서 얘기한 간단한 프로파일링 관점에 대한 코드이다. 어드바이스의 순서는 Ordered 인터페이스로 제어한다. 어드바이스 순서에 대한 자세한 내용은 Section 8.2.4.7, “어드바이스 순서”를 참고해라.


package x.y;
 
import org.aspectj.lang.ProceedingJoinPoint;
import org.springframework.util.StopWatch;
import org.springframework.core.Ordered;
 
public class SimpleProfiler implements Ordered {
 
  private int order;
 
  // 어드바이스의 순서를 제어할 수 있게 한다
  public int getOrder() {
    return this.order;
  }
 
  public void setOrder(int order) {
    this.order = order;
  }
 
  // 이 메서드는 around advice 이다
  public Object profile(ProceedingJoinPoint call) throws Throwable {
    Object returnValue;
    StopWatch clock = new StopWatch(getClass().getName());
    try {
      clock.start(call.toShortString());
      returnValue = call.proceed();
    } finally {
      clock.stop();
      System.out.println(clock.prettyPrint());
    }
    return returnValue;
  }
} 


<?xml version="1.0" encoding="UTF-8"?>
     xsi:schemaLocation="
 
  <bean id="fooService" class="x.y.service.DefaultFooService"/>
 
  <!-- 관점이다 -->
  <bean id="profiler" class="x.y.SimpleProfiler">
    <!-- 트랜잭션이 적용된 어드바이스 이전에 실행한다 (그러므로 순서(order) 번호가 작다) -->
    <property name="order" value="1"/>
  </bean>
 
  <tx:annotation-driven transaction-manager="txManager" order="200"/>
 
  <aop:config>
    <!-- 이 어드바시스는 트랜잭션이 적용된 어드바이스 주위에서(around) 실행될 것이다 -->
    <aop:aspect id="profilingAspect" ref="profiler">
      <aop:pointcut id="serviceMethodWithReturnValue"
              expression="execution(!void x.y..*Service.*(..))"/>
      <aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>
    </aop:aspect>
  </aop:config>
 
  <bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
    <property name="driverClassName" value="oracle.jdbc.driver.OracleDriver"/>
    <property name="url" value="jdbc:oracle:thin:@rj-t42:1521:elvis"/>
    <property name="username" value="scott"/>
    <property name="password" value="tiger"/>
  </bean>
 
  <bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    <property name="dataSource" ref="dataSource"/>
  </bean>
</beans> 


위의 설정으로 원하는 순서에 따라 fooService 빈에 프로파일링과 트랜잭셔널 관점을 적용한다. 유사한 방법으로 다수의 추가 관점을 설정한다.

다음 예제는 위의 설정과 같은 효과가 있지만 순수하게 XML의 선언적인 접근을 사용한다.

<?xml version="1.0" encoding="UTF-8"?>
     xsi:schemaLocation="
 
  <bean id="fooService" class="x.y.service.DefaultFooService"/>
 
  <!-- 프로파일링 어드바이스 -->
  <bean id="profiler" class="x.y.SimpleProfiler">
    <!-- 트랜잭션이 적용된 어드바이스 이전에 실행된다. (그러므로 순서(order) 번호가 낮다) -->
    <property name="order" value="1"/>
  </bean>
 
  <aop:config>
 
    <aop:pointcut id="entryPointMethod" expression="execution(* x.y..*Service.*(..))"/>
 
    <!-- 프로파일링 어드바이스 이후에 실행될 것이다. (order 속성을 비교해봐라) -->
    <aop:advisor
        advice-ref="txAdvice"
        pointcut-ref="entryPointMethod"
        order="2"/> <!-- order 값이 프로파일링 관점보다 크다 -->
 
    <aop:aspect id="profilingAspect" ref="profiler">
      <aop:pointcut id="serviceMethodWithReturnValue"
              expression="execution(!void x.y..*Service.*(..))"/>
      <aop:around method="profile" pointcut-ref="serviceMethodWithReturnValue"/>
    </aop:aspect>
 
  </aop:config>
 
  <tx:advice id="txAdvice" transaction-manager="txManager">
    <tx:attributes>
      <tx:method name="get*" read-only="true"/>
      <tx:method name="*"/>
    </tx:attributes>
  </tx:advice>
 
  <!-- DataSource와 PlatformTransactionManager같은 다른 <bean/> 정의는 여기에 한다 -->
</beans>


앞 의 설정으로 순서에 따라 프로파일링 관점과 트랜잭셔널 관점이 fooService 빈에 적용될 것이다. 트랜잭셔널 어드바이스에 진입한 이후와 트랜잭셔널 어드바이스가 끝나기 전에 프로파일링 어드바이스를 실행하려고 하면 그냥 프로파일링 관점 빈의 order 프로퍼티 값을 트랜잭셔널 어드바이스의 order 값보다 높게 바꿔주면 된다.

추가적인 관점도 비슷한 방법으로 설정한다.


11.5.9 AspectJ로 @Transactional 사용하기
스 프링 컨테이너 외부에서 AspectJ 관점으로 스프링 프레임워크의 @Transactional 지원을 사용할 수도 있다. 이렇게 하려면 일단 클래스(선택적으로 클래스의 메서드에)에 @Transactional 어노테이션을 붙히고 그 다음 spring-aspects.jar에 정의된 org.springframework.transaction.aspectj.AnnotationTransactionAspect와 어플리케이션은 연결(위브, weave)한다. 관점도 반드시 트랜잭션 관리자로 설정해야 한다. 당연히 관점을 의존성 주입하는데 스프링 프레임워크의 IoC 컨테이너를 사용할 수 있다. 트랜잭션 관리 관점을 설정하는 가장 간단한 방법은 <tx:annotation-driven/> 요소를 사용하고 Section 11.5.6, “@Transactional 사용하기”에서 설명한 것처럼 aspectj에 mode 속성을 지정하는 것이다. 여기서는 스프링 컨테이너 외부에서 동작하는 어플리케이션에 대해서 얘기하고 있으므로 프로그래밍적으로 설정하는 방법을 설명할 것이다.

Note
계속 읽기 전에 Section 11.5.6, “@Transactional 사용하기”와 Chapter 8, 스프링의 관점 지향 프로그래밍를 읽으면 도움이 될 것이다.


// 적절한 트랜잭션 관리자를 생성한다
DataSourceTransactionManager txManager = new DataSourceTransactionManager(getDataSource());
 
// 사용할 AnnotationTransactionAspect를 설정한다. 이는 반드시 트랜잭션이 적용된 어떤 메서드라도 실행하기 전에 이뤄져야 한다.
AnnotationTransactionAspect.aspectOf().setTransactionManager(txManager);


Note
이 관점을 사용할 때는 클래스가 구현한 인터페이스(존재한다면)가 아니라 구현 클래스(또는 구현클래스의 메서드)에 어노테이션을 붙혀야 한다. AspectJ는 인터페이스에 붙은 어노테이션은 상속받지 않는다는 자바의 규칙을 따른다.

클래스에 붙은 @Transactional 어노테이션은 클래스의 모든 메서드 실행에 대한 기본 트랜잭션 동작을 지정한다.

클래스내의 메서드에 붙은 @Transactional 어노테이션은 클래스 어노테이션(존재한다면)이 지정한 기본 트랜잭션 동작을 덮어쓴다. 가시성에 상관없이 모든 메서드는 어노테이션이 붙을 수 있다.

AnnotationTransactionAspect으로 어플리케이션을 위빙하려면 어플리케이션은 AspectJ로 구성하거나(AspectJ 개발 가이드 참고) 로드타입 위빙을 사용해야 한다. AspectJ를 사용하는 로드타임 위빙에 대한 내용은 Section 8.8.4, “스프링 프레임워크에서 AspectJ를 사용한 로드타임 위빙(Load-time weaving)”를 참고해라.


11.6 프로그래밍적인 트랜잭션 관리
스프링 프레임워크는 프로그래밍적인 트랜잭션 관리의 두가지 수단을 제공한다.

  • TransactionTemplate 사용하기.
  • PlatformTransactionManager 구현체를 직접 사용하기.
스프링 팀은 프로그래밍적인 트랜잭션 관리에 보통 TransactionTemplate를 추천한다. 두번째 접근은 예외 처리에 대한 부단이 적기는 하지만 JTA UserTransaction API를 사용하는 것과 유사하다.


11.6.1 TransactionTemplate 사용하기
TransactionTemplate 은 JdbcTemplate같은 다른 스프링 템플릿과 같은 접근을 채택했다. TransactionTemplate는 어플리케이션 코드가 보일러플레이트 획득과 트랜잭셔널 리소스의 해지에서 자유롭도록 콜백 접근을 사용한다. 그래서 코드는 의도지향적(intention driven)이 되고 작성한 코드는 개발자가 하고자 한것에만 오로지 집중할 수 있다.

Note
다 음에 예제에서 보듯이 TransactionTemplate를 사용하면 스프링의 트랜잭션 인프라스트럭처 API에 완전히 커플링된다. 개발요구사항에 프로그래밍적인 트랜잭션 관리가 적합하든지 적합하지 않든지간에 결정은 스스로 하는 것이다.

트 랜잭션 컨텍스트에서 실행되어야 하고 명시적으로 TransactionTemplate를 사용할 어플리케이션 코드는 다음과 같다. 어플리케이션 개발자는 트랜잭션 컨텍스트에서 실행해야하는 코드를 담고 있는 TransactionCallback 구현체를 작성한다. 그 다음 작성한 커스텀 TransactionCallback의 인스턴스를 TransactionTemplate에 노출된 execute(..) 메서드에 전달한다.


public class SimpleService implements Service {
 
  // 이 인스턴스의 모든 메서드에서 공유되는 하나의 TransactionTemplate
  private final TransactionTemplate transactionTemplate;
 
  // PlatformTransactionManager를 제공하기 위해 생성자 주입을 사용한다
  public SimpleService(PlatformTransactionManager transactionManager) {
    Assert.notNull(transactionManager, "The 'transactionManager' argument must not be null.");
    this.transactionTemplate = new TransactionTemplate(transactionManager);
  }
 
  public Object someServiceMethod() {
    return transactionTemplate.execute(new TransactionCallback() {
 
      // 이 메서드의 코드는 트랜잭션 컨텍스트에서 실행된다
      public Object doInTransaction(TransactionStatus status) {
        updateOperation1();
        return resultOfUpdateOperation2();
      }
    });
  }
} 


반환값이 없으면 다음과 같이 익명 클래스로 간편한 TransactionCallbackWithoutResult 클래스를 사용해라.

transactionTemplate.execute(new TransactionCallbackWithoutResult() {
 
  protected void doInTransactionWithoutResult(TransactionStatus status) {
    updateOperation1();
    updateOperation2();
  }
});

콜백내의 코드는 제공된 TransactionStatus 객체의 setRollbackOnly() 메서드를 호출해서 트랜잭션을 롤백할 수 있다.

 transactionTemplate.execute(new TransactionCallbackWithoutResult() {

 
  protected void doInTransactionWithoutResult(TransactionStatus status) {
    try {
      updateOperation1();
      updateOperation2();
    } catch (SomeBusinessExeption ex) {
      status.setRollbackOnly();
    }
  }
});


11.6.1.1 트랜잭션 설정 지정하기
프 로그래밍적으로나 설정에서 TransactionTemplate에 전파모드, 격리수준, 타임아웃 등의 트랜잭션 설정을 지정할 수 있다. 기본적으로 TransactionTemplate 인스턴스는 기본 트랜잭션 설정을 가진다. 다음 예제는 특정 TransactionTemplate의 트랜잭션 설정을 프로그래밍적으로 커스터마이징한 것을 보여준다.

public class SimpleService implements Service {
 
  private final TransactionTemplate transactionTemplate;
 
  public SimpleService(PlatformTransactionManager transactionManager) {
    Assert.notNull(transactionManager, "The 'transactionManager' argument must not be null.");
    this.transactionTemplate = new TransactionTemplate(transactionManager);
 
    // 원한다면 트랜잭션 설정은 여기서 명시적으로 설정할 수 있다
    this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
    this.transactionTemplate.setTimeout(30); // 30 초
    // 등등...
  }
} 


다음 예제는 스프링 XML 설정을 사용해서 몇가지 커스텀 트랜잭션 설정으로 TransactionTemplate를 정의한다. sharedTransactionTemplate를 필요한 만큼의 서비스에 주입할 수 있다.

<bean id="sharedTransactionTemplate" class="org.springframework.transaction.support.TransactionTemplate">
  <property name="isolationLevelName" value="ISOLATION_READ_UNCOMMITTED"/>
  <property name="timeout" value="30"/>
</bean> 


마지막으로 TransactionTemplate 클래스의 인스턴스들은 스레드세이프하므로 어떤 대화식(conversational) 상태도 유지하지 않는다. 하지만 TransactionTemplate 인스턴스는 설정(configuration) 상태를 유지한다. 그러므로 다수의 클래스가 하나의 TransactionTemplate 인스턴스를 공유할 것이므로 클래스가 다른 설정(예를 들어 다른 격리수준)의 TransactionTemplate를 사용해야 한다면 두 가지의 다른 TransactionTemplate 인스턴스를 생성해야 한다.


11.6.2  PlatformTransactionManager 사용하기
트 랜잭션을 관리하는데 org.springframework.transaction.PlatformTransactionManager를 직접 사용할 수도 있다. 단순히 사용하는 PlatformTransactionManager의 구현체를 빈 레퍼런스로 빈에 전달해라. 그 다음 TransactionDefinition와 TransactionStatus를 사용해서 트랜잭션을 시작하고 롤백하고 커밋할 수 있다.

 DefaultTransactionDefinition def = new DefaultTransactionDefinition();

// 트랜잭션 이름만이 프로그래밍적으로 명시적으로 설정할 수 있다
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
 
TransactionStatus status = txManager.getTransaction(def);
try {
  // 여기의 비즈니스 로직을 실행한다
}
catch (MyException ex) {
  txManager.rollback(status);
  throw ex;
}
txManager.commit(status);


11.7 프로그래밍적인 트랜잭션 관리와 선언적인 트랜잭션 중에서 선택하기
프 로그래밍적인 트랜잭션 관리는 적은 수의 트랜잭션 작업이 있을 때만 보통 좋은 생각이다. 예를 들어 특정 업데이트 작업에만 트랜잭션이 필요한 웹어플리케이션인 경우 스프링이나 다른 기술을 사용해서 트랜잭션이 적용된 프록시를 설정하기 원치 않을 것이다. 이러한 경우 TransactionTemplate을 사용하는 것이 좋은 접근이 될 수 있다. 명시적으로 트랜잭션 이름을 설정할 수 있는 것도 트랜잭션 관리에 프로그래밍적인 접근을 사용해서만 할 수 있는 것이다.

반면에 어플리케이션에 매우 많은 트랜잭션 작업이 있다면 선언적인 트랜잭션 관리가 일반적으로 휼륭하다. 선언적인 트랜잭션 관리는 비즈니스 로직 외부에서 트랜잭션 관리를 하고 설정하기가 어렵지 않다. EJB CMT가 아니라 스프링 프레임워크를 사용할 때 선언적인 트랜잭션 관리의 설정 비용은 엄청나게 줄어든다.


11.8 어플리케이션 서버에 특화된 통합
스 프링의 트랜잭션 추상화는 보통 어플리케이션 서버와 관계가 없다. 게다가 JTA UserTransaction와 TransactionManager 객체들을 검색하는 JNDI를 선택적으로 수행하는 스프링의 JtaTransactionManager 클래스는 어플리케이션 서버마다 다른 TransactionManager의 위치를 자동으로 탐지한다. JTA TransactionManager로의 접근해서 특히 트랜잭션 중지 지원같은 향상된 트랜잭션을 사용할 수 있다. 자세한 내용은 JtaTransactionManager Javadoc을 참고해라.

스프링의 JtaTransactionManager는 Java EE 어플리케이션 서버에서 실행할 때의 일반적인 선택이고 별도의 설정을 하지 않고도 일반적인 모든 서버에서 동작한다고 알려져있다. 트랜잭션 중지같은 향샹된 기능은 다수의 서버에서 잘 동작한다.(GlassFish, JBoss, Geronimo, Oracle OC4J를 포함해서) 하지만 완전한 트랜잭션 중지 지원과 더 향상된 통합을 위해서 스프링은 IBM WebSphere, BEA WebLogic 서버, Oracle OC4J에 대한 전용 아답터를 제공한다. 이러한 아답터들은 다음 섹션에서 설명한다.

WebLogic 서버, WebSphere, OC4J를 포함한 일반적인 시나리오에서는 간편한 <tx:jta-transaction-manager/> 설정요소의 사용을 고려해 봐라. 이 요소를 설정하면 의존하는 서버를 자동으로 탐지하고 플랫폼에서 사용할 수 있는 가장 좋은 트랜잭션 관리자를 선택한다. 즉, 서버에 특화된 아답터 클래스(앞의 섹션에서 설명했듯이)를 명시적으로 설정하지 않아도 된다. 반대로 아답터 클래스들은 자동으로 선택하고 기본 폴백(fallback)으로 표준 JtaTransactionManager를 사용한다.


11.8.1 IBM WebSphere
WebSphere 6.1.0.9 이상의 버전에서 사용하길 추천하는 스프링 JTA 트랜잭션 관리자는 WebSphereUowTransactionManager이다. 이 전용 아답터는 웹스피어 어플리케이션 서버 6.0.2.19 이상의 버전과 6.1.0.9 이상의 버전에서 사용할 수 있는 IBM의 UOWManager API를 사용한다. 이 아답터를 사용하면 스프링 주도의 트랜잭션 중지(PROPAGATION_REQUIRES_NEW으로 시작된 것처럼 중지/복귀)를 IBM이 공식적으로 지원한다!


11.8.2 BEA WebLogic 서버
웹 로직 서버 9.0 이상에서는 JtaTransactionManager 클래스 대신 WebLogicJtaTransactionManager를 보통 사용할 것이다. 이 클래스는 웹로직에 특화된 클래스로 일반적인 JtaTransactionManager의 하위클래스로 표준 JTA 의미를 넘어 웹로직이 관리하는 트랜잭션 환경에서 스프링의 트랜잭션 정의를 완전히 지원한다. 트랜잭션 이름, 트랜잭션당 격리 수준, 모든 경우에서 트랜잭션을 적절히 복귀하는 등의 기능을 포함한다.


11.8.3 Oracle OC4J
스프링은 OC4J 10.1.3 이상의 버전을 위해서 전용 아답터 클래스인 OC4JJtaTransactionManager를 제공한다. 이 클래스는 앞에서 설명한 WebLogicJtaTransactionManager 클래스와 유사해서 OC4J에 트랜잭션 이름, 트랜잭션당 격리 수준같은 부가가치를 제공한다.

트랜잭션 중지를 포함한 전체 JTA 기능은 OC4J상에서도 스프링의 JtaTransactionManager로 잘 동작한다. 전용 OC4JJtaTransactionManager 아답터는 표준 JTA 이상의 부가가치를 제공할 뿐이다.


11.9 일반적인 문제에 대한 해결책

11.9.1 지정한 DataSource에 잘못된 트랜잭션 관리자의 사용
트 랜잭션 기술과 요구사항의 선택에 기반해서 올바른 PlatformTransactionManager 구현체를 사용해라. 적절히 사용하면 스프링 프레임워크는 단지 직관적이고 이식성있는 추상화를 제공할 뿐이다. 전역 트랜잭션을 사용한다면 모든 트랜잭션 작업에 org.springframework.transaction.jta.JtaTransactionManager 클래스(또는 이 클래스의 하위클래스이면서 어플리케이션에 특화된 클래스)를 반드시 사용해야 한다. 그렇지 않으면 트랜잭션 인프라스트럭처가 컨테이너 DataSource 인스턴스같은 리소스에 지역 트랜잭션을 수행하려고 한다. 이러한 지역 트랜잭션은 적합하지 않으며 좋은 어플리케이션 서버라면 에러로 취급한다.


11.10 관련 자료
스프링 프레임워크의 트랜잭션 지원에 대한 더 자세한 정보는 다음을 참고해라.

  • XA를 사용하든지 안하든간에 스프링에서 분산 트랜잭션은 스프링소스의 David Syer가 스프링 어플리케이션에서 분산 트랜잭션의 7가지 패턴을 설명하는 JavaWorld의 발표자료이다. 7가지 패턴 중 3가지는 XA를 사용하고 4가지는 사용하지 않는다.
  • 자바 트랜잭션 디자인 전략InfoQ의 책으로 자바에서 트랜잭션에 대한 좋은 소개를 제공한다. 그리고 이 책은 스프링 프레임워크와 EJB3 모두에서 트랜잭션을 설정하고 사용하는 방법에 대한 예제들도 포함되어 있다.



출처 : http://ilikesura.tistory.com/entry/%EC%A4%91%EB%B3%B5%EB%A1%9C%EA%B7%B8%EC%9D%B8-%EC%84%B8%EC%85%98-%EA%B4%80%EB%A6%AC%ED%95%98%EA%B8%B0-%EC%84%A4%EC%A0%95%EB%B2%95



세션 관리

타임아웃 감지하기

스프링 시큐리티에서 유효하지 않은 세션 ID를 감지하고 적절한 URL로 리다이렉트 시키도록 설정할 수 있다. 이것은 session-management 엘리먼트를 사용한다.

 
<http> ... <session-management invalid-session-url="/sessionTimeout.htm" /> </http>

동시 세션 제어

사용자가 동시에 한번만 로그인할 수 있도록 제한하고 싶으면, 스프링 시큐리티는 다음과 같이 간단하게 추가할 수 있도록 지원한다. 세션 생명주기와 관련된 이벤트를 스프링 시큐리티가 받을 수 있도록 하기 위해, 우선 다음의 리스너를 web.xml 파일에 추가할 필요가 있다.


  <listener>
    <listener-class>
      org.springframework.security.web.session.HttpSessionEventPublisher
    </listener-class>
  </listener>

그리고 애플리케이션 컨텍스트에 다음의 코드를 추가한다.

  <http>
    ...
    <session-management>
        <concurrency-control max-sessions="1" />
    </session-management>
  </http>

이것은 한 사용자가 동시에 두번 로그인 하는것을 방지한다. (두번째 로그인으로 인해 첫번째 로그인은 무효화된다) 종종 두번째 로그인을 방지할 필요가 있는데, 그런 경우에는 다음과 같이 할 수 있다.

  <http>
    ...
    <session-management>
        <concurrency-control max-sessions="1" error-if-maximum-exceeded="true" />
    </session-management>
  </http>

그러면 두번째 로그인은 거부될 것이다. "거부"될 경우는, 폼-기반 로그인이 사용되는 환경에서는 authentication-failure-url 로 보내진다. 만약 두번째 로그인이 "remember-me"와 같은 비-상호적 메커니즘에 의해 수행되었다면, "unauthorized(불허가)"(402) 에러가 발생할 것이다. 대신에 에러페이지를 사용하려면 session-management 엘리먼트의 session-authentication-error-url 속성을 추가할 수 있다.

만약 폼-기반 로그인에 직접 작성한 인증 필터를 사용한다면, 동시 세션 제어 기능을 명시적으로 설장할 수 있다. 더 자세한 사항은 세션관리 장에서 찾을 수 있다.

Session Fixation Attack 방지

악의적인 사용자가 사이트에 접근하기 위한 세션을 만들고, 그 세션을 통해 다른 사용자로 로그인 하려고 하는 경우(예를 들어, 세션에 ID를 파라미터로 포함하여 전송하는 경우) Session fixation attack의 잠재적인 위험이 존재하게 된다. 스프링 시큐리티는 이러한 공격을 자동으로 막기 위하여 사용자 로그인 때마다 새로운 세션을 생성한다. 이러한 방지 기능이 필요하지 않거나, 다른 기능들과 충돌이 발생할 경우에는, <session-management>의 session-fixation-protection 속성값으로 동작을 제어할 수 있다. 속성은 다음과 같은 세가지 옵션값들을 가진다.

  • migrateSession - 새로운 세션을 생성하고 기존의 세션 값들을 새 세션에 복사해준다. 기본값으로 설정되어 있다.
  • none - 아무것도 수행하지 않는다. 원래의 세션이 유지된다.
  • newSession - "깨끗한" 새로운 세션을 생성한다. 기존의 세션데이터는 복사하지 않는다.



XML 없이 Java만 사용해서 설정하기

출처 : http://breadmj.wordpress.com/2013/08/04/spring-3-only-java-config-without-xml/


Spring과의 첫만남

내가 스프링을 처음으로 접한 것은 스프링 프레임워크의 버전이 3.0 으로 막 올라간지 얼마 안되었을 때였다.

대략 3년정도 된 것 같은데 그때 작성했던 코드들을 아직도 사용하고 있다. 물론 자바 로직은 기능 변화에 맞추어 많이 변경되었다. 하지만 맨 처음에 작성했던 스프링 설정은 큰 변화없이 지금까지 사용중이다. 어떻게 보면 조금이라도 더 나은 설정을 위한 노력이 부족했다고 생각할 수도 있고 아니면 그 반대로 스프링을 사용했기에 (3년전의 내 코딩 실력에도 불구하고) 지금까지 안정적으로 유지했다고 생각할 수도 있다.

Spring 설정은 어플리케이션의 뼈대를 이룬다

스프링을 사용한다면 스프링 설정은 그 어플리케이션의 뼈대를 이루게 된다. 조금 억지를 넣어서 말해보자면 스프링 설정이 곧 어플리케이션 설계라고 할 수도 있다. 그렇기에 현재 운영중인 어플리케이션의 기반을 건드리는 것이 무서운 점도 어느정도 있었다. 스프링 설정이 조금 알아보기 힘들다거나, 미관상(?) 지저분해 보인다거나, 사소한 실수가 보이더라도 운영이 불가능한 상태가 아닌 이상 웬만하면 건드리지 않았다. 그러다보니 기능은 점점 늘어나는데 설정은 지저분하게 남아있어 변경하기 힘들고, 하위 호환성을 엄청 신경쓰기로 유명한 스프링의 버전을 올리는 것도 꺼려지는 지경에 이르렀다.

공포의 applicationContext.xml

그러나 과연 스프링 설정을 바꾸기 고민되는 것이, 설정 파일을 지저분하게 만들어놓은 것 때문만일까? 물론 아니니까 이 글을 썼겠지. 나는 그 이유의 반 이상이 스프링 설정에 XML 을 이용했기 때문이라고 생각한다.

스프링을 맨 처음 접했을 때, applicationContext.xml 이라는 무시무시한 이름의 파일안에 더 무시무시한 빈 설정들을 보고 식겁했던 기억이 난다. 뭐 무슨 컨버전 어쩌고.. 핸들러.. 리졸버.. 난 그것들이 무엇이고 왜 필요하며, 이 설정들을 스프링이 어떻게 읽어가는지 이해하는데까지 1년 이상의 시간이 걸렸다. 물론 그것들을 완벽하게 이해하지 못해도 어느정도 사용가능하긴 하다. 뭐든 다 삽질하면서 배우는거니까.

왜 꼭 xml 로 설정해야 하죠?

각설하고, 나와 비슷한 생각들을 많이들 했던 것 같다. 오래 전부터 스프링 설정을 XML 이 아닌 오직 Java만으로 할 수 있도록 이런저런 노력들이 이어져왔다. 그것이 근래 들어서 Servlet 3.0 스펙이 확정되고, 또 그 스펙을 구현한 톰캣 7.0 이 나오면서 꽃을 피웠다. xml 설정 단 한줄도 없이 자바만으로 스프링을 사용할 수 있게 된 것이다.

Java로 설정하면 뭐가 좋을까?

그렇다면 XML에 비해 Java만을 사용해서 설정하는 것이 어떤 이득이 있을까? 개인적인 견해도 섞여있다.

  1. 설정 파일을 따로 유지할 필요가 없다. 그냥 자바 클래스이다. 찾기 쉽다.
  2. 보다 명료하다. 어떤 것들이 빈으로 만들어지는지 파악하기 쉽다.
  3. IDE의 자동완성 기능을 사용할 수 있다. 자바 코드이기 때문이다. 그래서 작성과 수정이 빠르다.
  4. 어플리케이션 로직과 설정 코드를 동일한 언어로 만들 수 있다. 한 언어만 쓰는게 간편하니 좋다.
  5. 설정 코드에 break point 를 걸어서 디버깅할 수 있다.

이 정도만 해도 충분히 자바 코드를 이용해서 설정하는 의미가 있다. 나는 개인적으로 XML 로 만들어져있는 스프링 설정 파일을 읽거나 수정하는 것이 고역이었다. 그에 반해 자바 코드로 설정을 하니 이렇게 좋을 수가 없었다. 특히나 스프링을 처음으로 접해보는 초심자라면 XML 설정보다는 자바 설정을 이용하는 것이 더더욱 좋겠다.

그래서 뭘 어떻게 하는거라고?

서론은 이쯤하고, 이제 Java 를 이용해서 스프링 설정을 하는 방법을 알아보자. 어플리케이션은 서블릿을 만든다고 가정한다. 스프링을 이용해서 주로 서블릿을 많이 만들고 또 사용법이 다 비슷비슷하니, 다른 형태의 어플리케이션을 작성한다 하더라도 충분히 참고할만하다. 여기 나오는 모든 코드는 나의 GitHub 프로젝트 중 SpringMVCTest 프로젝트에 다 포함되어있다.

목표는 다음과 같다.

  • Spring 의 application context 설정들을 Java 로 바꾼다. root-context.xml, servlet-context.xml 요런것들 말이다.
  • web.xml 설정을 Java 로 바꾼다.

자, 그럼 시작해보자.

첫번째, pom.xml 설정 (메이븐 설정)

프로젝트 의존성 관리는 메이븐을 이용한다고 가정한다.

1. Spring 버전 3.1 이상 사용한다. 나는 현재 최신버전인 3.2.2.RELEASE 를 사용했다. 

<!-- Spring -->
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-context</artifactId>
  <version>3.2.2.RELEASE</version>
</dependency>
<dependency>
  <groupId>org.springframework</groupId>
  <artifactId>spring-webmvc</artifactId>
  <version>3.2.2.RELEASE</version>
</dependency> 


2. servlet-api 버전 3.0 이상 사용한다. web.xml 을 없애기 위해서는 서블릿 3.0 이상의 스펙이 필요하다. 

<!-- use Servlet 3.0 spec -->
<!-- Java Config 를 사용하기 위해서는 서블릿 3.0 이상의 스펙이 필요하다. -->
<!-- 톰캣의 경우 7.0 이상을 사용해야 한다. -->
<dependency>
  <groupId>javax.servlet</groupId>
  <artifactId>javax.servlet-api</artifactId>
  <version>3.0.1</version>
  <scope>provided</scope>
</dependency> 


3. Spring 버전 3.1.x 라면 cglib 을 dependency 에 추가한다. @Configuration 어노테이션을 사용하기 위해서 필요하다. 만약 추가해주지 않는다면 런타임 에러가 발생할 것이다. 스프링 버전이 3.2.x 라면 Spring 에 cglib 이 포함되어 있으므로 선언할 필요없다. 

<!-- @Configuration 어노테이션을 쓰기 위해서는 cglib 이 필요하다. -->
<!-- Spring 버전 3.2 이상부터 Spring 에 cglib 이 포함되므로, 버전에 따라 포함할지 말지 결정한다. -->
<dependency>
  <groupId>cglib</groupId>
  <artifactId>cglib</artifactId>
  <version>2.2.2</version>
  <scope>runtime</scope>
</dependency> 


4. maven-war-plugin 에 아래와 같이 failOnMissingWebXml 을 false 로 설정한다. 이 설정이 없다면 web.xml 파일이 존재하지 않는다고 투덜댈 것이다. 

<plugin>

  <groupId>org.apache.maven.plugins</groupId>
  <artifactId>maven-war-plugin</artifactId>
  <version>2.3</version>
  <configuration>
    <failOnMissingWebXml>false</failOnMissingWebXml>
  </configuration>
</plugin>


두번째, root-context.xml 없애기

root-context 에는 주로 프로퍼티 홀더 설정이나 datasource 같이 여러 서블릿에서 공통으로 사용할 설정들이 들어간다. 서블릿을 하나만 띄운다면 root context 와 servlet context 를 굳이 구분할 필요는 없지만, 이 내용은 논점에서 벗어나므로, 일단 root context 와 servlet context 가 구분되어 있다고 가정한다. 하지만 두 context 설정을 바꾸는 것은 근본적으로 동일하다.

그럼 아래 코드를 보자.

// import..

/**
 * 루트 설정용 클래스.
 * 이 클래스는 스프링의 root-context.xml 의 역할을 대신한다.
 * @author mj
 *
 */
@Configuration
public class RootConfig {
 
    @Value("${jdbc.driverClassName}")
    private String jdbcDriverClassName;
 
    @Value("${jdbc.url}")
    private String jdbcUrl;
 
    @Value("${jdbc.username}")
    private String jdbcUsername;
 
    @Value("${jdbc.password}")
    private String jdbcPassword;
 
    private static final String APP_CONFIG_FILE_PATH = "application.xml";
 
    /**
     * 프로퍼티 홀더는 다른 빈들이 사용하는 프로퍼티들을 로딩하기 때문에, static 메소드로 실행된다.
     * 다른 일반 빈들이 만들어지기전에 먼저 만들어져야 한다.
     * @return
     */
    @Bean
    public /* static 메소드에요! */ static PropertyPlaceholderConfigurer propertyPlaceholderConfigurer()
    {
        PropertyPlaceholderConfigurer ppc = new PropertyPlaceholderConfigurer();
        ppc.setLocations(new Resource[] { new ClassPathResource(APP_CONFIG_FILE_PATH) });
        return ppc;
    }
 
    @Bean
    public DataSource dataSource()
    {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName(this.jdbcDriverClassName);
        dataSource.setUrl(this.jdbcUrl);
        dataSource.setUsername(this.jdbcUsername);
        dataSource.setPassword(this.jdbcPassword);
        return dataSource;
    }
 
    // 기타 다른 bean 설정들..
}


  • 클래스를 하나 만든다. 이름은 root context 라는 것을 알아볼 수 있는 적절한 이름이면 된다.
  • 클래스에 @Configuration 어노테이션을 붙여준다. 설정용 클래스라는 것을 스프링에게 알려주는 역할이다.
  • root-context.xml 에 bean 을 등록하듯이, 빈으로 만들어지길 원하는 오브젝트를 리턴하는 메소드를 만든다. 이 메소드 내에서 빈에다가 필요한 설정을 해주고, 그것을 리턴해주면 된다.
  • 빈으로 등록되길 원하는 메소드들에는 @Bean 어노테이션을 붙여준다. 이 어노테이션이 있어야 스프링이 그 메소드들을 실행해서 빈으로 만든다.
  • property placeholder 처럼, 다른 빈보다 먼저 등록되어야 하는 것들은 static 메소드로 만든다. java 에서 static 의 역할이 무엇인지 생각해본다면 static 을 붙여주는 것이 아주 자연스럽다.

세번째, servlet-context.xml 없애기

// import..

 
/**
 * MVC 설정용 클래스.
 * 이 클래스는 스프링의 sevlet-context.xml 의 역할을 대신한다.
 * @author mj
 */
@Configuration
@EnableWebMvc
@EnableAsync // @Async 어노테이션을 사용하기 위함
@ComponentScan(
    basePackages="com.nethru.test",
    excludeFilters=@ComponentScan.Filter(Configuration.class)
)
public class MvcConfig extends WebMvcConfigurerAdapter // 인터셉터를 추가하기 위해 WebMvcConfigurerAdapter 를 상속한다
{
    @Bean
    public ViewResolver viewResolver()
    {
        InternalResourceViewResolver resolver = new InternalResourceViewResolver();
        resolver.setPrefix("/WEB-INF/views/");
        resolver.setSuffix(".jsp");
        return resolver;
    }
 
    /**
     * 인터셉터 추가
     */
    @Override
    public void addInterceptors(InterceptorRegistry registry)
    {
        registry.addInterceptor(new CorsInterceptor());
    }
}

  • root context와 마찬가지로 적절한 이름의 클래스를 만든 후, @Configuration 어노테이션을 붙여준다.
  • mvc:annotation-driven 은 @EnableWebMvc 어노테이션이 대신한다. 마찬가지로 task:annotation-driven 은 @EnableAsync 가 대신한다. 또한 트랜잭션이나 기타 다른 기능들을 이렇게 어노테이션 하나로 활성화시킬 수 있다.
  • context:component-scan 은 @ComponentScan 어노테이션이 대신한다. xml과 마찬가지로 basePackage 와 filter 를 지정할 수 있다.
  • 빈으로 등록될 오브젝트를 리턴하는 메소드를 만든 후, @Bean 어노테이션을 붙여준다. 위 예제에서는 뷰 리졸버를 빈으로 등록하고 있다.
  • 인터셉터를 등록하기 위해서는 추가적인 작업이 필요하다. WebMvcConfigurerAdapter 라는 스프링 클래스를 상속한 후, addInterceptors() 메소드를 override 한다. 메소드 내에서 필요한 인터셉터와 매핑을 지정해주면 된다.

네번째, web.xml 없애기

일단 코드부터 보자.

// import..

 
/**
 * WebApplicationInitializer 를 상속하면, 서블릿 컨테이너가 실행될 때 onStartup() 메소드가 자동으로 호출된다.
 * 이 클래스는 web.xml 의 역할을 대신하거나 보충한다.
 * @author mj
 *
 */
public class Initializer implements WebApplicationInitializer
{
    @Override
    public void onStartup(ServletContext servletContext)
            throws ServletException
    {
        AnnotationConfigWebApplicationContext rootContext = new AnnotationConfigWebApplicationContext();
        rootContext.register(RootConfig.class);
        servletContext.addListener(new ContextLoaderListener(rootContext));
 
        this.addDispatcherServlet(servletContext);
        this.addUtf8CharacterEncodingFilter(servletContext);
    }
 
    /**
     * Dispatcher Servlet 을 추가한다.
     * CORS 를 가능하게 하기 위해서 dispatchOptionsRequest 설정을 true 로 한다.
     * @param servletContext
     */
    private void addDispatcherServlet(ServletContext servletContext)
    {
        AnnotationConfigWebApplicationContext applicationContext = new AnnotationConfigWebApplicationContext();
        applicationContext.getEnvironment().addActiveProfile("production");
        applicationContext.register(MvcConfig.class);
 
        ServletRegistration.Dynamic dispatcher = servletContext.addServlet("dispatcher", new DispatcherServlet(applicationContext));
        dispatcher.setLoadOnStartup(1);
        dispatcher.addMapping("/");
        dispatcher.setInitParameter("dispatchOptionsRequest", "true"); // CORS 를 위해서 option request 도 받아들인다.
    }
 
    /**
     * UTF-8 캐릭터 인코딩 필터를 추가한다.
     * @param servletContext
     */
    private void addUtf8CharacterEncodingFilter(ServletContext servletContext)
    {
        FilterRegistration.Dynamic filter = servletContext.addFilter("CHARACTER_ENCODING_FILTER", CharacterEncodingFilter.class);
        filter.setInitParameter("encoding", "UTF-8");
        filter.setInitParameter("forceEncoding", "true");
        filter.addMappingForUrlPatterns(null, false, "/*");
    }
}

  • WebApplicationInitializer 인터페이스를 구현한 클래스를 만든다. 이 클래스는 web.xml의 역할을 대신할 클래스이다.
  • onStartup() 메소드를 override 한다. 이 메소드는 서블릿 컨테이너가 실행될 때 자동으로 호출된다. 이 부분이 Servlet API 3.0 이상 필요한 부분이다. 자세한 메커니즘은 Spring 의 WebApplicationInitialzer javadoc 을 참고한다.
  • 필요한 각종 설정을 Java code 로 구현한다. 예를 들면 DispatcherServlet 이나 CharacterEncodingFilter 같은 것들을 등록해주는 일이다. 알다시피 이것들은 기존에 web.xml 에 기술하던 것들이었다.

결론

  •  한번 java 로 스프링 설정을 해보고나면 xml 설정으로 돌아가기 힘들것이다.
  • 생각보다 어렵지 않다. 익숙하지 않아서 그렇지 예제 코드들을 보고 Spring 문서들을 읽어보면 할 수 있다.


출처 : http://willygwu2003.blog.me/130171432318

참고 : http://docs.spring.io/spring-batch/reference/html/

다음의 내용은 Spring Batch 2.0 Documentation의 중에 기본적인 내용을 요약한 것입니다.

 

1. Job Configuration 

 

1.1. Restartability

 

restartable='false'로 설정된 Job은 다시 재 실행 될 수 없습니다.

일반적으로 Job은 실패한 시점에서 다시 재 시작을 할 수 있으야 하나, 어떤 경우에는 어떤 시점에서 재 시작을 해야하는지 알 수 없는 경우가 있습니다. 이 경우에는 다음과 같이 설정하여 재시작을 원천적으로 막는 방법이 있습니다.

일반적으로, 재시작 할 수 없는 Job이 실패한 경우에는 관리자 또는 개발자에 의해 수동적으로 처리하게 됩니다.

<job id="footballJob" restartable="false">
    ...
</job>

1.2. JobExecutionListener

 

Job이 실행 lifecycle 이벤트를 받아 커스텀 로직을 정의할 필요가 있을때, 다음의 리스너를 등록하면 유용합니다.

Job이 성공 또는 실패에 상관없이 afterJob은 실행됩니다.

public interface JobExecutionListener {

    void beforeJob(JobExecution jobExecution);

    void afterJob(JobExecution jobExecution);

}
<job id="footballJob">
    <step id="playerload"          parent="s1" next="gameLoad"/>
    <step id="gameLoad"            parent="s2" next="playerSummarization"/>
    <step id="playerSummarization" parent="s3"/>
    <listeners>
        <listener ref="sampleListener"/>
    </listeners>
</job>

 

1.3. Configuring JobRepository

 

Database Repository 

 

<job-repository id="jobRepository"
    data-source="dataSource"
    transaction-manager="transactionManager"
    isolation-level-for-create="SERIALIZABLE"
    table-prefix="BATCH_"
 max-varchar-length="1000"
/>

 

In-Memory Repository

 

In-Memory Repository는 다음의 단점을 가지고 있음으로, 실제 Production 환경에서는 Database Repository가 주 설정 방법입니다.

 

- 재 시작된 경우 모든 상태 정보가 없어집니다.

- 같은 JobParameters 값을 가진 두개의 JobInstance가 동시에 실행된다는 보장이 없습니다.

- Multi-threaded Job 환경에서는 적합하지 않습니다.

<bean id="jobRepository" 
  class="org.springframework.batch.core.repository.support.MapJobRepositoryFactoryBean">
    <property name="transactionManager" ref="transactionManager"/>
</bean>

 

2. Step Configuration 

 

2.1. Commit Interval

 

다음은 각 트랜잭션에서 10개의 아이템이 처리되도록 설정하였습니다. 트랜잭션이 시작되면, ItemReader가 아이템을 일고, readCount가 10이되면 ItemWriter에 아이템 리스트가 전달됩니다. 그리고 트랜잭션이 컴밋됩니다.

commit-interval='10' 설정에 의해 'Chunk' 프로세스 단위가 결정됩니다.

<job id="sampleJob">
    <step id="step1">
        <tasklet>
            <chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
        </tasklet>
    </step>
</job>

 

2.2. Configuring Step Restart

 

'start-limit'은 Step이 실패 이후 재식작 가능한 수를 통제합니다. 다음의 예에서 Step은 단 한번 실행될 수 있고 다시 실행되면 exception이 발생하게 됩니다. default 값은 Interger.MAX_VALUE 입니다.

<step id="step1">
    <tasklet start-limit="1">
        <chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
    </tasklet>
</step>

 재시작 가능한 Job에서는, 처음의 Step 실행에서 성공 또는 실패에 상관없이 항상 재 실행이 될 필요가 있습니다. 디폴트로 'COMPLETED' 상태코드의 Step은 재 실행되더라도 Skip이 됩니다.

하지만 어떤 경우에는 이미 성공한 Step이라도 다시 실행 될 필요가 있는데, 이 경우에 다음과 같이 allow-start-if-complete='true'로 설정하면 됩니다.

 

<step id="step1">
    <tasklet allow-start-if-complete="true">
        <chunk reader="itemReader" writer="itemWriter" commit-interval="10"/>
    </tasklet>
</step>

2.3. Configuring Skip Logic

 

많은 경우에 에러가 발생하였을 경우 Step이 실패하는 것이 아니라, 단순히 Skip하고 계속 진행 될 필요가 있습니다.

다음의 예는 FlatFileReader가 사용되었고 프로세스 중에 FlatFileParseException이 발행한 아이템은 Skip되도록 설정되었습니다.

또한 skip-limit=10 설정에 의해 skipCount가 10에 도달하면 해당 step은 실패하게 됩니다.

<step id="step1">
   <tasklet>
      <chunk reader="flatFileItemReader" writer="itemWriter" 
             commit-interval="10" skip-limit="10">
         <skippable-exception-classes>
            <include class="org.springframework.batch.item.file.FlatFileParseException"/>
         </skippable-exception-classes>
      </chunk>
   </tasklet>
</step>

다음은 Skippable한 Exception 처리와 그렇지 못한 Exception을 설정한 예입니다.

<include> 태그의 Exception은 Skip이 가능한 Exception이며, <exclude> 태그안의 Exception이 발생한 경우에는 Step이 실패하게 됩니다.

<step id="step1">
    <tasklet>
        <chunk reader="flatFileItemReader" writer="itemWriter" 
               commit-interval="10" skip-limit="10">
            <skippable-exception-classes>
                <include class="java.lang.Exception"/>
                <exclude class="java.io.FileNotFoundException"/>
            </skippable-exception-classes>
        </chunk>
    </tasklet>
</step>

 

2.4. Configuring Retry Logic

 

단순이 Exception이 발생한 경우은 회복가능한 Exception가 불가능한 Exception이 있습니다.

다음과 같이 데이터베이스 LOCK에 의해 실패한 경우에는 해당 아이템에 대한 처리를 Skip하는 것이 아니라 잠시 이후 다시 처리하면 성공할 확율이 높습니다. 이러한 경우에 다음과 같은 설정이 유용합니다.

 

<step id="step1">
   <tasklet>
      <chunk reader="itemReader" writer="itemWriter" 
             commit-interval="2" retry-limit="3">
         <retryable-exception-classes>
            <include class="org.springframework.dao.DeadlockLoserDataAccessException"/>
         </retryable-exception-classes>
      </chunk>
   </tasklet>
</step>

 

2.5. Controlling Rollback

 

일반적으로 ItemWriter의 포르세스 중에 Exception이 발생한 경우에는 Rollback이 발생합니다.

하지만, 특정 시나리오에서는 롤백이 발생하지 않기를 원하는 경우가 있습니다. 이런 경우 <no-rollback-exception-classes> 설정이 유용합니다.

<step id="step1">
   <tasklet>
      <chunk reader="itemReader" writer="itemWriter" commit-interval="2"/>
      <no-rollback-exception-classes>
         <include class="org.springframework.batch.item.validator.ValidationException"/>
      </no-rollback-exception-classes>
   </tasklet>
</step>

 

 ItemReader는 기본적으로 읽어온 아이템을 버퍼에 저장합니다다. 따라서, rollback이 발생하더라도 다시 아이템을 ItemReader로 부터러 읽어 올 필요가 없습니다. 하지만 , 아이템을 JMS Queue로 부터 읽어 온 경우에는 rollback이 발생하면 아이템을 다시 Queue에서 읽어와야 합니다. 즉 버퍼를 사용하지 않도록 설정하는 요소가 is-reader-transactional-queue='true' 입니다.

 

<step id="step1">
    <tasklet>
        <chunk reader="itemReader" writer="itemWriter" commit-interval="2"
               is-reader-transactional-queue="true"/>
    </tasklet>
</step>

 

다음과 같이 트랜잭션 isolation, propagation, timeout 셋팅이 Step 레벨에서 가능합니다.

<step id="step1">
    <tasklet>
        <chunk reader="itemReader" writer="itemWriter" commit-interval="2"/>
        <transaction-attributes isolation="DEFAULT" 
                                propagation="REQUIRED" 
                                timeout="30"/>
    </tasklet>
</step>

 

2.6. Registering ItemStreams with Step

 

Step이 실패한 경우 재 시작할 필요가 있는데 이 경우 Repository로 부터 Step의 StepExecution 메티 정보를 참조하게 됩니다.

ItemStreams 인터페이스를 구현한 ItemReader, ItemProcessor, ItemWriter는 주기적으로 Step의 상태 정보를 저장 및 업데이트하게 됩니다.

하지만 커스텀 reader, processor, writer은 다음과 같이 명시적으로 ItemStreams로 등록되어야 합니다.

<step id="step1">
    <tasklet>
        <chunk reader="itemReader" writer="compositeWriter" commit-interval="2">
            <streams>
                <stream ref="fileItemWriter1"/>
                <stream ref="fileItemWriter2"/>
            </streams>
        </chunk>
    </tasklet>
</step>

<beans:bean id="compositeWriter" 
            class="org.springframework.batch.item.support.CompositeItemWriter">
    <beans:property name="delegates">
        <beans:list>
            <beans:ref bean="fileItemWriter1" />
            <beans:ref bean="fileItemWriter2" />
        </beans:list>
    </beans:property>
</beans:bean>

 

2.7. Configuring Listener

 

Step에 Listener 설정하는 방법

<step id="step1">
    <tasklet>
        <chunk reader="reader" writer="writer" commit-interval="10"/>
        <listeners>
            <listener ref="chunkListener"/>
        </listeners>
    </tasklet>
</step>

StepExecutionListener

 

Step Execution의 가장 일반적인 리스너로써, Step이 시작하기 전, 종료 이후 Notification을 처리 할 수 있습니다.

주로 정상적으로 종료되었는지 아니면 실패하였는지에 대한 후 처리를 위해 사용됩니다.

public interface StepExecutionListener extends StepListener {

    void beforeStep(StepExecution stepExecution);

    ExitStatus afterStep(StepExecution stepExecution);

}

 

ChunkListener

 

특정 트랜잭션안에서 'chunk' 프로세스 시작전과 완류 이후에 어떤 로직을 수행하는데 도움이 되는 리스너입니다.

public interface ChunkListener extends StepListener {

    void beforeChunk();

    void afterChunk();

}

 

ItemReadListener

 

ItemReader에 의해 아이템 읽기 프로세스 중에 에러가 발생한 경우, 로그와 같은 후 처리 하기에 유용한 리스너입니다.

public interface ItemReadListener<T> extends StepListener {
  
    void beforeRead();

    void afterRead(T item);
    
    void onReadError(Exception ex);

}

 

ItemProcessListener

 

ItemProcessor에 의해 아이템 비즈니스 로직 프로세스 중에 에러가 발생한 경우, 로그와 같은 후 처리 하기에 유용한 리스너입니다.

public interface ItemProcessListener<T, S> extends StepListener {

    void beforeProcess(T item);

    void afterProcess(T item, S result);

    void onProcessError(T item, Exception e);

}

 

ItemWriteListener 

 

ItemWriter에 의해 아이템 쓰기 프로세스 중에 에러가 발생한 경우, 로그와 같은 후 처리 하기에 유용한 리스너입니다.

 public interface ItemWriteListener<S> extends StepListener {

    void beforeWrite(List<? extends S> items);

    void afterWrite(List<? extends S> items);

    void onWriteError(Exception exception, List<? extends S> items);

}

 

SkipListener

 

ItemReader, ItemProcessor, ItemWriter 프로세스 중에 Skip된 아이템을 추적하는데 유용한 리스너입니다.

주로 Skip된 아이템을 로깅하는데 사용됩니다.

상기의 'Configuring Skip Logic'을 참조 바랍니다.

 

public interface SkipListener<T,S> extends StepListener {

    void onSkipInRead(Throwable t);

    void onSkipInProcess(T item, Throwable t);

    void onSkipInWrite(S item, Throwable t);

}

 

 

2.8 Controller Step Flow

 

Sequential Flow

 

'Step' 엘리먼트의 'next' 에트리뷰트를 적용함으로써 순차적인 Step 흐름을 설정할 수 있습니다.

단점으로는, 만약 stepA가 실패하면 전체 Job이 실패하게 됩니다.

<job id="job">
    <step id="stepA" parent="s1" next="stepB" />
    <step id="stepB" parent="s2" next="stepC"/>
    <step id="stepC" parent="s3" />
</job>

Conditional Flow 

 

다음의 조건적인 Step 흐름 설정으로 stepA의 'ExitStatus'가 'FAILED'인 경우 stepC로 이동하며, 그 외의 'EixtStatus' 결과값은 stepB로 그 흐름이 이동함을 말합니다.

<job id="job">
    <step id="stepA" parent="s1">
        <next on="*" to="stepB" />
        <next on="FAILED" to="stepC" />
    </step>
    <step id="stepB" parent="s2" next="stepC" />
    <step id="stepC" parent="s3" />
</job>
상기의 ExitStatus는 BatchStatus가 아님을 주목하시길 바랍니다. BatchStatus는 Job 및 Step의 상태를 기록하기 위한 JobExecution 및 StepExecutio의 property로, 그 값은 다음과 같습니다. - COMPLETED, STARTING, STARTED, STOPPING, STOPPED, FAILED, ABANDONED 또는 UNKNOWN. 반면, ExitStatus는 Step이 실행 종료 이후의 상태를 나타내는 코드로 다음 step으로 이동할 것인지 여기서 Job을 멈출 것인지를 판단하는 코드입니다.
public class SkipCheckingListener extends StepExecutionListenerSupport {

    public ExitStatus afterStep(StepExecution stepExecution) {
        String exitCode = stepExecution.getExitStatus().getExitCode();
        if (!exitCode.equals(ExitStatus.FAILED.getExitCode()) && 
              stepExecution.getSkipCount() > 0) {
            return new ExitStatus("COMPLETED WITH SKIPS");
        }
        else {
            return null;
        }
    }

}


+ Recent posts