본문 바로가기

다우 & Web/SPRING

Custom annotation을 이용한 Signal 처리

안녕하세요

개발 세끼의 '첫끼'입니다.

서버 개발을 하다 보면, 아주 예외적인 케이스를 위해서 본인만 호출할 수 있는 api를 만들곤 합니다.

하지만,, 이런 API가 외부로 노출 되었을 경우를 생각한다면, 굉장히 큰 취약점을 스스로 노출시키는 것과 같습니다.

그렇다면, 이런 API 노출 이외에 ‘서버 데몬’과 ‘서버 운영자’간의 대화는 어떤 방식으로 이뤄질 수 있을까요?

저는 오래전에 즐겨 사용 했었던 'Signal Programming' 에서 찾아보려 합니다.

java에서도 이런 os의 시그널을 처리할 수 있는 Handler를 제공해주고 있는데, 저는 불편한 Signal 처리를 Spring Framework위에서 아주 자연스럽고, Spring 스럽게 커스텀해서 쓰는 방법에 대해서 글을 적어볼까 합니다.

아래 예제는, 특정 Signal이 Process에 전달되었을 때 Config에 저장 된 외부 URL을 다른 URL로 변경하는것을 다룹니다.

  1. Signal Execute Annotation 추가

@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) 
@Documented public 
@interface SignalExecute 
{ 
    String signalName();
}
  1. Signal이 전달 되었을때 호출되는 Method 위에 어노테이션을 추가

@Component
@Slf4j
public class ApplicationConfig {

    private String removeServerUrl = "http://remote.server.com";

    @SignalExecute(signalName = "USR2")
    public void changeUrl() {
        this.removeServerUrl = "http://remote2.server.com";
    }

}

이제 이 글을 읽는 분들이 어떤 걸 만들지 대충 감을 잡으셨을 것 같은데요
특정 signal이 전달되었을 때, Spring Component들 안에 있는 SignalExecute라는 Annotation이 붙어있는 Method를 모두 호출시켜주려고 합니다.

  1. 간단하게 class의 method에서 annotation을 검색할 수 있는 utility class를 작성

@AllArgsConstructor
public class AnnotationScanner {

    private Object scanObject;

    public List<Method> filterMethod(Class annotationClass) {
        Method[] methods = this.scanObject.getClass().getMethods();
        List<Method> signalMethod = new ArrayList<>();

        for (Method method : methods) {
            if (method.getAnnotation(annotationClass) != null) {
                signalMethod.add(method);
            }
        }
        return signalMethod;
    }

    public boolean hasAnnotationMethod(Class annotationClass) {
        return !this.filterMethod(annotationClass).isEmpty();
    }
}
  1. 아래와 같은 코드로 USR2를 핸들링할 수 있고, SignalExecute annotation 이 달린 모든 method를 호출시키는 handler를 만든다.

mport com.mad.dev.hsim.signal.annotation.SignalExecute;
import com.mad.dev.hsim.signal.scanner.AnnotationScanner;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.stereotype.Component;
import sun.misc.Signal;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

@Component
@Slf4j
public class SignalHandler implements sun.misc.SignalHandler, ApplicationListener<ContextRefreshedEvent> {

    @Autowired
    private Object[] beans;

    private List signalBeans;

    @Override
    public void handle(Signal signal) {
        log.info("signal : " + signal.getName());
        this.signalBeans.forEach(bean ->{
            List<Method> exeMethod = new AnnotationScanner(bean).filterMethod(SignalExecute.class);
            if(!exeMethod.isEmpty()){
                exeMethod.forEach(method -> {
                    SignalExecute signalExecute = method.getAnnotation(SignalExecute.class);
                    if(!signal.getName().equals(signalExecute.signalName())){ return; }
                    try {
                        method.invoke(bean);
                    } catch (IllegalAccessException | InvocationTargetException e) {
                        log.error("signal method invoke error : {}", method.getName(), signal.getName());
                    }
                });
            }
        });
    }

    @Override
    public void onApplicationEvent(ContextRefreshedEvent contextRefreshedEvent) {
        this.signalBeans = Arrays.stream(this.beans).filter(bean -> new AnnotationScanner(bean).hasAnnotationMethod(SignalExecute.class)).collect(Collectors.toList());
        String[] signals = {"USR2"};
        Arrays.stream(signals).forEach(signal ->{
            try {
                Signal.handle(new Signal(signal), this);
            }catch(IllegalArgumentException e){
                log.error("signal handle err : " + e.getMessage());
            }
        });
    }
}

이제 아래와 같이 SIGUSR2를 해당 프로세스 PID로 전달시키면, 아래의 changeUrl 함수가 호출되는 것을 볼 수 있습니다.

kill -SIGUSR2 29419

    @SignalExecute(signalName = "USR2")
    public void changeUrl() {
        this.removeServerUrl = "http://remote2.server.com";
    }

이상으로
개발 세끼의 '첫끼'였습니다.