LostCatBox

UsingAOPAtSpring

Word count: 1.2kReading time: 7 min
2022/12/24 Share

공통적인 메서드 추출 방법(AOP)

Created Time: August 7, 2022 8:57 AM
Last Edited Time: October 4, 2022 12:06 PM
References: https://shinsunyoung.tistory.com/67
https://shinsunyoung.tistory.com/83
Tags: Computer

왜?

현재 유저가 해당 유저인지 확인(validation)하는 빈도가 많아졌고, 이를 매번 메서드에서 구현해주었다.

  • 공통되는 유저인증부분에 대해 중복코드를 줄일 수 있는 방법은 없을까?
  • 공통되는 Log남기는 로직에서 중복코드를 줄일수있을까?

대표적으로 해결방법은 Aop로 생각된다.

Aop

관점 지향 프로그래밍!(Aspect Oriented Programming)

어떤 로직을 기준으로 핵심적인 관점, 부가적인 관점으로 나누어서 보고 그 관점을 기준으로 각각 모듈화하겠다는 것이다. (모듈화란 어떤 공통된 로직이나 기능을 하나의 단위로 묶는 것을 말한다)

아래 OOP(객체 지향 프로그래밍)의 한계. 클래스별로 단일 책임을 갖는것은 좋지만, 같은 기능을 갖는 중복 코드들이 각 클래스에 존재해야함.

1

아래는 OOP의 한계를 해결하기 위해, 클래스들의 핵심 기능과 부가 기능(공통적)을 나눠 관심사를 분리한다. 공통적인(부가기능)부분을 모듈화한다

이를 통해, 관심사 분리+다양한 클래스에서 Aspect을 재활용하며 공통 사용할수있도록함.

2

AspectJ 사용되는 에너테이션 설명

참조: https://vmpo.tistory.com/100

  • @Pointcut : aspectJ를 적용할 타겟을 정의해준다. 전체 컨트롤러의 함수대상, 특정 어노테이션을 설정한 함수대상, 특정 메소드 대상 등 개발자가 적용하길 원하는 범위를 정의하는 어노테이션
  • @Before : aspectJ를 적용할 타겟 메소드가 실행되기 ‘전’ 수행됨
  • @AfterReturning : aspectJ를 적용할 타겟 메소드가 실행된 ‘후’ 수행됨 (제일 마지막에 수행됨)
  • @Around : aspectJ를 적용할 타겟 메소드 실행 전 , 후 처리를 모두 할 수 있음

구현 예시(로그남기기 기능)

프로젝트 설명

  • 개발을 하면서 로그는 에러가 난 이유를 찾거나 값을 확인하는데 중요한 역할을 한다. 하지만 @Slf4j를 사용하여 매번 log.info()등을 사용하는 것은 다음과같은 단점이 존재한다.
    • 중복된 코드
    • 로그를 찍지 않으면 값 확인 불가능
  • 따라서 AOP를 사용해 요청이 오면 요청 데이터, 응답 데이터, 요청까지 걸린 시간을 로그로 자동으로 찍어주는 코드를 작성해보자

의존성 추가

Gradle

1
implementation 'org.springframework.boot:spring-boot-starter-aop'

@EnableAspectJAutoProxy

Application.java

1
@EnableAspectJAutoProxy // 추가

@Aspect 클래스 만들기

Log 관련 설정을 할 클래스를 만들어준다

LogConfig.java

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
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
@Slf4j
@Component
@Aspect
public class LogConfig {
//around이므로 메서드 실행 전, 후 시점에 메서드 실행함. 본 메서드 실행은 pjp.proceed()
////within으로 범위설정가능
@Around("within(hello.postpractice.controller.*)")
public Object logging(ProceedingJoinPoint pjp) throws Throwable {
String params = getRequestParams(); //request값 가져오기

long startAt = System.currentTimeMillis();
log.info("-----------> REQUEST : {}({}) = {}", pjp.getSignature().getDeclaringTypeName(),
pjp.getSignature().getName(), params);

Object result = pjp.proceed(); // 본 메서드 실행

long endAt = System.currentTimeMillis();

log.info("-----------> RESPONSE : {}({}) = {} ({}ms)", pjp.getSignature().getDeclaringTypeName(),
pjp.getSignature().getName(), result, endAt - startAt);

return result;
}

// Get request values
private String getRequestParams() {

String params = "없음";

RequestAttributes requestAttributes = RequestContextHolder
.getRequestAttributes(); // 3

if (requestAttributes != null) {
HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder
.getRequestAttributes()).getRequest();

Map<String, String[]> paramMap = request.getParameterMap();
if (!paramMap.isEmpty()) {
params = " [" + paramMapToString(paramMap) + "]";
}
}

return params;

}
private String paramMapToString(Map<String, String[]> paramMap) {
return paramMap.entrySet().stream()
.map(entry -> String.format("%s -> (%s)",
entry.getKey(), Joiner.on(",").join(entry.getValue())))
.collect(Collectors.joining(", "));
}
}

