네트워크 프로그램의 성능 : 안정성, 확장성, 속도
네트워크 애플리케이션을 개발해야 하는 네트워크 프로그래머가 고려해야 하는 애플리케이션의 성능은 크게 안정성, 확장성, 속도의 세 가지로 생각해 볼 수 있다.
◆ 안정성 : 네트워크 상에서 정보를 안전하게 전달하고 여러 가지 오류에 잘 대처하는 것
◆ 확장성 : 사용자가 늘어나도 무리 없이 잘 감당하는 것, 특히 서버 프로그램에서 요구되는 성능이다.
◆ 속도 : 많은 정보를 가능한 짧은 시간에 처리하고 전달하는 것.
네트워크 애플리케이션 프로그램의 개발에서 가장 우선 시 해야 하는 것은 필자의 생각에는 일단 안정성이다. 특히 인터넷 환경에서 동작해야 하는 애플리케이션의 경우 더욱 그러하다. 아무리 훌륭한 기능을 갖춘 프로그램이라 하더라도 네트워크 환경에서 튼튼하게 동작하지 않는다면 불편해서 사용할 수 없는 프로그램이 된다.
일반적인 애플리케이션 개발을 하다가 네트워크 애플리케이션 개발에 입문한 경우 각종 API 함수 호출의 오류 검출에 대해서 좀 무신경한 편이다. 일반적인 애플리케이션의 경우 그냥 단순히 오류 여부 정도만 체크하면 되고, 보통 에러의 원인도 단순 명료한 경우가 대부분이다. 하지만 네트워크 프로그램의 경우 소켓 API 호출 시 발생하는 오류를 체크함은 물론 오류의 종류도 판별해서 경우에 따라 잘 대처해야 한다. LAN 환경에서는 문제없이 잘 실행되던 애플리케이션도 인터넷 환경으로 가져 나가면 많은 문제를 일으키는 경우가 빈번하다. 발생한 오류의 원인이 뭔지를 가늠하는 것도 그렇게 만만치 않다. 일단 네트워크 프로그램은 철저하게 방어적으로 작성해야 하고 가능한 세세한 오류 보고를 출력하도록 해야 한다.
다음은 Winsock API로 작성된 네트워크 프로그램의 일부이다. TCP 프로토콜로 설정된 소켓으로부터 데이터를 전송받는 아주 간단한 코드이다. 지면 관계상 단순화된 것이지만 recv() 함수의 오류를 크게 두 종류로 처리하고 있고 구체적인 오류코드를 출력하도록 작성되어 있는 것을 알 수 있다.
int BUFFERLEN = 255;
char buf[BUFFERLEN];
int ByteCount = 0;
int n;
while (((n = recv(sock, buf + byteCount, BUFFERLEN – byteCount, 0)) > 0)
{
byteCount += n;
if(byteCount > buf[0])
break;
}
if (n < 0)
{
printf(“read() failed with error: %d ”, WSAGetLastError());
return;
}
if (n == 0)
{
printf(“Connection was closed ”);
return;
}
recv() 함수의 경우 모두 16가지 종류의 오류 코드를 발생시킬 수 있다. 오류의 원인은 내부적인 요인도 있을 수 있고 외부적인 요인도 있을 수 있다. 좌우지간 네트워크 프로그램은 돌다리도 두드리는 심정으로 꼼꼼하고 조심스럽게 작성해야 한다. 그렇다고 외부적인 요인으로 발생하는 오류까지 방지할 수 있는 것은 아니다.
사실 외부적 원인으로 인한 네트워크 프로그램의 오류 발생은 피할 수 없는 일이다. 통제가 가능한 프로그램 내부의 논리적 오류는 물론 말끔히 제거해야 하겠지만 통제가 불가능한 환경에서 오는 원인으로 발생하는 오류들은 피할 길이 없다. 이런 경우는 물론 그러한 오류들로 인해서 프로그램이 불안정해지거나 하는 일 없이 튼튼하게 작동할 수 있도록 잘 대처해야 한다.
확장성은 특히 서버 프로그램 개발에서 중요한 이슈가 되는데, 서버 프로그램은 분산 시스템, 멀티쓰레딩, 병렬 입출력 등 더욱 전문적이고 어려운 기술적 주제들을 익혀야 한다. 윈도우 플랫폼을 기준으로 한 것이지만 참고자료 ❷에서 이에 관한 유용한 내용을 얻을 수 있을 것이다. 속도의 경우 사실 모든 애플리케이션이 갖춰야 하는 속성이고 네트워크 애플리케이션의 경우도 예외가 아니다. 특히 대규모 온라인 게임에서 중요한 이슈가 된다. 게임은 보통 실시간성을 요구하기 때문이다.
대역폭과 지연 : 극복해야 할 장애물
네트워크의 성능을 가늠할 때 흔히 대역폭(bandwidth)을 사용하는 경우가 보통이다. 대역폭은 단위 시간당 전송할 수 있는 정보의 양이다. 네트워크의 환경에 따라 넉넉한 대역폭이 지원되기도 하고 그렇지 못하기도 하다. 네트워크 애플리케이션이 열악한 환경에서도 좋은 성능을 내기 위해서는 부족한 대역폭을 가진 환경에서도 적절한 방법으로 데이터를 처리할 수 있어야 한다. 무엇보다도 애플리케이션이 가능한 적은 대역폭을 사용하고도 제 기능을 할 수 있도록 애플리케이션 프로토콜을 신중하게 잘 설계해야 한다. 대역폭을 줄이는 방법은 크게 두 가지로 볼 수 있다.
◆ 데이터 패킷의 용량 줄이기 : 실제 데이터는 1바이트면 충분히 표현이 가능한데, 4바이트 정수에 담아서 전달 하는 일이 없어야 한다. 필요하다면 데이터의 크기를 바이트 단위가 아닌 비트 단위로 처리해서 한 비트도 낭비하는 일이 없도록 해야 한다. 그리고 패킷의 압축도 고려할 수 있다.
◆ 정보 전송 빈도 낮추기 : 데이터 성격에 따라 자주 보내지 않아도 되는 데이터는 패킷을 보내는 시간 간격을 늘려 잡으면 단위 시간당 전송되는 전송량을 줄일 수 있다. 애플리케이션의 성격에 따라서는 보내는 대상을 줄이는 방법 등이 고려될 수 있다.
다음은 Tribes라는 온라인 게임에서 대역폭을 줄이기 위해서 데이터를 비트 단위로 묶어서 전송하는 실제 코드 예이다.
// 패킷에 비트 단위로 기록하는 코드
if (stream->writeBool(updateDamage)) { // 1비트 사용
stream->writeInt(mDamageState, 2); // 2비트 사용
if (mDamageState != Dead)
stream->writeInt(mDamageLevel,6);// 6비트 사용
if (stream->writeBool(mRepairActive)) //1비트 사용
stream->writeInt(mRepairRate,4); // 4비트 사용
}
// 대응되는 읽기 코드
if (stream->readBool()) { // 1비트 읽기
mDamageState = stream->readInt(2); // 2비트 읽기
if (mDamageState != Dead)
mDamageLevel = stream->readInt(6);// 6비트 읽기
mRepairActive = stream->readBool(); // 1비트 읽기
if (mRepairActive)
mRepairRate = stream->readInt(4); // 4비트 읽기
}
비트 단위 처리를 위해 특별히 작성되었을 writeBool, writeInt, readBool, readInt 함수들을 이용해 비트 단위로 데이터 처리를 하고 있는 걸 확인할 수 있다. 이외에도 데이터를 여러 개의 패킷으로 나누어 보내지 말고 하나의 패킷으로 통합해서 보내면 전송 프로토콜들의 헤더 부분이 절약된다. 하지만 이런 전략을 TCP 프로토콜에서 사용할 경우 오류로 재전송이 발생할 경우에는 오히려 재 전송량만 늘어나고 역효과가 날 수 있다.
대개 일반적인 네트워크 애플리케이션의 경우 대역폭만 신경쓰면 되고 지연에 대해서는 거의 고려할 일이 없다. 하지만 실시간 처리가 중요한 온라인 게임의 경우 지연 처리가 매우 중요한 이슈가 된다. 일반적인 네트워크 애플리케이션에서 지연이 조금 발생한다고 해서 별다른 문제가 되지 않는다. 웹 페이지가 전송이 끝나 화면에 출력되는 데 걸리는 수초 간의 시간을 기다리는 데 익숙한 사용자들에게 100ms도 안 되는 지연이 있었다고 해도 알아 챌 수도 없고 설사 1000ms(1초) 정도의 제법 긴 지연이 발생해도 전송을 기다리는 시간에 비하면 완전히 무시할 수 있는 수치이다. 그냥 좀더 기다리면 되는 것이다. 그렇기 때문에 일반적인 네트워크 애플리케이션 프로그래머들은 대개 지연에 대해서 잘 인식하지 못하는 경우가 많다. 하지만 온라인 게임의 경우에는 긴 지연이 근본적으로 게임 플레이를 불가능하게 하기도 한다.
대역폭은 기술의 발달과 함께 더 빠른 전송이 가능한 수준으로 계속 발전할 수 있겠지만 지연의 경우는 근본적인 물리적 한계이기도 하다. 우선 네트워크의 한 노드에서 다른 노드로 어떤 신호가 전달되는 데는 시간이 걸린다. 물론 이 신호는 광케이블이나 동축 케이블 등을 타고 전자기 신호의 형태로 전달되기 때문에 기본적으로 빛의 속도로 진행한다. 하지만 빛의 속도가 유한하기 때문에 대륙을 넘어 갈 경우에는 빛의 속도의 한계로 지연이 느낄 수 있을 정도로 발생한다. 멀리 미국에 있는 친지와 국제 전화를 해본 이들은 이런 지연을 이미 경험했을 것이다.
실제 네트워크 상에서는 패킷이 여러 네트워크 장비들의 버퍼에 대기하고 프로토콜이 패킷을 처리하는 시간 등이 소요되기 때문에 물리적인 신호 전달 시간보다 더 많은 시간 지연이 발생한다. 패킷 오류나 손실로 재전송까지 있게 되면 더욱 늘어나는 것이다. 그래서 대개 미국에 있는 인터넷 사이트로 Ping을 걸어보면 100ms 이상의 패킷 왕복시간이 측정된다. 국내 사이트의 경우는 네트워크 망이 양호한 상태에서는 보통 30ms를 넘지 않는다. 그리고 결정적으로 서버에서 클라이언트의 요청을 받아 답을 돌려 줄 때까지의 계산 시간 등을 고려하면 지연은 더욱 늘어나게 된다. 그래서 온라인 게임의 경우 통제할 수 있는 범위에서는 가능한 지연을 줄이고 1/4초 정도까지의 지연은 적절히 감추는 방법을 강구해야 한다.
그래서 보통 실시간성을 요구하는 온라인 게임에서는 프로토콜로 UDP를 대부분 선택한다. UDP를 선택하면 오류의 수정이나 처리를 애플리케이션 수준에서 적절히 감당해야 하지만, 가벼운 프로토콜이기 때문에 프로토콜 상의 지연은 가장 적게 줄일 수 있다. 그리고 당연한 일로 서버의 처리 속도 효율을 높여야 한다. 서버의 분산을 통해 부하를 줄이거나 멀티쓰레드 프로그래밍을 통해 최대한의 병렬 실행을 이끌어내는 것 등이 서버 상에서 할 수 있는 일이다(2부 참조). 그리고 데드 레커닝(Dead Reckoning) 같은 기법을 사용해 지연을 적절히 감추는 처리도 필요하다.
네트워크 애플리케이션을 개발해야 하는 네트워크 프로그래머가 고려해야 하는 애플리케이션의 성능은 크게 안정성, 확장성, 속도의 세 가지로 생각해 볼 수 있다.
◆ 안정성 : 네트워크 상에서 정보를 안전하게 전달하고 여러 가지 오류에 잘 대처하는 것
◆ 확장성 : 사용자가 늘어나도 무리 없이 잘 감당하는 것, 특히 서버 프로그램에서 요구되는 성능이다.
◆ 속도 : 많은 정보를 가능한 짧은 시간에 처리하고 전달하는 것.
네트워크 애플리케이션 프로그램의 개발에서 가장 우선 시 해야 하는 것은 필자의 생각에는 일단 안정성이다. 특히 인터넷 환경에서 동작해야 하는 애플리케이션의 경우 더욱 그러하다. 아무리 훌륭한 기능을 갖춘 프로그램이라 하더라도 네트워크 환경에서 튼튼하게 동작하지 않는다면 불편해서 사용할 수 없는 프로그램이 된다.
일반적인 애플리케이션 개발을 하다가 네트워크 애플리케이션 개발에 입문한 경우 각종 API 함수 호출의 오류 검출에 대해서 좀 무신경한 편이다. 일반적인 애플리케이션의 경우 그냥 단순히 오류 여부 정도만 체크하면 되고, 보통 에러의 원인도 단순 명료한 경우가 대부분이다. 하지만 네트워크 프로그램의 경우 소켓 API 호출 시 발생하는 오류를 체크함은 물론 오류의 종류도 판별해서 경우에 따라 잘 대처해야 한다. LAN 환경에서는 문제없이 잘 실행되던 애플리케이션도 인터넷 환경으로 가져 나가면 많은 문제를 일으키는 경우가 빈번하다. 발생한 오류의 원인이 뭔지를 가늠하는 것도 그렇게 만만치 않다. 일단 네트워크 프로그램은 철저하게 방어적으로 작성해야 하고 가능한 세세한 오류 보고를 출력하도록 해야 한다.
다음은 Winsock API로 작성된 네트워크 프로그램의 일부이다. TCP 프로토콜로 설정된 소켓으로부터 데이터를 전송받는 아주 간단한 코드이다. 지면 관계상 단순화된 것이지만 recv() 함수의 오류를 크게 두 종류로 처리하고 있고 구체적인 오류코드를 출력하도록 작성되어 있는 것을 알 수 있다.
int BUFFERLEN = 255;
char buf[BUFFERLEN];
int ByteCount = 0;
int n;
while (((n = recv(sock, buf + byteCount, BUFFERLEN – byteCount, 0)) > 0)
{
byteCount += n;
if(byteCount > buf[0])
break;
}
if (n < 0)
{
printf(“read() failed with error: %d ”, WSAGetLastError());
return;
}
if (n == 0)
{
printf(“Connection was closed ”);
return;
}
recv() 함수의 경우 모두 16가지 종류의 오류 코드를 발생시킬 수 있다. 오류의 원인은 내부적인 요인도 있을 수 있고 외부적인 요인도 있을 수 있다. 좌우지간 네트워크 프로그램은 돌다리도 두드리는 심정으로 꼼꼼하고 조심스럽게 작성해야 한다. 그렇다고 외부적인 요인으로 발생하는 오류까지 방지할 수 있는 것은 아니다.
사실 외부적 원인으로 인한 네트워크 프로그램의 오류 발생은 피할 수 없는 일이다. 통제가 가능한 프로그램 내부의 논리적 오류는 물론 말끔히 제거해야 하겠지만 통제가 불가능한 환경에서 오는 원인으로 발생하는 오류들은 피할 길이 없다. 이런 경우는 물론 그러한 오류들로 인해서 프로그램이 불안정해지거나 하는 일 없이 튼튼하게 작동할 수 있도록 잘 대처해야 한다.
확장성은 특히 서버 프로그램 개발에서 중요한 이슈가 되는데, 서버 프로그램은 분산 시스템, 멀티쓰레딩, 병렬 입출력 등 더욱 전문적이고 어려운 기술적 주제들을 익혀야 한다. 윈도우 플랫폼을 기준으로 한 것이지만 참고자료 ❷에서 이에 관한 유용한 내용을 얻을 수 있을 것이다. 속도의 경우 사실 모든 애플리케이션이 갖춰야 하는 속성이고 네트워크 애플리케이션의 경우도 예외가 아니다. 특히 대규모 온라인 게임에서 중요한 이슈가 된다. 게임은 보통 실시간성을 요구하기 때문이다.
대역폭과 지연 : 극복해야 할 장애물
네트워크의 성능을 가늠할 때 흔히 대역폭(bandwidth)을 사용하는 경우가 보통이다. 대역폭은 단위 시간당 전송할 수 있는 정보의 양이다. 네트워크의 환경에 따라 넉넉한 대역폭이 지원되기도 하고 그렇지 못하기도 하다. 네트워크 애플리케이션이 열악한 환경에서도 좋은 성능을 내기 위해서는 부족한 대역폭을 가진 환경에서도 적절한 방법으로 데이터를 처리할 수 있어야 한다. 무엇보다도 애플리케이션이 가능한 적은 대역폭을 사용하고도 제 기능을 할 수 있도록 애플리케이션 프로토콜을 신중하게 잘 설계해야 한다. 대역폭을 줄이는 방법은 크게 두 가지로 볼 수 있다.
◆ 데이터 패킷의 용량 줄이기 : 실제 데이터는 1바이트면 충분히 표현이 가능한데, 4바이트 정수에 담아서 전달 하는 일이 없어야 한다. 필요하다면 데이터의 크기를 바이트 단위가 아닌 비트 단위로 처리해서 한 비트도 낭비하는 일이 없도록 해야 한다. 그리고 패킷의 압축도 고려할 수 있다.
◆ 정보 전송 빈도 낮추기 : 데이터 성격에 따라 자주 보내지 않아도 되는 데이터는 패킷을 보내는 시간 간격을 늘려 잡으면 단위 시간당 전송되는 전송량을 줄일 수 있다. 애플리케이션의 성격에 따라서는 보내는 대상을 줄이는 방법 등이 고려될 수 있다.
다음은 Tribes라는 온라인 게임에서 대역폭을 줄이기 위해서 데이터를 비트 단위로 묶어서 전송하는 실제 코드 예이다.
// 패킷에 비트 단위로 기록하는 코드
if (stream->writeBool(updateDamage)) { // 1비트 사용
stream->writeInt(mDamageState, 2); // 2비트 사용
if (mDamageState != Dead)
stream->writeInt(mDamageLevel,6);// 6비트 사용
if (stream->writeBool(mRepairActive)) //1비트 사용
stream->writeInt(mRepairRate,4); // 4비트 사용
}
// 대응되는 읽기 코드
if (stream->readBool()) { // 1비트 읽기
mDamageState = stream->readInt(2); // 2비트 읽기
if (mDamageState != Dead)
mDamageLevel = stream->readInt(6);// 6비트 읽기
mRepairActive = stream->readBool(); // 1비트 읽기
if (mRepairActive)
mRepairRate = stream->readInt(4); // 4비트 읽기
}
비트 단위 처리를 위해 특별히 작성되었을 writeBool, writeInt, readBool, readInt 함수들을 이용해 비트 단위로 데이터 처리를 하고 있는 걸 확인할 수 있다. 이외에도 데이터를 여러 개의 패킷으로 나누어 보내지 말고 하나의 패킷으로 통합해서 보내면 전송 프로토콜들의 헤더 부분이 절약된다. 하지만 이런 전략을 TCP 프로토콜에서 사용할 경우 오류로 재전송이 발생할 경우에는 오히려 재 전송량만 늘어나고 역효과가 날 수 있다.
대개 일반적인 네트워크 애플리케이션의 경우 대역폭만 신경쓰면 되고 지연에 대해서는 거의 고려할 일이 없다. 하지만 실시간 처리가 중요한 온라인 게임의 경우 지연 처리가 매우 중요한 이슈가 된다. 일반적인 네트워크 애플리케이션에서 지연이 조금 발생한다고 해서 별다른 문제가 되지 않는다. 웹 페이지가 전송이 끝나 화면에 출력되는 데 걸리는 수초 간의 시간을 기다리는 데 익숙한 사용자들에게 100ms도 안 되는 지연이 있었다고 해도 알아 챌 수도 없고 설사 1000ms(1초) 정도의 제법 긴 지연이 발생해도 전송을 기다리는 시간에 비하면 완전히 무시할 수 있는 수치이다. 그냥 좀더 기다리면 되는 것이다. 그렇기 때문에 일반적인 네트워크 애플리케이션 프로그래머들은 대개 지연에 대해서 잘 인식하지 못하는 경우가 많다. 하지만 온라인 게임의 경우에는 긴 지연이 근본적으로 게임 플레이를 불가능하게 하기도 한다.
대역폭은 기술의 발달과 함께 더 빠른 전송이 가능한 수준으로 계속 발전할 수 있겠지만 지연의 경우는 근본적인 물리적 한계이기도 하다. 우선 네트워크의 한 노드에서 다른 노드로 어떤 신호가 전달되는 데는 시간이 걸린다. 물론 이 신호는 광케이블이나 동축 케이블 등을 타고 전자기 신호의 형태로 전달되기 때문에 기본적으로 빛의 속도로 진행한다. 하지만 빛의 속도가 유한하기 때문에 대륙을 넘어 갈 경우에는 빛의 속도의 한계로 지연이 느낄 수 있을 정도로 발생한다. 멀리 미국에 있는 친지와 국제 전화를 해본 이들은 이런 지연을 이미 경험했을 것이다.
실제 네트워크 상에서는 패킷이 여러 네트워크 장비들의 버퍼에 대기하고 프로토콜이 패킷을 처리하는 시간 등이 소요되기 때문에 물리적인 신호 전달 시간보다 더 많은 시간 지연이 발생한다. 패킷 오류나 손실로 재전송까지 있게 되면 더욱 늘어나는 것이다. 그래서 대개 미국에 있는 인터넷 사이트로 Ping을 걸어보면 100ms 이상의 패킷 왕복시간이 측정된다. 국내 사이트의 경우는 네트워크 망이 양호한 상태에서는 보통 30ms를 넘지 않는다. 그리고 결정적으로 서버에서 클라이언트의 요청을 받아 답을 돌려 줄 때까지의 계산 시간 등을 고려하면 지연은 더욱 늘어나게 된다. 그래서 온라인 게임의 경우 통제할 수 있는 범위에서는 가능한 지연을 줄이고 1/4초 정도까지의 지연은 적절히 감추는 방법을 강구해야 한다.
그래서 보통 실시간성을 요구하는 온라인 게임에서는 프로토콜로 UDP를 대부분 선택한다. UDP를 선택하면 오류의 수정이나 처리를 애플리케이션 수준에서 적절히 감당해야 하지만, 가벼운 프로토콜이기 때문에 프로토콜 상의 지연은 가장 적게 줄일 수 있다. 그리고 당연한 일로 서버의 처리 속도 효율을 높여야 한다. 서버의 분산을 통해 부하를 줄이거나 멀티쓰레드 프로그래밍을 통해 최대한의 병렬 실행을 이끌어내는 것 등이 서버 상에서 할 수 있는 일이다(2부 참조). 그리고 데드 레커닝(Dead Reckoning) 같은 기법을 사용해 지연을 적절히 감추는 처리도 필요하다.
"Network Programming" 카테고리의 다른 글
- Socket에서 완벽한 Receive처리 (0)2007/03/02
- 네트워크 프로그래밍의 원리 이해 - 5 (0)2005/05/29
- 네트워크 프로그래밍의 원리 이해 - 4 (0)2005/05/29
- 네트워크 프로그래밍의 원리 이해 - 3 (0)2005/05/29
- 네트워크 프로그래밍의 원리 이해 - 2 (0)2005/05/28

수안이의 컴퓨터 연구실



Leave your greetings.