Home > SpringBoot > πŸ›οΈ[SpringBoot] μŠ€ν”„λ§ λΆ€νŠΈμ˜ 핡심 κ°œλ… - 관점 μ§€ν–₯ ν”„λ‘œκ·Έλž˜λ°

πŸ›οΈ[SpringBoot] μŠ€ν”„λ§ λΆ€νŠΈμ˜ 핡심 κ°œλ… - 관점 μ§€ν–₯ ν”„λ‘œκ·Έλž˜λ°
SpringBoot

πŸ›οΈ[SpringBoot] μŠ€ν”„λ§ λΆ€νŠΈμ˜ 핡심 κ°œλ… - 관점 μ§€ν–₯ ν”„λ‘œκ·Έλž˜λ°

AOPλž€?

μžλ°”μ—μ„œλŠ” μž¬μ‚¬μš© κ°€λŠ₯ν•œ μ½”λ“œλ₯Ό λ§Œλ“€κΈ° μœ„ν•΄ 객체 μ§€ν–₯ ν”„λ‘œκ·Έλž˜λ°(OOP)을 μ‚¬μš©ν•©λ‹ˆλ‹€. 클래슀λ₯Ό μž‘μ„±ν•˜κ³  객체λ₯Ό μƒμ„±ν•˜λ©°, 곡톡 κΈ°λŠ₯은 λΆ€λͺ¨ 클래슀둜 μž‘μ„±ν•΄ 상속을 톡해 μž¬μ‚¬μš©ν•©λ‹ˆλ‹€.

ν•˜μ§€λ§Œ μžλ°”λŠ” 수직적 상속 외에도 μˆ˜ν‰μ μœΌλ‘œ 곡톡 관심사λ₯Ό κ΅¬ν˜„ν•˜λŠ” 방법을 μ œκ³΅ν•˜λŠ”λ°, 이것이 λ°”λ‘œ 관점 μ§€ν–₯ ν”„λ‘œκ·Έλž˜λ°(AOP, Aspect Oriented Programming)μž…λ‹ˆλ‹€.

Spring BootλŠ” AOPλ₯Ό 쉽고 νŽΈλ¦¬ν•˜κ²Œ κ΅¬ν˜„ν•  수 μžˆλ„λ‘ Spring AOPλ₯Ό μ œκ³΅ν•©λ‹ˆλ‹€.


μ‹€μ „ 예제: λ©”μ„œλ“œ μ‹€ν–‰ μ‹œκ°„ μΈ‘μ •

문제 상황

λͺ¬ν…ŒμΉ΄λ₯Όλ‘œ κΈ°λ²•μœΌλ‘œ μ›μ£Όμœ¨μ„ κ³„μ‚°ν•˜λŠ” λ©”μ„œλ“œκ°€ μžˆλ‹€κ³  κ°€μ •ν•΄λ΄…μ‹œλ‹€. 이 λ©”μ„œλ“œμ˜ μ‹€ν–‰ μ‹œκ°„μ„ μΈ‘μ •ν•˜λ €λ©΄ λ‹€μŒκ³Ό 같이 μ½”λ“œλ₯Ό μž‘μ„±ν•΄μ•Ό ν•©λ‹ˆλ‹€.

// PiCalculator.java
public class PiCalculator {
    public static void main(String[] args) {
        PiCalculator pi = new PiCalculator();
        System.out.println(pi.calculate(100000000));
    }
    
    double calculate(int points) {
        long start = System.currentTimeMillis();
        int circle = 0;
        
        for (long i = 0; i < points; i++) {
            double x = Math.random() * 2 - 1;
            double y = Math.random() * 2 - 1;
            if (x * x + y * y <= 1) {
                circle++;
            }
        }
        
        long executionTime = System.currentTimeMillis() - start;
        System.out.println("executed in " + executionTime + "ms.");
        return 4.0 * circle / points;
    }
}

문제점: μ‹€ν–‰ μ‹œκ°„μ„ μΈ‘μ •ν•˜κ³  싢은 λ©”μ„œλ“œκ°€ μ—¬λŸ¬ 개라면 맀번 이런 μ½”λ“œλ₯Ό λ°˜λ³΅ν•΄μ•Ό ν•©λ‹ˆλ‹€.

