🍃프로그래머스 백엔드 데브코스 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.*(..))
execution(public * com.wiley.spring.ch8.MyBean.*(..))
execution(public String com.wiley.spring.ch8.MyBean.*(..))
execution(public * com.wiley.spring.ch8.MyBean.*(long, ..))
within(com.wiley..*)
within(com.wiley.spring.ch8.MyService)
within(MyServiceInterface+)
within(com.wiley.spring.ch8.MyBaseService+)
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가 다시 해당 데이터를 조회할 때 이전과는 다른 결과 집합이 반환됨