Spring Framework를 사용하면 가장 먼저 접하는 기초적인 용어가 3가지 있습니다.
바로 IoC와 DI, AOP 입니다.
면접 단골 문제로 많이 나오는 개념이기 때문에 제대로 알아두는 것이 좋습니다.
1. AOP란?
Spring의 핵심 개념 중 하나인 AOP는 Aspect-Oriented Programming의 약자로 핵심 로직과 부가 기능을 분리하여 애플리케이션 전체에 걸쳐 사용되는 부가 기능을 모듈화하고 재사용할 수 있도록 지원하는 것 입니다.
AOP를 번역하면 관점 지향 프로그래밍이라고 합니다. 이 말의 뜻은 프로젝트 구조를 바라보는 관점을 바꿔보자는 의미로 해석할 수 있습니다.
우리는 많은 공통 모듈이 있습니다. 대표적으로 로깅이나 오류 검사 및 처리, 성능 최적화 등 부가기능으로써 활용되는 모듈들이 있습니다. 이러한 모듈을 각 기능 별로 동일하게 넣는 것은 중복 코드가 될 수 있습니다.
AOP 개념을 활용하면 인프라 혹은 부가 기능을 모듈화 함으로써 공통된 기능을 재사용 할 수 있도록 코드를 작성할 수 있습니다.
이러한 AOP의 장점은 애플리케이션 전체에 흩어진 공통 기능을 하나의 장소에서 관리할 수 있다는 점이 있고, 두 번째는 핵심 로직과 부가 기능의 명확한 분리로, 핵심 로직은 자신의 목적만 집중하고, 그 외에 사항은 신경쓰지 않아도 된다는 장점이 있습니다.
2. AOP 사용 방법
AOP를 사용하는 방법은 3가지 입니다.
- 컴파일 시점에 사용한다.
- 클래스 로딩 시점에 사용한다.
- 런타임 시점에 사용한다.
각 시점에 따라 사용 방법이 다릅니다. 하나씩 살펴보죠.
컴파일 시점
Java 파일을 컴파일러를 통해 class를 만드는 시점에 부가 기능 로직을 추가하는 방식입니다.
모든 지점에 적용이 가능하다는 장점이 있습니다.
단점으로는 AspectJ가 제공하는 컴파일러를 무조건 사용해야 한다는 점과 복잡하다는 단점이 있습니다.
클래스 로딩 시점
class 파일을 JVM 내부의 클래스 로더에 보관하기 전에 조작하여 부가 기능 로직을 추가하는 방식입니다.
클래스 로딩 또한 모든 지점에 적용이 가능합니다.
특별한 옵션과 클래스 로더 조작기를 지정해야 하기 때문에 운영이 어렵다는 단점이 있습니다.
런타임 시점 – Pick
Spring Framework에서 사용하는 방식입니다.
컴파일이 끝나고 클래스 로더에 이미 올라간 Java가 실행된 다음에 동작하는 방식입니다.
실제 대상 코드는 그대로 유지되고, 프록시를 통해 부가 기능이 적용됩니다. 이 프록시는 메서드 오버라이딩 개념으로 동작하기에 메서드에만 적용가능합니다. 그래서 스프링 빈에만 AOP를 적용할 수 있죠.
특별한 조건이나 모듈이 필요 없고 스프링만 있으면 AOP를 적용할 수 있기 때문에 스프링 AOP는 런타임 방식을 활용합니다.
스프링 AOP는 AspectJ 문법을 사용하고 프록시 방식의 AOP를 제공합니다. AspectJ가 제공하는 어노테이션이나 관련 인터페이스만 사용하기 때문에 앞서 컴파일 시점에서 필요했던 컴파일러 등을 사용하지 않습니다. 즉, AOP는 AspectJ를 직접 사용하는 것이 아닙니다.
3. AOP 주요 개념
- Aspect : 흩어진 관심사를 모듈화 한 것. 주로 부가기능을 모듈화합니다.
- Target : Aspect를 적용하는 곳(Class, Method, Interface 등등)
- Advice : 실질적으로 어떤 일을 해야할 지에 대한 것을 정의합니다. 실질적으로 부가기능을 담은 구현체입니다.
- JoinPoint : Advice가 적용할 위치, 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점을 적용하는 것.
- PointCut : JointPoint의 상세한 스펙을 정의하는 것. JointPoint는 추상적이라면 PointCut은 그것을 구체화하여 Advice가 실행할 시점을 정확하게 정의합니다.
@Component
@Aspect
public class PerfAspect {
@Around("@annotation(PerLogging)")
public Object logPerf(ProceedingJoinPoint pjp) throws Throwable{
long begin = System.currentTimeMillis();
Object retVal = pjp.proceed();
System.out.println(System.currentTimeMillis() - begin);
return retVal;
}
}
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.CLASS)
public @interface PerLogging {
}
@Component
public class SimpleEventService implements EventService {
@PerLogging
@Override
public void createEvent() {
try {
Thread.sleep(1000);
} catch(InterruptedException e) {
e.printStackTrace();
}
System.out.println("Created an event");
}
@Override
public void publishEvent() {
try {
Thread.sleep(1000);
} catch (InterruptedException e){
e.printStackTrace();;
}
System.out.println("Published an event");
}
@PerLogging
@Override
public void deleteEvent() {
System.out.println("Delete an event");
}
}
위 코드를 보면 PerfAspect라는 클래스에 @Aspect 어노테이션을 활용한 것을 볼 수 있습니다. 그 상태에서 Around 어노테이션을 통해 원하는 특정 Method를 지정하면 해당 메소드가 실행될 때마다 logPerf 메서드가 실행됩니다.
Target Method 종류
- @Before : 어드바이스 타켓 메소드가 호출되기 전에 어드바이스 기능을 수행
- @Afeter : 타켓 메소드의 결과에 관계없이 타겟 메소드가 완료되면 어드바이스 기능 수행
- @AfterReturning : 타켓 메소드가 성공적으로 결과 값을 반환 후에 어드바이스 기능을 수행
- @AfterThrowing : 타겟 메소드가 수행 중 예외를 던지게 되면 어드바이스 기능을 수행
- @Around : 어드바이스가 타켓 메소드를 감싸서 타겟 메소드 호출 전과 후에 어드바이스 기능을 수행한다.
DI 란?
DI는 Dependency Injection의 약자로 의존성 주입이라는 뜻을 가지고 있습니다. 이후에 다룰 IoC와 함께 이야기되는 개념이지만 IoC와 DI는 같은 개념이 아니라는 것을 알아야 합니다. DI는 IoC에서 다루는 프로그램 제어권을 역전시키는 방법 중 하나로써 활용됩니다.
DI는 디자인 패턴 중 하나로 객체의 의존관계를 외부에서 주입시키는 패턴입니다.
여기서 나온 의존관계란 의존대상 A가 변하면, 그것이 B에 영향을 미친다는 뜻을 의미합니다.
public class A {
private B b = new B();
}
위와 같은 코드를 A가 B에 의존한다라고 이야기 할 수 있습니다. B가 변경될 때 A도 영향을 미치게 되기 때문입니다.
아래와 같은 코드가 DI 방식으로 의존을 주입한 코드입니다.
@RestController
@RequestMapping("/members")
public class MemberController {
private final MemberService memberService;
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
...
}
위 코드처럼 의존성을 주입하면 memberService가 변동되더라도 MemerController는 영향이 없도록 할 수 있습니다.
이렇게 활용되는 DI는 어떤 장점이 있을까요?
먼저 DI 원칙을 지킨 코드는 더욱 깔끔하게 작성할 수 있으며, 객체에 의존성이 제공될 때 분리하는 것이 더욱 효과적입니다. 객체는 의존성을 조회하지 않으며 의존성의 위치나 클래스를 알지 못합니다. 또한, 의존성이 추상화된 Interface나 추상 클래스에 있다면 테스트가 쉬워집니다. 여기에 재사용성을 높일 수 있기 때문에 DI를 활용하고 있습니다.
스프링에서 객체는 Bean라고 부르며, 프로젝트가 실행될 때 Bean으로 관리하는 개체들의 생성과 소멸에 대해 자동적으로 작업해주는데 이것을 스프링에서는 Bean 컨테이너라고 부릅니다.
IoC 란?
IoC는 Inversion of Control의 약자로 제어의 역전이라는 뜻을 가지고 있습니다. 말 그대로 메소드나 객체의 호출작업을 개발자가 결정하는 것이 아니라 외부에서 결정되는 것을 의미합니다.
객체의 의존성을 역전시키면 객체 간의 결합도가 줄어들고 유연한 코드를 작성할 수 있습니다. 가독성 및 코드 중복, 유지보수를 편하게 할 수 있습니다. 이런 이유로 IoC를 활용합니다.
기존의 객체 생성 순서와 IoC 객체 생성 순서는 약간의 차이점이 있습니다.
기존의 객체 생성 순서
- 객체 생성
- 의존성 객체 생성 – 클래스 내부에서 생성
- 의존성 객체 메소드 호출
스프링 객체 생성 순서
- 객체 생성
- 의존성 객체 주입 – 제어권을 스프링에게 위임하여 스프링이 만들어놓은 객체를 주입한다.
- 의존성 객체 메소드 호출
스프링이 모든 의존성 객체를 스프링이 실행될 때 전부 만들어 줍니다. 그리고 만들어진 객체를 필요한 곳에 주입을 해주는데 이것을 Bean이라는 이름으로 다룹니다. Bean은 싱글턴 패턴의 특징을 가지고 있으며, 제어의 흐름을 사용자가 아닌 스프링이 가지고 작업을 처리하게 됩니다.
참고 자료
https://engkimbs.tistory.com/746
https://velog.io/@backtony/Spring-AOP-%EC%B4%9D%EC%A0%95%EB%A6%AC