스프링은 자바의 다양한 테이터 액세스 기술을 위한 트랜잭션 매니저를 제공해준다. 트랜잭션 매니저를 빈으로 등록하고 선언적인 방식의 트랜잭션 관리 기능에서 사용하게 한다. 트랜잭션 매니저 빈의 이름은 관례적으로 transactionManager를 사용한다. 트랜잭션 매니저를 참조하는 전용 태그에서는 참조 애트리뷰트에 이 이름이 디폴트로 선언되어 있기 때문에 생략할 수도 있다. 여러 개의 DB를 독립적으로 사용하지 않는 한 트랜잭션 매니저는 한 개만 사용할 수 있다.
그런데 DB는 하나지만 두 가지 이상의 데이터 액세스 기술을 동시에 사용하는 경우는 어떨까? 예를 들어 각각 JDBC와 iBatis로 만든 DAO를 동시에 사용한다거나, JPA와 JDBC 또는 하이버네이트와 iBatis를 함께 사용하는 경우는 어떨까? 여기서 함께 사용한다는 건, 두 개 이상의 기술을 사용해서 만든 DAO를 하나의 트랜잭션 안에서 사용한다는 뜻이다. 예를 들면 JPA DAO로 일부 엔티티-테이블을 업데이트하는 것과 JDBC DAO로는 복잡한 DB전용 쿼리를 사용해 데이터를 가져오는 것을 하나의 트랜잭션 안에서 진행시키고 싶을 수 있다.
물론 가능하면 애플리케이션의 데이터 액세스 기술과 방식은 한 가지로 통일하는 게 좋다. 하지만 때로는 두 가지 이상의 데이터 액세스 기술을 혼합해서 사용해야 할 경우도 없지 않다. 현재 사용하는 데이터 액세스 기술과는 다른 기술을 사용하는 다른 시스템에서 개발된 DAO를 가져와 사용하고 싶을 수도 있고, JPA나 하이버네이트를 기본적으로 사용하지만 DB전용 네이티브 SQL을 사용하고 싶은 경우도 있다. 물론 JPA나 하이버네이트에서도 일반 SQL을 사용해 쿼리를 작성할 수 있다. 하지만 SQL을 본격적으로 사용하려고 하면 iBatis나 스프링 JDBC를 사용하는 것이 편리하다.
스프링은 두 개 이상의 데이터 액세스 기술로 만든 DAO를 하나의 트랜잭션으로 묶어서 사용하는 방법을 제공한다. 물론 이때도 DB당 트랜잭션 매니저는 하나만 사용한다는 원칙은 바뀌지 않는다. 대신 하나의 트랜잭션 매니저가 여러 개의 데이터 액세스 기술의 트랜잭션 기능을 지원해주도록 만드는 것이다.
트랜잭션 매니저별 조합 기능 기술
트랜잭션 통합이 가능한 데이터 액세스 기술의 조합을 살펴보자. 트랜잭션 매니저는 하나만 사용되므로 각 트랜잭션 매니저별로 사용 가능한 기술을 알고 있으면 적절한 기술과 트랜잭션 매니저의 선택이 가능할 것이다.
DataSourceTransactionManager
DataSourceTransactionManager를 트랜잭션 매니저로 등록하면 JDBC와 iBatis 두 가지 기술을 함께 사용할 수 있다. 트랜잭션을 통합하려면 항상 동일한 DataSource를 사용해야 한다는 점을 잊지 말자
다음 그림은 두 가지 기술이 적용된 빈과 DataSource, 트랜잭션 매니저의 의존관계다. JDBC DAO와 iBatis DAO가 같은 DataSource를 사용하도록 만들어주기만 하면 된다. DataSourceTransactionManager 는 DataSource로부터 Connection 정보를 가져와 같은 DataSource를 사용하는 JDBC DAO와 iBatis DAO 작업에 트랜잭션 동기화 기능을 제공한다.
JpaTransactionManager
JPA 트랜잭션은 JPA API를 이용해 처리된다. 따라서 기본적으로는 JPA 단독으로 트랜잭션을 관리하게 된다. 그런데 스프링에서는 JPA의 EntityManagerFactory의 datasource 속성에 DataSource를 설정할 수 있다. 그리고 이 DataSource를 JDBC DAO나 iBatis DAO에서도 사용할 수 있다. 이렇게 같은 DataSource를 공유하게 해주면 JPA의 트랜잭션을 담당하는 JpaTransactionManager에 의해 세 가지 기술을 이용하는 DAO 작업을 하나의 트랜잭션으로 관리해줄 수 있다. JpaTransactionManager를 통해 JPA가 사용하는 트랜잭션을 같은 DataSource를 의존하고 있는 JDBC DAO와 iBatis DAO에 동기화해주는 것이다.
다음 그림은 JpaTransactionManager를 사용할 때 세 가지 기술의 DAO가 하나의 트랜잭션으로 동기화되는 구조를 나타낸다. JpaTransactionManager 는 직접 DataSource를 의존하고 있지는 않지만 EntityManagerFactory가 사용하는 DataSource를 이용해 트랜잭션 동기화를 해준다. 이 덕분에 같은 DataSource를 사용하는 JDBC, iBatis DAO와도 트랜잭션을 통합할 수 있는 것다.
HibernateTransactionManager
하이버네이트 DAO를 사용한다면 HibernateTransactionManager를 트랜잭션 매니저로 등록해야 한다. HibernateTransactionManager도 JpaTransactionManager와 동일한 방식을 이용해서 SessionFactory와 같은 DataSource를 공유하는 JDBC, iBatis DAO와 트랜잭션을 공유하게 해준다. 따라서 하이버네이트, JDBC, iBatis 세 가지 기술의 DAO를 통합해서 사용할 수 있다.
위 그림에서 EntityManagerFactory 대신 SessionFactory를, JpaTransactionManager 대신 HibernateTransactionManager 를 대입하면 그 구조와 의존관계를 파악할 수 있을 것이다.
JtaTransactionManager
서버가 제공하는 트랜잭션 서비스를 JTA를 통해 이용하면 모든 종류의 데이터 액세스 기술의 DAO가 같은 트랜잭션 안에서 동작하게 만들 수 있다. JTA는 같은 DB에 대해 다른 기술을 사용할 때뿐 아니라 다른 DB를 사용하는 DAO도 하나의 트랜잭션으로 묶어줄 수 있다. 가장 강력하고 편리한 기능이지만 JTA 서버환경을 구성해야 하고 서버의 트랜잭션 매니저와 XA를 지원하는 특별한 DataSource를 구성하는 등의 부가적인 준비작업이 필요하다.
단지 하나의 DB를 사용하는 여러 가지 기술의 트랜잭션을 통합하려고 한다면 JTA를 사용해야 할 이유는 없다. 반면에 하나 이상의 DB 또는 JMS와 같은 트랜잭션이 지원되는 서비스를 통합해서 하나의 트랜잭션으로 관리하려고 할 때는 JTA가 반드시 필요하다.
ORM과 비 ORM DAO를 함께 사용할 때의 주의사항
JPA나 하이버네이트 같은 엔티티 기반의 ORM 기술과 JDBC, iBatis 같은 SQL 기반의 비 ORM 기술을 함께 사용하고 하나의 트랜잭션으로 묶어서 사용하는 것은 기술적으로 볼 때 아무런 문제가 없다. 하지만 각 기술의 특징을 잘 이해하지 않으면 예상치 못한 오류를 만날 수 있다.
JPA와 JDBC를 사용해서 만든 아래 두개의 DAO가 있다고 해보자
1 2 3 4 5 6 7 8 9 10 11 12 |
public class MemberJpaDao { @PersistenceContext EntityManager entityManager; public void add(Member m) { entityManager.persist(m); } ... } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
public class MemberJdbcDao extends JdbcDaoSupport { SimpleJdbcInsert insert; protected void initTemplate(config) { insert = new SimpleJdbcInsert(getDataSource()).withTableName("member"); } public void add(Member m) { insert execute(new BeanPropertySqlParameterSource(m)); } public long count() { return getJdbcTemplate().queryForObject("select count(*) from member", Long.class).longValue(); } } |
각기 다른 기술을 사용하는 이 두 개의 DAO가 하나의 트랜잭션 안에서 동작하도록 설정해준다. 그리고 아래와 같은 코드를 트랜잭션 안에서 실행하면 어떤 결과가 나올지 예측해보자.
1 2 3 |
jdbcDao.add(new Member(1, "Spring", 1.2)); jpaDao.add(new Member(2, "jpa", 1.2)); int count = jdbcDao.count(); |
먼저 JDBC DAO를 사용해서 Member 를 추가하고, 다음은 JPA DAO를 이용해서 또 다른 Member를 추가했다. 그러고 나서 Member 테이블의 로우의 개수를 가져오는 쿼리를 이용해서 등록된 Member의 개수를 가져왔다. Member를 두 번 추가했으니 처음에 테이블이 비어 있었다면 당연히 count가 2가 돼야 한다. 하지만 이 코드를 실행해보면 count에 1이 들어 있음을 알게 된다. 같은 트랜잭션 안에서 동작하게 했고 각각 INSERT 문장을 실행하는 메소드를 호출했는데 왜 두 번 추가한 Member의 개수가 1이라고 나오는 것일까?
그 이유는 JPA와 같은 ORM과 JDBC API를 직접 사용하는 비 ORM의 특성이 다르기 때문이다.
JPA나 하이버네이트는 단순히 JDBC API를 간접적으로 실행해주는 방식이 아니다. 물론 JPA나 하이버네이트에서 새로 만든 오브젝트에 영속성을 부여해주면 결국 INSERT 문이 생성돼서 DB로 전달되기는 할 것이다. 하지만 영속성을 부여하는 persist()나 save() 같은 메소드를 호출한다고 바로 DB에 INSERT SQL이 전달되는 것이 아니다. JPA나 하이버네이트는 새로 등록된 오브젝트를 일단 엔티티 매니저나 세션에만 저장해 둔다. 엔티티 매니저나 세션을 1차 캐시라고도 부르기 때문에 이렇게 저장해두는 것을 캐싱한다고 말하기도 한다. 캐싱을 한다는 의미는 DB에 INSERT하는 것을 최대한 지연시킨다는 뜻이다. 일단 persist()로 등록했지만 트랜잭션이 끝나기 전에 다시 변경될 수도 있기 때문이다. 따라서 DB에 동기화가 필요한 시점, 예를 들어 트랜잭션의 종료되거나 등록된 엔티티가 반영돼야만 정상적인 결과가 나올 수 있는 쿼리가 실행될 때까지 실제 DB로 등록하는 것을 지연시키는 기법을 사용한다. 간단한 캐시이긴 하지만, 나름 성능 향샹을 가져올 수 있고 코드를 유연하게 만들 수 있는 유용한 방법이다.
문제는 이 때문에 MemberJpaDao의 add()에서 entityManager.persist()를 실행했다고 해도 바로 DB에는 INSERT문이 전달되지 않는다는 점이다. 단지 메모리의 캐시에 저장되어 있을 뿐이다. JPA입장에서는 작업이 모두 끝나고 트랜잭션이 커밋되는 순간 INSERT문을 만들어 DB에 저장을 시도할 것이다.
그런데 JDBC에서는 JPA의 그런 사정을 알지 못한다. 따라서 JDBC는 count() 메소드가 실행되면 그 순간 DB에 바로 조회용 SQL을 보내서 현재 테이블에 등록된 로우의 개수를 가져온다. 따라서 JPA의 캐시에만 있고 DB에는 반영되지 않은 두 번째 add()의 결과는 나타나지 않을 것이다.
원래 JPA나 하이버네이트는 JDBC등과 함께 사용하도록 설계된 것이 아니기 때문에 이런 문제가 발생한다. 따라서 ORM과 비 ORM 기술을 함께 사용할 때 상당히 주의를 기울여야 한다. DAO를 이용하는 서비스 계층의 코드는 사실 DAO가 어떤 기술로 만들어졌는지를 알지 못한다. 따라서 별 의심 없이 위와 같은 코드를 그냥 작성해보리기 쉽다.
그렇다면 이 문제를 어떻게 풀어야 할까?
해결방법은 한 가지 뿐이다. JPA나 하이버네이트의 1차 개시에 저장됐지만 DB에는 아직 반영되지 않은 엔티티가 있다면, 관련 테이블을 참조하는 JDBC DAO나 iBatis DAO를 바로 이용하면 안 된다. 따라서 JDBC DAO의 쿼리를 사용하려면 JPA나 하이버네이트의 1차 캐시의 내용을 먼저 DB에 반영해야 한다.
가장 단순한 해결책은 JPA의 저장이나 수정 작업을 한 후에는 강제로 캐시의 내용을 DB로 보내주는 EntityManager나 Session의 flush() 메소드를 사용하는 것이다. 아래 코드는 MemberJpaDao의 메소드를 이 방식으로 동작하도록 수정한 것이다.
1 2 3 4 5 6 7 8 9 10 11 12 13 |
public class MemberJpaDao { @PersistenceContext EntityManager entityManager; public void add(Member m) { entityManager.persist(m); entityManager.flush(); } ... } |
flush() 메소드는 현재 캐시의 내용을 즉시 DB에 반영한다. 따라서 JDBC DAO의 add() 메소드를 실행했을 때처럼 바로 INSERT 문이 DB로 전달되어 새로운 레코드가 추가된다. 이렇게 해두면 이후에 JDBC DAO에서 Member 테이블의 count() 쿼리를 실행해도 JPA에서 진행한 작업까지 모두 반영된 결과를 가져올 수 있다.
이렇게 add()나 merge() 등에서 항상 flush()를 사용하도록 만들면 간단히 문제를 해결할 수 있긴 하지만, 반면에 JPA나 하이버네이트 입장에서는 1차 캐시의 장점을 희생해야 한다. JDBC DAO를 함께 사용하지 않고 JPA만 사용하는 비즈니스 로직에서도 항상 flush()를 쓴다는 것은 손해다. 또, JPA나 하이버네이트에서는 persist()나 merge()를 명시적으로 호출하지 않아도 DB에서 가져온 엔티티 오브젝트는 필드를 수정하는 것만으로도 UPDATE가 일어나 수정이 된다는 문제가 있다. 이런 경우에도 의도적으로 flush()를 수행해줘야 한다. 따라서 코드가 지저분해질 뿐만 아니라, DAO의 사용 기술을 의식해서 서비스 계층의 코드를 만들어야 한다는 문제점도 있다.
또 다른 접근 방법은 JDBC의 DAO가 호출될 때 JPA나 하이버네이트의 캐시를 flush() 하도록 만들어 주는 것이다. AOP를 이용하면 JPA/하이버네이트 캐시의 flush()를 호출해주는 부가기능을 JDBC DAO에 간단히 추가해줄 수 있다. 이렇게 해두면 JDBC DAO를 사용하지 않고 JPA DAO만 이용하는 경우에는 JTA 캐시를 효과적으로 활용할 수 있고, JDBC DAO를 함께 사용할 때도 데이터의 정확성을 보장해줄 수 있다.
'IT와' 카테고리의 다른 글
[Spring 3 - Transaction] AOP 방식 - 프록시와 AspectJ (0) | 2021.10.19 |
---|---|
[Spring 3 - Transaction] 트랜잭션 속성 (0) | 2021.10.19 |
[Spring 3 - Transaction] JTA를 이용한 글로벌/분산 트랜잭션 (0) | 2021.10.19 |
[Spring 3 - 데이터 액세스 기술] 개요 및 공통 개념 (0) | 2021.10.19 |
ERWIN 7.3에 MySQL connection + ERWIN 에서 reverse engineer (0) | 2021.09.02 |
댓글