제임스딘딘의
Tech & Life

개발자의 기록 노트

동기화 소켓 vs 비동기화 소켓, 사용시 각각의 장단점

제임스-딘딘 2012. 1. 26. 04:16
// 동기화와 비동기화 소켓 사용시 유의 사항에 가까울지도

동기화 소켓의 경우는 한쪽이 write이면 다른 한쪽은 read로서 다른 일하지말고 read대에만 신경 집중해야 합니다.
그래서 write하는 곳에서 자료를 다 보낼때까지 read하는 쪽은 다 받을때까지 Block(다음 구문으로 넘어가지 않음) 되어 있어야하죠. 클라이언트 측에서는 다 받을때까지 기다리고 난후 다음 일을 하면되겠지만, 서버측에서는 read에만 집중할 수 없고 다른 일을 해야합니다. 즉 다른 요청에 대해서 Listen도 해야하고, 기타 다른 처리도 해야겠지요. 이런 이유로 서버측에서는 Forking(자기 자신을 복제함)을 하든가 Threading을 하던가해서 현재의 일을 하나의 Thread(Forking의 경우는 Process)에게 맡기고 다시 서버의 본질적인 작업을 실행합니다. Forking은 유닉스 계열에서 사용하는 방법이고, Threading은 윈도우즈 계열에서 사용하는 방법입니다. 단순 서빙이나 안정성에 있어서는 Forking이 좋은 방법이나 정보 교환(게임 서버의 경우)을 할 이유가 있을때는 Threading이 훨씬 낫죠. Thread의 경우는 독자적으로 움직이기는 하지만 하나의 Process내에 있어서 공용 변수를 설정/사용하기가 쉽지요. 다만 동기화(이 동기화는 Thread 동기화를 의미 함)를 잘시켜야하죠. Thread-Safe한 객체에 대해서는 굳이 동기화를 시킬 필요는 없습니다. Fork의 경우에는 각 Process가 다른 주소 공간에 있기때문에 이런 작업을 위해 공유 메모리나 Signal을 사용해야하죠. 사용하는게 그다지 어럽지는 않지만 디버깅하는 것도 까다롭고 여러모로 까다롭습니다. 

비동기화 소켓의 경우는 한쪽이 Write를 하건 뭘 하건간에 read하는 쪽과 무관합니다. Write를 하는쪽은 그냥 알아서 Write하고 받는 쪽은 그냥 받는대로 read합니다. 소켓 라이브러리에서 받는대로 현재까지 받은 자료를 상위 프로그램에 event와 함께 알려주죠. 이런 이유는 비동기 소켓은 Threading을 할 필요가 없습니다. 어찌보면 아주 편하게 보이지요. 그러나 댓가가 따릅니다. 한쪽에서 ABC1234567890 이라는 자료를 날렸다고 하면, 받는 쪽에서는 ABC1234567890라는 Data를 event와 함께 받으면 아무런 고민할 것도 없죠. 그러나 절대 이걸 보장해주지 않습니다. ABC1234567890이 ABC1234 한번 읽고, 567890 요렇게 읽고 해서 두번 읽을 수도 있고, ABC1234567890 다음에 PQR7777이라는 자료를 잽싸게 보냈다면, ABC1234567890PQ 읽고, R7777 이렇게 읽을 수도 있습니다. 왜 이렇게 되나하면, TCP는 Stream으로 다루어지며 그 Stream의 스케줄은 전적으로 TCP를 관장하는 소켓 라이브러리가 하기때문입니다. 그렇다면 이렇게 읽은 자료를 써먹기위해서는 읽은 자료를 순서대로 저장하여 모은후 다시 Parsing을 해야하죠. 즉 parsing을 하기위한 간단한 프로토콜을 사용자가 다시 만들어서 사용해야 하는겁니다. 그러나 좀 하다보면 parsing을 하는 것도 그다지 어렵지 않다는걸 알게될겁니다. [크기][크기만큼의자료][크기][크기만큼의자료]... 이렇게 반복적으로 자료를 구성하면 간단해집니다. 그리고 만약 비동기 소켓을 사용하고도 한쪽에서 ABC1234567890를 보내면 받는쪽도 ABC1234567890를 항상 받을거라고 생각하고 코딩한 프로그램이 있다면 그건 손을 아주 많이 봐야합니다. "동기화 소켓+Threading"과 "비동기화 소켓"은 모두 장단점을 가지고 있지만 TCP의 동기화와 비동기화 작동 원리에 대한 완벽한 이해가 없다면 "동기화 소켓+Threading"를 사용해야 합니다. "비동기화 소켓"은 위에서 말했다시피 자료 재조합과정과 프로토콜 파싱 과정이 필요하며, 클라이언트가 n명이라면 n개가 필요하게 됩니다. 이런거 물론 만들면되지만 안만드니까 문제가 되죠. 이것만 제대로 만들고 사용한다면, Serialization이나 Data-Binding에서 동기화 방식보다 훨씬나은 성능과 훤씬 깔끔한 코딩 복잡도가 나오게 됩니다.

