새소식

스프링

커스텀 어노테이션과 AOP 사용해서 공통기능 처리하기

  • -

 

작년 이맘때쯤에 스프링을 처음 배우면서 AOP를 사용해 봤던 기억이 났습니다.

 

 

경북대 BE_도기헌 6주차 과제(3단계) by Dockerel · Pull Request #335 · kakao-tech-campus-2nd-step2/spring-gift-point

안녕하세요! 이번 3단계 과제는 포인트 추가하기였습니다. 과제 요구 조건에 따라 관리자 페이지에서 포인트 충전 기능 및 주문 시 포인트 사용 기능 추가하였습니다. 이때 admin만 포인트 충전을

github.com

이게 맞나 싶은 AOP 사용

 

사실 뭐 틀린게 어딨겠냐 싶긴 한데

 

 

뭔가... 뭔가네요. 가독성도 별로고 ProductService와 강한 결합도를 가지고 있어 나중에 유지보수하기도 힘들어질 것 같습니다.

 

하여튼 이번에 AOP를 사용하게 된 계기는 이러합니다.

 

현재 요청을 넣은 회원이 챗봇 히스토리의 주인이 맞는지 확인하는 로직이 꽤 많이 반복되었거덩요.

 

그냥 메서드 추출을 통해 분리할까 하다가 의도를 명확하게 드러내고 재사용 가능하게 커스텀 어노테이션 + AOP 조합으로 구현해보기로 했습니다.

 

 

시작

 

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface CheckHistoryOwner {
}

 

일단 시원하게 커스텀 어노테이션 한사바리 갈겨줬습니다. 메서드에 붙일 거니까 METHOD로 설정해주시고요, 런타임시 검사할거니까 RetentionPolicy는 RUNTIME으로 설정해줍시다.

 

@Aspect
@Component
@RequiredArgsConstructor
public class HistoryOwnerCheckAspect {

    private final HistoryRepository historyRepository;

    @Before("@annotation(knu_chatbot.annotation.CheckHistoryOwner)")
    public void checkHistoryOwner(JoinPoint joinPoint) {

 

다음엔 AOP를 구현해줘야겠죠. 일단 위에서 정의한 커스텀 어노테이션이 붙은 메서드 실행 전에 AOP가 실행될 수 있도록 @Before로 지정해줍시다.

 

Long memberId = findParameterValue(joinPoint, "memberId", Long.class);
Long historyId = findParameterValue(joinPoint, "historyId", Long.class);

 

다음엔 메서드에서 파라미터 값들을 꺼내와줬습니다. 실제 구현코드는

 

private <T> T findParameterValue(JoinPoint joinPoint, String parameterName, Class<T> type) {
    MethodSignature signature = (MethodSignature) joinPoint.getSignature();
    String[] parameterNames = signature.getParameterNames();
    Object[] args = joinPoint.getArgs();

    for (int i = 0; i < parameterNames.length; i++) {
        if (parameterNames[i].equals(parameterName)) {
            return type.cast(args[i]);
        }
    }

    throw new KnuChatbotException("AOP: @" + signature.getMethod().getName() + " 메서드에서 '" + parameterName + "' 파라미터를 찾을 수 없습니다.", INTERNAL_SERVER_ERROR);
}

 

파라미터 이름들로 for문 돌려줬습니다. 생각보다 단순하죠?

 

그리고 그 다음은 꺼내온 값들로 원하는 로직 돌려주시면 됩니다.

 

@Aspect
@Component
@RequiredArgsConstructor
public class HistoryOwnerCheckAspect {

    private final HistoryRepository historyRepository;

    @Before("@annotation(knu_chatbot.annotation.CheckHistoryOwner)")
    public void checkHistoryOwner(JoinPoint joinPoint) {
        Long memberId = findParameterValue(joinPoint, "memberId", Long.class);
        Long historyId = findParameterValue(joinPoint, "historyId", Long.class);

        History historyCheck = findHistoryById(historyId);

        Long historyOwner = historyCheck.getMember().getId();
        if (!historyOwner.equals(memberId)) {
            throw new KnuChatbotException("히스토리 주인이 아닙니다.", HttpStatus.FORBIDDEN);
        }
    }

    private <T> T findParameterValue(JoinPoint joinPoint, String parameterName, Class<T> type) {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        String[] parameterNames = signature.getParameterNames();
        Object[] args = joinPoint.getArgs();

        for (int i = 0; i < parameterNames.length; i++) {
            if (parameterNames[i].equals(parameterName)) {
                return type.cast(args[i]);
            }
        }

        throw new KnuChatbotException("AOP: @" + signature.getMethod().getName() + " 메서드에서 '" + parameterName + "' 파라미터를 찾을 수 없습니다.", INTERNAL_SERVER_ERROR);
    }

    public History findHistoryById(Long historyId) {
        return historyRepository.findById(historyId)
                .orElseThrow(() -> new KnuChatbotException("히스토리가 존재하지 않습니다.", HttpStatus.NOT_FOUND));
    }
}

 

첨에 이 방법 생각해냈을 때는 진짜 개쩐다고 생각했는데 막상 하고나니까 간단하네요.

 

아무튼 공통로직이 여러곳에서 사용되면서 그 부분이 실제 동작과는 무관하면 이걸 좀 유식한 말로 Separation of Concerns(SoC)라 하던데 어쨌든 그럼 AOP로 빼보시면 좋을 것 같습니다.

 

오늘은 여기까지

 

Contents

포스팅 주소를 복사했습니다

이 글이 도움이 되었다면 공감 부탁드립니다.