AOP
- 관점 지향 프로그래밍(Aspect Orientied Programming)
- 공통관심사항(common - cross cutting concern) 과 비지니스로직(core concern) 을 분리해서 구현한 후 설정 파일이나 어노테이션을 이용해서 결합을 시켜나가는 프로그래밍 방식
- 복잡한 로직을 구현할 때 이 방식을 취합니다.
- 파이썬에서도 어노테이션을 이용해서 이러한 구현이 어느정도 가능합니다.
- spring에서는 url에 반응하는 HandlerInterceptor 와 method 호출에 반응하는 AOP 2가지 형태로 구현
HandlerInterceptor
- AOP 구현을 위해 제공되는 인터페이스
1.메소드
1) preHandle : Controller의 요청 처리 메소드를 호출하기 전에 호출되는 메소드로 이 메소드에서 true를 리턴하면 Controller의 요청 처리 메소드로 이동하고 false를 리턴하면 Controller의 요청 처리 메소드로 가지 않습니다.
2) postHandle : Controller의 요청처리 메소드가 처리를 전부 수행하고 view로 이동하기 직전에 호출되는 메소드로 예외가 발생하면 호출되지 않습니다.
3) afterCompletion : 요청을 전부 처리 한 후 호출되는 메소드로 예외 발생여부에 상관없이 무조건 호출되는 메소드
2.설정
1) 모든 요청에 반응
<mvc:interceptors>
<ref bean="인터셉터의 id"/>
</mvc:interceptors>
2) 특정 URL 패턴에 반응
<mvc:interceptors>
<mvc:interceptor>
<mvc:mapping path="URL 패턴"/>
<ref bean="인터셉터의 id"/>
</mvc:interceptor>
</mvc:interceptors>
- mvc:mapping 과 ref bean 의 순서를 변경하면 안됨.
3.chat 요청으로 이동할 때 로그인이 되어 있지 않으면 로그인 페이지로 이동하도록 interceptor를 생성
1) HandlerInterceptorAdapter 클래스를 extends 한 클래스를 기본 패키지 안에 생성
- com.gmail.hiwony7933.util.AuthInterceptor
// 클래스 이름에 Adapter 가 붙는 클래스는
// Adaptor를 제외한 인터페이스가 존재하는데
// 인터페이스는 모든 메소드를 재정의 해야 하고 Adapter 클래스는 필요한 메소드만
// 재정의 하면 된다.
//Bean을 자동 생성해주는 어노테이션 Component, Controller, Service, Repository
//RestController
@Component
public class AuthInterceptor extends HandlerInterceptorAdapter {
//Controller 가기 전에 호출되는 메소드
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handle) {
boolean result = false ;
//로그인이 안된 것을 확인
if(request.getSession().getAttribute("member") != null) {
// true를 리턴하도록 만들면 원래 처리를 하러 이동
result=true;
}else {
try {
//로그인이 되어 있지 않으면 로그인 페이지로 이동
response.sendRedirect("login"); // 어디로 갈지 가르쳐줌 login으로 가라
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
// 요청을 정상적으로 처리하고 난 후 호출되는 메소드
@Override
public void postHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handle,
ModelAndView mav) {
System.out.println("Contoller의 요청을 정상적으로 처리");
//로그 기록하는 경우가 많다.
}
}
1) servlet-context.xml 파일에 interceptor 설정하는 코드를 추가
<!-- interseptor 설정 : chat으로 이동 할 때 interceptor 가 동작하도록 설정 -->
<interceptors>
<interceptor>
<mapping path="/chat"/>
<beans:ref bean="authInterceptor" />
</interceptor>
</interceptors>
4.chat.jsp 파일에서 이름을 입력하지 않고 로그인 한 유저의 nickname을 이용하기
- 로그인을 하면 session에 member 라는 이름으로 Member 객체가 저장되어 있음
- 자바스크립트에서 메시지를 전송하는 send 메소드를 수정
var send = function(){
// 입력한 내용을 WebSocket 서버에게 전달하고 message 란은 클리어
var nickname = '${member.nickname};
var msg = document.getElementById("message").value;
wsocket.send(nickname + ":" + msg);
document.getElementById("message").value='';
}
5.chat으로 이동할려고 했는데 로그인이 되어 있지 않아서 로그인 페이지로 이동한 후 로그인에 성공하면 chat으로 이동하도록 해주는 설정을 추가
- 로그인 페이지로 이동할 때 메시지 저장해서 이동
- 인터셉터에서 이동할려고 했던 주소를 세션에 저장
- 로그인 성공했을 때 세션에 저장된 데이터를 확인해서 그 페이지로 이동하도록 해줘야 한다.
1) 인터셉터의 preHandle 메소드 수정
@Override
public boolean preHandle(
HttpServletRequest request,
HttpServletResponse response,
Object handle) {
boolean result = false ;
//로그인이 안된 것을 확인
if(request.getSession().getAttribute("member") != null) {
result=true;
}else {
try {
//로그인 페이지로 이동할 때 메시지 저장
request.getSession().setAttribute("msg", "로그인이 되어야 가능한 서비스임니다");
// 요청 URL을 확인
String requestURI = request.getRequestURI();
String contextPath = request.getContextPath();
String command = requestURI.substring(contextPath.length());
//파라미터 가져오기
String queryString = request.getQueryString();
//파라미터가 있으면 command 뒤에 붙이기
if(queryString != null) {
command = command + "?" + queryString;
}
// 세션에 command 저장
request.getSession().setAttribute("dest", command);
//로그인이 되어 있지 않으면 로그인 페이지로 이동
response.sendRedirect("login");
} catch (IOException e) {
e.printStackTrace();
}
}
return result;
}
2) HomeController의 login을 POST로 처리하는 메소드를 수정
- login 성공했을 때 session의 dest 값이 있으면 dest로 이동하도록 만들어 주어야 합니다.
- 이동한 후에는 dest 값을 삭제해야 합니다.
@RequestMapping(value = "/login", method = RequestMethod.POST)
public String login(HttpServletRequest request, Model model, RedirectAttributes attrs) {
// RedirectAttributes 는 redirect 할 때 1번만 사용하는 데이터를
// 저장할 수 있는 Spring이 제공하는 클래스
// 서비스의 메소드 호출
Member member = memberService.login(request);
// 로그인 처리도 redirect로 이동
if (member == null) {
//로그인 실패의 경우 msg를 저장하고 login으로 다시 이동
attrs.addFlashAttribute("msg", "없는 아이디거나 잘못된 비밀번호입니다.");
return "redirect:login";
} else {
// 이동할 URL이 있는지 확인
String dest = (String)request.getSession().getAttribute("dest");
if(dest !=null ) {
//dest에 저장된 내용을 삭제 - 계속 남아서 문제를 발생 시킬 가능성이 있음
request.getSession().removeAttribute("dest");
return "redirect:" + dest;
}
//로그인 성공이면 메인페이지로 이동
return "redirect:./";
}
}
Spring의 AOP
- 메소드의 수행 전이나 수행후에 공통괸 내용을 수행하고자 하는 경우 사용
- Interceptor는 URL에 대해서 반응하기 때문에 웹 프로그래밍에서만 사용이 가능하지만 AOP는 메소드에 반응하기 때문에 모든 프로그래밍에서 사용이 가능
Sping에서 AOP에 대해서 proxy 패턴을 사용합니다.
- AOP를 적용한 클래스를 상속받아서 Spring이 별도의 클래스를 만들고 인스턴스를 만들어서 메소드 호출을 하도록 합니다.
- final class에는 AOP 적용이 안됩니다. (final 클래스는 상속받을수 없는 클래스이다.)
구현 가능한 Advice(메소드 호출 시점)
- Befor Advice : 메소드 호출 전
- After Returning Advice : 메소드가 수행을 종료하고 리턴 한 후
- After Throwing Advice : 메소드 수행 중 예외가 발생한 경우
- After Advice : 메소드 수행 후
- Around Advice : 메소드 수행전과 수행 후
구현방법
- XML 기반의 POJO 클래스 또는 MethodInterceptor 인터페이스를 이용한 구현
- @Aspect Annotation을 이용하는 방법
XML 기반이 먼저 만들어 졌고 나중에 어노테이션이 추가된 것입니다.
필요한 의존성 라이브러리
- aspectjweaver, spring-aop, spring-aspects
1.XML 스키마를 이용한 AOP 설정 방법
1) pom.xml 파일에 AOP를 위한 의존성을 추가
2) 공통관심사항 가진 클래스를 생성해서 메소드를 추가
3) Spring 설정 파일에
4) Advice 를 어떤 point cut에 설정할 것인지를 지정해주면 된다.
2.실습
1) aspectjweaver, spring-aop, spring-aspects 의존성을 추가
- pom.xml
<!-- AOP 사용을 위한 의존성 라이브러리 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.8.8</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aop</artifactId>
<version>${org.springframework-version}</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-aspects</artifactId>
<version>${org.springframework-version}</version>
</dependency>
2) Advice(공통 관심 사항을 가진 객체)로 사용할 클래스를 생성하고 메소드 작성
- 기본 패키지 안에 생성
- com.gmail.hiwony7933.util.LoggingAdvice
// Spring에서는 DTO 클래스를 제외하고는 클래스 이름 위에 어노테이션을 추가해서
// bean을 자동생성하도록 합니다.
@Component
public class LoggingAdvice {
// 리턴 타입과 매개변수는 변경할 수 없습니다.
public Object invoke(ProceedingJoinPoint joinPoint) {
// 현재 시간을 출력
Calendar cal = new GregorianCalendar();
Date date = new Date(cal.getTimeInMillis());
System.out.println(date + " 요청이 발생함") ;
Object obj = null;
try {
//원래 호출할 메소드
obj = joinPoint.proceed();
} catch (Throwable e) {
e.printStackTrace();
}
return obj;
}
}
3) sevlet-context.xml 파일에 aop 네임스페이스를 추가
4) servlet-context.xml 파일에 aop 설정 태그를 추가
<!-- aop 설정 -->
<!-- com.gmail.hiwony7933 패키지 내에 있는 Controller라는 이름으로 끝나는 클래스의
메소드이름은 상관없고 매개변수개수도 상관없이 호출만 되면 LoggingAdvice 클래스의 invoke 메소드가 호출됨니다. -->
<aop:config>
<aop:aspect id="traceAspect" ref="loggingAdvice">
<aop:pointcut id="publicMethod" expression="execution(public * com.gmail.hiwony7933..*Contreoller.*(..))" />
<aop:around pointcut-ref="publicMethod" method="invoke"/>
</aop:aspect>
</aop:config>
3.Aspect 어노테이션을 이용하는 방법
1) java 클래스에 @Aspect 라는 어노테이션을 추가
2) 수행할 메소드 위에 execution 을 작성
3) 스프링 설정 파일에 aop:aspectj-autoproxy 태그를 설정
4. 실습
- Dao 클래스의 메소드를 호출 할 때 현재 시간과 데이터베이스에 접근했다고 로그를 기록
1) 기본 패키지 안에 Advice로 사용할 클래스를 생성하고 메소드 작성
- 기본패키지.util.LoggingDao
@Component
//AOP 클래스라는 어노테이션
@Aspect
public class LoggingDao {
@Around("execution(public * com.gmail.hiwony7933..*Dao.*(..))")
public Object invoke(ProceedingJoinPoint joinPoint) {
// 현재 시간을 출력
Calendar cal = new GregorianCalendar();
Date date = new Date(cal.getTimeInMillis());
System.out.println(date + "데이터베이스에 접근 ") ;
Object obj = null ;
try {
obj = joinPoint.proceed();
}catch (Throwable e) {
e.printStackTrace();
}
return obj;
}
}
2) servlet-context.xml 에 auto-proxy 설정
<!-- 어노테이션을 추가한 AOP 클래스가 동작하도록 해주는 설정 -->
<aop:aspectj-autoproxy/>