• 카테고리

    질문 & 답변
  • 세부 분야

    백엔드

  • 해결 여부

    해결됨

트랜잭션

23.06.29 01:28 작성 조회수 407

0

[질문 템플릿]
1. 강의 내용과 관련된 질문인가요? (예)
2. 인프런의 질문 게시판과 자주 하는 질문에 없는 내용인가요? (예)
3. 질문 잘하기 메뉴얼을 읽어보셨나요? (예)

[질문 내용]
안녕하세요. 강의 들으면서 혼자 이것저것 만들어보면서 이해가 안가는부분이있어서 관련 강의에 질문남깁니다.

스프링 aop를 사용해서 로깅 기능을 만들어보고있는중에 막히는 부분이있는데. joinpoint가 실행되고 정상 흐름일때,예외 상황일때 로그 저장 기능을 만들고있습니다. 코드를 알려드리면

@Around(pointcut)
public Object logMethod(ProceedingJoinPoint joinPoint) throws Throwable {
  try {
    result = joinPoint.proceed(); // @Transactional
  } catch {
    logService.saveLog(args); // @Transactional
    throw e;
  }
  logService.saveLog(args);

  return result;
}

간략히 이런식 구성돼있습니다.

joinpoint가 실행되는 매서드에는 @Transactional이 붙어있어 트랜잭션이 실행되고 logService.saveLog에도 @Transactional이 붙어있어 트랜잭션이 실행됩니다.

여기서 문제가 제가 이해하기론 트랜잭션안에서 트랜잭션이 실행될때 내부 트랜잭션, 외부 트랜잭션으로 나뉘고 이것들을 통합하는 하나의 물리트랜잭션으로 된다고 이해했는데, 위 코드의 상황에는 joinpoint.proceed에서 생성된 트랜잭션 안에서 또 다른 트랜잭션이 생성된게 아닌 joinpoint.proceed가 완전히 수행된후 logService.saveLog로 새로운 트랜잭션이 시작된거같은데 이때도 joinpoint.proceed에서 예외가 발생하면 logService.saveLog도 커밋이 되지 않더라구요. logService.saveLog의 @Transactional의 속성을 Requires_new로 하면 예외 상황에서도 잘 저장이되구요.

내부 트랜잭션 외부 트랜잭션의 구분이 하나의 @Transactional과 같은 트랜잭션 안에서 또다른 트랜잭션이 생성될때만 구분되는게아니라 사용자 요청이 들어오고 응답이 나가기 전까지의 모든 트랜잭션이 연관되는건지 궁금합니다.

아니면 애초에 제가 잘못 하고있는게 있는걸까요..?

답변 1

답변을 작성해보세요.

0

안녕하세요. 기범님

도움을 드리고 싶지만 질문 내용만으로는 답변을 드리기 어렵습니다.

실제 동작하는 전체 프로젝트를 압축해서 구글 드라이브로 공유해서 링크를 남겨주세요.

구글 드라이브 업로드 방법은 다음을 참고해주세요.

https://bit.ly/3fX6ygx

 

주의: 업로드시 링크에 있는 권한 문제 꼭 확인해주세요

 

추가로 다음 내용도 코멘트 부탁드립니다.

1. 문제 영역을 실행할 수 있는 방법

2. 문제가 어떻게 나타나는지에 대한 상세한 설명

감사합니다.

기범님의 프로필

기범

질문자

2023.06.30

구글 드라이브

최대한 수정없이 바로 작동하게 수정해서 올렸는데 혹시라도 안되는부분있으면 말씀해주세요. (코드가 난잡하고 지저분하고 복잡하지만 질문에 해당하는 부분은 최대한 간단하게 설명드리겠습니다.)

스프링aop를 사용하여 로그 저장기능을 만들어보던중 어드바이스 내부에서 트랜잭션을 사용하여 로직이 수행되는 부분에서 트랜잭션의 커밋, 롤백 부분에서 막히는부분이 있었습니다. 포인트컷을 회원가입상황에 걸리도록 해놓아서 회원가입시 정상 흐름, 예외 상황에서 로그 저장기능을 테스트해보았습니다.

일단 제가 테스트했던 방법은 직접 서버 가동시키고 크롬으로 테스트했었습니다. 순서대로 알려드리면

  1. 서버 가동

    1-1. 서버 가동시 InitData.class 에서 몇 가지 엔티티들을 데이터베이스에 저장해놓기 때문에 로깅 엔티티(domain/log/UserActivityLog)도 데이터베이스에 1개 저장됩니다. 직접 데이터베이스에서 확인 가능합니다.

  2. localhost:8080 접속

  3. 우측 상단 signup 접속

  4. username, password값에 간단한 임의의값만 입력하고 회원가입버튼을 누르면 정상요청되게 해놨으니 username, password만 간단하게 입력하고 넘어가면 될것같습니다.

  5. 정상적으로 회원가입이 된다면 User, UserAcitivityLog 테이블에 값이 하나씩 추가됩니다.

  6. 회원가입시 username을 ex로 한다면 예외가 발생하게 해놓아서 ex로 회원가입시 예외가 발생합니다.

  7. 이땐 User, UserActivityLog 모두 테이블에 변화가없습니다

