본문 바로가기

Frame Work/Spring

Spring AOP

 

※ AOP(Aspect Oriented Programming) ?

 

어플리케이션은 다양한 공통 기능이 필요하고 이러한 공통 기능들은 어플리케이션의 핵심 비지니스 로직과 구분되는 기능입니다. 핵심 비지니스 로직과 구분하기 위해 공통 기능을 공통 관심 사항(Cross-Cutting Concern) 이라고 표현하며, 핵심 로직을 핵심 관심 사항(Core Concern)이라고 표현합니다.

 

이때 공통의 관심사항들을 객체 지향 기법(상속이나 위임 등등)을 사용해서 여러 모듈에 효과적으로 적용하는데 한계가 있고 이는 중복된 코드를 만들어낸다. 이런 한계를 극복하기 위해 AOP 프로그래밍 기법이 등장했습니다. 

 

AOP는 문제를 바라보는 관점을 기준으로 프로그래밍 하는 기법을 의미합니다. 

 

이해를 돕기 위한 사진

 

핵심 로직을 구현한 코드에 공통된 기능 관련 코드는 없기 때문에 공통 기능이 변경되더라도 핵심 로직을 구현한 코드를 변경할 필요가 없다는 장점이 있습니다. 그냥 공통기능 코드만 변경하고 핵심 로직 구현 코드에 적용만 하면 됩니다. 

 

 

*  AOP 용어 

 

- Advice : 언제 공통 관심 기능을 핵심 로직에 적용 할 것인지 정의합니다. 

 

ex) 메서드를 호출 하기 전 (시점: 언제) 에 트랜잭션(기능 : 공통 기능)을 시작한다.

 

- Joinpoint : Advice를 적용 가능한 지점을 의미합니다.  메서드 호출, 필드 값 변경 등이 이에 해당합니다.

 

- Pointcut : Joinpoint의 부분 집합으로서 실제로 Advice가 적용되는 Joinpoint를 나타냅니다. 스프링은 정규표현식이나 AspectJ의 문법을 이용해서 표현 가능한데, 실제적으로 AspectJ 문법의 비중이 매우 높습니다. 

 

- Weaving : Advice를 핵심 로직 코드에 적용하는 것을 weaving이라고 합니다. 즉, 공통 코드를 핵심 로직 코드에 삽입하는 것이 weaving 입니다. 

 

- Aspect : 여러 객체에 공통으로 적용되는 공통 관심 사항을 Aspect라고 합니다. 트랙잭션이나 보안, 로깅 등이 Aspect의 예시입니다. 

 

* Weaving 방식 종류 

 

1. 컴파일 시에 Weaving 

 

핵심 로직을 구현한 자바 소스 코드를 컴파일 할 때에 알맞은 위치에 공통 코드를 삽입합니다. 이때 AOP가 적용된 클래스 파일이 생성됩니다.

 

2. 클래스 로딩 시에 Weaving

 

원본 클래스 파일은 변경하지 않고 클래스를 로딩 할 때에 JVM이 변경된 바이트 코드를 사용하도록 함으로써 AOP를 적용합니다. 

 

3. 런타임 시에 Weaving 하기 

 

소스 코드나 클래스 정보 자체를 변경하지 않고 프록시를 이용하여 AOP를 적용합니다. 즉, 핵심 로직을 구현한 객체에 직접 접근하는 것이 아니라 중간에 프록시를 생성하여 프록시를 통해서 핵심 로직을 구현한 객체에 접근하는 방식입니다. 

 

프록시는 핵심 로직을 실행하기 전 또는 후에 공통 기능을 적용하는 방식으로 AOP를 적용합니다. 메서드가 호출될 때에만 Advice를 적용할 수 있기 때문에 필드 값 변경과 같은 Joinpoint에 대한 적용을 할 수 없다는 단점이 있습니다. 

 

 

※ Spring에서 AOP

 

- 스프링은 자체적으로 프록시 기반의 AOP를 지원합니다.

 

- 프록시를 사용하기 때문에 Weaving은 런타임 시에 가능합니다. 

 

- 스프링은 3가지 방식으로 AOP를 구현합니다. 

 

1) XML 스키마 기반의 POJO 클래스를 이용한 AOP 구현 

2) AspectJ 5/6 에서 정의한 @Aspect 어노테이션 기반의 AOP 구현

