스프링 부트로 프로젝트를 진행하다 보면, 순환 참조라는 문제를 맞닥뜨리게 될 때가 존재한다. 아래가 그 간단한 예시이다.
// 회원
class MemberService {
private OrderService orderService;
public MemberService(){
orderService = new OrderService();
}
// 이하생략...
}
// 주문
class OrderService {
private MemberService memberService;
public OrderService(){
memberService = new MemberService();
}
}
// 순환 참조로 인해 스택 오버플로우 오류 발생
public class Main {
public static void main(String[] args) {
MemberService memberService = new MemberService();
}
}
현재 회원 서비스 클래스(이후 클래스 생략)가 주문 서비스를 참조하고 있고, 주문 서비스가 회원 서비스를 참조하고 있는 상황이다.
이때, 둘 중 하나의 객체를 생성하려고 하면, 위의 코드는 회원 서비스의 객체를 새로 생성하고 있는데, 회원 서비스 객체를 생성하면, 그 안에서 새로운 주문 서비스의 객체도 생성될 것이다. 그런데 주문 서비스 객체 안에도 새로운 회원 서비스 객체를 생성하는 코드가 존재하기 때문에,
회원 서비스 생성, 주문 서비스 생성, 회원 서비스 생성, 주문 서비스 생성...
이런식으로 무한 반복이 일어나서 스택 오버플로우가 발생하게 될 것이다.
이러한 문제를 해결하기 위한 방법에는 여러가지가 존재하는데, 나는 스프링 이벤트라는 방식을 채택하였다.
스프링 이벤트를 사용하여 순환 참조를 해결한 예시를 하단에 작성해보겠다.
import org.springframework.context.ApplicationEvent;
public class OrderEvent extends ApplicationEvent {
private String username;
public OrderEvent(Object source, String username) {
super(source);
this.username = username;
}
}
"이벤트" 라는 단어가 생소하다면, React 와 같이 프론트 엔드 개발을 할 때, 버튼을 클릭했을 때 발생하는 OnClickEvent와 같은 개념이 존재하지 않았는가? 이때 사용된 Event라는 개념과 동일한 의미를 가진다고 보면 된다.
여하튼 OrderEvent 클래스는 ApplicationEvent 라는 클래스를 상속받게 된다. 해당 클래스는 스프링 부트에서 이벤트를 사용하기 위해 상속받아야 하는 클래스이다.
@Service
@Slf4j
public class OrderService {
// ...
@Async
@EventListener
public void onOrderEvent(OrderEvent event) {
// 매개변수인 OrderEvent가 발생했을 때 실행되는 로직
log.warn("OrderEvent가 감지되었습니다 : " + event.getUsername());
// 주문 처리 로직...
}
}
이때, 이벤트를 감지하는 것을 스프링 부트에서는 "이벤트를 리스닝한다." 라고 칭한다. 스프링 부트의 코드에서 이벤트 리스너라는 어노테이션을 보게 된다면, 해당 어노테이션 아래의 코드 블록은 이벤트가 발생했을 때 실행될 자세한 로직을 구현하고 있다고 생각하면 된다.
위 코드의 onOrderEvent 와 같이 이벤트에 대한 자세한 로직을 구현하고 있는 메서드를 일반적으로 "이벤트 핸들러" 라고 부른다.
추가적으로, 일반적으로 React를 다루어본 사람이라면 많이 봤을, @Async 라는 어노테이션을 상단에 붙여주면 해당 스프링 이벤트가 비동기적으로 실행된다.
만약 이벤트와 이벤트 핸들러가 각각 여러개가 존재한다면, 어떤 이벤트가 어떤 이벤트 핸들러와 매핑되고 있는 것인지에 대한 의문이 생길수도 있다.
이때 이벤트와 이벤트 리스너는 "타입" 을 통하여 매핑이 되게 된다.
즉, 타입을 통하여 특정 이벤트가 어떠한 이벤트 리스너에 의하여 처리가 될지 결정이 되게 되는 것이다. 위 코드에서는 주문 서비스안에 정의되어 있는 이벤트 리스너가 OrderEvent 타입을 매개변수로 가지고 있다.
순환 참조가 발생했을 때, 한 쪽에 이벤트 구독, 즉 이벤트 리스너를 선언해 놓았다면, 다른 한쪽에는 이벤트 발행에 대한 코드를 작성해 놓아야 한다. 현실에서도 신문사에서 신문을 발행을 해주어야, 독자들이 이를 구독을 할 수가 있지 않나? 스프링 이벤트도 이와 마찬가지라고 생각하면 된다.
주문 서비스 쪽에서 이벤트 리스너를 선언해 놓았기 때문에, 반대 쪽인 회원 서비스에서는 이벤트 발행에 대한 코드만을 작성하면 된다.
@Service
public class MemberService {
// 이벤트 발행에 대한 의존성을 생성자 주입 방식을 통해 주입
private final ApplicationEventPublisher eventPublisher;
public MemberService(ApplicationEventPublisher eventPublisher) {
this.eventPublisher = eventPublisher;
}
public void placeOrder(String username) {
// 주문 관련 로직
// ...
// 주문 이벤트 발행
OrderEvent orderEvent = new OrderEvent(this, username);
eventPublisher.publishEvent(orderEvent);
}
}
이벤트를 발행한다는 것에 대해서 풀어서 설명하자면, 회원 서비스에서 이벤트 발행을 한다는 말은 즉, 회원이 주문을 할 때 실행되는 로직을 직접적으로 주문 서비스를 참조해서가 아닌, 주문 이벤트를 통해서 구현한다는 것을 의미한다.
위 코드에서 회원 서비스는 이벤트 발행에 대한 의존성을 주입받고 있다.
이벤트 발행에 대한 의존성을 주입받은 회원 서비스는 이벤트를 발행시키는 것이 가능해진다.
추가적으로, 당연한 것이지만 이벤트 발행자는 이벤트를 발행만 할 뿐, 누가 해당 이벤트를 처리할 것인지는 알지 못하며, 알 필요조차 없다. 자세한 로직은 주문 이벤트 쪽에서 타입을 통하여 해당 이벤트가 이벤트 핸들러와 매핑되어, 이벤트 핸들러 쪽에서 실행된다.
사실 순환 참조를 해결하는 방법에는 스프링 이벤트 이외에도 좀 더 간단한 여러가지 방법들이 존재한다.
그러나 내가 그 중에서 스프링 이벤트를 채택한 이유는 가장 낮은 결합도에 존재한다. 다른 방식들은 순환 참조라는 상황을 유지하며 예외처리를 하는 방식으로 순환 참조가 발생시키는 문제 상황을 대처하나, 스프링 이벤트는 아예 순환 참조가 발생하는 두 클래스를 분리하는 것이 가능하기에 가장 낮은 결합도를 가지고 있다. 결합도와 유연성은 반비례하기 때문에, 가장 낮은 결합도를 가지는 방식을 채택하는 것이 더욱 유연한 코드를 작성할 수 있는 길이라고 생각하여 해당 방식을 채택한 것이다.
비유를 하자면, 순환 참조라는 일인용 화장실이 존재한다.
그런데 두 사람이 화장실을 사용하고 싶어한다.
다른 방식들은 한 명은 세면대를 쓰고, 한 명은 변기를 쓰게 하여 일인용 화장실에 두명을 넣는 방식이라면,
스프링 이벤트는 간이 화장실을 추가적으로 만들어 한 명을 간이 화장실에 보내는 것이라고 할 수 있다.
당연히 화장실에서 볼일을 보는데, 옆에서 다른 사람이 손을 씻고, 세수를 하고 있는 것보다는 서로 다른 화장실을 사용하는 것이 더 편하고 자유롭게 화장실을 사용할 수 있지 않겠는가?
(아래 두 이미지 출처 : https://www.youtube.com/watch?v=TJUIkLFpgGo)
위의 구조는 직접적으로 다른 서비스의 객체를 호출하는 구조이다.
위와 같은 구조에서 스프링 이벤트를 사용하는 구조로 구조를 변경한다면 아래와 같이 구조가 변하게 된다.
A라는 클래스는 B라는 객체를 생성하는 것이 아니라 단순하게 이벤트를 발행하며, B에서는 이 이벤트를 구독, 즉 Listen 하여 알맞은 로직을 실행한다.
이때 로직이 구현되어 있는 메서드 위에는 이벤트 리스너(@EventListener)라는 어노테이션을 붙이게 되고, 추가적으로 비동기(@Async)설정을 하는 것이 가능하다. 또한 이벤트와 이벤트 핸들러는 타입을 통하여 매핑된다.