AOPλ₯Ό ν™œμš©ν•œ ν•΄κ²°μ±…

λ‹€μŒκ³Ό 같이 μ• λ…Έν…Œμ΄μ…˜ ν•˜λ‚˜λ§Œ μΆ”κ°€ν•˜λ©΄ μžλ™μœΌλ‘œ μ‹€ν–‰ μ‹œκ°„μ΄ μΈ‘μ •λœλ‹€λ©΄ μ–Όλ§ˆλ‚˜ νŽΈν• κΉŒμš”?

@PrintExecutionTime
double calculate(int points) {
    int circle = 0;
    for (long i = 0; i < points; i++) {
        double x = Math.random() * 2 - 1;
        double y = Math.random() * 2 - 1;
        if (x * x + y * y <= 1) {
            circle++;
        }
    }
    return 4.0 * circle / points;
}

이것이 λ°”λ‘œ AOP의 μž₯μ μž…λ‹ˆλ‹€!


Spring AOP μ„€μ •

1. μ˜μ‘΄μ„± μΆ”κ°€

build.gradle νŒŒμΌμ— λ‹€μŒ μ˜μ‘΄μ„±μ„ μΆ”κ°€ν•©λ‹ˆλ‹€.

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-aop'
}

⚠️ Spring AOPλŠ” Spring Initializrμ—μ„œ 직접 μΆ”κ°€ν•  수 μ—†μœΌλ―€λ‘œ μˆ˜λ™μœΌλ‘œ μΆ”κ°€ν•΄μ•Ό ν•©λ‹ˆλ‹€.


AOP κ΅¬ν˜„ν•˜κΈ°

2. μ»€μŠ€ν…€ μ• λ…Έν…Œμ΄μ…˜ μ •μ˜

// PrintExecutionTime.java
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface PrintExecutionTime {
}

μ£Όμš” μ„€μ •:

  • @Target(ElementType.METHOD): λ©”μ„œλ“œμ—λ§Œ 적용 κ°€λŠ₯
  • @Retention(RetentionPolicy.RUNTIME): λŸ°νƒ€μž„μ— λ™μž‘ (컴파일 μ‹œμ μ΄ μ•„λ‹˜)

3. Aspect 클래슀 κ΅¬ν˜„

// PrintExecutionTimeAspect.java
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Component
@Aspect
public class PrintExecutionTimeAspect {
    
    @Around("@annotation(PrintExecutionTime)")
    public Object printExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        Object result = joinPoint.proceed();
        long executionTime = System.currentTimeMillis() - start;
        
        System.out.println("executed " + joinPoint.toShortString() + 
                           " with " + joinPoint.getArgs().length + 
                           " args in " + executionTime + "ms.");
        return result;
    }
}

λ™μž‘ 원리:

  1. @PrintExecutionTime이 뢙은 λ©”μ„œλ“œκ°€ 호좜되면
  2. μ‹€μ œ λ©”μ„œλ“œ λŒ€μ‹  printExecutionTime λ©”μ„œλ“œκ°€ λ¨Όμ € 싀행됨
  3. joinPoint.proceed()둜 μ›λž˜ λ©”μ„œλ“œλ₯Ό 호좜
  4. μ „ν›„λ‘œ μΆ”κ°€ 둜직(μ‹€ν–‰ μ‹œκ°„ μΈ‘μ •) μˆ˜ν–‰

4. μ• λ…Έν…Œμ΄μ…˜ 적용

// Pi.java
import org.springframework.stereotype.Component;

@Component
public class Pi {
    
    @PrintExecutionTime
    double calculate(int points) {
        int circle = 0;
        for (long i = 0; i < points; i++) {
            double x = Math.random() * 2 - 1;
            double y = Math.random() * 2 - 1;
            if (x * x + y * y <= 1) {
                circle++;
            }
        }
        return 4.0 * circle / points;
    }
}

5. μ‹€ν–‰ 예제

// PiApplication.java
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;

@Component
public class PiApplication implements ApplicationRunner {
    