@Around

  • 대상 객체(within으로 범위 설정 가능)의 메서드 실행 전, 후 시점에 메소드를 실행

ProceedingJoinPoint

  • 호출되는 객체에 대한 정보, 실행될 메소드에 대한 정보가 존재

getRequestattributes()

  • 호출된 값의 Request 값을 얻을 때 사용, 없으면 null 반환
  • 이는 currentRequestAttributes와 비슷하지만 애는 값이 없으면 예외 발생함

proceed()

  • 핵심 메서드 실행

로직 설명

  • 요청
  • 메서드 실행 전에 컨트롤러와 메서드 이름, Request값의 정보를 담은 Request 로그 출력
  • proceed() 메서드를 이용해 원래 실행해야하는 본 핵심 메서드 실행
  • 실행 후에 컨트롤러와 메서드 이름, 반환된 값의 정보를 담은 Response 로그 출력
  • 응답

log.info로 response찍힌모습

3

커스텀 에너테이션 작성

목표: (로그 남기기 기능에서 제외시키기)

에너테이션 = 메타 데이터 (프로그램에 추가적인 정보를 제공해주는 정보)(이때 메타 데이터는 애플리케이션이 처리해야 할 데이터가 아니라 컴파일 과정과 런타임에서 코드를 어떻게 컴파일하고 처리할것인지에 대한 정보를 말한다)

ex) Aspect을 짜놓고, 특정 에너테이션을 가지면 Aspect처리에서 제외 또는 다른 로직 실행 등등

Spring Annotation의 원리

Spring에서 bean만들고 등록하는 방법중 대표적인 @Component 에너테이션을 살펴보자

spring에서 해당 @Component를 가진애들은 doscan으로 읽어서 bean에 등록하는 로직을 가지고있다

1
2
3
4
5
6
7
8
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Indexed
public @interface Component {
String value() default "";

}

@Target, @Rentetion, @Documneted 총 3개의 어노테이션과 @Indexed 어노테이션을 갖고 있습니다.

  • @Documented : Java doc에 문서화 여부를 결정합니다.
  • @Retention : 어노테이션의 지속 시간을 정합니다.(유효기간)
    • RetentionPolicy.SOURCE : 컴파일 후에 정보들이 사라집니다. 이 어노테이션은 컴파일이 완료된 후에는 의미가 없으므로, 바이트 코드에 기록되지 않습니다. 예시로는 @Override와 @SuppressWarnings 어노테이션이 있습니다.
    • RetentionPolicy.CLASS : default 값 입니다. 컴파일 타임 때만 .class 파일에 존재하며, 런타임 때는 없어집니다. 바이트 코드 레벨에서 어떤 작업을 해야할 때 유용합니다. Reflection 사용이 불가능 합니다.
    • RetentionPlicy.RUNTIME : 이 어노테이션은 런타임시에도 .class 파일에 존재 합니다. 커스텀 어노테이션을 만들 때 주로 사용합니다. Reflection 사용 가능이 가능합니다.
  • @Target : 어노테이션을 작성할 곳 입니다. default 값은 모든 대상입니다.
    • 예를 들어 @Target(ElementType.FIELD)
      로 지정해주면, 필드에만 어노테이션을 달 수 있습니다. 만약 필드 말고 다른부분에 어노테이션을 사용한다면 컴파일 때 에러가 나게 됩니다.

Custom 에너테이션 만들기

  1. 에너테이션 타입은 @interface로 정의해야합니다. 모든 에너테이션은 자동적으로 java.lang.Annotation 인터페이스를 상속하기 때문에 다른 클래스나 인터페이스를 상속 받으면 안됩니다.
  2. 파라미터 멤버들의 접근자는 public이거나 default여야만 합니다.
  3. 파라미터 멤버들은 byte,short,char,int,float,double,boolean,의 기본타입과 String, Enum, Class, 어노테이션만 사용할 수 있습니다.
  4. 클래스 메소드와 필드에 관한 어노테이션 정보를 얻고 싶으면, 리플렉션만 이용해서 얻을 수 있습니다. 다른 방법으로는 어노테이션 객체를 얻을 수 없습니다.

LogExclusion

  • LogExclusion 에너테이션은 함수의 메타데이터로써 Aspect에서 이 에너테이션있을시, 로깅 동작 X
