작년 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());
}
}
이렇게 스프링이 지원하는 이벤트를 활용하여 서비스 간의 결합도를 낮추는 방법을 알아봤습니다. 애너테이션을 이용하면 어렵지 않게 구현가능하며 비동기도 쉽게 구현할 수 있습니다. 관리할 클래스(이벤트 구현체)가 늘어난다는 단점이 있지만 상황에 따라 적절히 활용하면 유연한 애플리케이션을 구현하는데 도움이 될 것 같습니다.
'웹 개발 > Spring' 카테고리의 다른 글
Singleton Scope 빈에 Request Scope 빈을 주입받고 싶다면… (Scoped Proxy Bean) (0) | 2022.07.13 |
---|---|
[스프링 시큐리티] 스프링 시큐리티의 구조를 살펴보자! (2) | 2021.11.23 |
[SpringMVC] 스프링 MVC의 구조를 이해해보자 (3/3) (0) | 2021.07.30 |
[SpringMVC] 스프링 MVC의 구조를 이해해보자 (2/3) (0) | 2021.07.09 |
[SpringMVC] 스프링 MVC의 구조를 이해해보자 (1/3) (0) | 2021.07.09 |
댓글