    @Autowired
    private Pi pi;
    
    @Override
    public void run(ApplicationArguments args) throws Exception {
        System.out.println("PI with 10,000 points = " + pi.calculate(10000));
        System.out.println("PI with 100,000 points = " + pi.calculate(100000));
        System.out.println("PI with 1,000,000 points = " + pi.calculate(1000000));
        System.out.println("PI with 10,000,000 points = " + pi.calculate(10000000));
        System.out.println("PI with 100,000,000 points = " + pi.calculate(100000000));
    }
}

μ‹€ν–‰ κ²°κ³Ό

executed execution(Pi.calculate(..)) with 1 args in 1ms.
PI with 10,000 points = 3.1376
executed execution(Pi.calculate(..)) with 1 args in 4ms.
PI with 100,000 points = 3.14336
executed execution(Pi.calculate(..)) with 1 args in 18ms.
PI with 1,000,000 points = 3.141108
executed execution(Pi.calculate(..)) with 1 args in 169ms.
PI with 10,000,000 points = 3.1411124
executed execution(Pi.calculate(..)) with 1 args in 1724ms.
PI with 100,000,000 points = 3.14153364

Spring AOP μ£Όμš” μ• λ…Έν…Œμ΄μ…˜

1. @Before

λ©”μ„œλ“œ μ‹€ν–‰ 전에 μˆ˜ν–‰ν•  λ‘œμ§μ„ κ΅¬ν˜„ν•©λ‹ˆλ‹€.

@Component
@Aspect
public class PrintExecutionTimeAspect {
    
    @Before("@annotation(PrintExecutionTime)")
    public void beforePrintExecutionTime(JoinPoint joinPoint) {
        System.out.println("do something before " + joinPoint.toShortString() +
                          " with " + joinPoint.getArgs().length + " args.");
    }
}

2. @After

λ©”μ„œλ“œ μ‹€ν–‰ 후에 μˆ˜ν–‰ν•  λ‘œμ§μ„ κ΅¬ν˜„ν•©λ‹ˆλ‹€ (μ˜ˆμ™Έ λ°œμƒ 여뢀와 λ¬΄κ΄€ν•˜κ²Œ 항상 μ‹€ν–‰).

@Component
@Aspect
public class PrintExecutionTimeAspect {
    
    @After("@annotation(PrintExecutionTime)")
    public void afterPrintExecutionTime(JoinPoint joinPoint) {
        System.out.println("do something after " + joinPoint.toShortString() +
                          " with " + joinPoint.getArgs().length + " args.");
    }
}

3. @AfterReturning

λ©”μ„œλ“œκ°€ μ •μƒμ μœΌλ‘œ λ°˜ν™˜κ°’μ„ λ¦¬ν„΄ν•œ 후에 μˆ˜ν–‰ν•  λ‘œμ§μ„ κ΅¬ν˜„ν•©λ‹ˆλ‹€.

@Component
@Aspect
public class PrintExecutionTimeAspect {
    
    @AfterReturning(
        pointcut = "@annotation(PrintExecutionTime)",
        returning = "result"
    )
    public void afterReturning(JoinPoint joinPoint, Object result) {
        System.out.println("afterReturning " + joinPoint.toShortString()
                          + " with " + joinPoint.getArgs().length
                          + " args returning " + result.toString());
    }
}

4. @AfterThrowing

λ©”μ„œλ“œ μ‹€ν–‰ 쀑 μ˜ˆμ™Έκ°€ λ°œμƒν–ˆμ„ λ•Œ μˆ˜ν–‰ν•  λ‘œμ§μ„ κ΅¬ν˜„ν•©λ‹ˆλ‹€.

@Component
@Aspect
public class PrintExecutionTimeAspect {
    
    @AfterThrowing(
        pointcut = "@annotation(PrintExecutionTime)",
        throwing = "ex"
    )
    public void afterThrowing(JoinPoint joinPoint, Exception ex) {
        System.out.println("afterThrowing " + joinPoint.toShortString()
                          + " with " + joinPoint.getArgs().length
                          + " args throwing " + ex.toString());
    }
}

5. @Around

