[6/30 TIL] SPRING(AOP, @Transactional, 트랜잭션 전파 및 격리)

yumyeonghan·2023년 7월 2일
0

🍃프로그래머스 백엔드 데브코스 4기 교육과정을 듣고 정리한 글입니다.🍃

AOP

  • 프로그래밍을 하다보면 API 호출 시간 측정 같은 공통적인 기능이 많이 발생
  • 자바에서 공통 기능을 모듈에 적용하기 위해 상속을 이용하지만, 다중 상속이 불가능하다는 한계가 존재
  • AOP는 OOP(Object Oriented Programming, 객체 지향 프로그래밍)를 돕는 보조적인 기술로, 핵심적인 관심 사항(Core Concern)과 공통 관심 사항(Cross-Cutting Concern)으로 분리시키고 각각을 모듈화 하는 것을 의미

그림 출처

AOP 적용 시점

  • 컴파일 시점
  • 클래스 로딩 시점
  • 런타임 시점(스프링 AOP)
    그림 출처

Spring AOP

  • Spring AOP는 기본적으로 인터페이스 기반인 JDK Proxy를 사용
  • 비즈니스 객체가 인터페이스를 구현하지 않은 경우, 클래스 기반인 CGLib Proxy를 사용
  • 테스트에서 AOP 적용하려면 @EnableAspectJAutoProxy를 설정
  • AOP는 스프링 빈으로 등록된 객체에만 프록시로 동작해서 작동
    그림 출처

JDK Proxy

  • 인터페이스를 구현한 클래스에 대해 프록시 객체를 생성
  • 프록시 객체는 인터페이스의 메서드 호출을 갖고, InvocationHandler 인터페이스를 구현한 클래스에서 지정한 로직을 실행
  • InvocationHandler는 핵심 로직을 호출하기 전과 후에 추가적인 작업을 수행하는 방식으로 동작
interface UserService {
    void getUserInfo();
}

class UserServiceImpl implements UserService {
    @Override
    public void getUserInfo() {
        System.out.println("Fetching user information...");
    }
}

class LoggingHandler implements InvocationHandler {
    private final Object target;

    public LoggingHandler(Object target) {
        this.target = target;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        System.out.println("Before method invocation");
        Object result = method.invoke(target, args);
        System.out.println("After method invocation");
        return result;
    }
}

public class Main {
    public static void main(String[] args) {
        UserService userService = new UserServiceImpl();

        UserService proxy = (UserService) Proxy.newProxyInstance(
                Main.class.getClassLoader(),
                new Class[]{UserService.class},
                new LoggingHandler(userService)
        );

        proxy.getUserInfo();
    }
}

CGLib Proxy

  • 클래스를 상속하여 프록시 객체를 생성
  • 프록시 객체는 핵심 로직을 호출하기 전과 후에 추가적인 작업을 수행하는 방식으로 동작

AOP 주요 용어

그림 출처

  • 타겟(Target)
    • 핵심 기능을 담고 있는 모듈로서 부가기능을 부여할 대상(클래스 객체)
  • 조인포인트(Join Point)
    • 어드바이스가 적용될 수 있는 위치
    • 타겟 객체가 구현한 인터페이스의 모든 메서드
  • 포인트 컷(Pointcut)
    • 어드바이스를 적용할 타겟의 메서드를 선별하는 정규표현식
    • 포인트컷 표현식은 execution으로 시작하고 메서드의 Signature를 비교하는 방법을 주로
      이용함
    • execution( com.wiley.spring.ch8.MyBean.(..))
  • 어드바이스(Advice)
    • 어드바이스는 타겟의 특정 조인트포인트에 제공할 부가기능
    • Advice에는 다음 그림과 같이 @Before, @After, @Around., @AfterReturning, @AfterThrowing 등이 있음
  • 애스펙트(Aspect)
    • 애스펙트 = 어드바이스 + 포인트컷
    • Spring에서는 Aspect를 빈으로 등록해서 사용합니다.
  • 위빙(Weaving)
    • 타겟의 조인 포인트에 어드바이즈를 적용하는 과정(AOP가 적용 되는 런타임 시점)

포인트 컷 예시

// 모든 메서드에 적용되는 포인트컷
execution(* com.wiley.spring.ch8.MyBean.*(..))
// com.wiley.spring.ch8.MyBean 클래스의 모든 public 메서드에 적용되는 포인트컷
execution(public * com.wiley.spring.ch8.MyBean.*(..))
// com.wiley.spring.ch8.MyBean 클래스의 리턴 타입이 String인 모든 public 메서드에 적용되는 포인트컷
execution(public String com.wiley.spring.ch8.MyBean.*(..))
// com.wiley.spring.ch8.MyBean 클래스의 파라미터 중 첫 번째 파라미터가 long인 메서드에 적용되는 포인트컷
execution(public * com.wiley.spring.ch8.MyBean.*(long, ..))