"동기화 소켓+Threading"은 Serialization이나 Data-Binding이 좀 까다롭고, Thread 동기화도 잘해야하는데 이걸 잘 못하는 사람도 꽤 많습니다. 왠만한 객체가 Thread-Safe 할거라고 생각하는 것도 문제죠. Thread-Safe하지 않은 것은 반드시 Critical-Section에서 처리해야 하죠. Thread가 Access하는 변수가 한두개 아니라면 여러 Critical-Section이 겹치는 문제도 나오고 Deadlock되는 상황도 일어나죠. 

경험에 의하면 봤을때 들이는 노력과 복잡도가 비슷한것 같군요. 둘은 만들고자 하는게 뭔지에 따라서 장단점이 더 확연히 드러납니다. 

파일 전송의 경우에는 "동기화 소켓+Threading" 방법이 코딩하기도 간단하고, 안정성도 좋고 속도도 좋습니다. 이걸 "비동기화 소켓"으로 구현해서 테스트 해봤는데, 파싱하는데 드는 약간의 오버해드를 고려해도 속도가 느리지는 않습니다. 다만 만들기 아주 어렵습니다. 비동기의 경우에는 대용량 파일의 경우에는 Flow-Control도 해주어야 하기때문에 더 만들기 어렵죠. TCP인데 무슨 Flow-Control을 해줘야 하나라고 생각할분 있을겁니다. 그러나 해보면 알겠지만 Write 하는 쪽에서 받는쪽 신경안쓰고 무작정 Write하면 송신 Overflow가 발생해서 자료 다날라갑니다. 이것은 비동기 소켓의 또 다른 단점이기도 하죠. 

채팅의 경우 "동기화 소켓+Threading"이 간단하며 뭐 달리 추가로 해줄 것도 없어서 좋습니다. "비동기화 소켓"의 경우에는 여전히 잘 잘라내기위한 파싱이 필요합니다. 제가 만든 '다기능 채팅'의 경우에는 "비동기화 소켓"을 사용합니다. 참고로 Text 기반 프로토콜의 경우라면 Delimiter를 사용하면 간단 명료하게 프로토콜 만들 수 있습니다. 채팅의 경우 Text 기반 프로토콜로 구성하면되겠죠. 참고로 파일 전송의 경우는 이진 자료를 다루므로 Delimiter를 사용하는건 불가능합니다.

게임의 경우는 게임의 종류에 따라 달라지는데, 보드 게임류같은 경우는 단지 정보를 알려주면 되므로 채팅의 연속선상에 있다고 볼수 있지만 MUG 형의 경우는 접속자간에 서버내의 가상 사회에서의 Interaction이 필요하게 되므로 "동기화 소켓+Threading"을 해야합니다. 

결론은 비동기 소켓은 채팅과 보드 게임류 프로토콜에 잘 맞다는겁니다. 그외에는 제가 달리 말하지 않아도 현실적으로 거의 대부분 "동기화 소켓+Threading"을 사용합니다.