SimpleJpaRepository
먼저 Spring JPA에서 기본으로 구현된 SimpleJpaRepository 코드를 확인했다.
SimpleJpaRespository
는 전체 메소드에 @Transactional(readOnly = true)
를 적용하고 save, delete를 수행하는 메소드에서는 @Transactional
에 적용한 것을 확인했다. 일반적으로 조회할 때 사용하는 메소드 findById, findAll
, 저장할 때 사용하는 메소드 save
가 SimpleJpaRepository
에서 구현된 메소드를 호출한다는 사실도 알게되었다. 따라서 직접 @Transactional
을 추가/적용하지 않아도 Transaction이 걸리는 것을 로그를 출력하여 확인했다.
@Transactional을 직접 추가했을 경우와 하지 않았을 경우 로그 비교
아래는 @Transactional을 추가하지 않은 경우
의 JPA 로그이다.
[nio-8080-exec-4] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [org.springframework.data.jpa.repository.support.SimpleJpaRepository.save]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
[nio-8080-exec-4] o.s.orm.jpa.JpaTransactionManager : Opened new EntityManager [SessionImpl(1165081108<open>)] for JPA transaction
[nio-8080-exec-4] o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@64362f08]
Hibernate:
insert
into
wastes
(created_at, modified_at, address, content, file_name, like_count, member_id, sell_status, title, transaction_at, view_count, waste_category, waste_price, waste_status)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
[nio-8080-exec-4] o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
[nio-8080-exec-4] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(1165081108<open>)]
[nio-8080-exec-4] o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(1165081108<open>)] after transaction
간단하게 정리하면 다음과 같다.
- Create new transaction
- Open new EntityManager
- Expose JPA transaction
- insert 쿼리 수행
- Initiating transaction commit
- Committing JPA transaction
- Closing JPA EntityManager
그리고 @Transactional을 추가한 경우
의 JPA 로그이다.
[nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Creating new transaction with name [freshtrash.freshtrashbackend.service.WasteService.addWaste]: PROPAGATION_REQUIRED,ISOLATION_DEFAULT
[nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Opened new EntityManager [SessionImpl(826833692<open>)] for JPA transaction
[nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Exposing JPA transaction as JDBC [org.springframework.orm.jpa.vendor.HibernateJpaDialect$HibernateConnectionHandle@260b8450]
[nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(826833692<open>)] for JPA transaction
[nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Participating in existing transaction
Hibernate:
insert
into
wastes
(created_at, modified_at, address, content, file_name, like_count, member_id, sell_status, title, transaction_at, view_count, waste_category, waste_price, waste_status)
values
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
[nio-8080-exec-1] org.hibernate.dialect.Dialect : HHH000400: Using dialect: org.hibernate.dialect.MariaDB106Dialect
[nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Initiating transaction commit
[nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Committing JPA transaction on EntityManager [SessionImpl(826833692<open>)]
[nio-8080-exec-1] o.s.orm.jpa.JpaTransactionManager : Closing JPA EntityManager [SessionImpl(826833692<open>)] after transaction
간단하게 정리하면 다음과 같습니다.
- Create new transaction
- Open new EntityManager
- Expose JPA transaction
- Found thread-bound EntityManager
- Participating in existing transaction
- insert 쿼리 수행
- Initiating transaction commit
- Committing JPA transaction
- Closing JPA EntityManager
@Transactional의 Propagation
위 로그를 통해 @Transactional 을 추가했을 경우 다음 두 가지가 추가로 실행되는 것을 확인할 수 있었다.
4. Found thread-bound EntityManager
5. Participating in existing transaction
그리고 @Transactional
은 기본값으로 Propagation 전략이 PROPAGATION_REQUIRED
, Isolation Level은 ISOLATION_DEFAULT
(MariaDB 기준으로 READ_COMMITTED와 동일)으로 설정이 되어있다는 것을 확인할 수 있었다.
앞서 추가로 실행된 두 가지 과정은 @Transactional
의 Propagation 기능에 의해 추가된 것이며, 직접 추가한 @Transactional
에 의해서 Transaction을 생성하고 SimpeJpaRepository
에서 이미 추가된 @Transactional
이 생성된 Transaction이 있는지 찾아서 사용한다는 것도 알게되었습니다.
Found thread-bound EntityManager
구체적으로 코드에서는 아래처럼 ThreadLocal
을 사용하여 thread-bound로 EntityManager를 저장하고 가져오는 것을 확인했다.
private static final ThreadLocal<Map<Object, Object>> resources = new NamedThreadLocal("Transactional resources");
Participating in existing transaction
AbstractPlatformTransactionManager
에서는 위 resources에서 EntityManager가 존재하면 가져와서 사용하는 것을 확인했다.
public final TransactionStatus getTransaction(@Nullable TransactionDefinition definition) throws TransactionException {
// ... 생략
if (this.isExistingTransaction(transaction)) {
return this.handleExistingTransaction(def, transaction, debugEnabled);
}
// ... 생략
}
쿼리 메소드를 직접 추가했을 경우
SimpleJpaRepository
에서 구현되지 않은, 직업 추가한 쿼리 메소드를 실행했을 때 로그를 확인해보았다.
쿼리 메소드
현재 프로젝트에서 추가한 existsByIdAndMember_Id
쿼리 메소드를 기준으로 확인했다.
public interface WasteRepository extends JpaRepository<Waste, Long> {
boolean existsByIdAndMember_Id(Long wasteId, Long memberId);
}
JPA 로그
[nio-8080-exec-1] tor$SharedEntityManagerInvocationHandler : Creating new EntityManager for shared EntityManager invocation
Hibernate:
select
waste0_.id as col_0_0_
from
wastes waste0_
inner join
members member1_
on waste0_.member_id=member1_.id
where
waste0_.id=?
and member1_.id=? limit ?
이를 통해서 쿼리 메소드를 따로 추가했을 경우에는 @Transactional
이 적용되지 않았기 때문에 Transaction이 생성되지 않는다는 사실을 확인할 수 있었다.
@Transactional의 read_only 옵션
@Transactional(read_only=true)를 적용했을 경우에도 마찬가지로 Transaction이 생성된다는 것을 확인했다. 그리고 read_only 옵션이 정확하게 어떤 역할을 하는 것인지 알고 싶어 구글링해보고 JDBC(MariaDB Driver)에 대한 로그를 찍어보았다.
JPA의 read_only 옵션
@Transactional
에 read_only를 적용하지 않으면 엔티티가 영속성 컨텍스트(1차 캐시)에 저장되고, read_only를 적용하면 저장되지 않아 메모리를 절약할 수 있다는 내용을 알게되었다.
JDBC의 read_only 옵션
@Transactional(read_only=true)
가 적용되었을 때 JDBC에 대한 로그를 찍어보았을 때 다음과 같았다.
[nio-8080-exec-1] log4jdbc.log4j2 : 1. Connection.isValid(5) returned true
[nio-8080-exec-1] log4jdbc.log4j2 : 1. Connection.setReadOnly(true) returned
[nio-8080-exec-1] log4jdbc.log4j2 : 1. Connection.getAutoCommit() returned true
[nio-8080-exec-1] log4jdbc.log4j2 : 1. Connection.setAutoCommit(false) returned
[nio-8080-exec-1] log4jdbc.log4j2 : 1. PreparedStatement.new PreparedStatement returned
... 생략
[nio-8080-exec-1] log4jdbc.log4j2 : 1. ResultSet.next() returned false
[nio-8080-exec-1] log4jdbc.log4j2 : 1. ResultSet.close() returned void
[nio-8080-exec-1] log4jdbc.log4j2 : 1. PreparedStatement.getMaxRows() returned 0
[nio-8080-exec-1] log4jdbc.log4j2 : 1. PreparedStatement.getQueryTimeout() returned 0
[nio-8080-exec-1] log4jdbc.log4j2 : 1. PreparedStatement.close() returned
[nio-8080-exec-1] log4jdbc.log4j2 : 1. Connection.commit() returned
[nio-8080-exec-1] log4jdbc.log4j2 : 1. Connection.setAutoCommit(true) returned
[nio-8080-exec-1] log4jdbc.log4j2 : 1. Connection.setReadOnly(false) returned
[nio-8080-exec-1] log4jdbc.log4j2 : 1. Connection.clearWarnings() returned
[nio-8080-exec-1] log4jdbc.log4j2 : 1. Connection.clearWarnings() returned
readOnly 부분만 보았을 때 Connection.setReadOnly(true)
, Connection.setReadOnly(false)
을 실행한다는 것을 확인할 수 있었는데, 실제로 왜 readOnly를 설정해주는 것인지 알고자 mariadb-connector-j
오픈소스를 찾아보았다.
mariadb-connector-j
Connection.java
에서 setReadOnly
코드는 다음과 같이 구현되어 있었다.
@Override
public void setReadOnly(boolean readOnly) throws SQLException {
lock.lock();
try {
if (this.readOnly != readOnly) {
client.setReadOnly(readOnly);
}
this.readOnly = readOnly;
getContext().addStateFlag(ConnectionState.STATE_READ_ONLY);
} finally {
lock.unlock();
}
}
client는 구현체로 MultiPrimaryClient
, MultiPrimaryReplicaClient
, StandardClient
가 존재했다. StandardClient
, MultiPrimaryClient
에서 구현된 setReadOnly
메소드는 다음과 같이 아무런 로직없이 예외 처리만 수행하는 것을 확인할 수 있었다.
@Override
public void setReadOnly(boolean readOnly) throws SQLException {
if (closed) {
throw new SQLNonTransientConnectionException("Connection is closed", "08000", 1220);
}
}
하지만 MultiPrimaryReplicaClient
에서는 readOnly 값에 따라 replicaClient, primaryClient를 설정해 주는 것을 확인했다.
구글링을 해보았을 때도 read_only 값에 따라 AWS의 Aurora DB를 사용했을 때 조회 전용 DB로 자동으로 라우팅을 해준다는 내용을 볼 수 있었다.
위 사실들을 바탕으로 read_only 옵션이 primary(읽기/쓰기) DB로 라우팅할 것인지 replica(읽기) DB로 라우팅할 것인지를 자동으로 판단해줄 수 있게해주는 옵션이라고 판단할 수 있었다.