[Spring] DI & AOP
Dependency 의존관계란?
'A가 B를 의존한다.' 라는 표현은 어떤 의미일까? 토비의 스프링에서는 다음과 같이 정의한다.
의존대상 B가 변하면, 그것이 A에 영향을 미친다.
즉, B의 기능이 수정된다면 A에게까지 영향을 미친다.
다음 예시를 보며 이해해보자.
"햄버거 가게 요리사는 햄버거 레시피에 의존한다." 는 말은 햄버거 레시피가 변경되었을 때, 변경된 레시피에 따라서 요리사는 햄버거 만드는 방법을 바꿔야한다. 레시피의 변화가 요리사의 행위에 영향을 미쳤기 때문에 "요리사는 레시피에 의존한다"고 말할 수 있다.
코드로 표현해보면 다음과 같다.
class BurgerChef {
private HamBurgerRecipe hamBurgerRecipe;
public BurgerChef() {
hamBurgerRecipe = new HamBurgerRecipe();
}
}
여기에서 HamBurgerRecipe만을 의존할 수 있는 구조로 되어있다. 만약 더 다양한 햄버거 레시피를 의존 받을 수 있게 구현하려면 인터페이스로 추상화해야한다.
이를 코드로 표현해보면 다음과 같다.
class BurgerChef {
private BurgerRecipe burgerRecipe;
public BurgerChef() {
burgerRecipe = new HamBurgerRecipe();
//burgerRecipe = new CheeseBurgerRecipe();
//burgerRecipe = new ChickenBurgerRecipe();
}
}
interface BugerRecipe {
newBurger();
// 이외의 다양한 메소드
}
class HamBurgerRecipe implements BurgerRecipe {
public Burger newBurger() {
return new HamBerger();
}
// ...
}
class CheeseBurgerRecipe implements BurgerRecipe {
public Buerger newBurger() {
return new CheeseBurger();
}
// ...
}
의존관계를 인터페이스로 추상화하게 되면, 더 다양한 의존 관계를 맺을 수 있고 실제 구현 클래스와의 관계가 느슨해지고, 결합도가 낮아진다.
그럼 DI(Dependency Injection)은 뭔가?
위에 코드를 보면 BurgerChef 내부적으로 의존관계인 BurgerRecipe가 어떤 값을 가질지 직접 정하고 있다. 만약 어떤 BurgerRecipe를 만들지를 버거 가게 사장님이 정하는 상황을 가정해보자. 즉, BurgerChef가 의존하고 있는 BurgerRecipe를 외부(사장님)에서 결정하고 주입하는 것이다.
이처럼 의존관계를 외부에서 결정하고 주입하는 것이 DI(의존관계 주입)이다.
DI를 통해서 모듈 간의 결합도가 낮아지고 유연성이 높아진다.
DI 구현 방법
DI는 의존관계를 외부에서 결정하는 것이기 때문에, 클래스 변수를 결정하는 방법들이 곧 DI를 구현하는 방법이다. 런타임 시점의 의존관계를 외부에서 주입해 DI 구현이 완성된다.
"Burger 레스토랑 주인이 어떤 레시피를 주입하는지 결정하는 예시로 설명하고자 한다."
[생성자 이용]
class BurgerChef {
private BurgerRecipe burgerRecipe;
public BurgerChef(BurgerRecipe burgerRecipe) {
this.burgerRecipe = burgerRecipe;
}
}
class BurgerRestaurantOwner {
private BurgerChef burgerChef = new BurgerChef(new HamburgerRecipe());
public void changeMenu() {
burgerChef = new BurgerChef(new CheeseBurgerRecipe());
}
}
[메소드 이용]
class BurgerChef {
private BurgerRecipe burgerRecipe = new HamburgerRecipe();
public void setBurgerRecipe(BurgerRecipe burgerRecipe) {
this.burgerRecipe = burgerRecipe;
}
}
class BurgerRestaurantOwner {
private BurgerChef burgerChef = new BurgerChef();
public void changeMenu() {
burgerChef.setBurgerRecipe(new CheeseBurgerRecipe());
}
}
DI의 장점은 뭘까?
1. 의존성이 줄어든다.
의존성이 높을 수록 대상이 변화했을 때, 이에 맞게 다른 것도 수정해야한다. DI로 구현했을 경우, 주입받는 대상이 변하더라도 그 구현 자체를 수정할 일이 없거나 줄어들게 된다.
2. 재사용성이 높은 코드가 된다.
기존에 BurgerChef 내부에서만 사용되었던 BurgerRecipe를 별도로 구분하여 구현한다면, 다른 클래스에서 재사용할 수 있다.
3. 가독성이 높아진다.
기능들을 별도로 분리하게 되어 자연스럽게 가독성이 높아진다.
관점 지향 프로그래밍 (AOP)란 뭘까?
AOP는 OOP를 돕는 보조적인 기술로, 관심사의 분리의 문제를 해결하기 위해 만들어진 프로그래밍 패러다임이다.
여러 객체에 공통으로 적용할 수 있는 기능을 분리함으로써 재사용성을 높일 수 있다.
만약 메서드를 실행할 때마다 공통적으로 확인하고 싶은 정보가 있는데, 모든 메서드에 해당 로그 코드를 작성하면 코드가 길어질 뿐만아니라 가독성이 떨어질 것이다.
이를 AOP로 분리시킨다면..
@Aspect
@Component
public class ParameterAop {
//com/example/aop/controller 패키지 하위 클래스들 전부 적용하겠다고 지점 설정
@Pointcut("execution(* com.example.aop.controller..*.*(..))")
private void cut() {}
//cut() 메서드가 실행 되는 지점 이전에 before() 메서드 실행
@Before("cut()")
public void before(JoinPoint joinPoint) {
//실행되는 함수 이름을 가져오고 출력
MethodSignature methodSignature = (MethodSignature) joinPoint.getSignature();
Method method = methodSignature.getMethod();
System.out.println(method.getName() + "메서드 실행");
//메서드에 들어가는 매개변수 배열을 읽어옴
Object[] args = joinPoint.getArgs();
//매개변수 배열의 종류와 값을 출력
for(Object obj : args) {
System.out.println("type : "+obj.getClass().getSimpleName());
System.out.println("value : "+obj);
}
}
//cut() 메서드가 종료되는 시점에 afterReturn() 메서드 실행
//@AfterReturning 어노테이션의 returning 값과 afterReturn 매개변수 obj의 이름이 같아야 함
@AfterReturning(value = "cut()", returning = "obj")
public void afterReturn(JoinPoint joinPoint, Object obj) {
System.out.println("return obj");
System.out.println(obj);
}
}
AOP 클래스를 구현함으로써 이전보다 코드가 훨씬 간결해지고 가독성이 높아지는 것을 확인할 수 있다.