데이터 액세스
참조 문서의 이 부분에서는 데이터 액세스 및 데이터 액세스 계층과 비즈니스 또는 서비스 계층 간의 상호 작용에 대해 다룹니다.
Spring의 포괄적인 트랜잭션 관리 지원에 대해 자세히 다룬 다음, Spring 프레임워크가 통합되는 다양한 데이터 액세스 프레임워크와 기술에 대해 자세히 다룹니다.
섹션 요약
트랜잭션 관리
포괄적인 트랜잭션 지원은 Spring 프레임워크를 사용해야 하는 가장 강력한 이유 중 하나입니다. Spring 프레임워크는 트랜잭션 관리를 위한 일관된 추상화를 제공하여 다음과 같은 이점을 제공합니다:
- JTA(Java Transaction API), JDBC, Hibernate, JPA(Java Persistence API) 등 다양한 트랜잭션 API 전반에 걸쳐 일관된 프로그래밍 모델.
- 선언적 트랜잭션 관리 지원.
- JTA와 같은 복잡한 트랜잭션 API보다 프로그래밍 방식의 트랜잭션 관리를 위한 더 간단한 API.
- Spring의 데이터 액세스 추상화와의 뛰어난 통합.
다음 섹션에서는 Spring 프레임워크의 트랜잭션 기능 및 기술에 대해 설명합니다:
- Spring 프레임워크의 트랜잭션 지원 모델의 장점에서는EJB 컨테이너 관리 트랜잭션(CMT) 대신 Spring 프레임워크의 트랜잭션 추상화를 사용하거나 독점 API를 통해 트랜잭션을 구동하도록 선택하는 이유에 대해 설명합니다.
- Spring 프레임워크 트랜잭션 추상화 이해에서는핵심 클래스에 대한 개요와 다양한 소스에서 DataSource 인스턴스를 구성하고 가져오는 방법을 설명합니다.
- 트랜잭션과 리소스 동기화에서는애플리케이션 코드가 리소스를 올바르게 생성, 재사용 및 정리하는 방법을 설명합니다.
- 선언적 트랜잭션관리에서는 선언적 트랜잭션 관리에 대한 지원을 설명합니다.
- 프로그래밍 방식 트랜잭션관리에서는 프로그래밍 방식(즉, 명시적으로 코딩된) 트랜잭션 관리에 대한 지원을 다룹니다.
- 트랜잭션 바운드 이벤트에서는 트랜잭션 내에서 애플리케이션 이벤트를 사용하는 방법을 설명합니다.
이 장에는 모범 사례,애플리케이션 서버 통합, 일반적인 문제에 대한 해결책에 대한 논의도 포함되어 있습니다.
Advantages of the Spring Framework’s Transaction Support Model
전통적으로 EE 애플리케이션 개발자는 트랜잭션 관리를 위해 글로벌 또는 로컬 트랜잭션이라는 두 가지 선택지를 가지고 있었지만, 두 가지 모두 심각한 한계가 있었습니다. 다음 두 섹션에서는 글로벌 및 로컬 트랜잭션 관리에 대해 검토한 다음 Spring 프레임워크의 트랜잭션 관리 지원이 글로벌 및 로컬 트랜잭션 모델의 한계를 어떻게 해결하는지에 대해 설명합니다.
Global Transactions
글로벌 트랜잭션을 사용하면 일반적으로 관계형 데이터베이스 및 메시지 큐와 같은 여러 트랜잭션 리소스로 작업할 수 있습니다. 애플리케이션 서버는 (부분적으로는 예외 모델 때문에) 번거로운 API인 JTA를 통해 글로벌 트랜잭션을 관리합니다. 또한, JTA UserTransaction은 일반적으로 JNDI에서 소싱해야 하므로 JTA를 사용하려면 JNDI도 사용해야 합니다. 글로벌 트랜잭션을 사용하면 일반적으로 애플리케이션 서버 환경에서만 JTA를 사용할 수 있기 때문에 애플리케이션 코드의 재사용 가능성이 제한됩니다.
이전에는 글로벌 트랜잭션을 사용하는 데 선호되는 방법은 EJB CMT(컨테이너 관리 트랜잭션)를 사용하는 것이었습니다. CMT는 선언적 트랜잭션 관리의 한 형태입니다(프로그래밍 트랜잭션 관리와는 구별됨). EJB CMT는 트랜잭션 관련 JNDI 조회의 필요성을 제거하지만, EJB 자체를 사용하려면 JNDI를 사용해야 합니다. 트랜잭션을 제어하기 위해 Java 코드를 작성할 필요성을 대부분 제거하지만 전부는 아닙니다. 중요한 단점은 CMT가 JTA 및 애플리케이션 서버 환경에 묶여 있다는 것입니다. 또한 EJB에서 비즈니스 로직을 구현하기로 선택한 경우에만 사용할 수 있습니다(또는 최소한 트랜잭션 EJB 파사드 뒤에). 일반적으로 EJB의 단점이 너무 커서, 특히 선언적 트랜잭션 관리를 위한 강력한 대안이 있는 상황에서 이는 매력적인 제안이 아닙니다.
Local Transactions
로컬 트랜잭션은 JDBC 연결과 관련된 트랜잭션과 같이 리소스별로 다릅니다. 로컬 트랜잭션은 사용하기 더 쉬울 수 있지만 여러 트랜잭션 리소스에서 작동할 수 없다는 큰 단점이 있습니다. 예를 들어, JDBC 연결을 사용하여 트랜잭션을 관리하는 코드는 글로벌 JTA 트랜잭션 내에서 실행할 수 없습니다. 애플리케이션 서버는 트랜잭션 관리에 관여하지 않기 때문에 여러 리소스에서 정확성을 보장하는 데 도움을 줄 수 없습니다. (대부분의 애플리케이션이 단일 트랜잭션 리소스를 사용한다는 점에 주목할 필요가 있습니다.) 또 다른 단점은 로컬 트랜잭션이 프로그래밍 모델을 침범한다는 것입니다.
Spring Framework’s Consistent Programming Model
Spring은 글로벌 트랜잭션과 로컬 트랜잭션의 단점을 해결합니다. 애플리케이션 개발자는 어떤 환경에서도 일관된 프로그래밍 모델을 사용할 수 있습니다. 코드를 한 번만 작성하면 다양한 환경에서 다양한 트랜잭션 관리 전략의 이점을 누릴 수 있습니다. Spring 프레임워크는 선언적 트랜잭션 관리와 프로그래밍 방식의 트랜잭션 관리를 모두 제공합니다. 대부분의 사용자는 선언적 트랜잭션 관리를 선호하며, 대부분의 경우 이를 권장합니다.
프로그래매틱 트랜잭션 관리를 사용하면 개발자는 모든 기본 트랜잭션 인프라에서 실행할 수 있는 Spring 프레임워크 트랜잭션 추상화로 작업합니다. 선언적 모델을 선호하는 개발자는 일반적으로 트랜잭션 관리와 관련된 코드를 거의 또는 전혀 작성하지 않으므로 Spring 프레임워크 트랜잭션 API나 다른 트랜잭션 API에 의존하지 않습니다.
Spring 프레임워크의 트랜잭션 관리 지원은 엔터프라이즈 Java 애플리케이션에 애플리케이션 서버가 필요한 시점에 대한 기존의 규칙을 바꿉니다.
특히, EJB를 통한 선언적 트랜잭션에만 애플리케이션 서버가 필요하지 않습니다. 실제로 애플리케이션 서버에 강력한 JTA 기능이 있더라도 Spring 프레임워크의 선언적 트랜잭션이 EJB CMT보다 더 강력한 성능과 더 생산적인 프로그래밍 모델을 제공한다고 판단할 수 있습니다.
일반적으로 애플리케이션이 여러 리소스에서 트랜잭션을 처리해야 하는 경우에만 애플리케이션 서버의 JTA 기능이 필요하며, 이는 많은 애플리케이션에서 요구되는 사항은 아닙니다. 많은 고급 애플리케이션은 대신 확장성이 뛰어난 단일 데이터베이스(예: Oracle RAC)를 사용합니다. 독립형 트랜잭션 관리자(예:Atomikos 트랜잭션)도 다른 옵션입니다. 물론 JMS(Java Message Service) 및 JCA(Jakarta EE Connector Architecture)와 같은 다른 애플리케이션 서버 기능이 필요할 수도 있습니다.
Spring 프레임워크는 애플리케이션을 완전히 로드된 애플리케이션 서버로 확장할 시기를 선택할 수 있도록 해줍니다. EJB CMT 또는 JTA를 사용하는 유일한 대안이 로컬 트랜잭션(예: JDBC 연결)으로 코드를 작성하는 것이었고, 해당 코드를 글로벌 컨테이너 관리 트랜잭션 내에서 실행해야 하는 경우 막대한 재작업에 직면해야 했던 시대는 지났습니다. Spring 프레임워크에서는 코드가 아닌 구성 파일의 일부 빈 정의만 변경하면 됩니다.
Understanding the Spring Framework Transaction Abstraction
Spring 트랜잭션 추상화의 핵심은 트랜잭션 전략의 개념입니다. 트랜잭션 전략은 트랜잭션 매니저, 특히 명령형 트랜잭션 관리를 위한org.springframework.transaction.PlatformTransactionManager 인터페이스와 반응형 트랜잭션 관리를 위한org.springframework.transaction.ReactiveTransactionManager 인터페이스에 의해 정의됩니다. 다음 목록은PlatformTransactionManager API의 정의를 보여줍니다:
public interface PlatformTransactionManager extends TransactionManager {
TransactionStatus getTransaction(TransactionDefinition definition) throws TransactionException;
void commit(TransactionStatus status) throws TransactionException;
void rollback(TransactionStatus status) throws TransactionException;
}
이것은 주로 서비스 공급자 인터페이스(SPI)이지만 애플리케이션 코드에서프로그래밍 방식으로 사용할 수 있습니다.PlatformTransactionManager는 인터페이스이므로 필요에 따라 쉽게 모킹하거나 스터브할 수 있습니다. 이 인터페이스는 JNDI와 같은 조회 전략에 묶여 있지 않습니다.PlatformTransactionManager 구현은 Spring 프레임워크 IoC 컨테이너의 다른 객체(또는 빈)처럼 정의됩니다. 이러한 이점만으로도 Spring 프레임워크 트랜잭션은 JTA로 작업할 때에도 가치 있는 추상화입니다. 트랜잭션 코드를 직접 사용할 때보다 훨씬 더 쉽게 테스트할 수 있습니다.
다시 말하지만, Spring의 철학에 따라 PlatformTransactionManager 인터페이스의 메서드 중 하나에서 던질 수 있는 TransactionException은 체크되지 않습니다(즉, java.lang.RuntimeException 클래스를 확장합니다). 트랜잭션 인프라 장애는 거의 항상 치명적입니다. 드물지만 애플리케이션 코드가 트랜잭션 장애로부터 실제로 복구할 수 있는 경우에도 애플리케이션 개발자는 TransactionException을 포착하고 처리하도록 선택할 수 있습니다. 중요한 점은 개발자가강제로 그렇게 할 필요가 없다는 것입니다.
GetTransaction(...) 메서드는트랜잭션 정의 매개변수에 따라 트랜잭션 상태 객체를 반환합니다. 반환된 트랜잭션 상태는 새 트랜잭션을 나타낼 수도 있고, 현재 호출 스택에 일치하는 트랜잭션이 있는 경우 기존 트랜잭션을 나타낼 수도 있습니다. 이 후자의 경우의 의미는 Jakarta EE 트랜잭션 컨텍스트와 마찬가지로 TransactionStatus가 실행 스레드와 연관되어 있다는 것입니다.
Spring 프레임워크 5.2부터 Spring은 반응형 유형 또는 Kotlin 코루틴을 사용하는 반응형 애플리케이션을 위한 트랜잭션 관리 추상화도 제공합니다. 다음 목록은org.springframework.transaction.ReactiveTransactionManager에 의해 정의된 트랜잭션 전략을 보여줍니다:
public interface ReactiveTransactionManager extends TransactionManager {
Mono<ReactiveTransaction> getReactiveTransaction(TransactionDefinition definition) throws TransactionException;
Mono<Void> commit(ReactiveTransaction status) throws TransactionException;
Mono<Void> rollback(ReactiveTransaction status) throws TransactionException;
}
반응형 트랜잭션 관리자는 주로 서비스 공급자 인터페이스(SPI)이지만 애플리케이션 코드에서 프로그래밍 방식으로 사용할 수도 있습니다. ReactiveTransactionManager는 인터페이스이므로 필요에 따라 쉽게 모킹하거나 스터브할 수 있습니다.
트랜잭션 정의 인터페이스는 다음을 지정합니다:
- 전파: 일반적으로 트랜잭션 범위 내의 모든 코드는 해당 트랜잭션에서 실행됩니다. 그러나 트랜잭션 컨텍스트가 이미 존재할 때 트랜잭션 메서드가 실행되는 경우 동작을 지정할 수 있습니다. 예를 들어, 기존 트랜잭션에서 코드가 계속 실행되거나(일반적인 경우), 기존 트랜잭션이 일시 중단되고 새 트랜잭션이 생성될 수 있습니다. Spring은 EJB CMT에서 익숙한 모든 트랜잭션 전파 옵션을 제공합니다. Spring에서 트랜잭션 전파의 의미에 대해 알아보려면 트랜잭션 전파를 참조하세요.
- 격리: 이 트랜잭션이 다른 트랜잭션의 작업으로부터 격리되는 정도입니다. 예를 들어, 이 트랜잭션이 다른 트랜잭션에서 커밋되지 않은 쓰기를 볼 수 있나요?
- 타임아웃: 이 트랜잭션이 시간 초과되어 기본 트랜잭션 인프라에 의해 자동으로 롤백되기 전까지 실행되는 시간입니다.
- 읽기 전용 상태: 코드가 데이터를 읽지만 수정하지 않을 때 읽기 전용 트랜잭션을 사용할 수 있습니다. 읽기 전용 트랜잭션은 최대 절전 모드를 사용하는 경우와 같은 일부 경우에 유용한 최적화가 될 수 있습니다.
이러한 설정은 표준 트랜잭션 개념을 반영합니다. 필요한 경우 트랜잭션 격리 수준 및 기타 핵심 트랜잭션 개념을 설명하는 리소스를 참조하세요. 이러한 개념을 이해하는 것은 Spring 프레임워크 또는 트랜잭션 관리 솔루션을 사용하는 데 필수적입니다.
트랜잭션 상태 인터페이스는 트랜잭션 코드가 트랜잭션 실행을 제어하고 트랜잭션 상태를 쿼리할 수 있는 간단한 방법을 제공합니다. 개념은 모든 트랜잭션 API에 공통적이므로 익숙할 것입니다. 다음 목록은트랜잭션 상태 인터페이스를 보여줍니다:
public interface TransactionStatus extends TransactionExecution, SavepointManager, Flushable {
@Override
boolean isNewTransaction();
boolean hasSavepoint();
@Override
void setRollbackOnly();
@Override
boolean isRollbackOnly();
void flush();
@Override
boolean isCompleted();
}
Spring에서 선언적 트랜잭션 관리를 선택하든 프로그래밍 방식의 트랜잭션 관리를 선택하든 관계없이 올바른 TransactionManager 구현을 정의하는 것은 절대적으로 중요합니다. 일반적으로 종속성 주입을 통해 이 구현을 정의합니다.
트랜잭션 관리자 구현은 일반적으로 작동하는 환경에 대한 지식이 필요합니다: JDBC, JTA, 최대 절전 모드 등입니다. 다음 예제는 로컬 PlatformTransactionManager 구현을 정의하는 방법을 보여줍니다(이 경우 일반 JDBC 사용)
다음과 유사한 빈을 생성하여 JDBC 데이터 소스를 정의할 수 있습니다:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}" />
<property name="url" value="${jdbc.url}" />
<property name="username" value="${jdbc.username}" />
<property name="password" value="${jdbc.password}" />
</bean>
그러면 관련 PlatformTransactionManager 빈 정의에DataSource 정의에 대한 참조가 있습니다. 다음 예제와 비슷해야 합니다:
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
Jakarta EE 컨테이너에서 JTA를 사용하는 경우, JNDI를 통해 얻은 컨테이너 DataSource를 Spring의 JtaTransactionManager와 함께 사용합니다. 다음 예제는 JTA 및 JNDI 조회 버전의 모습을 보여줍니다:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:jee="http://www.springframework.org/schema/jee"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/jee
https://www.springframework.org/schema/jee/spring-jee.xsd">
<jee:jndi-lookup id="dataSource" jndi-name="jdbc/jpetstore"/>
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager" />
<!-- other <bean/> definitions here -->
</beans>
컨테이너의 전역 트랜잭션 관리 인프라를 사용하기 때문에 JtaTransactionManager는 데이터 소스 (또는 다른 특정 리소스)에 대해 알 필요가 없습니다.
데이터소스 빈의 앞의 정의는 jee 네임스페이스의 <jndi-lookup/> 태그를 사용합니다. 자세한 내용은JEE 스키마를 참조하세요 |
JTA를 사용하는 경우 트랜잭션 관리자 정의는 JDBC, Hibernate JPA 또는 기타 지원되는 기술 등 사용하는 데이터 액세스 기술에 관계없이 동일하게 표시되어야 합니다. 이는 JTA 트랜잭션이 글로벌 트랜잭션이기 때문에 모든 트랜잭션 리소스를 포함할 수 있기 때문입니다 |
모든 Spring 트랜잭션 설정에서 애플리케이션 코드는 변경할 필요가 없습니다. 로컬 트랜잭션에서 전역 트랜잭션으로 또는 그 반대로 이동하는 경우에도 구성을 변경하는 것만으로 트랜잭션 관리 방식을 변경할 수 있습니다.
Hibernate Transaction Setup
다음 예제와 같이 Hibernate 로컬 트랜잭션을 쉽게 사용할 수도 있습니다. 이 경우 애플리케이션 코드에서 Hibernate 세션 인스턴스를 가져오는 데 사용할 수 있는 Hibernate LocalSessionFactoryBean을 정의해야 합니다.
DataSource 빈 정의는 이전에 표시된 로컬 JDBC 예제와 유사하므로 다음 예제에는 표시되지 않습니다.
(비-JTA 트랜잭션 관리자가 사용하는) DataSource가 JNDI를 통해 조회되고 Jakarta EE 컨테이너에 의해 관리되는 경우, 트랜잭션은 (Jakarta EE 컨테이너가 아닌) Spring 프레임워크가 관리하므로 비트랜잭션이어야 합니다 |
이 경우 txManager 빈은 HibernateTransactionManager 유형입니다. DataSourceTransactionManager에 DataSource에 대한 참조가 필요한 것과 같은 방식으로,HibernateTransactionManager에는 SessionFactory에 대한 참조가 필요합니다. 다음 예제에서는 세션팩토리와 txManager 빈을 선언합니다:
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
<value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=${hibernate.dialect}
</value>
</property>
</bean>
<bean id="txManager" class="org.springframework.orm.hibernate5.HibernateTransactionManager">
<property name="sessionFactory" ref="sessionFactory"/>
</bean>
Hibernate 및 Jakarta EE 컨테이너 관리 JTA 트랜잭션을 사용하는 경우, 다음 예제와 같이 JDBC에 대한 이전 JTA 예제에서와 동일한 JtaTransactionManager를 사용해야 합니다. 또한 트랜잭션 코디네이터와 연결 해제 모드 구성을 통해 Hibernate가 JTA를 인식하도록 하는 것이 좋습니다:
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
<value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=${hibernate.dialect}
hibernate.transaction.coordinator_class=jta
hibernate.connection.handling_mode=DELAYED_ACQUISITION_AND_RELEASE_AFTER_STATEMENT
</value>
</property>
</bean>
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
또는 동일한 기본값을 적용하기 위해 로컬 세션 팩토리 빈에 JtaTransactionManager를 전달할 수도 있습니다:
<bean id="sessionFactory" class="org.springframework.orm.hibernate5.LocalSessionFactoryBean">
<property name="dataSource" ref="dataSource"/>
<property name="mappingResources">
<list>
<value>org/springframework/samples/petclinic/hibernate/petclinic.hbm.xml</value>
</list>
</property>
<property name="hibernateProperties">
<value>
hibernate.dialect=${hibernate.dialect}
</value>
</property>
<property name="jtaTransactionManager" ref="txManager"/>
</bean>
<bean id="txManager" class="org.springframework.transaction.jta.JtaTransactionManager"/>
Synchronizing Resources with Transactions
이제 다양한 트랜잭션 관리자를 생성하는 방법과 트랜잭션에 동기화해야 하는 관련 리소스에 연결하는 방법(예: DataSourceTransactionManager에서JDBC 데이터 소스, HibernateTransactionManager에서 Hibernate SessionFactory 등)을 명확히 알 수 있을 것입니다. 이 섹션에서는 애플리케이션 코드에서 (직접 또는 간접적으로, JDBC, Hibernate 또는 JPA와 같은 지속성 API를 사용하여) 이러한 리소스가 올바르게 생성, 재사용 및 정리되도록 보장하는 방법에 대해 설명합니다. 또한 이 섹션에서는 관련 트랜잭션 관리자를 통해 트랜잭션 동기화가 (선택 사항으로) 트리거되는 방법에 대해서도 설명합니다.
High-level Synchronization Approach
선호되는 접근 방식은 Spring의 최상위 템플릿 기반 지속성 통합 API를 사용하거나 네이티브 리소스 팩토리 관리를 위해 트랜잭션 인식 팩토리 빈 또는 프록시와 함께 네이티브 ORM API를 사용하는 것입니다. 이러한 트랜잭션 인식 솔루션은 내부적으로 리소스 생성 및 재사용, 정리, 리소스의 선택적 트랜잭션 동기화 및 예외 매핑을 처리합니다. 따라서 사용자 데이터 액세스 코드는 이러한 작업을 처리할 필요 없이 상용구가 아닌 지속성 로직에만 집중할 수 있습니다. 일반적으로 네이티브 ORM API를 사용하거나 JdbcTemplate을 사용하여 JDBC 액세스를 위한 템플릿 접근 방식을 취합니다. 이러한 솔루션은 이 참조 문서의 후속 섹션에 자세히 설명되어 있습니다.
Low-level Synchronization Approach
DataSourceUtils (JDBC용), EntityManagerFactoryUtils (JPA용),SessionFactoryUtils (Hibernate용) 등과 같은 클래스는 더 낮은 수준에서 존재합니다. 애플리케이션 코드가 네이티브 지속성 API의 리소스 유형을 직접 처리하도록 하려는 경우, 이러한 클래스를 사용하여 적절한 Spring 프레임워크 관리 인스턴스를 얻고, 트랜잭션을 (선택적으로) 동기화하며, 프로세스에서 발생하는 예외가 일관된 API에 올바르게 매핑되도록 할 수 있습니다.
예를 들어, JDBC의 경우 DataSource에서 getConnection() 메서드를 호출하는 기존 JDBC 방식 대신 다음과 같이 Spring의org.springframework.jdbc.datasource.DataSourceUtils 클래스를 사용할 수 있습니다:
Connection conn = DataSourceUtils.getConnection(dataSource);
기존 트랜잭션에 이미 동기화(연결)된 연결이 있는 경우 해당 인스턴스가 반환됩니다. 그렇지 않으면 메서드 호출이 새 연결 생성을 트리거하고, 이 연결은 (선택 사항으로) 기존 트랜잭션에 동기화되어 동일한 트랜잭션에서 나중에 재사용할 수 있게 됩니다. 앞서 언급했듯이, 모든SQLException은 스프링 프레임워크의 선택되지 않은 DataAccessException 유형 계층 중 하나인 스프링 프레임워크 CannotGetJdbcConnectionException으로 래핑됩니다. 이 접근 방식은 SQLException에서 쉽게 얻을 수 있는 것보다 더 많은 정보를 제공하며 데이터베이스 간, 심지어 다른 지속성 기술 간에도 이식성을 보장합니다.
이 접근 방식은 Spring 트랜잭션 관리 없이도 작동하므로(트랜잭션 동기화는 선택 사항) 트랜잭션 관리를 위해 Spring을 사용하든 사용하지 않든 상관없이 사용할 수 있습니다.
물론 Spring의 JDBC 지원, JPA 지원 또는 최대 절전 모드 지원을 사용한 후에는 관련 API를 직접 사용하는 것보다 Spring 추상화를 통해 작업하는 것이 훨씬 더 행복하기 때문에 일반적으로 DataSourceUtils나 다른 헬퍼 클래스를 사용하지 않는 것을 선호합니다. 예를 들어, JDBC 사용을 간소화하기 위해 Spring JdbcTemplate 또는jdbc.object 패키지를 사용하는 경우 올바른 연결 검색이 백그라운드에서 수행되므로 특별한 코드를 작성할 필요가 없습니다.
TransactionAwareDataSourceProxy
가장 낮은 수준에는 TransactionAwareDataSourceProxy 클래스가 존재합니다. 이것은 대상 데이터소스에 대한 프록시로, 대상 데이터소스를 래핑하여 Spring 관리 트랜잭션에 대한 인식을 추가합니다. 이 점에서 Jakarta EE 서버에서 제공하는 트랜잭션 JNDI데이터소스와 유사합니다.
기존 코드를 호출하고 표준 JDBC DataSource 인터페이스 구현을 전달해야 하는 경우를 제외하고는 이 클래스가 필요하거나 사용하려는 경우가 거의 없을 것입니다. 이 경우 이 코드는 사용 가능하지만 Spring에서 관리하는 트랜잭션에 참여할 수 있습니다. 앞서 언급한 상위 수준 추상화를 사용하여 새 코드를 작성할 수 있습니다.
선언적 트랜잭션 관리
대부분의 Spring 프레임워크 사용자는 선언적 트랜잭션 관리를 선택합니다. 이 옵션은 애플리케이션 코드에 가장 적은 영향을 미치므로 비침입형 경량 컨테이너의 이상에 가장 부합합니다 |
Spring 프레임워크의 선언적 트랜잭션 관리는 Spring 측면 지향 프로그래밍(AOP)을 통해 가능합니다. 그러나 트랜잭션 측면 코드는 Spring 프레임워크 배포와 함께 제공되며 상용구 방식으로 사용될 수 있으므로 일반적으로 이 코드를 효과적으로 활용하기 위해 AOP 개념을 이해할 필요는 없습니다.
Spring 프레임워크의 선언적 트랜잭션 관리는 개별 메서드 수준까지 트랜잭션 동작(또는 그 부족)을 지정할 수 있다는 점에서 EJB CMT와 유사합니다. 필요한 경우 트랜잭션 컨텍스트 내에서 setRollbackOnly() 호출을 수행할 수 있습니다. 두 가지 유형의 트랜잭션 관리의 차이점은 다음과 같습니다:
- 스프링 프레임워크의 선언적 트랜잭션 관리는 JTA에 묶여 있는 EJB CMT와 달리 모든 환경에서 작동합니다. 구성 파일을 조정하여 JDBC, JPA 또는 Hibernate를 사용하여 JTA 트랜잭션 또는 로컬 트랜잭션과 함께 작동할 수 있습니다.
- Spring 프레임워크의 선언적 트랜잭션 관리는 EJB와 같은 특수 클래스뿐만 아니라 모든 클래스에 적용할 수 있습니다.
- Spring 프레임워크는 선언적롤백 규칙을 제공하는데, 이는 EJB에는 없는 기능입니다. 롤백 규칙에 대한 프로그래밍 방식과 선언적 지원이 모두 제공됩니다.
- Spring 프레임워크에서는 AOP를 사용하여 트랜잭션 동작을 사용자 정의할 수 있습니다. 예를 들어 트랜잭션 롤백의 경우 사용자 정의 동작을 삽입할 수 있습니다. 트랜잭션 조언과 함께 임의의 조언을 추가할 수도 있습니다. EJB CMT를 사용하면setRollbackOnly()를 제외하고는 컨테이너의 트랜잭션 관리에 영향을 줄 수 없습니다.
- Spring 프레임워크는 고급 애플리케이션 서버처럼 원격 호출을 통한 트랜잭션 컨텍스트의 전파를 지원하지 않습니다. 이 기능이 필요한 경우 EJB를 사용하는 것이 좋습니다. 그러나 일반적으로 트랜잭션이 원격 호출에 걸쳐 전파되는 것을 원하지 않으므로 이러한 기능을 사용하기 전에 신중하게 고려해야 합니다.
롤백 규칙의 개념이 중요합니다. 롤백 규칙을 사용하면 자동 롤백을 유발하는 예외(및 throw 가능 항목)를 지정할 수 있습니다. 이는 Java 코드가 아닌 구성에서 선언적으로 지정할 수 있습니다. 따라서 TransactionStatus 객체에서 setRollbackOnly( )를 호출하여 현재 트랜잭션을 롤백할 수도 있지만, 대부분의 경우 MyApplicationException이 항상 롤백을 발생시켜야 한다는 규칙을 지정할 수 있습니다. 이 옵션의 가장 큰 장점은 비즈니스 객체가 트랜잭션 인프라에 의존하지 않는다는 것입니다. 예를 들어, 일반적으로 Spring 트랜잭션 API나 다른 Spring API를 가져올 필요가 없습니다.
EJB 컨테이너의 기본 동작은 시스템 예외(일반적으로 런타임 예외)에서 트랜잭션을 자동으로 롤백하지만, EJB CMT는 애플리케이션 예외(즉, java.rmi.RemoteException 이외의 확인된 예외)에서 트랜잭션을 자동으로 롤백하지 않습니다. 선언적 트랜잭션 관리를 위한 Spring의 기본 동작은 EJB 규칙을 따르지만(롤백은 체크되지 않은 예외에서만 자동으로 수행됨), 이 동작을 사용자 정의하는 것이 유용할 때가 많습니다.
Spring 프레임워크의 선언적 트랜잭션 구현 이해하기
트랜잭션 어노테이션으로 클래스에 주석을 달고, @EnableTransactionManagement를 구성에 추가하고, 모든 것이 어떻게 작동하는지 이해하기를 기대하는 것만으로는 충분하지 않습니다. 더 깊은 이해를 돕기 위해 이 섹션에서는 트랜잭션 관련 이슈의 맥락에서 Spring 프레임워크의 선언적 트랜잭션 인프라의 내부 작동을 설명합니다.
Spring 프레임워크의 선언적 트랜잭션 지원과 관련하여 이해해야 할 가장 중요한 개념은 이 지원이AOP 프록시를 통해 활성화된다는 점과 트랜잭션 조언이 메타데이터(현재 XML 또는 주석 기반)에 의해 구동된다는 점입니다. AOP와 트랜잭션 메타데이터의 조합은 적절한 TransactionManager 구현과 함께 트랜잭션 인터셉터를 사용하여 메서드 호출을 중심으로 트랜잭션을 구동하는 AOP 프록시를 생성합니다.
Spring AOP는 AOP 섹션에서 다룹니다 |
Spring 프레임워크의 트랜잭션 인터셉터는 명령형 및 반응형 프로그래밍 모델을 위한 트랜잭션 관리를 제공합니다. 인터셉터는 메서드 반환 유형을 검사하여 원하는 트랜잭션 관리 방식을 감지합니다. Publisher 또는 Kotlin Flow (또는 그 하위 유형)와 같은 반응형 유형을 반환하는 메서드는 반응형 트랜잭션 관리에 적합합니다. Void를 포함한 다른 모든 반환 유형은 명령형 트랜잭션 관리를 위해 코드 경로를 사용합니다.
트랜잭션 관리 방식은 어떤 트랜잭션 관리자가 필요한지에 영향을 줍니다. 필수 트랜잭션에는 PlatformTransactionManager가 필요하고, 반응형 트랜잭션에는ReactiveTransactionManager 구현이 사용됩니다.
트랜잭션은 일반적으로PlatformTransactionManager가 관리하는 스레드 바운드 트랜잭션과 함께 작동하며, 현재 실행 스레드 내의 모든 데이터 액세스 작업에 트랜잭션을 노출합니다. 참고: 메서드 내에서 새로 시작된 스레드에는 전파되지 않습니다.
ReactiveTransactionManager에 의해 관리되는 반응형 트랜잭션은 스레드-로컬 속성 대신 Reactor 컨텍스트를 사용합니다. 결과적으로, 참여하는 모든 데이터 액세스 작업은 동일한 리액터 파이프라인의 동일한 리액터 컨텍스트 내에서 실행되어야 합니다.
ReactiveTransactionManager로 구성된 경우, 모든 트랜잭션으로 구분된 메서드는 리액티브 파이프라인을 반환할 것으로 예상됩니다. 무효 메서드나 일반 반환 유형은 해당 @Transactional 선언의transactionManager 속성을 통해 일반 PlatformTransactionManager와 연결해야 합니다.
|
다음 이미지는 트랜잭션 프록시에서 메서드를 호출하는 개념도를 보여줍니다:
선언적 트랜잭션 구현 예시
다음 인터페이스와 그에 수반되는 구현을 고려해 보세요. 이 예에서는 특정 도메인 모델에 집중하지 않고 트랜잭션 사용에 집중할 수 있도록Foo 및 Bar 클래스를 플레이스홀더로 사용합니다. 이 예제에서는 구현된 각 메서드의 본문에서 DefaultFooService 클래스가 UnsupportedOperationException인스턴스를 던진다는 사실이 유용합니다. 이러한 동작을 통해 트랜잭션이 생성된 후UnsupportedOperationException 인스턴스에 대한 응답으로 롤백되는 것을 볼 수 있습니다. 다음 목록은 FooService인터페이스를 보여줍니다:
// the service interface that we want to make transactional
package x.y.service;
public interface FooService {
Foo getFoo(String fooName);
Foo getFoo(String fooName, String barName);
void insertFoo(Foo foo);
void updateFoo(Foo foo);
}
다음 예제는 앞의 인터페이스의 구현을 보여줍니다:
package x.y.service;
public class DefaultFooService implements FooService {
@Override
public Foo getFoo(String fooName) {
// ...
}
@Override
public Foo getFoo(String fooName, String barName) {
// ...
}
@Override
public void insertFoo(Foo foo) {
// ...
}
@Override
public void updateFoo(Foo foo) {
// ...
}
}
FooService 인터페이스의 처음 두 메서드인 getFoo(String) 및getFoo(String, String)는 읽기 전용 시맨틱을 가진 트랜잭션의 컨텍스트에서 실행되어야 하고 다른 메서드인 insertFoo(Foo ) 및 updateFoo(Foo)는 읽기-쓰기 시맨틱을 가진 트랜잭션의 컨텍스트에서 실행되어야 한다고 가정해 보겠습니다. 다음 구성은 다음 몇 단락에서 자세히 설명합니다:
<!-- from the file 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the service object that we want to make transactional -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- the transactional advice (what 'happens'; see the <aop:advisor/> bean below) -->
<tx:advice id="txAdvice" transaction-manager="txManager">
<!-- the transactional semantics... -->
<tx:attributes>
<!-- all methods starting with 'get' are read-only -->
<tx:method name="get*" read-only="true"/>
<!-- other methods use the default transaction settings (see below) -->
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- ensure that the above transactional advice runs for any execution
of an operation defined by the FooService interface -->
<aop:config>
<aop:pointcut id="fooServiceOperation" expression="execution(* x.y.service.FooService.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceOperation"/>
</aop:config>
<!-- don't forget the DataSource -->
<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>
<!-- similarly, don't forget the TransactionManager -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- other <bean/> definitions here -->
</beans>
앞의 구성을 살펴보세요. 여기서는 서비스 객체인 fooService 빈을 트랜잭션으로 만들려고 한다고 가정합니다. 적용할 트랜잭션 시맨틱은 <tx:advice/> 정의에 캡슐화되어 있습니다. <tx:advice/> 정의는 " get으로 시작하는 모든 메서드는 읽기 전용 트랜잭션의 컨텍스트에서 실행되고, 다른 모든 메서드는 기본 트랜잭션 의미론으로 실행된다"로 읽힙니다. <tx:advice/> 태그의 트랜잭션관리자 속성은 트랜잭션을 구동할트랜잭션 관리자 빈의 이름(이 경우txManager 빈)으로 설정됩니다.
연결하려는 트랜잭션 매니저의 빈 이름에 트랜잭션 매니저라는 이름이 있는 경우 트랜잭션 조언(<tx:advice/>)에서 트랜잭션 매니저 속성을 생략할 수 있습니다. 연결하려는 트랜잭션 관리자 빈의 이름이 다른 이름인 경우에는 앞의 예에서와 같이 트랜잭션 관리자속성을 명시적으로 사용해야 합니다 |
<aop:config/> 정의는txAdvice 빈에서 정의한 트랜잭션 조언이 프로그램의 적절한 지점에서 실행되도록 합니다. 먼저, FooService 인터페이스에 정의된 모든 작업의 실행과 일치하는 포인트컷을 정의합니다(fooServiceOperation). 그런 다음 어드바이저를 사용하여 포인트컷을 txAdvice와 연결합니다. 결과는 fooServiceOperation이 실행될 때 txAdvice에 정의된 조언이 실행됨을 나타냅니다.
<aop:pointcut/> 요소 내에 정의된 표현식은 AspectJ 포인트컷 표현식입니다. Spring의 포인트컷 표현식에 대한 자세한 내용은 AOP 섹션을 참조하세요.
일반적인 요구 사항은 전체 서비스 계층을 트랜잭션으로 만드는 것입니다. 이를 위한 가장 좋은 방법은 서비스 계층의 모든 연산과 일치하도록 포인트컷 표현식을 변경하는 것입니다. 다음 예제는 그 방법을 보여줍니다:
<aop:config>
<aop:pointcut id="fooServiceMethods" expression="execution(* x.y.service.*.*(..))"/>
<aop:advisor advice-ref="txAdvice" pointcut-ref="fooServiceMethods"/>
</aop:config>
앞의 예에서는 모든 서비스 인터페이스가 x.y.service 패키지에 정의되어 있다고 가정합니다. 자세한 내용은 AOP 섹션을 참조하세요 |
이제 구성을 분석했으니 "이 모든 구성이 실제로 무엇을 하는 것일까?"라고 자문해 보셨을 것입니다
앞서 표시된 구성은 fooService 빈 정의에서 생성된 객체 주위에 트랜잭션 프록시를 만드는 데 사용됩니다. 프록시는 트랜잭션 조언과 함께 구성되므로 프록시에서 적절한 메서드가 호출되면 해당 메서드와 관련된 트랜잭션 구성에 따라 트랜잭션이 시작, 일시 중단, 읽기 전용으로 표시되는 등의 작업을 수행합니다. 앞서 표시된 구성을 테스트하는 다음 프로그램을 고려해 보세요:
public final class Boot {
public static void main(final String[] args) throws Exception {
ApplicationContext ctx = new ClassPathXmlApplicationContext("context.xml");
FooService fooService = ctx.getBean(FooService.class);
fooService.insertFoo(new Foo());
}
}
앞의 프로그램을 실행한 출력은 다음과 비슷해야 합니다(Log4J 출력과 DefaultFooService 클래스의insertFoo(..) 메서드가 던진 UnsupportedOperationException의 스택 추적은 명확성을 위해 잘라냈습니다):
<!-- the Spring container is starting up... -->
[AspectJInvocationContextExposingAdvisorAutoProxyCreator] - Creating implicit proxy for bean 'fooService' with 0 common interceptors and 1 specific interceptors
<!-- the DefaultFooService is actually proxied -->
[JdkDynamicAopProxy] - Creating JDK dynamic proxy for [x.y.service.DefaultFooService]
<!-- ... the insertFoo(..) method is now being invoked on the proxy -->
[TransactionInterceptor] - Getting transaction for x.y.service.FooService.insertFoo
<!-- the transactional advice kicks in here... -->
[DataSourceTransactionManager] - Creating new transaction with name [x.y.service.FooService.insertFoo]
[DataSourceTransactionManager] - Acquired Connection [org.apache.commons.dbcp.PoolableConnection@a53de4] for JDBC transaction
<!-- the insertFoo(..) method from DefaultFooService throws an exception... -->
[RuleBasedTransactionAttribute] - Applying rules to determine whether transaction should rollback on java.lang.UnsupportedOperationException
[TransactionInterceptor] - Invoking rollback for transaction on x.y.service.FooService.insertFoo due to throwable [java.lang.UnsupportedOperationException]
<!-- and the transaction is rolled back (by default, RuntimeException instances cause rollback) -->
[DataSourceTransactionManager] - Rolling back JDBC transaction on Connection [org.apache.commons.dbcp.PoolableConnection@a53de4]
[DataSourceTransactionManager] - Releasing JDBC Connection after transaction
[DataSourceUtils] - Returning JDBC Connection to DataSource
Exception in thread "main" java.lang.UnsupportedOperationException at x.y.service.DefaultFooService.insertFoo(DefaultFooService.java:14)
<!-- AOP infrastructure stack trace elements removed for clarity -->
at $Proxy0.insertFoo(Unknown Source)
at Boot.main(Boot.java:11)
반응형 트랜잭션 관리를 사용하려면 코드에서 반응형 타입을 사용해야 합니다.
Spring 프레임워크는 ReactiveAdapterRegistry를 사용하여 메서드 반환 유형이 반응형인지 여부를 결정합니다 |
다음 목록은 이전에 사용된 FooService의 수정된 버전을 보여 주지만 이번에는 코드가 반응형 유형을 사용합니다:
// the reactive service interface that we want to make transactional
package x.y.service;
public interface FooService {
Flux<Foo> getFoo(String fooName);
Publisher<Foo> getFoo(String fooName, String barName);
Mono<Void> insertFoo(Foo foo);
Mono<Void> updateFoo(Foo foo);
}
다음 예제는 이전 인터페이스의 구현을 보여줍니다:
package x.y.service;
public class DefaultFooService implements FooService {
@Override
public Flux<Foo> getFoo(String fooName) {
// ...
}
@Override
public Publisher<Foo> getFoo(String fooName, String barName) {
// ...
}
@Override
public Mono<Void> insertFoo(Foo foo) {
// ...
}
@Override
public Mono<Void> updateFoo(Foo foo) {
// ...
}
}
명령형과 반응형 트랜잭션 관리는 트랜잭션 경계 및 트랜잭션 속성 정의에 대해 동일한 의미를 공유합니다. 명령형 트랜잭션과 반응형 트랜잭션의 주요 차이점은 후자의 지연된 특성입니다. 트랜잭션 인터셉터는반환된 리액티브 타입을 트랜잭션 연산자로 장식하여 트랜잭션을 시작하고 정리합니다. 따라서 트랜잭션 리액티브 메서드를 호출하면 실제 트랜잭션 관리가 리액티브 타입의 처리를 활성화하는 구독 타입으로 이연됩니다.
반응형 트랜잭션 관리의 또 다른 측면은 프로그래밍 모델의 자연스러운 결과인 데이터 이스케이프와 관련이 있습니다.
명령형 트랜잭션의 메서드 반환 값은 메서드가 성공적으로 종료되면 트랜잭션 메서드에서 반환되므로 부분적으로 계산된 결과가 메서드 클로저에서 빠져나가지 않습니다.
반응형 트랜잭션 메서드는 계산을 시작하고 완료하겠다는 약속과 함께 계산 시퀀스를 나타내는 반응형 래퍼 타입을 반환합니다.
게시자는 트랜잭션이 진행 중일 때 데이터를 방출할 수 있지만 반드시 완료될 필요는 없습니다. 따라서 전체 트랜잭션의 성공적인 완료에 의존하는 메서드는 호출 코드에서 완료 및 버퍼 결과를 보장해야 합니다.
선언적 트랜잭션 롤백
이전 섹션에서는 애플리케이션에서 클래스(일반적으로 서비스 계층 클래스)의 트랜잭션 설정을 선언적으로 지정하는 방법에 대한 기본 사항을 설명했습니다. 이 섹션에서는 XML 구성에서 간단한 선언적 방식으로 트랜잭션의 롤백을 제어하는 방법에 대해 설명합니다. 트랜잭션 어노테이션을 사용하여 선언적으로 롤백 의미를 제어하는 방법에 대한 자세한 내용은@트랜잭션설정을 참조하세요.
Spring 프레임워크의 트랜잭션 인프라에 트랜잭션의 작업이 롤백될 것임을 나타내는 권장 방법은 트랜잭션의 컨텍스트에서 현재 실행 중인 코드에서 예외를 던지는 것입니다. Spring 프레임워크의 트랜잭션 인프라 코드는 호출 스택을 버블업하면서 처리되지 않은 예외를 포착하고 트랜잭션을 롤백 대상으로 표시할지 여부를 결정합니다.
기본 구성에서 Spring 프레임워크의 트랜잭션 인프라 코드는 런타임에 체크되지 않은 예외의 경우에만 롤백을 위해 트랜잭션을 표시합니다. 즉, 던져진 예외가 RuntimeException의 인스턴스 또는 하위 클래스인 경우입니다.오류 인스턴스도 기본적으로 롤백을 초래합니다.
스프링 프레임워크 5.2부터 기본 구성은 '실패'를 반환할 때 트랜잭션 롤백을 트리거하는 Vavr의 Try 메서드에 대한 지원도 제공합니다. 이를 통해 함수형 오류를 처리하고 실패 시 트랜잭션이 자동으로 롤백되도록 할 수 있습니다. Vavr의 Try에 대한 자세한 내용은 Vavr 공식 문서를 참조하세요. 다음은 트랜잭션 메서드에서 Vavr의 Try를 사용하는 방법의 예시입니다:
@Transactional
public Try<String> myTransactionalMethod() {
// If myDataAccessOperation throws an exception, it will be caught by the
// Try instance created with Try.of() and wrapped inside the Failure class
// which can be checked using the isFailure() method on the Try instance.
return Try.of(delegate::myDataAccessOperation);
}
Spring 프레임워크 6.1부터 CompletableFuture(및 일반 Future) 반환값을 특별 처리하여 원래 메서드에서 반환될 때 예외적으로 완료된 경우 해당 핸들에 대한 롤백을 트리거합니다. 이는 실제 메서드 구현이 CompletableFuture 서명(런타임에 @Async 처리에 의해 프록시 호출에 대한 실제 비동기 핸들에 자동 적용)을 준수해야 하는 @Async 메서드를 대상으로 하며 예외를 다시 던지는 대신 반환된 핸들에 노출되는 것을 선호합니다:
@Transactional @Async
public CompletableFuture<String> myTransactionalMethod() {
try {
return CompletableFuture.completedFuture(delegate.myDataAccessOperation());
}
catch (DataAccessException ex) {
return CompletableFuture.failedFuture(ex);
}
}
트랜잭션 메서드에서 던져지는 체크된 예외는 기본 구성에서 롤백되지 않습니다. 롤백 규칙을 지정하여 확인된 예외를 포함하여 트랜잭션을 롤백할 예외 유형을 정확하게 구성할 수 있습니다.
롤백 규칙
롤백 규칙은 지정된 예외가 발생할 때 트랜잭션을 롤백할지 여부를 결정하며, 규칙은 예외 유형 또는 예외 패턴을 기반으로 합니다.
롤백 규칙은 롤백 대상 및 롤백 대상 없음속성을 통해 XML에서 구성할 수 있으며, 이를 통해 규칙을 패턴으로 정의할 수 있습니다. 트랜잭션을 사용하는 경우 롤백 규칙은 각각 예외 유형 또는 패턴에 따라 규칙을 정의할 수 있는 rollbackFor/noRollbackFor 및 rollbackForClassName/noRollbackForClassName 속성을 통해 구성할 수 있습니다.
예외 유형으로 롤백 규칙을 정의하면 해당 유형이 던져진 예외의 유형과 그 상위 유형과 일치하는 데 사용되므로 유형 안전성을 제공하고 패턴을 사용할 때 발생할 수 있는 의도하지 않은 일치를 방지할 수 있습니다. 예를 들어, jakarta.servlet.ServletException.class의 값은 jakarta.servlet.ServletException 유형 및 그 하위 클래스의 던져진 예외와만 일치합니다.
롤백 규칙이 예외 패턴으로 정의된 경우, 패턴은 정규화된 클래스 이름 또는 예외 유형에 대한 정규화된 클래스 이름의 하위 문자열( Throwable의 하위 클래스여야 함)이 될 수 있으며, 현재 와일드카드는 지원되지 않습니다. 예를 들어, "jakarta.servlet.Serv letException" 또는 "ServletException "의 값은 jakarta.servlet.ServletException 및 그 하위 클래스와 일치합니다.
|
다음 XML 스니펫은롤백-포 속성을 통해 예외 패턴을 제공하여 확인된 애플리케이션별 예외 유형에 대한 롤백을 구성하는 방법을 보여 줍니다:
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="get*" read-only="true" rollback-for="NoProductInStockException"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
예외가 발생할 때 트랜잭션을 롤백하지 않으려면 '롤백 없음' 규칙을 지정할 수도 있습니다. 다음 예는 Spring 프레임워크의 트랜잭션 인프라가 처리되지 않은 InstrumentNotFoundException이 발생하더라도 수반되는 트랜잭션을 커밋하도록 지시합니다:
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="updateStock" no-rollback-for="InstrumentNotFoundException"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
Spring 프레임워크의 트랜잭션 인프라가 예외를 포착하고 구성된 롤백 규칙을 참조하여 트랜잭션을 롤백할지 여부를 결정할 때 가장 강력한 일치 규칙이 승리합니다. 따라서 다음 구성의 경우 InstrumentNotFoundException 이외의 모든 예외가 발생하면 관련 트랜잭션이 롤백됩니다:
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="*" rollback-for="Throwable" no-rollback-for="InstrumentNotFoundException"/>
</tx:attributes>
</tx:advice>
프로그래밍 방식으로 필요한 롤백을 지정할 수도 있습니다. 이 프로세스는 간단하지만 상당히 침습적이며 코드를 Spring 프레임워크의 트랜잭션 인프라에 긴밀하게 연결합니다. 다음 예제는 프로그래밍 방식으로 필수 롤백을 표시하는 방법을 보여줍니다:
public void resolvePosition() {
try {
// some business logic...
} catch (NoProductInStockException ex) {
// trigger rollback programmatically
TransactionAspectSupport.currentTransactionStatus().setRollbackOnly();
}
}
가능하면 선언적 접근 방식을 사용하여 롤백할 것을 강력히 권장합니다. 프로그래밍 방식의 롤백은 꼭 필요한 경우 사용할 수 있지만, 깔끔한 POJO 기반 아키텍처를 달성하는 데는 역행하는 사용법이 될 수 있습니다.
빈마다 다른 트랜잭션 의미 구성하기
여러 개의 서비스 계층 객체가 있고 각 객체에 완전히 다른 트랜잭션 구성을 적용하려는 시나리오를 생각해 보세요. 서로 다른 포인트컷 및조언 참조 속성 값을 가진 별개의 <aop:advisor/> 요소를 정의하면 됩니다.
비교를 위해 먼저 모든 서비스 계층 클래스가 루트 x.y.service 패키지에 정의되어 있다고 가정해 보겠습니다. 해당 패키지(또는 하위 패키지)에 정의된 클래스의 인스턴스이고 이름이 Service로 끝나는 모든 빈이 기본 트랜잭션 구성을 갖도록 하려면 다음과 같이 작성할 수 있습니다:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<aop:config>
<aop:pointcut id="serviceOperation"
expression="execution(* x.y.service..*Service.*(..))"/>
<aop:advisor pointcut-ref="serviceOperation" advice-ref="txAdvice"/>
</aop:config>
<!-- these two beans will be transactional... -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<bean id="barService" class="x.y.service.extras.SimpleBarService"/>
<!-- ... and these two beans won't -->
<bean id="anotherService" class="org.xyz.SomeService"/> <!-- (not in the right package) -->
<bean id="barManager" class="x.y.service.SimpleBarManager"/> <!-- (doesn't end in 'Service') -->
<tx:advice id="txAdvice">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<!-- other transaction infrastructure beans such as a TransactionManager omitted... -->
</beans>
다음 예는 완전히 다른 트랜잭션 설정으로 두 개의 별개의 빈을 구성하는 방법을 보여줍니다:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<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>
<!-- this bean will be transactional (see the 'defaultServiceOperation' pointcut) -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- this bean will also be transactional, but with totally different transactional settings -->
<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>
<!-- other transaction infrastructure beans such as a TransactionManager omitted... -->
</beans>
<tx:advice/> 설정
이 섹션에서는 <tx:advice/> 태그를 사용하여 지정할 수 있는 다양한 트랜잭션 설정을 요약합니다. 기본 <tx:advice/> 설정은 다음과 같습니다:
- 전파 설정은 필수입니다.
- 격리 수준은 기본값입니다.
- 트랜잭션은 읽기-쓰기입니다.
- 트랜잭션 시간 제한은 기본값으로 기본 트랜잭션 시스템의 기본 시간 제한을 사용하거나 시간 제한이 지원되지 않는 경우 시간 제한이 없습니다.
- 모든 런타임 예외는 롤백을 트리거하고, 체크된 예외는 롤백을 트리거하지 않습니다.
이러한 기본 설정을 변경할 수 있습니다. 다음 표에는 <tx :advice/> 및 <tx:attributes/> 태그 내에 중첩된 <tx:method/> 태그의 다양한 속성이 요약되어 있습니다:
표 1. <tx:method/> 설정속성필수?기본값설명
name | 예 | 트랜잭션 속성을 연결할 메소드 이름입니다. 와일드카드(*) 문자를 사용하여 동일한 트랜잭션 속성 설정을 여러 메소드에 연결할 수 있습니다(예: get*, handle*, on*Event 등). | |
전파 | 아니요 | 필수 | 트랜잭션 전파 동작. |
격리 | 아니요 | 기본값 | 트랜잭션 격리 수준. REQUIRED 또는 REQUIRES_NEW의 전파 설정에만 적용됩니다. |
timeout | 아니요 | -1 | 트랜잭션 시간 초과(초). REQUIRED 또는 REQUIRES_NEW 전파에만 적용됩니다. |
읽기 전용 | 아니요 | false | 읽기-쓰기 대 읽기 전용 트랜잭션. REQUIRED 또는 REQUIRES_NEW에만 적용됩니다. |
롤백 대상 | 아니요 | 롤백을 트리거하는 쉼표로 구분된 예외 인스턴스 목록입니다. 예: com.foo.MyBusinessException,ServletException. | |
no-rollback-for | No | 롤백을 트리거하지 않는 예외 인스턴스의 쉼표로 구분된 목록입니다. 예: com.foo.MyBusinessException,ServletException. |
Using @Transactional
트랜잭션 구성에 대한 XML 기반 선언적 접근 방식 외에도 어노테이션 기반 접근 방식을 사용할 수 있습니다. Java 소스 코드에서 트랜잭션 의미를 직접 선언하면 해당 코드에 훨씬 더 가깝게 선언할 수 있습니다. 트랜잭션으로 사용되어야 하는 코드는 어차피 거의 항상 그런 식으로 배포되기 때문에 과도한 결합의 위험은 크지 않습니다.
표준 jakarta.transaction.Transactional 어노테이션은 Spring의 자체 어노테이션을 대체하는 드롭인 방식으로도 지원됩니다. 자세한 내용은 JTA 설명서를 참조하세요 |
트랜잭션 어노테이션을 사용할 때 제공되는 사용 편의성은 다음 텍스트에서 설명하는 예제를 통해 가장 잘 설명됩니다. 다음 클래스 정의를 고려해 보세요:
// the service class that we want to make transactional
@Transactional
public class DefaultFooService implements FooService {
@Override
public Foo getFoo(String fooName) {
// ...
}
@Override
public Foo getFoo(String fooName, String barName) {
// ...
}
@Override
public void insertFoo(Foo foo) {
// ...
}
@Override
public void updateFoo(Foo foo) {
// ...
}
}
위와 같이 클래스 수준에서 사용되는 어노테이션은 선언 클래스의 모든 메서드(및 그 하위 클래스)에 대한 기본값을 나타냅니다. 또는 각 메서드에 개별적으로 주석을 달 수도 있습니다. Spring이 트랜잭션으로 간주하는 메서드에 대한 자세한 내용은메서드 가시성을참조하십시오. 클래스 수준 어노테이션은 클래스 계층 구조의 상위 조상 클래스에는 적용되지 않으며, 이러한 시나리오에서는 하위 클래스 수준 어노테이션에 참여하기 위해 상속된 메서드를 로컬로 다시 선언해야 합니다.
위와 같은 POJO 클래스가 Spring 컨텍스트에서 빈으로 정의된 경우, @Configuration 클래스의 @EnableTransactionManagement어노테이션을 통해 빈 인스턴스를 트랜잭션으로 만들 수 있습니다. 자세한 내용은자바독을참조하세요.
XML 구성에서는 <tx:annotation-driven/> 태그가 비슷한 편의를 제공합니다:
<!-- from the file 'context.xml' -->
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<!-- this is the service object that we want to make transactional -->
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- enable the configuration of transactional behavior based on annotations -->
<!-- a TransactionManager is still required -->
<tx:annotation-driven transaction-manager="txManager"/>
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<!-- (this dependency is defined somewhere else) -->
<property name="dataSource" ref="dataSource"/>
</bean>
<!-- other <bean/> definitions here -->
</beans>
빈 인스턴스를 트랜잭션으로 만드는 줄입니다. |
연결하려는 트랜잭션 관리 자의 빈 이름에트랜잭션 관리자라는 이름이 있는 경우 <tx:annotation-driven/>태그에서 트랜잭션 관리자 속성을 생략할 수 있습니다. 종속성 주입하려는 TransactionManager 빈에 다른 이름이 있는 경우 앞의 예제에서와 같이 트랜잭션-관리자 속성을 사용해야 합니다 |
반응형 트랜잭션 메서드는 다음 목록에서 볼 수 있듯이 명령형 프로그래밍 배열과 달리 반응형 반환 유형을 사용합니다:
// the reactive service class that we want to make transactional
@Transactional
public class DefaultFooService implements FooService {
@Override
public Publisher<Foo> getFoo(String fooName) {
// ...
}
@Override
public Mono<Foo> getFoo(String fooName, String barName) {
// ...
}
@Override
public Mono<Void> insertFoo(Foo foo) {
// ...
}
@Override
public Mono<Void> updateFoo(Foo foo) {
// ...
}
}
리액티브 스트림 취소 신호와 관련하여 반환된 퍼블리셔에 대한 특별한 고려 사항이 있다는 점에 유의하세요. 자세한 내용은 "트랜잭션 오퍼레이터 사용" 아래의취소 신호섹션을 참조하세요.
프록시 모드에서 메서드 가시성 및 @Transactional
트랜잭션 어노테이션은 일반적으로 공개 가시성을 가진 메서드에 사용됩니다. 6.0부터는 클래스 기반 프록시에서 기본적으로 보호되거나 패키지 가시성을 가진 메서드도 트랜잭션으로 설정할 수 있습니다. 인터페이스 기반 프록시의 트랜잭션 메서드는 항상 공용이어야 하며 프록시된 인터페이스에 정의되어 있어야 한다는 점에 유의하세요. 두 종류의 프록시 모두 프록시를 통해 들어오는 외부 메소드 호출만 가로채게 됩니다.
서로 다른 종류의 프록시에서 메서드 가시성을 일관되게 처리하려면(5.3까지는 기본값이었음) publicMethodsOnly를 지정하는 것을 고려하세요:
스프링 테스트 컨텍스트 프레임워크는 기본적으로 비공개 @Transactional 테스트 메서드도 지원합니다. 예제는 테스트 장의 트랜잭션 관리를참조하세요.
|
인터페이스 정의, 인터페이스의 메서드, 클래스 정의 또는 클래스의 메서드에 @Transactional 어노테이션을 적용할 수 있습니다. 그러나 @Transactional 어노테이션이 있다는 것만으로는 트랜잭션 동작을 활성화할 수 없습니다. 트랜잭션 어노테이션은 해당 런타임 인프라에서 해당 메타데이터를 사용하여 트랜잭션 동작으로 적절한 빈을 구성하는 데 사용할 수 있는 메타데이터일 뿐입니다. 앞의 예제에서 <tx:annotation-driven/> 요소는 런타임에 실제 트랜잭션 관리를 켭니다.
Spring 팀은 5.0부터 인터페이스 기반 및 대상 클래스 프록시에서 작동하더라도 인터페이스의 어노테이션 메서드에 의존하지 말고@Transactional 어노테이션으로 구체적인 클래스의 메서드에 어노테이션할 것을 권장합니다. Java 어노테이션은 인터페이스에서 상속되지 않으므로 AspectJ 모드 사용 시 인터페이스 선언 어노테이션은 여전히 위빙 인프라에서 인식되지 않으므로 어스펙트가 적용되지 않습니다. 결과적으로 트랜잭션 어노테이션이 조용히 무시될 수 있습니다: 롤백 시나리오를 테스트할 때까지 코드가 "작동"하는 것처럼 보일 수 있습니다 |
프록시 모드(기본값)에서는 프록시를 통해 들어오는 외부 메서드 호출만 차단됩니다. 즉, 자체 호출(사실상 대상 객체 내의 메서드가 대상 객체의 다른 메서드를 호출하는 것)은 호출된 메서드가 @Transactional로 표시되어 있어도 런타임에 실제 트랜잭션으로 이어지지 않습니다. 또한 예상되는 동작을 제공하려면 프록시가 완전히 초기화되어야 하므로 초기화 코드에서 이 기능에 의존해서는 안 됩니다(예: @PostConstruct 메서드) |
자체 호출도 트랜잭션으로 래핑될 것으로 예상되는 경우 AspectJ 모드(다음 표의 모드 속성 참조)를 사용하는 것을 고려하세요. 이 경우 애초에 프록시가 없습니다. 대신, 대상 클래스는 모든 종류의 메서드에서 @Transactional 런타임 동작을 지원하도록 우븐(즉, 바이트 코드가 수정됨)됩니다.
트랜잭션 관리자 | N/A( 트랜잭션 관리 컨피규레이터 자바독 참조) | 트랜잭션 관리자 | 사용할 트랜잭션 관리자의 이름입니다. 앞의 예제에서처럼 트랜잭션 매니저의 이름이 트랜잭션Manager가 아닌 경우에만 필요합니다. |
mode | mode | proxy | 기본 모드(프록시)는 Spring의 AOP 프레임워크(프록시 의미론에 따라 앞에서 설명한 대로 프록시를 통해 들어오는 메서드 호출에만 적용)를 사용하여 어노테이션된 빈을 프록시 처리합니다. 대신 대체 모드(aspectj)는 영향을 받는 클래스를 Spring의 AspectJ 트랜잭션 측면으로 위빙하여 모든 종류의 메서드 호출에 적용하도록 대상 클래스 바이트 코드를 수정합니다. AspectJ 위빙을 사용하려면 클래스 경로에 spring-aspects.jar가있어야 하고 로드 시간 위빙(또는 컴파일 시간 위빙)을 활성화해야 합니다. (로드 시간 위빙을 설정하는 방법에 대한 자세한 내용은 Spring 구성을 참조하세요.) |
프록시-타겟-클래스 | proxyTargetClass | false | 프록시 모드에만 적용됩니다. 트랜잭션 어노테이션이 있는 클래스에 대해 생성되는 트랜잭션 프록시 유형을 제어합니다. Proxy-target-class속성이 true로 설정되어 있으면 클래스 기반 프록시가 생성됩니다. Proxy-target-class가 false이거나 속성이 생략된 경우 표준 JDK 인터페이스 기반 프록시가 생성됩니다. (다양한 프록시 유형에 대한 자세한 내용은 프록시 메커니즘을 참조하세요.) |
order | order | Ordered.LOWEST_PRECEDENCE | 트랜잭션으로 주석이 달린 빈에 적용되는 트랜잭션 조언의 순서를 정의합니다. (AOP 조언의 순서 지정과 관련된 규칙에 대한 자세한 내용은 조언 순서 지정을 참조하십시오.) 순서를 지정하지 않으면 AOP 하위 시스템이 조언의 순서를 결정합니다. |
트랜잭션 어노테이션 처리를 위한 기본 조언 모드는 프록시로, 프록시를 통해서만 호출을 가로챌 수 있습니다. 같은 클래스 내의 로컬 호출은 이러한 방식으로 가로챌 수 없습니다. 보다 고급 차단 모드를 사용하려면 컴파일 타임 또는 로드 타임 위빙과 함께 aspectj 모드로 전환하는 것을 고려하세요 |
Proxy-target-class 속성은 @Transactional 어노테이션이 있는 클래스에 대해 어떤 유형의 트랜잭션 프록시가 생성되는지 제어합니다.Proxy-target-class가 true로 설정되면 클래스 기반 프록시가 생성됩니다.Proxy-target-class가 false이거나 속성이 생략된 경우 표준 JDK 인터페이스 기반 프록시가 생성됩니다. (다양한 프록시 유형에 대한 설명은 프록시 메커니즘을참조하세요.) |
enableTransactionManagement 및 <tx:annotation-driven/>은 정의된 동일한 애플리케이션 컨텍스트의 빈에서만@Transactional을 찾습니다. 즉, DispatcherServlet에 대한 WebApplicationContext에주석 중심 구성을 넣으면 서비스가 아닌 컨트롤러에서만 @Transactional 빈을 확인합니다. 자세한 내용은 MVC를 참조하세요 |
메서드의 트랜잭션 설정을 평가할 때는 가장 많이 파생된 위치가 우선합니다. 다음 예제의 경우, DefaultFooService 클래스는 클래스 수준에서 읽기 전용 트랜잭션에 대한 설정으로 어노테이션되지만, 같은 클래스의 updateFoo(Foo) 메서드의@Transactional 어노테이션은 클래스 수준에서 정의된 트랜잭션 설정보다 우선합니다.
@Transactional(readOnly = true)
public class DefaultFooService implements FooService {
public Foo getFoo(String fooName) {
// ...
}
// these settings have precedence for this method
@Transactional(readOnly = false, propagation = Propagation.REQUIRES_NEW)
public void updateFoo(Foo foo) {
// ...
}
}
@Transactional Settings
트랜잭션 주석은 인터페이스, 클래스 또는 메서드가 트랜잭션 시맨틱을 가져야 함을 지정하는 메타데이터입니다(예: "이 메서드가 호출되면 새로운 읽기 전용 트랜잭션을 시작하여 기존 트랜잭션을 일시 중단"). 기본 @Transactional 설정은 다음과 같습니다:
- 전파 설정은 PROPAGATION_REQUIRED입니다.
- 격리 수준은 ISOLATION_DEFAULT입니다.
- 트랜잭션은 읽기-쓰기입니다.
- 트랜잭션 시간 제한은 기본값은 기본 트랜잭션 시스템의 기본 시간 제한이며, 시간 제한이 지원되지 않는 경우 없음으로 설정됩니다.
- 런타임 예외 또는 오류는 롤백을 트리거하지만, 체크된 예외는 롤백을 트리거하지 않습니다.
이러한 기본 설정을 변경할 수 있습니다. 다음 표에는 @Transactional 어노테이션의 다양한 속성이 요약되어 있습니다:
value | 문자열 | 사용할 트랜잭션 관리자를 지정하는 선택적 한정자입니다. |
트랜잭션 관리자 | 문자열 | 값의 별칭입니다. |
레이블 | 트랜잭션에 표현식 설명을 추가할 문자열 레이블의 배열입니다. | 트랜잭션 관리자가 레이블을 평가하여 구현별 동작을 실제 트랜잭션과 연결할 수 있습니다. |
전파 | 열거형: 전파 | 선택적 전파 설정입니다. |
격리 | 열거형: 격리 | 선택적 격리 수준입니다. REQUIRED 또는 REQUIRES_NEW의 전파 값에만 적용됩니다. |
timeout | int (초 단위) | 선택적 트랜잭션 시간 제한. REQUIRED 또는 REQUIRES_NEW의 전파 값에만 적용됩니다. |
timeoutString | 문자열 (세분화된 초 단위) | 시간 제한을 초 단위의 문자열 값(예: 자리 표시자)으로 지정하는 대안입니다. |
readOnly | 부울 | 읽기-쓰기 트랜잭션 대 읽기 전용 트랜잭션. REQUIRED 또는 REQUIRES_NEW 값에만 적용됩니다. |
롤백 대상 | Throwable에서 파생되어야 하는 클래스 객체의 배열입니다. | 롤백을 유발해야 하는 예외 유형의 선택적 배열입니다. |
롤백포클래스이름 | 예외 이름 패턴의 배열입니다. | 롤백을 유발해야 하는 예외 이름 패턴의 선택적 배열입니다. |
noRollbackFor | Throwable에서 파생되어야 하는 클래스 객체의 배열입니다. | 롤백을 일으키지 않아야 하는 예외 유형의 선택적 배열입니다. |
noRollbackForClassName | 예외 이름 패턴의 배열입니다. | 롤백을 일으키지 않아야 하는 예외 이름 패턴의 선택적 배열입니다. |
패턴 기반 롤백 규칙의 의도하지 않은 일치 가능성에 대한 롤백 규칙 의미, 패턴 및 경고에 대한 자세한 내용은 롤백규칙을참조하세요 |
현재 트랜잭션의 이름을 명시적으로 제어할 수 없으며, 여기서 '이름'은 트랜잭션 모니터와 로깅 출력에 표시되는 트랜잭션 이름을 의미합니다. 선언적 트랜잭션의 경우, 트랜잭션 이름은 항상 정규화된 클래스 이름 + . + 트랜잭션에 대해 조언하는 클래스의 메서드 이름입니다. 예를 들어 BusinessService 클래스의handlePayment(..) 메서드가 트랜잭션을 시작한 경우 트랜잭션의 이름은 com.example.BusinessService.handlePayment가 됩니다.
Multiple Transaction Managers with @Transactional
대부분의 Spring 애플리케이션에는 하나의 트랜잭션 매니저만 필요하지만, 단일 애플리케이션에 여러 개의 독립적인 트랜잭션 매니저가 필요한 상황이 있을 수 있습니다. 트랜잭션 어노테이션의 값 또는 트랜잭션 매니저 속성을 사용하여 사용할 트랜잭션매니저의 ID를 선택적으로 지정할 수 있습니다. 이것은 빈 이름 또는 트랜잭션 관리자 빈의 한정자 값일 수 있습니다. 예를 들어 한정자 표기법을 사용하면 애플리케이션 컨텍스트에서 다음 Java 코드와 다음 트랜잭션 관리자 빈 선언을 결합할 수 있습니다:
public class TransactionalService {
@Transactional("order")
public void setSomething(String name) { ... }
@Transactional("account")
public void doSomething() { ... }
@Transactional("reactive-account")
public Mono<Void> doSomethingReactive() { ... }
}
다음 목록은 빈 선언을 보여줍니다:
<tx:annotation-driven/>
<bean id="transactionManager1" class="org.springframework.jdbc.support.JdbcTransactionManager">
...
<qualifier value="order"/>
</bean>
<bean id="transactionManager2" class="org.springframework.jdbc.support.JdbcTransactionManager">
...
<qualifier value="account"/>
</bean>
<bean id="transactionManager3" class="org.springframework.data.r2dbc.connection.R2dbcTransactionManager">
...
<qualifier value="reactive-account"/>
</bean>
이 경우 트랜잭션 서비스의 개별 메서드는 주문, 계정 및 반응형 계정한정자로 구분된 별도의 트랜잭션 관리자에서 실행됩니다. 특별히 한정된 트랜잭션 매니저 빈을 찾을 수 없는 경우 기본 <tx:annotation-driven> 대상 빈 이름인 트랜잭션 매니저가 계속 사용됩니다.
Custom Composed Annotations
다양한 메서드에서 @Transactional과 동일한 속성을 반복적으로 사용하는 경우 Spring의 메타 어노테이션 지원을 통해 특정 사용 사례에 맞게 사용자 정의된 어노테이션을 정의할 수 있습니다. 예를 들어 다음 어노테이션 정의를 고려해 보세요:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "order", label = "causal-consistency")
public @interface OrderTx {
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Transactional(transactionManager = "account", label = "retryable")
public @interface AccountTx {
}
앞의 주석을 사용하면 이전 섹션의 예제를 다음과 같이 작성할 수 있습니다:
public class TransactionalService {
@OrderTx
public void setSomething(String name) {
// ...
}
@AccountTx
public void doSomething() {
// ...
}
}
앞의 예에서는 구문을 사용하여 트랜잭션 관리자 한정자와 트랜잭션 레이블을 정의했지만 전파 동작, 롤백 규칙, 타임아웃 및 기타 기능도 포함할 수 있었습니다.
Transaction Propagation
이 섹션에서는 Spring에서 트랜잭션 전파의 몇 가지 의미에 대해 설명합니다. 이 섹션은 트랜잭션 전파에 대한 적절한 소개가 아니라는 점에 유의하세요. 그보다는 Spring의 트랜잭션 전파에 관한 몇 가지 의미론에 대해 자세히 설명합니다.
Spring으로 관리되는 트랜잭션에서는 물리적 트랜잭션과 논리적 트랜잭션의 차이점과 이 차이점에 전파 설정이 어떻게 적용되는지 알아두세요.
Understanding PROPAGATION_REQUIRED
PROPAGATION_REQUIRED는 아직 트랜잭션이 없는 경우 현재 범위에 대해 로컬로 물리적 트랜잭션을 수행하거나 더 큰 범위에 대해 정의된 기존 '외부' 트랜잭션에 참여하도록 합니다. 이는 동일한 스레드 내의 일반적인 호출 스택 배열(예: 모든 기본 리소스가 서비스 수준 트랜잭션에 참여해야 하는 여러 리포지토리 메서드에 위임하는 서비스 파사드)에서의 기본값으로 잘 작동합니다.
기본적으로 참여하는 트랜잭션은 로컬 격리 수준, 시간 초과 값 또는 읽기 전용 플래그(있는 경우)를 무시하고 외부 범위의 특성에 합류합니다. 격리 수준이 다른 기존 트랜잭션에 참여할 때 격리 수준 선언이 거부되도록 하려면 트랜잭션 관리자에서 validateExistingTransactions 플래그를 true로 전환하는 것이 좋습니다. 이 비관용 모드는 읽기 전용 불일치(즉, 읽기 전용 외부 범위에 참여하려고 하는 내부 읽기-쓰기 트랜잭션)도 거부합니다 |
전파 설정이 PROPAGATION_REQUIRED인 경우, 설정이 적용되는 각 메서드에 대해 논리적 트랜잭션 범위가 만들어집니다. 이러한 각 논리적 트랜잭션 범위는 롤백 전용 상태를 개별적으로 결정할 수 있으며, 외부 트랜잭션 범위는 내부 트랜잭션 범위와 논리적으로 독립적입니다. 표준 PROPAGATION_REQUIRED 동작의 경우, 이러한 모든 범위는 동일한 물리적 트랜잭션에 매핑됩니다. 따라서 내부 트랜잭션 범위에 설정된 롤백 전용 마커는 외부 트랜잭션의 실제 커밋 가능성에 영향을 미칩니다.
그러나 내부 트랜잭션 범위에서 롤백 전용 마커를 설정하는 경우, 외부 트랜잭션은 롤백 자체를 결정하지 않았으므로 (내부 트랜잭션 범위에 의해 자동으로 트리거되는) 롤백은 예기치 않은 것입니다. 이 시점에서 해당UnexpectedRollbackException이 발생합니다. 이는 트랜잭션 호출자가 커밋이 실제로 수행되지 않았는데도 커밋이 수행되었다고 오해하지 않도록 하기 위해 예상되는 동작입니다. 따라서 (외부 호출자가 알지 못하는) 내부 트랜잭션이 트랜잭션을 롤백 전용으로 조용히 표시하는 경우에도 외부 호출자는 여전히 커밋을 호출합니다. 외부 호출자는 롤백이 대신 수행되었음을 명확하게 표시하기 위해 UnexpectedRollbackException을 수신해야 합니다.
Understanding PROPAGATION_REQUIRES_NEW
PROPAGATION_REQUIRES_NEW는 PROPAGATION_REQUIRED와 달리 영향을 받는 각 트랜잭션 범위에 대해 항상 독립적인 물리적 트랜잭션을 사용하며, 외부 범위에 대한 기존 트랜잭션에 절대 참여하지 않습니다. 이러한 배열에서는 기본 리소스 트랜잭션이 다르므로 외부 트랜잭션이 내부 트랜잭션의 롤백 상태에 영향을 받지 않고 완료 직후 내부 트랜잭션의 잠금이 해제되어 독립적으로 커밋하거나 롤백할 수 있습니다. 이러한 독립 내부 트랜잭션은 자체 격리 수준, 타임아웃 및 읽기 전용 설정을 선언하고 외부 트랜잭션의 특성을 상속받지 않을 수 있습니다.
내부 트랜잭션이 새 데이터베이스 연결과 같은 자체 리소스를 확보하는 동안 외부 트랜잭션에 연결된 리소스는 바인딩된 상태로 유지됩니다. 여러 스레드가 활성 외부 트랜잭션을 가지고 있고 내부 트랜잭션에 대한 새 연결을 확보하기 위해 대기하는 경우 연결 풀이 더 이상 이러한 내부 연결을 제공할 수 없어 연결 풀이 소진되고 잠재적으로 데드락이 발생할 수 있습니다. 연결 풀의 크기가 동시 스레드 수를 1 이상 초과하는 적절한 크기가 아니면 PROPAGATION_REQUIRES_NEW를 사용하지 마세요 |
Understanding PROPAGATION_NESTED
PROPAGATION_NESTED는 롤백할 수 있는 여러 저장 지점이 있는 단일 물리적 트랜잭션을 사용합니다. 이러한 부분 롤백을 사용하면 내부 트랜잭션 범위에서 해당 범위에 대한 롤백을 트리거할 수 있으며, 일부 작업이 롤백된 후에도 외부 트랜잭션은 물리적 트랜잭션을 계속할 수 있습니다. 이 설정은 일반적으로 JDBC 세이브포인트에 매핑되므로 JDBC 리소스 트랜잭션에서만 작동합니다. Spring의 DataSourceTransactionManager를 참조하세요.
트랜잭션 연산 조언
트랜잭션 연산과 몇 가지 기본적인 프로파일링 조언을 모두 실행하고 싶다고 가정해 보겠습니다. <tx:annotation-driven/> 컨텍스트에서 이를 어떻게 적용할 수 있을까요?
UpdateFoo(Foo) 메서드를 호출하면 다음과 같은 동작을 보고 싶을 것입니다:
- 구성된 프로파일링 측면이 시작됩니다.
- 트랜잭션 조언이 실행됩니다.
- 조언된 객체의 메서드가 실행됩니다.
- 트랜잭션이 커밋됩니다.
- 프로파일링 측면은 전체 트랜잭션 메서드 호출의 정확한 지속 시간을 보고합니다.
이 장에서는 트랜잭션에 적용되는 것을 제외하고는 AOP를 자세히 설명하지 않습니다. AOP 구성 및 AOP 일반에 대한 자세한 내용은 AOP를 참조하세요 |
다음 코드는 앞서 설명한 간단한 프로파일링 측면을 보여줍니다:
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;
// allows us to control the ordering of advice
public int getOrder() {
return this.order;
}
public void setOrder(int order) {
this.order = order;
}
// this method is the 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;
}
}
조언 순서는 주문됨 인터페이스를 통해 제어됩니다. 조언 순서에 대한 자세한 내용은조언 순서 지정하기를 참조하세요.
다음 구성은 프로파일링 및 트랜잭션 측면이 원하는 순서대로 적용된 fooService 빈을 만듭니다:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- this is the aspect -->
<bean id="profiler" class="x.y.SimpleProfiler">
<!-- run before the transactional advice (hence the lower order number) -->
<property name="order" value="1"/>
</bean>
<tx:annotation-driven transaction-manager="txManager" order="200"/>
<aop:config>
<!-- this advice runs around the transactional advice -->
<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>
비슷한 방식으로 추가 측면을 얼마든지 구성할 수 있습니다.
다음 예제는 앞의 두 예제와 동일한 설정을 생성하지만 순수하게 XML 선언적 접근 방식을 사용합니다:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/tx
https://www.springframework.org/schema/tx/spring-tx.xsd
http://www.springframework.org/schema/aop
https://www.springframework.org/schema/aop/spring-aop.xsd">
<bean id="fooService" class="x.y.service.DefaultFooService"/>
<!-- the profiling advice -->
<bean id="profiler" class="x.y.SimpleProfiler">
<!-- run before the transactional advice (hence the lower order number) -->
<property name="order" value="1"/>
</bean>
<aop:config>
<aop:pointcut id="entryPointMethod" expression="execution(* x.y..*Service.*(..))"/>
<!-- runs after the profiling advice (cf. the order attribute) -->
<aop:advisor advice-ref="txAdvice" pointcut-ref="entryPointMethod" order="2"/>
<!-- order value is higher than the profiling aspect -->
<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>
<!-- other <bean/> definitions such as a DataSource and a TransactionManager here -->
</beans>
앞선 구성의 결과는 프로파일링 및 트랜잭션 측면이 순서대로 적용된 fooService 빈입니다. 프로파일링 조언이 들어올 때는 트랜잭션 조언 이후에, 나갈 때는 트랜잭션 조언 이전에 실행되도록 하려면 프로파일링 측면 빈의 순서 속성 값을 트랜잭션 조언의 순서 값보다 높게 바꾸면 됩니다.
비슷한 방식으로 추가 측면을 구성할 수 있습니다.
AspectJ와 함께 @Transactional 사용
스프링 프레임워크의 @Transactional 지원을 스프링 컨테이너 외부에서 AspectJ 측면을 통해 사용할 수도 있습니다. 이렇게 하려면 먼저 클래스(및 선택적으로 클래스의 메서드)에 @Transactional 어노테이션을 추가한 다음, 애플리케이션을spring-aspects.jar 파일에 정의된org.springframework.transaction.aspectj.AnnotationTransactionAspect와 링크(위빙)하세요. 또한 트랜잭션 관리자를 사용하여 측면을 구성해야 합니다. Spring 프레임워크의 IoC 컨테이너를 사용하여 의존성 주입을 처리할 수 있습니다. 트랜잭션 관리 측면을 구성하는 가장 간단한 방법은 <tx:annotation-driven/> 요소를 사용하고 @Transactional 사용하기에 설명된 대로 aspectj에 mode속성을 지정하는 것입니다. 여기서는 Spring 컨테이너 외부에서 실행되는 애플리케이션에 초점을 맞추기 때문에 프로그래밍 방식으로 수행하는 방법을 보여드리겠습니다.
계속하기 전에 @Transactional 및AOP 사용하기를 각각 읽어보시기 바랍니다 |
다음 예제에서는 트랜잭션 관리자를 생성하고 이를 사용하도록AnnotationTransactionAspect를 구성하는 방법을 보여줍니다:
// construct an appropriate transaction manager
DataSourceTransactionManager txManager = new DataSourceTransactionManager(getDataSource());
// configure the AnnotationTransactionAspect to use it; this must be done before executing any transactional methods
AnnotationTransactionAspect.aspectOf().setTransactionManager(txManager);
이 측면을 사용할 때는 클래스가 구현하는 인터페이스(있는 경우)가 아니라 구현 클래스(또는 해당 클래스 내의 메서드 또는 둘 다)에 주석을 달아야 합니다. AspectJ는 인터페이스에 대한 어노테이션은 상속되지 않는다는 Java의 규칙을 따릅니다 |
클래스의 @Transactional 어노테이션은 클래스의 모든 공용 메서드 실행에 대한 기본 트랜잭션 시맨틱을 지정합니다.
클래스 내의 메서드에 대한 @Transactional 어노테이션은 클래스 어노테이션(있는 경우)이 제공하는 기본 트랜잭션 의미를 재정의합니다. 표시 여부에 관계없이 모든 메서드에 주석을 달 수 있습니다.
AnnotationTransactionAspect로 애플리케이션을 위빙하려면 AspectJ로 애플리케이션을 빌드하거나( AspectJ 개발 가이드 참조) 로드 시간 위빙을 사용해야 합니다. AspectJ를 사용한 로드 시간 위빙에 대한 설명은 Spring 프레임워크에서 AspectJ를 사용한 로드 시간 위빙을 참조하세요.
Programmatic Transaction Management
Spring 프레임워크는 프로그래밍 방식의 트랜잭션 관리를 위한 두 가지 수단을 제공합니다:
- 트랜잭션 템플릿 또는 트랜잭션 오퍼레이터.
- 트랜잭션 관리자를 직접 구현합니다.
Spring 팀은 일반적으로 명령형 흐름에서 프로그래밍 방식의 트랜잭션 관리를 위해 TransactionTemplate을, 반응형 코드에는 TransactionalOperator를 권장합니다. 두 번째 접근 방식은 예외 처리가 덜 번거롭지만 JTA UserTransaction API를 사용하는 것과 유사합니다.
Using the TransactionTemplate
트랜잭션템플릿은 Jdbc템플릿과 같은 다른 Spring템플릿과 동일한 접근 방식을 채택합니다. 콜백 접근 방식을 사용하므로(애플리케이션 코드가 보일러플레이트 획득 및 트랜잭션 리소스 릴리스 작업을 하지 않아도 됩니다), 코드가 원하는 작업에만 집중한다는 점에서 인텐션 중심적인 코드가 생성됩니다.
다음 예제에서 알 수 있듯이, 트랜잭션 템플릿을 사용하면 Spring의 트랜잭션 인프라와 API에 완전히 연결됩니다. 프로그래매틱 트랜잭션 관리가 개발 요구 사항에 적합한지 여부는 개발자가 직접 결정해야 합니다 |
트랜잭션 컨텍스트에서 실행되어야 하고트랜잭션 템플릿을 명시적으로 사용하는 애플리케이션 코드는 다음 예제와 유사합니다. 애플리케이션 개발자는 트랜잭션 컨텍스트에서 실행해야 하는 코드가 포함된 트랜잭션 콜백 구현(일반적으로 익명 내부 클래스로 표현됨)을 작성할 수 있습니다. 그런 다음 사용자 정의 트랜잭션 콜백의 인스턴스를 트랜잭션 템플릿에 노출된실행(...) 메서드에 전달할 수 있습니다. 다음 예제는 그 방법을 보여줍니다:
public class SimpleService implements Service {
// single TransactionTemplate shared amongst all methods in this instance
private final TransactionTemplate transactionTemplate;
// use constructor-injection to supply the PlatformTransactionManager
public SimpleService(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
}
public Object someServiceMethod() {
return transactionTemplate.execute(new TransactionCallback() {
// the code in this method runs in a transactional context
public Object doInTransaction(TransactionStatus status) {
updateOperation1();
return resultOfUpdateOperation2();
}
});
}
}
반환값이 없는 경우 다음과 같이 익명 클래스와 함께 편리한 TransactionCallbackWithoutResult 클래스를 사용할 수 있습니다:
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
updateOperation1();
updateOperation2();
}
});
콜백 내의 코드는 다음과 같이 제공된 트랜잭션 상태 객체에서setRollbackOnly() 메서드를 호출하여 트랜잭션을 롤백할 수 있습니다:
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
protected void doInTransactionWithoutResult(TransactionStatus status) {
try {
updateOperation1();
updateOperation2();
} catch (SomeBusinessException ex) {
status.setRollbackOnly();
}
}
});
트랜잭션 설정 지정
프로그래밍 방식으로 또는 구성에서 트랜잭션 템플릿에서 트랜잭션 설정(예: 전파 모드, 격리 수준, 시간 초과 등)을 지정할 수 있습니다. 기본적으로 트랜잭션 템플릿 인스턴스에는기본 트랜잭션 설정이 적용됩니다. 다음 예는 특정 트랜잭션 템플릿에 대한 트랜잭션 설정을 프로그래밍 방식으로 사용자 지정하는 방법을 보여줍니다:
public class SimpleService implements Service {
private final TransactionTemplate transactionTemplate;
public SimpleService(PlatformTransactionManager transactionManager) {
this.transactionTemplate = new TransactionTemplate(transactionManager);
// the transaction settings can be set here explicitly if so desired
this.transactionTemplate.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
this.transactionTemplate.setTimeout(30); // 30 seconds
// and so forth...
}
}
다음 예는 Spring XML 구성을 사용하여 몇 가지 사용자 정의 트랜잭션 설정이 포함된 트랜잭션 템플릿 을 정의합니다:
<bean id="sharedTransactionTemplate"
class="org.springframework.transaction.support.TransactionTemplate">
<property name="isolationLevelName" value="ISOLATION_READ_UNCOMMITTED"/>
<property name="timeout" value="30"/>
</bean>
그런 다음 필요한 만큼의 서비스에 sharedTransactionTemplate을주입할 수 있습니다.
마지막으로, 트랜잭션 템플릿 클래스의 인스턴스는 대화 상태를 유지하지 않는다는 점에서 스레드 안전합니다. 그러나 트랜잭션 템플릿 인스턴스는 구성 상태를 유지합니다. 따라서 여러 클래스가 하나의 트랜잭션 템플릿 인스턴스를 공유할 수 있지만, 한 클래스가 다른 설정(예: 다른 격리 수준)으로 트랜잭션 템플릿을 사용해야 하는 경우 두 개의 별개의 트랜잭션 템플릿 인스턴스를 만들어야 합니다.
Using the TransactionalOperator
트랜잭션 연산자는 다른 반응형 연산자와 유사한 연산자 설계를 따릅니다. 콜백 접근 방식을 사용하며(애플리케이션 코드가 보일러플레이트 획득 및 트랜잭션 리소스 릴리스 작업을 하지 않아도 되기 때문에), 코드가 원하는 작업에만 집중한다는 점에서 인텐션 중심적인 코드를 생성합니다.
다음 예제에서 알 수 있듯이 TransactionalOperator를 사용하면 Spring의 트랜잭션 인프라와 API에 완전히 연결됩니다. 프로그래매틱 트랜잭션 관리가 개발 요구 사항에 적합한지 여부는 개발자가 직접 결정해야 합니다 |
트랜잭션 컨텍스트에서 실행되어야 하고 TransactionalOperator를 명시적으로 사용하는 애플리케이션 코드는 다음 예제와 유사합니다:
public class SimpleService implements Service {
// single TransactionalOperator shared amongst all methods in this instance
private final TransactionalOperator transactionalOperator;
// use constructor-injection to supply the ReactiveTransactionManager
public SimpleService(ReactiveTransactionManager transactionManager) {
this.transactionalOperator = TransactionalOperator.create(transactionManager);
}
public Mono<Object> someServiceMethod() {
// the code in this method runs in a transactional context
Mono<Object> update = updateOperation1();
return update.then(resultOfUpdateOperation2).as(transactionalOperator::transactional);
}
}
트랜잭션 오퍼 레이터는 두 가지 방식으로 사용할 수 있습니다:
- 프로젝트 리액터 유형을 사용하는 오퍼레이터 스타일(mono.as(transactionalOperator::transactional))
- 다른 모든 경우에 대한 콜백 스타일(transactionalOperator.execute(TransactionCallback<T>))
콜백 내의 코드는 다음과 같이 제공된 ReactiveTransaction 객체에서 setRollbackOnly()메서드를 호출하여 트랜잭션을 롤백할 수 있습니다:
transactionalOperator.execute(new TransactionCallback<>() {
public Mono<Object> doInTransaction(ReactiveTransaction status) {
return updateOperation1().then(updateOperation2)
.doOnError(SomeBusinessException.class, e -> status.setRollbackOnly());
}
}
});
취소 신호
리액티브 스트림에서 구독자는 구독을 취소하고게시자를 중지할 수 있습니다. 다음(),take(long), timeout(Duration) 등과 같은 다른 라이브러리뿐만 아니라 Project Reactor의 오퍼레이터도 취소를 발행할 수 있습니다. 오류로 인한 취소인지 아니면 단순히 더 이상 사용할 관심이 없어서인지 취소 이유를 알 수 있는 방법은 없습니다. 버전 5.3부터는 취소 신호가 롤백으로 이어지므로 트랜잭션퍼블리셔의 다운스트림에서 사용되는 오퍼레이터를 고려하는 것이 중요합니다. 특히 플럭스 또는 기타 다중값 퍼블리셔의 경우, 트랜잭션이 완료되려면 전체 출력이 소비되어야 합니다.
트랜잭션 설정 지정하기
트랜잭션 오퍼레이터에 대한 트랜잭션 설정(예: 전파 모드, 격리 수준, 타임아웃 등)을 지정할 수 있습니다. 기본적으로 트랜잭션오퍼레이터 인스턴스에는기본 트랜잭션 설정이 있습니다. 다음 예는 특정 트랜잭션오퍼레이터에 대한 트랜잭션 설정의 사용자 지정 예제입니다:
public class SimpleService implements Service {
private final TransactionalOperator transactionalOperator;
public SimpleService(ReactiveTransactionManager transactionManager) {
DefaultTransactionDefinition definition = new DefaultTransactionDefinition();
// the transaction settings can be set here explicitly if so desired
definition.setIsolationLevel(TransactionDefinition.ISOLATION_READ_UNCOMMITTED);
definition.setTimeout(30); // 30 seconds
// and so forth...
this.transactionalOperator = TransactionalOperator.create(transactionManager, definition);
}
}
Using the TransactionManager
다음 섹션에서는 명령형 및 반응형 트랜잭션 관리자의 프로그래밍 방식 사용법을 설명합니다.
플랫폼트랜잭션 매니저 사용
명령형 트랜잭션의 경우,org.springframework.transaction.PlatformTransactionManager를 직접 사용하여 트랜잭션을 관리할 수 있습니다. 이렇게 하려면 빈 참조를 통해 사용하는 PlatformTransactionManager의 구현을 빈에 전달합니다. 그런 다음 트랜잭션 정의 및트랜잭션 상태 객체를 사용하여 트랜잭션을 시작하고, 롤백하고, 커밋할 수 있습니다. 다음 예제는 그 방법을 보여줍니다:
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
TransactionStatus status = txManager.getTransaction(def);
try {
// put your business logic here
} catch (MyException ex) {
txManager.rollback(status);
throw ex;
}
txManager.commit(status);
리액티브 트랜잭션 관리자 사용
반응형 트랜잭션으로 작업할 때,org.springframework.transaction.ReactiveTransactionManager를 직접 사용하여 트랜잭션을 관리할 수 있습니다. 이렇게 하려면 빈 참조를 통해 사용하는 ReactiveTransactionManager의 구현을 빈에 전달합니다. 그런 다음 트랜잭션 정의 및ReactiveTransaction 객체를 사용하여 트랜잭션을 시작하고, 롤백하고, 커밋할 수 있습니다. 다음 예제는 그 방법을 보여줍니다:
DefaultTransactionDefinition def = new DefaultTransactionDefinition();
// explicitly setting the transaction name is something that can be done only programmatically
def.setName("SomeTxName");
def.setPropagationBehavior(TransactionDefinition.PROPAGATION_REQUIRED);
Mono<ReactiveTransaction> reactiveTx = txManager.getReactiveTransaction(def);
reactiveTx.flatMap(status -> {
Mono<Object> tx = ...; // put your business logic here
return tx.then(txManager.commit(status))
.onErrorResume(ex -> txManager.rollback(status).then(Mono.error(ex)));
});
프로그래밍 방식과 선언적 트랜잭션 관리 중 선택하기
프로그래밍 방식의 트랜잭션 관리는 일반적으로 트랜잭션 작업의 수가 적은 경우에만 사용하는 것이 좋습니다. 예를 들어 특정 업데이트 작업에 대해서만 트랜잭션이 필요한 웹 애플리케이션이 있는 경우 Spring이나 다른 기술을 사용하여 트랜잭션 프록시를 설정하고 싶지 않을 수 있습니다. 이 경우트랜잭션 템플릿을 사용하는 것이 좋은 방법일 수 있습니다. 트랜잭션 이름을 명시적으로 설정할 수 있는 것도 트랜잭션 관리에 대한 프로그래밍 방식을 사용해야만 할 수 있는 일입니다.
반면에 애플리케이션에 수많은 트랜잭션 작업이 있는 경우 선언적 트랜잭션 관리가 일반적으로 가치가 있습니다. 트랜잭션 관리를 비즈니스 로직에서 벗어나게 하고 구성하기 어렵지 않습니다. EJB CMT가 아닌 Spring 프레임워크를 사용하면 선언적 트랜잭션 관리의 구성 비용이 크게 줄어듭니다.
트랜잭션 바인딩 이벤트
Spring 4.2부터 이벤트 리스너를 트랜잭션의 단계에 바인딩할 수 있습니다. 일반적인 예는 트랜잭션이 성공적으로 완료되었을 때 이벤트를 처리하는 것입니다. 이렇게 하면 현재 트랜잭션의 결과가 리스너에게 실제로 중요한 경우 이벤트를 보다 유연하게 사용할 수 있습니다.
이벤트 리스너는 @EventListener 어노테이션을 사용하여 일반 이벤트 리스너를 등록할 수 있습니다. 트랜잭션에 바인딩해야 하는 경우 @TransactionalEventListener를 사용합니다. 이렇게 하면 리스너는 기본적으로 트랜잭션의 커밋 단계에 바인딩됩니다.
다음 예제는 이 개념을 보여줍니다. 컴포넌트가 주문 생성 이벤트를 게시하고 해당 이벤트가 게시된 트랜잭션이 성공적으로 커밋된 경우에만 해당 이벤트를 처리해야 하는 리스너를 정의한다고 가정해 보겠습니다. 다음 예제는 이러한 이벤트 리스너를 설정합니다:
@Component
public class MyComponent {
@TransactionalEventListener
public void handleOrderCreatedEvent(CreationEvent<Order> creationEvent) {
// ...
}
}
트랜잭션 리스 너 어노테이션은 리스너가 바인딩되어야 하는 트랜잭션의 단계를 사용자 정의할 수 있는 phase 속성을 노출합니다. 유효한 단계는 BEFORE_COMMIT, AFTER_COMMIT (기본값), AFTER_ROLLBACK, 그리고 트랜잭션 완료(커밋이든 롤백이든)를 집계하는AFTER_COMPLETION입니다.
실행 중인 트랜잭션이 없으면 필요한 시맨틱을 준수할 수 없으므로 리스너는 전혀 호출되지 않습니다. 그러나 어노테이션의 fallbackExecution속성을 true로 설정하여 해당 동작을 재정의할 수 있습니다.
6.1부터, @TransactionalEventListener는PlatformTransactionManager가 관리하는 스레드 바운드 트랜잭션과 ReactiveTransactionManager가 관리하는 반응형 트랜잭션에서 작동할 수 있습니다. 전자의 경우, 리스너는 현재 스레드 바운드 트랜잭션을 볼 수 있도록 보장됩니다. 후자는 스레드 로컬 변수 대신 Reactor 컨텍스트를 사용하므로 이벤트 소스로서퍼블리싱된이벤트 인스턴스에 트랜잭션 컨텍스트를 포함해야 합니다. 자세한 내용은TransactionalEventPublisher자바독을 참조하세요.
|
애플리케이션 서버별 통합
Spring의 트랜잭션 추상화는 일반적으로 애플리케이션 서버에 구애받지 않습니다. 또한 Spring의 JtaTransactionManager 클래스(선택적으로 JTA UserTransaction 및 TransactionManager 객체에 대한 JNDI 조회를 수행할 수 있음)는 애플리케이션 서버에 따라 달라지는 후자의 객체 위치를 자동 감지합니다. JTA트랜잭션 매니저에 액세스할 수 있으면 트랜잭션 의미론이 향상되며, 특히 트랜잭션 일시 중단을 지원할 수 있습니다. 자세한 내용은JtaTransactionManager자바독을 참조하세요.
Spring의 JtaTransactionManager는 Jakarta EE 애플리케이션 서버에서 실행하기 위한 표준 선택이며 모든 일반 서버에서 작동하는 것으로 알려져 있습니다. 트랜잭션 일시 중단과 같은 고급 기능은 특별한 구성 없이도 많은 서버(GlassFish, JBoss 및 Geronimo 포함)에서도 작동합니다.
Solutions to Common Problems
이 섹션에서는 몇 가지 일반적인 문제에 대한 해결 방법을 설명합니다.
Using the Wrong Transaction Manager for a Specific DataSource
선택한 트랜잭션 기술 및 요구 사항에 따라 올바른 PlatformTransactionManager 구현을 사용하세요. 올바르게 사용하면 Spring 프레임워크는 간단하고 이식 가능한 추상화를 제공할 뿐입니다. 전역 트랜잭션을 사용하는 경우, 모든 트랜잭션 작업에 대해org.springframework.transaction.jta.JtaTransactionManager 클래스(또는 그애플리케이션 서버별 하위 클래스 )를 사용해야 합니다. 그렇지 않으면 트랜잭션 인프라는 컨테이너 DataSource인스턴스와 같은 리소스에서 로컬 트랜잭션을 수행하려고 시도합니다. 이러한 로컬 트랜잭션은 의미가 없으며, 정상적인 애플리케이션 서버는 이를 오류로 처리합니다.
추가 리소스
Spring 프레임워크의 트랜잭션 지원에 대한 자세한 내용은 다음을 참조하세요:
- Spring의분산 트랜잭션, XA 유무에 따른 분산 트랜 잭션은 Spring의 David Syer가 Spring 애플리케이션의 분산 트랜잭션에 대한 7가지 패턴(이 중 3가지는 XA를 사용하고 4가지는 사용하지 않음)을 안내하는 JavaWorld 프레젠테이션입니다.
- Java 트랜잭션 설계 전략은 InfoQ에서 제공하는 책으로, Java의 트랜잭션에 대해 잘 정리된 소개를 제공합니다. 또한 Spring 프레임워크와 EJB3 모두에서 트랜잭션을 구성하고 사용하는 방법에 대한 예제가 나란히 포함되어 있습니다.
DAO Support
Spring의 DAO(데이터 액세스 객체) 지원은 일관된 방식으로 데이터 액세스 기술(예: JDBC, Hibernate 또는 JPA)로 쉽게 작업할 수 있도록 하는 데 목적이 있습니다. 이를 통해 앞서 언급한 지속성 기술 간에 매우 쉽게 전환할 수 있으며, 각 기술에 고유한 예외를 잡을 걱정 없이 코딩할 수 있습니다.
Consistent Exception Hierarchy
Spring은SQLException과 같은 기술별 예외를 DataAccessException을 루트 예외로 하는 자체 예외 클래스 계층 구조로 편리하게 변환할 수 있는 기능을 제공합니다. 이러한 예외는 원래 예외를 래핑하므로 문제가 발생했을 수 있는 정보를 잃을 위험이 전혀 없습니다.
JDBC 예외 외에도 Spring은 JPA 및 Hibernate 관련 예외도 래핑하여 집중된 런타임 예외 집합으로 변환할 수 있습니다. 이를 통해 복구 불가능한 대부분의 지속성 예외를 적절한 계층에서만 처리할 수 있으며, DAO에서 성가신 상용구 캐치 앤 던지기 블록과 예외 선언을 사용하지 않아도 됩니다. (하지만 여전히 필요한 곳에서 예외를 트래핑하고 처리할 수 있습니다.) 위에서 언급한 것처럼 JDBC 예외(데이터베이스별 방언 포함)도 동일한 계층으로 변환되므로 일관된 프로그래밍 모델 내에서 JDBC로 일부 작업을 수행할 수 있습니다.
앞의 논의는 다양한 ORM 프레임워크를 지원하는 Spring의 다양한 템플릿 클래스에도 적용됩니다. 인터셉터 기반 클래스를 사용하는 경우, 애플리케이션은 가급적이면 SessionFactoryUtils의 convertHibernateAccessException(..) 또는 convertJpaAccessException(..) 메서드에 각각 위임하여 HibernateException 및 PersistenceException 자체 처리에 신경을 써야 합니다. 이러한 메서드는 예외를 org.springframework.dao예외 계층 구조의 예외와 호환되는 예외로 변환합니다. 지속성 예외를 선택하지 않으면 예외도 발생할 수 있습니다(예외 측면에서 일반적인 DAO 추상화를 희생하지만).
다음 이미지는 Spring이 제공하는 예외 계층 구조를 보여줍니다. (이미지에 자세히 설명된 클래스 계층 구조는 전체DataAccessException 계층 구조의 하위 집합만 보여줍니다.)
Annotations Used to Configure DAO or Repository Classes
DAO(데이터 액세스 객체) 또는 리포지토리가 예외 번역을 제공하도록 보장하는 가장 좋은 방법은 @Repository 어노테이션을 사용하는 것입니다. 이 어노테이션을 사용하면 컴포넌트 검색 지원에서 DAO 및 리포지토리에 대한 XML 구성 항목을 제공하지 않고도 DAO 및 리포지토리를 찾아서 구성할 수 있습니다. 다음 예는 @Repository 어노테이션을 사용하는 방법을 보여줍니다:
@Repository
public class SomeMovieFinder implements MovieFinder {
// ...
}
리포지토리 어노테이션. |
모든 DAO 또는 리포지토리 구현은 사용되는 지속성 기술에 따라 지속성 리소스에 대한 액세스가 필요합니다. 예를 들어, JDBC 기반 리포지토리는 JDBC 데이터소스에 대한 액세스 권한이 필요하고, JPA 기반 리포지토리는EntityManager에 대한 액세스 권한이 필요합니다. 이를 수행하는 가장 쉬운 방법은 @Autowired, @Inject, @Resource 또는 @PersistenceContext어노테이션 중 하나를 사용하여 이 리소스 종속성을 주입하는 것입니다. 다음 예제는 JPA 리포지토리에서 작동합니다:
@Repository
public class JpaMovieFinder implements MovieFinder {
@PersistenceContext
private EntityManager entityManager;
// ...
}
기존 최대 절전 모드 API를 사용하는 경우 다음 예제에서 볼 수 있듯이 SessionFactory를 주입할 수 있습니다:
@Repository
public class HibernateMovieFinder implements MovieFinder {
private SessionFactory sessionFactory;
@Autowired
public void setSessionFactory(SessionFactory sessionFactory) {
this.sessionFactory = sessionFactory;
}
// ...
}
여기서 보여드리는 마지막 예제는 일반적인 JDBC 지원을 위한 것입니다. 초기화 메서드 또는 생성자에 데이터소스를 주입할 수 있으며, 이데이터 소스를 사용하여 JdbcTemplate및 기타 데이터 액세스 지원 클래스(예: SimpleJdbcCall 등)를 생성할 수 있습니다. 다음 예제는 데이터소스를 자동 와이어링합니다:
@Repository
public class JdbcMovieFinder implements MovieFinder {
private JdbcTemplate jdbcTemplate;
@Autowired
public void init(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// ...
}
이러한 어노테이션을 활용하도록 애플리케이션 컨텍스트를 구성하는 방법에 대한 자세한 내용은 각 지속성 기술의 구체적인 적용 범위를 참조하세요 |
JDBC를 사용한 데이터 액세스
Spring 프레임워크 JDBC 추상화가 제공하는 값은 아래 표에 요약된 일련의 작업으로 가장 잘 알 수 있습니다. 이 표는 Spring이 처리하는 작업과 사용자가 책임져야 하는 작업을 보여줍니다.
표 1. Spring JDBC - 누가 무엇을 하나요?액션Spring당신
연결 매개변수를 정의합니다. | X | |
연결을 엽니다. | X | |
SQL 문을 지정합니다. | X | |
매개변수 선언 및 매개변수 값 제공 | X | |
문을 준비하고 실행합니다. | X | |
결과를 반복하도록 루프를 설정합니다(있는 경우). | X | |
각 반복에 대해 작업을 수행합니다. | X | |
예외를 처리합니다. | X | |
트랜잭션을 처리합니다. | X | |
연결, 문 및 결과 집합을 닫습니다. | X |
Spring 프레임워크는 JDBC를 지루한 API로 만들 수 있는 모든 낮은 수준의 세부 사항을 처리합니다.
JDBC 데이터베이스 액세스를 위한 접근 방식 선택하기
JDBC 데이터베이스 액세스의 기반을 형성하기 위해 여러 가지 접근 방식 중에서 선택할 수 있습니다. 세 가지 버전의 JdbcTemplate 외에도 SimpleJdbcInsert 및 SimpleJdbcCall접근 방식은 데이터베이스 메타데이터를 최적화하며, RDBMS Object 스타일은 보다 객체 지향적인 접근 방식을 제공합니다. 이러한 접근 방식 중 하나를 사용하기 시작한 후에도 다른 접근 방식의 기능을 포함하도록 믹스 앤 매치할 수 있습니다.
- JdbcTemplate은 고전적이고 가장 널리 사용되는 Spring JDBC 접근 방식입니다. 이 "가장 낮은 수준의" 접근 방식과 다른 모든 접근 방식은 내부적으로 JdbcTemplate을 사용합니다.
- NamedParameterJdbcTemplate은 기존의 JDBC ? 자리 표시자 대신 명명된 매개 변수를 제공하기 위해 JdbcTemplate을 래핑합니다. 이 접근 방식은 SQL 문에 대해 여러 개의 매개변수가 있는 경우 더 나은 문서화와 사용 편의성을 제공합니다.
- SimpleJdbcInsert 및 SimpleJdbcCall은 데이터베이스 메타데이터를 최적화하여 필요한 구성의 양을 제한합니다. 이 접근 방식을 사용하면 테이블 또는 프로시저의 이름과 열 이름과 일치하는 매개변수 맵만 제공하면 되므로 코딩이 간소화됩니다. 이 방법은 데이터베이스가 적절한 메타데이터를 제공하는 경우에만 작동합니다. 데이터베이스에서 이 메타데이터를 제공하지 않는 경우에는 매개변수의 명시적인 구성을 제공해야 합니다.
- 데이터 액세스 계층을 초기화하는 동안 재사용 가능하고 스레드에 안전한 객체를 만들려면 MappingSqlQuery, SqlUpdate 및 StoredProcedure를 포함한 RDBMS 객체를 만들어야 합니다. 이 접근 방식을 사용하면 쿼리 문자열을 정의하고, 매개 변수를 선언하고, 쿼리를 컴파일할 수 있습니다. 이렇게 하면 실행(...), 업데이트(...) 및찾기(...) 메서드를 다양한 매개변수 값으로 여러 번 호출할 수 있습니다.
패키지 계층 구조
Spring 프레임워크의 JDBC 추상화 프레임워크는 네 가지 패키지로 구성됩니다:
- core: Org.springframework.jdbc.core 패키지에는 JdbcTemplate 클래스와 다양한 콜백 인터페이스, 그리고 다양한 관련 클래스가 포함되어 있습니다. Org.springframework.jdbc.core.simple이라는 이름의 하위 패키지에는 SimpleJdbcInsert 및SimpleJdbcCall 클래스가 포함되어 있습니다. 또 다른 하위 패키지인org.springframework.jdbc.core.namedparam에는 NamedParameterJdbcTemplate클래스와 관련 지원 클래스가 포함되어 있습니다. JDBC 코어 클래스를 사용하여 기본 JDBC 처리 및 오류 처리 제어, JDBC 배치 작업 및 SimpleJdbc 클래스를 사용한 JDBC 작업 간소화를 참조하십시오.
- 데이터 소스: Org.springframework.jdbc.datasource 패키지에는 DataSource에 쉽게 액세스할 수 있는 유틸리티 클래스와 Jakarta EE 컨테이너 외부에서 수정되지 않은 JDBC 코드를 테스트하고 실행하는 데 사용할 수 있는 다양한 간단한 DataSource 구현이 포함되어 있습니다. Org.springframework.jdbc.datasource.embedded라는 이름의 하위 패키지는 HSQL, H2, Derby와 같은 Java 데이터베이스 엔진을 사용하여 임베디드 데이터베이스를 생성하는 데 대한 지원을 제공합니다.데이터베이스 연결 제어 및 임베디드 데이터베이스 지원을 참조하세요.
- 객체: Org.springframework.jdbc.object 패키지에는 RDBMS 쿼리, 업데이트 및 저장 프로시저를 스레드 안전하며 재사용 가능한 객체로 표현하는 클래스가 포함되어 있습니다.JDBC 연산을 Java 객체로 모델링하기를 참조하세요. 이 스타일은 쿼리에서 반환되는 객체가 자연스럽게 데이터베이스에서 연결이 끊어지지만 보다 객체 지향적인 접근 방식을 제공합니다. 이 상위 수준의 JDBC 추상화는 org.springframework.jdbc.core 패키지의 하위 수준 추상화에 따라 달라집니다.
- 지원: Org.springframework.jdbc.support 패키지는 SQLException변환 기능과 일부 유틸리티 클래스를 제공합니다. JDBC 처리 중에 던져진 예외는 org.springframework.dao 패키지에 정의된 예외로 변환됩니다. 즉, Spring JDBC 추상화 계층을 사용하는 코드는 JDBC 또는 RDBMS 전용 오류 처리를 구현할 필요가 없습니다. 변환된 모든 예외는 선택 해제되므로 복구할 수 있는 예외를 포착하는 동시에 다른 예외는 호출자에게 전파되도록 할 수 있습니다. SQLExceptionTranslator 사용을 참조하십시오.
Using the JDBC Core Classes to Control Basic JDBC Processing and Error Handling
이 섹션에서는 JDBC 핵심 클래스를 사용하여 오류 처리를 비롯한 기본 JDBC 처리를 제어하는 방법을 설명합니다. 여기에는 다음 항목이 포함됩니다:
Using JdbcTemplate
JdbcTemplate은 JDBC 코어 패키지의 중심 클래스입니다. 리소스의 생성 및 릴리스를 처리하여 연결을 닫는 것을 잊어버리는 것과 같은 일반적인 오류를 방지하는 데 도움이 됩니다. 이 클래스는 핵심 JDBC 워크플로우의 기본 작업(예: 문 생성 및 실행)을 수행하며, 애플리케이션 코드가 SQL 및 추출 결과를 제공하도록 남겨둡니다. JdbcTemplate 클래스:
- SQL 쿼리 실행
- 문 및 저장 프로시저 호출 업데이트
- ResultSet 인스턴스에 대한 반복 및 반환된 매개변수 값 추출을 수행합니다.
- JDBC 예외를 포착하여 org.springframework.dao 패키지에 정의된 더 많은 정보를 제공하는 일반적인 예외 계층 구조로 변환합니다. ( 일관된 예외 계층 구조 참조)
코드에 JdbcTemplate을 사용하는 경우, 콜백 인터페이스만 구현하면 명확하게 정의된 컨트랙트를 제공할 수 있습니다.JdbcTemplate 클래스에서 제공하는 Connection이 주어지면 PreparedStatementCreator 콜백 인터페이스는 SQL 및 필요한 매개 변수를 제공하는 준비된 문을 생성합니다. 콜러블 문을 생성하는콜러블 스테이트먼트 크리에이터 인터페이스도 마찬가지입니다.RowCallbackHandler 인터페이스는 ResultSet의 각 행에서 값을 추출합니다.
데이터 소스 참조로 직접 인스턴스화를 통해 DAO 구현 내에서 JdbcTemplate을 사용하거나, Spring IoC 컨테이너에서 구성하여 DAO에 빈 참조로 제공할 수 있습니다.
DataSource는 항상 Spring IoC 컨테이너에서 빈으로 구성해야 합니다. 첫 번째 경우에는 빈이 서비스에 직접 제공되고, 두 번째 경우에는 준비된 템플릿에 제공됩니다 |
이 클래스에서 발행된 모든 SQL은 템플릿 인스턴스의 정규화된 클래스 이름에 해당하는 범주(일반적으로JdbcTemplate이지만JdbcTemplate 클래스의 사용자 정의 하위 클래스를 사용하는 경우 다를 수 있음) 아래 DEBUG 수준에서 로깅됩니다.
다음 섹션에서는 JdbcTemplate 사용의 몇 가지 예를 제공합니다. 이 예제는 JdbcTemplate에 의해 노출되는 모든 기능의 전체 목록이 아닙니다. 이에 대한 자세한 내용은 관련 javadoc을 참조하세요.
쿼리(SELECT)
다음 쿼리는 관계의 행 수를 가져옵니다:
int rowCount = this.jdbcTemplate.queryForObject("select count(*) from t_actor", Integer.class);
다음 쿼리는 바인드 변수를 사용합니다:
int countOfActorsNamedJoe = this.jdbcTemplate.queryForObject(
"select count(*) from t_actor where first_name = ?", Integer.class, "Joe");
다음 쿼리는 문자열을 찾습니다:
String lastName = this.jdbcTemplate.queryForObject(
"select last_name from t_actor where id = ?",
String.class, 1212L);
다음 쿼리는 단일 도메인 개체를 찾아서 채웁니다:
Actor actor = jdbcTemplate.queryForObject(
"select first_name, last_name from t_actor where id = ?",
(resultSet, rowNum) -> {
Actor newActor = new Actor();
newActor.setFirstName(resultSet.getString("first_name"));
newActor.setLastName(resultSet.getString("last_name"));
return newActor;
},
1212L);
다음 쿼리는 도메인 개체 목록을 찾아서 채웁니다:
List<Actor> actors = this.jdbcTemplate.query(
"select first_name, last_name from t_actor",
(resultSet, rowNum) -> {
Actor actor = new Actor();
actor.setFirstName(resultSet.getString("first_name"));
actor.setLastName(resultSet.getString("last_name"));
return actor;
});
마지막 두 코드 조각이 실제로 동일한 애플리케이션에 존재하는 경우, 두 개의 RowMapper 람다 표현식에 존재하는 중복을 제거하고 필요에 따라 DAO 메서드에서 참조할 수 있는 단일 필드로 추출하는 것이 좋습니다. 예를 들어, 앞의 코드 조각을 다음과 같이 작성하는 것이 더 나을 수 있습니다:
private final RowMapper<Actor> actorRowMapper = (resultSet, rowNum) -> {
Actor actor = new Actor();
actor.setFirstName(resultSet.getString("first_name"));
actor.setLastName(resultSet.getString("last_name"));
return actor;
};
public List<Actor> findAllActors() {
return this.jdbcTemplate.query("select first_name, last_name from t_actor", actorRowMapper);
}
JdbcTemplate을 사용한 업데이트(INSERT, UPDATE, DELETE)
Update(..) 메서드를 사용하여 삽입, 업데이트 및 삭제 작업을 수행할 수 있습니다. 매개변수 값은 일반적으로 변수 인수로 제공되거나 객체 배열로 제공됩니다.
다음 예제는 새 항목을 삽입합니다:
this.jdbcTemplate.update(
"insert into t_actor (first_name, last_name) values (?, ?)",
"Leonor", "Watling");
다음 예제는 기존 항목을 업데이트합니다:
this.jdbcTemplate.update(
"update t_actor set last_name = ? where id = ?",
"Banjo", 5276L);
다음 예제는 항목을 삭제합니다:
this.jdbcTemplate.update(
"delete from t_actor where id = ?",
Long.valueOf(actorId));
기타 JdbcTemplate 연산
실행(..) 메서드를 사용하여 임의의 SQL을 실행할 수 있습니다. 따라서 이 메서드는 DDL 문에 자주 사용됩니다. 콜백 인터페이스, 바인딩 변수 배열 등을 사용하는 변형으로 인해 과부하가 걸립니다. 다음 예제는 테이블을 생성합니다:
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
다음 예제는 저장 프로시저를 호출합니다:
this.jdbcTemplate.update(
"call SUPPORT.REFRESH_ACTORS_SUMMARY(?)",
Long.valueOf(unionId));
보다 정교한 저장 프로시저 지원은 나중에 다룹니다.
JdbcTemplate 모범 사례
JdbcTemplate 클래스의 인스턴스는 일단 구성되면 스레드에 안전합니다. 이는 중요한데, JdbcTemplate의단일 인스턴스를 구성한 다음 이 공유 참조를 여러 DAO(또는 리포지토리)에 안전하게 주입할 수 있기 때문입니다. JdbcTemplate은 데이터 소스에 대한 참조를 유지하지만 이 상태는 대화 상태가 아니라는 점에서 상태 저장소입니다.
JdbcTemplate 클래스(및 연결된NamedParameterJdbcTemplate 클래스)를 사용하는 일반적인 방법은 Spring 구성 파일에서 DataSource를 구성한 다음 해당 공유 DataSource 빈을 DAO 클래스에 종속적으로 주입하는 것입니다. JdbcTemplate은 데이터소스에 대한 세터에서 생성됩니다. 이렇게 하면 다음과 유사한 DAO가 생성됩니다:
public class JdbcCorporateEventDao implements CorporateEventDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
다음 예제는 해당 XML 구성을 보여줍니다:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<bean id="corporateEventDao" class="com.example.JdbcCorporateEventDao">
<property name="dataSource" ref="dataSource"/>
</bean>
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
</beans>
명시적 구성의 대안은 종속성 주입을 위해 구성 요소 검색 및 어노테이션 지원을 사용하는 것입니다. 이 경우 @Repository로클래스에 주석을 달고(컴포넌트 스캔의 후보가 됨) @Autowired로 DataSource 설정자 메서드에 주석을 달 수 있습니다. 다음 예제는 그 방법을 보여줍니다:
@Repository
public class JdbcCorporateEventDao implements CorporateEventDao {
private JdbcTemplate jdbcTemplate;
@Autowired
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
// JDBC-backed implementations of the methods on the CorporateEventDao follow...
}
리포지토리로 클래스에 주석을 추가합니다. | |
DataSource 설정자 메서드에 @Autowired로 주석을 추가합니다. | |
DataSource를 사용하여 새 JdbcTemplate을 만듭니다. |
다음 예제는 해당 XML 구성을 보여줍니다:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xsi:schemaLocation="
http://www.springframework.org/schema/beans
https://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
https://www.springframework.org/schema/context/spring-context.xsd">
<!-- Scans within the base package of the application for @Component classes to configure as beans -->
<context:component-scan base-package="org.springframework.docs.test" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
</beans>
Spring의 JdbcDaoSupport 클래스를 사용하고 다양한 JDBC 지원 DAO 클래스가 이 클래스에서 확장되는 경우, 하위 클래스는JdbcDaoSupport 클래스에서 setDataSource(...) 메서드를 상속받습니다. 이 클래스에서 상속할지 여부를 선택할 수 있습니다.JdbcDaoSupport 클래스는 편의상 제공된 것입니다.
위의 템플릿 초기화 스타일 중 어떤 것을 사용하든(또는 사용하지 않든), SQL을 실행할 때마다 JdbcTemplate 클래스의 인스턴스를 새로 생성할 필요는 거의 없습니다. 일단 구성되면 JdbcTemplate 인스턴스는 스레드에 안전합니다. 애플리케이션이 여러 데이터베이스에 액세스하는 경우 여러 개의 데이터 소스와 그에 따라 서로 다르게 구성된 여러 개의 JdbcTemplate 인스턴스가 필요한 여러 개의 JdbcTemplate인스턴스가 필요할 수 있습니다.
Using NamedParameterJdbcTemplate
NamedParameterJdbcTemplate 클래스는 기존 자리 표시자( '?') 인수만 사용하여 JDBC 문을 프로그래밍하는 것과 달리 명명된 매개 변수를 사용하여 JDBC 문을 프로그래밍하는 데 대한 지원을 추가합니다. NamedParameterJdbcTemplate 클래스는JdbcTemplate을 래핑하고 래핑된 JdbcTemplate에 위임하여 대부분의 작업을 수행합니다. 이 섹션에서는 NamedParameterJdbcTemplate 클래스에서 JdbcTemplate 자체와 다른 부분, 즉 명명된 매개 변수를 사용하여 JDBC 문을 프로그래밍하는 부분만 설명합니다. 다음 예제는 NamedParameterJdbcTemplate을 사용하는 방법을 보여줍니다:
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActorsByFirstName(String firstName) {
String sql = "select count(*) from t_actor where first_name = :first_name";
SqlParameterSource namedParameters = new MapSqlParameterSource("first_name", firstName);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
Sql변수에 할당된 값에 명명된 매개변수 표기법이 사용되었으며, 해당 값이 ( MapSqlParameterSource 유형의) namedParameters변수에 연결되는 것을 확인할 수 있습니다.
또는 맵 기반 스타일을 사용하여 명명된 매개변수와 해당 값을NamedParameterJdbcTemplate 인스턴스에 전달할 수 있습니다. NamedParameterJdbcOperations에 의해 노출되고NamedParameterJdbcTemplate 클래스에 의해 구현되는 나머지 메서드도 비슷한 패턴을 따르며 여기서는 다루지 않습니다.
다음 예제는 맵 기반 스타일의 사용을 보여줍니다:
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActorsByFirstName(String firstName) {
String sql = "select count(*) from t_actor where first_name = :first_name";
Map<String, String> namedParameters = Collections.singletonMap("first_name", firstName);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
NamedParameterJdbcTemplate와 관련된(그리고 동일한 Java 패키지에 존재하는) 한 가지 멋진 기능은 SqlParameterSource 인터페이스입니다. 이전 코드 조각 중 하나에서 이 인터페이스의 구현 예제(MapSqlParameterSource 클래스)를 이미 보셨을 것입니다. SqlParameterSource는 명명된 매개변수 값의 소스로서 NamedParameterJdbcTemplate에 대한 소스입니다. MapSqlParameterSource 클래스는 키는 매개변수 이름이고 값은 매개변수 값인 java.util.Map을 둘러싼 어댑터인 간단한 구현입니다.
또 다른 SqlParameterSource 구현은 BeanPropertySqlParameterSource클래스입니다. 이 클래스는 임의의 JavaBean(즉, JavaBean 규칙을 준수하는 클래스의 인스턴스)을 래핑하고 래핑된 JavaBean의 속성을 명명된 매개변수 값의 소스로 사용합니다.
다음 예제는 일반적인 JavaBean을 보여줍니다:
public class Actor {
private Long id;
private String firstName;
private String lastName;
public String getFirstName() {
return this.firstName;
}
public String getLastName() {
return this.lastName;
}
public Long getId() {
return this.id;
}
// setters omitted...
}
다음 예제는 NamedParameterJdbcTemplate을 사용하여 앞의 예제에 표시된 클래스의 멤버 수를 반환합니다:
// some JDBC-backed DAO class...
private NamedParameterJdbcTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int countOfActors(Actor exampleActor) {
// notice how the named parameters match the properties of the above 'Actor' class
String sql = "select count(*) from t_actor where first_name = :firstName and last_name = :lastName";
SqlParameterSource namedParameters = new BeanPropertySqlParameterSource(exampleActor);
return this.namedParameterJdbcTemplate.queryForObject(sql, namedParameters, Integer.class);
}
NamedParameterJdbcTemplate 클래스는 클래식 JdbcTemplate템플릿을 래핑한다는 것을 기억하세요. 래핑된 JdbcTemplate 인스턴스에 액세스하여 JdbcTemplate 클래스에만 있는 기능에 액세스해야 하는 경우,getJdbcOperations() 메서드를 사용하여JdbcOperations 인터페이스를 통해 래핑된 JdbcTemplate에 액세스할 수 있습니다.
응용 프로그램의 컨텍스트에서 NamedParameterJdbcTemplate 클래스를 사용하는 방법에 대한 지침은 JdbcTemplate 모범 사례를참조하십시오.
Unified JDBC Query/Update Operations: JdbcClient
6.1부터 NamedParameterJdbcTemplate의 명명된 매개 변수 문과 일반 JdbcTemplate의 위치 매개 변수 문을 유창한 상호 작용 모델을 갖춘 통합 클라이언트 API를 통해 사용할 수 있습니다.
예를 들어, 위치 매개변수의 경우:
private JdbcClient jdbcClient = JdbcClient.create(dataSource);
public int countOfActorsByFirstName(String firstName) {
return this.jdbcClient.sql("select count(*) from t_actor where first_name = ?")
.param(firstName)
.query(Integer.class).single();
}
예를 들어, 명명된 매개변수의 경우:
private JdbcClient jdbcClient = JdbcClient.create(dataSource);
public int countOfActorsByFirstName(String firstName) {
return this.jdbcClient.sql("select count(*) from t_actor where first_name = :firstName")
.param("firstName", firstName)
.query(Integer.class).single();
}
유연한 결과 확인과 함께RowMapper 기능도 사용할 수 있습니다:
List<Actor> actors = this.jdbcClient.sql("select first_name, last_name from t_actor")
.query((rs, rowNum) -> new Actor(rs.getString("first_name"), rs.getString("last_name")))
.list();
사용자 지정 RowMapper 대신 매핑할 클래스를 지정할 수도 있습니다. 예를 들어 액터에 레코드 클래스, 사용자 지정 생성자, 빈 속성 또는 일반 필드로 첫 번째 이름과 마지막 이름 속성이 있다고 가정해 보겠습니다:
List<Actor> actors = this.jdbcClient.sql("select first_name, last_name from t_actor")
.query(Actor.class)
.list();
필수 단일 객체 결과를 사용합니다:
Actor actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?")
.param(1212L)
.query(Actor.class)
.single();
Java.util.Optional 결과를 사용합니다:
Optional<Actor> actor = this.jdbcClient.sql("select first_name, last_name from t_actor where id = ?")
.param(1212L)
.query(Actor.class)
.optional();
그리고 업데이트 문의 경우:
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (?, ?)")
.param("Leonor").param("Watling")
.update();
또는 명명된 매개 변수가 있는 업데이트 문:
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)")
.param("firstName", "Leonor").param("lastName", "Watling")
.update();
개별 명명된 매개 변수 대신 매개 변수 소스 객체(예: 레코드 클래스, 빈 속성이 있는 클래스 또는 위의 Actor 클래스와 같이 첫 번째 이름 및 마지막 이름 속성을 제공하는 일반 필드 홀더)를 지정할 수도 있습니다:
this.jdbcClient.sql("insert into t_actor (first_name, last_name) values (:firstName, :lastName)")
.paramSource(new Actor("Leonor", "Watling")
.update();
위의 쿼리 결과뿐만 아니라 매개변수에 대한 자동 Actor 클래스 매핑은 직접 사용할 수 있는 암시적 SimplePropertySqlParameterSource 및 SimplePropertyRowMapper전략을 통해 제공됩니다. 이 전략은 BeanPropertySqlParameterSource 및 BeanPropertyRowMapper/DataClassRowMapper를 대체할 수 있으며, JdbcTemplate 및 NamedParameterJdbcTemplate 자체로도 사용할 수 있습니다.
일괄 삽입 및 저장 프로시저 호출과 같은 고급 기능은 일반적으로 추가 사용자 정의가 필요합니다. 일괄 삽입 및 저장 프로시저 호출과 같은 고급 기능을 사용하려면 Spring의 SimpleJdbcInsert 및 SimpleJdbcCall 클래스 또는 JdbcClient에서 사용할 수 없는 일반 직접 JdbcTemplate 사용을 고려하세요 |
Using SQLExceptionTranslator
SQLExceptionTranslator는 데이터 액세스 전략과 관련하여 불가지론적인 SQLException과Spring의 자체 org.springframework.dao.DataAccessException 간에 변환할 수 있는 클래스에서 구현할 수 있는 인터페이스입니다. 구현은 보다 정밀한 구현을 위해 일반(예: JDBC용 SQLState 코드 사용) 또는 독점(예: Oracle 오류 코드 사용)이 될 수 있습니다. 이 예외 변환 메커니즘은 SQLException을 전파하지 않고 DataAccessException을 전파하는 일반적인 JdbcTemplate 및 JdbcTransactionManager 진입점 뒤에 사용됩니다.
6.0부터 기본 예외 변환기는 SQLExceptionSubclassTranslator로, 몇 가지 추가 검사를 통해 JDBC 4 SQLException 서브클래스를 감지하고 SQLStateSQLExceptionTranslator를 통해 SQLState 내성 검사로 폴백합니다. 이는 일반적으로 일반적인 데이터베이스 액세스에 충분하며 공급업체별 감지가 필요하지 않습니다. 이전 버전과의 호환성을 위해 아래 설명된 대로 사용자 지정 오류 코드 매핑과 함께 SQLErrorCodeSQLExceptionTranslator를 사용하는 것을 고려하세요 |
SQLErrorCodeSQLExceptionTranslator는 sql-error-codes.xml이라는 파일이 클래스 경로의 루트에 있을 때 기본적으로 사용되는 SQLExceptionTranslator의구현입니다. 이 구현은 특정 공급업체 코드를 사용합니다.SQLState 또는 SQLException 서브클래스 번역보다 더 정확합니다. 오류 코드 번역은 SQLErrorCodes라는 JavaBean 유형 클래스에 보관된 코드를 기반으로 합니다. 이 클래스는 이름에서 알 수 있듯이sql-error-codes.xml이라는 구성 파일의 내용을 기반으로 SQLErrorCodes를 만들기 위한 팩토리인 SQLErrorCodesFactory에 의해 생성 및 채워집니다. 이 파일은 공급업체 코드로 채워지며 DatabaseMetaData에서 가져온DatabaseProductName을 기반으로 합니다. 사용 중인 실제 데이터베이스의 코드가 사용됩니다.
SQLErrorCodeSQLExceptionTranslator는 다음 순서로 일치하는 규칙을 적용합니다:
- 서브클래스에 의해 구현된 모든 사용자 정의 번역. 일반적으로 제공된 구체적인SQLErrorCodeSQLExceptionTranslator가 사용되므로 이 규칙은 적용되지 않습니다. 실제로 서브클래스 구현을 제공한 경우에만 적용됩니다.
- SQLErrorCodes 클래스의 customSqlExceptionTranslator 속성으로 제공되는 SQLExceptionTranslator 인터페이스의 모든 사용자 지정 구현.
- 일치하는 항목이 있는지 CustomSQLErrorCodesTranslation 클래스의 인스턴스 목록( SQLErrorCodes 클래스의customTranslations 속성으로 제공됨)을 검색합니다.
- 오류 코드 일치가 적용됩니다.
- 폴백 번역기를 사용합니다. SQLExceptionSubclassTranslator가 기본 폴백 번역기입니다. 이 번역을 사용할 수 없는 경우 다음 폴백 번역기는 SQLStateSQLExceptionTranslator입니다.
기본적으로 오류 코드 및 사용자 지정 예외 번역을 정의하는 데는 SQLErrorCodesFactory가 사용됩니다. 클래스 경로에서 sql-error-codes.xml이라는 파일에서 조회되며, 사용 중인 데이터베이스의 데이터베이스 메타데이터에서 데이터베이스 이름을 기반으로 일치하는 SQLErrorCodes 인스턴스를 찾습니다 |
다음 예제에서 볼 수 있듯이 SQLErrorCodeSQLExceptionTranslator를 확장할 수 있습니다:
public class CustomSQLErrorCodesTranslator extends SQLErrorCodeSQLExceptionTranslator {
protected DataAccessException customTranslate(String task, String sql, SQLException sqlEx) {
if (sqlEx.getErrorCode() == -12345) {
return new DeadlockLoserDataAccessException(task, sqlEx);
}
return null;
}
}
앞의 예에서는 특정 오류 코드(-12345)가 번역되고 다른 오류는 기본 번역기 구현에 의해 번역되도록 남겨져 있습니다. 이 사용자 지정 번역기를 사용하려면 setExceptionTranslator 메서드를 통해 JdbcTemplate에 전달해야 하며 이 번역기가 필요한 모든 데이터 액세스 처리에 이 JdbcTemplate을 사용해야 합니다. 다음 예제는 이 사용자 지정 번역기를 사용하는 방법을 보여줍니다:
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
// create a JdbcTemplate and set data source
this.jdbcTemplate = new JdbcTemplate();
this.jdbcTemplate.setDataSource(dataSource);
// create a custom translator and set the DataSource for the default translation lookup
CustomSQLErrorCodesTranslator tr = new CustomSQLErrorCodesTranslator();
tr.setDataSource(dataSource);
this.jdbcTemplate.setExceptionTranslator(tr);
}
public void updateShippingCharge(long orderId, long pct) {
// use the prepared JdbcTemplate for this update
this.jdbcTemplate.update("update orders" +
" set shipping_charge = shipping_charge * ? / 100" +
" where id = ?", pct, orderId);
}
사용자 지정 번역기에 데이터 소스가 전달되어sql-error-codes.xml에서 오류 코드를 조회합니다.
Running Statements
SQL 문을 실행하려면 코드가 거의 필요하지 않습니다.JdbcTemplate과 함께 제공되는 편의 메서드를 포함하여 DataSource와JdbcTemplate이 필요합니다. 다음 예제에서는 새 테이블을 생성하는 최소한의 기능을 갖춘 클래스에 포함해야 할 내용을 보여줍니다:
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class ExecuteAStatement {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public void doExecute() {
this.jdbcTemplate.execute("create table mytable (id integer, name varchar(100))");
}
}
Running Queries
일부 쿼리 메서드는 단일 값을 반환합니다. 한 행에서 개수 또는 특정 값을 검색하려면 queryForObject(..)를 사용합니다. 후자는 반환된 JDBC 타입을 인수로 전달된 Java 클래스로 변환합니다. 유형 변환이 유효하지 않은 경우InvalidDataAccessApiUsageException이 발생합니다. 다음 예제에는 정수에 대한 쿼리 메서드와 문자열을 쿼리하는 쿼리 메서드 두 가지가 포함되어 있습니다:
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class RunAQuery {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int getCount() {
return this.jdbcTemplate.queryForObject("select count(*) from mytable", Integer.class);
}
public String getName() {
return this.jdbcTemplate.queryForObject("select name from mytable", String.class);
}
}
단일 결과 쿼리 메서드 외에도 쿼리가 반환한 각 행에 대한 항목이 포함된 목록을 반환하는 메서드도 여러 개 있습니다. 가장 일반적인 메서드는 열 이름을 키로 사용하여 각 요소가 각 열에 대해 하나의 항목이 포함된 맵인 리스트를 반환하는 queryForList(..)입니다. 앞의 예제에 메서드를 추가하여 모든 행의 목록을 검색하면 다음과 같이 될 수 있습니다:
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public List<Map<String, Object>> getList() {
return this.jdbcTemplate.queryForList("select * from mytable");
}
반환된 목록은 다음과 유사합니다:
[{name=Bob, id=1}, {name=Mary, id=2}]]
Updating the Database
다음 예는 특정 기본 키의 열을 업데이트합니다:
import javax.sql.DataSource;
import org.springframework.jdbc.core.JdbcTemplate;
public class ExecuteAnUpdate {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public void setName(int id, String name) {
this.jdbcTemplate.update("update mytable set name = ? where id = ?", name, id);
}
}
앞의 예제에서 SQL 문에는 행 매개변수에 대한 자리 표시자가 있습니다. 매개변수 값을 vararg로 전달하거나 객체 배열로 전달할 수 있습니다. 따라서 프리미티브 래퍼 클래스에서 프리미티브를 명시적으로 래핑하거나 자동 박싱을 사용해야 합니다.
Retrieving Auto-generated Keys
업데이트() 편의 메서드는 데이터베이스에서 생성된 기본 키의 검색을 지원합니다. 이 지원은 JDBC 3.0 표준의 일부입니다. 자세한 내용은 사양의 13.6장을 참조하십시오. 이 메서드는 첫 번째 인수로 PreparedStatementCreator를 취하며, 이것이 필요한 삽입 문을 지정하는 방식입니다. 다른 인수는 업데이트에서 성공적으로 반환될 때 생성된 키를 포함하는 KeyHolder입니다. 적절한 PreparedStatement를생성하는 표준 단일 방법은 없습니다(메서드 서명이 이러한 방식인 이유를 설명합니다). 다음 예제는 Oracle에서는 작동하지만 다른 플랫폼에서는 작동하지 않을 수 있습니다:
final String INSERT_SQL = "insert into my_test (name) values(?)";
final String name = "Rob";
KeyHolder keyHolder = new GeneratedKeyHolder();
jdbcTemplate.update(connection -> {
PreparedStatement ps = connection.prepareStatement(INSERT_SQL, new String[] { "id" });
ps.setString(1, name);
return ps;
}, keyHolder);
// keyHolder.getKey() now contains the generated key
Controlling Database Connections
Using DataSource
Spring은 DataSource를 통해 데이터베이스에 대한 연결을 가져옵니다. DataSource는 JDBC 사양의 일부이며 일반화된 연결 팩토리입니다. 이를 통해 컨테이너 또는 프레임워크는 애플리케이션 코드에서 연결 풀링 및 트랜잭션 관리 문제를 숨길 수 있습니다. 개발자는 데이터베이스에 연결하는 방법에 대한 세부 사항을 알 필요가 없습니다. 이는 데이터 소스를 설정하는 관리자의 책임입니다. 코드를 개발하고 테스트하면서 두 가지 역할을 모두 수행할 가능성이 높지만, 프로덕션 데이터 소스가 어떻게 구성되는지 반드시 알 필요는 없습니다.
Spring의 JDBC 계층을 사용하는 경우, JNDI에서 데이터 소스를 가져오거나 타사에서 제공하는 연결 풀 구현을 사용하여 직접 구성할 수 있습니다. 전통적인 선택은 빈 스타일 DataSource 클래스가 있는 Apache Commons DBCP 및 C3P0이며, 최신 JDBC 연결 풀의 경우 빌더 스타일 API가 있는 HikariCP를 대신 고려할 수 있습니다.
Spring 배포에 포함된 DriverManagerDataSource 및 SimpleDriverDataSource 클래스는 테스트 목적으로만 사용해야 합니다! 이러한 변형은 풀링을 제공하지 않으며 연결에 대한 여러 요청이 있을 때 성능이 저하됩니다 |
다음 섹션에서는 Spring의 DriverManagerDataSource 구현을 사용합니다. 다른 몇 가지 DataSource 변형은 나중에 다룹니다.
DriverManagerDataSource를 구성하려면:
- 일반적으로 JDBC 연결을 가져올 때와 마찬가지로 DriverManagerDataSource로 연결을 가져옵니다.
- DriverManager가드라이버 클래스를 로드할 수 있도록 JDBC 드라이버의 정규화된 클래스 이름을 지정합니다.
- JDBC 드라이버마다 다른 URL을 제공합니다. (올바른 값은 드라이버 설명서를 참조하세요
- 데이터베이스에 연결할 사용자 이름과 비밀번호를 입력합니다.
다음 예는 Java에서 DriverManagerDataSource를 구성하는 방법을 보여줍니다:
DriverManagerDataSource dataSource = new DriverManagerDataSource();
dataSource.setDriverClassName("org.hsqldb.jdbcDriver");
dataSource.setUrl("jdbc:hsqldb:hsql://localhost:");
dataSource.setUsername("sa");
dataSource.setPassword("");
다음 예는 해당 XML 구성을 보여줍니다:
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
다음 두 예는 DBCP 및 C3P0의 기본 연결 및 구성을 보여줍니다. 풀링 기능을 제어하는 데 도움이 되는 더 많은 옵션에 대해 알아보려면 각 연결 풀링 구현에 대한 제품 설명서를 참조하세요.
다음 예는 DBCP 구성을 보여줍니다:
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driverClassName}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
다음 예는 C3P0 구성을 보여줍니다:
<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
<property name="driverClass" value="${jdbc.driverClassName}"/>
<property name="jdbcUrl" value="${jdbc.url}"/>
<property name="user" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<context:property-placeholder location="jdbc.properties"/>
Using DataSourceUtils
DataSourceUtils 클래스는 JNDI에서 연결을 가져오고 필요한 경우 연결을 닫는정적 메서드를 제공하는 편리하고 강력한 헬퍼 클래스입니다. DataSourceTransactionManager와 함께 스레드 바운드 JDBC 연결을 지원할 뿐만 아니라 JtaTransactionManager 및 JpaTransactionManager와도 연결할 수 있습니다.
JdbcTemplate은 모든 JDBC 작업 뒤에 이를 사용하여 진행 중인 트랜잭션에 암시적으로 참여하는 DataSourceUtils 연결 액세스를 암시합니다.
Implementing SmartDataSource
SmartDataSource 인터페이스는 관계형 데이터베이스에 대한 연결을 제공할 수 있는 클래스에서 구현해야 합니다. 이 인터페이스는 DataSource 인터페이스를 확장하여 이를 사용하는 클래스가 주어진 작업 후에 연결을 닫아야 하는지 여부를 쿼리할 수 있도록 합니다. 이 사용법은 연결을 재사용해야 할 때 효율적입니다.
Extending AbstractDataSource
AbstractDataSource는 Spring의 DataSource구현을 위한 추상 베이스 클래스입니다. 모든 DataSource 구현에 공통적인 코드를 구현합니다. 자체 DataSource구현을 작성하는 경우 AbstractDataSource 클래스를 확장해야 합니다.
Using SingleConnectionDataSource
단일 연결 데이터 소스 클래스는 각 사용 후 닫히지 않는 단일 연결을 래핑하는 SmartDataSource인터페이스의 구현입니다. 이 클래스는 멀티 스레딩을 지원하지 않습니다.
클라이언트 코드가 풀링된 연결을 가정하여 닫기를 호출하는 경우(지속성 도구를 사용할 때처럼) suppressClose 속성을 true로 설정해야 합니다. 이 설정은 물리적 연결을 래핑하는 닫기 억제 프록시를 반환합니다. 더 이상 이 속성을 네이티브 Oracle 연결 또는 유사한 객체로 캐스팅할 수 없다는 점에 유의하세요.
단일 연결 데이터 소 스는 주로 테스트 클래스입니다. 일반적으로 간단한 JNDI 환경과 함께 애플리케이션 서버 외부에서 코드를 쉽게 테스트할 수 있습니다. DriverManagerDataSource와 달리 항상 동일한 연결을 재사용하므로 물리적 연결이 과도하게 생성되는 것을 방지할 수 있습니다.
Using DriverManagerDataSource
DriverManagerDataSource 클래스는 빈 속성을 통해 일반 JDBC 드라이버를 구성하고 매번 새Connection을 반환하는 표준 DataSource인터페이스의 구현입니다.
이 구현은 Spring IoC 컨테이너의 DataSource 빈으로 또는 간단한 JNDI 환경과 함께 Jakarta EE 컨테이너 외부의 테스트 및 독립 실행형 환경에 유용합니다. 풀을 가정하는 Connection.close() 호출은 연결을 닫으므로 모든 데이터 소스 인식 지속성 코드가 작동해야 합니다. 그러나 JavaBean 스타일의 연결 풀(예: commons-dbcp)을 사용하는 것은 테스트 환경에서도 매우 쉬우므로 거의 항상DriverManagerDataSource보다 이러한 연결 풀을 사용하는 것이 좋습니다.
Using TransactionAwareDataSourceProxy
트랜잭션 인식 데이터 소스 프록시는 대상 데이터 소스에 대한 프록시입니다. 이 프록시는 대상 데이터소스를 래핑하여 Spring 관리 트랜잭션에 대한 인식을 추가합니다. 이 점에서 Jakarta EE 서버에서 제공하는 트랜잭션 JNDI 데이터소스와 유사합니다.
이미 존재하는 코드를 호출하고 표준 JDBC DataSource 인터페이스 구현을 전달해야 하는 경우를 제외하고는 이 클래스를 사용하는 것이 거의 바람직하지 않습니다. 이 경우에도 이 코드를 계속 사용할 수 있으며, 동시에 이 코드가 Spring 관리 트랜잭션에 참여하도록 할 수 있습니다. 일반적으로 리소스 관리를 위한 상위 수준의 추상화(예:JdbcTemplate 또는 DataSourceUtils)를 사용하여 직접 새 코드를 작성하는 것이 좋습니다 |
자세한 내용은 트랜잭션 인식 데이터 소스 프록시자바독을 참조하세요.
Using DataSourceTransactionManager / JdbcTransactionManager
DataSourceTransactionManager 클래스는 단일 JDBC 데이터소스에 대한 PlatformTransactionManager구현입니다. 이 클래스는 지정된 데이터소스의 JDBC 연결을현재 실행 중인 스레드에 바인딩하여 데이터소스당 하나의 스레드 바인딩 연결을 허용할 수 있습니다.
애플리케이션 코드는 Java EE의 표준DataSource.getConnection 대신DataSourceUtils.getConnection(DataSource) 을 통해 JDBC 연결을 검색하는 데 필요합니다. 이것은 체크된 SQLExceptions 대신 체크되지 않은 org.springframework.dao 예외를 던집니다. 모든 프레임워크 클래스(예: JdbcTemplate)는 이 전략을 암시적으로 사용합니다. 트랜잭션 관리자와 함께 사용하지 않는 경우, 조회 전략은 DataSource.getConnection과 똑같이 작동하므로 어떤 경우에도 사용할 수 있습니다.
DataSourceTransactionManager 클래스는 적절한 JDBC 문 쿼리 시간 초과로 적용되는 저장점(PROPAGATION_NESTED), 사용자 지정 격리 수준 및 시간 초과를 지원합니다. 후자를 지원하려면 애플리케이션 코드에서 JdbcTemplate을 사용하거나 생성된 각 문에 대해 DataSourceUtils.applyTransactionTimeout(...) 메서드를 호출해야 합니다.
단일 리소스인 경우 컨테이너가 JTA 트랜잭션 코디네이터를 지원할 필요가 없으므로 JtaTransactionManager 대신 DataSourceTransactionManager를 사용할 수 있습니다. 필요한 연결 조회 패턴을 고수한다면 이러한 트랜잭션 매니저 간 전환은 구성의 문제일 뿐입니다. JTA는 세이브포인트나 사용자 정의 격리 수준을 지원하지 않으며 시간 초과 메커니즘이 다르지만, 그 외에는 JDBC 리소스 및 JDBC 커밋/롤백 관리 측면에서 유사한 동작을 노출한다는 점에 유의하세요.
실제 리소스 연결의 JTA 스타일 지연 검색을 위해 Spring은 대상 연결 풀에 해당하는 DataSource프록시 클래스를 제공합니다. 이는 실제 문 실행 없이 잠재적으로 빈 트랜잭션(이러한 시나리오에서 실제 리소스를 가져오는 경우는 없음)에 특히 유용하며, 트랜잭션 동기화 읽기 전용 플래그 또는 격리 수준을 고려하는 라우팅 DataSource 앞(예: IsolationLevelDataSourceRouter)에서도 사용할 수 있습니다.
LazyConnectionDataSourceProxy는 또한 읽기 전용 트랜잭션 중에 사용할 읽기 전용 연결 풀에 대한 특별한 지원을 제공하여, 기본 연결 풀에서 가져올 때 모든 트랜잭션의 시작과 끝에서 JDBC 연결의 읽기 전용 플래그를 전환하는 오버헤드를 피합니다(JDBC 드라이버에 따라 비용이 많이 들 수 있음).
5.3부터 Spring은 커밋/롤백 시 예외 변환 기능을 추가하는 확장된 JdbcTransactionManager 변형을 제공합니다( JdbcTemplate와 일치). DataSourceTransactionManager가 (JTA와 유사하게) TransactionSystemException만던지는 경우, JdbcTransactionManager는 데이터베이스 잠금 실패 등을 해당 DataAccessException 서브클래스로 변환합니다. 애플리케이션 코드는 이러한 예외에 대비해야 하며, 트랜잭션 시스템 예외만을 예상해서는 안 됩니다. 이러한 시나리오에서는 JdbcTransactionManager를 사용하는 것이 좋습니다 |
예외 동작 측면에서 JdbcTransactionManager는JpaTransactionManager와 거의 동일하며, R2dbcTransactionManager와도 서로를 즉각적으로 보완/대체하는 역할을 합니다. 반면에 DataSourceTransactionManager는 JtaTransactionManager와 동등하며 이를 직접 대체할 수 있습니다.
JDBC Batch Operations
대부분의 JDBC 드라이버는 동일한 준비된 문에 대한 여러 호출을 일괄 처리하는 경우 향상된 성능을 제공합니다. 업데이트를 일괄 처리로 그룹화하면 데이터베이스에 대한 왕복 횟수를 제한할 수 있습니다.
Basic Batch Operations with JdbcTemplate
특수 인터페이스의 두 가지 메서드인BatchPreparedStatementSetter를 구현하고 해당 구현을 batchUpdate 메서드 호출의 두 번째 매개 변수로 전달하여 JdbcTemplate 일괄 처리를 수행합니다. GetBatchSize 메서드를 사용하여 현재 배치의 크기를 제공할 수 있습니다. SetValues 메서드를 사용하여 준비된 문의 매개 변수에 대한 값을 설정할 수 있습니다. 이 메서드는getBatchSize 호출에서 지정한 횟수만큼 호출됩니다. 다음 예제에서는 목록의 항목을 기반으로 t_actor 테이블을 업데이트하며, 전체 목록이 배치로 사용됩니다:
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[] batchUpdate(final List<Actor> actors) {
return this.jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
new BatchPreparedStatementSetter() {
public void setValues(PreparedStatement ps, int i) throws SQLException {
Actor actor = actors.get(i);
ps.setString(1, actor.getFirstName());
ps.setString(2, actor.getLastName());
ps.setLong(3, actor.getId().longValue());
}
public int getBatchSize() {
return actors.size();
}
});
}
// ... additional methods
}
파일에서 업데이트 또는 읽기 스트림을 처리하는 경우 선호하는 배치 크기가 있지만 마지막 배치에 해당 수의 항목이 없을 수 있습니다. 이 경우 입력 소스가 모두 소진되면 일괄 처리를 중단할 수 있는 InterruptibleBatchPreparedStatementSetter 인터페이스를 사용할 수 있습니다. IsBatchExhausted 메서드를 사용하면 배치의 끝을 알릴 수 있습니다.
Batch Operations with a List of Objects
JdbcTemplate과 NamedParameterJdbcTemplate은 모두 일괄 업데이트를 제공하는 다른 방법을 제공합니다. 특별한 배치 인터페이스를 구현하는 대신 호출의 모든 매개변수 값을 목록으로 제공합니다. 프레임워크는 이러한 값을 반복하고 내부 준비된 문 설정자를 사용합니다. 명명된 매개변수를 사용하는지 여부에 따라 API가 달라집니다. 명명된 매개변수의 경우 배치의 각 멤버에 대해 하나의 항목씩SqlParameterSource 배열을 제공합니다. 이 배열을 만들려면SqlParameterSourceUtils.createBatch 편의 메서드를 사용하여 빈 스타일 객체 배열(매개변수에 해당하는 게터 메서드 포함),문자열 키 맵 인스턴스(해당 매개변수를 값으로 포함) 또는 두 가지를 혼합하여 전달할 수 있습니다.
다음 예는 명명된 매개변수를 사용한 일괄 업데이트를 보여줍니다:
public class JdbcActorDao implements ActorDao {
private NamedParameterTemplate namedParameterJdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.namedParameterJdbcTemplate = new NamedParameterJdbcTemplate(dataSource);
}
public int[] batchUpdate(List<Actor> actors) {
return this.namedParameterJdbcTemplate.batchUpdate(
"update t_actor set first_name = :firstName, last_name = :lastName where id = :id",
SqlParameterSourceUtils.createBatch(actors));
}
// ... additional methods
}
기존 ? 자리 표시자를 사용하는 SQL 문의 경우 업데이트 값이 포함된 객체 배열이 포함된 목록을 전달합니다. 이 객체 배열에는 SQL 문의 각 자리 표시자에 대해 하나의 항목이 있어야 하며, SQL 문에 정의된 것과 동일한 순서로 있어야 합니다.
다음 예제는 클래식 JDBC ? 자리 표시자를 사용한다는 점을 제외하면 앞의 예제와 동일합니다:
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[] batchUpdate(final List<Actor> actors) {
List<Object[]> batch = new ArrayList<>();
for (Actor actor : actors) {
Object[] values = new Object[] {
actor.getFirstName(), actor.getLastName(), actor.getId()};
batch.add(values);
}
return this.jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
batch);
}
// ... additional methods
}
앞서 설명한 모든 일괄 업데이트 메서드는 각 일괄 항목의 영향을 받는 행 수를 포함하는 int 배열을 반환합니다. 이 개수는 JDBC 드라이버에서 보고합니다. 카운트를 사용할 수 없는 경우 JDBC 드라이버는 -2 값을 반환합니다.
이러한 시나리오에서는 기본 PreparedStatement에서 값을 자동으로 설정하는 경우 각 값에 해당하는 JDBC 유형을 주어진 Java 유형에서 파생해야 합니다. 일반적으로 잘 작동하지만 문제가 발생할 가능성이 있습니다(예: Map에 포함된null 값의 경우). Spring은 기본적으로 이러한 경우 ParameterMetaData.getParameterType을 호출하는데, 이 경우 JDBC 드라이버를 사용하면 비용이 많이 들 수 있습니다. 최신 드라이버 버전을 사용해야 하며, 성능 문제가 발생하는 경우(Oracle 12c, JBoss 및 PostgreSQL에서 보고됨) spring.jdbc.getParameterType.ignore 속성을 true로설정(JVM 시스템 속성 또는SpringProperties 메커니즘을 통해)하는 것을 고려해야 합니다.
또는 앞서 설명한 대로 BatchPreparedStatementSetter를 통해, List<Object[]> 기반 호출에 주어진 명시적 유형 배열을 통해, 사용자 지정 MapSqlParameterSource 인스턴스에서 registerSqlType 호출을 통해 또는 null 값에 대해서도 Java 선언 속성 유형에서 SQL 유형을 파생하는 BeanPropertySqlParameterSource를통해 해당 JDBC 유형을 명시적으로 지정하는 것도 고려할 수 있습니다.
|
Batch Operations with Multiple Batches
앞의 일괄 업데이트 예제에서는 너무 커서 여러 개의 작은 배치로 나누고 싶은 배치를 처리했습니다. 앞서 언급한 메서드를 여러 번 호출하여 이 작업을 수행할 수 있지만 이제 더 편리한 방법이 있습니다. 이 메서드에는 SQL 문 외에도 매개 변수가 포함된 객체컬렉션, 각 배치에 대해 수행할 업데이트 횟수, 준비된 문의 매개 변수 값을 설정하는 ParameterizedPreparedStatementSetter가 필요합니다. 프레임워크는 제공된 값을 반복하여 업데이트 호출을 지정된 크기의 배치로 나눕니다.
다음 예는 배치 크기 100을 사용하는 배치 업데이트를 보여줍니다:
public class JdbcActorDao implements ActorDao {
private JdbcTemplate jdbcTemplate;
public void setDataSource(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
public int[][] batchUpdate(final Collection<Actor> actors) {
int[][] updateCounts = jdbcTemplate.batchUpdate(
"update t_actor set first_name = ?, last_name = ? where id = ?",
actors,
100,
(PreparedStatement ps, Actor actor) -> {
ps.setString(1, actor.getFirstName());
ps.setString(2, actor.getLastName());
ps.setLong(3, actor.getId().longValue());
});
return updateCounts;
}
// ... additional methods
}
이 호출의 배치 업데이트 메서드는 각 업데이트의 영향을 받는 행 수 배열과 함께 각 배치에 대한 배열 항목이 포함된 int 배열 배열을 반환합니다. 최상위 배열의 길이는 실행되는 배치의 수를 나타내고 두 번째 수준 배열의 길이는 해당 배치의 업데이트 횟수를 나타냅니다. 각 배치의 업데이트 수는 제공된 총 업데이트 개체 수에 따라 모든 배치에 대해 제공된 배치 크기(마지막 배치가 더 작을 수 있음)여야 합니다. 각 업데이트 문에 대한 업데이트 횟수는 JDBC 드라이버가 보고하는 횟수입니다. 개수를 사용할 수 없는 경우 JDBC 드라이버는 -2 값을 반환합니다.
Simplifying JDBC Operations with the SimpleJdbc Classes
SimpleJdbcInsert 및 SimpleJdbcCall 클래스는 JDBC 드라이버를 통해 검색할 수 있는 데이터베이스 메타데이터를 활용하여 간단한 구성을 제공합니다. 즉, 코드에서 모든 세부 정보를 제공하려는 경우 메타데이터 처리를 재정의하거나 해제할 수 있지만 미리 구성해야 하는 항목이 줄어듭니다.
Inserting Data by Using SimpleJdbcInsert
먼저 최소한의 구성 옵션이 있는 SimpleJdbcInsert 클래스를 살펴봅니다. 데이터 액세스 계층의 초기화 메서드에서 SimpleJdbcInsert를 인스턴스화해야 합니다. 이 예제에서 초기화 메서드는setDataSource 메서드입니다. SimpleJdbcInsert 클래스를 서브클래싱할 필요는 없습니다. 대신 새 인스턴스를 만들고 withTableName 메서드를 사용하여 테이블 이름을 설정할 수 있습니다. 이 클래스의 구성 메서드는 모든 구성 메서드를 체인화할 수 있는 SimpleJdbcInsert의 인스턴스를 반환하는 유동적인 스타일을 따릅니다. 다음 예제에서는 하나의 구성 메서드만 사용합니다(나중에 여러 메서드에 대한 예제를 보여드리겠습니다):
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource).withTableName("t_actor");
}
public void add(Actor actor) {
Map<String, Object> parameters = new HashMap<>(3);
parameters.put("id", actor.getId());
parameters.put("first_name", actor.getFirstName());
parameters.put("last_name", actor.getLastName());
insertActor.execute(parameters);
}
// ... additional methods
}
여기에 사용된 실행 메서드는 일반 java.util.Map을 유일한 매개변수로 사용합니다. 여기서 주의해야 할 점은 Map에 사용되는 키가 데이터베이스에 정의된 대로 테이블의 열 이름과 일치해야 한다는 것입니다. 이는 실제 삽입 문을 구성하기 위해 메타데이터를 읽기 때문입니다.
Retrieving Auto-generated Keys by Using SimpleJdbcInsert
다음 예제는 앞의 예제와 동일한 삽입을 사용하지만, ID를 전달하는 대신 자동 생성된 키를 검색하여 새 액터 객체에 설정합니다. SimpleJdbcInsert를 생성할 때 테이블 이름을 지정하는 것 외에도 usingGeneratedKeyColumns 메서드를 사용하여 생성된 키 열의 이름을 지정합니다. 다음 목록은 작동 방식을 보여줍니다:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
Map<String, Object> parameters = new HashMap<>(2);
parameters.put("first_name", actor.getFirstName());
parameters.put("last_name", actor.getLastName());
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}
이 두 번째 접근 방식을 사용하여 삽입을 실행할 때 가장 큰 차이점은 맵에 ID를 추가하지 않고 executeAndReturnKey 메서드를 호출한다는 것입니다. 이 메서드는 도메인 클래스에서 사용되는 숫자 유형의 인스턴스를 만들 수 있는java.lang.Number 객체를 반환합니다. 여기서 특정 Java 클래스를 반환하기 위해 모든 데이터베이스에 의존할 수는 없습니다. java.lang.Number는 신뢰할 수 있는 기본 클래스입니다. 자동 생성된 열이 여러 개 있거나 생성된 값이 숫자가 아닌 경우 executeAndReturnKeyHolder 메서드에서 반환되는 KeyHolder를 사용할 수 있습니다.
Specifying Columns for a SimpleJdbcInsert
다음 예제에서 볼 수 있듯이usingColumns 메서드로 열 이름 목록을 지정하여 삽입에 사용할 열을 제한할 수 있습니다:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingColumns("first_name", "last_name")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
Map<String, Object> parameters = new HashMap<>(2);
parameters.put("first_name", actor.getFirstName());
parameters.put("last_name", actor.getLastName());
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}
삽입의 실행은 메타데이터에 의존하여 사용할 열을 결정하는 것과 동일합니다.
Using SqlParameterSource to Provide Parameter Values
맵을 사용하여 매개변수 값을 제공하는 것은 잘 작동하지만 사용하기에 가장 편리한 클래스는 아닙니다. Spring은 대신 사용할 수 있는 SqlParameterSource인터페이스의 몇 가지 구현을 제공합니다. 첫 번째는 값을 포함하는 JavaBean 호환 클래스가 있는 경우 매우 편리한 클래스인 BeanPropertySqlParameterSource입니다. 이 클래스는 해당 getter 메서드를 사용하여 매개변수 값을 추출합니다. 다음 예제는 BeanPropertySqlParameterSource를 사용하는 방법을 보여줍니다:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
SqlParameterSource parameters = new BeanPropertySqlParameterSource(actor);
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}
또 다른 옵션은 Map과 비슷하지만 연쇄할 수 있는 더 편리한 addValue 메서드를 제공하는 MapSqlParameterSource입니다. 다음 예는 사용 방법을 보여줍니다:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcInsert insertActor;
public void setDataSource(DataSource dataSource) {
this.insertActor = new SimpleJdbcInsert(dataSource)
.withTableName("t_actor")
.usingGeneratedKeyColumns("id");
}
public void add(Actor actor) {
SqlParameterSource parameters = new MapSqlParameterSource()
.addValue("first_name", actor.getFirstName())
.addValue("last_name", actor.getLastName());
Number newId = insertActor.executeAndReturnKey(parameters);
actor.setId(newId.longValue());
}
// ... additional methods
}
보시다시피 구성은 동일합니다. 이러한 대체 입력 클래스를 사용하려면 실행 코드만 변경하면 됩니다.
Calling a Stored Procedure with SimpleJdbcCall
SimpleJdbcCall 클래스는 데이터베이스의 메타데이터를 사용하여 인/아웃 파라미터의 이름을 조회하므로 명시적으로 선언할 필요가 없습니다. 원하는 경우 또는 Java 클래스에 대한 자동 매핑이 없는 매개변수(예: ARRAY또는 STRUCT)가 있는 경우 매개변수를 선언할 수 있습니다. 첫 번째 예는 MySQL 데이터베이스에서 VARCHAR 및 DATE 형식의 스칼라 값만 반환하는 간단한 프로시저를 보여줍니다. 이 예제 프로시저는 지정된 액터 항목을 읽고 out 매개변수 형식으로first_name, last_name 및 birth_date 열을 반환합니다. 다음 목록은 첫 번째 예제를 보여줍니다:
CREATE PROCEDURE read_actor (
IN in_id INTEGER,
OUT out_first_name VARCHAR(100),
OUT out_last_name VARCHAR(100),
OUT out_birth_date DATE)
BEGIN
SELECT first_name, last_name, birth_date
INTO out_first_name, out_last_name, out_birth_date
FROM t_actor where id = in_id;
END;
In_id 매개변수에는 조회하려는 액터의 ID가 포함됩니다. Out매개 변수는 테이블에서 읽은 데이터를 반환합니다.
SimpleJdbcCall을 선언하는 방식은 SimpleJdbcInsert를 선언하는 것과 유사합니다. 데이터 액세스 계층의 초기화 메서드에서 클래스를 인스턴스화하고 구성해야 합니다. StoredProcedure 클래스에 비해 하위 클래스를 만들 필요가 없으며 데이터베이스 메타데이터에서 조회할 수 있는 매개 변수를 선언할 필요가 없습니다. 다음 SimpleJdbcCall 구성의 예에서는 앞의 저장 프로시저를 사용합니다( DataSource 외에 구성 옵션은 저장 프로시저의 이름뿐입니다):
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadActor;
public void setDataSource(DataSource dataSource) {
this.procReadActor = new SimpleJdbcCall(dataSource)
.withProcedureName("read_actor");
}
public Actor readActor(Long id) {
SqlParameterSource in = new MapSqlParameterSource()
.addValue("in_id", id);
Map out = procReadActor.execute(in);
Actor actor = new Actor();
actor.setId(id);
actor.setFirstName((String) out.get("out_first_name"));
actor.setLastName((String) out.get("out_last_name"));
actor.setBirthDate((Date) out.get("out_birth_date"));
return actor;
}
// ... additional methods
}
호출 실행을 위해 작성하는 코드에는 IN 매개 변수가 포함된 SqlParameterSource를만드는 것이 포함됩니다. 입력 값에 제공된 이름과 저장 프로시저에 선언된 매개변수 이름의 이름이 일치해야 합니다. 메타데이터를 사용하여 저장 프로시저에서 데이터베이스 개체를 참조하는 방법을 결정하기 때문에 대/소문자가 일치할 필요는 없습니다. 저장 프로시저의 소스에 지정된 내용이 반드시 데이터베이스에 저장되는 방식과 일치하는 것은 아닙니다. 일부 데이터베이스는 이름을 모두 대문자로 변환하는 반면, 다른 데이터베이스는 소문자를 사용하거나 지정된 대로 대소문자를 사용합니다.
실행 메서드는 IN 매개 변수를 사용하여 저장 프로시저에 지정된 대로 이름으로 키가 지정된 모든 OUT매개 변수가 포함된 Map을 반환합니다. 이 경우out_first_name, out_last_name 및 out_birth_date입니다.
실행 메서드의 마지막 부분은 검색된 데이터를 반환하는 데 사용할 액터 인스턴스를 생성합니다. 다시 말하지만, 저장 프로시저에서 선언된 대로 out 매개변수의 이름을 사용하는 것이 중요합니다. 또한 결과 맵에 저장된 아웃파라미터 이름의 대소문자가 데이터베이스의 아웃 파라미터 이름과 일치해야 하며, 이는 데이터베이스마다 다를 수 있습니다. 코드의 이식성을 높이려면 대소문자를 구분하지 않는 조회를 수행하거나 Spring에 LinkedCaseInsensitiveMap을 사용하도록 지시해야 합니다. 후자를 수행하려면 자체 JdbcTemplate을 생성하고 setResultsMapCaseInsensitive속성을 true로 설정할 수 있습니다. 그런 다음 이 사용자 정의된 JdbcTemplate 인스턴스를 SimpleJdbcCall의 생성자에 전달할 수 있습니다. 다음 예제는 이 구성을 보여줍니다:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadActor;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("read_actor");
}
// ... additional methods
}
이 작업을 수행하면 반환된 매개변수 이름에 사용되는 대/소문자 충돌을 방지할 수 있습니다.
Explicitly Declaring Parameters to Use for a SimpleJdbcCall
이 장의 앞부분에서 메타데이터에서 매개변수를 추론하는 방법을 설명했지만, 원하는 경우 명시적으로 선언할 수도 있습니다. 가변적인 수의 SqlParameter 개체를 입력으로 받는 선언 파라미터 메서드를 사용하여 SimpleJdbcCall을 생성하고 구성하면 됩니다. SqlParameter를 정의하는 방법에 대한 자세한 내용은 다음 섹션을 참조하세요.
사용하는 데이터베이스가 Spring 지원 데이터베이스가 아닌 경우 명시적 선언이 필요합니다. 현재 Spring은 다음 데이터베이스에 대한 저장 프로시저 호출의 메타데이터 조회를 지원합니다: Apache Derby, DB2, MySQL, Microsoft SQL Server, Oracle 및 Sybase. 또한 MySQL, Microsoft SQL Server 및 Oracle에 대한 저장된 함수의 메타데이터 조회도 지원합니다 |
매개변수 중 하나, 일부 또는 전부를 명시적으로 선언하도록 선택할 수 있습니다. 매개변수를 명시적으로 선언하지 않은 경우에도 매개변수 메타데이터는 계속 사용됩니다. 잠재적 매개변수에 대한 메타데이터 조회의 모든 처리를 건너뛰고 선언된 매개변수만 사용하려면 선언의 일부로 withoutProcedureColumnMetaDataAccess 메서드를 호출하면 됩니다. 데이터베이스 함수에 대해 두 개 이상의 서로 다른 호출 서명이 선언되어 있다고 가정해 보겠습니다. 이 경우 useInParameterNames를 호출하여 지정된 서명에 포함할 IN 매개변수 이름 목록을 지정합니다.
다음 예제는 완전히 선언된 프로시저 호출을 보여주며 앞의 예제의 정보를 사용합니다:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadActor;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.procReadActor = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("read_actor")
.withoutProcedureColumnMetaDataAccess()
.useInParameterNames("in_id")
.declareParameters(
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
new SqlOutParameter("out_last_name", Types.VARCHAR),
new SqlOutParameter("out_birth_date", Types.DATE)
);
}
// ... additional methods
}
두 예제의 실행 및 최종 결과는 동일합니다. 두 번째 예는 메타데이터에 의존하지 않고 모든 세부 사항을 명시적으로 지정합니다.
How to Define SqlParameters
SimpleJdbc 클래스 및 RDBMS 연산 클래스( JDBC 연산을 Java 객체로 모델링하기에서 다룸)에 대한 매개 변수를 정의하려면 SqlParameter 또는 그 하위 클래스 중 하나를 사용할 수 있습니다. 이렇게 하려면 일반적으로 생성자에서 매개 변수 이름과 SQL 유형을 지정합니다. SQL 유형은 java.sql.Types 상수를 사용하여 지정합니다. 이 장의 앞부분에서 다음과 유사한 선언을 보았습니다:
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
첫 번째 줄의 SqlParameter는 IN 매개변수를 선언합니다. IN 매개 변수는 저장 프로시저 호출과 쿼리 모두에 사용할 수 있으며, SqlQuery와 그 하위 클래스( SqlQuery 이해에서 다룸)를 사용하여 쿼리에 사용할 수 있습니다.
두 번째 줄( SqlOutParameter 포함)은 저장 프로시저 호출에 사용할 out 매개 변수를 선언합니다. InOut 매개 변수(프로시저에 IN 값을 제공하고 값도 반환하는 매개 변수)를 위한 SqlInOutParameter도 있습니다.
입력 값을 제공하는 데는 SqlParameter 및 SqlInOutParameter로 선언된 매개변수만 사용됩니다. 이는 (이전 버전과의 호환성을 위해) SqlOutParameter로 선언된 매개변수에 대해 입력 값을 제공할 수 있는 StoredProcedure 클래스와는 다릅니다 |
IN 매개변수의 경우 이름과 SQL 유형 외에도 숫자 데이터의 경우 배율을 지정하거나 사용자 지정 데이터베이스 유형의 경우 유형 이름을 지정할 수 있습니다. OUT 매개변수의 경우, REF 커서에서 반환된 행의 매핑을 처리하는 RowMapper를 제공할 수 있습니다. 또 다른 옵션은 반환 값의 사용자 정의 처리를 정의할 수 있는 기회를 제공하는 SqlReturnType을 지정하는 것입니다.
Calling a Stored Function by Using SimpleJdbcCall
프로시저 이름 대신 함수 이름을 제공한다는 점을 제외하면 저장 프로시저를 호출하는 것과 거의 동일한 방식으로 저장된 함수를 호출할 수 있습니다. 함수를 호출할 것을 나타내기 위해 구성의 일부로withFunctionName 메서드를 사용하면 함수 호출에 해당하는 문자열이 생성됩니다. 함수를 실행하기 위해 특수 호출(실행 함수)이 사용되며 함수 반환값을 지정된 유형의 객체로 반환하므로 결과 맵에서 반환값을 검색할 필요가 없습니다. 이와 유사한 편리한 메서드( 실행 오브젝트라는 이름)는 아웃매개변수가 하나만 있는 저장 프로시저에도 사용할 수 있습니다. 다음 예제(MySQL용)는 액터의 전체 이름을 반환하는 get_actor_name이라는저장된 함수를 기반으로 합니다:
CREATE FUNCTION get_actor_name (in_id INTEGER)
RETURNS VARCHAR(200) READS SQL DATA
BEGIN
DECLARE out_name VARCHAR(200);
SELECT concat(first_name, ' ', last_name)
INTO out_name
FROM t_actor where id = in_id;
RETURN out_name;
END;
이 함수를 호출하기 위해 다음 예제와 같이 초기화 메서드에서 다시 SimpleJdbcCall을 생성합니다:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall funcGetActorName;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.funcGetActorName = new SimpleJdbcCall(jdbcTemplate)
.withFunctionName("get_actor_name");
}
public String getActorName(Long id) {
SqlParameterSource in = new MapSqlParameterSource()
.addValue("in_id", id);
String name = funcGetActorName.executeFunction(String.class, in);
return name;
}
// ... additional methods
}
사용된 실행 함수 메서드는 함수 호출의 반환값이 포함된 문자열을 반환합니다.
Returning a ResultSet or REF Cursor from a SimpleJdbcCall
결과 집합을 반환하는 저장 프로시저나 함수를 호출하는 것은 약간 까다롭습니다. 일부 데이터베이스는 JDBC 결과 처리 중에 결과 집합을 반환하는 반면, 다른 데이터베이스는 특정 유형의 매개 변수를 명시적으로 등록해야 합니다. 두 접근 방식 모두 결과 집합을 반복하고 반환된 행을 처리하기 위해 추가 처리가 필요합니다. SimpleJdbcCall을 사용하면 반환ResultSet 메서드를 사용하고 특정 매개변수에 사용할 RowMapper구현을 선언할 수 있습니다. 결과 처리 중에 결과 집합이 반환되는 경우, 정의된 이름이 없으므로 반환된 결과는 RowMapper구현을 선언한 순서와 일치해야 합니다. 지정된 이름은 실행 문에서 반환되는 결과 맵에 처리된 결과 목록을 저장하는 데 여전히 사용됩니다.
다음 예제(MySQL용)는 IN 매개 변수를 사용하지 않고 t_actor 테이블의 모든 행을 반환하는 저장 프로시저를 사용합니다:
CREATE PROCEDURE read_all_actors()
BEGIN
SELECT a.id, a.first_name, a.last_name, a.birth_date FROM t_actor a;
END;
이 프로시저를 호출하려면 RowMapper를 선언하면 됩니다. 매핑하려는 클래스는 JavaBean 규칙을 따르므로, 새로운 인스턴스 메서드에서 매핑할 필수 클래스를 전달하여 생성된 BeanPropertyRowMapper를 사용할 수 있습니다. 다음 예에서는 그 방법을 보여 줍니다:
public class JdbcActorDao implements ActorDao {
private SimpleJdbcCall procReadAllActors;
public void setDataSource(DataSource dataSource) {
JdbcTemplate jdbcTemplate = new JdbcTemplate(dataSource);
jdbcTemplate.setResultsMapCaseInsensitive(true);
this.procReadAllActors = new SimpleJdbcCall(jdbcTemplate)
.withProcedureName("read_all_actors")
.returningResultSet("actors",
BeanPropertyRowMapper.newInstance(Actor.class));
}
public List getActorsList() {
Map m = procReadAllActors.execute(new HashMap<String, Object>(0));
return (List) m.get("actors");
}
// ... additional methods
}
실행 호출은 매개 변수를 받지 않으므로 빈 맵을 전달합니다. 그런 다음 결과 맵에서 액터 목록을 검색하여 호출자에게 반환합니다.
Modeling JDBC Operations as Java Objects
Org.springframework.jdbc.object 패키지에는 보다 객체 지향적인 방식으로 데이터베이스에 액세스할 수 있는 클래스가 포함되어 있습니다. 예를 들어 쿼리를 실행하고 비즈니스 객체의 속성에 매핑된 관계형 열 데이터가 포함된 비즈니스 객체가 포함된 목록으로 결과를 다시 가져올 수 있습니다. 또한 저장 프로시저를 실행하고 업데이트, 삭제 및 삽입 문을 실행할 수도 있습니다.
많은 Spring 개발자들은 아래에 설명된 다양한 RDBMS 연산 클래스( StoredProcedure 클래스 제외)를 곧바로 JdbcTemplate 호출로 대체할 수 있다고 생각합니다. 쿼리를 전체 클래스로 캡슐화하는 대신 JdbcTemplate의 메서드를 직접 호출하는 DAO 메서드를 작성하는 것이 더 간단할 때가 많습니다(쿼리를 전체 클래스로 캡슐화하는 것과 반대).
그러나 RDBMS 작업 클래스를 사용하여 측정 가능한 값을 얻는다면 이러한 클래스를 계속 사용해야 합니다.
|
Understanding SqlQuery
SqlQuery는 SQL 쿼리를 캡슐화하는 재사용 가능한 스레드 안전 클래스입니다. 서브클래스는 쿼리 실행 중에 생성되는 ResultSet을 반복하여 얻은 행당 하나의 객체를 생성할 수 있는 RowMapper 인스턴스를 제공하기 위해 newRowMapper(..) 메서드를 구현해야 합니다. 행을 Java 클래스에 매핑하기 위한 훨씬 더 편리한 구현을 제공하는 MappingSqlQuery 서브클래스가 있기 때문에 SqlQuery 클래스는 거의 직접적으로 사용되지 않습니다. SqlQuery를 확장하는 다른 구현으로는MappingSqlQueryWithParameters 및 UpdatableSqlQuery가 있습니다.
Using MappingSqlQuery
MappingSqlQuery는 재사용 가능한 쿼리로, 구체적인 서브클래스가 추상적인 mapRow(..) 메서드를 구현하여 제공된 ResultSet의 각 행을 지정된 유형의 객체로 변환해야 합니다. 다음 예는 t_actor 관계의 데이터를 Actor 클래스의 인스턴스에 매핑하는 사용자 지정 쿼리를 보여줍니다:
public class ActorMappingQuery extends MappingSqlQuery<Actor> {
public ActorMappingQuery(DataSource ds) {
super(ds, "select id, first_name, last_name from t_actor where id = ?");
declareParameter(new SqlParameter("id", Types.INTEGER));
compile();
}
@Override
protected Actor mapRow(ResultSet rs, int rowNumber) throws SQLException {
Actor actor = new Actor();
actor.setId(rs.getLong("id"));
actor.setFirstName(rs.getString("first_name"));
actor.setLastName(rs.getString("last_name"));
return actor;
}
}
이 클래스는 Actor 유형으로 매개변수화된 MappingSqlQuery를 확장합니다. 이 고객 쿼리의 생성자는 DataSource를 유일한 매개 변수로 사용합니다. 이 생성자에서는 이 쿼리의 행을 검색하기 위해 실행해야 하는 SQL과 DataSource를 사용하여 수퍼클래스의 생성자를 호출할 수 있습니다. 이 SQL은 준비된 문을 생성하는 데 사용되므로 실행 중에 전달할 매개 변수에 대한 자리 표시자를 포함할 수 있습니다. 각 매개변수는 SqlParameter를 전달하는 declareParameter메서드를 사용하여 선언해야 합니다. SqlParameter는 이름과 java.sql.Types에 정의된 JDBC 유형을 취합니다. 모든 매개변수를 정의한 후compile() 메서드를 호출하여 문을 준비하고 나중에 실행할 수 있습니다. 이 클래스는 컴파일된 후에도 스레드 안전하므로 DAO가 초기화될 때 이러한 인스턴스가 생성되는 한 인스턴스 변수로 유지되어 재사용할 수 있습니다. 다음 예제는 이러한 클래스를 정의하는 방법을 보여줍니다:
private ActorMappingQuery actorMappingQuery;
@Autowired
public void setDataSource(DataSource dataSource) {
this.actorMappingQuery = new ActorMappingQuery(dataSource);
}
public Actor getActor(Long id) {
return actorMappingQuery.findObject(id);
}
앞의 예시에서 메서드는 유일한 파라미터로 전달된 ID를 가진 액터를 검색합니다. 하나의 객체만 반환하고 싶기 때문에 id를 매개변수로 사용하여 findObject 편의 메서드를 호출합니다. 대신 객체 목록을 반환하고 추가 매개변수를 받는 쿼리가 있다면, 변수로 전달된 매개변수 값의 배열을 취하는 실행메서드 중 하나를 사용합니다. 다음 예제는 이러한 메서드를 보여줍니다:
public List<Actor> searchForActors(int age, String namePattern) {
return actorSearchMappingQuery.execute(age, namePattern);
}
Using SqlUpdate
SqlUpdate 클래스는 SQL 업데이트를 캡슐화합니다. 쿼리와 마찬가지로 업데이트 객체는 재사용이 가능하며, 모든 RdbmsOperation 클래스와 마찬가지로 업데이트는 매개 변수를 가질 수 있으며 SQL로 정의됩니다. 이 클래스는 쿼리 개체의실행(...) 메서드와 유사한 여러 update(...) 메서드를 제공합니다. SqlUpdate 클래스는 구체적입니다. 예를 들어 사용자 정의 업데이트 메서드를 추가하기 위해 서브클래싱할 수 있습니다. 그러나 SQL을 설정하고 매개변수를 선언하여 쉽게 매개변수화할 수 있으므로 SqlUpdate클래스를 서브클래싱할 필요는 없습니다. 다음 예에서는 execute라는 사용자 정의 업데이트 메서드를 생성합니다:
import java.sql.Types;
import javax.sql.DataSource;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;
public class UpdateCreditRating extends SqlUpdate {
public UpdateCreditRating(DataSource ds) {
setDataSource(ds);
setSql("update customer set credit_rating = ? where id = ?");
declareParameter(new SqlParameter("creditRating", Types.NUMERIC));
declareParameter(new SqlParameter("id", Types.NUMERIC));
compile();
}
/**
* @param id for the Customer to be updated
* @param rating the new value for credit rating
* @return number of rows updated
*/
public int execute(int id, int rating) {
return update(rating, id);
}
}
Using StoredProcedure
StoredProcedure 클래스는 RDBMS 저장 프로시저의 객체 추상화를 위한 추상 슈퍼클래스입니다.
상속된 sql 속성은 RDBMS에서 저장 프로시저의 이름입니다.
StoredProcedure 클래스에 대한 매개 변수를 정의하려면 SqlParameter 또는 그 하위 클래스 중 하나를 사용할 수 있습니다. 다음 코드 스니펫에 표시된 것처럼 생성자에서 매개변수 이름과 SQL 유형을 지정해야 합니다:
new SqlParameter("in_id", Types.NUMERIC),
new SqlOutParameter("out_first_name", Types.VARCHAR),
SQL 유형은 java.sql.Types 상수를 사용하여 지정합니다.
첫 번째 줄( SqlParameter 포함)은 IN 매개변수를 선언합니다. IN 매개 변수는 저장 프로시저 호출과 SqlQuery 및 그 서브클래스를 사용하는 쿼리 모두에 사용할 수 있습니다( SqlQuery 이해에서 다룸).
두 번째 줄( SqlOutParameter 포함)은 저장 프로시저 호출에 사용할 out 매개 변수를 선언합니다. InOut 매개 변수(프로시저에 in 값을 제공하고 값을 반환하는 매개 변수)를 위한 SqlInOutParameter도 있습니다.
In 매개변수의 경우 이름과 SQL 유형 외에도 숫자 데이터의 경우 배율을 지정하거나 사용자 지정 데이터베이스 유형의 경우 유형 이름을 지정할 수 있습니다. Out 매개변수의 경우, REF 커서에서 반환되는 행의 매핑을 처리하는 RowMapper를 제공할 수 있습니다. 또 다른 옵션은 반환 값의 사용자 지정 처리를 정의할 수 있는 SqlReturnType을 지정하는 것입니다.
다음 간단한 DAO의 예는 모든 Oracle 데이터베이스와 함께 제공되는 함수(sysdate())를 호출하기 위해 StoredProcedure를 사용합니다. 저장 프로시저 기능을 사용하려면 StoredProcedure를 확장하는 클래스를 만들어야 합니다. 이 예제에서 StoredProcedure 클래스는 내부 클래스입니다. 그러나StoredProcedure를 재사용해야 하는 경우 최상위 클래스로 선언할 수 있습니다. 이 예제에는 입력 매개변수가 없지만 출력 매개변수는SqlOutParameter 클래스를 사용하여 날짜 유형으로 선언됩니다. 실행() 메서드는 프로시저를 실행하고 결과 Map에서 반환된 날짜를 추출합니다. 결과 Map에는 매개변수 이름을 키로 사용하여 선언된 각 출력 매개변수(이 경우 하나만)에 대한 항목이 있습니다. 다음 목록은 사용자 지정 StoredProcedure 클래스를 보여줍니다:
import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class StoredProcedureDao {
private GetSysdateProcedure getSysdate;
@Autowired
public void init(DataSource dataSource) {
this.getSysdate = new GetSysdateProcedure(dataSource);
}
public Date getSysdate() {
return getSysdate.execute();
}
private class GetSysdateProcedure extends StoredProcedure {
private static final String SQL = "sysdate";
public GetSysdateProcedure(DataSource dataSource) {
setDataSource(dataSource);
setFunction(true);
setSql(SQL);
declareParameter(new SqlOutParameter("date", Types.DATE));
compile();
}
public Date execute() {
// the 'sysdate' sproc has no input parameters, so an empty Map is supplied...
Map<String, Object> results = execute(new HashMap<String, Object>());
Date sysdate = (Date) results.get("date");
return sysdate;
}
}
}
다음 StoredProcedure 예제에는 두 개의 출력 매개변수(이 경우 Oracle REF 커서)가 있습니다:
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class TitlesAndGenresStoredProcedure extends StoredProcedure {
private static final String SPROC_NAME = "AllTitlesAndGenres";
public TitlesAndGenresStoredProcedure(DataSource dataSource) {
super(dataSource, SPROC_NAME);
declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
declareParameter(new SqlOutParameter("genres", OracleTypes.CURSOR, new GenreMapper()));
compile();
}
public Map<String, Object> execute() {
// again, this sproc has no input parameters, so an empty Map is supplied
return super.execute(new HashMap<String, Object>());
}
}
TitlesAndGenresStoredProcedure 생성자에서 사용된 선언 파라미터(...) 메서드의 오버로드된 변형이 어떻게 RowMapper구현 인스턴스로 전달되는지 주목하세요. 이는 기존 기능을 재사용할 수 있는 매우 편리하고 강력한 방법입니다. 다음 두 예제는 두 개의 RowMapper 구현에 대한 코드를 제공합니다.
TitleMapper 클래스는 다음과 같이 제공된 ResultSet의 각 행에 대해 ResultSet을 Title 도메인 객체에 매핑합니다:
import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Title;
import org.springframework.jdbc.core.RowMapper;
public final class TitleMapper implements RowMapper<Title> {
public Title mapRow(ResultSet rs, int rowNum) throws SQLException {
Title title = new Title();
title.setId(rs.getLong("id"));
title.setName(rs.getString("name"));
return title;
}
}
장르맵퍼 클래스는 다음과 같이 제공된 결과 집합의 각 행에 대해 장르 도메인 객체에 결과 집합을 매핑합니다:
import java.sql.ResultSet;
import java.sql.SQLException;
import com.foo.domain.Genre;
import org.springframework.jdbc.core.RowMapper;
public final class GenreMapper implements RowMapper<Genre> {
public Genre mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Genre(rs.getString("name"));
}
}
RDBMS의 정의에 하나 이상의 입력 매개변수가 있는 저장 프로시저에 매개변수를 전달하려면 다음 예제와 같이 슈퍼클래스에서 유형이 지정되지 않은 실행(Map) 메서드로 위임하는 강력한 유형의 실행(...) 메서드를 코딩할 수 있습니다:
import java.sql.Types;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import javax.sql.DataSource;
import oracle.jdbc.OracleTypes;
import org.springframework.jdbc.core.SqlOutParameter;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.StoredProcedure;
public class TitlesAfterDateStoredProcedure extends StoredProcedure {
private static final String SPROC_NAME = "TitlesAfterDate";
private static final String CUTOFF_DATE_PARAM = "cutoffDate";
public TitlesAfterDateStoredProcedure(DataSource dataSource) {
super(dataSource, SPROC_NAME);
declareParameter(new SqlParameter(CUTOFF_DATE_PARAM, Types.DATE);
declareParameter(new SqlOutParameter("titles", OracleTypes.CURSOR, new TitleMapper()));
compile();
}
public Map<String, Object> execute(Date cutoffDate) {
Map<String, Object> inputs = new HashMap<String, Object>();
inputs.put(CUTOFF_DATE_PARAM, cutoffDate);
return super.execute(inputs);
}
}
Common Problems with Parameter and Data Value Handling
매개변수 및 데이터 값과 관련된 일반적인 문제는 Spring 프레임워크의 JDBC 지원에서 제공하는 다양한 접근 방식에 존재합니다. 이 섹션에서는 이러한 문제를 해결하는 방법을 다룹니다.
Providing SQL Type Information for Parameters
일반적으로 Spring은 전달된 매개변수의 유형에 따라 매개변수의 SQL 유형을 결정합니다. 매개변수 값을 설정할 때 사용할 SQL 유형을 명시적으로 제공할 수 있습니다. 이는 때때로 NULL 값을 올바르게 설정하기 위해 필요합니다.
여러 가지 방법으로 SQL 유형 정보를 제공할 수 있습니다:
- JdbcTemplate의 많은 업데이트 및 쿼리 메서드는 int 배열 형식의 추가 매개 변수를 사용합니다. 이 배열은 java.sql.Types 클래스의 상수 값을 사용하여 해당 매개 변수의 SQL 유형을 나타내는 데 사용됩니다. 각 매개변수에 대해 하나의 항목을 입력합니다.
- 이 추가 정보가 필요한 매개변수 값을 래핑하기 위해 SqlParameterValue 클래스를 사용할 수 있습니다. 이렇게 하려면 각 값에 대해 새 인스턴스를 만들고 생성자에서 SQL 유형과 매개변수 값을 전달합니다. 숫자 값에 대한 선택적 스케일 매개변수를 제공할 수도 있습니다.
- 명명된 매개변수로 작동하는 메서드의 경우, SqlParameterSource 클래스,BeanPropertySqlParameterSource 또는 MapSqlParameterSource를 사용할 수 있습니다. 이 두 클래스에는 명명된 매개변수 값에 대한 SQL 유형을 등록하는 메서드가 있습니다.
Handling BLOB and CLOB objects
데이터베이스에 이미지, 기타 바이너리 데이터, 대용량 텍스트 덩어리를 저장할 수 있습니다. 이러한 대용량 객체를 바이너리 데이터의 경우 BLOB(Binary Large OBject), 문자 데이터의 경우 CLOB(Character Large OBject)라고 합니다. Spring에서는 이러한 대형 객체를 처리하기 위해 JdbcTemplate을 직접 사용할 수도 있고, RDBMS 객체와 SimpleJdbc 클래스에서 제공하는 상위 추상화를 사용할 수도 있습니다. 이러한 모든 접근 방식은 LOB(대용량 객체) 데이터의 실제 관리를 위해 LobHandler 인터페이스의 구현을 사용합니다.LobHandler는 삽입할 새 LOB 객체를 생성하는 데 사용되는 getLobCreator 메서드를 통해 LobCreator 클래스에 대한 액세스를 제공합니다.
LobCreator와 LobHandler는 LOB 입력 및 출력에 대해 다음과 같은 지원을 제공합니다:
- BLOB
-
- byte[]: getBlobAsBytes 및 setBlobAsBytes
- InputStream: getBlobAsBinaryStream 및 setBlobAsBinaryStream
- CLOB
-
- String: getClobAsString 및 setClobAsString
- InputStream: getClobAsAsciiStream 및 setClobAsAsciiStream
- 판독기: getClobAsCharacterStream 및 setClobAsCharacterStream
다음 예제는 BLOB을 생성하고 삽입하는 방법을 보여줍니다. 나중에 데이터베이스에서 다시 읽는 방법을 보여드리겠습니다.
이 예제에서는 JdbcTemplate과AbstractLobCreatingPreparedStatementCallback의 구현을 사용합니다. 이 예제에서는setValues라는 메서드 하나를 구현합니다. 이 메서드는 SQL 삽입 문에서 LOB 열의 값을 설정하는 데 사용하는 LobCreator를 제공합니다.
이 예제에서는 이미 DefaultLobHandler의 인스턴스로 설정된 변수 lobHandler가 있다고 가정합니다. 일반적으로 종속성 주입을 통해 이 값을 설정합니다.
다음 예제는 BLOB을 생성하고 삽입하는 방법을 보여줍니다:
final File blobIn = new File("spring2004.jpg");
final InputStream blobIs = new FileInputStream(blobIn);
final File clobIn = new File("large.txt");
final InputStream clobIs = new FileInputStream(clobIn);
final InputStreamReader clobReader = new InputStreamReader(clobIs);
jdbcTemplate.execute(
"INSERT INTO lob_table (id, a_clob, a_blob) VALUES (?, ?, ?)",
new AbstractLobCreatingPreparedStatementCallback(lobHandler) {
protected void setValues(PreparedStatement ps, LobCreator lobCreator) throws SQLException {
ps.setLong(1, 1L);
lobCreator.setClobAsCharacterStream(ps, 2, clobReader, (int)clobIn.length());
lobCreator.setBlobAsBinaryStream(ps, 3, blobIs, (int)blobIn.length());
}
}
);
blobIs.close();
clobReader.close();
(이 예제에서는) 일반 DefaultLobHandler인 lobHandler를 전달합니다. | |
SetClobAsCharacterStream 메서드를 사용하여 CLOB의 내용을 전달합니다. | |
SetBlobAsBinaryStream 메서드를 사용하여 BLOB의 내용을 전달합니다. |
DefaultLobHandler.getLobCreator()에서 반환된 LobCreator에서 setBlobAsBinaryStream, setClobAsAsciiStream 또는setClobAsCharacterStream 메서드를 호출하는 경우 선택적으로 contentLength 인수에 음수값을 지정할 수 있습니다. 지정된 콘텐츠 길이가 음수인 경우,DefaultLobHandler는 길이 매개변수 없이 JDBC 4.0의 변형된 set-stream 메서드를 사용합니다. 그렇지 않으면 지정된 길이를 드라이버에 전달합니다.
콘텐츠 길이를 제공하지 않고 LOB 스트리밍을 지원하는지 확인하려면 사용하는 JDBC 드라이버의 설명서를 참조하세요.
|
이제 데이터베이스에서 LOB 데이터를 읽을 차례입니다. 여기에서도 동일한 인스턴스 변수 lobHandler와 DefaultLobHandler에 대한 참조가 있는 JdbcTemplate을사용합니다. 다음 예는 그 방법을 보여줍니다:
List<Map<String, Object>> l = jdbcTemplate.query("select id, a_clob, a_blob from lob_table",
new RowMapper<Map<String, Object>>() {
public Map<String, Object> mapRow(ResultSet rs, int i) throws SQLException {
Map<String, Object> results = new HashMap<String, Object>();
String clobText = lobHandler.getClobAsString(rs, "a_clob");
results.put("CLOB", clobText);
byte[] blobBytes = lobHandler.getBlobAsBytes(rs, "a_blob");
results.put("BLOB", blobBytes);
return results;
}
});
GetClobAsString 메서드를 사용하여 CLOB의 내용을 검색합니다. | |
GetBlobAsBytes 메서드를 사용하여 BLOB의 내용을 검색합니다. |
Passing in Lists of Values for IN Clause
The SQL standard allows for selecting rows based on an expression that includes a variable list of values. A typical example would be select * from T_ACTOR where id in (1, 2, 3). This variable list is not directly supported for prepared statements by the JDBC standard. You cannot declare a variable number of placeholders. You need a number of variations with the desired number of placeholders prepared, or you need to generate the SQL string dynamically once you know how many placeholders are required. The named parameter support provided in the NamedParameterJdbcTemplate takes the latter approach. You can pass in the values as a java.util.List (or any Iterable) of simple values. This list is used to insert the required placeholders into the actual SQL statement and pass in the values during statement execution.
Be careful when passing in many values. The JDBC standard does not guarantee that you can use more than 100 values for an IN expression list. Various databases exceed this number, but they usually have a hard limit for how many values are allowed. For example, Oracle’s limit is 1000. |
In addition to the primitive values in the value list, you can create a java.util.List of object arrays. This list can support multiple expressions being defined for the in clause, such as select * from T_ACTOR where (id, last_name) in ((1, 'Johnson'), (2, 'Harrop')). This, of course, requires that your database supports this syntax.
Handling Complex Types for Stored Procedure Calls
저장 프로시저를 호출할 때 데이터베이스에 특정한 복잡한 타입을 사용할 수 있습니다. 이러한 유형을 수용하기 위해 Spring은 저장 프로시저 호출에서 반환될 때 이를 처리하기 위한 SqlReturnType과 저장 프로시저에 매개 변수로 전달될 때 SqlTypeValue를 제공합니다.
SqlReturnType 인터페이스에는 구현해야 하는 단일 메서드( getTypeValue라는 이름)가 있습니다. 이 인터페이스는 SqlOutParameter 선언의 일부로 사용됩니다. 다음 예제에서는 사용자가 선언한 유형 ITEM_TYPE의 Oracle STRUCT 객체 값을 반환하는 방법을 보여 줍니다:
public class TestItemStoredProcedure extends StoredProcedure {
public TestItemStoredProcedure(DataSource dataSource) {
// ...
declareParameter(new SqlOutParameter("item", OracleTypes.STRUCT, "ITEM_TYPE",
(CallableStatement cs, int colIndx, int sqlType, String typeName) -> {
STRUCT struct = (STRUCT) cs.getObject(colIndx);
Object[] attr = struct.getAttributes();
TestItem item = new TestItem();
item.setId(((Number) attr[0]).longValue());
item.setDescription((String) attr[1]);
item.setExpirationDate((java.util.Date) attr[2]);
return item;
}));
// ...
}
SqlTypeValue를 사용하여 Java 객체(예: TestItem)의 값을 저장 프로시저에 전달할 수 있습니다. SqlTypeValue 인터페이스에는 구현해야 하는 단일 메서드(createTypeValue라는 이름)가 있습니다. 활성 연결이 전달되며, 이를 사용하여 StructDescriptor 인스턴스 또는 ArrayDescriptor 인스턴스와 같은 데이터베이스별 객체를 만들 수 있습니다. 다음 예제는 StructDescriptor 인스턴스를 생성합니다:
final TestItem testItem = new TestItem(123L, "A test item",
new SimpleDateFormat("yyyy-M-d").parse("2010-12-31"));
SqlTypeValue value = new AbstractSqlTypeValue() {
protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
StructDescriptor itemDescriptor = new StructDescriptor(typeName, conn);
Struct item = new STRUCT(itemDescriptor, conn,
new Object[] {
testItem.getId(),
testItem.getDescription(),
new java.sql.Date(testItem.getExpirationDate().getTime())
});
return item;
}
};
이제 저장 프로시저의실행 호출을 위한 입력 매개변수가 포함된 이 SqlTypeValue를 Map에 추가할 수 있습니다.
SqlTypeValue의 또 다른 용도는 Oracle 저장 프로시저에 값 배열을 전달하는 것입니다. Oracle에는 이 경우에 사용해야 하는 자체 내부 ARRAY 클래스가 있으며, 다음 예제에서 볼 수 있듯이 SqlTypeValue를 사용하여 Oracle ARRAY의 인스턴스를 생성하고 Java ARRAY의 값으로 이를 채울 수 있습니다:
final Long[] ids = new Long[] {1L, 2L};
SqlTypeValue value = new AbstractSqlTypeValue() {
protected Object createTypeValue(Connection conn, int sqlType, String typeName) throws SQLException {
ArrayDescriptor arrayDescriptor = new ArrayDescriptor(typeName, conn);
ARRAY idArray = new ARRAY(arrayDescriptor, conn, ids);
return idArray;
}
};
https://docs.spring.io/spring-framework/reference/data-access/jdbc/embedded-database-support.html
여기서부터 번역 필요
'언어 > Spring Docs 번역' 카테고리의 다른 글
Testing (2) (0) | 2024.07.07 |
---|---|
Testing (1) (0) | 2024.07.07 |
Core Technologies / Null-safety, Data Buffers and Codecs, Logging, Ahead of Time Optimizations (0) | 2024.07.07 |
Core Technologies / Spring AOP APIs (0) | 2024.07.07 |
Core Technologies / Aspect Oriented Programming with Spring (0) | 2024.07.07 |