BackEnd/Spring

Spring_AOP_국비_Day87

Leo.K 2022. 7. 6. 15:15

[ AOP의 개요 ]

  • 서비스 개요
    • 객체지향 프로그래밍(Object Oriented Programming)을 보완하는 개념으로 어플리케이션을 객체지향적으로 모듈화 하여 작성하더라도 다수의 객체들에 분산되어 중복적으로 존재하는 공통 관심사가 여전히 존재한다. AOP는 이를 횡단관심으로 분리하여 핵심관심과 엮어서 처리할 수 있는 방법을 제공한다.
    • 로깅, 보안, 트랜잭션 등의 공통적인 기능의 활용을 기존의 비즈니스 로직에 영향을 주지 않고 모듈화 처리를 지원하는 프로그래밍 기법

OOP의 단점 : 예를 들어 로깅처리를 위해 특정 메서드의 수행 시간을 측정하려고 할 때, 객체 지향 프로그래밍은 객체를 기반으로 프로그래밍 하기 때문에 같은 결과를 도출하는 중복된 코드를 모든 클래스에 작성해야 한다. 이는 이후에 수행시간을 체크하는 코드의 수정이 생긴다면 이 코드를 가지고 있는 모든 클래스의 코드를 수정해야 한다는 유지보수의 어려움이 있다.

위의 단점을 해결하기 위한 방법으로 AOP 방법론이 대두되었다. OOP는 100개의 클래스가 공통적인 처리를 필요한다고 가정할 때, 100개의 클래스에 같은 코드를 모두 삽입해야 한다. 하지만 AOP는 같은 처리에 대한 코드를 중앙에서 관리하기 때문에 코드를 모든 클래스에 삽입할 필요가 없다. -> 유지보수가 용이하다.

[ AOP 주요 개념 ] - 관점 지향 프로그램

  • Join Point
    • 횡단 관심(Crosscutting Concerns) 모듈이 삽입되어 동작할 수 있는 실행 가능한 특정 위치를 말함
    • 메소드 호출, 메소드 실행 자체, 클래스 초기화, 객체 생성 시점 등
  • Pointcut(감시지점 - 관찰지점을 설정하는 것)
    • Pointcut은 어떤 클래스의 어느 JoinPoint를 사용할 것인지를 결정하는 선택 기능을 말함
    • 프로그램의 흐름이 특정 감시지점(PointCut)을 통과할 때, 클래스에 대한 정보를 가지는 JoinPoint를 Advice에게 전달하여 코드를 수행하도록 호출한다.
    • 가장 일반적인 Pointcut은 ‘특정 클래스에 있는 모든 메소드 호출’로 구성된다.
  • 애스펙트(Aspect)
    • Advice와 Pointcut의 조합
    • 어플리케이션이 가지고 있어야 할 로직과 그것을 실행해야 하는 지점을 정의한 것
  • Advice(부가 기능을 구현한 코드)
    • Advice는 관점(Aspect)의 실제 구현체로 결합점에 삽입되어 동작할 수 있는 코드이다
    • Advice 는 결합점(JoinPoint)과 결합하여 동작하는 시점에 따라 before advice, after advice, around advice 타입으로 구분된다
    • 특정 Join point에 실행하는 코드
  • Weaving
    • Pointcut에 의해서 결정된 JoinPoint에 지정된 Advice를 삽입하는 과정
    • Weaving은 AOP가 기존의 Core Concerns 모듈의 코드에 전혀 영향을 주지 않으면서 필요한 Crosscutting Concerns 기능을 추가할 수 있게 해 주는 핵심적인 처리 과정임

[ AOP의 장점 ]

  1. 중복 코드의 제거
    1. 횡단 관심(CrossCutting Concerns)을 여러 모듈에 반복적으로 기술되는 현상을 방지
  2. 비즈니스 로직의 가독성 향상
    1. 핵심기능 코드로부터 횡단 관심 코드를 분리함으로써 비즈니스 로직의 가독성 향상
  3. 생산성 향상
    1. 비즈니스 로직의 독립으로 인한 개발의 집중력을 높임
  4. 재사용성 향상
    1. 횡단 관심 코드는 여러 모듈에서 재사용될 수 있음
  5. 변경 용이성 증대
    1. 횡단 관심 코드가 하나의 모듈로 관리되기 때문에 이에 대한 변경 발생시 용이하게 수행할 수 있음

 

