Spring AOP를 내 맘대로 활용해봤습니다.

konu·2023년 12월 18일
0

스프링

목록 보기
2/8
post-thumbnail

 

1. AOP

AOP는 Aspect Oriented-Programming의 준말이다.
aspect, 즉 관점은 여기서 비즈니스 로직과 관계없지만
여러 메서드에서 공통적으로 거쳐야 하는 로직을 뜻한다.

가장 대표적인 예는
로그를 찍는 로직이라든가
데이터 트랜잭션을 담당하는 로직이 될 수 있겠다.

 

1) Advice

aspcet의 구현체를 뜻한다.

2) Join Point

advice를 적용하는 시점을 뜻하며,
메서드 실행 전후나 생성자 호출 등을 포함한다.

3) Pointcut

어떤 join point에 advice를 적용할지 표현하는 정규식이다.

 

 

2. 내 맘대로 Spring AOP

위에서 설명했듯,
AOP는 주로 여러 메서드에서 비즈니스 로직과 별개로 거쳐야 하는
부가적인 로직을 공통적으로 묶어서 제공하기 위한 기법이다.

그러나 나는 이번 캡스톤 디자인 프로젝트에서
AOP에 비즈니스 로직을 담아보았다.

 

1) Notification

@AllArgsConstructor
@NoArgsConstructor
@Builder
@Getter
@Entity
public class Notification extends BaseEntity implements Comparable<Notification> {

    /* DB id */
    @Id @GeneratedValue
    private long id;


    /* 연관관계 */
    @ManyToOne(fetch = FetchType.LAZY)
    private Member to;

    @ManyToOne(fetch = FetchType.LAZY)
    private Member from;

    private Type type;


    /* 메서드 */
    public enum Type {
        COMMENT(COMMENT_TYPE),
        LIKE(LIKES_TYPE),
        FOLLOW(FOLLOW_TYPE),
        UNFOLLOW(UNFOLLOW_TYPE);

        private final String name;

        Type(String name) {
            this.name = name;
        }

        public String getName() {
            return name;
        }
    }


    @Override
    public int compareTo(Notification other) {
        if (other == null)
            return BIGGER;

        return (-1) * this.getCreatedAt().compareTo(other.getCreatedAt());
    }
}

 

Notification 엔티티는 '알림' 기능을 위한 엔티티다.

내부에 정의한 Enum을 보면 알겠지만,
회원이 연관된 댓글, 좋아요, 팔로우, 언팔로우 등의 이벤트가 있을 때마다
알림 인스턴스를 만들기로 했다.

그런데 문제는 댓글, 좋아요, 팔로우, 언팔로우 등이
모두 각각의 서비스 클래스에 흩어져 있다는 사실이었다.

때마침 AspectJ가 떠올랐고,
나는 스프링 프로젝트를 사용중이었기 때문에
좀 더 간편한 스펙을 가진 Spring AOP를 활용해서 알림 기능을 구현했다.

 

2) NotificationService

@RequiredArgsConstructor
@Transactional
@Aspect
@Service
public class NotificationService {

