본문 바로가기
웹 개발/Spring

[Spring] 스프링 이벤트를 활용하여 서비스 간의 의존성 제거하기

by 어썸오184 2022. 2. 5.
728x90
반응형

작년 12월 19일에 진행한 유스콘에서 스프링 이벤트 처리에 대한 세션이 있었는데요, 내용이 너무 좋아서 포스팅으로 정리하며 제대로 이해해보고자 합니다.

반태형님이 지식 공유를 해주셨고 예제 코드는 여기서 확인하실 수 있습니다.
스프링 공식 문서에서 이벤트에 관한 내용을 참고하고 싶으시면 여기를 보시면 됩니다.

애플리케이션을 만들다보면 아래처럼 서로 다른 서비스 간에 의존성이 발생하는 경우가 생깁니다.

@Service
public class UserService {

    private final UserRepository userRepository;
    private final AdminService adminService;
    private final EmailService emailService;
    private final CouponService couponService;

    public UserService(UserRepository userRepository, AdminService adminService, EmailService emailService, CouponService couponService) {
        this.userRepository = userRepository;
        this.adminService = adminService;
        this.emailService = emailService;
        this.couponService = couponService;
    }

    ...
}

UserService는 새로운 회원이 가입을 할 경우, 회원을 생성한 후 어드민에게 알림을 보내고 쿠폰 서비스에 해당 회원을 등록한 후 회원에게 이메일을 보냅니다. 이 과정에서 AdminService, EmailService, CouponService와의 의존성이 생기게 됩니다.

public void create(UserRequest userRequest) {
    User user = new User(
            userRequest.getName(),
            userRequest.getEmail(),
            userRequest.getPhoneNumber()
    );
    userRepository.save(user);

    adminService.alarm(user.getName());
    couponService.register(user.getEmail());
    emailService.sendEmail(user.getEmail());
}

@Service
@Slf4j
public class AdminService {
    public void alarm(String username) {
        log.info("어드민 서비스 : {}님이 회원으로 등록되었습니다", username);
    }
}

이렇게 서비스 간에 의존성이 생기게 될 경우 문제가 발생할 수 있습니다. 예를 들어 AdminService에서 어떤 기능을 수행하기 위해서 UserService를 참조하게 되면 AdminService와 UserService 사이에 순환참조가 발생하여 애플리케이션을 빌드할 수 없게 됩니다(스프링 부트 2.6부터 순환참조 기본 정책이 금지로 변경되었습니다).

이 경우 이벤트를 활용하여 서비스간의 결합도를 낮출 수 있습니다.

그림으로 도식화하면 이렇게 표현해볼 수 있을 것 같습니다.

위처럼 UserService가 각 서비스에 의존하여 직접 요청을 보내는 형태에서

 

위처럼 UserService가 이벤트를 생성하여 ApplicationEventPublisher에게 넘겨주고 이벤트를 발행하면 ApplicationContext가 ApplicationListener를 구현한 빈들에게 notify를 해주는 형태로 바뀌게 됩니다. 서비스 간의 의존성이 사라진 것을 확인할 수 있죠.

Spring Event

스프링은 이벤트 핸들링을 위해 ApplicationEvent 클래스와 ApplicationEventListner 인터페이스를 제공합니다. 만약 빈이 ApplicationEventListener를 구현하고 있다면 이벤트가 발행될 때마다 ApplicationContext가 해당 빈에게 notify를 해주게됩니다. 내부적으로는 옵저버 패턴을 사용해 구현되어있다고 합니다.

스프링은 ContextStartedEvent , ContextClosedEvent 등의 몇가지 빌트인 이벤트를 제공합니다. 커스텀한 이벤트를 생성하기 위해서는 ApplicationEvent를 상속한 이벤트 구현체를 만들어주면 됩니다.

import org.springframework.context.ApplicationEvent;

public class UserAdminEvent extends ApplicationEvent {

    private String username;

    public UserAdminEvent(Object source, String username) {
        super(source);
        this.username = username;
    }

    public String getUsername() {
        return username;
    }
}

이벤트를 발행하기 위해서는 해당 서비스에서 ApplicationEventPublisher를 주입받아 publishEvent() 메서드를 호출합니다. 이때 발행하고자 하는 이벤트를 인자로 넘겨줍니다 (예시를 간단하게 하기 위해 어드민 알림 서비스만 구현했습니다).

import org.springframework.context.ApplicationEventPublisher;

@Service
public class UserService {

    private final UserRepository userRepository;
    private final ApplicationEventPublisher publisher;

    ...

    public void create(UserRequest userRequest) {
        User user = new User(
                userRequest.getName(),
                userRequest.getEmail(),
                userRequest.getPhoneNumber()
        );
        userRepository.save(user);

        publisher.publishEvent(new UserAdminEvent(this, user.getName()));
    }

    ...
}

이벤트를 Listen하고 동작을 수행하려면 ApplicationEventListner를 구현한 클래스를 빈으로 등록하고 onApplicationPublish() 메서드를 구현하면 됩니다.

import org.springframework.context.event.EventListener;

@Component
@Slf4j
public class AdminServiceEventListener implements ApplicationListener<UserAdminEvent> {
    @Override
    public void onApplicationEvent(UserAdminEvent event) {
        log.info("어드민 서비스: {}님이 회원으로 등록되었습니다.", event.getUsername());
    }
}

이제 UserService가 ApplicationEventPublisher에게 이벤트를 생성하여 넘겨주고 publisher가 이벤트를 발행하면 ApplicationContext가 빈으로 등록된 Listener를 notify합니다.

같은 기능을 수행하면서도 서비스간의 결합도를 낮춘 구조가 완성되었습니다.

Spring 4.2부터는 애너테이션을 이용해 더욱 간단하게 구현할 수 있습니다. 빈으로 등록된 컴포넌트에 원하는 동작을 구현한 후 @EventListener 애너테이션만 붙여주면 됩니다. 더이상 ApplicationListener를 구현하지 않아도 됩니다.


@Component
@Slf4j
public class AdminServiceEventListener {

    @EventListener
    public void alarm(UserAdminEvent event) {
        log.info("어드민 서비스: {}님이 회원으로 등록되었습니다.", event.getUsername());
    }
}

이벤트 구현체 또한 ApplicationEvent를 상속하지 않아도 됩니다.

public class UserAdminEvent {

    private String username;

    public UserAdminEvent(String username) {
        this.username = username;
    }

    public String getUsername() {
        return username;
    }
}

더욱 놀라운 것은 @Async 애너테이션만 붙여주면 스프링이 알아서 비동기로 해당 동작을 수행해줍니다.

@Component
@Slf4j
public class AdminServiceEventListener {

    @EventListener
    @Async
    public void alarm(UserAdminEvent event) {
        log.info("어드민 서비스: {}님이 회원으로 등록되었습니다.", event.getUsername());
    }
}

이렇게 스프링이 지원하는 이벤트를 활용하여 서비스 간의 결합도를 낮추는 방법을 알아봤습니다. 애너테이션을 이용하면 어렵지 않게 구현가능하며 비동기도 쉽게 구현할 수 있습니다. 관리할 클래스(이벤트 구현체)가 늘어난다는 단점이 있지만 상황에 따라 적절히 활용하면 유연한 애플리케이션을 구현하는데 도움이 될 것 같습니다.

728x90
반응형

댓글