본 글은 📚 스프링 입문을 위한 자바 객체지향의 원리와 이해 를 읽고 정리한 내용입니다.
스프링의 3대 프로그래밍 모델 중 첫 번째인 DI를 지난 글에서 살펴봤다. 두 번째는 AOP이다. AOP는 Aspect-Oriented Programming의 약자이고, 이를 번역하면 관점 지향 프로그래밍이 된다.
스프링 DI가 의존성(new)에 대한 주입이라면 스프링 AOP는 로직(code) 주입이라고 할 수 있다.
위의 그림을 보면 입금, 출금, 이체 모듈에서 로깅, 보안, 트랜잭션 기능이 반복적으로 나타나는 것을 볼 수 있다. 프로그램을 작성하다 보면 이처럼 다수의 모듈에 공통적으로 나타나는 부분이 존재한다. 바로 이것을 횡단 관심사(cross-sutting concern)라고 한다.
코드 = 핵심 관심사 + 횡단 관심사
핵심 관심사는 모듈별로 다르지만 횡단 관심사는 모듈별로 반복되어 중복해서 나타나는 부분이다. "반복/중복은 분리해서 한 곳에서 관리"라는 말이 프로그래머라면 떠올라야 한다.
남자와 여자의 삶을 프로그래밍 한다고 생각했을 때. 이들이 집에 들어가서 하는 일을 간단하게 의사코드로 작성해보자.
남자용 의사코드 | 여자용 의사코드 | 예외상황처리 |
열쇠로 문을 열고 집에 들어간다. | 열쇠로 문을 열고 집에 들어간다. | 집에 불남 -> 119에 신고한다. |
컴퓨터로 게임을 한다. | 책을 펴고 공부를 한다. | |
소등하고 잔다. | 소등하고 잔다. | |
자물쇠로 잠그고 집을 나선다. | 자물쇠로 잠그고 집을 나선다. |
두 의사코드를 보면서 중복해서 나타나는 횡단 관심사와 각 의사 코드에서만 나타나는 핵심 관심사를 분리해보자.
// 남자 클래스
public class Boy {
public void runSomething() {
System.out.println("열쇠로 문을 열고 집에 들어간다.");
try {
System.out.println("컴퓨터로 게임을 한다.");
} catch (Exception e) {
if (e.getMessage().equals("집에 불남")) {
System.out.println("119에 신고한다.");
}
} finally {
System.out.println("소등하고 잔다.");
}
System.out.println("자물쇠를 잠구고 집을 나선다.");
}
}
// 여자 클래스
public class Girl {
public void runSomething() {
System.out.println("열쇠로 문을 열고 집에 들어간다.");
try {
System.out.println("책을 펴고 공부를 한다.");
} catch (Exception e) {
if (e.getMessage().equals("집에 불남")) {
System.out.println("119에 신고한다.");
}
} finally {
System.out.println("소등하고 잔다.");
}
System.out.println("자물쇠를 잠구고 집을 나선다.");
}
}
public class Start {
public static void main(String[] args) {
Boy romeo = new Boy();
Girl juliet = new Girl();
romeo.runSomething();
juliet.runSomething();
}
}
// 결과
열쇠로 문을 열고 집에 들어간다.
컴퓨터로 게임을 한다.
소등하고 잔다.
자물쇠를 잠구고 집을 나선다.
열쇠로 문을 열고 집에 들어간다.
책을 펴고 공부를 한다.
소등하고 잔다.
자물쇠를 잠구고 집을 나선다.
"스프링 DI가 의존성에 대한 주입이라면 스프링 AOP는 로직 주입이라고 볼 수 있다." 로직을 주입한다면 어디에 주입할 수 있을까?
객체 지향에서 로직(코드)이 있는 곳은 당연히 메소드 안쪽이다. 그럼 메소드에서 코드를 주입할 수 있는 곳은 몇 군데일까?
왼쪽 그림에서 볼 수 있듯이 Around, Before, After, AfterReturning, AfterThrowing 으로 총 5 군데이다. 그림에서는 메소드 시작 전, 메소드 종료 후라고 표현했지만, 메소드 시작 직후, 메소드 종료 직전으로 생각해도 된다.
이제 스프링 AOP를 통해 어떻게 횡단 관심사를 분리해 낼 수 있는지, 분리된 횡단 관심사(로직)를 어떻게 실행 시간에 메소드에 주입할 수 있는지를 살펴보겠다.
☝🏻 일단 덤벼 보자 - 실전편
앞 절에서 Boy.java, Girl.java를 만들어 횡단 관심사와 핵심 관심사가 무엇인지 살펴보았다. 이제 AOP 적용을 해보자. AOP 적용을 위해 인터페이스 기반으로 Boy.java 를 바꿔보겠다.
Person이라는 인터페이스를 추가하고 Boy.java는 Person을 구현하고 핵심 관심사가 아닌 나머지 부분을 모두 지우자.
// Person 인터페이스
public interface Person {
void runSomething();
}
// Boy 클래스
public class Boy implements Person {
public void runSomething() {
System.out.println("컴퓨터로 게임을 한다.");
}
}
횡단 관심사를 지우니 Boy.java 클래스가 조금 깔끔해졌다. 자, 이제 Start.java가 스프링 프레임워크 기반에서 구동될 수 있게 변경해보자.
// 실행 클래스
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Start {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("aop002/aop002.xml");
Person romeo = context.getBean("boy",Person.class);
romeo.runSomething();
}
}
그리고 스프링 설정 파일인 XML파일을 aop002.xml 파일명으로 만들어보자.
XML파일을 만드는 방법은 패키지 선택 -> new -> other -> spring -> Spring Bean Configuration File 을 선택한 뒤 파일명.xml 로 파일명을 지정하면 만들 수 있다.
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:aop="http://www.springframework.org/schema/aop"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
<aop:aspectj-autoproxy />
<bean id="myAspect" class="aop002.MyAspect"/>
<bean id="boy" class="aop002.Boy"/>
</beans>
그리고 위와 같이 소스를 변경하자. xmlns 에 추가하는 코드들은 Namespaces 탭으로 이동한 뒤 aop를 선택하면 자동으로 생긴다.
그리고 MyAspect 클래스도 아래와 같이 만들자.
// MyAspect 클래스 생성
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
@Aspect
public class MyAspect {
@Before("execution(public void aop002.Boy.runSomething())")
public void before(JoinPoint joinPoint) {
System.out.println("얼굴 인식 확인 : 문을 개방하라");
// System.out.println("열쇠로 문을 열고 집에 들어간다.");
}
}
간단하게 처리하기 위해 어노테이션을 사용했다.
- @Aspect : 이 클래스를 이제 AOP에서 사용하겠다는 의미.
- @Before : 대상 메소드 실행 전에 이 메소드를 실행하겠다는 의미.
JoinPoint 는 @Before에서 선언된 메소드인 aop002.Boy.runSomething()을 의미한다.
이제 열쇠로 문을 열고 집에 들어가는 것이 아니라 스프링 프레임워크가 사용자를 인식해 자동으로 문을 열어주게 된다.
이제 Start.java 를 실행해보면 다음과 같은 결과가 나온다.
INFO : ---- (생략)
얼굴 인식 확인 : 문을 개방하라
컴퓨터로 게임을 한다.
만약 에러가 난다면 pom.xml에 아래의 부분을 찾아
<!-- AspectJ -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjrt</artifactId>
<version>${org.aspectj-version}</version>
</dependency>
<!-- 추가한 코드 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.6</version>
</dependency>
위 추가한 코드라고 주석이 달린 코드를 추가하자. 그럼 정상작동하는 것을 볼 수 있을 것이다.
✌🏻 일단 덤벼 보자 - 설명편
위에서 횡단 관심사를 분리하는 예제를 실습했다. 사실 코드로만 봐서는 이해가 잘 되지 않는 부분도 있을 것이다. 이제 이론적인 부분을 살펴보며 하나씩 차근차근 이해해보자.
AOP 적용 전 후의 Boy.java와 관련 코드를 비교해보자.
System.out.println("열쇠로 문을 열고 집에 들어간다.")만 AOP로 구현했음에도 코드의 양이 상당히 늘어났다. 여기서 주목할 점은 Boy.java의 코드가 어떻게 됐느냐이다. 횡단 관심사는 모두 사라졌고 오직 핵심 관심사만 남아있다.
파일은 4개로 분리되어 개발해야하지만 유지보수나 추가 개발하는 관점에서 보면 무척 편한 코드가 되었다. AOP를 적용하면서 Boy.java에 단일 책임 원칙(SRP)을 자연스럽게 적용하게 된 것이다.
위에 실전편에서 실행결과는 아래와 같았다.
INFO : ---- (생략)
얼굴 인식 확인 : 문을 개방하라
컴퓨터로 게임을 한다.
이로서 위의 사진처럼 @Before로 만들어진 before() 메소드가 런타임에 위의 사진처럼 주입이 되는 것을 알 수 있다. 조금 더 상세하게 나타내면 다음과 같다.
@Before("execution(public void aop002.Boy.runSomething())") 부분을 @Before("execution(* runSomething())") 이렇게 고쳐보자. 그리고 실행하면 정상 작동하는 것을 볼 수 있다. 이제 Girl.java의 runSomething() 메소드도 @Before 를 통해 같은 로직을 주입할 수 있다는 것을 의미한다. Girl.java 에 Person 인터페이스를 구현하고 핵심 관심사만 남기고 지운 뒤 아래와 같이 소스를 변경해보자.
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;
public class Start {
public static void main(String[] args) {
ApplicationContext context = new ClassPathXmlApplicationContext("aop002/aop002.xml");
Person romeo = context.getBean("boy",Person.class);
Person juliet = context.getBean("girl",Person.class);
romeo.runSomething();
juliet.runSomething();
}
}
// XML에 추가할 코드
<bean id="girl" class="aop002.Girl"/>
실행결과는 다음과 같다.
얼굴 인식 확인 : 문을 개방하라
컴퓨터로 게임을 한다.
얼굴 인식 확인 : 문을 개방하라
책을 펴고 공부를 한다.
이제 유지보수, 추가 개발이 용이하다는 것이 무슨 말인지 와닿을 것이다. 아래는 AOP적용 전 후의 전체적인 비교를 하는 사진이다.
위의 사진에서 초록색 부분은 사라진 부분, 빨간색 부분은 스프링 AOP가 인터페이스 기반으로 작동하기 위해 추가된 부분, 굵게 표시된 부분은 스프링 프레임워크를 적용하고 활용하는 부분이다.
스프링 AOP는 인터페이스 기반이므로 Person.java 인터페이스가 등장한 것이고, MyAspect.java는 횡단 관심사를 처리하기 때문에 존재한다.
그럼 이제 XML파일을 파해쳐보자.
<?xml version="1.0" encoding="UTF-8"?>
<beans 생략/>
<aop:aspectj-autoproxy />
<bean id="myAspect" class="aop002.MyAspect"/>
<bean id="boy" class="aop002.Boy"/>
<bean id="girl" class="aop002.Girl"/>
</beans>
세 개의 빈으로 이루어져 있으며 빈이 존재하는 이유는 객체의 생성과 의존성 주입을 스프링 프레임워크에게 위임하기 위해서이다. 스프링 프레임워크는 객체 생성 뿐만 아니라 객체의 생명주기 전반에 걸쳐 빈의 생성에서 소멸까지 관리한다. boy빈과 girl빈은 AOP의 적용 대상이기에 등록할 필요가 있고 myAspect 빈은 Aspect이기에 등록해야 한다.
그럼 <aop:aspectj-autoproxy />은 무엇인가? j는 자바의 약자, auto는 자동, proxy는 프록시 패턴을 이용해 횡단 관심사를 핵심 관심사에 주입하는 것이다.
호출하는 쪽에서 romeo.runSomething() 메소드를 호출하면 프록시가 그 요청을 받아 진짜 romeo 객체에게 요청을 전달한다. 또한 중앙의 runSomething() 메소드는 주고받는 내용을 감시하거나 조작할 수 있다.
스프링 AOP는 프록시를 사용한다. 그런데 스프링 AOP에서 호출하는쪽과 호출당하는 쪽 모두 프록시가 존재하는 것을 모른다. 오직 스프링 프레임워크만 프록시의 존재를 안다. 프록시의 역할은 중간에서 가로채는 것이다.
결국 <aop:aspectj-autoproxy />는 스프링 프레임워크에게 AOP 프록시를 사용하라고 알려주는 지시자 같은 것이다. auto가 붙었으니 자동으로! 라는 뜻도 들어간다.
길고 긴 스프링 AOP에 대해서 알아봤다. 하지만 여기서 끝이 아니다. 스프링 AOP의 용어에 대해서 다음 글에서 살펴보자.
마지막으로 스프링 AOP의 핵심을 정리해보자.
- 스프링 AOP는 인터페이스(interface) 기반이다.
- 스프링 AOP는 프록시(proxy) 기반이다.
- 스프링 AOP는 런타임(runtime) 기반이다.
댓글