요약/번역/참조 : Before a JDBC operation, flush the Hibernate Session (includes TSE example code)

문제 상황

ORM을 사용하는 코드와 ORM을 사용하지 않는 코드(생 SQL)를 한 뭉탱이 한 트랜잭션으로 처리할 때, 데이터베이스에 있는 데이터를 사용할 수 없는 이슈가 발생할 수 있다.

섞어서 사용해야 하는 이유

많은 엔터프라이즈 애플리케이션에서 도메인 모델을 저장하고 (심지어 복잡한 도메인 모델을) 가져올 때 ORM을 사용하고 있다.

그렇다고해서 일반 SQL을 사용하는 것이 완전히 사라지진 않을 것이며, 다음의 경우에는 일반 SQL이 필요하다.
- 테스트 코드 : ORM 툴을 사용하여 객체를 DB에 넣었을 때, SQL을 사용하여 전체 레코드 수를 가져와서 비교하는 등의 테스트를 할 때 용이하다.
- 스토어드 프로시져 사용시 : 프로젝트에서 스토어드 프로시져를 사용하는 경우가 많았고 이것을 ORM과 혼용하길 원하는 경우가 많았다. 예를 들어, 새로운 객체들을 집어넣은 뒤에, 가용한 레코드들과 새로운 넣은 레코드들의 연관이 필요한 경우가 있다.(뭔소린지..흠;;)
- 상당히 많은 객체에 대한 함수 사용시 : 백만개의 주문을 true 에서 false로 변경할 때, 저자는 ORM보다 SQL을 선호한다고 합니다.(하긴 뭐 이게 더 편할 수도 있겠지..)

ORM과 SQL 혼용시 발생하는 이슈

DB는 비어있다고 가정하고, 다음의 수도Pseudo 코드를 보자.

start transaction

create part with name Bolt
associate with ORM engine (i.e. save using entity manager)

update part set stock = 15 where name='Bolt'

end transaction

위에서 두 수도 코드의 update문(SQL문)은 위에서 Bolt라는 부품을 Entity Manager에게 저장하라고 했는데도 불구하고 실패하게 됩니다.

Entity Manager가 저장을 요청한 순간에 부품 객체를 저장하는 것이 아니라 트랜잭션이 끝날 때 저장하기 때문입니다.

ORM의 이런 속성을 write-behind 라고 합니다. 이 개념때문에, 사용할 수 있을 것 같다고 생각한 순간인데도 실제로는 사용하지 못하는 경우가 발생합니다.(위 상황으로 유추할 때, write-behind는 DB에 접속하는 빈도수를 줄이기 위해 사용하는 기술 같습니다.)

올바른 해결책

단순하게 다음과 같이 위의 수도 코드를 두 개의 트랜잭션으로 나누는 것을 생각할 수 있습니다.

start transaction
create part with name Bolt
associate with ORM engine (i.e. save using entity manager)
end transaction

start transaction
update part set stock = 15 where name='Bolt'
end transaction

하지만 이 것은 틀린 답입니다. 원자성을 깨트렸습니다. (그렇다면, 위의 두 트랜잭션을 nested transaction으로 가지는 하나의 트랜잭션으로 묶으면 되지 않을까?)

올바른 해결책은 SQL 쿼리를 실행하기 전에, ORM 엔진이 변경사항을 DB에 저장하도록 하는 것입니다. JPA도 하이버네이트 처럼 이런 방법을 제공하는데, 이게 바로  Flushing 입니다. 이것을 사용하여 수도 코드를 다음과 같이 고칠 수 있습니다.

start transaction

create part with name Bolt
associate with ORM engine (i.e. save using entity manager)

*** flush

update part set stock = 15 where name='Bolt'

end transaction

해결책 적용하기

사용자 삽입 이미지수도 코드를 시퀀스 다이어그램으로 나타낸 것입니다.

자 이제 flush() 메소드를 추가해야 하는데, 어디에 추가해야 할까요? addPart() 메소드 안의 맨 뒤에 추가할까요? 아니면 updateStock() 메소드 안에서 UPDATE 문 바로 뒤에 둘 까요?

