2022년 4월 15일 TIL

AOP(Aspect Of Programming)

  • 관점 지향 프로그래밍
  • 코드 핵심부(기능)를 어수선하게 채우지 않고도 비즈니스 로직에 핵심적이지 않은 동작(부가 기능)들을 프로그램에 추가할 수 있다.

Cross Cutting Concerns

  • 여러 layer에서 공통으로 해결해야하는 문제(부가기능)
  • AOP를 이용하여 이를 해결할 수 있다. → 핵심 기능과 부가 기능 분리

AOP 적용 시점

  • 컴파일 시점
    컴파일 전에 공통 구현 코드(부가 기능)를 소스에 삽입
  • 클래스 로딩 시점
    클래스를 로딩할 때 byte code에 공통 구현 코드(부가 기능)를 삽입
  • 런타임 시점
    • proxy를 이용하여 부가 기능 구현
    • Spring AOP

Spring AOP

  • JDK Proxy와 CGLib Proxy를 활용한다.
    • JDK Proxy의 경우 별도의 객체를 만들지 않으면서 다이나믹하게 Proxy를 만들어 사용할 수 있다.

Schema-based AOP, @AspectJ

  • @AspectJ(어노테이션)를 더 많이 사용
  • spring-boot-starter-aop 필요

AOP 주요 용어

  • 타겟(Target)

    • AOP 적용 대상
    • 핵심 기능
  • 조인포인트(Join Point)

    • 어드바이스가 적용될 수 있는 위치
    • 타겟 객체가 구현한 인터페이스의 모든 메서드
  • 포인트컷(Pointcut)

    • 여러 조인포인트 중 어디에 AOP를 적용할 것인가?
    • 어드바이스를 적용할 타겟의 메서드를 식별하는 정규표현식
  • 애스펙트(Aspect)

    • 애스펙트(클래스) = 어드바이스(메서드) + 포인트컷
    • Bean으로 등록해서 사용
  • 어드바이스(Advice)

    • 타겟에 적용할 부가 기능
  • 위빙(Weaving)

    • 타겟의 조인 포인트에 어드바이스를 적용하는 것

AOP 코드

기본

@Aspect
@Component
public class LoggingAspect {
    private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);

    @Around("org.prgms.kdt.aop.CommonPointcut.servicePublicMethodPointCut()")
    public Object log(ProceedingJoinPoint joinPoint) throws Throwable{
        log.info("Before method called {}", joinPoint.getSignature().toString());
        var result = joinPoint.proceed();
        log.info("After method called with result => {}", result);
        return result;
    }
}
  • @Aspect 사용
  • @Component로 Bean 등록
  • 포인트컷 지정자(PCD)
    • @Around("execution(접근제한자 반환타입 패키지명.클래스명.메서드이름(타입 OR ..))")

      execution은 가장 정교한 포인트컷을 만들 수 있다
      within은 메소드가 아닌 특정 타입에 속한 메소드를 포인트 컷으로 설정

포인트컷 개선-(1)

@Aspect
@Component
public class LoggingAspect {
    private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);
    
    @Pointcut("execution(public * org.prgms.kdt..*Service.*(..))")
    public void servicePublicMethodPointCut(){};
    
    @Around("servicePublicMethodPointCut()")
    public Object log(ProceedingJoinPoint joinPoint) throws Throwable{
        log.info("Before method called {}", joinPoint.getSignature().toString());
        var result = joinPoint.proceed();
        log.info("After method called with result => {}", result);
        return result;
    }
}

포인트컷 개선-(2)

@TrackTime이 붙은 메서드에 AOP가 적용 된다.

@Target(METHOD)
@Retention(RUNTIME)
public @interface TrackTime { }
@Aspect
@Component
public class LoggingAspect {
    private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);
    
    @Around("@annotation(org.prgms.kdt.aop.TrackTime)")
    public Object log(ProceedingJoinPoint joinPoint) throws Throwable{
        log.info("Before method called {}", joinPoint.getSignature().toString());
        var result = joinPoint.proceed();
        log.info("After method called with result => {}", result);
        return result;
    }
}
@Override
@TrackTime
public Voucher insert(Voucher voucher) {
   storage.put(voucher.getVoucherId(), voucher);
   return voucher;
}

Spring Transaction 관리

  • spring-boot-starter-jdbc 필요

programatic transaction management

  • DatasourceTransactionManager 이용
var transaction = transactionManager.getTransaction(new DefaultTransactionDefinition());

try {
    jdbcTemplate.update("update customers set name = :name where customer_id = UUID_TO_BIN(:customerId)", toParaMap(customer));
    jdbcTemplate.update("update customers set email = :email where customer_id = UUID_TO_BIN(:customerId)", toParaMap(customer));
    transactionManager.commit(transaction);
}catch(DataAccessException e) {
    logger.error("Got error", e);
    transactionManager.rollback(transaction);
}
  • TransactionTemplate 이용