    private final NotificationRepository notificationRepository;

 
언뜻 보면 서비스 클래스 선언부는 아주 평범하다.
@Service 붙이고 @RequiredArgsConstructor로 리포지토리 입력받고 있다.

하나 특별한 게 있다면
@Aspect를 붙였다는 점.

스프링에서는 @Aspect와 컴포넌트 선언을 통해
AOP 클래스를 정의할 수 있다.
(위에서는 @Service로 @Component를 대체한다)

 

@AfterReturning(
            pointcut = "execution( * nora.movlog.service.user.LikesService.add(..))",
            returning = "likes"
)
public void joinLikesFrom(Likes likes) {
        notificationRepository.save(Notification.builder()
                .to(likes.getPost().getMember())
                .from(likes.getMember())
                .type(LIKE)
                .build());
}

 

@AfterReturning은 join point를 명시하기 위함이며,
advice를 적용할 메서드가 반환한 이후에 호출된다.

알림은 좋아요, 댓글, 팔로우와 언팔로우의 로직이 모두 종료된 이후에
생성되는 것이 맞기 때문에 @AfterReturning이 찰떡이다.

(참고로 @Around가 가장 빈번히 사용되는데,
타겟 메서드가 실행되는 시점을 aspect 메서드 내에서 결정할 수 있기 때문에
이점이 많다.)

 

pointcut = "execution( * nora.movlog.service.user.LikesService.add(..))"

 

위 부분은 위에서 언급한 pointcut을 명시한 @AfterReturning의 속성 부분이다.
패키지를 타고 타고 LikesService의 add 메서드가 호출될 때 적용됨을 나타낸다.

 

returning = "likes"

 

위 부분은 나도 이번에 적용하면서 처음 알게 되었는데,
@AfterReturning의 경우 반환 이후에 호출되기 때문에
타겟 메서드의 반환값을 특정한 이름(likes)으로 받아서
aspect 메서드의 시그니쳐에 담을 수 있다.

 

@AfterReturning(
            pointcut = "execution( * nora.movlog.service.user.LikesService.add(..))",
            returning = "likes"
)
public void joinLikesFrom(Likes likes) {
        notificationRepository.save(Notification.builder()
                .to(likes.getPost().getMember())
                .from(likes.getMember())
                .type(LIKE)
                .build());
}

 

다시 돌아와서 보면,
시그니쳐 부분에서 Likes 객체를 받는 것을 알 수 있을 것이다.
(참고로 Likes 객체는 '좋아요'를 담당한다)

그래서 우리는 좋아요 객체에 담긴
<1> 좋아요 받는 게시물의 주인 회원에 대한 정보
<2> 좋아요를 보내는 회원에 대한 정보
를 뽑아낼 수 있게 되었다.

 

 

@AfterReturning(
            pointcut = "execution( * nora.movlog.service.user.CommentService.write(..))",
            returning = "comment"
    )
    public void joinCommentFrom(Comment comment) {
        notificationRepository.save(Notification.builder()
                .to(comment.getPost().getMember())
                .from(comment.getMember())
                .type(COMMENT)
                .build());
    }
@AfterReturning(
            pointcut = "execution( * nora.movlog.service.user.MemberService.follow(..))",
            returning = "members"
    )
    public void joinFollowFrom(Map<String, Member> members) {
        notificationRepository.save(Notification.builder()
                .to(members.get(FOLLOWER))
                .from(members.get(FOLLOWING))
                .type(FOLLOW)
                .build());
    }
@AfterReturning(
            pointcut = "execution( * nora.movlog.service.user.MemberService.unfollow(..))",
            returning = "members"
    )
    public void joinUnfollowFrom(Map<String, Member> members) {
        notificationRepository.save(Notification.builder()
                .to(members.get(FOLLOWER))
                .from(members.get(FOLLOWING))
                .type(UNFOLLOW)
                .build());
    }

 

위에서 소개한 방식과 거의 동일하게
댓글과 팔로우, 언팔로우에 대한 Advice도 정의했다.

 

 


(그래서?)

 

3. 결론

결론적으로,
비즈니스 로직에 동일한 비즈니스 로직을 여러 군데
끼워넣기 싫어서 이러한 우회 방식을 생각해냈는데
테스트는 모두 어렵지 않게 통과했다.

<토비의 스프링>에서나 인터넷 블로그에서는 주로
AOP를 메서드 실행 시간을 측정하거나 트랜잭션 로직을 빼돌리기 위해 사용했으나
조금 다르게 접근한 게 읽는 이에게 신선할런지 하여 적어보았다.

(만약 이러한 적용 방식에 치명적인 문제점이 있을 수 있다면
댓글로 알려주세요 !)

profile
日日是好日

0개의 댓글