3) 스프링 API를 이용한 AOP 구현 

 

- 위의 3가지 방식 중 어떤 것을 사용하더라도 내부적으로는 프록시를 이용하여 AOP가 구현되기 때문에 메서드 호출에 대해서만 AOP를 적용할 수 있습니다. 

 

* 프록시를 통한 AOP  

 

 

- 어떤 대상 객체에 대해 AOP를 적용할 지의 여부는 설정파일을 통해서 지정할 수 있으며 스프링은 설정 정보를 이용하여 런타임 시에 대상 객체에 대한 프록시 객체를 생성합니다.

 

- 대상 객체가 인터페이스를 구현하고 있다면 스프링은 자바 리플렉션 API가 제공하는 java.lang.reflect.Proxy를 이용하여 프록시 객체를 생성합니다.

 

- 인터페이스를 기반으로 프록시 객체를 생성하기 때문에 인터페이스에 정의되어 있지 않은 메서드 에 대해서는 AOP가 적용되지 않습니다.

 

 

- 대상 객체가 인터페이스를 구현하고 있지 않다면 스프링은 CGLIB 라이브러리를 이용하여 클래스에 대한 프록시 객체를 생성합니다. CGLIB 라이브러리는 대상 클래스를 상속받아 런타임시 동적으로 자바 클래스의 프록시를 구현합니다. 따라서 대상 프록시가 final인 경우에는 프록시를 생성 할 수 없고 final 메서드인 경우에는 AOP를 적용할 수 없습니다. 

 

 

※ AOP 예시 

 

- 간단한 로그를 찍는 Log class 생성 

 

package tommy.spring.web.common;
public class LogAdvice {
public void printLog() {
System.out.println("[로그] : 비즈니스 로직 수행 전 동작");
}
}

 

- 게시글 서비스 인터페이스 생성 

 

package tommy.spring.web.board;

import java.util.List;

public interface BoardService {
	void insertBoard(BoardVO vo);
	void updateBoard(BoardVO vo);
	void deleteBoard(BoardVO vo);
	BoardVO getBoard(BoardVO vo);
	List<BoardVO> getBoardList(BoardVO vo);
}

 

- 게시글 서비스를 상속받을 Impl 생성 

 

package tommy.spring.web.board.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tommy.spring.web.board.BoardService;
import tommy.spring.web.board.BoardVO;
import tommy.spring.web.common.LogAdvice;
@Service("boardService")
public class BoardServiceImpl implements BoardService {
@Autowired
private BoardDAO boardDAO;
private LogAdvice log;
public BoardServiceImpl() {
log = new LogAdvice();
}
public void insertBoard(BoardVO vo) {
log.printLog();
boardDAO.insertBoard(vo);
}
public void updateBoard(BoardVO vo) {
log.printLog();
boardDAO.updateBoard(vo);
}
public void deleteBoard(BoardVO vo) {
log.printLog();
boardDAO.deleteBoard(vo);
}
public BoardVO getBoard(BoardVO vo) {
log.printLog();
return boardDAO.getBoard(vo);
}
public List<BoardVO> getBoardList(BoardVO vo) {
log.printLog();
return boardDAO.getBoardList(vo);
}
}

 

-> 각 서비스 로직을 수행 할 때마다 log를 찍어준다. 

 

 

- 구현 클래스인 Client 부분 작성 

 

package tommy.spring.web.board;

import java.util.List;

import org.springframework.context.support.AbstractApplicationContext;
import org.springframework.context.support.GenericXmlApplicationContext;

public class BoardServiceClient {

	public static void main(String[] args) {
		// 1. 스프링 컨테이너 구동 
		AbstractApplicationContext container =
				new GenericXmlApplicationContext("applicationContext.xml");
		// 2. 스프링 컨테이너로 부터 BoardServiceImpl 객체를 Lookup 한다. 
		BoardService boardService = (BoardService) container.getBean("boardService");
		
		// 3. 글 등록 기능 테스트 
		BoardVO vo = new BoardVO();
		vo.setTitle("임시제목");
		vo.setWriter("홍길동");
		vo.setContent("일등!");
		boardService.insertBoard(vo);
		
		// 4. 글 검색 기능 테스트 
		List<BoardVO> boardList = boardService.getBoardList(vo);
		for (BoardVO board : boardList) {
			System.out.println("---->" + board.toString());
		}
		
		// 5. 스프링 컨테이너 종료 
		container.close();
		
	}

}

 

 

