Use Boss-Worker model to accept connections with java.net.ServerSocket

일반적인 소켓 API를 이용하여 멀티쓰레디드 서버 프로그램을 만들 때, 쓰레딩 방식을 결정함에 있어서, Boss-Worker 모델Peer 모델을 고려하게 됩니다.

서버 프로그램에 있어서 Boss-Worker 모델이라고 하면, (하나의) Boss thread에서 accept를 수행하고 Boss thread가 Worker thread에게 accept된 커넥션(connection)을 전달하여, Worker thread가 커넥션에 따른 서버 로직을 수행하는 것입니다. 한편, Peer 모델은 각 thread가 각자 커넥션을 accept하려고 시도하고, accept된 커넥션에 대해서 서버 로직을 각자의 thread에서 수행하는 것을 얘기합니다.

두 모델간에 어느 것이 낫냐는 질문을 종종 받곤 하는데, accept할 기회만 충분히 주어진다면, 큰 차이가 없다고 볼 수 있습니다. 굳이 말하자면, Peer 모델의 경우 (어떤 이유로든) thread 수가 부족하다면 accept될 기회가 없어서 자원이 남는데도 불구하고 불필요하게 처리량(throughput)이 떨어지는 서버가 만들어질 수도 있습니다. 클라이언트의 접속에 대해서 별 걱정없다는 면에서 Boss-Worker 모델이 좀 더 속편하다고 볼 수도 있습니다. 처리량의 문제는 클라이언트의 리퀘스트 양이라는 문제와 서버가 이를 처리할 수 있는 능력(capacity)이라는 문제와 연결되어있는데, 당연히 서버는 자신이 처리할 수 있는 양 이상을 처리하려고 시도하지 않아야겠죠. 이 때, 커넥션을 가지고 있는 상태에서 서비스할지 말지 결정하는 것이 커넥션이라는 하위 메커니즘에 대해서 이것저것 고민하는 것보다는 훨씬 편리합니다. (제 경험상, 다른 일을 할 수 없을 정도로 커넥션(클라이언트의 리퀘스트)이 들어오는 것은 상식적으로 클라이언트가 잘못된 상태거나 클라이언트-서버의 설계가 부적절하게 설계된 것입니다.)

원래 얘기로 돌아와서, Boss-Worker 모델과 Peer 모델은 적어도 POSIX 소켓 API를 이용하는 멀티쓰레디드 서버 프로그램에서는 일반적이지만, Java의 java.net.ServerSocket을 이용하여 accept하는 경우에는 Peer 모델을 사용하는 것은 부적절해 보입니다. 그 이유는 단순하게도 ServerSocket.accept() 메서드가 synchronized 메서드이기 때문입니다.

이 사실 자체가 정상적으로 ServerSocket을 accept하는 상황에서 문제가 되지는 않습니다. 하지만, Peer thread들을 정상적으로 종료시키려면 문제가 되기 시작합니다. ServerSocket.accept()는 non-blocking I/O이고 (당연하게도) interrupt도 불가능합니다. 따라서 Peer thread를 종료시키려면, ServerSocket.accept()에서 빠져나오게 하기 위한 방법이 필요합니다. ServerSocket.accept()를 빠져나오게 하기 위해서는 가짜 접속을 하거나, ServerSocket.setSoTimeout() 메서드를 통해서 소켓 타임아웃을 설정하는 방법밖에는 없습니다. 가짜 접속을 하는 방법이 복잡해 보이므로, 소켓 타임아웃에 의존해봅시다. 하지만, ServerSocket.accpet() 메서드는 synchronized 메서드이기 때문에, 모든 Peer thread에 대해서 소켓 타임아웃이 적용되어 모두 종료되려면, (number of peer threads) * (socket timeout) 만큼의 시간이 걸립니다. 그러면 가짜 접속을 하는 방법을 고려해볼까요? 이쯤에서 짜증이 나기 시작합니다.

해결책은 그냥 처음부터 Boss-Worker 모델을 사용하는 것입니다. 위에서 언급했듯이 숙제로 짜는 프로그램이 아니라면, Boss-Worker 모델이 Peer 모델에 비해서 별로 복잡하지도 않습니다. (제대로 짠 프로그램이라면, 어차피 통신 방법과 서버 로직은 잘 분리되어있어야 하겠죠) Boss-Worker 모델에서는 당연히 한 시점에 하나의 thread가 ServerSocket.accept()를 호출하므로 (socket timeout) 만큼의 시간이 걸릴 뿐입니다.

Peer 모델을 그대로 사용하면서, NIO의 java.nio.channels.Selector를 사용하는 방법도 있습니다만, 애초에 서버 프로그램을 NIO를 이용하여 짜지 않았기 때문에, 필요 이상의 복잡함이 추가됩니다.

사실 이런 종류의 문제들은 서버 프로그램에서 항상 반복되는 문제들이고 MINA와 같은 프레임워크들이 잘 해결하고 있습니다. 처음부터 이런 프레임워크를 고려하는 것도 한가지 방법입니다.

댓글 달기

이메일 주소는 공개되지 않습니다.

이 사이트는 스팸을 줄이는 아키스밋을 사용합니다. 댓글이 어떻게 처리되는지 알아보십시오.