페이지별 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를 생성해주었다. 진입한 controller와 method명을 담는 컬럼도 생성
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에 저장 완료!
여기까지 해보다가 프로젝트가 중단되서 .. 한 이틀 했나..? 엉성하고.. 허술하지만.. 공부한데까지만 정리 해놓음.