본문 바로가기
BackEnd/Spring

프록시 패턴과 @Transaction 동작원리

by sorryisme 2024. 11. 7.

 

"@Transaction만 붙으면 자동으로 트랜잭션이 동작하는데 이거 신기하지 않으세요"

 

멘토링 중 멘토님이 말씀하셨다. 물론 신기하다 허나 왜 그런지 고민해본적이 있었던가..

 

그냥 당연하듯이 동작할거라는 생각 하에 어떤 원리로 동작하는지에 대한 고민이 부족했기에 공부를 다시 시작했다

이 부분에 대한 깊은 동작 원리나 공부는 추후 토비의 스프링을 공부할 때 배워야겠지만 지금 단계에서는 먼저 프록시 패턴과 트랜잭션이 프록시 패턴과 어떤 연관관계가 있는지 공부해보았다

 

Proxy Pattern

  • 대상 원본 객체를 대리하여 로직의 흐름을 제어하는 행동
  • 원본 객체의 수정 없이 제어가 가능하다

구현 코드

public interface IUserController {
    User login(String id, String passwd);
    User register(String id, String passwd);
}

public class UserController implements IUserController {
    @Override
    public User login(String id, String passwd) {
        return null;
    }

    @Override
    public User register(String id, String passwd) {
        return null;
    }
}

public class UserControllerProxy implements IUserController {

    private UserController userController;

    public UserControllerProxy(UserController userController) {
        this.userController = userController;
    }

    @Override
    public User login(String id, String passwd) {
        long startTime = System.currentTimeMillis();
        User user = this.userController.login(id, passwd);
        long endTime = System.currentTimeMillis();
        System.out.println(endTime - startTime);
        return user;
    }

    @Override
    public User register(String id, String passwd) {
        long startTime = System.currentTimeMillis();
        User user = this.userController.register(id, passwd);
        long endTime = System.currentTimeMillis();
        System.out.println(endTime - startTime);
        return user;
    }
}

  • 상속이나 인터페이스를 통한 프록시 패턴의 구현은 반복되는 코드가 발생될 수 있다
  • 동적 프록시를 통해서 이를 해결할 수 있다

 

동적 프록시 생성

public class MetricControllerProxy {

    public Object createProxy(Object proxiedObject) {
        Class<?>[] interfaces = proxiedObject.getClass().getInterfaces();
        DynamicProxy dynamicProxy = new DynamicProxy(proxiedObject);

        return Proxy.newProxyInstance(proxiedObject.getClass().getClassLoader(), interfaces, dynamicProxy);
    }

    private class DynamicProxy implements InvocationHandler {

        private Object target;
        public DynamicProxy(Object target) {
            this.target = target;
        }

        @Override
        public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
            long startTime = System.currentTimeMillis();

            //동적 메소드 호출
            Object result = method.invoke(target, args);
            
            long endTime = System.currentTimeMillis();
            System.out.println(endTime - startTime);

            return result;
        }
    }
}

 

 

 

@Transaction 동작원리

  • JDK에서 제공하는 동적 프록시가 아닌 cglib라는 라이브러리를 사용


디버깅 결과 프록시를 통한 동작임을 확인할 수 있다
code generator 라는 라이브러를 활용함

중요한 건 동적 프록시가 대신 하는 행위가 동일하다는 점이다

CGlib vs JDK 프록시

  • JDK 프록시는 인터페이스 구현체에 대해서만 프록시 구현이 가능
  • CGLIb Proxy는 클래스 상속을 이용하기 때문에 인터페이스가 존재하지 않아도 가@Transaction 동작원리
    • JDK에서 제공하는 동적 프록시가 아닌 cglib라는 라이브러리를 사용
    중요한 건 동적 프록시가 대신 하는 행위는 동일하다CGlib vs JDK 프록시
    • JDK 프록시는 인터페이스 구현체에 대해서만 프록시 구현이 가능
    • CGLIb Proxy는 클래스 상속을 이용하기 때문에 인터페이스가 존재하지 않아도 가능!

 

실제로 트랜잭션을 프록시하는 패턴을 간단한 예제로 만들어보았다

 

1) 시나리오

  1. 계좌 서비스 실행
  2. 송금 / 충전 기능
  3. Transaction begin (콘솔 출력)
  4. 비즈니스 로직
  5. Transaction commit or rollback
  6. 응답

2) 리팩터링

  1. 계좌 서비스 프록시 (Transaction 을 해주는 애)
  2. 실제 송금 / 충전 구현체
  3. 비즈니스로직 구현
  4. 트랜잭션은 프록시에 의해 실행

 

시나리오 코드

public class Account {
    private double balance;

    public Account() {
        this.balance = 0;
    }

    public double getBalance() {
        return balance;
    }

    public void draw(double amount) {
        balance -= amount;
        System.out.println("**** draw ****" + balance);
    }