- 결과 확인 

 

 

- 문제점

 

위와 같이 작성된 프로그램은 BoardServiceImpl 클래스와 LogAdvice 객체가 소스코드에 강력하게 결합되어 있어서 LogAdvice 클래스를 다른 클래스로 변경해야 하거나 공통기능에 해당하는 printLog() 메서드의 시그니처가 변경되는 상황에서 유연하게 대처할 수 없습니다. 

 

OOP처럼 모듈화가 뛰어난 언어를 사용하여 개발을 하더라도 공통모듈에 해당하는 Advice 클래스 객체를 생성하고 공통 메서드를 호출하는 코드가 비즈니스 메서드에 있다면 핵심관 심과 횡단관심을 완벽하게 분리할 수 없다. 스프링 AOP는 이런 OOP의 한계를 극복할 수 있습니다.

 

 

- AOP 적용 

 

- log 클래스를 빼고 원래대로 클래스를 돌려줍니다.

 

package tommy.spring.web.board.impl;
import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import tommy.spring.web.board.BoardService;
import tommy.spring.web.board.BoardVO;
@Service("boardService")
public class BoardServiceImpl implements BoardService {
@Autowired
private BoardDAO boardDAO;
public void insertBoard(BoardVO vo) {
boardDAO.insertBoard(vo);
}
public void updateBoard(BoardVO vo) {
boardDAO.updateBoard(vo);
}
public void deleteBoard(BoardVO vo) {
boardDAO.deleteBoard(vo);
}
public BoardVO getBoard(BoardVO vo) {
return boardDAO.getBoard(vo);
}
public List<BoardVO> getBoardList(BoardVO vo) {
return boardDAO.getBoardList(vo);
}
}

 

- src/main/resources 하위에 applicationContext.xml aop 설정 (namespace에서 aop 사용 체크)

 

<context:component-scan base-package="tommy.spring.web"></context:component-scan>
<bean id="log" class="tommy.spring.web.common.LogAdvice"></bean>
<aop:config>
<!-- aspectj expression 문법 사용 -->
<aop:pointcut expression="execution(* tommy.spring.web..*Impl.*(..))" id="allPointcut"/>
<aop:aspect ref="log">
<aop:before method="printLog" pointcut-ref="allPointcut"/>
</aop:aspect>
</aop:config>

 

- 만약 다른 로그를 적용하기 위해 클래스를 바꾼다면?

 

* 다른 로그 클래스 생성 

 

package tommy.spring.web.common;
public class Log4jAdvice {
public void printLogging() {
System.out.println("[로그 - Log4jAdvice] : 비즈니스 로직 수행 전 동작");
}
}

 

* applicationContext.xml 파일 수정 

 

<context:component-scan base-package="tommy.spring.web"></context:component-scan>
<!-- bean 객체만 변경 -->
<bean id="log" class="tommy.spring.web.common.Log4jAdvice"></bean>
<aop:config>
<aop:pointcut expression="execution(* tommy.spring.web..*Impl.*(..))" id="allPointcut"/>
<aop:aspect ref="log">
<!-- 해당 객체의 로그 메소드 이름 -->
<aop:before method="printLogging" pointcut-ref="allPointcut"/>
</aop:aspect>
</aop:config>

 

- 위에서 했던 것처럼 구현 클래스 실행 결과 확인

 

로그만 바뀐 모습

 

결과적으로 스프링 AOP는 클라이언트가 핵심관심에 해당하는 비지니스 메서드를 호출할 때 횡단관심에 대한 메서드를 적절하게 실행 해줍니다. 이때 핵심관심 메서드와 횡단관심 메서드 사이에서 소스코드 상의 결합은 발생하지 않으며 이것이 우리가 AOP를 사용하는 이유입니다. 

 

'Frame Work > Spring' 카테고리의 다른 글

Spring Transaction  (0) 2022.10.22
Spring JDBC  (0) 2022.10.21
Spring DI  (0) 2022.10.19
Spring FrameWork 특징  (2) 2022.10.15
Spring FrameWork 목적 및 POJO 방식  (2) 2022.10.14