제가 의도했던건 회원가입이 정상흐름이나 예외상황이나 모두 로그는 정상 저장되게 만들고싶었습니다. aop/LogAop에 해당 코드를 간단하게 보면

@Around("userServiceCreatePointCut()")
    public Object userServiceCreateAround(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Exception e) {
            userActivityLogService.saveLog(userId, ip, action, params, e.getMessage(), null);
            throw e;
        }
        userActivityLogService.saveLog(userId, ip, action, params, resultStr, resultTime);

        return result;
    }

이런식으로 돼있고, joinPoint.proceed에서 예외가 발생해도 catch문에서 로그는 저장하게되어있습니다. 그런데 joinPoint.proceed에서 예외 발생시 로그는 저장이안되고 userActivityLogService.saveLog 매서드에 트랜잭션 속성을 Required_new로 해야만 로그가 저장됩니다.

제가 예상한 로직으로는

joinPoint.proceed() -> ProxyUserSerivce 호출 -> 트랜잭션 시작 -> UserService .save() -> 트랜잭션 종료 -> ProxyUserService 종료 -> joinPoint.proceed 종료 -> ProxyUserAcitivityLogService 호출 -> 트랜잭션 시작 -> UserActivityLogService.saveLog() -> 트랜잭션 종료.

이런식으로 될거라고 생각하는데 joinPoint.proceed에서 예외가 발생하면 UserActivityLogService도 커밋이안되고 롤백이되는게 이해가 잘 안가는거같아요.

UserService.save의 트랜잭션과 UserActivityLogService.saveLog의 트랜잭션이 서로 연관이있는건가요?

안녕하세요. 기범님

결론부터 말씀드리면 AOP의 순서를 지정하시면 됩니다.

스프링이 생성하는 트랜잭션 AOP는 다음과 같이 가장 늦은 순서로 실행됩니다.

org.springframework.transaction.annotation.EnableTransactionManagement {

int order() default Ordered.LOWEST_PRECEDENCE;

}

그리고 우리가 AOP를 만들때 순서를 지정하지 않으면 순서를 보장하지 않습니다. 기본은 보통 Ordered.LOWEST_PRECEDENCE로 가장 늦은 순서가 됩니다.

둘다 가장 늦은 순서이기 때문에 순서가 보장되지는 않지만, 스프링이 내부에서 트랜잭션 AOP를 먼저 수행하고, 그 다음에 직접 만든 AOP를 수행합니다.

이 부분은 다음과 같이 로그를 지정해서 확인할 수 있습니다. (강의 내용에서도 해당 부분을 설명합니다.)

logging.level:
org.springframework.orm: debug

실행해보면 다음과 같이 트랜잭션이 먼저 수행되고, 그 다음에 직접 만든 AOP가 수행되는 것을 확인할 수 있습니다. 이렇게 되면 이미 트랜잭션이 수행되었기 때문에 이후에 실행하는 데이터베이스 사용 로직은 해당 트랜잭션에 참여하게 됩니다. (물론 propagation 전략에 따라서 다른 결과가 나옵니다.)

실행 로그 간략

o.s.orm.jpa.JpaTransactionManager : Found thread-bound EntityManager [SessionImpl(2080573056<open>)] for JPA transaction

song.spring3.aop.LogAop : [song.spring3.service.UserService] (saveUser)

 

결론

직접 만든 AOP에 다음과 같이 순서를 지정해주시면 직접 만든 AOP를 먼저 실행할 수 있습니다.

@Order(Ordered.HIGHEST_PRECEDENCE)
public class LogAop {}

감사합니다.

기범님의 프로필

기범

질문자

2023.07.03

aop의 순서가 보장되지 않아서 발생한 문제였군요. 답변 감사합니다.

혹시 추가로 보통 직접 만드는 aop에서 트랜잭션 관련 기능이 수행되게 만들진 않나요? ordered.highest_precedence로 직접만든 aop를 선순위로 시작하게 하는게 뭔가 일반적인 방법같아 보이진 않아서요. 아니면 aop강의에서 알려주신 1, 2 같은 정수로 순서를 선언 해놔도 트랜잭션 aop보다 선순위로 배정되서 순서를 선언하는 방법으로 많이 사용되나요?

이 부분은 비즈니스 상황과 어떤 AOP를 사용할지에 따라서 달라집니다.

AOP가 순서에 의존하지 않으면 좋겠지만, 실제로 사용하면 지금같은 트랜잭션 문제 등등 다양한 문제들이 AOP 순서에 영향을 주게 됩니다.

직접 만든 AOP가 다른 AOP들 보다 항상 먼저 실행되어야 하면 ordered.highest_precedence를 사용하고, 그렇지 않다면 1,2 같은 정수를 사용하시면 됩니다. 물론 상수로 만들어서 사용하면 더 좋겠지요?

감사합니다.

기범님의 프로필

기범

질문자

2023.07.04

답변 감사합니다!