// com.wiley 패키지와 그 하위 패키지에 속하는 모든 클래스의 메서드에 적용되는 포인트컷
within(com.wiley..*)
// com.wiley.spring.ch8.MyService 클래스의 메서드에 적용되는 포인트컷
within(com.wiley.spring.ch8.MyService)
// MyServiceInterface 인터페이스를 구현하는 모든 클래스의 메서드에 적용되는 포인트컷
within(MyServiceInterface+)
// MyBaseService 클래스를 상속하는 모든 클래스의 메서드에 적용되는 포인트컷
within(com.wiley.spring.ch8.MyBaseService+)
// ArithmeticCalculator 인터페이스를 구현하는 클래스 또는 UnitCalculator 인터페이스를 구현하는 클래스의 메서드에 적용되는 포인트컷
within(ArithmeticCalculator+) || within(UnitCalculator+)
  • execution(메서드 단위)
    • execution 표현식은 메서드 실행 Join Point를 지정하는데 사용
    • 이 표현식은 메서드의 접근 제어자, 리턴 타입, 클래스 이름, 메서드 이름, 파라미터 타입 등을 조합하여 특정 메서드들을 선택
    • 예를 들어, execution( com.example.myapp...*(..))는 com.example.myapp 패키지와 그 하위 패키지에 속하는 모든 클래스의 모든 메서드를 선택
  • within(클래스 단위)
    • within 표현식은 클래스 내부의 Join Point를 선택하는데 사용
    • 이 표현식은 특정한 클래스나 패키지에 속하는 메서드들을 선택
    • 예를 들어, within(com.example.myapp..*)는 com.example.myapp 패키지와 그 하위 패키지에 속하는 모든 클래스의 메서드들을 선택

AOP 사용

로깅 AOP

@Aspect
@Component
public class LoggingAspect {

    @Before("execution(* com.example.myapp..*.*(..))")
    public void logClassExecution() {
        System.out.println("Class execution logged!");
    }
}
  • @Aspect, @Component 설정으로 애스팩트를 정의하고, 빈으로 등록
  • @Before("execution( com.example.myapp...*(..))")
    • com.example.myapp 패키지 및 해당 하위 패키지 내에서 모든 메서드가 실행 되기 전에 로그 출력

시간 측정 AOP

@Aspect
@Component
public class TimeMeasurementAspect {

    @Around("execution(* com.example.myapp..*.*(..))")
    public Object measureTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long startTime = System.currentTimeMillis();
        Object result = joinPoint.proceed(); // 조인포인트(실제 메서드) 실행
        long endTime = System.currentTimeMillis();
        long executionTime = endTime - startTime;

        System.out.println("Method execution time: " + executionTime + "ms");

        return result;
    }
}
  • @Aspect, @Component 설정으로 애스팩트를 정의하고, 빈으로 등록
  • @Around("execution( com.example.myapp...*(..))")
    • com.example.myapp 패키지 및 해당 하위 패키지 내에서 모든 메서드가 실행 되기 전과 후에 로직 동작
  • ProceedingJoinPoint 매개변수를 사용해서 메서드 실행을 제어하고 실행 시간을 계산

어노테이션 AOP

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
}
  • 런타임 시점까지 동작하고, 메서드에 적용할 수 있는 어노테이션 생성
@Aspect
@Component
public class LoggingAspect {

    @Pointcut("@annotation(com.example.myapp.Loggable)")
    public void loggableMethod() {
    }

    @AfterReturning("loggableMethod()")
    public void afterLoggableMethodExecution() {
        System.out.println("After loggable method execution: Logging after method execution!");
    }
}
  • @Pointcut("@annotation(com.example.myapp.Loggable)")
    • 해당 위치에 있는 어노테이션이 적용된 메서드를 포인트컷으로 정의
  • @AfterReturning("loggableMethod()")
    • @Loggable 어노테이션이 적용된 메서드가 실행되었을 때 afterLoggableMethodExecution() 실행

@Transactional

  • 지금까지는 직접 트랜잭션 매니저를 통해 트랜잭션을 관리
  • 스프링에서는 @Transactional을 통해 더욱 쉽게 관리
  • AOP가 적용되어 실제 쿼리문만 작성하면, 쿼리문 전 후로 트랜잭션 매니저가 만들어지고 try-catch 블록이 적용
  • 클래스 단위나, 메서드 단위에 적용

예시 코드

	@Transactional
    public void insertDataWithTransaction(UUID id, String name, int age) {
        String sql = "INSERT INTO users (id, name, age) VALUES (:id, :name, :age)";
        Map<String, Object> params = new HashMap<>();
        params.put("id", toBytes(id));
        params.put("name", name);
        params.put("age", age);

        jdbcTemplate.update(sql, params);
    }

트랜잭션 전파

  • 트랜잭션의 전파는 @Transactional 붙은 메서드가 안에서 또다른 @Transactional 붙은 메서드를 호출할 때 해당 트랜잭션을 재사용할지, 새로운 트랜잭션을 시작할지 등을 결정하는 방법
  • @Transactional 어노테이션에 propagation 속성을 사용하여 전파 옵션을 지정

