2.4.1 EntityManagerFactory 등록
- JPA Persistence Context에 접근하고 엔티티 인스턴스를 관리하려면 JPA의 핵심 인터페이스인 EntityManager를 구현한 오브젝트가 필요
- EntityManager는 JPA에서 두가지 방식으로 관리
- 애플리케이션이 관리하는 EntityManager
- JavaEE와 JavaSE에서 모두 사용 가능
- 컨테이너가 관리하는 EntityManager
- 반드시 EntityManagerFactory를 빈으로 등록해줘야 함
LocalEntityManagerFactoryBean
- JPA 스펙의 JavaSE 기동 방식을 이용해 EntityManagerFactory를 생성
<bean id="emf" class="org.springframework.orm.jpa.LocalEntityManagerFactoryBean" />
- 이 빈은
PersistentProvider 자동 감지 기능을 이용해 프로바이더를 찾고 META-INF/persistence.xml에 담긴 정보를 활용해 EntityManagerFactory를 생성
- 이 방식은 JPA만을 사용하는 단순한 환경에서 적용 가능하지만 스프링에서 본격적으로 사용하기에는 많은 제약사항이 발생
- 가장 큰 단점은 스프링의 빈으로 등록한 DataSource를 사용 못함
- 스프링이 제공하는 바이트코드 위빙 기법도 적용할 수 없음
JavaEE 5 서버가 제공하는 EntityManagerFactory
- JPA는 JavaSE 환경보다는 JavaEE에서 서버가 제공하는 JPA 프로바이더를 통해 사용하는 것이 일반적인 방법
- 스프링에선 JNDI를 통해 서버가 제공하는 EntityManager와 EntityFactory를 제공 받음
- 서버의 JTA를 이용해 트랜젹솬 관리 기능을 활용
- 이 방식은 JPA를 지원하는 JavaEE 5이상의 서버에 배치하고 JPA 프로바이더와 서버가 요구하는 설정을 해뒀다고 전제
- 모든 JPA 기능은 서버와 JPA 퍼시스턴스 유닛 설정을 따름
<jee:jndi-lookup id="emf" jndi-name="persistence/myPersistenceUnit" />
- jndi-name은 JNDI로 등록된 퍼시스턴스 유닛의 이름을 지정
LocalContainerEntityManagerFactoryBean
- 스프링이 직접 제공하는 컨테이너 관리 EntityManager를 위한 EntityManagerFatocy를 생성
- JavaEE 서버에 배치하지 않아도 컨테이너에서 동작하는 JPA 기능 활용 가능
- 스프링이 제공하는 일관성 있는 데이터 액세스 기술 접근 방법도 적용 가능
<bean id="emf"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
</bean>
- 이 빈은 META-INF/persistence.xml을 참고해 퍼시스턴스 유닛과 이를 활용하는 EntityManagerFactory를 생성
- DB 연결정보는 persistence.xml 프로퍼티에 등록하지 않아도 됌
- 대신 스프링에 빈으로 DataSource를 JPA에서 사용
- META-INF/persistence.xml 파일 예시
<?xml version="1.0" encoding="UTF-8"?>
<persistence xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_2.0.xsd"
version="2.0">
<persistence-unit name="default">
<class>springbook.learningtest.spring.jpa.Member</class>
<exclude-unlisted-classes />
<properties>
<property name="eclipselink.waving" value="false" />
</properties>
</persistence-unit>
</persistence>
- 일부 프로퍼티 설정은 JPA 구현 제품마다 다름을 주의
- 자바오브젝트와 RDB 테이블 사이의 매핑과 변환을 위한 정보는 orm.xml과 같은 매핑 정보를 담은 파일에 정의하거나 애노테이션을 이용해 클래스 안에 정의 가능
@Entity
public class Member {
@Id
int id;
@Column(length=100)
String name;
@Column(nullable=false)
double point;
// 수정자(setter), 접근자(getter)
...
}
- LocalContainerEntityManagerFactoryBean에는 dataSource 외에 다음과 같은 프로퍼티 추가가 가능
persistenceUnitName
- persistence.xml에는 하나 이상의 퍼시스턴스 유닛이 정의될 수 있음
- persistence.xml에 다음과 같이 퍼시스턴스 유닛이 정의되어 있고, 이 설정을 따라 EntityManagerFactory 빈을 정의해야한다면 아래와 같이 추가
<!--persistence.xml-->
<persistence-unit name="subPersistenceUnit">
<!-- LocalContainerEntityManagerFactoryBean -->
<property name="persistenceUnitName" value="subPersistenceUnit" />
persistenceXmlLocation
- LocalContainerEntityManagerFactoryBean은 디폴트 위치인 META-INF에서 persistence.xml 파일을 찾음
- 일부 WAS에서는 META-INF/persistence.xml 파일을 자동으로 인식해서 서버가 관리하는 EntityManagerFactory와 충돌이 발생
- 따라서 스프링이 제공하는 컨테이너 관리 EntityManager를 적용하고 동시에 JPA 프로바이더를 제공하는 WAS에 배포할 경우 디폴트 위치에 persistence.xml을 넣는건 피해야 함
- persistenceXmlLocation의 프로퍼티에 파일 위치를 지정하여 변경
- 가장 간단한 방법은 파일의 이름을 spring-persistence.xml과 같이 변경하는 방법
<property name="persistenceXmlLocation" value="META-INF/spring-persistence.xml" />
jpaProperties, jpaPropertyMap
- EntityManagerFactory를 위한 프로퍼티를 지정할 때 사용
- jpaProperties는 <props>를 이용, jpaPropertyMap은 <map>을 이용해 정보를 입력
<!--persistence.xml-->
<persistence-unit name="default">
...
<properties>
<property name="eclipselink.waving" value="false" />
<properties>
</persistence-unit>
- persistence.xml 파일 대신 스프링의 LocalContainerEntityManagerFactoryBean 빈에 정의
<!--LocalContainerEntityManagerFactoryBean-->
<property name="jpaProperties">
<props>
<prop key="eclipselink.waving">false</prop>
</props>
</property>
jpaVendorAdapter
- JPA는 비슷한 기능이지만 벤더별로 다른 설정 프로퍼티를 사용해야 하는 경우가 많음
- 구현 벤더별로 다르게 지정되는 프로퍼티나 설정을 JpaVendorAdapter를 이용하면 스프링이 정의한 표준 프로퍼티를 이용해 지정 가능
- JPA는 코드상에서 데이터 액세스 로직을 작성할 때 자바오브젝트와 전용 쿼리 언어, Criteria 등을 이용
- 이런 API와 오브젝트 쿼리 등을 DB가 이해할 수 있는 SQL 언어로 전환해서 처리
- ORM 계층의 언어와 API가 아닌 DB SQL을 출력하고 싶을때 아래와 같이 옵션 설정
<property name="jpaVendorAdapter">
<bean class="org.springframework.orm.jpa.vendor.EclipseLinkJpaVendorAdapter">
<property name="showSql" value="true" />
</bean>
</property>
- showSql이라는 프로퍼티는 true, false로 지정 가능
- 밴더 어댑터에 맞는 설정으로 변환 (EclipseLink: eclipselink.logging.level=FINE, 등)
- 스프링 3.1에선 EclipseLink. Hibernate, OpenJpa, TopLink 네가지 벤더를 위한 JpaVendorAdapter를 지원
JpaVendorAdapter를 이용하면 showSql뿐 아니라 매핑 DDL을 자동생성해주는 generatedDdl과 DB 종류인 database, DB 플랫폼 정보인 databasePlatform 등을 지정
- 이 외에도 각 벤더의 JPA 구현에 따라 달라지는 다양한 정보를
EntityManagerFactory 생성을 위헤 제공
loadtimeWeaver
- JPA는 POJO 클래스를 ORM의 엔티티로 사용
- POJO방식의 단점은 오브젝트가 만들어 진 후엔 컨테이너가 직접 관리할 수 없다는 점
- 엔티티 사이에 관계도 JPA 인터페이스가 아닌 직접 POJO 오브젝트끼리 연결 되있음
- JPA는 그래서 단순한 자바 코드로 만들어진 엔티티 클래스의 바이트코드를 직접 조작해서 확장된 기능을 추가하는 방식을 이용
- 최적화와 그룹 페칭(fetching) 등의 고급 기능을 적용 가능
- 이미 컴파일된 클래스 바이트 코드를 조작해서 새로운 기능을 추가하는 것을 바이트코드 향상 기법이라고 함
- 클래스의 바이트 코드를 향상시키는 방법은 두가지
- 바이트코드를 빌드 중에 변경하는 것, JPA 벤더가 제공하는 바이트 코드 컴파일러가 필요하며 매번 빌드 작업중에 이 과정을 반드시 거쳐야 함
- 런타임 시에 클래스 바이트코드를 메모리에 로딩하면서 다이내믹하게 바이트코드를 변경해서 기능을 추가하는 방법
- 특별한 툴과 ANT 등을 이용한 번거로운 빌드 작업을 추가하지 않아도 되기 떄문에 애용되는 방법
- 런타임 시에 클래스를 로딩하면서 기능을 추가하는 것을 로드타임 위빙이라 하고 이런 기능을 가진 클래스를 로드타임 위버라고 부름
- 자바 5 이상에서는 JVM을 기동할 때 javaagent 옵션을 줘서 JVM이 로딩하는 모든 자바 클래스를 조작할 수 있는 기능을 넣었음
- JPA 구현 제품은 대부분 자바 에이전트를 이용해 로드타임 위버를 적용 가능
- 자바 에이전트는 간단해 보이지만 단점이 많음
- 엔티티 클래스는 몇십 개지만, JVM을 통해 로딩되는 모든 클래스를 일일이 다 확인
- 서버환경에서는 기동하는 JVM에 자바 에이전트를 설정해줘야하고, 운영정책과 관리 측면에서 부담을 줄 수 있어서 거부될 위험이 많음
- LocalContainerEntityManagerFactoryBean의 loadtimeWaver프로퍼티를 아래로 지정
<property name="loadTimeWeaber">
<bean class=
"org.springframework.instrument.classloading.InstrumentationLoadTimeWeaver" />
</property>
트랜잭션 매니저
- 컨테이너가 관리하는
EntityManager 방식에는 컨테이너가 제공하는 트랜잭션 매니저가 반드시 필요
- 스프링의 EntityManager를 사용하려면 적절한 트랜잭션 매니저 등록이 필요
- 스프링 JDBC는 트랜잭션 매니저가 없어도 동작, JDBC 자체가 자동 트랜잭션 모드를 갖고 있기 때문에 명시적으로 트랜잭션 관리를 안해도 됌
- 반면 JPA는 반드시 트랜잭션 안에서 동작하도록 설계
- LocalContainerEntityManagerFactoryBean을 통해 컨테이너 관리 EntityManger를 사용할 때는 트랜잭션 매니저를 다음과 같이 추가
<bean id="emf" class=
"org.springframework.orm.jpa.localContainerEntityManagerFactoryBean">
...
</bean>
<bean id="transactionManager"
class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="emf" />
</bean>
- JPA를 이용하는 DAO 코드는 스프링이 관리하는 트랜잭션 관리 기능을 이용 가능
- @Transactional이나 트랜잭션 AOP를 이용해서 트랜잭션 경계설정을 해주면 자동으로 JPA 트랜잭션을 시작하고 커밋하도록 만듬
JpaTransactionManager를 사용하면 DataSource를 공유하는 JDBC DAO와 트랜잭션 공유 가능 (서비스 계층의 코드가 JPA DAO와 JDBC DAO를 하나의 트랜잭션 안에서 사용 가능)
- JTA 트랜잭션을 이용하는 경우라면 JtaTransactionManager를 사용
2.4.2 EntityManager와 JpaTemplate
- JDBC나 iBatis를 사용한 템플릿처럼 JPA도 템플릿 방식을 지원
- 템플릿을 통해 JPA 코드가 스프링 트랜잭션과 동기화 할 수 있도록 하고 예외를 변환하는 등의 기능을 지원 받음
- 스프링에선 JpaTemplate뿐 아니라 JPA API를 직접 사용해 DAO를 작성 가능
- JPA의 핵심 프로그래밍 인터페이스는 EntityManager
- EntityManager 오브젝트를 가져올 수 있으면 JPA의 모든 기능을 이용 가능
JpaTemplate
- jdbcTemplate, SqlMapClientTemplate와 동일하게 템플릿 방식으로 JPA 코드를 작성 가능
- 장점 : 템플릿을 이용하면 반복 작업을 줄여주고 예외 변환같은 편리한 기능을 제공
- 단점 : 데이터 액세스 기술이 직접 제공하는 API 대신 템플릿의 메소드와 콜백을 사용
- 스프링에선 JPA를 사용할 때 JpaTemplate은 자주 사용 안함
- JPA API가 익숙치 않고 스프링 템플릿 방식이 편리할 때 JpaTemplate를 사용 권장
public class MemberTemplateDao {
private JpaTemplate jpaTemplate;
@Autowired
public void init(EntityManager emf) {
jpaTemplate = new JpaTemplate(emf);
}
}
JpaDaoSupport 클래스를 상속해서 DAO를 만들면 JpaTemplate 생성코드 생략 가능JpaTemplate 필요할 떄는 getJpaTemplate()를 이용
JpaTemplate를 사용할 떄는 기본적으로 JpaCallback 인터페이스의 doInJpa()에 필요한 작업을 입력
DoinJpa()는 파라미터로 EntityManager를 전달, 이를 통해 모든 JPA 기능을 사용가능
List<Member> ms = templateDao.jpaTemplate.execute(new JpaCallback<List<Member>>() {
public List<Member> doInJpa(EntityManager entityManager) throws
PersistenceException {
return entityManager.createQuery("select m from Member m".getResultList();
}
});
- JpaTemplate은 콜백 오브젝트 없이도 간단한 메소드를 이용해 EntityManager가 제공하는 대부분의 기능을 사용 가능하게 해줌
Member m = new Member(1, "Spring", 8.9);
jpaTemplate.persist(m);
Member m2 = templateDao.jpaTemplate.find(Member.class, 1);
애플리케이션 관리 EntityManager와 @PersistenceUnit
- 애플리케이션 코드가 관리하는 EntityManager는 JavEE환경과 JavaSE 모두 사용 가능
- EntityManager는 EntityManagerFactory가 있다면 다음과 같이 직접 생성
EntityManager em = entityManagerFactory.createEntityManager();
- 컨테이너가 관리하지 않는 EntityManager이므로 트랜잭션은 다음과같이 사용
em.getTransaction().begin();
Member m = new Member(1, "Spring", 7.8);
em.persist(m);
Long count = em.createQuery("select count(m) from Member m", Long.class)
.getSingleResult();
em.getTransaction().commt();
@Autowired, @Resource
- EntityManagerFactory를 빈으로 등록하는 것은 LocalContainerEntityManagerFactoryBean을 이용하는 방법과 JNDI를 통해서 서버가 제공하는 EntityManagerFactory를 가져와 빈으로 사용하는 방법 두가지가 존재
- 어떤 방법이든 EntityManagerFactory는 빈으로 등록되므로 @Autowired, @Resource 또는 일반적인 스프링 DI방식으로 DAO 사용 가능
public class MemberDao {
@Autowired EntityManagerFactory emf;
public void addMember(Member member) {
EntityManager em = emf.createEntityManager();
...
}
}
- EntityManagerFactory는 EntityManager를 만들 수 있는 createEntityManager()를 제공
@PersistenceUnit
- 스프링의 DI 방식 대신 JPA 표준 스팩에 나온 방식을 이용
- javax.persistence 패키지의 @PersistenceUnit 애노테이션을 사용
public class MemberDao {
@PersistenceUnit EntityManagerFacotry emf;
@Autowired를 쓸때와 다른점은 스프링 의존도가 전혀 없는 순수한 JPA 코드라는 점
- JavaEE 컨테이너에서 EntityManagerFactory를 제공받아 사용하는 전형적인 JPA 코드
- @PersistenceUnit에 의해 주입되려면 애노테이션을 이용한 의존관계 설정이 가능한 컨테이너이거나, XML에 반드시 다음 설정이 필요
<context:annotation-config />
- 내부적으론
<context:annotation-config />에 의해 자동등록되는 PersistenceAnnotationBeanPostProcessor 후처리기가 @PersistenceUnit에 대한 DI 담당
- EntityManagerFactory를 직접 이용하는 방법은 실제로 자주 사용 안함
- EntityManagerFactory 인터페이스에서 제공하는 QueryBuilder, Metamodal, PersistenceUnitUtil을 가져오거나 캐시나 프로퍼티 정보를 참조할 필요가 있을때 사용
컨테이너 관리 EntityManager와 @PersistenceContext
- DAO가 컨테이너로부터 EntityManager를 직접 주입받으려면 JPA의 @PersistenceContext을 사용
- EntityManager는 스프링의 빈으로 등록하지 않음, 따라서 스프링 DI 방법으론 주입X
- 스프링에서는 JavaEE 컨테이너가 관리하는 EntityManager를 주입받는 방법을 스프링 애플리케이션의 코드에도 동일하게 사용 가능
- 표준 스펙을 따르는 프로그래밍 모델을 스프링에서도 동일하게 사용 가능해짐
- 인스턴스 변수에서 한번 DI 받아놓고 여러 스레드에서 사용할 수 있는 이유
- @PersistenceContext로 주입받은 EntityManager는 실제가 아니라 현재 진행 중인 트랜잭션에 연결되는 퍼시스턴스 컨텍스트를 갖는 일종의 프록시
- 하나의 오브젝트를 공유하는 듯하지만 각 스레드가 자신의 컨텍스트에 따라서 만들어진 독립적인 오브젝트를 사용하도록 연결해주는 기능을 가진 프록시를 이용하는 것
- 컨테이너가 관리하는 EntityManager도 이와 비슷한 방식으로 동작
- DAO에서 마치 하나의 EntityManager를 독점적으로 사용하듯이 접근하지만 실제로는 트랜잭션마다 다른 EntityManager 오브젝트를 사용
- JPA 스펙에 따르면 트랜잭션 스코프의 퍼시스턴스 컨텍스트는 JTA를 통해 트랜잭션 관리
- 스프링은 이를
JpaTransactionManager를 통해 관리하기 떄문에 JavaEE 서버의 트랜잭션 매니저와 JTA가 없어도 JPA의 장점인 @PersistenceContext를 통한 EntityManager 주입 가능
@PersistenceContext와 확장된 퍼시스턴스 컨텍스트
- @PersistenceCOntext를 이용해서 컨테이너가 관리하는 EntityManager를 주입
- 다만 type 앨리먼트의 값이 다름, type을
PersistenceContextType.EXTENDS로 지정
- 트랜잭션 스코프 대신 확장된 스코프를 갖는 EntityManager가 만들어짐
- JPA에서 확장된 퍼시스턴스 컨텍스트는 상태유지 세션빈에 바인딩되는 것을 의미
- 상태유지 세션빈은 사용자별로 독립적이며 장기간 보존되는 오브젝트를 생성
- 세션빈에 바인딩되는 EntityManager 역시 사용자별로 독립적으로 장기간 보존
- 상태를 가진 세션빈이나 장기간 지속되는 스코프 빈에만 싱글톤 빈으로 사용 가능
- 하나 이상의 오브젝트로 만들어지기 때문에 각각 독립적으로 EntityManager를 가질 수 있기 때문에 스코프 프록시를 사용할 필요가 없음
public class MemberDao {
@PersistenceContext(type=PersistenceContextType.EXTENDED) EntityManager em;
- 확장된 퍼시스턴스 컨텍스트는 싱글톤 빈에 적용하면 안되며, 상태를 가진 세션빈 또는 스프링 스코프 빈의 활용 방법을 잘 이해하고 이에 맞게 EntityManager 활용할때 추천
JPA 예외 반환
- JpaTemplate이 JPA API를 직접 사용하는 방법이 좀 더 나은 점은 JPA의 예외를 스프링의 데이터 액세스 예외 추상화 클래스 계층인 DataAccessException의 예외를 반환해준다는 점
- JPA의 API는 자동 예외 변환이 발생X, 런타임 예외를 발생시키기 떄문에 try/catch 블록이나 throws 선언이 필요 없음
- EntityManager와 같은 리소스 관리는 JPA 지원 컨테이너가 알아서 관리
JPA 예외 변환 AOP
- JPA API를 직접 이용하는 경우에도 JPA 예외를 스프링의 DataAccessException 전환 가능
- AOP를 이용해서 JPA API가 던지는 JPA 예외를 스프링의 예외로 전환하는 부가기능을 추가
- 이 방법을 이용하려면 다음 두가지 작업이 필요
@Repository
- 예외 변환이 필요한 DAO 클래스에 @Repository 애노테이션을 부여
- @Repositry는 @Component와 같은 자동인식을 위한 스테레오타입 애노테이션
- 빈 스캐너를 사용하지 않는 경우에도 @Repository를 DAO에 사용 가능
- @Repository가 붙은 DAO 클래스의 메소드는 AOP 예외 변환 기능이 부가될 빈으로 선정
PersistenceExceptionTranslationPostProcessor
- @Repository가 붙은 빈을 찾아 예외 변환 기능을 가진 AOP 어드바이스를 적용해주는 후처리기가 필요
- PersistenceExceptionTranslationPostProcessor를 빈으로 등록만 하면 사용 가능
<bean class=
"org.springframework.dao.annotation.PersistenceExceptionTranslationPostProcessor" />
- SQL 에러 코드를 직접 해석해서 일관된 의미의 예외로 변환해주는 JdbcTemplate의 예외 변환과는 달리, JPA의 예외 자체가 분류하기 힘든 예외로 던져주기 떄문에 해석해서 변환
- JPA 스펙이 정의하는 예외는 제한적, 대부분의 DB 예외는 벤더가 정의한 예외나 시스템 예외 등으로 포장돼서 전달되고 스프링은 포장된 예외의 코드 수준까지 분석해 변환하는 기능X
- JPA 실행 중에 발생한 DB 예외등은 JDBC와 다른 방식으로 전달, 패러다임이 다르기 떄문에 발생하는 근본적인 한계
JDBC/iBatis와 JPA/하이버네이트 같은 ORM은 프로그래밍이 근본적으로 많은 차이 존재- POJO 기반의 투명한 영속성이 적용된 ROM의 엔티티 오브젝트를 사용하는 서비스 계층 코드와 항상 명시적으로 특정 SQL을 실행하는 JDBC/iBatis의 서비스 계층 코드는 미묘하지만 분명한 차이가 존재
- 예외 변환을 통해 완벽하게 독립적인 예외를 기대할 순 없지만 JPA의 예외를 스프링의 예외로 변경하는 건 가치가 있는 작업
@Repository와 PersistenceExceptionTanslationPostProcessor를 이용한 예외 변환 AOP를 반드시 적용