반응형

스프링을 사용하다보면 @Transactional을 굉장히 많이 사용한다.

 

에러가 발생하면 transaction을 rollback 시켜주기 때문에 사용한다라고는 알고 있지만, 어떠한 방식으로 동작하는지는 제대로 알아보지 않은 것 같다.

 

우선 블로그들과 spring framework의 github를 참고했다.

 

@Transactional을 사용하면 interceptor가 중간에 가져와서 invokeWithinTransaction가 실행되도록 한다.

 

안에서 작성된 코드는 이정도 되는 것 같다.

 

https://github.com/spring-projects/spring-framework/blob/main/spring-tx/src/main/java/org/springframework/transaction/annotation/Transactional.java

protected @Nullable Object invokeWithinTransaction(Method method, @Nullable Class<?> targetClass,
			final InvocationCallback invocation) throws Throwable {

		// If the transaction attribute is null, the method is non-transactional.
		TransactionAttributeSource tas = getTransactionAttributeSource();
		final TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
		final TransactionManager tm = determineTransactionManager(txAttr, targetClass);

		if (this.reactiveAdapterRegistry != null && tm instanceof ReactiveTransactionManager rtm) {
			boolean isSuspendingFunction = KotlinDetector.isSuspendingFunction(method);
			boolean hasSuspendingFlowReturnType = isSuspendingFunction &&
					COROUTINES_FLOW_CLASS_NAME.equals(new MethodParameter(method, -1).getParameterType().getName());

			ReactiveTransactionSupport txSupport = this.transactionSupportCache.computeIfAbsent(method, key -> {
				Class<?> reactiveType =
						(isSuspendingFunction ? (hasSuspendingFlowReturnType ? Flux.class : Mono.class) : method.getReturnType());
				ReactiveAdapter adapter = this.reactiveAdapterRegistry.getAdapter(reactiveType);
				if (adapter == null) {
					throw new IllegalStateException("Cannot apply reactive transaction to non-reactive return type [" +
							method.getReturnType() + "] with specified transaction manager: " + tm);
				}
				return new ReactiveTransactionSupport(adapter);
			});

			return txSupport.invokeWithinTransaction(method, targetClass, invocation, txAttr, rtm);
		}

		PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
		final String joinpointIdentification = methodIdentification(method, targetClass, txAttr);

		if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager cpptm)) {
			// Standard transaction demarcation with getTransaction and commit/rollback calls.
			TransactionInfo txInfo = createTransactionIfNecessary(ptm, txAttr, joinpointIdentification);

			Object retVal;
			try {
				// This is an around advice: Invoke the next interceptor in the chain.
				// This will normally result in a target object being invoked.
				retVal = invocation.proceedWithInvocation();
			}
			catch (Throwable ex) {
				// target invocation exception
				completeTransactionAfterThrowing(txInfo, ex);
				throw ex;
			}
			finally {
				cleanupTransactionInfo(txInfo);
			}

			if (retVal != null && txAttr != null) {
				TransactionStatus status = txInfo.getTransactionStatus();
				if (status != null) {
					if (retVal instanceof Future<?> future && future.isDone()) {
						try {
							future.get();
						}
						catch (ExecutionException ex) {
							Throwable cause = ex.getCause();
							Assert.state(cause != null, "Cause must not be null");
							if (txAttr.rollbackOn(cause)) {
								status.setRollbackOnly();
							}
						}
						catch (InterruptedException ex) {
							Thread.currentThread().interrupt();
						}
					}
					else if (vavrPresent && VavrDelegate.isVavrTry(retVal)) {
						// Set rollback-only in case of Vavr failure matching our rollback rules...
						retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
					}
				}
			}

			commitTransactionAfterReturning(txInfo);
			return retVal;
		}

		else {
			Object result;
			final ThrowableHolder throwableHolder = new ThrowableHolder();

			// It's a CallbackPreferringPlatformTransactionManager: pass a TransactionCallback in.
			try {
				result = cpptm.execute(txAttr, status -> {
					TransactionInfo txInfo = prepareTransactionInfo(ptm, txAttr, joinpointIdentification, status);
					try {
						Object retVal = invocation.proceedWithInvocation();
						if (retVal != null && vavrPresent && VavrDelegate.isVavrTry(retVal)) {
							// Set rollback-only in case of Vavr failure matching our rollback rules...
							retVal = VavrDelegate.evaluateTryFailure(retVal, txAttr, status);
						}
						return retVal;
					}
					catch (Throwable ex) {
						if (txAttr.rollbackOn(ex)) {
							// A RuntimeException: will lead to a rollback.
							if (ex instanceof RuntimeException runtimeException) {
								throw runtimeException;
							}
							else {
								throw new ThrowableHolderException(ex);
							}
						}
						else {
							// A normal return value: will lead to a commit.
							throwableHolder.throwable = ex;
							return null;
						}
					}
					finally {
						cleanupTransactionInfo(txInfo);
					}
				});
			}
			catch (ThrowableHolderException ex) {
				throw ex.getCause();
			}
			catch (TransactionSystemException ex2) {
				if (throwableHolder.throwable != null) {
					logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
					ex2.initApplicationException(throwableHolder.throwable);
				}
				throw ex2;
			}
			catch (Throwable ex2) {
				if (throwableHolder.throwable != null) {
					logger.error("Application exception overridden by commit exception", throwableHolder.throwable);
				}
				throw ex2;
			}

			// Check result state: It might indicate a Throwable to rethrow.
			if (throwableHolder.throwable != null) {
				throw throwableHolder.throwable;
			}
			return result;
		}
	}

 