두 방법 다 안 좋습니다.
- addPart() 메소드 안에 넣는 방법은 write-behind 방식을 깨트립니다. 그러면 여러 개의 부품을 하나의 트랜잭션에서 추가할 때 최적화를 할 수 없습니다.
- updateStock() 메소드 안에 넣는 방법이 조금 더 나아보이겠지만, 만약 한 트랜잭션에 SQL 문이 여러 개면 어떨까요? flush()가 의미가 있나요?
사용자 삽입 이미지결론적으로 필요한 것은 세 가지(부품 추가, 부품 수정, 세션 플러시)인데, 이 요구 사항을 만족시키기 위해 코드를 수정할 수 있는 부분은 두 곳 뿐입니다. 바로 여기가 Aspect-Oriented 가 제 몫을 하는 지점입니다. AOP 기술은 코드를 추가할 수 있는 추가적인 공간을 제공합니다. 즉 별도의 모듈로 위의 요구사항을 해결할 수 있도록 해줍니다.

세 개의 요구사항을 각각의 모듈로 처리하기

새로운 부품 추가하기

private SessionFactory sessionFactory;

public void insertPart(Part p) {
        sessionFactory.getCurrentSession().save(p);
}

부품 가격 수정하기

private SimpleJdbcTemplate jdbcTemplate;

public void updateStock(Part p, int stock) {
        jdbcTemplate.update("update stock set stock = stock + ? where number=?",
                stock, p.getNumber());
}

세션 동기화하기

정리해보면, 'JDBC을 할 떄마다, 세션에 dirty 상태이면 flush해야한다.' 이것을 좀 더 정리하면, 'JDBC 작업을 수행하기 전에, 세션이 dirty 상태이면 flush 하라.' 이 것을 다음과 같이 언제, 어디서, 무엇을로 쪼갤 수 있습니다.

    * 언제: before
    * 어디서: a call to a JDBC operation
    * 무엇을: flush a dirty Hibernate session

이렇게 쪼개두면, AspectJ로 구현하기 쉽습니다. 이것을 AspectJ로 구현하면 다음과 같습니다.

public aspect HibernateStateSynchronizer {

        private SessionFactory sessionFactory;
      
        public void setSessionFactory(SessionFactory sessionFactory() {
                this.sessionFactory = sessionFactory;
        }

        pointcut jdbcOperation() :
                call(* org.springframework.jdbc.core.simple.SimpleJdbcTemplate.*(..));
              
        before() jdbcOperation() {
                Session session = sessionFactory.getCurrentSession();
                if (session.isDirty()) {
                        session.flush();
                }
        }
}

생각해 볼 것

먼저, 위의 Aspect는 SimpleJdbcTemplate의 모든 메소드 호출에 적용을 했는데, 특정 애노테이션이 붙은 메소드에만 적용할 수 있도록 포인트컷을 수정할 수 있습니다. (예) execution(@JdbcOperation *(..))

두 번째로 생각해 볼 것은 가용한 하이버네이트 세션이 없을 때 입니다. SessionFactory.getCurrentSession() 은 항상 새로운 세션을 생성해 줍니다. 그런데 SessionFactory 없거나, 세션이 아예 만들어지지 않았을 때에도 위의 Aspect가 동작하려면, 스프링에서 제공하는 SessionFactoryUtils 를 사용해서 세션을 가져오도록 할 수 있습니다. (어지럽...)

소스 코드

HibernateStateSynchronizer 는 AspectJ를 사용해서 구현했고, 간단하게 Spring AOP를 사용하여 구현할 수도 있습니다.

HibernateCarPartsInventoryTests 는 위의 수도 코드를 테스트 했고, aspect가 가용하면 테스트가 통과하고 가용하지 않다면 테스트는 실패합니다.

현재는 before 어드바이스를 주석처리 해놨기 때문에 테스트가 fail 할 것이고, pom.xml을 보면 Maven AspectJ 플러그인이 있는데, 이게 버전 충돌이 나지만 무시해도 됩니다.

ek21.zip
감상문

대단합니다. 어쩜 이리 깔끔하게 정리할 수 있는지, 사진도 멋있네요.(전 여자를 좋아합니다.) Spring AOP를 공부하면서 AspectJ로 살짝 공부하긴 했지만, 실제 언제 어떻게 Aspect를 적용할지는 많이 고민해보지를 않았었습니다. 그냥 막연하게 레퍼런스에 나와있는 정도의 로깅, 트랜잭션, 보안.. 그나마 이러한 것들도 이미 스프링에서 만들어서 제공해주고 있기 때문에 더더욱 별 고민이 없었죠. 이 글은 AOP 초딩같은 저에게 진짜 Aspect Orient Programming이 뭔지 한 수 보여주는 것 같습니다. Alef Arendsen 당케쉔.