실시간으로 점수를 변경해야하는 popcat과 같은 서비스를 개발하는 중이다.
대상 유저는 같은 학교 학생들로 최대 200명 정도의 사용자를 받을 수 있다.

popcat에서는 websocket을 사용해서 실시간으로 데이터를 업데이트하는것이 아닌 polling 방식을 사용하여 주기적으로 http 요청을 보내 정보를 업데이트한다.
하지만 전세계 유저들을 대상으로하는 popcat과 달리 교내에서 많아야 200명 정도의 유저를 받기 때문에 가능하다면 websocket을 사용하여 실시간으로 데이터를 업데이트 하고 싶었다.
tomcat, springboot 내에서 구현blocking io
springboot - STOMPpub/sub 구조 사용하여 room,channel 구현가능
springboot - spring-boot-starter-websocket대부분 직접 구현해야함
처음 떠올린 선택지 두가지는 non-blocking I/O를 지원하지 않아 많은 요청을 한번에 처리하기 힘들었고, room, channel등 편의 기능이 부족했다.
room,channel 지원non-blocking ionetty or node.js 사용springboot - netty-socketio
node.js - socket.io
다음으로 선택지로 찾은 node.js의 socket.io는 위에서 문제가 되었던 부분을 모두 해결했다.
하지만 node.js는 이미 개발된 user 도메인 관련 서비스와 분리하여 따로 개발해야 했다. 그러한 상황이 나쁘지만은 않지만, node.js clustering, MSA에는 관심도, 경험도, 엄두도 없었기에 피하고 싶었다.
그렇게 고민을 하던 찰나, netty-socketio란 라이브러리에 대해 알게됐다.
netty-socketio는 socketio의 java 구현체로 springboot에서 tomcat이외에 netty라는 서버를 올려 socketio를 구현했다. netty를 사용하여 non-blocking I/O를 지원한다.
@Configuration
@Slf4j
public class SocketIoConfig {
@Bean
public SocketIOServer socketIOServer(AuthorizationListener authorizationListener){
com.corundumstudio.socketio.Configuration config = new com.corundumstudio.socketio.Configuration();
config.setHostname("127.0.0.1");
config.setPort(8081);
log.info("{} processors are available",Runtime.getRuntime().availableProcessors());
config.setBossThreads(1);
config.setWorkerThreads(5);
config.setAuthorizationListener(authorizationListener);
return new SocketIOServer(config);
}
}
@Component
@RequiredArgsConstructor
public class SocketIoServerLifeCycle {
private final SocketIOServer socketIoServer;
@PostConstruct
public void start() {
socketIoServer.start();
}
@PreDestroy
public void stop() {
socketIoServer.stop();
}
}
SocketIOServer를 Bean으로 등록하고 @PostConsrtuct로 start()를 호출하여 서버를 시작한다.
@Controller
public class SocketIOController {
private final SocketIOServer server;
private final SocketIOService socketIOService;
public SocketIoController(SocketIOServer server,SocketIOService socketIOService) {
this.server = server;
this.socketIOService = SocketIOService;
server.addConnectListener(socketIOService::onConnect);
server.addDisconnectListener(socketIOService::onDisconnect);
server.addEventListener("event", EventDto.class, socketIOService::onEvent);
}
}
@Service
public class SocketIOService {
public void onConnect(SocketIOClient client){
//이벤트 처리
}
public void onDisconnect(SocketIOClient client){
//이벤트 처리
}
public void onEvent(SocketIOClient client, EventDto event, AckRequest ackSender){
//이벤트 처리
}
}
SocketIOServer에 addEventListener를 통해 이벤트 리스너를 추가한다.이벤트에서 지정한 타입의 param을 받아 요청을 처리할 수 있다.
spring security가 tomcat에 filter들을 추가하여 작동하고, netty-socketio는 이름처럼 tomcat 웹서버가 아닌 netty라는 웹서버를 하나 더 올려, 그곳에서 websocket 요청을 처리한다.
따라서 spring security말고 다른 방법으로 인증/인가를 구현해야 한다.
web flux의 reactive spring security를 적용하는건 무리였다
다행히도 netty-socketio에서 AuthorizationListener라는 filter와 비슷한걸 제공한다.
SocketIOConfig에서 이렇게 리스너를 추가해주었다.
config.setAuthorizationListener(authorizationListener);
@Component
@RequiredArgsConstructor
public class CustomAuthorizationListener implements AuthorizationListener {
@Override
public AuthorizationResult getAuthorizationResult(HandshakeData handshakeData){
String httpHeader = handshakeData.getHttpHeaders()
//인증 로직
return new AuthorizationResult(true, Map.of("userAuthData",user));
}
}
Listener에서 HttpHeader, SessionId 등이 포함된 HandshakeData를 가지고 인증을 진행한다. 그 후에 AuthorizationResuilt를 반환하는데, true라면 뒤에 넣어준 데이터를 SocketIOClient 안에 넣어서, 이벤트 리스너에 넘겨준다.
이렇게 springboot에서 netty-socketio를 사용하여 socket.io 요청을 처리하는 방법을 알아보았다.