종류

  • Propagation.REQUIRED
    • 기본값으로, 호출되는 메서드는 실행 중인 트랜잭션이 있으면 해당 트랜잭션을 사용하고, 없으면 새로운 트랜잭션을 시작
  • Propagation.REQUIRES_NEW
    • 호출되는 메서드는 항상 새로운 트랜잭션을 시작하고, 이전 트랜잭션을 일시적으로 보류
  • Propagation.SUPPORTS
    • 메서드가 실행 중인 트랜잭션이 있으면 해당 트랜잭션을 사용하고, 없으면 트랜잭션 없이 실행
  • Propagation.NOT_SUPPORTED
    • 메서드를 트랜잭션 없이 실행하고, 만약 이미 실행 중인 트랜잭션이 있으면 일시적으로 보류
  • Propagation.MANDATORY
    • 반드시 메서드가 실행 중인 트랜잭션 안에서 실행되어야 하고, 실행 중인 트랜잭션이 없으면 예외가 발생
  • Propagation.NEVER
    • 메서드가 실행 중인 트랜잭션이 없어야 하고, 만약 이미 실행 중인 트랜잭션이 있으면 예외가 발생

트랜잭션 격리

그림 출처

  • 트랜잭션 격리 수준은 여러 트랜잭션이 동시에 실행될 때 각 트랜잭션의 격리 수준을 어떻게 설정할지를 결정하는 방법
  • @Transactional 어노테이션에 isolation 속성을 사용하여 격리 수준을 지정
  • DEFAULT 격리 수준을 사용하는 것이 일반적이며, 성능 상의 이슈나 특별한 요구사항이 있을 때에만 다른 격리 수준을 선택

종류

  • DEFAULT (기본값)
    • 데이터베이스의 기본 격리 수준을 사용
    • 일반적으로 데이터베이스 제조사에서 설정한 기본 격리 수준이 적용
  • READ_UNCOMMITTED (Level 0)
    • 트랜잭션이 커밋되지 않은 데이터 변경 사항을 다른 트랜잭션에서도 볼 수 있음
    • 따라서 다른 트랜잭션이 변경한 데이터를 읽을 수 있으며, 이로 인해 Dirty Read, Non-Repeatable Read, Phantom Read 문제가 발생함
  • READ_COMMITTED (Level 1)
    • 트랜잭션이 커밋된 데이터만 다른 트랜잭션에서 볼 수 있음
    • Dirty Read 문제는 방지되지만, Non-Repeatable Read, Phantom Read 문제는 여전히 발생함
  • REPEATABLE_READ (Level 2)
    • 트랜잭션이 실행 도중 같은 데이터를 반복해서 읽을 때, 항상 동일한 결과가 나오도록 보장
    • 따라서 Non-Repeatable Read 문제는 방지되지만, Phantom Read 문제는 여전히 발생함
  • SERIALIZABLE (Level 3)
    • 가장 엄격한 격리 수준으로, 동시에 여러 트랜잭션이 동일한 데이터를 변경할 수 없도록 격리
    • 모든 형태의 데이터 불일치 문제를 방지하지만, 동시성이 저하됨

문제

  • Dirty Read (더티 리드)
    • Dirty Read는 트랜잭션이 아직 커밋되지 않은 다른 트랜잭션의 변경된 데이터를 읽는 상황
    • 예를 들어, 트랜잭션 A가 어떤 데이터를 변경하고 아직 커밋하지 않은 상태에서, 트랜잭션 B가 같은 데이터를 읽는 경우가 있을 때, 트랜잭션 B는 변경되지 않은 원래 값이 아닌 트랜잭션 A가 변경한 값(더티한 값)을 읽음
  • Non-Repeatable Read (비 반복 가능한 리드)
    • Non-Repeatable Read는 같은 트랜잭션에서 같은 쿼리를 반복해서 실행했을 때, 결과가 다른 상황
    • 예를 들어, 트랜잭션 A가 어떤 데이터를 읽고 있는데, 트랜잭션 B가 같은 데이터를 수정하거나 삭제하는 경우, 트랜잭션 A가 다시 해당 데이터를 읽을 때 이전과 다른 값이 반환됨
  • Phantom Read (유령 리드)
    • Phantom Read는 같은 트랜잭션 내에서 같은 쿼리를 반복해서 실행했을 때, 결과 집합에 다른 데이터가 추가되거나 제거되는 상황
    • 예를 들어, 트랜잭션 A가 어떤 조건으로 데이터를 조회하는데, 트랜잭션 B가 같은 조건으로 새로운 데이터를 추가하거나 삭제하는 경우, 트랜잭션 A가 다시 해당 데이터를 조회할 때 이전과는 다른 결과 집합이 반환됨
profile
웹 개발에 관심 있습니다.

0개의 댓글