AOP

  • 관점 지향 프로그래밍(Aspect Orientied Programming)
  • 공통관심사항(common - cross cutting concern) 과 비지니스로직(core concern) 을 분리해서 구현한 후 설정 파일이나 어노테이션을 이용해서 결합을 시켜나가는 프로그래밍 방식
  • 복잡한 로직을 구현할 때 이 방식을 취합니다.
    • 파이썬에서도 어노테이션을 이용해서 이러한 구현이 어느정도 가능합니다.
  • spring에서는 url에 반응하는 HandlerInterceptor 와 method 호출에 반응하는 AOP 2가지 형태로 구현

image-20200225105539230

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

image-20200225111029653

// 클래스 이름에 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 설정 파일에 태그를 이용해서 Aspect를 설정

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/>

results matching ""

    No results matching ""