우선 트랜잭션의 속성과 매니저를 가져온다.

TransactionAttributeSource tas = getTransactionAttributeSource();
TransactionAttribute txAttr = (tas != null ? tas.getTransactionAttribute(method, targetClass) : null);
TransactionManager tm = determineTransactionManager(txAttr, targetClass);

 

특별한 설정이 없다면, 트랜잭션을 처리한다.

PlatformTransactionManager ptm = asPlatformTransactionManager(tm);
...
if (txAttr == null || !(ptm instanceof CallbackPreferringPlatformTransactionManager cpptm)) {

}

 

아래의 메서드로 트랜잭션이 진행된다.

 

  • 트랜잭션 시작 (createTransactionIfNecessary)
  • 비즈니스 로직 실행 (invocation.proceedWithInvocation())
  • 예외 발생 시 롤백 (completeTransactionAfterThrowing)
  • 정상 종료 시 커밋 (commitTransactionAfterReturning)

 

아래의 메서드로 에러 발생시 롤백이 예약된다.

status.setRollbackOnly();

 

 

 

만약, 해당 트랜잭션이 중첩이 되어 있다면

호출한 트랜잭션으로 전파된다.

if (throwableHolder.throwable != null) {
    throw throwableHolder.throwable;
}
return result;

 

간단하게 정리하자면

 

  • Spring이 트랜잭션 어노테이션이 붙은 메서드를 감지
  • 그 메서드를 프록시(Proxy) 객체로 감쌉니다
  • 실제 메서드를 호출할 때, 프록시가 트랜잭션 관련 로직을 앞뒤로 끼워 넣습니다

이런식으로 트랜잭션이 처리된다.

 

반응형

팀원이 작성한 코드에서 성능 튜닝이 필요한 코드를 찾았다.

override fun patchAdmin(routineId: Int, newAdminId: Int, authentication: Authentication): ResponseEntity<CoBoResponseDto<CoBoResponseStatus>> {
        val user = userRepository.findById(authentication.name.toInt())
            .orElseThrow{throw NoSuchElementException("일치하는 사용자가 없습니다.")}

        val newAdmin = userRepository.findById(newAdminId)
            .orElseThrow{throw NoSuchElementException("일치하는 사용자가 없습니다.")}

        val routine = routineRepository.findById(routineId)
            .orElseThrow{throw NoSuchElementException("일치하는 루틴이 없습니다.")}

        if (routine.admin != user)
            throw IllegalAccessException("수정 권한이 없습니다.")

        if (!participationRepository.existsByUserAndRoutine(newAdmin, routine))
            throw NoSuchElementException("참여 정보가 없는 사용자입니다.")

        routine.admin = newAdmin
        routineRepository.save(routine)

        return CoBoResponse<CoBoResponseStatus>(CoBoResponseStatus.SUCCESS).getResponseEntity()
    }

 

해당 코드이다.

 

무려 데이터베이스에 5번 접근하게 된다.

마지막에 save하는 부분은 데이터베이스에서 데이터를 가져온 후 호출해야 하기 때문에 마지막에 실행해야 하지만, 위의 4개의 접근은 의존성이 존재하지 않기 때문에 4개를 각각 다른 쓰레드에서 비동기적으로 실행이 가능하다.

 

