SET ~ 쿼리를 없애 쿼리를 최적화하기set session transaction read only
SET autocommit=0
SELECT 쿼리
commit
SET autocommit=1
set session transaction read write 왜 줄여야하나? 실제 호출해야할 쿼리는 SELECT 쿼리 하나인데, SELECT 쿼리 하나 당 불필요하게 추가로 5개의 쿼리가 호출됨 만약 1K의 SELECT 쿼리가 DB로 실행된다고 할 때, DB로 들어오는 요청은 실제로 6K가 되는 것이며 이로 인해 DB 서버의 리소스 사용량도 증가하게 됨 또한 애플리케이션 단에서도 DB와의 통신이 그만큼 더 늘어나는 것이므로, 하나의 쿼리를 실행하는 것보다 레이턴시가 더 증가함 따라서 이와 같이 불필요하게 수행되는 설정 쿼리들을 줄인다면, 애플리케이션 응답 속도도 더 높이고 DB 서버의 리소스 사용량도 줄일 수 있음. (일석이조의 효과!)아래 설명은 아래 예시코드를 바탕으로 수행한 결과예요.
@Service
class MemberService(
private val memberRepository: MemberRepository,
) {
fun getMember(id: Long): Member? {
// findByIdOrNull은 SimpleJpaRepository에서 제공해주는 기본 메서드를 사용
return memberRepository.findByIdOrNull(id)
}
}
interface MemberRepository : JpaRepository<Member, Long>
@Entity
class Member(
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
var id: Long? = null,
var name: String,
)
1. MySQL 접속
mysql> mysql -uroot -p
2. 설정 변경
mysql> set global general_log=ON
3. 로그 파일 조회
1) 로그 파일 위치(파일명) 확인
mysql> show global variables like 'general_log_file';
+------------------+---------------------------------------------+
| Variable_name | Value |
+------------------+---------------------------------------------+
| general_log | OFF |
| general_log_file | /usr/local/var/mysql_3306/Joanne.log |
+------------------+---------------------------------------------+
2) 쉘에서 파일 조회
shell> tail -f /usr/local/var/mysql_3306/Joanne.log
4. 테스트 종료 후 다시 설정 변경
mysql> set global general_log=OFF@Repository
@Transactional(readOnly = true)
public class SimpleJpaRepository<T, ID> implements JpaRepositoryImplementation<T, ID> { SimpleJpaRepository에는 자체적으로 @Transactional(readOnly = true 어노테이션이 붙어있어요. 해당 어노테이션은 트랜잭션을 읽기 전용으로 열게 돼요.TransactionAspectSupport에서 TransactionAttributeSource를 세팅함.
이때 TransactionalRepositoryProxyPostProcessor 의 enableDefaultTransactions 옵션을 @EnableJpaRepositories에서 설정해준 enableDefaultTransactions의 값으로 설정함.
@EnableJpaRepositories(
// default 값은 true예요.
enableDefaultTransactions = false,
)
TransactionAspectSupport 의 invokeWithinTransaction 메서드 호출
해당 메서드 내부에서 transactionAttribute를 가져옴. (getTransactionAttribute)
AbstractFallbackTransactionAttributeSource 에 정의된 getTransactionAttribute 는 해당 메서드에 대해 정의된 트랜잭션을 cache 에서 가져오는데, 첫번째 호출의 경우 캐시에 등록된 값이 없기 때문에 직접 TransactionAttribute를 계산한다. (computeTransactionAttribute)
computeTransactionAttribute가 호출되면, RepositoryAnnotationTransactionAttributeSource 의 computeTransactionAttribute 가 호출되는데, 해당 메서드 내부를 보면 다음과 같다.
@Override
@Nullable
protected TransactionAttribute computeTransactionAttribute(Method method, @Nullable Class<?> targetClass) {
// Don't allow no-public methods as required.
if (allowPublicMethodsOnly() && !Modifier.isPublic(method.getModifiers())) {
return null;
}
// Ignore CGLIB subclasses - introspect the actual user class.
Class<?> userClass = targetClass == null ? targetClass : ProxyUtils.getUserClass(targetClass);
// The method may be on an interface, but we need attributes from the target class.
// If the target class is null, the method will be unchanged.
Method specificMethod = ClassUtils.getMostSpecificMethod(method, userClass);
// If we are dealing with method with generic parameters, find the original method.
specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
TransactionAttribute txAtt = null;
if (specificMethod != method) {
// Fallback is to look at the original method.
txAtt = findTransactionAttribute(method);
if (txAtt != null) {
return txAtt;
}
// Last fallback is the class of the original method.
txAtt = findTransactionAttribute(method.getDeclaringClass());
if (txAtt != null || !enableDefaultTransactions) {
return txAtt;
}
}
// First try is the method in the target class.
txAtt = findTransactionAttribute(specificMethod);
if (txAtt != null) {
return txAtt;
}
// Second try is the transaction attribute on the target class.
txAtt = findTransactionAttribute(specificMethod.getDeclaringClass());
if (txAtt != null) {
return txAtt;
}
if (!enableDefaultTransactions) {
return null;
}
// Fallback to implementation class transaction settings of nothing found
// return findTransactionAttribute(method);
Method targetClassMethod = repositoryInformation.getTargetClassMethod(method);
if (targetClassMethod.equals(method)) {
return null;
}
txAtt = findTransactionAttribute(targetClassMethod);
if (txAtt != null) {
return txAtt;
}
txAtt = findTransactionAttribute(targetClassMethod.getDeclaringClass());
if (txAtt != null) {
return txAtt;
}
return null;
}
일부만을 이용하여 설명에 덧붙여보자면,
computeTransactionAttribute의 method 인자로 CrudRepository.findById가 호출됨.
// TransactionalRepositoryProxyPostProcessor.java
TransactionAttribute txAtt = null;
if (specificMethod != method) {
// 여기서의 method는 CrudRepository.findById
// 해당 메서드에는 @Transactional 어노테이션이 없어 여전히 txAtt는 null
txAtt = findTransactionAttribute(method);
if (txAtt != null) {
return txAtt;
}
// *Last fallback is the class of the original method.*
txAtt = findTransactionAttribute(method.getDeclaringClass());
// txAtt가 null이지만, enableDefaultTransactions를 false로 주었기 때문에
// 해당 조건문의 결과값이 true가 되어 txAtt는 null을 반환하게 된다.
if (txAtt != null || !enableDefaultTransactions) {
return txAtt;
}
}
...
위 compute를 호출했던 호출부로 다시 돌아가서, NULL_TRANSACTION_ATTRIBUTE를 담고, 트랜잭션 없이 쿼리를 수행한다.
// AbstractFallbackTransactionAttributeSource.java
TransactionAttribute txAttr = computeTransactionAttribute(method, targetClass);
// Put it in the cache.
// computeTransactionAttribute의 결과값이 null이기 때문에,
if (txAttr == null) {
// attributeCache에 NULL TRANSACTION을 넣고,
// SimpleJpaRepository에 정의된 @Transactional(readOnly=true) 옵션이 먹히지 않은 채로
// 트랜잭션 없이 쿼리가 수행된다.
this.attributeCache.put(cacheKey, NULL_TRANSACTION_ATTRIBUTE);
}
// TransactionalRepositoryProxyPostProcessor.java
TransactionAttribute txAtt = null;
if (specificMethod != method) {
// 여기서의 method는 CrudRepository.findById
// 해당 메서드에는 @Transactional 어노테이션이 없어 여전히 txAtt는 null
txAtt = findTransactionAttribute(method);
if (txAtt != null) {
return txAtt;
}
// *Last fallback is the class of the original method.*
txAtt = findTransactionAttribute(method.getDeclaringClass());
// txAtt가 null이고, enableDefaultTransactions를 true로 주었기 때문에
// 해당 조건문을 통과하지 못하고 아래로 내려간다.
if (txAtt != null || !enableDefaultTransactions) {
return txAtt;
}
}
// specificMethod = BridgeMethodResolver.findBridgedMethod(specificMethod);
// 위 함수를 통해 구해진 메서드로서 현재는 SimpleJpaRepository.findById가 된다.
// findById 메서드 자체에는 Transaction이 명시되어있지 않기 때문에 txAtt = null
txAtt = findTransactionAttribute(specificMethod);
if (txAtt != null) {
return txAtt;
}
// Class인 SimpleJpaRepository에는 @Transactional(readOnly=true)가 붙어있음.
txAtt = findTransactionAttribute(specificMethod.getDeclaringClass());
// 반환된 txAtt는 PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly
if (txAtt != null) {
return txAtt;
}
if (!enableDefaultTransactions) {
return null;
}
... 위 compute를 호출했던 호출부로 다시 돌아가서, PROPAGATION_REQUIRED,ISOLATION_DEFAULT,readOnly를 담고, 별도로 트랜잭션을 명시해주지 않았더라도 SimpleJpaRepository의 메서드를 사용한다면트랜잭션과 함께 쿼리를 수행하게 된다.enableDefaultTransactions=false로 주고 실행하게되면 SimpleJpaRepository에 정의된 기본 메서드를 사용할 때에도 트랜잭션 없이 쿼리를 수행하게 되어 불필요한 SET.. 등을 수행하지 않을 수 있다.
enableDefaultTransactions=false로 주게 되면 SimpleJpaRepository에 정의된 메서드를 사용하더라도 트랜잭션 없이 사용하기 때문에 영속성 컨텍스트를 활용할 수 없음.
트랜잭션이 필요한 모든 구간에 명시적으로 Transactional 어노테이션 등을 사용해주어야함.
아주 유용하군