    public void deposit(double amount) {
        balance += amount;
        System.out.println("**** deposit ****" + balance);

    }
}

public class AccountService {

    Account account;

    AccountService(Account account) {
        this.account = account;
    }

    public void draw(int amount) {
        try {
            System.out.println("TRANSACTION BEGIN");
            long startTime = System.currentTimeMillis();

            if (amount < 0) {
               throw new IllegalArgumentException("Amount cannot be negative");
            }

            if (this.account.getBalance() <= 0) {
                throw new IllegalArgumentException("Balance is negative");
            }

            this.account.draw(amount);

            long endTime = System.currentTimeMillis();
            System.out.println(endTime - startTime);

            System.out.println("TRANSACTION COMMIT");
        } catch (Exception e) {
            System.out.println("ROLL BACK");
        }
    }

    public void deposit(int amount) {
        try {
            System.out.println("TRANSACTION BEGIN");
            long startTime = System.currentTimeMillis();

            if (amount < 0) {
                throw new IllegalArgumentException("Amount cannot be negative");
            }

            this.account.deposit(amount);

            long endTime = System.currentTimeMillis();
            System.out.println(endTime - startTime);
            System.out.println("TRANSACTION COMMIT");
        } catch (Exception e) {
            System.out.println("ROLL BACK");
        }
    }}

## 구현
public class TransactionExample {
    public static void main(String[] args) {
				AccountService accountService = new AccountService(new Account());
				accountService.deposit(1000);
        accountService.draw(1000);
    }
}

 

 

프록시 패턴을 적용한 사례 (리팩토링)

public interface AccountInterface {
    void draw(int amount);
    void deposit(int amount);
}

//기존 섞여있는 코드에서 비즈니스 코드만 포함
public class AccountService implements AccountInterface {

    Account account;

    AccountService(Account account) {
        this.account = account;
    }

    public void draw(int amount) {
        if (amount < 0) {
            throw new IllegalArgumentException("Amount cannot be negative");
        }

        if (this.account.getBalance() <= 0) {
            throw new IllegalArgumentException("Balance is negative");
        }

        this.account.draw(amount);
    }

    public void deposit(int amount) {
        if (amount < 0) {
            throw new IllegalArgumentException("Amount cannot be negative");
        }

        this.account.deposit(amount);
    }
}

public class AccountProxy implements AccountInterface {

    private final AccountInterface accountService;

    AccountProxy(AccountInterface accountService) {
        this.accountService = accountService;
    }

    @Override
    public void draw(int amount) {
        try {
            System.out.println("TRANSACTION BEGIN");
            long startTime = System.currentTimeMillis();

            //분리된 코드 실행
            this.accountService.draw(amount);

            long endTime = System.currentTimeMillis();
            System.out.println(endTime - startTime);

            System.out.println("TRANSACTION COMMIT");
        } catch (Exception e) {
            System.out.println("ROLL BACK");
        }
    }

    @Override
    public void deposit(int amount) {
        try {
            System.out.println("TRANSACTION BEGIN");
            long startTime = System.currentTimeMillis();

            //분리된 코드 실행
            this.accountService.deposit(amount);

            long endTime = System.currentTimeMillis();
            System.out.println(endTime - startTime);
            
            System.out.println("TRANSACTION COMMIT");
        } catch (Exception e) {
            System.out.println("ROLL BACK");
        }
    }
}

public class TransactionExample {
    public static void main(String[] args) {

        AccountInterface accountService = new AccountService(new Account());
        AccountProxy proxy = new AccountProxy(accountService);

        proxy.deposit(1000);
        proxy.draw(1000);

    }
}

  • 프록시를 적용했음에도 여전히 중복되어있는 코드가 발생되었다
  • 추후 유지보수가 힘들 가능성이 높아 동적프록시를 적용해보았다.

 

JDK 동적 프록시를 사용한 예시

public class TransactionInvocationHandler implements InvocationHandler {
    private Object target;

    TransactionInvocationHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        Object result = null;
        try {
            System.out.println("TRANSACTION BEGIN");
            long startTime = System.currentTimeMillis();

            //분리된 코드 실행
            result = method.invoke(target, args);

            long endTime = System.currentTimeMillis();
            System.out.println(endTime - startTime);

            System.out.println("TRANSACTION COMMIT");
        } catch (Exception e) {
            System.out.println("ROLL BACK");
        }

        return result;
    }
}

public class TransactionExample {
    public static void main(String[] args) {

        AccountInterface accountService = new AccountService(new Account());

        AccountInterface proxy = (AccountInterface) Proxy.newProxyInstance(
                accountService.getClass().getClassLoader(),
                accountService.getClass().getInterfaces(),
                new TransactionInvocationHandler(accountService)
        );

        proxy.deposit(1000);
        proxy.draw(1000);

    }
}