if (!participationRepository.existsByUserAndRoutine(newAdmin, routine))
            throw NoSuchElementException("참여 정보가 없는 사용자입니다.")

 

현재 이 코드는 의존성이 존재하지만, JPA가 아닌 QueryDsl을 사용하여 리펙토링해서 의존성을 없애도록 하였다.

 

participationRepository.existsByUserIdAndRoutineIdByQueryDsl(newAdminId, routineId)

 

이렇게 Entity가 아닌 Int 타입의 Id로 검색하도록 코드를 변경했다.

override fun existsByUserIdAndRoutineIdByQueryDsl(userId: Int, routineId: Int): Boolean {
        return jpaQueryFactory
            .select(participation)
            .from(participation)
            .leftJoin(participation.user)
            .where(
                participation.routine.id.eq(routineId),
                participation.user.kakaoId.eq(userId)
            ).fetchFirst() != null
    }

 

이 부분은 QueryDsl의 코드이다.

 

이렇게 저 코드에서도 비동기적으로 실행 할 수 있도록 의존성을 제거하였다.

 

이제 application 단계에서 코드들을 비동기적으로 처리하여 속도를 높여보자.

override fun patchAdmin(routineId: Int, newAdminId: Int, authentication: Authentication): ResponseEntity<CoBoResponseDto<CoBoResponseStatus>> {

        val userCompletableFuture = CompletableFuture.supplyAsync{
            userRepository.findById(authentication.name.toInt())
                .orElseThrow{throw NoSuchElementException("일치하는 사용자가 없습니다.")}
        }

        val newAdminFuture = CompletableFuture.supplyAsync {
            userRepository.findById(newAdminId)
                .orElseThrow{throw NoSuchElementException("일치하는 사용자가 없습니다.")}
        }

        val routineFuture = CompletableFuture.supplyAsync{
            routineRepository.findById(routineId)
                .orElseThrow{throw NoSuchElementException("일치하는 루틴이 없습니다.")}
        }

        val isParticipationFuture = CompletableFuture.supplyAsync{
            participationRepository.existsByUserIdAndRoutineIdByQueryDsl(newAdminId, routineId)
        }

        return CompletableFuture.allOf(userCompletableFuture, newAdminFuture, routineFuture, isParticipationFuture)
            .thenApplyAsync {

                val user = userCompletableFuture.get()
                val newAdmin = newAdminFuture.get()
                val routine = routineFuture.get()
                val isParticipation = isParticipationFuture.get()

                if (routine.admin != user)
                    throw IllegalAccessException("수정 권한이 없습니다.")

                if (!isParticipation)
                    throw NoSuchElementException("참여 정보가 없는 사용자입니다.")

                routine.admin = newAdmin
                routineRepository.save(routine)

                CoBoResponse<CoBoResponseStatus>(CoBoResponseStatus.SUCCESS).getResponseEntity()
            }.get()
    }

 

이렇게 코드를 변경하였다.

 

위의 4개를 CompletableFuture로 담아 비동기적으로 실행하였으며, 4개의 쓰레드가 모두 완료되었을 때를 allOf로 가져와 한 곳에서 모아 결과를 확인 한 후 다시 routineRepository에 저장하도록 하였다.

 

위의 4개의 접근은 한번에 실행하기 때문에 사실상 저 코드는 2개의 데이터베이스 접근의 시간과 비슷하게 튜닝이 되었을 것이다.

 

얼마나 속도가 좋아졌는지 궁금하여 로그로 시간을 찍어보았다.

 

아래 사진은 기존 코드의 실행시간이다.

 

아래 사진은 튜닝한 코드의 실행 시간이다.

 

데이터베이스의 접근 4개를 동시에 처리해서 그런지, 2배 이상의 성능 향상이 일어난 것을 볼 수 있다.

 

최근에 리액티브 프로그래밍에 재미를 느껴 따라해보았는데, 비동기가 왜 필요한지를 다시 한 번 느낄 수 있었던 것 같다.

'크무톡톡 프로젝트' 카테고리의 다른 글

네이버 Oauth 로그인, SpringBoot  (2) 2024.07.23
Nginx로 Swagger Proxy_pass  (1) 2024.07.22
Springboot와 DialogFlow 연동 - API  (0) 2024.01.17
SMTP 서버 구축  (0) 2024.01.04
EC2에 Java 17 설치  (1) 2023.12.29

+ Recent posts