[ AOP 라이브러리 등록하기 ]

 

[ context-7-myaop.xml ] 환경설정 파일을 추가해서 aop에 대한 설정을 한다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<?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/beans https://www.springframework.org/schema/beans/spring-beans.xsd
        http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd">
    
    <!-- Root Context: defines shared resources visible to all other web components -->
    <!-- AOP Test -->
 
    <!-- 공통 관심사항이 구현된 Advice객체 생성 -->
    <bean id="advice" class="advice.Advice" />
 
    <!-- AOP설정 : Target객체의 모든 메서드에 Advice에 구현된 공통기능을 적용한다. -->
    <aop:config>
<!-- service패키지 안에 있는 Service로 끝나는 모든 클래스 내부에 정의된 모든 메소드가 호출될 때, advice에게 알려줘! -->
        <aop:pointcut expression="execution(public * service.*Service.*(..))"
            id="myPoint" />
        <aop:aspect id="test" ref="advice">
            <aop:before method="before" pointcut-ref="myPoint" />
            <aop:after method="after"   pointcut-ref="myPoint" />
        </aop:aspect>
    </aop:config>
            
</beans>
 
cs

위의 코드를 복사하면 에러가 날 텐데 아래의 이미지대로 namespace를 설정해주면 된다.

 

AOP를 구현해보기 위해 오늘은 service객체를 사용하여 아래의 이미지대로 구현해보고자 한다. 

이미지처럼 준비과정으로 TestDao, Test2Dao, TestDaoImplmpl, Test2DaoImpl, TestServcie, TestServiceImpl을 생성한다. 위의 객체들을 모두 생성해서 준비가 완료되었다면, 빨간 실선의 방향대로 dao를 service객체에 의존성 주입한다.

service 객체에서는 dao에서 수집한 데이터를 취합해서 한꺼번에 컨트롤러에게 전달하는 역할을 수행하기 때문이다. 이처럼 여러개의 객체로부터 주입받는 경우에는 생성자 주입이 편리하다.

이제 TestController를 생성해준다. 지금까지와는 다르게 컨트롤러는 dao로부터 의존성을 주입받는 것이 아니라, service객체가 dao의 데이터를 취합해놓았기 때문에, Service로부터 의존성을 주입받는다. 

 

그림의 이미지와는 약간 다르다면 준비된 예제는 dao에 데이터가 이미 존재하기 때문에 DB에는 접근하지 않는다. 따라서 dao에 sqlSession을 주입할 필요는 없다.

위의 클래스를 모두 생성했다면, 지금부터는 환경설정 파일을 수정하면서 의존성을 주입시키는 코드를 작성할 것이다. 

[ context-3-dao.xml ] - dao객체 생성

[ context-4-service.xml ] - service를 생성하면서, dao로부터 의존성 주입

[ servlet-context.xml ] - testController를 생성하면서, service로부터 의존성 주입받는다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
<!-- context-3-dao.xml -->
<bean id="dao1"  class="dao.TestDaoImpl"></bean>
<bean id="dao2"  class="dao.Test2DaoImpl"></bean>
 
<!-- context-4-service.xml -->
<bean id="test_service" class="service.TestServiceImpl">
    <constructor-arg ref="dao1"/>
    <constructor-arg ref="dao2"/>
</bean>
 
<!-- servlet-context.xml -->
<!-- 수동 생성(직접 의존성 주입할 것이다.) -->
<beans:bean class="controller.TestController">
    <beans:constructor-arg ref="test_service"/>
</beans:bean>
cs

지금까지의 과정을 잘 따라왔다면 아래와 같은 beans graph가 생성될 것이다.

[ beans graph가 안보이는 경우 ]

프로젝트 -> properties -> spring -> bean support -> add xml config -> 의존성을 주입한 xml파일을 모두 선택한다. configFiles -> add XML Config -> config Sets -> new - > 방금 추가한 xml파일을 선택한 후 ok한다.

이제 출력을 진행해보자!

[ TestController ]

[ total_list.jsp ]

