페이지별 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에 저장 완료!
여기까지 해보다가 프로젝트가 중단되서 .. 한 이틀 했나..? 엉성하고.. 허술하지만.. 공부한데까지만 정리 해놓음.