우리가 배운 개념이 어디서 어떻게 쓰이는지 알아보자
컴퓨터 네트워크(Computer Network)는 컴퓨터와 컴퓨터를 통신망으로 연결한 것을 말한다. 흔히 컴퓨터 네트워크를 배울 때는 OSI 모형(Open Systems Interconnection Reference Model)을 기반으로 공부한다. OSI 7계층으로 많이들 알고 있는데, 각 계층은 하위 계층의 서비스를 받으면서 상위 계층에게 서비스를 제공한다. 먼저 OSI 1. 물리계층에서 4. 전송 계층까지 살펴본 뒤, 리눅스 환경에서 5. 세션 계층부터 7. 응용 계층까지의 사용에 대해 알아보도록 하겠다.
컴퓨터 네트워크에서 목표는 간단하다. 컴퓨터로부터 다른 컴퓨터로 데이터를 전송하는 것. 목표는 간단하지만 실제로는 매우 복잡한 과정을 거친다. 전송 계층에서 신뢰성 있는 전송 서비스를 제공하는 것, 네트워크 계층에서 네트워크 노드 간의 라우팅 서비스를 제공하는 것, 데이터 링크 계층에서 물리적으로 연결된 노드에게 데이터를 전송하는 서비스를 제공하는 것, 물리 계층에서 실제로 신호로 비트를 전송하는 서비스를 제공하는 것이 잘 이뤄져야 비로소 호스트(host)에게 데이터가 전송된다. 이 글에서는 물리 계층과 데이터 링크 계층은 간략하게만 언급하고 네트워크 계층, 전송 계층과 그 상위 계층에 대해 자세히 다루려고 한다.
네트워크 쪽에는 별도의 용어가 많기 때문에 용어를 먼저 정리해보자.
컴퓨터 네트워크(Computer Network) : 컴퓨터와 컴퓨터를 통신망으로 연결한 것
노드(Node) : 컴퓨터 네트워크상에 연결된 장치
호스트(Host) : 고유 IP 주소를 가진 노드
링크(Link) : 물리적으로 노드와 노드를 연결하는 통로
홉(Hop) : 거리의 단위. 보통 한 링크를 이동하면 한 홉이라고 한다.
경로(Path) : 네트워크 상의 두 노드 간의 이동 경로
프로토콜(Protocol) : 데이터 통신을 원활하게 하기 위해 필요한 통신 규약
전송 계층에서 제공하는 서비스는 신뢰성 있는 통신인 TCP(Transmission Control Protocol)와 신뢰성이 없는 통신인 UDP(User Datagram Protocol)로 나뉜다. 응용 프로그램이 소켓을 통해 보내는 데이터 단위는 메세지(Message), TCP 통신에서 데이터 단위는 세그먼트(Segment), UDP 통신에서 데이터 단위는 데이터그램(Datagram)이라고 한다. 네트워크 계층에서는 패킷(Packet)이라는 데이터 단위를 사용하고, 데이터 링크 계층에서는 프레임(Frame), 마지막으로 물리 계층에서는 비트(Bit) 단위로 전송한다. 이 단위는 프로토콜 데이터 단위(Protocol Data Unit)로 알려진 것인데, OSI 모델에서의 단위이다. 인터넷 프로토콜 스위트(Internet Protocol Suite)에서는 전송 계층에서의 단위를 세그먼트, 네트워크 계층에서의 단위를 데이터그램, 네트워크 접근 계층에서의 단위를 프레임으로 부른다.
각 계층에서는 하위 계층으로부터 문제없이 서비스를 잘 받고, 실제로 어떻게 작동하는지는 알 필요가 없다는 가정 하에 진행된다. 예를 들어 전송 계층에서는 목적지까지 데이터를 잘 전송하는 서비스를 제공하는 것이 목표인데, 하위 계층의 역할인 패킷 경로 제어에 대해서는 관심을 두지 않는다. 이러한 원칙은 투명성(Transparent)으로 널리 알려져 있다.
통신 연결이 유지되는 것을 지향하는 프로토콜을 연결 지향 프로토콜, 연결을 유지하지 않는 프로토콜을 비연결 프로토콜이라고 한다. 연결 지향 프로토콜은 연결을 계속 유지하기 위한 비용이 들기 때문에 더 비싼 반면 비연결 프로토콜은 연결 유지 비용이 들지 않기 때문에 저렴하다. 예를 들어 전화 연결은 연결된 상태를 유지하기 때문에 연결 지향 프로토콜이라고 볼 수 있다. 비싸다고 무조건 나쁜 것이 아니고, 싸다고 무조건 좋은 것이 아니기 때문에 적절한 선택을 해야 한다.
IP 프로토콜은 비연결 프로토콜이지만 IP 프로토콜을 이용하는 TCP 프로토콜은 연결 지향 프로토콜이다. 또다시 TCP 프로토콜을 이용하는 HTTP 프로토콜은 비연결 프로토콜이다. 이렇듯 프로토콜을 어떻게 활용하느냐에 따라 연결 지향과 비연결 프로토콜로 바뀔 수 있다.
연결 지향 프로토콜에서는 이미 연결되어 있기 때문에 어떤 사람이 질의를 보냈는지 연결을 이용하여 알 수 있다. 위에서 예를 든 전화 통화는 이미 통화 연결이 성립될 때 서로 누군지 알기 때문에 연결이 유지되어 있기만 하면 시간이 지난 뒤 다시 말을 해도 누군지 알 수 있다. 하지만 비연결 프로토콜은 매번 새롭게 연결이 성립되기 때문에 필요한 경우 매 연결 시 자신이 누구인지 알려줘야 한다. 예를 들어 HTTP 프로토콜을 이용한 웹 환경의 경우 쿠키나 세션을 통해 매번 자신을 식별할 수 있는 정보를 함께 전송한다.
전송 계층의 역할은 목적지까지 데이터를 잘 도착하도록 하는 것이다. 연결 지향 데이터 스트림 지원, 신뢰성 있는 데이터 전송, 흐름 제어, 그리고 다중화와 같은 편리한 서비스를 제공한다. 전송 계층에서 가장 널리 알려진 프로토콜이 바로 TCP, UDP 프로토콜이다. 앞서 나열한 편리한 서비스들은 거의 다 TCP 프로토콜로 제공되는 것이다. 이러한 편리한 서비스들을 제공하기 위해서는 복잡한 처리를 요구하고, 이는 TCP 프로토콜을 이용하는 비용이 크다는 것을 뜻한다. 때로는 이러한 서비스들이 필요 없는 경우도 있다. 그런 때에는 UDP 프로토콜을 이용하여 저비용으로 통신할 수도 있다.
TCP 프로토콜은 호스트에서만 작동하고, 중간 라우터 노드들에서는 작동하지 않는다. 앞서 언급했듯이 TCP 프로토콜은 복잡한 처리를 요구하기 때문에 모든 중간 라우터 노드들에서까지 TCP 프로토콜을 작동하게 한다면 지금처럼 많은 데이터 전송을 처리할 수가 없기 때문이다.
TCP 프로토콜의 기능은 신뢰성 있는 데이터 전송(Reliable Data Transfer, RDT), 연결 제어(Connection Control), 흐름 제어(Flow Control), 혼잡 제어(Congestion Control)가 있다. 만약 은행 서비스를 이용하는데 금액에 대한 데이터 전송이 잘못되어 1억 원을 보낸 것이 1만 원을 보냈다고 처리된다면 재앙과도 같을 것이다. TCP 프로토콜의 신뢰성 있는 전송 기능이 있기 때문에 TCP 프로토콜 기반의 통신은 전송자가 보낸 데이터를 수신자가 그대로 전송받는다고 믿을 수 있다. TCP 프로토콜로 전송을 시작하는 것부터 각 기능을 제공하기 위한 자세한 방법을 알아보자.
응용 프로그램이 소켓을 통해 데이터를 전송하는 것부터 TCP 프로토콜이 시작된다. 한 컴퓨터에서 여러 개의 소켓과 프로세스가 존재할 수 있기 때문에 OS에서는 소켓과 프로세스를 식별하기 위해 포트(port) 번호를 따로 둔다. 한 서버에서 여러 소켓과의 연결을 유지하는 것을 생각해보면 왜 이러한 기능이 필요한지 알 수 있을 것이다. 그러나 보통 컴퓨터에 연결된 통신 링크는 하나이기 때문에 하나의 링크를 통해 여러 소켓의 데이터를 주고받아야 한다. 이를 위해 OS에서는 전송하려는 데이터를 TCP/UDP 세그먼트로 만들 때 헤더를 추가하여 공통적으로 출발지 포트 번호와 목적지 포트 번호를 둔다. 이러한 작업을 Multiplexing이라 하고, 목적지에 도착한 세그먼트는 헤더를 확인하여 Demultiplexing 된 뒤 대상 소켓에게 데이터를 전달한다. 편지를 보낼 때 주소뿐만 아니라 그 주소의 누구에게 보내는 것인지도 함께 명시하는 것을 생각하면 이해가 쉬울 것이다.
신뢰성 없는 통신을 기반으로 신뢰성 있는(오류가 없는) 통신을 지원하기 위해 단계적으로 가정을 줄이고 상황을 일반화시켜나간다. 신뢰성 있는 통신을 한다는 것은 잘못된 데이터를 전송받지 않고, 데이터의 순서 또한 유지되는 통신을 보장한다는 뜻이다. 이 항목에서 설명하는 내용은 다음 링크의 내용을 정리한 것이고, 사진들 모두 링크에 있는 것이다. 아래 그림은 신뢰성 있는 통신 서비스를 어떻게 제공하는지 보여주는 그림이다. 신뢰성 있는 데이터 전송 함수인 rdt_send 함수는 내부적으로 신뢰성 없는 채널을 이용하는 udt_send 함수를 이용하여 구현된다.
첫 시작인 rdt1.0에서는 신뢰성 있는 채널을 통해 데이터 전송이 이뤄진다고 가정한다. 데이터 전송에 에러가 발생하지 않는다는 가정 하의 전송이기 때문에 rdt_send 함수는 받은 데이터를 패킷으로 만들고 udt_send 함수로 패킷을 전송하기만 해도 신뢰성 있는 통신이 된다. 물론 수신자 측은 받은 패킷을 합쳐 데이터로 되돌리면 된다.
rdt2.0에서는 비트 에러가 존재할 수 있는 상황까지 수용한다. 전화 통화에서 받아쓰기를 한다고 생각해보자. 아마 상대방이 한 문장씩 말할 때마다 잘 들었다는 응답(OK) 혹은 다시 말해달라는 응답(Please repeat that)을 통해 받아쓰기를 잘 하고 있는지 확인할 수 있을 것이다. rdt2.0에서는 이 방식을 모티브로 삼아 작동하며, 이러한 응답을 통한 통신 조절을 ARQ(Automatic Repeat reQuest)라고 한다.
rdt2.0에서는 여기에 두 가지 추가적인 기능이 요구된다. 첫 번째로 신뢰성 없는 채널을 통한 통신이 이뤄지기 때문에 비트 에러가 있는지 감지할 수 있어야 한다. 이 감지를 위한 방법으로는 체크섬(checksum)이라는 훌륭한 방법이 있다. 두 번째로 수신자로부터 피드백을 받을 수 있어야 한다. 송신자는 수신자의 어떠한 피드백 없이는 전송이 어떻게 이뤄졌는지 알 수 없다. 이 피드백을 위해 수신자는 송신자로부터 데이터를 성공적으로 받았을 때 ACK, 잘못 받았을 때 NA(C)K를 보낸다. 아래 사진을 보면 수신자 측에서 패킷을 성공적으로 받았을 때 패킷이 손상되었다면(corrupt) NACK를 보내고, 패킷이 손상되지 않았다면(notcorrupt) ACK를 보내는 것을 확인할 수 있다. 이런 식으로 송신 후 기다리기 때문에 rdt2.0은 stop-and-wait 프로토콜로 알려져 있다.
그러나 ACK와 NAK 응답도 에러가 발생할 수 있다. ACK도 NAK도 오지 않으면 송신자 측에서는 무한정 기다리게 된다. 송신 측에서는 에러가 났는지 알 수가 없다. 또한, 중복 송신을 하게 되면 중복 수신을 하게 된다.
rdt2.1에서는 순서 번호(Sequence number)를 추가함으로써 중복 수신을 방지한다. 순서 번호만 확인하면 중복된 데이터를 받았는지 알 수 있으므로 중복 수신을 방지할 수 있다. stop-and-wait 프로토콜 상으로는 방금 전송한 패킷이 잘 전송되었는지만 알면 되기 때문에 순서 번호가 0 혹은 1만 있으면 된다. 아래 그림은 rdt2.1을 나타낸 그림이다. 0에 대한 패킷 전송 완료 후에는 1에 대한 패킷 전송을 하고, 그 후에는 다시 0에 대한 패킷을 전송한다. rdt2.0의 가정상 아직 패킷이 유실될 수 있는 상황은 아니기 때문에 이 방법은 문제가 없다.
여기서 잘못된 수신에 대한 NAK 응답이 아닌 ACK를 이용한 방법이 가능하다. rdt2.2는 NAK를 사용하지 않는 방법이다. ACK에 순서 번호를 추가함으로써 잘못된 데이터를 받은 것을 다시 송신하게 할 수 있다.
이제는 심지어 패킷이 사라질 수도 있는 채널을 통해 통신한다고 가정하자. 이 경우엔 패킷 손실을 어떻게 감지할지, 그리고 패킷 손실이 발생하면 어떻게 해야 할지 생각해봐야 한다. 패킷 손실이 됐을 때 대응 방법은 이미 rdt2.2까지의 고민을 통해 알 수 있다. 다시 보내는 것. 쉽고 간단한 방법으로 해결된다. rdt3.0에서는 송신자가 ACK를 받는 것에 대한 타임 아웃 타이머를 둬서 이를 해결한다. 시간 내에 데이터를 성공적으로 수신했다는 응답을 받지 못하면 데이터를 다시 보내는 것이다. 중복된 데이터 수신에 대해서는 이미 rdt2.2 상에서 처리된다.
rdt3.0까지 발전시켜나감으로써 이제 신뢰성 있는 통신을 할 수 있게 됐다. 하지만 이 방법은 한 번에 한 패킷씩 밖에 보내지 못하기 때문에 성능상으로 만족스럽지 못하다. 매우 먼 거리에 패킷을 보내려 한다면 패킷이 왔다 갔다(Round trip) 하는 것을 기다리는 시간 때문에 전송 속도가 매우 떨어질 것이다. 이제는 여러 패킷을 보낼 방법을 고민해야 한다. ACK를 받기 전에 다수의 패킷을 전송하는 방법을 통틀어 파이프라인 프로토콜이라고 하며, 세부적으로 GBN(Go-Back-N), SR(Selective Repeat) 프로토콜이 있다.
Go-Back-N 프로토콜은 수신자 측에서 지금까지 성공적으로 받은 패킷 순서 번호에 대한 ACK를 전송하며, 순서에 맞지 않는 패킷은 성공적으로 수신한 것으로 간주하지 않는다. 따라서, 송신자는 ACK를 받지 못한 가장 오래된 패킷부터 모두 재전송하게 된다. Selective Repeat 프로콜은 잘못된 순서의 패킷을 받았더라도 버퍼에 저장해둔다. 중간에 수신 실패한 패킷만 다시 전송하게끔 수신자 측은 개별적으로 ACK를 전송하며, 송신자 역시 패킷마다 타임 아웃 타이머를 가진다. Selective Repeat 프로토콜상으로는 상위 계층에게 현재 성공적으로 수신한 패킷까지만 제공함으로써 해당 패킷까지 신뢰성 있게 통신되었음을 보장한다. 아래 사진은 GBN 프로토콜과 SR 프로토콜을 요약한 사진이다.
이로써 신뢰성 없는 채널을 통한 신뢰성 있는 통신을 구현하는 방법에 대해 알아보았다. 실제 TCP에서는 Go-Back-N 프로토콜과 Selective Repeat 프로토콜을 하이브리드하여 사용된다고 한다.
여기서 의문이 생길 수 있는 부분은 타임 아웃 시간은 어떻게 결정하는가이다. 타임 아웃 시간을 너무 짧은 시간으로 두면 재전송 요청을 많이 하게 되고, 너무 긴 시간으로 두면 기다리는 시간이 길어진다. 적절한 타임 아웃 시간을 설정하기 위한 방법으로 그때그때 확인된 RTT(Round Trip Time, 패킷이 갔다 오는데 걸린 시간)인 SampleRTT를 이용하여 다음 RTT인 EstimatedRTT를 예측한다. 원리는 이전까지의 RTT와 다음 RTT는 비슷할 것이고, 변화가 생기면 소폭 반영하는 것이다. 식으로 나타내면 다음과 같다.
nextEstimatedRTT = (1 - a)*previousEstimatedRTT + a*sampleRTT
적절한 a, 예를 들면 0.125 정도를 설정하면 과거의 데이터에 더 비중을 둬서 변화를 수용하게 된다.
TCP 프로토콜은 연결 지향 프로토콜이고, 신뢰성 있는 통신을 제공하기 위하여 순서 번호를 사용한다고 하였다. TCP 프로토콜에서 통신이 이뤄지기 위해서는 먼저 서로 연결이 되어야 하고, 서로 임의의 시작 순서 번호를 알려줘야 한다. 그 과정을 3-way handshaking이라고 한다. 다음 박스의 내용은 연결 시작을 위한 3-way handshaking 과정을 보여준다.
Alice ---> Bob SYNchronize with my Initial Sequence Number of X
Alice <--- Bob I received your syn, I ACKnowledge that I am ready for [X+1]
Alice <--- Bob SYNchronize with my Initial Sequence Number of Y
Alice ---> Bob I received your syn, I ACKnowledge that I am ready for [Y+1]
TCP 프로토콜에서는 연결하자는 뜻으로 SYN 패킷을 보낸다. SYN은 SYNchronize의 약자로 Alice는 SYN 패킷과 함께 초기 순서 번호(Initial Sequence Number, ISN)를 Bob에게 보낸다. Bob은 Alice의 초기 순서 번호가 X인 것을 알게 되고, Alice에게 SYN, ACK가 합쳐진 패킷을 보내면서 자신의 초기 순서 번호가 Y임을 알려준다. 마지막으로 Alice는 Bob으로부터 초기 순서 번호를 잘 받았다는 의미로 ACK를 보낸다. 이 과정을 통해 두 호스트는 신뢰성 있는 통신을 할 수 있는 준비를 마치게 된다.
실제로는 3-way handshaking 과정에서 초기 순서 번호 외에도 수신 기본 윈도우 크기(rwnd), 여러 가지 추가적인 옵션 등을 교환하게 된다. 왜 2-way handshaking으로는 안 되는 걸까? 아래 사진은 2-way handshaking의 실패 시나리오다. 호스트 A가 호스트 B에게 연결 요청을 한 것이 딜레이가 많이 되어 다시 연결 요청을 하는 상황이 발생할 수 있다. 그 경우 호스트 B는 과거의 연결 요청에 대한 순서 번호로 응답을 하게 되고, 호스트 A는 잘못된 순서 번호의 패킷이 왔기 때문에 그 패킷을 버리게 된다. 따라서 성공적으로 통신을 할 수 없게 된다.
이제 연결 종료를 위한 4-way handshaking에 대해 알아보자. 호스트 A가 연결 종료를 하자는 FIN 패킷을 호스트 B에게 보내면 호스트 B가 응답으로 ACK 패킷을 호스트 A에게 보낸다. 호스트 B는 이제 남은 데이터를 모두 전송한 뒤 FIN 패킷을 호스트 A에게 보내고, 호스트 A는 응답으로 ACK 패킷을 호스트 B에게 보낸다. FIN 패킷을 보낸 시점부터 더 이상 데이터 전송이 불가능하다는 것이 핵심이다. 아래 그림은 연결 종료 4-way handshaking 과정을 보여준다.
송신량과 수신 처리량을 일치시키는 것을 흐름 제어라고 한다. 수신 측이 수신 가능한 양보다 더 많은 데이터를 전송하려 해봤자 수신 측의 버퍼에 남은 공간이 없다면 힘들게 전송한 데이터를 버리게 된다. 그럴 바에는 수신 측이 처리할 수 있는 양만큼만 전송하도록 제어하는 서비스가 흐름 제어다. 이를 위해서는 수신 측에서 송신 측에게 데이터를 다 처리하고 있지 못하다는 피드백을 줄 수 있는 방법이 필요하다. 이를 위해 크게 Stop and wait 방식과 Sliding Window 방식이 사용된다. Stop and wait 방식은 1개씩 프레임을 전송하는 방식이기 때문에 효율성이 나쁘고 거의 이용되지 않으므로 Sliding Window 방식을 알아보자.
수신 측은 여유 버퍼 공간 크기를 rwnd(TCP 패킷 헤더의 Window Size 필드)라는 값으로 송신 측에게 알려준다. 송신 측은 마지막으로 보낸 데이터 순서 번호(LastByteSent)에서 마지막으로 성공적으로 송신한 데이터 순서 번호(LastByteAcked)를 뺀 것이 rwnd를 넘지 않도록 데이터를 보낸다. 이 방식은 윈도우 기반의 방식이라는 점에서 신뢰성 있는 데이터 전송을 위한 Selective Repeat 방식과 비슷하다고 볼 수 있다.
반면에 네트워크가 혼잡하다고 판단될 때 데이터 송신량을 떨어뜨리는 것을 혼잡 제어라고 한다. 네트워크망이 처리 가능한 데이터량을 넘어서는 데이터를 전송하려 하면 결국 초과되는 양의 데이터는 전송에 실패하게 되고, 신뢰성 있는 데이터 전송 알고리즘에 의해 전송 실패한 데이터를 다시 전송시도하게 된다. 이러한 악순환이 반복되어 네트워크망이 혼잡해지는 문제가 일어나지 않도록 혼잡 제어 서비스를 제공한다.
혼잡 상태가 일어났을 땐 보통 라우터의 버퍼가 오버플로우 되어 패킷이 유실되거나 라우터의 버퍼에 대기함으로 인해 평소보다 큰 딜레이가 발생되는 두 가지로 나뉜다. 두 경우 모두 ACK를 받지 못하는 패킷에 대한 타임 아웃으로 알 수 있게 된다. AIMD(Additive Increase/Multiplicative Decrease), Slow Start, Fast Retransmit, Fast Recovery 등 몇 가지 방식들이 있지만 공통적으로 송신 측의 전송 버퍼의 크기 cwnd를 차례로 키워나가다가 전송 실패가 발생할 경우 전송량을 줄인다는 점은 비슷하다. rwnd는 수신 측이 알려주는 반면 cwnd는 송신 측이 앞서 예를 든 방식들을 이용하여 적절한 값을 찾아나간다.
결국 흐름 제어와 혼잡 제어 모두 송신 측이 데이터 전송량을 조절해야 한다는 점에서는 같다. 결과적으로 rwnd보다 많이 데이터를 전송해서도, cwnd보다 많이 데이터를 전송해서도 안 되기 때문에, rwnd와 cwnd 중 작은 값만큼의 양을 전송하면 된다.
지금까지 알아본 TCP 프로토콜에 비해 UDP 프로토콜은 매우 간략하다. TCP 프로토콜에서 제공하는 Multiplexing/Demultiplexing, 신뢰성 있는 데이터 전송, 연결 제어, 흐름 제어, 혼잡 제어 등의 서비스를 제공하지 않으면 UDP 프로토콜이라고 볼 수 있다. 이러한 서비스들을 제공하지 않기 때문에 부하(Overhead)가 매우 적다. 그렇기 때문에 많은 요청을 처리해줘야 하고, 신뢰성이 떨어져도 괜찮은 서비스들에서 유용하게 쓰일 수 있다. 대표적인 예로 DNS(Domain Name Service), VoIP(음성 인터넷 프로토콜), 온라인 게임 서버 등에서 쓰인다.
UDP 프로토콜에서는 수신자가 메세지를 수신했는지 알 수 없고, 보낸 순서대로 받도록 하지도 않는다. 이러한 기능이 필요하다면 상위 프로토콜 상의 처리가 필요하다. 예를 들어 DNS 질의 요청에 대한 응답이 일정 시간 내에 오지 않으면 다시 질의를 보낸다. 실시간 스트리밍이라면 오래된 순서 번호의 패킷은 그냥 버린다. 어차피 과거의 패킷이므로 굳이 사용할 필요가 없기 때문이다.
전송 계층에서는 소켓으로 어떻게 데이터를 잘 전달할 것인지에 대해 다뤘다. 비용이 더 들긴 하지만 보낸 데이터를 그대로 받을 수 있도록 해주는 TCP 프로토콜을 이용하거나, 낮은 비용으로 데이터를 보낼 수 있지만 신뢰성이 떨어지는 UDP 프로토콜을 선택할 수도 있다. 모든 경우에 적합한 방법은 없기 때문에 문제에 따라 적절한 선택을 해야 한다. 프로토콜을 통해 프로세스에게 데이터를 잘 전달해주는 것은 OS가 도와준다. 받은 데이터를 어떻게 활용하는지는 응용 계층에게 달려있다.
네트워크 계층의 역할은 전송 계층에서 보내려는 패킷을 목적지 노드까지 도착시키는 것이다. 네트워크 계층에서는 중간 라우터를 통한 라우팅(Routing)과 포워딩(Forwarding), 필요하다면 연결 설정을 담당한다. 가능한 빠르게 데이터를 전송하기 위해서 최적의 경로로 데이터를 전송해야 하지만 그것이 쉬운 일이 아니다. 이러한 일을 해주는 IP 프로토콜에 대해서 알아볼 것이다.
라우팅과 포워딩의 차이를 알아보자. 라우터는 또 다른 라우터들과 물리적으로 연결되어 있다고 보면 된다. 라우팅은 현재 라우터에서 연결된 라우터 중 어떤 라우터로 보내는 것이 목적지로 가는데 최적인지 결정하는 것이다. 라우팅 알고리즘이 수행되고 나면 어떤 헤더를 가진 패킷을 어떤 링크로 전송하라는 정보가 정리된 포워딩 테이블이 만들어진다. 만들어진 포워딩 테이블을 기반으로 라우터는 전달받은 패킷을 다른 라우터로 포워딩한다.
네트워크 계층의 ATM, Frame Relay, X.25와 같은 모델들은 다른 라우터까지의 대역폭을 예약해둘 수도 있다. 데이터 그램이 전송되기 전 두 호스트 간의 경로를 가상 연결(Virtual Connection)로 설정하여 두 호스트가 충분한 서비스를 제공받을 수 있도록 한다. 이 글에서는 인터넷 모델을 다루기 때문에 이에 대한 설명을 생략하고, 라우팅에 대해서만 자세히 다루고자 한다.
우리가 관심 있는 네트워크 계층의 프로토콜은 크게 라우팅 프로토콜, 인터넷 프로토콜, ICMP 프로토콜로 나뉜다. 라우팅 프로토콜은 포워딩 테이블을 만들기 위한 프로토콜이고, 인터넷 프로토콜은 주소 체계를 위한 프로토콜, ICMP 프로토콜은 에러 메세지를 전달받기 위해 주로 쓰이는 프로토콜이다.
데이터그램이 전송되기 전 연결을 먼저 설정하는 방식과 달리 데이터그램 네트워크에서는 그때그때 받은 데이터그램의 목적지 주소를 확인하여 포워딩 테이블에 따라 출력 링크로 전송한다. 포워딩 테이블의 각 항목은 주소 범위와 출력 링크를 가진다. 왜 주소 범위인지 궁금하다면 IPv4의 주소 체계는 2의 32승, 40억 개가량의 항목을 가질 수 있기 때문임을 생각해보면 된다. 만약 범위가 아니라 각 주소로 포워딩 테이블을 만든다면 테이블의 크기가 너무 커서 현실적이지 않을 것이다.
받은 데이터그램에 대해 포워딩 테이블에 있는 엔트리 중 어떤 것을 선택할지 결정하기 위해 Longest Prefix Matching 방법이 이용된다. 개념적으로 생각해보면 더 세부적으로 결정되어 있는 것을 따르는 선택 규칙이라고 보면 된다.
인터넷 프로토콜은 호스트의 주소 지정과 패킷 분할 및 조립 기능을 담당한다. 데이터 링크 계층에서 전달할 수 있는 데이터의 최대 크기를 MTU(Maximum Transmission Unit)이라고 하는데, 전송하고자 하는 데이터그램이 MTU보다 크다면 데이터그램을 분할하여 offset을 지정해준다. 분할된 데이터그램들은 최종 목적지에서 재조립된다.
전송 계층 프로토콜들의 패킷에서는 포트 번호까지만 나타났지만 인터넷 프로토콜부터는 IP 주소가 나타난다. 우리가 익숙하게 알고 있던 192.168.0.1 같은 주소는 IPv4 주소 체계에 의한 주소이다. 인터넷 프로토콜은 과거부터 꾸준히 발전되어 왔는데, IPv4는 전 세계적으로 사용된 첫 번째 인터넷 프로토콜이다. 현재는 주소 공간의 고갈로 인해 IPv6로 대체되고 있다. 개념상의 이해를 쉽게 하기 위해 설명은 IPv4를 기준으로 하겠다. IPv6에서 IPv4 프로토콜이 어떻게 이용되는지는 IPv6 링크의 IPv6 전환 기술을 참고.
네트워크망 상에서 호스트 또는 라우터 간의 물리 링크 연결을 인터페이스(Interface)라고 한다. 먼저 IP 주소는 호스트에 부여되는 것이 아닌 인터페이스에 부여되는 것이라는 사실을 알아둬야 한다. 라우터는 보통 여러 노드들과 연결되어 있으므로 여러 개의 인터페이스를 갖고 있고, 호스트는 보통 하나의 유선 연결이나 무선 연결이 되어 있으므로 하나 혹은 두 개의 인터페이스를 가진다. 실제 네트워크 망에서 모든 호스트가 라우터와 직접 연결되어 있진 않고, 중간에 이더넷 스위치(Ethernet Switch)라는 것들이 여러 개의 호스트와 연결되어 있고, 이더넷 스위치와 라우터가 연결되어 있다.
IP 주소로 표현 가능한 주소가 매우 많기 때문에 관리의 효율성을 위해 서브넷이라는 개념이 도입된다. 이에 따라 IP 주소는 서브넷 부분(상위 비트), 호스트 부분(하위 비트)으로 나뉘게 된다. 서브넷에 대해 다음 항목에서 좀 더 자세히 알아보자.
IP 주소의 서브넷 부분이 같은 노드들을 모아 서브넷이라고 한다. 정의가 순환 의존 관계에 있는 것 같다... 다르게는 중계하는 라우터 없이 물리적으로 연결된(이더넷 스위치 포함) 네트워크망이라고 말할 수 있다. 서브(sub)와 네트워크(network)의 합성어인 것을 보면 알겠지만 계층적으로 하위에 존재하는 네트워크이다.
서브넷을 활용하는 것은 관리상의 이점을 가진다. 우리나라에서 중앙 정부가 모든 부분을 완전 통제하지 않고 도를 나누고, 또 도 안에서도 시를 나누어 관리하듯이 네트워크망도 계층을 나누어 관리하는 것이 더 수월하다. 이처럼 하위 네트워크망(서브넷)의 관리를 맡겨버리는 방법을 서브네트워킹(Subnetworking)이라고 한다.
서브넷을 활용하기 위해서는 IP 주소에서 어디까지가 서브넷 부분인지 알려줄 필요가 있다. "223.1.1.0/24"와 같은 표현은 "223.1.1.0" 주소의 상위 24bit가 서브넷 부분임을 알려주고, "/24"를 서브넷 마스크라고 한다. 아마 네트워크 설정에서 아마 한 번쯤 봤을 것이다. 상위 24bit가 서브넷 부분이므로 이를 위한 서브넷 마스크를 주소로 표현하면 "255.255.255.0"이 되고, 서브넷 마스크와 IP 주소를 Bitwise AND 연산하면 서브넷 부분만 얻을 수 있게 된다.
클래스 주소 체계는 IP 주소의 서브넷 부분을 정확히 8bit, 16bit, 24bit로 나누는 방식이다. 따라서 A 클래스는 "/8", B 클래스는 "/16", C 클래스는 "/24"를 서브넷 마스크로 가진다. 그러나 이 방법은 C 클래스는 최대 254개 호스트, B 클래스는 최대 65534개의 호스트를 가질 수 있는 것에서 볼 수 있듯이 유연성이 떨어진다. 대신 "a.b.c.d/x"와 같은 형식으로 서브넷 부분이 임의의 길이를 가질 수 있도록 하는 방법을 CIDR(Classless Inter Domain Routing)이라고 한다.
지금까지의 공부로 IP 주소가 어떤 것인지 대충 이해했다. 그렇다면 호스트는 어떻게 IP 주소를 부여받을 수 있는가? 기본적으로 고정 IP와 유동 IP로 알려진 두 방법이 있다. 고정 IP는 특정 IP를 아예 호스트에 지정해버리는 방법이고, 유동 IP는 IP 주소를 동적으로 획득하는 방법이다. 네트워크에 연결됐을 때만 주소를 가지므로 IP 주소를 재활용할 수 있기 때문에 큰 장점이 있다. 특히 요즘은 모바일 기기로 잦은 네트워크 접속과 종료가 있는 상황에서는 더욱 유용하다. 유동 IP를 지원하는 가장 대중적인 방법은 DHCP(Dynamic Host Configuration Protocol) 프로토콜을 사용하는 것이다. 아마 네트워크나 공유기 설정에서도 쉽게 찾아볼 수 있을 것이다. DHCP 프로토콜에 대해 좀 더 자세한 설명은 다음 링크를 참고.
기관에서 여러 호스트로 이뤄진 서브넷을 만들어 관리하고 싶다면 주소 블록이 필요하다. 주소 블록은 어떻게 획득할까? 답은 "인터넷 서비스 공급자 ISP(Internet Service Provider)에게 돈 주고 산다"이다. ISP는 더 상위의 IP 주소 관리 단체(Internet Corporation for Assigned Names and Numbers, ICANN)로부터 주소 블록을 발급받는다. 그리고 발급받은 주소 블록을 쪼개어 고객들에게 주소 블록을 판다.
ISP는 인터넷 망에게 자신의 주소 블록을 목적지로 하는 패킷들을 모두 보내라고 광고한다. 넓은 서브넷상으로 라우팅 된 패킷이 ISP 망 내에 도착하면 또다시 더 좁은 서브넷으로 라우팅 되기를 반복할 것이다. 이러한 방식을 Hierarchical Addressing이라고 한다.
그런데 만약 어떤 기관이 다른 ISP로 이전한다면? 이전한 ISP가 아예 다른 주소 블록을 라우팅 하도록 광고하고 있었다면 Hierarchical Addressing으로 해당 기관에게 라우팅 하지 못하게 된다. 이 경우 ISP는 인터넷 망에게 추가적인 주소 블록도 광고한다. 더 좁은 서브넷은 서브넷의 길이가 더 길어질 것이므로 앞서 언급했었던 Longest Prefix Matching 방법에 의해 적절한 ISP로 라우팅 될 것이다.
NAT는 IP 주소 하나를 갖고 여러 호스트를 관리하는 방법이다. NAT 장비 내부에는 지역 네트워크가 형성되고, 지역 네트워크 상의 주소를 NAT 장비가 관리한다. 지역 네트워크에서 패킷이 외부로 나갈 때 NAT 장비를 거치게 되고, 이때 패킷의 출발 주소가 NAT 장비의 주소로 변경된다. 하나의 IP로 여러 호스트를 관리하기 때문에 NAT 장비는 출력하는 패킷의 포트 번호를 다른 번호로 바꿔버린다. 이렇게 하면 나중에 지역 네트워크로 진입하고자 하는 패킷의 목적지 포트 번호를 확인하여 적절한 지역 네트워크 호스트를 찾을 수 있게 된다.
NAT는 IP 주소를 효율적으로 사용할 뿐만 아니라 내부 장비들의 IP 주소를 숨김으로써 보안상의 이점도 가진다. 멀리 찾을 필요 없이 우리에게 가장 가까운 NAT 장비는 무선 기지국이다. 스마트폰이 셀룰러 네트워크에 연결되는 것이 기지국이라는 NAT에 연결되는 것이라고 보면 된다. NAT는 그 자체로도 복잡한 주제이므로 더 자세히 다루지 않겠다. 대신 홀 펀칭이라는 흥미로운 NAT 통과 기법을 소개한다. NAT는 내부에서 외부로 나가는 패킷이 발생하지 않으면 외부에서 내부로 들어오는 패킷을 막아버린다는 점에서 P2P 연결을 어렵게 하는데, 홀 펀칭 기법은 이를 해결하는 기법이다.
ICMP 프로토콜은 호스트와 라우터 사이의 네트워크 계층 정보를 통신하기 위해 사용되는 프로토콜이다. ping, traceroute 프로그램에서 쓰이는 것이 익숙하다. 특히 traceroute는 어떤 경로를 거쳐 호스트에 도착하는지 확인할 수 있어서 네트워크 관련 문제를 디버깅하고자 할 때 유용하게 쓰이곤 한다. 각 프로그램의 더 자세한 설명은 찾아보기 쉬우니 따로 더 다루지 않는다.
라우팅 알고리즘은 크게 Link State, Distance Vector, Hierarchical Routing 세 가지로 나뉜다. Link State는 주기적으로 링크의 비용을 주변 라우터들에게 알려주면(Link state broadcast) 최소 비용 경로를 다시 계산하는 방식이다. 그 익숙한 다익스트라의 최단 경로 알고리즘이 쓰인다. 라우터가 모든 목적지까지의 경로를 저장해야 하기 때문에 메모리가 많이 소모되고, 복잡한 그래프에 대해 최단 경로 알고리즘을 수행해야 하기 때문에 CPU가 많이 쓰인다. 그러나 모든 목적지까지의 경로를 알고 있으므로 라우팅 테이블 변화에 따른 전파 속도가 빠르다. OSPF(Open Shortest Path First) 프로토콜이 대표적이다. OSPF 프로토콜에 대한 자세한 설명은 다음 링크를 참고.
Distance Vector는 목적지까지의 모든 경로를 저장하지 않고 목적지까지의 거리(Hop count)와 목적지까지 가기 위해 어떤 이웃 라우터를 거쳐야 하는지 방향(Vector)만을 저장하는 방식이다. 이를 위해 벨먼-포드 알고리즘이 사용된다. 적은 정보만을 저장해도 되기 때문에 메모리가 절약되지만 라우팅 테이블에 변화가 생겼을 때 전파되는데 시간이 오래 걸리고, 정해진 시간마다 라우팅 테이블을 업데이트해야 하기 때문에 트래픽 낭비가 크다. RIP(Routing Information Protocol) 프로토콜이 대표적이다.
Hierarchical Routing은 앞서 언급한 Hierarchical Addressing과 비슷하다. 수많은 목적지에 대해 모두 라우팅 테이블을 저장하고 링크 상태를 알려주는 것은 너무 어렵다. 좀 더 쉽게 문제를 해결하기 위해 인터넷을 AS(Autonomous System)라는 좀 더 작은 구역으로 나눈다. 각 AS는 관리자에 의해 자치적으로 운영되며, 각자의 라우팅 프로토콜을 사용한다. AS 내에서의(Intra-AS) 라우팅 위한 프로토콜을 IGP(Interior Gateway Protocol)라고 하며 OSPF 프로토콜과 RIP 프로토콜이 주로 쓰인다. 반면 AS 외부로의(Inter-AS) 라우팅을 위해서는 먼저 다른 AS로 찾아갈 수 있도록 하는 라우팅이 필요하며, 이를 EGP(Exterior Gateway Protocol)이라고 한다. EGP로는 BGP(Border Gateway Protocol)이 주로 쓰인다.
이로써 네트워크 계층에서 주소 관리와 라우팅을 위한 부분들을 알아보았다. 전체 인터넷은 계층적 네트워크로 이루어져 있으며 하위 네트워크를 서브넷이라고 한다. 라우터는 전달받은 패킷의 목적지를 보고 포워딩 테이블에서 가장 적합한 인터페이스를 찾아 그 인터페이스로 패킷을 출력한다. 목적지 전부를 테이블로 만들기 어렵기 때문에 포워딩 테이블은 정확한 목적지 주소가 아닌 서브넷들로 구성되어 있다. 포워딩 테이블을 만들기 위해서 여러 가지 라우팅 알고리즘이 사용된다. 외부의 AS로 이동하기 위한 라우팅과 AS 내에서의 라우팅을 위한 프로토콜로 나뉜다. 공통적으로 라우터들은 서로 정보를 공유하며(Interworking) 최적의 라우팅을 위해 노력한다.
개인적으로 소프트웨어 개발을 위해서 데이터 링크 계층과 물리 계층까지 잘 알아야 한다고 생각하지는 않는다. 대신 이러한 일이 있구나 정도만 알고 넘어가면 되지 않을까 싶다. 따라서 이 항목은 간략하게만 쓰려고 한다.
데이터 링크 계층부터는 실제로 인터페이스를 통해 데이터 전송이 이뤄진다. 데이터 링크 계층에서는 흐름 제어(전송 계층과 다르다), 오류 제어, 반이중과 전이중 서비스를 제공한다. 인터페이스는 하나이고 양측이 함께 사용하기 때문에 양측이 골고루 사용할 수 있게끔 조율을 해줘야 공평할 것이다. 또한, 물리 계층에서 데이터 전송 시에는 물리적인 문제로 인해 잘못된 비트가 전송될 수도 있다. 이를 감지할 수 있도록 하는 서비스가 오류 감지이며, 어떤 오류 감지 기법은 수정 기능까지도 가진다.
데이터 링크 계층에서 사용되는 주소는 MAC(Media Access Control) 주소이다. IP 주소는 바뀔 수 있는 반면 MAC 주소는 네트워크 인터페이스에 할당된 고유 식별자이다. IP 패킷을 전송하기 위해 물리적 네트워크 주소인 MAC 주소가 필요한데, 이를 모를 때 ARP(Address Resolution Protocol) 프로토콜을 이용한다.
물리 계층에서는 물리적으로 비트를 전송하기 위한 일들을 한다. 그렇기에 당연히 하드웨어와 밀접한 관계에 있을 수밖에 없는 계층이다. 이더넷, 토큰 링 등이 이 계층에 포함되며 전기 신호 혹은 광 통신 등이 있다. 전기 신호는 멀리 이동할수록 약해지는데, 전기 신호를 다시 증폭시키기 위해 리피터가 사용된다. 우리 세대까지만 해도 전화선을 이용하던 것으로 익숙한 모뎀(MOdulator and DEModulator)도 이 계층에 속한다. 모뎀은 이름에 나타나 있듯이 디지털 신호를 아날로그 신호로 바꾸어 전송하고, 아날로그 신호를 다시 디지털 신호로 읽어내는 역할을 했었던 기계이다.
지금까지 패킷이 네트워크망을 거쳐 호스트에게 도착하는 여행을 알아보았다. 투명성의 원칙에 따라 호스트에 어떻게든 패킷이 올바르게 도착했다는 가정 하에 시작해보자. 네트워크 망을 통한 통신은 OS에 의해 관리되며, 실제로 우리가 프로그래밍하는 것은 OS 위에서 돌아가는 프로세스이다. 세션 계층은 프로세스가 통신을 관리하기 위한 방법에 대한 계층이다. 또한, TCP/IP 세션을 만들고 없애는 책임을 수행하는 계층이기도 하다.
프로세스가 OS에게서 데이터를 전달받기 위해서는 앞서 이미 언급한 소켓을 이용한다. 세션 계층은 추상적으로 논리적 연결에 대해 다루는 계층이므로 소켓 외에 명확히 콕 집어 이야기할만한 주제는 거의 없지만, 그 개념 자체는 응용 계층에서 재활용될 것이다.
소켓을 통해 전달받은 데이터는 아직 바이트 덩어리일 뿐이다. 문자만 해도 ASCII 코드 혹은 유니코드 등 다양한 방법으로 표현될 수 있고, 어떤 시스템은 정수 값을 16bit 만으로 표현할 수도 있다. 이렇듯 시스템마다 데이터 표현 방식이 다르기 때문에 이를 같은 데이터로 인식하기 위한 처리를 해주는 계층이 필요하며, 표현 계층이 그 기능을 수행한다. 말하자면 번역기와 같은 일을 하는 계층인 것이다.
압축과 암호화 또한 표현 계층에서의 서비스이다. 압축을 통해 전송하고자 하는 데이터를 좀 더 작게 만들어 보낼 수도 있고, 보안을 위해 암호화된 데이터로 보낼 수도 있다. TLS(Transport Layer Security)는 널리 쓰이고 있는 보안을 위한 암호 규약이다. 또는, RPC(Remote Procedure Call)를 위한 데이터 직렬화 등을 필요로 하기도 한다. XDR(eXternal Data Representation) 프로토콜이 직렬화 프로토콜의 대표적인 예이다.
표현 계층까지 거쳐 "이해할 수 있는" 데이터가 되었다. 이제 데이터를 주고받는 방법을 이용한 어떠한 일도 할 수 있다. 학부 강의에서 가장 많이 예를 드는 응용 계층의 프로토콜은 전자 메일과 관련된 SMTP(Simple MailTransfer Protocol) 프로토콜, 파일 전송을 위한 FTP(File Transfer Protocol) 프로토콜, WWW(World Wide Web) 세상의 정보를 주고받는 HTTP(HyperText Transfer Protocol) 프로토콜이다.
특히 HTTP 프로토콜은 가장 널리 쓰인다고 볼 수 있는 프로토콜 중 하나이다. 브라우저는 HTTP 프로토콜을 이용하여 지금 보고 있는 이 웹 페이지를 설명하는 HTML 문서를 받아오고, 함께 쓰인 CSS를 기반으로 페이지를 렌더링 하고, 또한 함께 쓰인 JS를 해석하여 실행함으로써 풍부한 웹 경험을 제공한다. 브라우저의 작동 원리에 대해서는 네이버 D2의 글이 아주 잘 설명되어 있어 한 번 읽어보기를 꼭 권하고 싶다.
꼭 브라우저가 아니더라도 HTTP 프로토콜이 워낙 잘 정의되어 있는 프로토콜이기 때문에 서버와 통신하기 위한 프로토콜을 별도로 정의하지 않고 HTTP 프로토콜을 그냥 이용하는 경우가 많다. 그러나 알아둬야 할 것은 뭐든지 프로토콜을 어떻게든 해석한 결과라는 것이다. 예를 들어, RESTful API는 요청 결과를 JSON(JavaScript Object Notation) 포맷으로 돌려주는 경우가 많다. 라이브러리를 이용할 때는 HTTP 응답의 Body를 JSON으로 파싱 하여 Dictionary 객체를 얻어 사용하곤 한다. 이미 HTTP 응답의 Body가 JSON 포맷으로 쓰여있다는 것을 알고 있어야 이 방법이 통한다. 실제 HTTP 응답의 Body는 바이트 덩어리일 뿐이고, 바이트 덩어리만 봐서는 무슨 내용인지 알 수 없다. 만약 서버가 XML(eXensible Markup Language) 포맷의 데이터를 줬는데 JSON으로 파싱 하려 한다면 에러가 날 것이다. 이를 명확하게 알려주기 위해 HTTP Header에 Content-Type 필드가 있는 것이다.
응용 계층에서의 프로그래밍을 위해서는 소켓 프로그래밍을 알아보면 된다. 그 자체로도 공부해야 할 것이 많을뿐더러 단순히 소켓 프로그래밍뿐만 아니라 다중 소켓 처리를 위한 방법 등 다양한 분야와 연관되므로 이 글에서 다루기 어렵다. 무엇보다 강조하고 싶은 것은 데이터를 다루는 규칙인 프로토콜을 정의하는 것과 그 프로토콜에 따라 작동하도록 프로그램을 작성하는 것이 네트워크 프로그래밍의 기본이라는 것이다.
이로써 컴퓨터 네트워크에 대한 여러 주제를 살펴보았다. 워낙 방대한 분야인 데다 생소한 용어도 많다 보니 정리하기가 쉽지 않았던 것 같다. 다음 편에서는 데이터베이스에 대해 다뤄보려고 한다.