본문 바로가기

Backend/Spring

[Spring] AOP

728x90

 

먼저 AOP가 필요한 상황에 대해 알아본다. 

AOP가 필요한 상황


 

공통 관심 사항(cross-cutting concern) vs 핵심 관심 사항(core concern)

 

예를 하나 들자

모든 메소드의 호출 시간을 측정해야할 일이 생겼다!

모든 메소드(999개)에 하나하나 시작과 끝 사이에 시간 측정 코드를 삽입?? → 불가능 (야근하면 가능 ㅎㅎ)

해본다면 다음과 같다. 

메소드 하나에 대해서 try안에 실제 핵심로직을 넣고 나머지 부분에 이렇게 시간을 측정하는 코드를 작성한다.

public Long join(Member member) {

	//시작 시간 
        long start = System.currentTimeMillis();
        
	try {
    	//실제 코드(핵심로직)
	    validateDuplicateMember(member); //중복 회원 검증
            memberRepository.save(member);
            return member.getId();
            
        } finally { //무조건 실행되는 부분
        	
           	//끝나는 시간  
            long finish = System.currentTimeMillis();
            
            //끝시간-시작시간 차이로 측정해냄
            long timeMs = finish - start;
            System.out.println("join " + timeMs + "ms");
		} 
}

이제 이 방법으로 남은 998개의 메소드에 try안에 핵심부분을 넣고 나머지 부분에 시간을 측정하는 코드를 삽입해주어야한다.

이렇게 핵심 로직을 제외한 나머지 메소드들이 동일하게 가져야 할 부분을 "공통 관심사항(cross-cutting concern)"이라고 부른다.

핵심 로직부분은 "핵심 관심 사항(core concern)"이라고 한다.

 

문제

  • 회원가입, 회원 조회에 시간을 측정하는 기능은 핵심 관심 사항 X 
  • 시간을 측정하는 로직은 공통 관심 사항
  • 시간을 측정하는 로직과 핵심 비즈니스의 로직이 섞여서 유지보수가 어려움!!
  • 시간을 측정하는 로직을 별도의 공통 로직으로 만들기 매우 어려움
  • 시간을 측정하는 로직을 변경할 때 모든 로직을 찾아가면서 변경해야 함

말만 들어도 피곤하다

 

 

AOP 적용

 

Aspect Oriented Programming(관점 중심 프로그래밍)

공통관심 사항과 핵심 관심사항을 분리하자!

위의 예시로 해결해보자

공통 관심 로직을 하나에 모아놓고, 적용하고 싶은 곳에 적용하는 방식을 사용한다.

 

 

 

aop디렉토리를 만들어, 공통 관심 로직을 생성한다.

시간 측정 기능이므로 TimeTraceAop라고 생성한다. 

 

 

TimeTraceAop

package hello.hellospring.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Aspect;

@Aspect
public class TimeTraceAop {


    @Around("execution(* hello.hellospring..*(..))") //작성한 공통 사항을 어디에 적용할 지 명시 
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable{
        long start = System.currentTimeMillis();

        System.out.println("START : "+ joinPoint.toString());
        try{
            return joinPoint.proceed();//다음 메소드로 진행되는 내용, return값이 필요함 ojbect 타입으로 리턴됨
        }finally{
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("END : "+ joinPoint.toString() + " " + timeMs + "ms");
        }
    }
}

여기서는 hello.hellospring하위의 모든 것에 적용된다.

@Around에 대해서는 공식문서를 참고하면 더 넓게 활용이 가능하다.

예를 들면 다음과 같이 작성하면 

    @Around("execution(* hello.hellospring.service..*(..))")

서비스와 관련된 메소드에만 적용된다. 

 

 

이제 이를 SpringConfig에서 Bean으로 등록해준다.

@Configuration
public class SpringConfig {

//생략 
    @Bean
    public TimeTraceAop timeTraceAop(){
        return new TimeTraceAop();
    }

}

이 방법도 있고, TimeTraceAop클래스를 만들 때 @Component 를 이용해 컴포넌트 스캔을 사용해도 되지만,

AOP는 특수경우 이므로 bean으로 등록해주는 것이 좋다. 

 

더보기

여기서 에러나는데, 

Bean으로 등록하지 않고, @Component로 해야만 잘 작동하는 것으로 보인다.. 

 

TimeTraceAop

package hello.hellospring.aop;

import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.springframework.stereotype.Component;

@Aspect
@Component
public class TimeTraceAop {

    @Around("execution(* hello.hellospring..*(..))") //작성한 공통 사항을 어디에 적용할 지 명시
    public Object execute(ProceedingJoinPoint joinPoint) throws Throwable{
        long start = System.currentTimeMillis();

        System.out.println("START : "+ joinPoint.toString());
        try{
            return joinPoint.proceed(); //return값이 필요함 ojbect 타입으로 리턴됨
        }finally{
            long finish = System.currentTimeMillis();
            long timeMs = finish - start;
            System.out.println("END : "+ joinPoint.toString() + " " + timeMs + "ms");
        }
    }
}

 

SpringConfig

package hello.hellospring;

import hello.hellospring.aop.TimeTraceAop;
import hello.hellospring.repository.*;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import javax.persistence.EntityManager;
import javax.sql.DataSource;

@Configuration
public class SpringConfig {


//spring api
    private final MemberRepository memberRepository;

    @Autowired
    public SpringConfig(MemberRepository memberRepository) { //스프링 컨테이너에서 멤버리포지토리를 찾음(spring jpa가 자동으로 생성한)
        this.memberRepository = memberRepository;
    }


    @Bean
    public MemberService memberService() {

        return new MemberService(memberRepository);//의존성 등록
    }

//    @Bean
//    public TimeTraceAop timeTraceAop(){
//        return new TimeTraceAop();
//    }


}

 이렇게 해주면 해결됨. 

이유는 bean으로 등록하면 추가적 설정이 필요한 것으로 보이는데.. 일단 설명을 스킵하셔서 생략 

  

 

 

 

이제 서버를 실행 후, 이것저것 실행해보면,

모든 메소드가 실행시마다 찍히는 모습을 볼 수 있다. 

 

 

위에서 설명한 문제점들이 다음과 같이 해결될 수 있다.

  • 회원가입, 회원 조회등 핵심 관심사항과 시간을 측정하는 공통 관심 사항을 분리한다. 
  • 시간을 측정하는 로직을 별도의 공통 로직으로 만들었다.
  • 핵심 관심 사항을 깔끔하게 유지할 수 있다.
  • 변경이 필요하면 이 로직만 변경하면 된다.
  • 원하는 적용 대상을 선택할 수 있다.

 

 

 

동작방식


 

적용 전후는 다음과 같고, 따라서 전체 서비스를 그림으로 나타내면 다음과 같다. 

콘솔 출력은 MemberContorller 생성자가 실행될 때, memberService.getClass() 프린트해보면 알 수 있다. 

DI를 이용했기 때문에 쓸 수 있는 기술이다! 

728x90