AOP로 컨트롤러 진입 마다 log 찍기

페이지별 view count를 측정하기 위해.. controller 진입 마다 log 테이블에 저장하기로 했다.
(하지만 이건 처음부터 잘못된 선택이였음..😅)

비록 view count를 측정하는 방법 접근이 좀 잘못된 거 같지만~ AOP 써본 기념으로 정리

AOP란?
AOP는 Aspect Oriented Programming 의 약자로 관점 지향 프로그래밍이라고 불린다. 관점 지향은 쉽게 말해 어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다. 여기서 모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다.

1. 먼저 log를 쌓을 Entity 설정

@Getter
@NoArgsConstructor
@SequenceGenerator(
        name="VIEWCOUNT_SEQ", //시퀀스 제너레이터 이름
        sequenceName="TBL_VIEWCOUNT_SEQ", //시퀀스 이름
        initialValue=1, //시작값
        allocationSize=1 //메모리를 통해 할당할 범위 사이즈
)
@Table(name = "TBL_VIEWCOUNT")
@Entity
public class UserAnaylsis extends BaseTimeEntity {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE, generator = "VIEWCOUNT_SEQ")
    private Long id;

    private String userId;

    private String controller;

    private String method;

    @Builder
    public UserAnaylsis(Long id, String userId, String controller, String method) {
        this.id = id;
        this.userId = userId;
        this.controller = controller;
        this.method = method;
    }
}

우리 회사는 oracle db를 쓰기 때문에 @SequenceGenerator로 id sequence를 생성해주었다. 진입한 controllermethod명을 담는 컬럼도 생성

2. JPA Auditing 생성

@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseTimeEntity {
    @CreatedDate
    private LocalDateTime createdDate;
}

스프링부트와 aws로 혼자 구현하는 웹서비스에서 본 JPA Auditing도 사용해줬다. 수정할 일은 없으니까 createDate만 생성. 위에 만든 엔티티에 extends 시켜준다. 그리고 application.java에 @EnableJpaAuditing 어노테이션 추가!

3. Aspect class 생성

나는 LogAspect라는 이름으로 class를 하나 생성해줬다.

@Aspect
@Component
@Slf4j
public class LogAspect {

}

AOP의 주요 개념

  • Aspect: 위에서 설명한 흩어진 관심사를 모듈화 한 것. 주로 부가기능을 모듈화함.
  • Target : Aspect를 적용하는 곳 (클래스, 메서드 .. )
  • Advice : 실질적으로 어떤 일을 해야할 지에 대한 것, 실질적인 부가기능을 담은 구현체
  • JointPoint : Advice가 적용될 위치, 끼어들 수 있는 지점. 메서드 진입 지점, 생성자 호출 시점, 필드에서 값을 꺼내올 때 등 다양한 시점에 적용가능
  • PointCut : JointPoint의 상세한 스펙을 정의한 것. 'A란 메서드의 진입 시점에 호출할 것'과 같이 더욱 구체적으로 Advice가 실행될 지점을 정할 수 있음

- pointcut

    @Pointcut(
            "within(@org.springframework.stereotype.Controller *) && " +
            "!within(@com.study.aop.analysis.NoLogging *) && " +
            "@annotation(org.springframework.web.bind.annotation.RequestMapping) && " +
            "execution(* com.study.controller..*.get*(xxx.xxx.xxx.VariableList, ..)) "
    )
    public void controller() { }
  • @Controller 어노테이션이 선언된 모든 class에서 포인트컷
  • @NoLogging 어노테이션이 붙은 target은 제외
  • @RequestMapping 어노테이션이 붙은 method만
  • controller 패키지 하위에서 get으로 시작하는 method 중 VariableList 이 parameter가 존재하는 것만.

포인트컷 표현식

(1) 지시자

  • within : 타입패턴 내에 해당하는 모든 것들을 포인트컷
  • execution : 가장 정교한 포인트컷을 만들수 있다. 리턴타입 패키지경로 클래스명 메소드명(매개변수)
  • bean : bean이름으로 포인트컷

(2) 리턴타입지정

표현식 설명
* 모든 리턴타입 허용
void 리턴타입이 void인 메소드 선택
!void 리턴타입이 void가 아닌 메소드 선택

(3) 패키지 지정

표현식 설명
com.study.domain 정확하게 com.study.domain 패키지만 선택
com.study.domain.. com.study.domain 패키지로 시작하는 모든 패키지 선택

(4) 클래스 지정

표현식 설명
UserVO UserVO클래스만 선택
*VO 이름이 VO로 끝나는 클래스만 선택
BaseObject+ 클래스 이름 뒤에 +가 붙으면 해당 클래스로부터 파생된 모든 자식 클래스를 선택. 인터페이스 이름 뒤에 +가 붙으면 해당 인터페이스를 구현한 모든 클래스 선택

(5) 메소드 지정

표현식 설명
*(..) 모든 메소드 선택
update*(..) 메소드명이 update로 시작하는 모든 메소드 선택

(6) 매개변수 지정

표현식 설명
(..) 모든 매개변수
(*) 반드시 1개의 매개변수를 가지는 메소드만 선택
(com.study.domain.user.model.User) 매개변수로 User를 가지는 메소드만 선택. 꼭 풀패키지명이 있어야함
(!com.study.domain.user.model.User) 매개변수로 User를 가지지않는 메소드만 선택
(Integer, ..) 한 개 이상의 매개변수를 가지되, 첫 번째 매개변수의 타입이 Integer인 메소드만 선택
(Integer, *) 반드시 두 개의 매개변수를 가지되, 첫 번째 매개변수의 타입이 Integer인 메소드만 선택

5. @NoLogging 어노테이션 만들기

@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface NoLogging { }
  • @Target : Type(class 레벨), Method (Method 레벨) 에 적용되도록 어노테이션 생성
@NoLogging
@Controller
public class IntroController { 
...
}

그리고 필요한 class나 method에 붙여줌.

4. log 저장

    @Before(value = "controller()")
    public void advice(JoinPoint thisJoinPoint) {
        System.out.println("#### LoginAspect 시작 ####");

        User loginUser = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal();
        count(loginUser, thisJoinPoint);

    }

    private void count(User loginUser, JoinPoint joinPoint){
        if(loginUser != null){
            UserAnalysisDto userAnalysisDto = setUserInfo(loginUser, joinPoint);
            save(userAnalysisDto);
        }
    }


    private UserAnalysisDto setUserInfo(User loginUser, JoinPoint joinPoint){

        String controller = joinPoint.getTarget().getClass().getSimpleName();
        String userId = loginUser.getSignedUser().getId();
        String method = joinPoint.getSignature().getName();

        return UserAnalysisDto.builder().controller(controller).userId(userId).method(method).build();
    }

    private void save(UserAnalysisDto dto){
        UserAnaylsis userAnaylsis = dto.toEntity();
        userAnaylsisRepository.save(userAnaylsis);
    }

이렇게 하고 실행하면 controller() 에 정해놓은 pointcut에 진입 전에 table에 저장 완료!

 


 

여기까지 해보다가 프로젝트가 중단되서 .. 한 이틀 했나..? 엉성하고.. 허술하지만.. 공부한데까지만 정리 해놓음.