1
2
3
4
5
@Target({ElementType.METHOD}) // 로그를 제외하는 에너테이션이므로 메서드위에 구현할것
@Retention(RetentionPolicy.RUNTIME) // 실행동안 유지해야하므로
public @interface LogExclusion {

}

LogConfig

  • controller 패키지 안에&& LogExclusion 에너테이션이없을때 조건일때만, logging실행하도록함(즉, @LogExclusion존재시 로깅안함)
1
2
3
4
5
6
7
8
public class LogConfig {
//around이므로 메서드 실행 전, 후 시점에 메서드 실행함. 본 메서드 실행은 pjp.proceed()
////within으로 범위설정가능
@Around("within(hello.postpractice.controller.*) && !@annotation(hello.postpractice.aop.LogExclusion)") //controller 패키지 안에&& LogExclusion 에너테이션이없을떄 !
public Object logging(ProceedingJoinPoint pjp) throws Throwable {
String params = getRequestParams(); //request값 가져오기
.....
}

커스텀 에너테이션의 단점

  • 에너테이션의 의도는 숨어있기 때문에 내부적으로 어떤 동작을 하게 되는지 명확하지 않다면, 로직 플로우를 이해하기 어렵게 되고, 코드정리가 덜 되어 현재 사용되지 않고 있는 어노테이션들이 있더라도 쉽사리 누군가가 손을 대기 부담스러워하는 경우가 있다. >> 무분별한 어노테이션 추가가 당장의 작업 속도를 끌어올릴 순 있지만, 긴 관점에서 시의적절한 것인지를 공감할 수 있어야 합니다.
  • 커스텀 에너테이션은 최대한 단기능, 공감할수있는정도의 범위를 가지는것이중요하다(다기능 구현시, 누군가 쉽사리 없앨수없다)

AOP활용하는 방법

선택하자

  • pointcut을 파일별로 하는것이 좋을지
  • 에너테이션을 통해 pointcut으로 지정할지
  • Around 사용시 within &&에너테이션 으로 활용(위에 예시처럼)

해당 비즈니스의 확장 가능성 점검

확장성에서는 파일별로 지정해주는것보다 확실히 에너테이션으로 해주는것이 훨씬 간편하지않을까?

구현 2

참조: https://velog.io/@kjw4840/java-spring-Custom-annotation을-만들어보자

  • 공통되는 유저인증부분에 대해 중복코드를 줄일 수 있는 방법은 없을까?

Annotation

  • @CheckAliveUser
    현재 권한 체크가 필요한 메서드에 쓸 에너테이션
    (현재 로직은 email에잇는 유저=라이브유저인지체크)
1
2
3
4
5
//커스텀 에너테이션 선언
@Retention(RetentionPolicy.RUNTIME) //런타임시에 체크해야하므로
@Target(ElementType.METHOD) //method에서 선언
public @interface CheckAliveUser {
}

CheckAliveUserConfig

  • @CheckAliveUser 해당 에너테이션 있는 곳에서는 메서드 실행이전에 현재 email이 db에서 live유저인지 체크 후 핵심메서드 실행됨.
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
@Slf4j
@Component
@Aspect
@RequiredArgsConstructor
public class CheckAuthConfig {

private final UserRepository userRepository;

@Pointcut("@annotation(hello.postpractice.aop.CheckAliveUser)") //해당 에너테이션이 있는경우 하위 함수실행
private void check(){}

@Before("check()")
public void before(){
Object principal = SecurityContextHolder.getContext().getAuthentication().getPrincipal();
String email = ((User) principal).getEmail();
userRepository.findByEmail(email).orElseThrow(() -> new UserNotFoundCException("유저없음"));
}

}
CATALOG
  1. 1. 공통적인 메서드 추출 방법(AOP)
  2. 2. 왜?
  3. 3. Aop
    1. 3.1. AspectJ 사용되는 에너테이션 설명
    2. 3.2. 구현 예시(로그남기기 기능)
      1. 3.2.1. 프로젝트 설명
      2. 3.2.2. 의존성 추가
      3. 3.2.3. @EnableAspectJAutoProxy
      4. 3.2.4. @Aspect 클래스 만들기
    3. 3.3. 로직 설명
      1. 3.3.1. log.info로 response찍힌모습
  4. 4. 커스텀 에너테이션 작성
    1. 4.1. Spring Annotation의 원리
    2. 4.2. Custom 에너테이션 만들기
      1. 4.2.1. LogExclusion
      2. 4.2.2. LogConfig
    3. 4.3. 커스텀 에너테이션의 단점
  5. 5. AOP활용하는 방법
  6. 6. 해당 비즈니스의 확장 가능성 점검
  7. 7. 구현 2
    1. 7.1. Annotation
    2. 7.2. CheckAliveUserConfig