λ©”μ„œλ“œ μ‹€ν–‰ μ „ν›„ λͺ¨λ‘λ₯Ό μ œμ–΄ν•  수 μžˆλŠ” κ°€μž₯ κ°•λ ₯ν•œ μ• λ…Έν…Œμ΄μ…˜μž…λ‹ˆλ‹€.


AOP μ• λ…Έν…Œμ΄μ…˜ λΉ„κ΅ν‘œ

μ• λ…Έν…Œμ΄μ…˜ μ‹€ν–‰ μ‹œμ  μ˜ˆμ™Έ 처리 λ°˜ν™˜κ°’ μ ‘κ·Ό λ©”μ„œλ“œ μ‹€ν–‰ μ œμ–΄ μ£Όμš” μš©λ„
@Before λ©”μ„œλ“œ μ‹€ν–‰ μ „ 무관 βœ— βœ— 인증, λ‘œκΉ…, νŒŒλΌλ―Έν„° 검증
@After λ©”μ„œλ“œ μ‹€ν–‰ ν›„(항상) 포함 βœ— βœ— λ¦¬μ†ŒμŠ€ 정리, 곡톡 ν›„μ²˜λ¦¬
@AfterReturning λ©”μ„œλ“œ 정상 μ’…λ£Œ ν›„ μ˜ˆμ™Έ μ‹œ λ―Έμ‹€ν–‰ βœ“ βœ— λ°˜ν™˜κ°’ λ‘œκΉ…, 응닡 ν›„μ²˜λ¦¬
@AfterThrowing λ©”μ„œλ“œ μ˜ˆμ™Έ λ°œμƒ ν›„ μ „μš© βœ— βœ— μ˜ˆμ™Έ λ‘œκΉ…, μ˜ˆμ™Έ μ „ν™˜
@Around λ©”μ„œλ“œ μ‹€ν–‰ μ „/ν›„ λͺ¨λ‘ 포함 βœ“ βœ“ νŠΈλžœμž­μ…˜, μ„±λŠ₯ μΈ‘μ •, 전체 μ œμ–΄

μ‹€λ¬΄μ—μ„œμ˜ AOP ν™œμš©

Spring BootλŠ” AOPλ₯Ό 기반으둜 λ‹€μ–‘ν•œ μ• λ…Έν…Œμ΄μ…˜μ„ μ œκ³΅ν•©λ‹ˆλ‹€. κ°€μž₯ λŒ€ν‘œμ μΈ μ˜ˆκ°€ @Transactionalμž…λ‹ˆλ‹€.

@Transactional
public void saveUser(User user) {
    userRepository.save(user);
    // λ©”μ„œλ“œ μ‹€ν–‰ μ „: BEGIN TRANSACTION
    // λ©”μ„œλ“œ μ‹€ν–‰ ν›„: COMMIT (λ˜λŠ” μ˜ˆμ™Έ μ‹œ ROLLBACK)
}

@Transactional μ• λ…Έν…Œμ΄μ…˜λ§Œ μΆ”κ°€ν•˜λ©΄ μžλ™μœΌλ‘œ νŠΈλžœμž­μ…˜ μ²˜λ¦¬κ°€ λ˜μ–΄ 맀우 νŽΈλ¦¬ν•©λ‹ˆλ‹€.


정리

AOP의 핡심 μž₯점:

  • βœ… 쀑볡 μ½”λ“œ 제거
  • βœ… λΉ„μ¦ˆλ‹ˆμŠ€ 둜직과 λΆ€κ°€ κΈ°λŠ₯ 뢄리
  • βœ… μ½”λ“œ μœ μ§€λ³΄μˆ˜μ„± ν–₯상
  • βœ… μˆ˜ν‰μ  관심사 처리

Spring AOPλ₯Ό ν™œμš©ν•˜λ©΄ λ‘œκΉ…, νŠΈλžœμž­μ…˜, λ³΄μ•ˆ, μ„±λŠ₯ μΈ‘μ • λ“±μ˜ 곡톡 관심사λ₯Ό 효율적으둜 관리할 수 μžˆμŠ΅λ‹ˆλ‹€.