왜 private메소드에는 @Transactional 어노테이션이 동작하지 않을까?

w-beom·2023년 2월 4일
4

왜 private메소드에는 @Transactional 어노테이션이 동작하지 않을까?

코드를 보다보면 메소드위에 @Transactional어노테이션이 붙어있는 것을 자주 볼 수 있습니다. 해당 어노테이션이 붙어있는 메소드는 트랜잭션이 걸린다는 것만 대략적으로 알고만 있었습니다.

어느날 문득 @Transactional 어노테이션이 붙어있는 메소드 아래 빨간줄이 떠있는것을 본적이 있었습니다.

메시지는 한글로 번역해 보니 '@Transactional' 어노테이션이 달린 메서드는 override 할 수 있어야 합니다. 라는 메세지였습니다. 처음에는 이 메시지를 봐도 무슨 소리인지 이해를 할 수가 없었습니다. 이해를 못하니 그냥 넘어가고 의심없이 트랜잭션이 정상적으로 동작한다고 생각하였습니다.

하지만 빨간줄이 뜨는 이유는 있었고 트랜잭션이 정상적으로 작동하지 않았는데, 그 이유에 대해서는 최근에 알게 되었습니다.

우선 결론만 말하자면 @Transactional Proxy형태로 동작하기 때문에 외부에서 접근 가능한 메소드만 @Transactional 설정이 가능합니다.

@Transactional

@Transactional 어노테이션은 스프링 프레임워크에서 제공하는 트랜잭션 처리방법입니다. 사용방법은 트랜잭션을 적용하고 싶은 메소드 위에 @Transactional 어노테이션을 작성하는 것이 일반적이고, 이를 선언적 트랜잭션이라고 부릅니다.

단, 트랜잭션을 적용하기 위해 어노테이션을 붙이기 전에 주의해야할 것이 있는데 @Transactional 어노테이션을 메소드에 적용할 경우 public 메소드에만 적용할 수 있습니다. 이 이유에 대해서는 스프링에서 트랜잭션을 구현한 방법에 대해서 알아야합니다. 구현한 방법을 알아보기 전에 몇 가지 개념에 대해서 알고가야합니다.

AOP

AOP는 Aspect Oriented Programming의 약자로 관점 지향 프로그래밍이라고 불립니다. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화 하겠다는 것입니다. 여기서 모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶어내는 것을 말합니다.

생과일쥬스를 만드는 방법을 예시로 들어보겠습니다. 생과일 쥬스를 만들기 위해서는 아래와 같은 방법(로직)으로 행동합니다.

  1. 컵을 준비한다.
  2. 컵에 얼음을 넣는다.
  3. 오렌지를 착즙한다.
  4. 빨대를 꽂는다.

위의 방법을 그림으로 표현하면 아래와 같습니다.

만약, 오렌지쥬스가 아닌 포도쥬스, 사과쥬스 등등 다른 과일쥬스를 마시고 싶을 때는 어떻게 해야할까요? 아래 그림과 같이 동작해야 할 것입니다.

여기서 불편한 점이 생기게 됩니다. 다른 과일쥬스를 마시기 위해서는 1번, 2번, 4번의 과정을 다시 반복해야 한다는 것 입니다.

AOP 개념을 적용해보면 생과일쥬스를 만드는 로직을 기준으로 핵심적인 관점(쥬스를 착즙하는 과정)과 부가적인 관점(컵,얼음,빨대를 준비하는 과정)으로 나누어 관점을 기준으로 나눌 수 있습니다. 이렇게 핵심적인 기능이 아닌 중간중간 삽입되어야 할 기능(관심)들을 횡단관심사라고 합니다.

Spring AOP

AOP를 구현하는 방법에는 3가지가 있습니다.

  • 컴파일 시점에 코드에 공통 기능을 삽입하는 방법
  • 클래스 로딩 시점에 바이트 코드에 공통 기능을 삽입하는 방법
  • 런타임에 프록시 객체를 생성해서 공통 기능을 삽입하는 방법 (프록시 패턴)

Spring AOP는 위 방법중 3번째 방법인 프록시 패턴을 이용해 AOP를 구현하고 있습니다.

프록시 패턴

프록시(Proxy)를 번역하면 대리자, 대변인의 의미를 갖고 있습니다. 대리자, 대변인은 누군가를 대신해서 그 역할을 수행하는 존재입니다. 이는 프로그램에서 똑같이 적용되는데, 프록시 객체에게 어떤 일을 대신 시키는 것 입니다.

다시 한번 생과일쥬스를 만드는 것으로 예를 들어보겠습니다. 먼저 프록시 패턴을 적용하기전의 코드입니다.

public interface JuiceMaker {
    void makeJuice();
}

// 사과쥬스 
public class AppleJuiceMaker implements JuiceMaker {
    public void makeJuice() {
        System.out.println("컵을 준비합니다.");
        System.out.println("컵에 얼음을 담습니다.");
        System.out.println("사과를 착즙합니다.");
        System.out.println("컵에 빨대를 꽂습니다.");
    }
}

// 포도쥬스
public class GrapeJuiceMaker implements JuiceMaker{
    public void makeJuice() {
        System.out.println("컵을 준비합니다.");
        System.out.println("컵에 얼음을 담습니다.");
        System.out.println("포도를 착즙합니다.");
        System.out.println("컵에 빨대를 꽂습니다.");
    }
}