오늘은 메인 프로그램(service:데이터 취합)의 수행 시간을 체크하도록 AOP를 사용해보았다. 아래와 같은 Advice코드를 작성하면 메인 코드에 수행시간을 측정하는 코드를 삽입하지 않아도 같은 효과를 볼 수 있다. 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
package advice;
 
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
 
public class Advice {
    long start;
    public void before(JoinPoint jp){
        Signature s =  jp.getSignature();
        //시작 시간
        start = System.currentTimeMillis();
        System.out.println("----before:" + s);
    }
    
    public void after(JoinPoint jp){
        Signature s =  jp.getSignature();
        //종료시간
        long end = System.currentTimeMillis();
        
        System.out.printf("---- 수행시간 : %s(ms) ----\n", end-start);
        System.out.println("----after:" + s.toLongString());
    }
}
 
cs

하지만 위의 코드는 아주 큰 맹점이 있다. 왜냐면 시작시간인 start가 전역 변수 즉, 공유 객체이기 때문이다. 하나의 예시를 생각해보자. 한 명의 사용자만 웹 사이트를 사용한다면 문제가 없겠지만, 동시간대 사용자가 한 명이라는 것은 개발자에게 있어 가장 가슴 아픈 소식이 아닌가? 

동시간대 이용자 A, B가 있다고 해보자. 두 사용자는 동일한 기능을 사용하고, 실제 기능의 수행시간이 5라고 가정하자.

먼저 A의 요청이 들어와서 before가 호출되었을 때, 시간이 3이라고 해보자. 2초 후에 아직 A가 응답을 받지 못한 경우 B가 들어와 같은 요청을 보냈다고 하면 start변수에는 5가 찍히게 된다. 그리고 다시 3초 후면 A가 요청한 작업이 끝나서 종료시간이 8이 찍히게 된다. 

이 부분에서 수행시간을 체크하게 되면 8-3=5가 나올 것 같지만, 8-5=3으로 잘못된 수행시간이 측정된다. 말 그대로 start라는 변수가 전역으로 생성되어 공유자원으로 사용되고 있기 때문에 A가 응답을 받기 전에 B의 시작시간이 덮어써지는 것이다. 좀 더 명확한 이해를 위해 아래 그림을 보자.

이를 해결하기 위해서는 각 사용자별로 독립적으로 사용할 수 있는 객체를 사용해야 한다. 감이 오는가? 클라이언트에서 사용자로부터 요청이 들어오면 톰캣이 하나의 객체를 독립적으로 할당해주고, 이 객체는 응답처리가 완료될 때까지 살아있다. 바로 HttpServletRequest객체이다. 이 객체에 start변수를 바인딩 하면, 동시간대에 다수의 사용자가 사용하더라도 데이터가 겹치는 것을 방지할 수 있다.

수정 코드 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
package advice;
 
import javax.servlet.http.HttpServletRequest;
 
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.springframework.beans.factory.annotation.Autowired;
 
public class Advice {
    
    //Advice객체가 수동생성 -> @AutoWired지원 x
    //                        -> <context:annotation-arg/>
    //한 사용자로부터 요청이 들어오면 요청에 대한 응답이 될 때까지 서버에서 사용자에게 개별적으로 제공하는 객체를 사용하면 맹점 방지가능
    @Autowired
    HttpServletRequest request;
    
    public void before(JoinPoint jp){
        Signature s =  jp.getSignature();
        //시작 시간
        long start = System.currentTimeMillis();
        request.setAttribute("start", start);
        System.out.println("----before:" + s);
    }
    
    public void after(JoinPoint jp){
        Signature s =  jp.getSignature();
        //종료시간
        long end = System.currentTimeMillis();
        long start = (long) request.getAttribute("start");
        
        System.out.printf("---- 수행시간 : %s(ms) ----\n", end-start);
        System.out.println("----after:" + s.toLongString());
    }
}
 
cs

'BackEnd > Spring' 카테고리의 다른 글

Spring_Day5_SpringLifeCycle  (0) 2022.07.11
Spring_Transaction_국비_Day89  (0) 2022.07.08
Spring_국비_Day86  (0) 2022.07.05
SpringFileUpload_국비_Day86  (0) 2022.07.04
SpringMVC_DB_국비Day84  (0) 2022.06.30