스프링 AOP
비즈니스 컴포넌트 개발에서 가장 중요한 두 가지 원칙은 낮은 결합도와 높은 응집도를 유지하는 것이다. 스프링의 의존성 부입을 이용하면 비즈니스 컴포넌트를 구성하는 객체등의 결합도를 떨어뜨릴 수 있어서 의존관계를 쉽게 변경할 수 있다. 스프링IoC가 결합도와 관련된 기능이라면, AOP(Aspect Oriented Programming)는 응집도와 관련된 기능이라 할 수 있다.
AOP를 이해하는데 가장 중요한 핵심 개념은 바로 관심 분리다. AOP에서는 메소드마다 공통으로 등장하는 로깅이나 예외, 트랜잭션 처리 같은 코드들을 횡단 관심(Crosscuting Concerns)이라고 한다. 이에 반해 사용자의 요청에 따라 실제로 수행되는 핵심 비즈니스 로직을 핵심 관심(Core Concerns)이라고 한다.
지금부터 기존의 OOP언어에서 완벽한 관심분리가 왜 어려운지를 실습으로 확인해보려고 한다. 그리고나서 AOP가 관심 분리 문제를 해결하는 과정을 살펴보면 AOP의 개념을 쉽게 이해할 수 있을 것이다.
우선 BoardService 컴포넌트의 모든 비즈니스 메소드가 실행되기 직전에 공통으로 처리할 로직을 LogAdvice 클래스에 printLog()메소드로 구현한다.
LogAdvice.java
public class LogAdvice {
public void printLog() {
System.out.println("[공통로그] 비즈니스 로직 수행 전 동작");
}
}
이렇게 구현된 LogAdvice클래스의 printLog()메소드를 BoardService컴포넌트에서 사용할 수 있도록 BoardServiceImpl클래스를 수정한다.
BoardServiceImpl.java
@Service("boardService")
public class BoardServiceImpl implements BoardService{
@Autowired
private BoardDAO boardDAO;
private LogAdvice logAdvice;
public BoardServiceImpl() {
logAdvice = new LogAdvice();
}
public void insertBoard(BoardVO vo) {
logAdvice.printLog();
boardDAO.insertBoard(vo);
}
public void updateBoard(BoardVO vo) {
logAdvice.printLog();
boardDAO.updateBoard(vo);
}
public void deleteBoard(BoardVO vo) {
logAdvice.printLog();
boardDAO.deleteBoard(vo);
}
public BoardVO getBoard(BoardVO vo) {
logAdvice.printLog();
return boardDAO.getBoard(vo);
}
public List<BoardVO> getBoardList(BoardVO vo){
logAdvice.printLog();
return boardDAO.getBoardList(vo);
}
}
BoardServiceImpl 객체가 생성될 때, 생성자에서 LogAdvice 객체도 같이 생성하고 각 메소드가 실행될 때 printLog도 호출되도록 한다. 이후 공통기능을 수정할 때는 printLog메소드만 수정하면 되기 때문에 관리가 편해졌다.
그러나 BoardServiceImpl 클래스와 LogAdvice 객체가 소스코드에서 강력하게 결합되어 있어서, LogAdvice 클래스를 다른 클래스로 변경하거나 공통기능에 해당하는 printLog() 메소드의 시그니처가 변경되는 상황에서는 유연하게 대처할 수 없다.
LogAdvice를 대체할 Log4jAdvice클래스를 만든다.
Log4jAdvice.java
public class Log4jAdvice {
public void printLogging() {
System.out.println("[공통 로그-Log4j] 비즈니스 로직 수행 전 동작");
}
}
BoardServiceImpl클래스의 모든 메소드는 Log4jAdvice를 이용하도록 수정해야 한다.
@Service("boardService")
public class BoardServiceImpl implements BoardService{
@Autowired
private BoardDAO boardDAO;
private Log4jAdvice log;
public BoardServiceImpl() {
log = new Log4jAdvice();
}
public void insertBoard(BoardVO vo) {
log.printLogging();
boardDAO.insertBoard(vo);
}
public void updateBoard(BoardVO vo) {
log.printLogging();
boardDAO.updateBoard(vo);
}
public void deleteBoard(BoardVO vo) {
log.printLogging();
boardDAO.deleteBoard(vo);
}
public BoardVO getBoard(BoardVO vo) {
log.printLogging();
return boardDAO.getBoard(vo);
}
public List<BoardVO> getBoardList(BoardVO vo){
log.printLogging();
return boardDAO.getBoardList(vo);
}
}
LogAdvice에서 Log4jAdvice로 바뀌는 순간 BoardServiceImpl 클래스의 생성자를 수정해야 한다. 또한 메소드도 printLogging으로 변경해야 한다.
다음과 같이 결과가 정상적으로 출력된다.
정리하면, OOP처럼 모듈화가 뛰어난 언어를 사용하여 개발하더라도 공통 모듈에 해당하는 Advice 클래스 객체를 생성하고 공통 메소드를 호출하는 코드가 비즈니스 메소드에 있다면, 핵심 관심과 횡단 관심을 완벽하게 분리할 수는 없다. 하지만 스프링의 AOP는 이런 OOP의 한계를 극복할 수 있도록 도와준다.
AOP 시작하기
이번에는 스프링의 AOP를 이용해서 핵심 관심과 횡단 관심을 분리해보자. 이 실습이 마무리되면 BoardServiceImpl 소스와는 무관하게 LogAdvice가 Log4jAdvice클래스의 메소드를 실행할 수 있게 된다.
먼저 앞에서 작성했던 BoardServiceImpl 클래스는 원래의 상태로 되돌린다.
@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);
}
}
AOP 라이브러리 추가
본격적으로 AOP를 적용하기 위해서 우선 BoardWeb 프로젝트에 있는 pom.xml파일을 수정하여 AOP관련 라이브러리를 추가한다.
<!-- 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.8.8</version>
</dependency>
aspercjweaver를 내려받을 수 있도록 pom.xml파일에 <dependency>설정을 추가하고, 'Maven Dependencies'라이브러리가 추가되었는지 확인한다.
네임스페이스 추가 및 AOP설정
AOP설정을 추가하려면 AOP에서 제공하는 엘리먼트들을 사용해야 한다. 따라서 applicationContext.xml파일에 aop네임스페이스를 추가한다. 그후 LogAdvice클래스를 <bean> 등록한다. 그리고 aop를 다음과 같이 설정한다.
<?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:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.2.xsd">
<context:component-scan base-package="com.springbook.biz">
</context:component-scan>
<bean id="log" class="com.springbook.biz.common.LogAdvice"></bean>
<aop:config>
<aop:pointcut id="allPointcut" expression="execution(* com.springbook.biz..*Impl.*(..))"/>
<aop:aspect ref="log">
<aop:before pointcut-ref="allPointcut" method="printLog"/>
</aop:aspect>
</aop:config>
</beans>
BoardServiceClient 프로그램을 실행하여 insertBoard()와 getBoardList() 메소드가 호출될 때 LogAdvice 클래스의 printLog() 메소드가 실행되는지 확인하면 된다.
다음과 같이 정상적으로 출력된다.
만약 LogAdvice를 Log4jAdvice로 교체하고 싶으면 스프링 설정 파일의 AOP설정만 다음과 같이 수정하면 된다.
<bean id="log" class="com.springbook.biz.common.Log4jAdvice"></bean>
<aop:config>
<aop:pointcut id="allPointcut" expression="execution(* com.springbook.biz..*Impl.*(..))"/>
<aop:aspect ref="log">
<aop:before pointcut-ref="allPointcut" method="printLogging"/>
</aop:aspect>
</aop:config>
실행결과를 보면 Log4jAdvice 클래스의 printLogging() 메소드가 실행되고 있음을 알 수 있다.
스프링의 AOP는 클라이언트가 핵심 관심에 해당하는 비즈니스 메소드를 호출할 때, 횡단 관심에 해당하는 메소드를 적절하게 실행하게 해준다. 이때, 핵심 관심 메소드와 횡단 관심 메소드 사이에서 소스상의 결합은 발생하지 않으며, 이것이 AOP를 사용하는 주된 목적이다.
AOP 용어 및 기본 설정
AOP 용어 정리
조인포인트(joinpoint)
: 조인포인트란 클라이언트가 호출하는 모든 비즈니스 메소드로서, BoardServiceImpl이나 UserServiceImpl 클래스의 모든 메소드를 조인포인트라고 생각하면 된다. 조인포인트를 다음에 설명할 '포인트컷 대상' 또는 '포인트컷 후보'라고도 하는데, 이는 조인포인트중에 포인트컷이 선택될 수 있기 때문이다.
포인트컷(pointcut)
: 클라이언트가 호출하는 모든 비즈니스 메소드가 조인포인트라면, 포인트컷은 필터링된 조인포인트를 의미한다.예를 들어, 트랜잭션을 처리하는 공통 기능을 만들었다고 가정하자. 이 횡단 관심 기능은 등록, 수정, 삭제 기능의 비즈니스 메소드에 대해서는 당연히 동작해야 하지만, 검색기능의 메소드에 대해서는 무관하므로 동작할 필요가 없다.
포인트컷을 테스트하기 위해 스프링 설정 파일에 기존에 사용하던 포인트컷을 복사히여 하나 더 추가해보자.
<aop:config>
<aop:pointcut id="allPointcut" expression="execution(* com.springbook.biz..*Impl.*(..))"/>
<aop:pointcut id="getPointcut" expression="execution(* com.springbook.biz..*Impl.get*(..))"/>
<aop:aspect ref="log">
<aop:before pointcut-ref="getPointcut" method="printLogging"/>
</aop:aspect>
</aop:config>
포인트컷은 <aop:pointcut> 엘리먼트로 선언하며, id 속성으로 포인트컷을 식별하기 위한 유일한 문자열을 선언한다.
expression 속성은 어떻게 설정되느냐에 따라 필터링되는 메소드가 달라진다. 첫번째 등록한 allPointcut은 리턴타입과 매개변수를 무시하고 com.springbook.biz패키지로 시작하는 클래스 중 Impl로 끝나는 클래스의 모든 메소드를 포인트컷으로 설정했다. getPointcut은 get으로 시작하는 메소드만 포인트컷으로 설정했다.
마지막으로 <aop:before> 엘리먼트에서 allPointcut을 참조했던 부분을 getPointcut으로 수정했다.
결과를 확인해보면 get으로 시작하는 메소드에서만 로그가 찍힌 것을 확인할 수 있다.
어드바이스(Advice)
어드바이스는 횡단 관심에 해당하는 공통 기능의 코드를 의미하며, 독립된 클래스의 메소드로 작성된다. 그리고 어드바이스로 구현된 메소드가 언제 동작할지 스프링 설정 파일을 통해서 지정할 수 있다.
스프링에서는 어드바이스의 동작 시점을 'before', 'after', 'after-returning', 'after-throwing', 'around' 등 다섯가지로 지정할 수 있다.
이전에 작성한 LogAdvice의 printLog() 메소드가 비즈니스 메소드 실행 후에 동작하도록 스프링 설정을 변경해보자.
<bean id="log" class="com.springbook.biz.common.LogAdvice"></bean>
<aop:config>
<aop:pointcut id="allPointcut" expression="execution(* com.springbook.biz..*Impl.*(..))"/>
<aop:pointcut id="getPointcut" expression="execution(* com.springbook.biz..*Impl.get*(..))"/>
<aop:aspect ref="log">
<aop:after pointcut-ref="getPointcut" method="printLog"/>
</aop:aspect>
</aop:config>
기존에 작성했던 <aop:before> 엘리먼트를 <aop:after>로 변경했다. BoardServiceClient 프로그램을 다시 실행하고 실행 결과를 확인해보면, getBoardList() 메소드가 실행된 이후에 printLog() 메소드가 실행된 것을 확인할 수 있다.
위빙(Weaving)
위빙은 포인트컷으로 지정한 핵심 관심 메소드가 호출될 때, 어드바이스에 해당하는 횡단 관심 메소드가 삽입되는 과정을 의미한다. 이 위빙을 통해서 비즈니스 메소드를 수정하지 않고도 횡단 관심에 해당하는 기능을 추가하거나 변경할 수 있다. 위빙을 처리하는 방식은 크게 컴파일타임 위빙, 로딩타임 위빙, 런타임 위빙이 있지만, 스프링에서는 런타임 위빙 방식만 지원한다.
애스팩트(Aspect) 또는 어드바이저(Advisor)
애스팩트는 포인트컷과 어드바이스의 결합으로서. 어떤 포인트컷 메소드에 대해서 어떤 어드바이스 메소드를 실행할지 결정한다. 이 애스팩트 설정에 따라 AOP의 동작방식이 결정되므로 AOP용어중 가장 중요한 개념이라 할 수 있다.
AOP 엘리먼트
<aop:config> 엘리먼트
AOP 설정에서 <aop:config>는 루트 엘리먼트이다. 스프링 설정 파일 내에 <aop:config> 엘리먼트는 여러 번 사용할 수 있으며, <aop:config> 엘리먼트 하위헤는 <aop:pointcut>, <aop:aspect> 엘리먼트가 위치할 수 있다.
<aop:pointcut> 엘리먼트
<aop:pointcut> 엘리먼트는 포인트컷을 지정하기 위해 사용하며, <aop:config>의 자식이나 <aop:aspect>의 자식 엘리먼트로 사용할 수 있다. 그러나 <aop:aspect> 하위에 설정된 포인트컷은 해당 <aop:aspect>에서만 사용할 수 있다.
<aop:aspect> 엘리먼트
애스펙트는 <aop:aspect> 엘리먼트 설정하며, 핵심 관심에 해당하는 포인트컷 메소드와 횡단 관심에 해당하는 어드바이스 메소드를 결합하기 위해 사용한다. 애스팩트를 어떻게 설정하느냐에 따라서 위빙 결과가 달라지므로 AOP에서 가장 중요한 설정이라 할 수 있다.
<aop:advisor> 엘리먼트
용어 정리에서 확인했듯이 <aop:advisor> 엘리먼트는 포인트컷과 어드바이스를 결합한다는 점에서 애스팩트와 같은 기능을 한다. 하지만 트랜잭션 설정 같은 몇몇 특수한 경우는 애스팩트가 아닌 어드바이저를 사용해야 한다.
포인트컷 표현식
포인트컷을 이용하면 어드바이스 메소드가 적용될 비즈니스 메소드를 정확하게 필터링할 수 있는데, 이때 다양한 포인트컷 표현식을 사용할 수 있다. 포인트컷 표현식은 메소드처럼 생신 execution 명시자를 이용하며, execution 명시자 안에 포인트컷 표현식을 기술한다.
1. 리턴타입 지정
리턴타임 지정에서 가장 기본적인 방법은 '*' 캐릭터를 이용하는 것이다.
- *: 모든 리턴타입을 허용
- void: 리턴타입이 void인 메소드를 선택
- !void: 리턴타입이 void가 아닌 메소드를 선택
2. 패키지 지정
패키지 경로를 지정할 때는 '*','..'캐릭터를 이용한다.
- com.spring.biz: 정확하게 com.spring.biz 패키지만 선택
- com.spring.biz..: com.spring.biz 패키지로 시작하는 모든 패키지 선택
- com.spring.biz..impl: com.spring.biz 패키지로 시작하면서 마지막 패키지 이름이 impl로 끝나는 패키지 선택
3. 클래스 지정
클래스 이름을 지정할 때는 '*','+'캐릭터를 이용한다.
- BoardServiceImpl: 정확히 BoardServiceImpl클래스만 선택
- *Impl: 클래스 이름이 Impl로 끝나는 클래스만 선택
- BoardService+: 클래스 이름에 '+'가 붙으면 해당 클래스로부터 파생된 모든 자식 클래스를 선택. 인터페이스 뒤에 '+'가 붙으면 해당 인터페이스를 구현한 모든 클래스를 선택
4. 메소드 지정
메소드를 지정할 때는 주로 '*'캐릭터를 사용하고 매개변수를 지정할 때는 '..'를 사용한다.
- *(..): 가장 기본 설정으로 모든 메소드 선택
- get*(..): 메소드 이름이 get으로 시작하는 모든 메소드 선택
5. 매개변수 지정
매개변수를 지정할 때는 '..', '*', 캐릭터를 사용하거나 정확한 타입을 지정한다.
- (..): 가장 기본 설정으로서 '..'은 매개변수의 개수와 타입에 제약이 없음을 의미
- (*): 반드시 1개의 매개변수를 가지는 메소드만 선택
- (com.springbook.user.UserVO): 매개변수로 UserVO를 가지는 메소드만 선택. 이때 클래스의 패키지 경로가 반드시 포함돼야 한다.
- (!com.springbook.user.UserVO): 매개변수 UserVO를 갖지 않는 메소드만 선택
- (Integer, ..): 한 개 이상의 매개변수를 가지되, 첫 번째 매개변수의 타입이 Integer인 메소드만 선택
- (Integer, *): 반드시 두개의 매개변수를 가지되, 첫 번째 매개변수의 타입이 Integer인 메소드만 선택
어드바이스 동작 시점
어드바이스는 각 조인포인트에 삽입되어 동작할 횡단 관심에 해당하는 공통 기능이며, 동작 시점은 각 AOP 기술마다 다르다. 스프링에서는 다섯 가지의 동작 시점을 제공한다.
동작 시점 | 설명 |
Before | 비즈니스 메소드 실행 전 동작 |
after returning | 비즈니스 메소드가 성공적으로 리턴되면 동작 |
after throwing | 비즈니스 메소드 실행 중 예외가 발생하면 동작(catch) |
after | 비즈니스 메소드가 실행 된 후 무조건 실행(finally) |
around | 메소드 호출 자체를 가채 비즈니스 메서드 실헹 잔후에 처리할 로직을 삽입할 수 있음 |
Befor 어드바이스
Before 어드바이스를 테스트하기 위해 비즈니스 메소드가 실행되기 전에 수행할 기능을 어드바이스 클래스로 구현한다.
public class BeforeAdvice {
public void beforeLog() {
System.out.println("[사전처리] 비즈니스 로직 수행 전 동작");
}
}
BeforeAdvice 클래스에서 선언한 brforeLog()메소드가 Before형태로 동작하도록 스프링 설정 파일을 수정한다.
<bean id="log" class="com.springbook.biz.common.LogAdvice"></bean>
<bean id="before" class="com.springbook.biz.common.BeforeAdvice"></bean>
<aop:config>
<aop:pointcut id="allPointcut" expression="execution(* com.springbook.biz..*Impl.*(..))"/>
<aop:pointcut id="getPointcut" expression="execution(* com.springbook.biz..*Impl.get*(..))"/>
<aop:aspect ref="before">
<aop:before pointcut-ref="allPointcut" method="beforeLog"/>
</aop:aspect>
</aop:config>
BeforeAdvice를 갖는 bean을 만들고 <aop:before>엘리먼트르 사용한다. 위 설정은 allPointcut으로 지정한 모든 Impl 클래스의 메소드가 실행되기 직전에 before로 지정한 어드바이스의 beforeLog() 메소드가 실행되도록 설정한 것이다.
메소드가 실행되기 전에 정상적으로 로그가 호출됨을 알 수 있다.
After Returning 어드바이스
After Returning 어드바이스는 포인트컷으로 지정된 메소드가 정상적으로 실행되고 나서, 메소드 수행 결과로 생성된 데이터를 리턴하는 시점에 동작한다.
public class AfterReturningAdvice {
public void afterLog() {
System.out.println("[사후 처리] 비즈니스 로직 수행 후 동작");
}
}
after-returning 어드바이스를 지정하려면 <aop:after-returning> 엘리먼트를 사용한다.
<bean id="afterReturning" class="com.springbook.biz.common.AfterReturningAdvice"></bean>
<aop:config>
<aop:pointcut id="allPointcut" expression="execution(* com.springbook.biz..*Impl.*(..))"/>
<aop:pointcut id="getPointcut" expression="execution(* com.springbook.biz..*Impl.get*(..))"/>
<aop:aspect ref="afterReturning">
<aop:after-returning pointcut-ref="allPointcut" method="afterLog"/>
</aop:aspect>
</aop:config>
메소드가 실행된 후 정상적으로 로그가 호출됨을 알 수 있다.
After Throwing
After Throwing 어드바이스는 포인트컷으로 지정한 메소드가 실행되다가 예외가 발생하는 시점에 동작한다.
public class AfterThrowingAdvice {
public void exceptionLog() {
System.out.println("[예외처리] 비즈니스 로직 수행 중 예외발생");
}
}
After Throwing 어드바이스는 <aop:after-throwing> 엘리먼트를 이용하여 설전한다. allPointcut이라는 포인트컷으로 지정한 메소드에서 예외가 발생할 경우 afterThrowing어드바이스의 execeptionLog() 메소드를 실행시키기 위한 설정이다.
<bean id="afterThrowing" class="com.springbook.biz.common.AfterThrowingAdvice"></bean>
<aop:config>
<aop:pointcut id="allPointcut" expression="execution(* com.springbook.biz..*Impl.*(..))"/>
<aop:pointcut id="getPointcut" expression="execution(* com.springbook.biz..*Impl.get*(..))"/>
<aop:aspect ref="afterThrowing">
<aop:after-throwing pointcut-ref="allPointcut" method="exceptionLog"/>
</aop:aspect>
</aop:config>
insertBoard를 다음과 같이 변경시켜 의도적으로 예외가 발생하게 한다.
public void insertBoard(BoardVO vo) {
if(vo.getSeq()==0) {
throw new IllegalArgumentException("0번 글은 등록할 수 없습니다." );
}
boardDAO.insertBoard(vo);
}
예외가 발생할 때 정상적으로 로그가 호출됐음을 알 수 있다.
After 어드바이스
try-catch-finally 구문에서 finally 블록처럼 예외 발생 여부에 상관없이 무조건 수행되는 어드바이스를 등록할 때 After어드바이스를 사용한다.
public class AfterAdvice {
public void finallyLog() {
System.out.println("[사후 처리] 비즈니스 로직 수행 후 무조건 동작");
}
}
스프링 설정파일에서 예외 처리 설정 밑에 After 애스팩트를 추가한다.
<bean id="afterThrowing" class="com.springbook.biz.common.AfterThrowingAdvice"></bean>
<bean id="after" class="com.springbook.biz.common.AfterAdvice"></bean>
<aop:config>
<aop:pointcut id="allPointcut" expression="execution(* com.springbook.biz..*Impl.*(..))"/>
<aop:pointcut id="getPointcut" expression="execution(* com.springbook.biz..*Impl.get*(..))"/>
<aop:aspect ref="afterThrowing">
<aop:after-throwing pointcut-ref="allPointcut" method="exceptionLog"/>
</aop:aspect>
<aop:aspect ref="after">
<aop:after pointcut-ref="allPointcut" method="finallyLog"/>
</aop:aspect>
</aop:config>
예외처리가 발생했음에도 finallyLog()가 먼저 작동하고 exceptionLog()메소드가 실행되는 것을 확인할 수 있다.
Around 어드바이스
Around 어드바이스는 클라이언트의 메소드 호출을 가로챈다. 그래서 클라이언트가 호출한 비즈니스 메소드가 실행되기 전에 사전 처리 로직을 수행할 수 있으며, 비즈니스 메소드가 모두 실행되고 나서 사후 처리 로직을 수행할 수 있다.
public class AroundAdvice {
public Object aroundLog(ProceedingJoinPoint pjp) throws Throwable{
System.out.println("[BEFORE]: 비즈니스 메소드 수행 전에 처리할 내용...");
Object obj = pjp.proceed();
System.out.println("[AFTER]: 비즈니스 메소드 수행 후에 처리할 내용...");
return obj;
}
}
pjp.proceed() 메소드 호출 앞에 작성된 코드는 Before 어드바이스와 동일하게 동작하며
pjp.proceed()메소드 호출 뒤에 작성된 코드는 After어드바이스와 동일하게 동작한다.
<bean id="around" class="com.springbook.biz.common.AroundAdvice"></bean>
<aop:config>
<aop:pointcut id="allPointcut" expression="execution(* com.springbook.biz..*Impl.*(..))"/>
<aop:pointcut id="getPointcut" expression="execution(* com.springbook.biz..*Impl.get*(..))"/>
<aop:aspect ref="around">
<aop:around pointcut-ref="allPointcut" method="aroundLog"/>
</aop:aspect>
</aop:config>
JointPoinr와 바인드 변수
횡단 관심에 해당되는 어드바이스 메소드를 의미 있게 구현하려면 클라이언트가 호출한 비즈니스 메소드의 정보가 필요하다. 예를 들어, After Throwing 기능의 어드바이스 메소드를 구현한다고 가정하자. 이때, 예외가 발생한 비즈니스 메소드 이름이 무엇인지, 그 메소드가 속한 클래스와 패키지 정보는 무엇인지 알아야 정확한 예외 처리 로직을 구현할 수 있다. 스프링에서는 이런 다양한 정보들을 이용할 수 있도록 JoinPoint 인터페이스를 제공한다.
JointPoint 메소드
다음은 JoinPoint에서 제공하는 유용한 메소드들이다.
메소드 | 설명 |
Signature getSignature() | 클라이언트가 호출한 메소드의 시그니처(리턴타입, 이름, 매개변수) 정보가 저장된 Signature 객체 리턴 |
Object getTarget() | 클라이언트가 호출한 비즈니스 메소드를 포함하는 비즈니스 객체 리턴 |
Object[] getArgs() | 클라이언트가 메소드를 호출할 때 넘겨준 인자 목록을 Object배열로 리턴 |
getSiganture() 메소드가 리턴하는 Signature객체를 이용하면 호출되는 메소드에 대한 다양한 정보를 얻을 수 있다.
메소드 | 설명 |
String getName() | 메소드의 이름 리턴 |
String toLongString | 메소드의 리턴타입, 이름, 매개변수를 패키지 경로까지 포함하여 리턴 |
String toShortString | 메소드 시그니처를 축약한 문자열로 리턴 |
JoinPoint 객체르 사용하려면 단지 JoinPoint를 어드바이스 메소드 매개변수로 선언만 하면 된다. 그러면 클라이언트가 비즈니스 메소드를 호출할 때, 스프링 컨테이너가 JoinPoint 객체를 생성한다. 그리고 메소드 호출괴 관련된 모든 정보를 JoinPoint 객체를 저장하여 어드바이스 메소드를 호출할 때 인자로 넘겨준다.
LogAdvice.java
public class LogAdvice {
public void printLog(JoinPoint jp) {
System.out.println("[공통로그] 비즈니스 로직 수행 전 동작");
}
}
Before 어드바이스
클라이언트가 비즈니스 메소드를 호출할 때, 인자로 넘겨준 값들을 JoinPoint를 이용하여 출력하도록 BeforeAdvice 클래스를 수정한다.
public class BeforeAdvice {
public void beforeLog(JoinPoint jp) {
String method = jp.getSignature().getName();
Object[] args = jp.getArgs();
System.out.println("[사전처리] "+method+"() 메소드 ARGS정보 : "+ args[0].toString());
}
}
JoinPoint 객체의 getSignature() 메소드를 이용하면, 클라이언트가 호출한 메소드 이름을 출력할 수 있다. 그리고 getArgs() 메소드를 통해 인자목록을 Object 배열로 얻어낼 수 있어서, 메소드 호출에 어떤 값들을 사용했는지도 알 수 있다.
<aop:config>
<aop:pointcut id="allPointcut" expression="execution(* com.springbook.biz..*Impl.*(..))"/>
<aop:pointcut id="getPointcut" expression="execution(* com.springbook.biz..*Impl.get*(..))"/>
<aop:aspect ref="before">
<aop:around pointcut-ref="getPointcut" method="beforeLog"/>
</aop:aspect>
</aop:config>
포인트 컷을 getPointcut을 사용함으로써 get으로 시작하는 메소드를 호출 하기전에 로그를 먼저 호출하도록 한다.
다음과 같이 호출된 메소드에 대한 내용들이 출력됐음을 알 수 있다.
After Returning 어드바이스
기존에 작성했던 After ReturningAdvice를 비즈니스 메소드가 리턴한 값을 이용하여 동작하도록 수정한다.
public class AfterReturningAdvice {
public void afterLog(JoinPoint jp, Object returnObj) {
String method = jp.getSignature().getName();
if(returnObj instanceof UserVO) {
UserVO user = (UserVO) returnObj;
if(user.getRole().equals("Admin")) {
System.out.println(user.getName()+" 로그인(Admin)");
}
}
System.out.println("[사후 처리] "+method+"() 메소드 리턴값: "+returnObj.toString());
}
}
afterLog() 메소드는 클라이언트가 호출한 비즈니스 메소드 정보를 알아내기 위해서 JoinPoint 객체를 첫 번째 매개변수로 선언한다. 그리고 Object 타입의 변수도 두 번째 매개변수로 선언되어 있는데, 이를 '바인드 변수'라고 한다. 바인드 변수를 비즈니스 메소드가 리턴한 결과값을 바인딩할 목적으로 사용되며, 어떤 값이 리턴될지 모르기 때문에 Object 타입으로 선언한다.
<aop:config>
<aop:pointcut id="allPointcut" expression="execution(* com.springbook.biz..*Impl.*(..))"/>
<aop:pointcut id="getPointcut" expression="execution(* com.springbook.biz..*Impl.get*(..))"/>
<aop:aspect ref="afterReturning">
<aop:after-returning pointcut-ref="getPointcut" method="afterLog" returning="returnObj"/>
</aop:aspect>
</aop:config>
정상적으로 리턴된 메소드의 정보가 출력됐음을 알 수 있다. 관리자로 로그인 됐가 때문에 추가적인 정보가 출력됐다.
After Throwing 어드바이스
기존에 작성했던 AfterThrowing를 수정하여 예외가 발생한 메소드 이름과 발생한 예외 객체의 메시지를 출력하도록 한다.
exceptionLog()
public class AfterThrowingAdvice {
public void exceptionLog(JoinPoint jp, Exception exceptionObj) {
String method = jp.getSignature().getName();
System.out.println("[예외처리] "+method+" () 메소드 수행 중 발생된 예외 메시지: "+exceptionObj.getMessage());
}
}
<aop:aspect ref="afterThrowing">
<aop:after-throwing pointcut-ref="allPointcut" method="exceptionLog" throwing="exceptionObj"/>
</aop:aspect>
다음과 같이 오류 메시지를 띄울수 있다.
Around 어드바이스
Around 어드바이스는 다른 어드바이스와는 다르게 반드시 ProceedingJoinPoint 객체를 매개변수로 받아야 한다. PreceedingJoinPoint 객체는 비즈니스 메소드를 호출하는 proceed() 메소드를 가지고 있으며 JoinPoint를 상속했다.
public class AroundAdvice {
public Object aroundLog(ProceedingJoinPoint pjp) throws Throwable{
String method = pjp.getSignature().getName();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Object obj = pjp.proceed();
stopWatch.stop();
System.out.println(method+"() 메소드 수행에 걸린 시간 : "+stopWatch.getTotalTimeMillis()+" (ms)초");
return obj;
}
}
<aop:aspect ref="around">
<aop:around pointcut-ref="allPointcut" method="aroundLog" />
</aop:aspect>
다음과 같이 메소드의 수행시간이 각 메소드가 호출될 때마다 출력된다.
어노테이션 기반 AOP
스프링 IoC를 학습하면서 XML 기반 설정과 어노테이션 기반 설정을 모두 사용했었다. 그리고 XML과 어노테이션 설정을 적절히 혼합하여 사용하면 XML 설정을 최소화하면서 객체들을 효율적으로 관리할 수 있었다. 스프링 AOP도 IoC와 마찬가지로 어노테이션 설정을 지원한다.
어노테이션 사용을 위한 스프링 설정
다음과 같이 스프링 설정파일에 추가해준다.
<?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:p="http://www.springframework.org/schema/p"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:aop="http://www.springframework.org/schema/aop"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context-4.2.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop-4.2.xsd
http://www.springframework.org/schema/tx
http://www.springframework.org/schema/tx/spring-tx-4.2.xsd">
<context:component-scan base-package="com.springbook.biz">
</context:component-scan>
<aop:aspectj-autoproxy/>
</beans>
어드바이스 클래스에 선언된 어노테이션들을 스프링 컨테이너가 처리하게 하려면, 반드시 어드바이스 객체가 생성되어 있어야 한다. 따라서 어드바이스 클래스는 반드시 스프링 설정 파일에 <bean> 등록하거나 @Service 어노테이션을 사용하여 컴포넌트가 검색될 수 있도록 해야 한다.
Annotation 설정 | @Service public class LogAdvice{} |
XML 설정 | <bean id="log" class="com.springbook.biz.common.LogAdvice"></bean> |
앞으로 진행되는 실습은 <bean>등록 대신 @Service 어노테이션을 사용할 것이다.
포인트컷 설정
기존에는 다음과 같이 포인트컷을 설정했다.
<aop:pointcut id="allPointcut" expression="execution(* com.springbook.biz..*Impl.*(..))"/>
어노테이션 설정으로 포인트컷을 선언할 때는 @Pointcut을 사용하며, 하나의 어드바이스 클래스 안에 여러 개의 포인트컷을 선언할 수 있다. 따라서 여러 포인트컷을 식별하기 위한 식별자가 필요한데, 이때 참조 메소드를 이용한다.
참조 메소드는 메소드 몸체가 비어있는, 즉 구현 로직이 없는 메소드이다. 따라서 어떤 기능 처리를 목적으로 하지 않고 단순히 포인트컷을 식별하는 이름으로만 사용된다.
public class LogAdvice {
@Pointcut("execution(* com.springbook.biz..*Impl.*(..))")
public void allPointcut() {}
@Pointcut("execution(* com.springbook.biz..*Impl.*get(..))")
public void getPointcut() {}
}
이후에 이 포인트컷을 참조할 때, @Pointcut이 붙은 참조 메소드 이름을 이용하여 특정 포인트컷을 지정할 수 있다.
어드바이스 설정
어드바이스의 동작 시점은 XML 설정과 마찬가지로 다섯 가지가 제공된다.
이때 반드시 어드바이스 메소드가 결합될 포인트컷을 참조해야 한다.
@Service
public class LogAdvice {
@Pointcut("execution(* com.springbook.biz..*Impl.*(..))")
public void allPointcut() {}
@Pointcut("execution(* com.springbook.biz..*Impl.*get(..))")
public void getPointcut() {}
@Before("allPointcut()")
public void printLog(JoinPoint jp) {
System.out.println("[공통로그] 비즈니스 로직 수행 전 동작");
}
}
위 설정은 allPointcut() 참조 메소드로 지정한 비즈니스 메소드가 호출될 때, 어드바이스 메소드인 printLog() 메소드가 Before 형태로 동작하도록 설정한 것이다.
애스팩트 설정
애스팩트는 포인트컷과 어드바이스의 결합이다. 따라서 @Aspect가 설정된 애스팩트 객체에는 반드시 포인트컷과 어드바이스를 결합하는 설정이 있어야 한다.
Before 어드바이스
@Service
@Aspect // Aspect = Pointcut + Advice
public class LogAdvice {
@Pointcut("execution(* com.springbook.biz..*Impl.*(..))")
public void allPointcut() {}
@Pointcut("execution(* com.springbook.biz..*Impl.*get(..))")
public void getPointcut() {}
@Before("allPointcut()")
public void printLog(JoinPoint jp) {
System.out.println("[공통로그] 비즈니스 로직 수행 전 동작");
}
}
다음과 같이 메소드가 호출되기 전에 정상적으로 출력된다.
After Returning 어드바이스
@Service
@Aspect
public class AfterReturningAdvice {
@Pointcut("execution(* com.springbook.biz..*Impl.*get(..))")
public void getPointcut() {}
@AfterReturning(pointcut="getPointcut()",returning="returnObj")
public void afterLog(JoinPoint jp, Object returnObj) {
String method = jp.getSignature().getName();
if(returnObj instanceof UserVO) {
UserVO user = (UserVO) returnObj;
if(user.getRole().equals("Admin")) {
System.out.println(user.getName()+" 로그인(Admin)");
}
}
System.out.println("[사후 처리] "+method+"() 메소드 리턴값: "+returnObj.toString());
}
}
@AfterReturning은 앞에서 설정한 @Before와 다르게 pointcut 속성을 이용하여 포인트컷을 참조하고 있다. 이는 After returning 어드바이스 비즈니스 메소드 수행 결과를 받아내기 위해서 바인드 변수를 지정해야 하기 때문이다.
After Throwing 어드바이스
앞에서 작성한 AfterThrowingAdvice 클래스에 관련된 어노테이션을 추가한다.
@Service
@Aspect
public class AfterThrowingAdvice {
@Pointcut("execution(* com.springbook.biz..*Impl.*(..))")
public void allPointcut() {}
@AfterThrowing(pointcut="allPointcut()", throwing = "exceptionObj")
public void exceptionLog(JoinPoint jp, Exception exceptionObj) {
String method = jp.getSignature().getName();
System.out.println("[예외처리] "+method+" () 메소드 수행 중 예외발생! ");
if(exceptionObj instanceof IllegalArgumentException ) {
System.out.println("부적합한 값이 입력됐습니다.");
}else if(exceptionObj instanceof NumberFormatException) {
System.out.println("숫자형식이 아닙니다.");
}else if(exceptionObj instanceof Exception) {
System.out.println("문제가 발생했습니다.");
}
}
}
@AfterThrowing은 앞에서 설정한 @AfterReturning과 마찬가지로 pointcut 속성을 이용하혀 포인트컷을 참조하고 있다. 이는 @AfterThrowing 역시 비즈니스 메소드에서 발생된 예외 객체를 받아낼 바인드 변수를 지정해야 하기 때문이다.
다음과 같이 에러가 출력됨을 알 수 있다.
After 어드바이스
이전에 작성했던 AfterAdvice 클래스를 다음과 같이 수정한다.
@Service
@Aspect
public class AfterAdvice {
@Pointcut("execution(* com.springbook.biz..*Impl.*(..))")
public void allPointcut() {}
@After("allPointcut()")
public void finallyLog() {
System.out.println("[사후 처리] 비즈니스 로직 수행 후 무조건 동작");
}
}
다음과 같이 메소드가 호출된 이후에 로그가 호출됨을 알 수 있다.
Around 어드바이스
@Service
@Aspect
public class AroundAdvice {
@Pointcut("execution(* com.springbook.biz..*Impl.*(..))")
public void allPointcut() {}
@Around("allPointcut()")
public Object aroundLog(ProceedingJoinPoint pjp) throws Throwable{
String method = pjp.getSignature().getName();
StopWatch stopWatch = new StopWatch();
stopWatch.start();
Object obj = pjp.proceed();
stopWatch.stop();
System.out.println(method+"() 메소드 수행에 걸린 시간 : "+stopWatch.getTotalTimeMillis()+" (ms)초");
return obj;
}
}
aroundLog() 메소드를 Around 어드바이스로 동작시키기 위해서 메소드 위에 @Around 어노테이션을 추가했다. 그리고 aroundLog() 메소드도 바인드 변수가 없기 때문에 포인트컷 메소드만 참조하면 된다.
다음과 같이 실행시간이 찍히는 것을 알 수 있다.
외부 Pointcut 참조하기
XML 설정으로 포인트컷을 관리했을 때는 스프링 설정 파일에 포인트컷을 여러 개 등록했다. 그리고 애스팩트를 설정할 때 pointcut-ref 속성으로 특정 포인트컷을 참조할 수 있었기 때문에 포인트컷을 재사용할 수 있었다.
하지만 어노테이션 설정으로 변경하고나서부터는 어드바이스 클래스마다 포인트컷 설정이 포함되면서, 비슷하거나 같은 포인트컷이 반복 선언되는 문제가 발생한다. 스프링은 이런 문제를 해결하고자 포인트컷을 외부에 독립된 클래스에 따로 설정하도록 한다.
다음처럼 시스템에서 사용할 모든 포인트컷을 PointcutCommon 클래스에 등록한다.
@Service
@Aspect
public class PointcutCommon {
@Pointcut("execution(* com.springbook.biz..*Impl.*(..))")
public void allPointcut() {}
@Pointcut("execution(* com.springbook.biz..*Impl.*get(..))")
public void getPointcut() {}
}
이렇게 정의된 포인트컷을 참조하려면 클래스 이름과 참조 메소드 이름을 조합하여 지정해야 한다. 다음은 사전 처리 기능의 BeforeAdvice 클래스를 수정한 모습이다.
@Service
@Aspect
public class BeforeAdvice {
@Before("PointcutCommon.allPointcut()")
public void beforeLog(JoinPoint jp) {
String method = jp.getSignature().getName();
Object[] args = jp.getArgs();
System.out.println("[사전처리] "+method+"() 메소드 ARGS정보 : "+ args[0].toString());
}
}
다음과 같이 외부 클래스에서 참조하여 포인트컷을 사용할 수 있다.
스프링 JDBC
JDBC는 가장 오랫동안 자바 개발자들이 사용한 DB연동 기술이다. JDBC를 이용하여 DB연동 프로그램을 개발하면 데이터베이스에 비 종속적인 DB연동 로직을 구현할 수 있다. 그러나 JDBC프로그램을 이용하려면 작성해야 할 코드가 매우 많다.
다음은 JDBCUtil 클래스를 사용하여 구현한 BoardDAO 클래스다. insertBoard()와 updateBoard() 메소드는 실행되는 SQL구문과 ?에 설정하는 값만 다를 뿐, JDBC에 해당하는 자바 코드는 거의 같다.
public class BoardDAO {
//JDBC 관련 변수
private Connection conn= null;
private PreparedStatement stmt = null;
private ResultSet rs = null;
//SQL 명령어들
private final String BOARD_INSERT = "insert into board(seq,title,writer,content)values"
+ "((select nvl(max(seq),0)+1 from board),?,?,?)";
private final String BOARD_UPDATE = "update board set title=?,content=? where seq=?";
private final String BOARD_DELETE = "delete board where seq=?";
private final String BOARD_GET = "select * from board where seq=?";
private final String BOARD_LIST = "select * from board order by seq desc";
//CRUD 기능의 메소드 구현
// 글 등록
public void insertBoard(BoardVO vo) {
System.out.println("===> JDBC로 insertBoard() 기능처리");
try {
conn = JDBCUtil.getConnection();
stmt = conn.prepareStatement(BOARD_INSERT);
stmt.setString(1, vo.getTitle());
stmt.setString(2, vo.getWriter());
stmt.setString(3, vo.getContent());
stmt.executeUpdate();
}catch(Exception e) {
e.printStackTrace();
}finally {
JDBCUtil.close(stmt, conn);;
}
}
// 글 수정
public void updateBoard(BoardVO vo) {
System.out.println("===> JDBC로 updateBoard() 기능처리");
try {
conn = JDBCUtil.getConnection();
stmt = conn.prepareStatement(BOARD_UPDATE);
stmt.setString(1, vo.getTitle());
stmt.setString(2, vo.getWriter());
stmt.setString(3, vo.getContent());
stmt.executeUpdate();
}catch(Exception e) {
e.printStackTrace();
}finally {
JDBCUtil.close(stmt, conn);;
}
}
//글 삭제
public void deleteBoard(BoardVO vo) {
System.out.println("===> JDBC로 deleteBoard() 기능처리");
try {
conn = JDBCUtil.getConnection();
stmt = conn.prepareStatement(BOARD_DELETE);
stmt.setInt(1, vo.getSeq());
stmt.executeUpdate();
}catch(Exception e) {
e.printStackTrace();
}finally {
JDBCUtil.close(stmt, conn);;
}
}
//글 상세 조회
public BoardVO getBoard(BoardVO vo) {
System.out.println("===> JDBC로 getBoard() 기능처리");
BoardVO board = null;
try {
conn = JDBCUtil.getConnection();
stmt = conn.prepareStatement(BOARD_GET);
stmt.setInt(1, vo.getSeq());
rs = stmt.executeQuery();
if(rs.next()) {
board = new BoardVO();
board.setSeq(rs.getInt("SEQ"));
board.setTitle(rs.getString("TITLE"));
board.setWriter(rs.getString("WRITER"));
board.setContent(rs.getString("CONTETN"));
board.setRegDate(rs.getDate("REGDATE"));
board.setCnt(rs.getInt("CNT"));
}
}catch(Exception e) {
e.printStackTrace();
}finally {
JDBCUtil.close(stmt, conn);;
}
return board;
}
//글 목록 조회
public List<BoardVO> getBoardList(BoardVO vo){
System.out.println("===> JDBC로 getBoard() 기능처리");
List<BoardVO> boardList = new ArrayList<BoardVO>();
try {
conn = JDBCUtil.getConnection();
stmt = conn.prepareStatement(BOARD_LIST);
rs = stmt.executeQuery();
while(rs.next()) {
BoardVO board = new BoardVO();
board.setSeq(rs.getInt("SEQ"));
board.setTitle(rs.getString("TITLE"));
board.setWriter(rs.getString("WRITER"));
board.setContent(rs.getString("CONTENT"));
board.setRegDate(rs.getDate("REGDATE"));
board.setCnt(rs.getInt("CNT"));
boardList.add(board);
}
}catch(Exception e) {
e.printStackTrace();
}finally {
JDBCUtil.close(stmt, conn);
}
return boardList;
}
}
이런 환경에서 새로운 기능의 메소드를 개발하려면, 결국 기존 메소드를 복사하여 SQL만 수정하는 방법뿐이다. 스프링은 JDBC 기반의 DB연동 프로그램을 쉽게 개발할 수 있도록 JdbcTemplate 클래스를 지원한다.
JdbcTemplate 클래스
jdbc Template은 GoF 디자이 패턴 중 템플릿 메소드 패턴이 적용된 클래스이다. 템플릿 메소드 패턴은 복잡하고 반복되는 알고리즘을 캡슐화해서 재사용하는 패턴으로 정의할 수 있다. 템플릿 메소드 패턴을 이용하면 반복해서 사용되는 알고리즘을 템플릿 메소드로 캡슐화할 수 있어서 JDBC처럼 코딩 순서가 정형화된 기술에서 유용하게 사용할 수 있다. 따라서 반복되는 DB 연동 로직은 JdbcTemplate 클래스의 템플릿 메소드가 제공하고, 개발자는 달라지는 SQL 구문과 설정값만 신경쓰면 된다.
스프링 JDBC 설정
라이브러리 추가
스프링 JDBC를 이용하려면 BoardWeb 프로젝트에있는 pom.xml 파일에 DBCP관련 <dependancy>설정을 추가해야 한다.
<!-- DBCP -->
<dependency>
<groupId>commons-dbcp</groupId>
<artifactId>commons-dbcp</artifactId>
<version>1.4</version>
</dependency>
DataSource 설정
JdbcTemplate 클래스가 JDBC API를 이용하여 DB 연동을 처리하려면 반드시 데이터베이스로부터 커넥션을 얻어야 한다. 따라서 JdbcTemplate 객체가 사용할 DataSource를 <bean> 등록하여 스프링 컨테이너가 생성하도록 해야 한다.
applicationContext.xml에 다음과 같이 추가시킨다.
<!-- datasource 설정 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.OracleDriver"/>
<property name="url" value="jdbc:oracle:thin:@localhost:1521:XE"/>
<property name="username" value="database"/>
<property name="password" value="database"/>
</bean>
BasicDataSource 객체는 연결이 필요한 프로퍼티들을 setter인젝션으로 설정해주면 된다. 그리고 BasicDataSource 객체가 삭제되기 전에 연결을 해제하고자 close() 메소드를 destroy-method 속성으로 지정했다.
프로퍼티 파일을 활용한 DataSource 설정
propertyPlaceholderConfigurer를 이용하면 외부의 프로퍼티 파일을 참조하여 DataSource를 설정할 수 있다. src/main/resource 소스폴더에 config 폴더를 생성하고 config 폴더에 database.properties 파일을 작성한다.
database.properties
jdbc.driver = oracle.jdbc.OracleDriver
jdbc.url = jdbc:oracle:thin:@localhost:1521:XE
jdbc.username=database
jdbc.password=database
<context:property-placeholder location="classpath:config/database.properties" />
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
프로퍼티 파일을 사용하려면 <context:property-placeholder>엘리먼트로 프로퍼티 파일의 위치를 등록해야 한다. 그리고 "${}" 구문을 이용하여 프로퍼티 이름으 지정하면 프로퍼티 값으로 치환되어 실행된다.
JdbcTemplate 메소드
update() 메소드
INSERT, UPDATE, DELETE 구문을 처리하려면 JdbcTemplate 클래스의 update() 메소드를 사용한다. update() 메소드의 사용법은 "?"에 값을 설정하는 방식에 따라 크게 두가지 형태가 있다.
첫 번째 SQL구문에 설정된 "?" 수만큼 값들을 차례대로 나열하는 방식이다.
메소드 | int update(String sql, Object... args) |
사용 예 | //글 수정 public void updateBoard(BoardVO vo){ String BOARD_UPDATE = "update board set title=?, content=? where seq=?"; int cnt = jdbcTemplate.update(BOARD_UPDATE, vo.getTitle(),vo.getContent,vo.getSeq()); System.out.println(cnt+ "건 데이터 수정"); } |
두 번째는 Object 배열 객체에 SQL 구문에 설정된 "?" 수만큼의 값들을 세팅하여 배열 객체를 두 번째 인자로 전달하는 방식이다.
메소드 | int update(String sql, Object[] args) |
사용 예 | //글 수정 public void updateBoard(BoardVO vo){ String BOARD_UPDATE = "update board set title=?, content=? where seq=?"; Object[] args = {vo.getTitle(), vo.getContent, vo.getSeq()}; int cnt = jdbcTemplate.update(BOARD_UPDATE, args); System.out.println(cnt+ "건 데이터 수정"); } |
queryForInt() 메소드
SELECT 구문으로 검색된 정숫값을 리턴받으려면 quertForInt() 메소드를 사용한다. 매개변수의 의미는 앞에서 살펴본 update() 메소드와 같다.
메소드 | int queryForInt(String sql) int queryForInt(String sql, Obejct... args) int queryForInt(String sql, Object[] args) |
사용 예 | // 전체 게시글 수 조회 public int getBoardTotalCount(BoardVO vo){ String BOARD_TOT_COUNT="select * from board"; int count = jdbcTemplate.queryForInt(BOARD_TOT_CNT); System.out.println("전체 게시글 수: "+count+" 건"); } |
queryForObject() 메소드
queryForObject() 메소드는 SELECT 구문의 실행 결과를 특정 자바 객체(Value Object)로 매핑하여 리턴받을 때 사용한다. queryForObject() 메소드는 검색결과가 없거나 검색결과가 두 개 이상이면 예외를 발생시킨다.
메소드 | Object queryForObject(String sql) Object queryForObject(String sql, RowMapper<T> rowMapper) Object queryForObject(String sql, Object[] args, owMapper<T> rowMapper) |
사용 예 | // 글 상세 조회 public BoardVO getBoard(BoardVO vo){ String BOARD_GET = "select * from board where seq=?"; Object[] args = {vo.getSeq()}; return jdbcTemplate.queryForObject(BOARD_GET, args, new BoardRowMapper()); } |
검색 결과를 특정 VO객체에 매핑하여 리턴하려면 RowMapper 인터페이스를 구현한 RowMapper 클래스가 필요하다. 결국, RowMapper 클래스는 테이블당 하나씩은 필요하다는 말이다. RowMapper 인터페이스에는 mapRow() 메소드가 있어서 검색 결과로 얻어낸 Row 정보를 어떤 VO에 어떻게 매핑할 것인지를 구현해주면 된다.
public class BoardRowMapper {
public BoardVO mapRow(ResultSet rs, int rowNum) throws SQLException {
BoardVO board = new BoardVO();
board.setSeq(rs.getInt("SEQ"));
board.setTitle(rs.getString("TITLE"));
board.setWriter(rs.getString("WRITER"));
board.setContent(rs.getString("CONTENT"));
board.setRegDate(rs.getDate("REGDATE"));
board.setCnt(rs.getInt("CNT"));
return board;
}
}
RowMapper 객체를 queryForObject() 메소드의 매개변수로 넘겨주면, 스프링 컨테이너는 SQL구문을 수행한 후 자동으로 RowMapper 객체의 mapRow() 메소드를 호출한다.
query() 메소드
queryForObject()가 SELECT 문으로 객체 하나가 검색할 때 사용하는 메소드라면, query() 메소드는 SELECT 문의 실행 결과가 목록일 때 사용한다. 기본 사용법은 queryForObject() 메소드와 같다. 따라서 query() 메소드에서도 검색 결과를 VO객체에 매핑하려면 RowMapper객체를 사용한다.
메소드 | List query(String sql) List query(String sql, RowMapper<T> rowMapper) List query(String sql, Object[] args, RowMapper<T> rowMapper) |
사용 예 | //글 목록 조회 public List<BoardVO> getBoardList(BoardVO vo){ String BOARD_LIST = "select * from board order by seq desc"; return jdbcTemplate.query(BOARD_LIST, new BoardRowMapper()); } |
query() 메소드가 실행되면 여러 건의 ROW 정보가 검색되며, 검색된 데이터 ROW 수만큼 RowMapper 객체의 mapRow() 메소드가 실행된다. 그리고 이렇게 ROW정보가 매핑된 VO 객체 여러 개가 List 컬렉션에 저장되어 리턴된다.
DAO 클래스 구현
스프링 JDBC를 이용하기 위한 모든 설정이 마무리됐으면, 이제 JdbcTemplate 객체를 이용하여 DAO클래스만 구현하면 된다.
첫 번째 방법: JdbcDaoSupport 클래스 상속
BoardDAOSpring.java
@Repository("BoardDAOSpring")
public class BoardDAOSpring extends JdbcDaoSupport{
//SQL 명령어들
private final String BOARD_INSERT = "insert into board(seq,title,writer,content)values"
+ "(13,?,?,?)";
private final String BOARD_UPDATE = "update board set title=?,content=? where seq=?";
private final String BOARD_DELETE = "delete board where seq=?";
private final String BOARD_GET = "select * from board where seq=?";
private final String BOARD_LIST = "select * from board order by seq desc";
@Autowired
public void setSuperDataSource(DataSource dataSource){
super.setDataSource(dataSource);
}
//CRUD 기능의 메소드 구현
// 글 등록
public void insertBoard(BoardVO vo) {
System.out.println("===> Spring JDBC로 insertBoard() 기능처리");
getJdbcTemplate().update(BOARD_INSERT, vo.getTitle(),vo.getWriter(),vo.getContent());
}
// 글 수정
public void updateBoard(BoardVO vo) {
System.out.println("===> spring JDBC로 updateBoard() 기능처리");
getJdbcTemplate().update(BOARD_UPDATE, vo.getTitle(),vo.getContent(),vo.getSeq());
}
//글 삭제
public void deleteBoard(BoardVO vo) {
System.out.println("===> spring JDBC로 deleteBoard() 기능처리");
getJdbcTemplate().update(BOARD_DELETE, vo.getSeq());
}
//글 상세 조회
public BoardVO getBoard(BoardVO vo) {
System.out.println("===> spring JDBC로 getBoard() 기능처리");
Object[] args = {vo.getSeq()};
return getJdbcTemplate().queryForObject(BOARD_GET, args, new BoardRowMapper());
}
//글 목록 조회
public List<BoardVO> getBoardList(BoardVO vo){
System.out.println("===> spring JDBC로 getBoardList() 기능처리");
return getJdbcTemplate().query(BOARD_LIST,new BoardRowMapper());
}
}
DAO 클래스를 구현할 때, JdbcDaoSupport 클래스를 부모 클래스로 지정하면 getJdbcTemplate() 메소드를 상속받을 수 있다. 그리고 getJdbcTemplate() 메소드를 호출하면 JdbcTemplate객체가 리턴되어 모든 메소드를 JdbcTemplate 객체로 구현할 수 있다.
그런데 문제는 getJdbcTemplate() 메소드가 JdbcTemplate 객체를 리턴하려면 DataSource객체를 가지고 있어야 한다. 따라서 반드시 부모 클래스인 JdbcDaoSupport 클래스의 setDataSource() 메소드를 호출하여 데이터소스 객체를 의존성 주입해야 한다.
@Autowired
public void setSuperDataSource(DataSource dataSource){
super.setDataSource(dataSource);
}
@Autowired 어노테이션은 주로 변수 위에 선언하는데 메소드 위에 선언해도 동작한다. 메소드 위에 @Autowired를 붙이면 해당 메소드를 스프링 컨테이너가 자동으로 호출해주며, 이때 메소드 매개변수 타입을 확인하고 해당 타입의 객체가 메모리가 존재하면 그 객체를 인자로 넘겨준다.
다음과 같이 SpringDAO로 메소드가 호출됨을 알 수 있다.
두 번째 방법: JdbcTemplate 클래스 <bean>등록, 의존성 주입
DAO클래스에서 jdbcTemplate 객체를 얻는 두 번째 방법은 JdbcTemplate클래스를 <bean> 등록하고, 의존성 주입으로 처리하는 것이다. 일반적으로 이 방법을 사용한다. 먼저 스프링 설정 파일에 JdbcTemplate클래스를 <bean>등록한다.
<!-- <context:property-placeholder location="./config/database.properties" />-->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.OracleDriver"/>
<property name="url" value="jdbc:oracle:thin:@localhost:1521:XE"/>
<property name="username" value="database"/>
<property name="password" value="database"/>
</bean>
<!-- spring JDBC설정 -->
<bean id="jdbcTemplate" class="org.springframework.jdbc.core.JdbcTemplate">
<property name="datasource" ref="datasource"/>
</bean>
이때 반드시 JdbcTemplate 객체에 DataSource 객체를 의존성 주입해야 한다. 그러고 나서 DAO 클래스에서는 @Autowired 어노테이션을 이용하여 JdbcTemplate 타입의 객체를 의존성 주입 처리하면 된다.
@Repository("BoardDAOSpring")
public class BoardDAOSpring extends JdbcDaoSupport{
//SQL 명령어들
private final String BOARD_INSERT = "insert into board(seq,title,writer,content)values"
+ "(13,?,?,?)";
private final String BOARD_UPDATE = "update board set title=?,content=? where seq=?";
private final String BOARD_DELETE = "delete board where seq=?";
private final String BOARD_GET = "select * from board where seq=?";
private final String BOARD_LIST = "select * from board order by seq desc";
@Autowired
private JdbcTemplate jdbcTemplate;
//CRUD 기능의 메소드 구현
// 글 등록
public void insertBoard(BoardVO vo) {
System.out.println("===> Spring JDBC로 insertBoard() 기능처리");
jdbcTemplate.update(BOARD_INSERT, vo.getTitle(),vo.getWriter(),vo.getContent());
}
// 글 수정
public void updateBoard(BoardVO vo) {
System.out.println("===> spring JDBC로 updateBoard() 기능처리");
jdbcTemplate.update(BOARD_UPDATE, vo.getTitle(),vo.getContent(),vo.getSeq());
}
//글 삭제
public void deleteBoard(BoardVO vo) {
System.out.println("===> spring JDBC로 deleteBoard() 기능처리");
jdbcTemplate.update(BOARD_DELETE, vo.getSeq());
}
//글 상세 조회
public BoardVO getBoard(BoardVO vo) {
System.out.println("===> spring JDBC로 getBoard() 기능처리");
Object[] args = {vo.getSeq()};
return jdbcTemplate.queryForObject(BOARD_GET, args, new BoardRowMapper());
}
//글 목록 조회
public List<BoardVO> getBoardList(BoardVO vo){
System.out.println("===> spring JDBC로 getBoardList() 기능처리");
return jdbcTemplate.query(BOARD_LIST,new BoardRowMapper());
}
}
JdbcDaoSupport 클래스를 상속하여 구현하는 것보다 좀 더 깔끔해진 것을 확인할 수 있다.
트랜잭션 처리
스프링과 비교되는 EJB는 모든 비즈니스 메소드에 대한 트랜잭션 관리를 EJB 컨테이너가 자동으로 처리해준다. 스프링에서도 EJB와 마찬가지로 트랜잭션 처리를 컨테이너가 자동으로 처리하도록 설정할 수 있는데, 이를 선언적 트랜잭션이라 한다.
스프링이 제공하는 모든 트랜잭션 관리자는 트랜잭션 관리에 필요한 commit(), rollback() 메소드를 가지고 있다.
다음과 같이 스프링 설정 파일에 DataSourceTransactionManager 클래스를 <bean> 등록한다.
<!-- datasource 설정 -->
<bean id="dataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close">
<property name="driverClassName" value="oracle.jdbc.OracleDriver"/>
<property name="url" value="jdbc:oracle:thin:@localhost:1521:XE"/>
<property name="username" value="database"/>
<property name="password" value="database"/>
</bean>
<!-- Transaction 설정 -->
<bean id="txManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource"/>
</bean>
트랜잭션 어드바이스 설정
트랜잭션 관리 기능의 어드바이스는 <tx:advice>엘리먼트를 사용하여 설정한다.
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
위 설정은 txAdvice라는 어드바이스 앞에서 설정한 txManager를 이용하여 트랜잭션을 관리한다는 설정이다. 그리고 <tx:attributes>의 자식 엘리먼트로 <tx:method>엘리먼트를 이용하여 트랜잭션을 적용할 메소드를 지정할 수 있다. 위 설정은 get으로 시작하는 모든 메소드는 read-only="true" 즉, 읽기 전용으로 처리되어 트랜잭션 관리 대상에서 제외하고 니머지 메소드들은 트랜잭션 관리에 포함된 것이다.
<tx:method> 엘리먼트가 가질 수 있는 속성들
속성 | 의미 |
name | 트랜잭션이 적용될 메소드 이름 지정 |
read-only | 읽기 전용 여부 지정(기본값 false) |
no-rollback-for | 트랜잭션을 롤백하지 않을 예외 지정 |
rollback-for | 트랜잭션을 롤백할 예외 지정 |
AOP 설정을 통한 트랜잭션 적용
<tx:advice id="txAdvice" transaction-manager="txManager">
<tx:attributes>
<tx:method name="get*" read-only="true"/>
<tx:method name="*"/>
</tx:attributes>
</tx:advice>
<aop:config>
<aop:pointcut id='txPointcut' expression="execution(* com.springbook.biz..*Impl.*(..))"/>
<aop:advisor pointcut-ref="txPointcut" advice-ref="txAdvice"/>
</aop:config>
트랜잭션 동작 순서
1. 클라이언트가 BoardServiceImpl 객체의 insertBoard() 메소드를 호출한다.
2. insertBoard()메소드의 비즈니스 로직이 수행된다.
3. txAdvice로 등록한 어드바이스가 동작하여, 참조하는 txManager의 rollback()메소드를 호출한다.
4. 문제없이 정상으로 수행됐다면 commit() 메소드를 호출한다.
댓글