// 오렌지쥬스
public class OrangeJuiceMaker implements JuiceMaker {
    public void makeJuice() {
        System.out.println("컵을 준비합니다.");
        System.out.println("컵에 얼음을 담습니다.");
        System.out.println("오렌지를 착즙합니다.");
        System.out.println("컵에 빨대를 꽂습니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        JuiceMaker appleJuiceMaker = new AppleJuiceMaker();
        appleJuiceMaker.makeJuice();

        JuiceMaker grapeJuiceMaker = new GrapeJuiceMaker();
        grapeJuiceMaker.makeJuice();
    }
}

// 실행결과

컵을 준비합니다.
컵에 얼음을 담습니다.
사과를 착즙합니다.
컵에 빨대를 꽂습니다.

컵을 준비합니다.
컵에 얼음을 담습니다.
포도를 착즙합니다.
컵에 빨대를 꽂습니다.

구현하고 싶은 쥬스 제조기 클래스를 JuiceMaker 인터페이스를 구현한 뒤 1번~4번의 과정을 구현합니다.

만약에 쥬스를 생성하는 로직에서 컵에 얼음을 담으면 쥬스가 밍밍해진다고 얼음을 빼고 쥬스를 만든다고 하면 사과, 오렌지, 포도쥬스 제조기의 메소드를 모두 수정해야 할 것입니다.

현재는 3개의 클래스의 메소드만 수정해도 되지만 만약에 더 많은 과일들이 있고 그 개수가 100개 1000개라면 수정하는 상상만해도 끔찍할 것 같습니다.

여기서 AOP개념을 도입해 프록시패턴을 이용해 구현해 보겠습니다. 우선 핵심 관점부가적인 관점 을 구분지어야 합니다.

  • 핵심관점 : 과일을 착즙하는 일
  • 부가적인 관점 : 횡단관심사인 컵, 얼음, 빨대를 준비하는 일

그리고 실제 객체의 일을 대신해줄 프록시 객체가 존재해야합니다.

public interface JuiceMaker {
    void makeJuice();
}
// =========== 핵심관점==========
// 사과쥬스 
public class AppleJuiceMaker implements JuiceMaker {
    public void makeJuice() {
        System.out.println("사과를 착즙합니다.");
    }
}

// 포도쥬스
public class GrapeJuiceMaker implements JuiceMaker{
    public void makeJuice() {
        System.out.println("포도를 착즙합니다.");
    }
}

// 오렌지쥬스
public class OrangeJuiceMaker implements JuiceMaker {
    public void makeJuice() {
        System.out.println("오렌지를 착즙합니다.");
    }
}

//============ 부가적인 관점 (프록시 객체) ============

// 프록시 객체
public class JuiceMakerProxy implements JuiceMaker {
    private JuiceMaker juiceMaker;

    public JuiceMakerProxy(JuiceMaker juiceMaker) {
        this.juiceMaker = juiceMaker;
    }

    public void before() {
        System.out.println("컵을 준비합니다.");
        System.out.println("컵에 얼음을 담습니다.");
    }

    public void after() {
        System.out.println("컵에 빨대를 꽂습니다.");
    }

    @Override
    public void makeJuice() {
        this.before();
        juiceMaker.makeJuice();
        this.after();
    }
}

// 클라이언트
public class Main {
    public static void main(String[] args) {
				// 프록시 객체를 생성한다.
        JuiceMaker juiceMakerProxy = new JuiceMakerProxy(new AppleJuiceMaker());
        juiceMakerProxy.makeJuice();

        System.out.println();

        juiceMakerProxy = new JuiceMakerProxy(new GrapeJuiceMaker());
        juiceMakerProxy.makeJuice();
    }
}

// 실행결과  위의 코드와 실행결과가 같다.
컵을 준비합니다.
컵에 얼음을 담습니다.
사과를 착즙합니다.
컵에 빨대를 꽂습니다.

컵을 준비합니다.
컵에 얼음을 담습니다.
포도를 착즙합니다.
컵에 빨대를 꽂습니다.

JuiceMakerProxy 프록시 객체는 JuiceMaker 인터페이스를 구현하면서 필드로 JuiceMaker 를 가져야합니다. 필드로 가지게 되는 JuiceMaker 의 일을 대신 해야하기 때문입니다.

여기서 이 글의 주제가 나오게 됩니다.
Spring AOP는 프록시패턴을 이용해 구현합니다. @Transactional 어노테이션은 스프링Bean으로 등록된 객체에 한하여 프록시 객체를 생성한 뒤 @Transactional 어노테이션이 붙은 메소드를 실행을 시킵니다.
만약 위의 코드에서 makeJuice()메소드가 private 메소드라면 프록시 객체가 필드로 갖고 있는 JuiceMaker객체의 메소드를 실행할 수 없어 트랜잭션이 정상적으로 작동하지 않는 것 입니다.

profile
습득한 지식과 경험을 나누며 다른 사람들과 문제를 함께 해결해 나가는 과정에서 서로가 성장할 수 있는 기회를 만들고자 노력합니다.

0개의 댓글