transactionTemplate.execute(new TransactionCallbackWithoutResult() {
    @Override
    protected void doInTransactionWithoutResult(TransactionStatus status) { // 알아서 롤백 처리
        // 아래의 2개의 명령어가 하나의 transaction
        jdbcTemplate.update("update customers set name = :name where customer_id = UUID_TO_BIN(:customerId)", toParaMap(customer));
        jdbcTemplate.update("update customers set email = :email where customer_id = UUID_TO_BIN(:customerId)", toParaMap(customer));
    }
});

declarative transaction management

  • @Transactional 사용
    • AOP(proxy를 사용한다)
      • 부가기능: 커밋, 롤백 등
      • 비지니스 로직에 집중 할 수 있도록 해준다.
    • service layer에서 사용
      여러 DML을 묶고 잘못되면 전체를 롤백을 한다.
@Override
@Transactional
public void createCustomers(List<Customer> customers) {
    customers.forEach(customerRepository::insert);
}

트랜잭션 전파

  • @Transactional(propagation = Propagation.값)
설명
REQUIRED기본값. 진행중인 트랜잭션이 있는 경우 그 트랜잭션을 사용. 만약 없다면 새로운 트랜잭션 시작.
MANDATORY호출 전에 반드시 진행 중인 트랜잭션이 존재해야한다. 존재하지 않을 경우 예외가 발생한다
REQUIRED_NEW항상 새로운 트랙잭션을 사용
SUPPORTS트랜잭션을 필요로 하지는 않는다. 하지만 진행 중인 트랜잭션이 있다면 해당 트랜잭션을 사용하게 된다.
NOT_SUPPORTED트랜잭션이 필요하지 않다는 것을 의미한다. 진행 중인 트랜잭션이 있다면 잠시 중단하고 메소드 실행이 종료된 후 트랜잭션을 재개한다.
NEVER트랜잭션이 필요하지 않다는 것을 의미한다. 진행 중인 트랜잭션이 있다면 예외를 발생시킨다.
NESTED이미 진행 중인 트랜잭션이 있으면 중첩 트랜잭션을 시작한다. 즉, 별개의 트랜잭션을 만드는 것이 아니라 트랜잭션 안에 다시 트랜잭션을 만든다. 중첩 트랜잭션은 먼저 시작된 트랜잭션의 커밋과 롤백에 영향을 받지만 자신의 커밋과 롤백은 먼저 시작된 트랜잭션에 영향을 주지 못한다. DB 벤더에 따라 지원이 안되는 경우도 있다.

트랜잭션 격리

트랜잭션 격리 수준

격리 수준설명
LV.0 READ_UNCOMMITED한 트랜잭션의 변경된 내용을 COMMIT이나 ROLLBACK과 상관 없이 다른 트랜잭션에서 읽을 수 있다.
LV1. READ_COMMITED한 트랜잭션의 변경 내용이 COMMIT 되어야만 다른 트랜잭션에서 조회할 수 있다. 가장 많이 사용되는 격리 수준
LV2. REPEATABLE_READ트랜잭션이 시작되기 전에 COMMIT된 내용에 대해서만 조회할 수 있다(트랜잭션 동안 같은 데이터를 읽게 한다. 여러 버전 만든다.). 일관성 있는 결과 보장.
LV3. SERIALIZABLE트랜잭션이 특정 테이블을 읽으면 다른 트랜잭션은 그 테이블의 데이터를 추가/변경/삭제할 수 없다.

격리에 따른 이슈(문제 유형)

  • Dirty Read
    커밋되지 않은 수정중인 데이터를 다른 트랜잭션에서 읽을 수 있도록 허용할 때 발생하는 현상이다.

    한 트랜잭션(T1)이 데이타에 접근하여 값을 A에서 B로 변경했고 아직 커밋을 하지 않았을때, 다른 트랜잭션(T2)이 해당 데이타를 조회한 상황에서 만약 T1이 변경을 commit하지 않는다면?
    😱 데이터가 꼬이는 문제가 발생

  • Non-Repeatable Read
    한 트랜잭션에서 같은 쿼리를 두 번 수행할 때 그 사이에 다른 트랜잭션 값을 수정 또는 삭제하면서 두 쿼리의 결과가 상이하게 나타나는 일관성이 깨진 현상이다.

  • Phantom Read
    한 트랜잭션 안에서 일정 범위의 레코드를 두 번 이상 읽었을 때, 첫번째 쿼리에서 없던 레코드가 두번째 쿼리에서 나타나는 현상이다.

참고하자1
참고하자2

좋은 웹페이지 즐겨찾기