본문으로 바로가기

728x90

이번 글에서는 Spring Framework 에서 Event를 사용하는 방법에 대해서 설명합니다. 필자는 이번에 개발중인 애플리케이션에서 고객사마다 커스터마이징 되는 부분을 어떻게 처리할 것인가에 대한 고민이 있었습니다. 기본적으로는 중심코드를 기반으로 이벤트를 처리하고 확장 또는 커스터마이징되는 코드에 대해서는 이벤트로 확장을 처리하도록 구현되었습니다.

 

본 예제는 스프링 프레임워크 4.2 이상에서 구현된 코드입니다.

 

이벤트 처리의 장점

- 처리하는 코드의 코드 일원화

- 비즈니스 로직과 부가적인 로직의 분리

- 커스터마이징에 대한 대응

 

1. 이벤트 생성하기

이벤트 인터페이스(가상클래스)

본 가상클래스는 Map 기반으로 간단한 이벤트를 구현하였습니다. Map을 사용한 이유는 추가적인 데이터를 동적으로 넣을 수 있어 확장이 용이하기 때문입니다.

import java.util.Map;

public abstract class SimpleEvent {
    private final Object source;		// 이벤트발생주체/객체
    private final Map<String, Object> data;	// 이벤트 데이터

    public SimpleEvent(Object source, Map<String, Object> data) {
        this.source = source;
        this.data = data;
    }

    public Object source() {
        return this.source;
    }

    public Map<String, Object> data() {
        return this.data;
    }
}

 

이벤트 구현 클래스

아래의 코드는 애플리케이션에서 여러 Controller에서 회원관련 코드의 처리를 이벤트를 통하여 일원하기 위해 구현된 코드의 일 부분입니다. 간단하게 설명하면 회원은 다음과 같은 상태로 회원가입을 처리합니다.

회원 아이디/이메일 검증 전 ->  이메일 검증 후 -> 회원정보 등록 및 관리자 승인 요청 > 관리자 승인/거부 > 회원가입 완료
import java.util.Map;

public class RegisterEvent extends SimpleEvent {
    public static boolean DEFAULT_LISTENER = true;		// 마스터 코드의 이벤트 실행여부

    public synchronized static void setState(boolean state) {
        RegisterEvent.DEFAULT_LISTENER = state;
    }

    public RegisterEvent(Object source, Map<String, Object> data) {
        super(source, data);
    }
}

 

2. 이벤트 수신하기

이벤트를 수신하는 클래스는 컴포넌트로 등록하여 초기에 스프링프레임워크의 빈에 등록되어 처리되도록 합니다.

@Component 어노테이션을 추가하여 컴포넌트로 등록합니다.

@PostConstruct 어노테이션을 지정하여 마스터 코드의 이벤트 수행을 방지하도록 합니다.

@EventListener 어노테이션을 지정하여 이벤트를 수신하도록 설정합니다.

@Async 어노테이션을 추가하여 이벤트가 비동기적으로 처리되게 합니다.

import javax.annotation.PostConstruct;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

@Component
@RequiredArgsConstructor
public class CustomEventListener {
    private final Environment environment;

    @PostConstruct
    public void init() {
        // 확장 이벤트 처리이므로 기본이벤트 처리가 구현되어 있을 경우에는 기본 실행을 하지 않도록 처리
        if(RegisterEvent.DEFAULT_LISTENER) RegisterEvent.setState(false);
    }
    
    @Async
    @EventListener
    public void onRegisterEvent(@NonNull final RegisterEvent event) {
        // 특화된 내용으로 프로파일이 지정한 것이 아닐 경우에는 실행안함
        if(!Arrays.asList(this.environment.getActiveProfiles()).contains("profile name")) return;


        Map<String, Object> data = event.data();
        if(data == null || !data.containsKey("REGISTER")) return;
        
        ...... 중간 생략......
        
        이 부분에 데이터 처리로직을 구현합니다.
        
        ...... 중간 생략......
    }
}

 

3. 이벤트 발생하기

ApplicationEventPublisher를 멤버변수에 추가하고 AutoWired되도록 합니다. 코드에는 생략되었으나 @RequiredArgsConstructor 어노케이션을 객체에 추가하여 자동으로 등록되도록 하였습니다.

이벤트의 데이터를 Map을 사용하는 이유는 다양하게 데이터를 추가하도록 하기 위함입니다. 위의 회원상태에 따라 Map에 상태값을 추가하여 상태에 따른 부가 로직 수행이 가능합니다.

    // 이벤트를 발생시키려는 코드의 멤버 변수로 ApplicationEventPublisher를 추가합니다.
    private final ApplicationEventPublisher eventPublisher;            
    
    // 이벤트처리를 원하는 코드에서 아래와 같이 코드를 추가합니다.
    eventPublisher.publishEvent(new RegisterEvent(this, Map.of("REGISTER", registerDto)));

 

4. 기타

본 코드에서는 이벤트 처리를 하나의 리스너(마스터 또는 커스터마이징)에서만 실행되로록 하였습니다. 필요에 따라서 Scope 관련 코드를 추가하여 실행 범위를 지정할 수 있도록 할 수 있습니다.

728x90