2. AOP - Aspect? 관점? 핵심 관심사? 횡단 관심사?
AOP는 Aspect-Oriented Programming의 약자이고, 이를 번역하면 관점 지향 프로그래밍이 된다.
스프링 DI가 의존성에 대한 주입이라면 스프링 AOP는 로직(code) 주입이라고 할 수 있다.
위 그림을 보면 입금, 출금, 이체 모듈에서 로깅, 보안, 트랜잭션 기능이 반복적으로 나타나는 것을 볼 수 있다.
프로그램을 작성하다 보면 이처럼 다수의 모듈에 공통적으로 나타나는 부분이 존재하는데, 바로 이것을 횡단 관심사(cross-cutting concern)라고 한다.
코드 = 핵심 관심사 + 횡단 관심사
핵심 관심사는 모듈별로 다르지만 횡단 관심사는 모듈별로 반복되어 중복해서 나타나는 부분이다.
남자와 여자의 삶을 프로그래밍한다고 생각해보자.
남자용 의사 코드
열쇠로 문을 열고 집에 들어간다.
컴퓨터로 게임을 한다.
소등하고 잔다.
자물쇠를 잠그고 집을 나선다.
--------------------------------
예외상황처리: 집에 불남 - 119에 신고한다.
여자용 의사 코드
열쇠로 문을 열고 집에 들어간다.
요리를 한다.
소등하고 잔다.
자물쇠를 잠그고 집을 나선다.
--------------------------------
예외상황처리: 집에 불남 - 119에 신고한다.
밑줄 그어진 부분이 핵심 관심사, 그 외의 부분이 횡단 관심사라고 할 수 있다.
그럼 로직을 어디에 주입할 수 있을까?
Around(메서드 전 구역), Before(메서드 시작 직후), After(메서드 종료 직전), AfterReturning(메서드 정상 종료 후), AfterThrowing(메서드에서 예외가 발생 후 종료 후)이다.
그럼 스프링 AOP를 통해 어떻게 횡단 관심사를 분리해 낼 수 있는지, 분리된 횡단 관심사를 어떻게 실행 시간에 메서드에 주입할 수 있는지 살펴보자.
2-1. 일단 덤벼보자 - 실전편
public class Boy {
public void runSomething() {
System.out.println("열쇠로 문을 열고 집에 들어간다.");
try {
System.out.println("컴퓨터로 게임을 한다.");
} catch (Exception ex) {
if (ex.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 ex) {
if (ex.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();
}
}
위에서 사용한 예시를 자바 코드로 구현한 것이다.
AOP 적용을 위해 일단 Boy.java만 인터페이스 기반으로 바꿔보자.
public interface Person {
void runSomething();
}
------------------------------------------------------------------------------------------------
public class Boy implements Person{
@Override
public void runSomething() {
System.out.println("컴퓨터로 게임을 한다.");
}
}
------------------------------------------------------------------------------------------------
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.JoinPoint;
@Aspect
public class MyAspect {
@Before("execution(public void aop002.Boy.runSomething ())")
public void before(JoinPoint joinPoint) {
System.out.println("얼굴 인식 확인: 문을 개방하라");
}
}
------------------------------------------------------------------------------------------------
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.xml");
Person romeo = context.getBean("boy", Person.class);
romeo.runSomething();
}
}
------------------------------------------------------------------------------------------------
<?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/aop
http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<aop:aspectj-autoproxy />
<bean id="myAspect" class="aop002.MyAspect" />
<bean id="boy" class="aop002.Boy" />
</beans>
먼저 Person 인터페이스를 만들고 Boy클래스가 Person 인터페이스를 구현하도록 만든다.
MyAspect 클래스에서 사용된 어노테이션
@Aspect: 이 클래스를 이제 AOP에서 사용하겠다는 의미
@Before: 대상 메서드 실행 전에 이 메서드를 실행하겠다는 의미
이제 열쇠로 문을 열고 집에 들어가는 것이 아니라 스프링 프레임워크가 사용자를 인식해 자동으로 문을 열어주게 된다.
Boy클래스에는 횡단 관심사는 모두 사라지고 핵심 관심사만 남은 것이 보인다.
개발할 때는 한 개의 Boy.java를 4개의 파일로 분할해서 개발해야하는 수고를 해야 하지만 추가 개발과 유지보수 관점에서 보면 무척 편한 코드가 된 것을 알 수 있다.
AOP를 적용하면서 Boy.java에 단일 책임 원칙(SRP)를 자연스럽게 적용하게 된 것이다.
만약 Gir.java나 Children.java 등의 파일을 추가한다고 생각해보자.
public interface Person {
void runSomething();
}
------------------------------------------------------------------------------------------------
public class Boy implements Person{
@Override
public void runSomething() {
System.out.println("컴퓨터로 게임을 한다.");
}
}
------------------------------------------------------------------------------------------------
public class Girl implements Person{
@Override
public void runSomething() {
System.out.println("요리를 한다.");
}
}
------------------------------------------------------------------------------------------------
public class Children implements Person{
@Override
public void runSomething() {
System.out.println("숙제를 한다.");
}
}
------------------------------------------------------------------------------------------------
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.JoinPoint;
@Aspect
public class MyAspect {
@Before("* runSomething ())")
public void before(JoinPoint joinPoint) {
System.out.println("얼굴 인식 확인: 문을 개방하라");
}
}
------------------------------------------------------------------------------------------------
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.xml");
Person romeo = context.getBean("boy", Person.class);
Person juliet = context.getBean("girl", Person.class);
Person rose = context.getBean("children", Person.class);
romeo.runSomething();
juliet.runSomething();
rose.runSomething();
}
}
------------------------------------------------------------------------------------------------
<?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/aop
http://www.springframework.org/schema/aop/spring-aop-3.1.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd">
<aop:aspectj-autoproxy />
<bean id="myAspect" class="aop002.MyAspect" />
<bean id="boy" class="aop002.Boy" />
<bean id="girl" class="aop002.Girl" />
<bean id="children" class="aop002.Children" />
</beans>
//결과
얼굴 인식 확인: 문을 개방하라
컴퓨터로 게임을 한다.
얼굴 인식 확인: 문을 개방하라
요리를 한다.
얼굴 인식 확인: 문을 개방하라
숙제를 한다.
별도의 파일을 추가할 것 없이 각 클래스에서 핵심 관심사만 구현하고 몇 줄만 추가하면 된다.
이제 코드를 차근차근 살펴보자.
Boy클래스가 Person 인터페이스를 구현한 이유는 스프링 AOP가 인터페이스 기반으로 작동하기 때문이다.
MyAspect.java는 횡단 관심사를 처리하는 용도로 만들었다. Boy클래스에서 사라진 횡단 관심사를 누군가는 처리해야 하기 때문이다.
2-2. 일단 덤벼 보자 - 용어편
지금부터 설명할 5가지 용어와 스프링 AOP에 숨어있는 프록시를 이해하면 스프링 AOP를 활용하는 데 충분하다.
- Pointcut - 자르는 지점? Aspect 적용 위치 지정자!
@Aspect
public class MyAspect {
@Before("execution(public void aop002.Boy.runSomething ())")
public void before(JoinPoint joinPoint) {
System.out.println("얼굴 인식 확인: 문을 개방하라");
}
}
여기서 public void aop002.Boy.runSomething() 이 바로 Pointcut이고, @Before 어노테이션은 aop002.Boy.runSomething()를 실행하기 전 선언하고 있는 메서드를 먼저 실행하라는 의미이다.
즉, public void before()는 횡단 관심사를 실행하는 메서드가 된다.
결국 Pointcut이라고 하는 것은 횡단 관심사를 적용할 타깃 메서드를 선택하는 지시자(메서드 선택 필터)인 것이다.
- JoinPoint - 연결점? 연결 가능한 지점!
Pointcut은 JoinPoint의 부분 집합이다. Pointcut의 후보가 되는 모든 메서드들이 JoinPoint, 즉 Aspect 적용이 가능한 지점이 된다.
@Aspect
public class MyAspect {
@Before("* runSomething ())")
public void before(JoinPoint joinPoint) {
System.out.println("얼굴 인식 확인: 문을 개방하라");
}
}
before() 메서드의 매개변수로 사용하는 JoinPoint의 실체는 무엇일까?
그때 그때 다르다.
romeo.runSomething() 메서드를 호출한 상태라면 JoinPoint는 romeo 객체의 runSomething() 메서드가 된다.
- Advice - 조언? 언제, 무엇을!
Advice는 pointcut에 적용할 로직, 즉 메서드를 의미하는데 여기에 더해 언제라는 개념까지 포함하고 있다.
- Aspect - 관점? 층면? Advisor의 집합체!
AOP에서 Aspect는 여러 개의 Advice와 여러 개의 Pointcut의 결합체를 의미하는 용어다.
Aspect = Advice들 + Pointcut들
- Advisor - 조언자? 어디서, 언제, 무엇을!
Advisor = 한 개의 Advice + 한 개의 Pointcut
Advisor는 스프링 AOP에서만 사용하는 용어이며 다른 AOP 프레임워크에서는 사용하지 않는다.
또 스프링 버전이 올라가면서 이제는 쓰지 말라고 권고하는 기능이기도 하다.
2-3. 일단 덤벼 보자 - POJO와 XML 기반 AOP
기존의 어노테이션 기반으로 작성한 AOP 예제를 POJO와 XML 기반으로 전환하기 위해 변경할 곳은 Aspect가 정의되어 있는 MyAspect.java와 스프링 설정 정보 XML 파일이다.
먼저 MyAspect.java 파일부터 보자.
import org.aspectj.lang.JoinPoint;
public class MyAspect {
public void before(JoinPoint joinPoint) {
System.out.println("얼굴 인식 확인: 문을 개방하라");
}
}
@Aspect 어노테이션과 @Before 어노테이션이 사라졌다.
이제 스프링 프레임워크에 의존하지 않는 POJO가 된 것이다.
(생략)
<aop:aspectj-autoproxy />
<bean id="myAspect" class="com.codestates.section2week3.aop002.MyAspect" />
<bean id="boy" class="com.codestates.section2week3.aop002.Boy" />
<bean id="girl" class="com.codestates.section2week3.aop002.Girl" />
<bean id="children" class="com.codestates.section2week3.aop002.Children" />
<aop:config>
<aop:aspect ref="myAspect">
<aop:before method="before" pointcut="execution(* runSomething())" />
</aop:aspect>
</aop:config>
스프링 설정 파일에는 aop 관련 태그가 추가되었다.
2-4. AOP 기초 완성
이번에는 After 어드바이스를 살펴보자.
public class MyAspect {
public void before(JoinPoint joinPoint) {
System.out.println("얼굴 인식 확인: 문을 개방하라");
}
public void lockDoor(JoinPoint joinPoint) {
System.out.println("주인님 나갔다: 어이 문 잠가!!!");
}
}
<aop:config>
<aop:aspect ref="myAspect">
<aop:before method="before" pointcut="execution(* runSomething())" />
<aop:after method="lockDoor" pointcut="execution(* runSomething())" />
</aop:aspect>
</aop:config>
Before 어드바이스와 유사해서 따로 설명은 하지 않겠다.
3. PSA - 일관성 있는 서비스 추상화
서비스 추상화의 예로 JDBC를 들 수 있다.
JDBC라는 표준 스펙이 있기에 오라클, MySQL, MS-SQL 중 어떤 걸 선택하느냐에 관계 없이 공통된 방식으로 코드를 작성할 수 있다.
이처럼 어댑터 패턴을 적용해 같은 일을 하는 다수의 기술을 공통의 인터페이스로 제어할 수 있게 한 것을 서비스 추상화라고 한다.
'Reading > 스프링 입문을 위한 자바 객체지향의 원리와 이해' 카테고리의 다른 글
7. 스프링 삼각형과 설정 정보_IoC/DI (0) | 2023.04.01 |
---|---|
5. 객체 지향 설계 5원칙 - SOLID (0) | 2023.03.29 |
4. 자바가 확장한 객체 지향 (0) | 2023.03.23 |
3. 자바와 객체 지향 (0) | 2023.03.23 |
2. 자바와 절차적/구조적 프로그래밍 (0) | 2023.03.21 |