안녕하세요
개발 세끼의 '첫끼'입니다.
서버 개발을 하다 보면, 아주 예외적인 케이스를 위해서 본인만 호출할 수 있는 api를 만들곤 합니다.
하지만,, 이런 API가 외부로 노출 되었을 경우를 생각한다면, 굉장히 큰 취약점을 스스로 노출시키는 것과 같습니다.
그렇다면, 이런 API 노출 이외에 ‘서버 데몬’과 ‘서버 운영자’간의 대화는 어떤 방식으로 이뤄질 수 있을까요?
저는 오래전에 즐겨 사용 했었던 'Signal Programming' 에서 찾아보려 합니다.
java에서도 이런 os의 시그널을 처리할 수 있는 Handler를 제공해주고 있는데, 저는 불편한 Signal 처리를 Spring Framework위에서 아주 자연스럽고, Spring 스럽게 커스텀해서 쓰는 방법에 대해서 글을 적어볼까 합니다.
아래 예제는, 특정 Signal이 Process에 전달되었을 때 Config에 저장 된 외부 URL을 다른 URL로 변경하는것을 다룹니다.
- Signal Execute Annotation 추가
@Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME)
@Documented public
@interface SignalExecute
{
String signalName();
}
- 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를 모두 호출시켜주려고 합니다.
- 간단하게 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();
}
}
- 아래와 같은 코드로 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";
}
이상으로
개발 세끼의 '첫끼'였습니다.