소켓(Socket)은 이름이 익숙한 개념이다. 개발을 시작한 지 얼마 안 됐어도 어디선가 한 번은 듣는다. 채팅을 만든다고 하면 소켓 이야기가 나오고, 실시간 알림을 붙인다고 하면 또 나온다.
그런데 "소켓이 뭐냐"고 물으면 답이 잘 나오지 않는다. 단어는 익숙한데 정확히 설명하려면 막힌다. 그래서 이 글에서는 소켓이라는 개념을 끝까지 파헤쳐서 완전히 잡아 두려고 한다. 어디까지가 소켓이고 어디부터는 소켓이 아닌지, 그 선을 정확히 긋는 것이 목표다.
가장 흔히 도는 설명은 이것이다. HTTP는 요청하고 응답받으면 연결이 끊기는데, 소켓은 연결을 계속 유지해 준다. 그러니까 소켓은 실시간 통신을 위한 것이다.
이 설명은 틀렸다. 소켓이 하는 일과 그 위에 얹힌 프로토콜이 하는 일을 한 덩어리로 뭉쳐 놓은 문장이다. 기술 이야기를 나눌 때 "웹소켓이 뭐냐"고 물으면 꽤 많은 사람이 "HTTP는 연결이 끊기는데 웹소켓은 연결을 유지하는 것"이라고 답한다. 절반은 맞는 말이라 오히려 고치기가 어렵다. "그럼 HTTP는 소켓을 안 쓰는 건가요?" 같은 질문 앞에서 반드시 막힌다.
먼저 답을 적어 두겠다. HTTP도 소켓을 쓴다. 브라우저로 뉴스 기사 하나를 열 때도 소켓이 쓰인다. 소켓은 실시간 통신을 위한 특별한 장치가 아니라, 네트워크로 무언가를 주고받는다면 무조건 지나가는 통로다. 연결을 오래 유지하는 것은 소켓의 성질이 아니고, 그 위에 얹힌 규칙이 정하는 일이다.
이 두 문장이 읽는 사람 머릿속에서 납득되게 만드는 것이 이 글의 목표다. 컴퓨터 전공이 아니어도, 개발 1~2년차여도 읽고 나면 남도록 쓰려고 한다. 대신 쉽게 쓰려고 사실을 뭉개지는 않는다. 비유를 먼저 쓰고, 그 비유가 어디까지만 맞는지도 같이 적어 둔다.
소제목에 번호를 붙였다. 뒤에서 앞을 다시 가리킬 때 번호로 부르겠다.
1부. 소켓은 물건이 아니라 창구다
1. "채팅은 소켓으로 만든다"는 말부터
이 말은 현장에서 정말 많이 쓰인다. 채팅을 만들라는 요구를 받으면 결국 웹소켓을 검토하게 되니, 방향 자체가 어긋나지는 않는다.
그런데 이 문장을 곧이곧대로 받아들이면 이상한 결론이 따라온다. 채팅이 소켓으로 만들어진다면, 채팅이 아닌 것은 소켓으로 만들어지지 않는다는 뜻이 된다. 로그인 버튼을 누르는 것, 상품 목록을 불러오는 것, 이미지를 내려받는 것은 소켓과 무관한 일이 된다.
사실은 정반대다. 인터넷을 타고 오가는 거의 모든 통신이 소켓을 거친다. 웹페이지 열기, 앱에서 API 호출하기, 이메일 보내기, 파일 내려받기. 전부 소켓이다.
그러니까 "채팅은 소켓으로 만든다"는 말은 "자동차는 바퀴로 굴러간다"와 비슷하다. 틀린 말은 아니지만, 자전거도 오토바이도 바퀴로 굴러간다. 바퀴는 자동차의 특징이 아니다.
그럼 채팅이 특별한 이유는 뭔가. 그건 소켓을 쓴다는 사실이 아니라, 소켓 위에서 오가는 대화의 규칙이 다르기 때문이다.
2. 소켓은 물건이 아니라 창구다
소켓을 처음 배울 때 가장 방해가 되는 게 이름이다. 소켓이라는 단어에는 이미 익숙한 뜻이 있다. 전기 콘센트의 구멍, 전구를 끼우는 자리. 뭔가 물리적인 부품이 떠오른다.
그런데 우리가 이야기하는 소켓은 부품이 아니다. 눈에 보이는 실체가 없다. 프로그램이 네트워크를 쓰려고 할 때 운영체제가 열어 주는 창구다.
우체국을 생각해 보자. 편지를 보내고 싶다고 해서 우리가 직접 우편 트럭을 몰고 상대 집으로 가지 않는다. 우체국 창구에 편지를 맡긴다. 그 다음부터는 우체국이 알아서 한다. 어떤 경로로 갈지, 어느 트럭에 실을지, 중간에 어디를 거칠지 우리는 모르고 알 필요도 없다. 우리가 아는 건 창구가 어디 있는지, 그리고 거기에 편지를 어떻게 맡기는지뿐이다.
소켓이 그 창구다. 프로그램은 네트워크 카드를 직접 만지지 않는다. 신호를 어떻게 전기로 바꿀지, 데이터를 어떤 크기로 쪼갤지, 중간에 유실되면 어떻게 다시 보낼지를 프로그램이 알 필요가 없다. 그건 운영체제의 일이다. 프로그램은 창구에 데이터를 넣고, 창구에서 데이터를 꺼낸다.
3. 왜 꼭 운영체제를 거쳐야 하나
여기서 자연스러운 의문이 생긴다. 왜 굳이 운영체제를 통해야 하나. 프로그램이 직접 하면 안 되나.
두 가지 이유가 있다.
첫째, 네트워크 카드는 컴퓨터에 하나뿐이거나 몇 개뿐인데 프로그램은 수십 개가 동시에 돌아간다. 브라우저와 카카오톡과 백그라운드 업데이트 프로그램이 다 같이 인터넷을 쓴다. 이들이 각자 네트워크 카드를 직접 만지면 서로 충돌한다. 누군가 중재해야 한다. 그게 운영체제다.
둘째, 보안이다. 프로그램이 네트워크 카드를 직접 제어할 수 있으면, 다른 프로그램의 통신을 훔쳐보거나 가로챌 수 있다. 그래서 하드웨어를 직접 만지는 권한은 운영체제만 갖는다. 프로그램은 운영체제에 부탁하는 방식으로만 쓸 수 있다.
이렇게 "직접 하지 말고 부탁하는 창구"를 두는 걸 추상화라고 부른다. 어려운 말이지만 뜻은 단순하다. 복잡한 걸 감춰 두고, 쓰는 사람에게는 간단한 사용법만 노출하는 것이다. 자동차 운전이 그렇다. 엔진에서 어떤 폭발이 일어나는지 몰라도 페달만 밟으면 간다.
4. 그래서 한 줄로 줄이면
지금까지를 한 문장으로 만들면 이렇다.
소켓은, 프로그램이 네트워크로 데이터를 주고받기 위해 운영체제가 제공하는 통신 창구다.
여기에 하나만 덧붙이면 정의가 완성된다. 통신에는 항상 양쪽이 있다. 편지를 보내는 사람과 받는 사람. 그래서 소켓은 통신의 한쪽 끝을 가리킨다. 끝점이라는 뜻으로 엔드포인트(endpoint)라고 부른다.
내 컴퓨터 상대 컴퓨터
[프로그램] [프로그램]
│ │
┌──┴──┐ ┌──┴──┐
│소켓 │ ◄────────── 연결 ──────────► │소켓 │
└──┬──┘ └──┬──┘
│ │
[운영체제] [운영체제]
│ │
[네트워크 카드] ─────── 인터넷 ────── [네트워크 카드]
이 그림에서 소켓은 양쪽에 하나씩 있다. 그리고 그 사이를 잇는 게 연결이다. 소켓과 연결은 다른 것이라는 게 이 그림의 핵심인데, 이 구분이 3부의 주제다.
일단 여기까지가 1부다. 소켓은 부품이 아니라 창구이고, 통신의 한쪽 끝을 가리킨다. 이제 왜 이 창구가 "파일"이라고 불리는지로 넘어간다.
2부. "소켓은 파일이다"라는 말은 무슨 뜻인가
5. 창구를 열면 번호표를 받는다
소켓 이야기를 검색하면 이런 문장을 자주 만난다. "리눅스에서 소켓은 파일이다." 또는 "소켓은 파일 디스크립터로 표현된다."
처음 보면 당황스럽다. 통신하는 창구가 왜 파일인가. 내 컴퓨터에 뭔가 파일이 생기는 건가.
여기서도 비유가 도움이 된다. 옷 보관소를 떠올려 보자. 코트를 맡기면 직원이 번호표를 준다. 47번. 그 다음부터 나는 코트가 어느 행거의 몇 번째에 걸려 있는지 알 필요가 없다. 나갈 때 47번을 내밀면 코트가 나온다.
운영체제도 똑같이 한다. 프로그램이 "소켓 하나 열어 줘"라고 부탁하면, 운영체제는 소켓의 실체를 자기 내부에 만들어 두고 프로그램에게는 번호표만 준다. 그 번호표가 파일 디스크립터(File Descriptor), 줄여서 FD다.
프로그램이 받는 건 그냥 정수 하나다. 3이라든가 7이라든가. 그 다음부터 프로그램은 "3번 창구에 이 데이터를 넣어 줘", "3번 창구에 도착한 데이터를 꺼내 줘"라고 말한다. 소켓의 실체는 계속 운영체제 안에 있고, 프로그램은 번호만 들고 있다.
6. 0번, 1번, 2번은 이미 예약되어 있다
FD가 정수라는 걸 알면 재미있는 사실 하나가 따라온다. 번호가 0부터 시작하는데, 앞의 세 개는 프로그램이 시작할 때부터 이미 정해져 있다.
0번 → 표준 입력 (키보드로 들어오는 것)
1번 → 표준 출력 (화면에 나가는 것)
2번 → 표준 에러 (에러 메시지가 나가는 것)
그래서 프로그램이 처음 소켓을 열면 보통 3번을 받는다. 앞의 세 자리는 이미 차 있으니 그 다음 빈 번호가 배정되는 것이다.
여기서 그냥 넘어가면 이건 쓸데없는 상식으로 남는다. 중요한 건 그 다음이다. 이 번호들은 특정 장치에 고정된 이름이 아니라, 무엇이든 끼울 수 있는 자리다.
예를 들어 0번은 표준 입력이지만 반드시 키보드인 건 아니다. 프로그램을 실행할 때 0번 자리에 키보드 대신 파일을 끼워 넣을 수 있다. 그러면 그 프로그램은 키보드를 기다리는 대신 파일 내용을 읽는다. 프로그램 코드는 한 글자도 안 바뀐다. 자기가 읽는 게 키보드인지 파일인지 모르기 때문이다. 프로그램은 그냥 "0번에서 읽어 달라"고 운영체제에 부탁할 뿐이다.
터미널을 좀 써 본 사람이라면 이미 이 번호를 쓰고 있었다. 명령 뒤에 붙이는 기호들이 그것이다.
command < input.txt 0번 자리에 파일을 끼운다 (키보드 대신 파일에서 읽기)
command > output.txt 1번 자리에 파일을 끼운다 (화면 대신 파일에 쓰기)
command 2> error.txt 2번 자리에 파일을 끼운다 (에러만 따로 파일에 모으기)
command > all.txt 2>&1 2번을 1번과 같은 곳으로 보낸다
마지막 줄의 2>&1에 들어 있는 2와 1이 바로 표준 에러와 표준 출력의 번호다. 로그를 파일로 남기려고 이 기호를 써 본 적이 있다면, 그때 이미 파일 디스크립터를 직접 지정하고 있었던 것이다.
이제 왜 이게 소켓 이야기인지 보인다. 번호가 자리일 뿐이라면, 그 자리에 소켓을 끼우는 것도 가능하다. 실제로 가능하다. 화면에 글자를 쓰던 프로그램의 1번 자리에 소켓을 끼우면, 그 프로그램의 출력이 화면 대신 네트워크로 나간다. 프로그램은 자기 출력이 어디로 가는지 모른 채 그대로 동작한다.
그러니까 파일과 화면과 소켓은 서로 갈아 끼울 수 있는 부품이다. 같은 번호 체계를 쓴다는 말은 이런 뜻이다. 성격이 전혀 달라 보이는 것들이 프로그램 입장에서는 구분되지 않는다.
여기까지 오면 확인해 볼 수 있는 것도 하나 생긴다. 리눅스에서는 실행 중인 프로그램이 지금 몇 번 자리에 무엇을 끼워 두고 있는지 목록으로 볼 수 있다. 그 목록에는 열어 둔 문서 파일과 네트워크 소켓이 나란히 찍힌다. 종류를 구분해서 다른 곳에 적어 두지 않는다. 한 줄에 하나씩, 같은 형식으로 같은 목록에 있다.
그리고 이 사실은 실무에서 에러 메시지로 나타난다. 서버에 접속이 몰릴 때 "열 수 있는 파일 개수를 초과했다"는 에러가 뜨는 경우가 있다. 파일을 하나도 열지 않은 서버에서도 이 에러가 난다. 처음 보면 당황스러운데, 이유는 단순하다. 접속 하나가 소켓 하나이고, 소켓 하나가 번호 자리 하나를 차지한다. 한 프로그램이 쓸 수 있는 번호 개수에는 상한이 있고, 그 상한이 기본값으로는 크지 않은 경우가 많다. 접속이 그 상한을 넘으면 더 이상 번호를 발급받을 수 없다.
이 에러 메시지가 "소켓은 파일이다"라는 말의 가장 실용적인 증거다. 비유로 그렇게 부르는 게 아니라, 운영체제가 실제로 소켓을 파일과 같은 자원으로 세고 있다는 뜻이다.
7. 그런데 디스크에 파일이 생기는 건 아니다
"소켓은 파일이다"라는 말을 듣고 컴퓨터 어딘가에 파일이 만들어진다고 생각하면 틀렸다. 소켓을 열어도 문서 폴더에 뭔가 생기지 않는다. 하드디스크에 저장되는 것도 없다.
디스크에 있는 문서 ─┐
키보드와 화면 ├─► 전부 FD로 지목해서 읽고 쓴다 = 전부 "파일"
프로그램 사이의 통로 │
네트워크 소켓 ─┘
이걸 "모든 것은 파일이다(Everything is a file)"라는 말로 표현한다. 유닉스를 만든 사람들의 설계 철학이다.
8. 예외 하나 — 진짜로 경로가 생기는 소켓도 있다
정확하게 하기 위해 예외를 하나 밝혀야 한다.
7번에서 "소켓을 열어도 파일이 생기지 않는다"고 했는데, 이건 인터넷을 통해 다른 컴퓨터와 통신하는 소켓 이야기다. 같은 컴퓨터 안에서 프로그램끼리 대화하는 용도의 소켓도 있는데, 이건 유닉스 도메인 소켓이라고 부르고 실제로 파일시스템에 경로가 생긴다. /var/run/something.sock 같은 모양이다.
이 파일은 데이터를 담고 있지 않다. 크기가 0이다. 그냥 "여기가 그 창구입니다"라고 알려 주는 문패 역할만 한다. 그래도 파일시스템에 실제로 존재하는 항목이라, 목록에도 보이고 권한 설정도 할 수 있다.
이 예외를 굳이 적는 이유는, 이런 걸 모르면 나중에 서버 설정 파일에서 .sock 경로를 보고 혼란스러워지기 때문이다. 7번의 설명은 네트워크 소켓에 한정된 이야기라고 기억해 두면 된다.
9. 왜 굳이 같은 번호표 체계를 쓰기로 했나
파일과 소켓을 같은 번호표로 다루면 뭐가 좋은가. 말로 설명하면 잘 안 와닿는데, 실제 명령과 코드를 나란히 놓으면 바로 보인다. 세 번 확인해 보자.
먼저 터미널 명령이다. 로그에서 에러만 골라내는 grep을 예로 들어 보자.
grep "error" app.log ← 파일에서 찾는다
tail -f app.log | grep "error" ← 앞 명령이 흘려보내는 것에서 찾는다
nc localhost 9000 | grep "error" ← 네트워크 소켓에서 오는 것에서 찾는다
세 줄에서 grep "error" 부분은 완전히 같다. grep은 자기가 지금 파일을 읽는지, 다른 명령의 출력을 읽는지, 네트워크에서 오는 데이터를 읽는지 모른다. 알 필요도 없다. grep이 하는 일은 "0번 자리에서 한 줄씩 읽어서 조건에 맞으면 1번 자리로 내보낸다"뿐이다. 그 자리에 무엇이 끼워져 있는지는 grep을 실행한 사람이 정한다.
이게 왜 가능한지는 프로그램 안쪽을 보면 더 분명하다. 다음은 파일에서 읽는 코드와 소켓에서 읽는 코드다.
// 파일에서 읽는 경우
fd = open("data.txt", ...); // 번호표를 받는다. 예를 들어 3번
read(fd, buffer, 100); // 3번에서 100바이트 읽는다
close(fd); // 3번을 닫는다
// 소켓에서 읽는 경우
fd = socket(...); // 번호표를 받는다. 역시 3번
connect(fd, ...); // 상대와 연결한다
read(fd, buffer, 100); // 3번에서 100바이트 읽는다
close(fd); // 3번을 닫는다
번호표를 받는 첫 줄만 다르다. 파일은 open으로 받고 소켓은 socket으로 받는다. 그런데 그 다음부터는 read도 close도 완전히 같은 명령이다. 소켓 전용 읽기 명령 같은 건 없다. 운영체제 입장에서는 3번 자리에서 읽어 달라는 부탁일 뿐이고, 그 자리에 파일이 끼워져 있으면 디스크에서 가져오고 소켓이 끼워져 있으면 네트워크에서 가져온다.
자바로 서버를 만들어 본 사람이라면 이 구조를 이미 만지고 있었다.
// 파일에서 읽을 때
InputStream in = new FileInputStream("data.txt");
// 소켓에서 읽을 때
InputStream in = socket.getInputStream();
// 어느 쪽이든 이 함수는 그대로 쓸 수 있다
void printAll(InputStream in) {
// in이 파일인지 소켓인지 이 함수는 모른다
}
InputStream을 받는 함수를 하나 만들어 두면 파일에도 쓰고 소켓에도 쓴다. 자바가 특별히 영리해서 이렇게 된 게 아니다. 그 아래 운영체제가 이미 둘을 같은 방식으로 다루고 있으니, 그 위에 얹은 자바도 자연스럽게 같은 모양이 된 것이다.
정리하면 이 설계로 얻는 게 세 가지다.
- 배울 사용법이 하나다. 읽기, 쓰기, 닫기를 알면 대상이 문서든 화면이든 네트워크든 다룰 수 있다.
- 도구를 작게 만들어 조합할 수 있다.
grep처럼 한 가지만 하는 도구를 만들어 두면 어디에 붙여도 쓸 수 있다. - 나중에 새로운 통신 수단이 생겨도 기존 프로그램을 고치지 않는다. 번호표만 발급되면 된다.
그리고 이 세 가지가 소켓에 대해 말해 주는 게 있다. 운영체제 입장에서 소켓은 특별한 물건이 아니라는 것이다. 읽고 쓸 수 있는 여러 대상 중 하나일 뿐이고, 특별 취급이 없다.
이 감각을 갖고 3부로 가면 다음 이야기가 자연스럽게 들어온다. 소켓은 대단한 장치가 아니라 그냥 창구다. 그래서 창구를 열었다는 것이 곧 연결되었다는 뜻이 될 수 없다.
10. 그래서 소켓은 파일인가
2부를 닫으면서 처음 질문으로 돌아가 보자. 소켓은 파일인가.
"소켓은 OS 레벨의 파일이다"라는 말을 들으면 디스크에 실제 파일이 하나 만들어진다고 생각하기 쉽다. 그런 뜻이 아니다.
소켓은 운영체제가 관리하는 통신용 자원이다. 프로그램이 소켓을 만들면 운영체제는 그 자원을 가리킬 수 있는 번호를 하나 건네준다. 그 번호가 파일 디스크립터(File Descriptor, FD)다.
한 줄로 줄이면 이렇게 된다. 소켓은 운영체제가 관리하는 통신 창구이고, 파일 디스크립터는 그 창구의 번호표다.
프로그램
│
│ FD = 3
▼
┌───────────────┐
│ 운영체제 │
│ │
│ Socket │
│ "통신 창구" │
└───────────────┘
프로그램이 하는 일은 이 번호를 들고 운영체제에 부탁하는 것뿐이다. "3번 소켓에서 데이터를 읽어 줘." 또는 "3번 소켓으로 데이터를 보내 줘." 창구 안에서 실제로 무슨 일이 벌어지는지는 프로그램이 알지 못하고, 알 필요도 없다.
유닉스 계열 운영체제는 이렇게 파일 디스크립터로 읽고 쓸 수 있는 대상을 하나의 통일된 방식으로 다룬다. 그래서 소켓도 흔히 파일처럼 다룬다고 표현한다.
그러니 이렇게 정리하면 정확하다. 소켓은 파일처럼 다뤄지는 자원이다. 디스크에 실제 파일이 만들어지는 것은 아니다.
여기까지 이해했다면 이제 아주 중요한 구분 하나를 해야 한다.
소켓을 만들었다는 것과 TCP 연결이 만들어졌다는 것은 완전히 다른 사건이다.
창구를 하나 열었다고 해서 아직 손님과 이어진 게 아닌 것처럼, 소켓을 생성했다고 해서 다른 컴퓨터와 TCP 연결이 만들어진 것은 아니다.
이제 3부에서 소켓을 만드는 socket과 연결을 맺는 connect가 각각 무엇을 하는지 살펴보자.
3부. 소켓을 만들었다고 연결된 건 아니다
11. 전화기를 샀다고 통화가 되는 건 아니다
이제부터가 소켓 오해의 핵심이다.
소켓을 만드는 것과 상대와 연결되는 것은 별개의 사건이다. 소켓을 열었다고 해서 어딘가와 연결되지 않는다.
전화기를 생각하면 바로 이해된다. 전화기를 사서 책상에 올려놨다. 이제 누군가와 통화 중인가? 아니다. 전화기는 통화를 하기 위한 장치일 뿐, 그 자체가 통화가 아니다. 통화를 하려면 번호를 누르고 상대가 받아야 한다.
소켓도 똑같다. 프로그램이 운영체제에 "소켓 하나 만들어 줘"라고 부탁하면 창구가 열리고 번호표가 나온다. 그게 전부다. 아직 아무와도 연결되지 않았다. 이 시점의 소켓은 상대가 누구인지도 모르고, 심지어 자기 주소도 정해지지 않은 상태다.
소켓 만들기
↓
번호표(FD) 받음 ← 여기까지는 "전화기를 책상에 놓은" 상태
↓
아직 연결 없음
이게 왜 중요한가. "소켓 = 연결"이라고 생각하면 뒤에 나오는 이야기가 전부 헷갈리기 때문이다. 특히 4부의 서버 이야기와 6부의 웹소켓 이야기가 그렇다.
12. 클라이언트가 하는 일 — 번호를 누른다
먼저 접속하는 쪽, 즉 클라이언트를 보자. 브라우저나 앱이 여기에 해당한다.
클라이언트는 두 단계를 밟는다.
첫째, 소켓을 만든다. 전화기를 놓는 단계다. 이 일을 하는 함수 이름이 socket이다.
둘째, 상대 주소를 지정해서 연결을 요청한다. 번호를 누르는 단계다. 이쪽 함수 이름은 connect다. 상대 주소는 IP 주소와 포트 번호로 이루어진다. IP는 어느 컴퓨터인지, 포트는 그 컴퓨터의 어느 프로그램인지를 가리킨다. 아파트 동 번호와 호수 같은 관계다.
여기서 잘 안 알려진 사실이 하나 있다. 클라이언트도 포트를 갖는다. 흔히 포트는 서버만 갖는 것으로 생각하는데, 연결을 시작하는 쪽에도 포트가 필요하다. 답장이 돌아올 곳이 있어야 하기 때문이다. 이 포트는 우리가 정하지 않고 운영체제가 남는 번호 중에 하나를 골라 붙여 준다. 매번 달라진다. 이 사실이 4부 18번에서 아주 중요하게 쓰인다.
13. 세 번 왕복하고 나서야 연결이다
연결 요청을 보내면 곧바로 연결되는 게 아니다. 양쪽이 세 번 신호를 주고받는다.
클라이언트 서버
"연결할까요?" ──────────────►
◄────────── "좋습니다. 그쪽도 준비됐죠?"
"네, 됐습니다" ─────────────►
이제부터 연결됨
이걸 3-way handshake라고 부른다. 악수를 세 번 한다는 뜻이다.
왜 세 번인가. 한 번으로는 부족하기 때문이다. 내가 "연결하자"고 보냈다고 해서 상대가 그걸 받았는지 알 수 없다. 상대가 "좋다"고 답해 줘야 안다. 그런데 그 답이 나에게 도착했는지는 상대도 모른다. 그래서 내가 다시 "받았다"고 확인해 준다. 양쪽 모두가 "상대도 준비됐다"는 사실을 아는 최소 횟수가 세 번이다.
전화로 치면 이런 대화다. 번호를 누른다. 상대가 받아서 "여보세요"라고 한다. 내가 "여보세요, 들리세요"라고 답한다. 이제 서로 목소리가 들린다는 걸 양쪽이 안다.
이 세 번이 다 끝나야 연결이 성립한다. 그리고 이 연결을 TCP 연결이라고 부른다. TCP는 데이터를 순서대로, 빠짐없이 전달하기 위한 규칙 모음의 이름이다.
14. 연결은 소켓 안에 있지 않고 소켓 사이에 있다
11번부터 13번까지를 하나로 묶으면 이런 그림이 된다.
클라이언트 서버
┌────────┐ ┌────────┐
│ 소켓 │ ══════ TCP 연결 ══════ │ 소켓 │
└────────┘ └────────┘
▲ ▲
│ │
양쪽 끝점 양쪽 끝점
소켓은 양 끝에 하나씩 있고, 연결은 그 사이에 걸쳐 있다. 연결은 소켓의 속성이 아니다. 두 소켓이 맺은 관계다.
이 문장이 이 글에서 가장 중요하다. 여기서부터 5부와 6부의 오해가 풀린다.
그리고 하나 더. 소켓과 TCP도 다른 것이다. TCP는 데이터를 어떻게 나누고 순서를 맞추고 빠진 걸 다시 보낼지에 대한 규칙이다. 소켓은 그 규칙을 쓰기 위해 프로그램이 붙잡는 창구다. 규칙과 창구는 다른 층에 있다.
프로그램
│ 데이터를 넣고 꺼낸다
▼
소켓 ← 창구
│
▼
TCP ← 규칙 (순서 보장, 재전송, 흐름 제어)
│
▼
IP ← 규칙 (어느 컴퓨터로 보낼지)
│
▼
네트워크 카드
소켓을 만들 때 "TCP를 쓸 창구로 만들어 달라"고 지정할 수도 있고, 다른 규칙을 쓸 창구로 만들 수도 있다. 창구가 규칙을 고르는 것이지, 창구가 곧 규칙은 아니다.
4부. 서버는 창구를 두 종류로 쓴다
15. 콜센터 대표번호를 생각해 보자
이제 서버 쪽을 보자. 여기에 소켓 이해의 두 번째 관문이 있다.
콜센터에 전화를 걸어 본 경험을 떠올려 보자. 대표번호가 하나 있다. 그 번호로 걸면 안내 음성이 나오고, 잠시 뒤 상담원과 연결된다. 그런데 나와 상담원이 통화하는 동안에도 대표번호는 계속 살아 있다. 다른 사람이 같은 번호로 계속 전화를 걸 수 있다.
여기서 회선이 두 종류라는 걸 눈치챌 수 있다. 전화를 받아들이는 대표번호가 하나 있고, 상담원과 실제로 대화하는 회선이 통화마다 따로 있다.
서버 소켓이 정확히 이 구조다. 이걸 모르면 "서버는 8080 포트 하나로 어떻게 수천 명을 동시에 상대하나" 같은 질문에서 막힌다.
16. 주소를 붙이고, 문을 열고, 손님을 받는다
서버가 하는 일은 네 단계다. 각각 이름이 붙어 있는데, 뜻만 알면 어렵지 않다.
첫째, 소켓을 만든다. 클라이언트와 같다. 창구 하나를 연다.
둘째, 그 소켓에 주소를 붙인다. IP와 포트를 정해 주는 일이다. 예를 들어 8080번 포트를 쓰겠다고 정한다. 이 단계를 바인딩(bind)이라고 부른다. 아직 아무 일도 일어나지 않는다. 간판을 걸었을 뿐이다. 클라이언트는 12번에서 봤듯이 이 단계를 직접 하지 않는다. 운영체제가 알아서 남는 포트를 붙여 준다. 서버는 반대다. 손님이 찾아올 주소니까 반드시 정해진 번호여야 한다.
셋째, 이 소켓을 "연결 요청을 기다리는 소켓"으로 전환한다. 이걸 리슨(listen)이라고 한다. 이 순간부터 이 소켓은 데이터를 주고받는 용도가 아니라 손님을 받는 용도가 된다. 콜센터 대표번호가 개설된 시점이다.
넷째, 실제로 들어온 연결 요청을 받아들인다. 이걸 액셉트(accept)라고 한다. 그리고 여기서 결정적인 일이 벌어진다.
17. accept가 새 창구를 만든다
액셉트를 호출하면 새로운 소켓이 하나 생긴다.
이게 소켓 이해에서 가장 자주 빠지는 부분이다. 손님을 받으면 그 손님과 대화할 전용 창구가 새로 열린다. 원래의 대기용 소켓은 그대로 남아서 다음 손님을 기다린다.
콜센터로 돌아가면 이렇다. 대표번호로 전화가 들어오면 상담원 한 명이 배정되어 전용 회선으로 대화한다. 대표번호는 그 통화에 묶이지 않는다. 계속 다음 전화를 받는다.
손님이 셋 들어오면 이렇게 된다.
서버
┌──────────────┐
│ 대기용 소켓 │ 포트 8080
│ (계속 유지) │
└──────┬───────┘
│ 손님이 올 때마다 새 창구를 만든다
┌────────────┼────────────┐
▼ ▼ ▼
┌────────┐ ┌────────┐ ┌────────┐
│ 소켓 A │ │ 소켓 B │ │ 소켓 C │
└───┬────┘ └───┬────┘ └───┬────┘
│ │ │
손님 A 손님 B 손님 C
즉 서버에는 성격이 다른 두 종류의 소켓이 있다.
| 종류 | 역할 | 개수 |
|---|---|---|
| 대기용 소켓 | 연결 요청을 받아들이기만 한다. 데이터를 주고받지 않는다 | 포트당 하나 |
| 통신용 소켓 | 특정 손님 한 명과 데이터를 주고받는다 | 접속한 손님 수만큼 |
자바 코드로 보면 이 구분이 눈에 보인다.
ServerSocket serverSocket = new ServerSocket(8080); // 대기용 소켓
while (true) {
Socket socket = serverSocket.accept(); // 손님이 올 때까지 기다린다
// 오면 통신용 소켓이 나온다
// 여기서 socket으로 이 손님과 대화한다
}
ServerSocket과 Socket이 아예 다른 타입인 게 우연이 아니다. 역할이 다르니 타입도 다르다.
18. 포트 하나로 수만 명을 받는 비밀
15번에서 던진 질문에 답할 차례다. 서버 포트는 8080 하나인데, 손님이 만 명이면 서버는 이 만 개의 대화를 어떻게 구분하나.
답은 12번에서 흘려 둔 사실에 있다. 클라이언트도 포트를 갖는다.
연결 하나는 네 개의 값으로 구분된다.
출발지 IP + 출발지 포트 + 목적지 IP + 목적지 포트
서버 쪽 두 값(목적지 IP와 포트)은 모든 손님에게 같다. 하지만 손님마다 IP가 다르고, 같은 IP에서 여러 번 접속해도 출발지 포트가 매번 다르게 배정된다. 그래서 네 값을 묶으면 연결마다 유일한 조합이 된다.
연결 1 → 1.2.3.4 : 51001 → 서버 : 8080
연결 2 → 1.2.3.4 : 51002 → 서버 : 8080 ← 같은 사람, 다른 출발지 포트
연결 3 → 5.6.7.8 : 40777 → 서버 : 8080 ← 다른 사람
운영체제는 데이터가 도착할 때 이 네 값을 보고 어느 통신용 소켓에 넣어야 할지 판단한다. 아파트 우편함과 비슷하다. 같은 동으로 온 우편물도 호수가 다르면 다른 우편함에 들어간다.
그래서 "포트 하나에 연결 하나"가 아니다. 포트 하나로 수만 개의 연결을 동시에 유지할 수 있다. 서버가 한 번에 받을 수 있는 접속 수를 제한하는 건 포트가 아니라 다른 요소들이다. 열 수 있는 FD 개수 상한, 메모리, 연결마다 붙는 처리 비용 같은 것들이다.
19. 소켓이라는 단어의 두 번째 뜻
여기서 용어를 하나 정리해야 한다. 안 하고 넘어가면 나중에 문서를 읽다가 혼란스러워진다.
소켓이라는 단어는 두 가지 뜻으로 쓰인다.
하나는 지금까지 이야기한 뜻이다. 프로그램이 통신하려고 붙잡는 창구, 번호표로 지목하는 그 대상. 코드를 쓸 때 마주치는 소켓이 이쪽이다.
다른 하나는 IP 주소와 포트 번호의 조합을 가리키는 뜻이다. TCP를 정의한 초기 표준 문서에서 소켓을 이 의미로 썼다. 이 정의를 따르면 연결 하나는 "소켓 두 개의 짝"으로 표현된다. 18번의 네 값이 바로 소켓 두 개인 셈이다.
두 뜻이 완전히 다른 이야기는 아니다. 창구에는 주소가 붙으니까 연결된다. 다만 문서에서 "소켓"을 봤을 때 이게 창구를 말하는지 주소 조합을 말하는지 구분해서 읽으면 훨씬 잘 읽힌다.
여기까지가 소켓 자체에 대한 이야기다. 이제 남은 건 처음에 던진 오해를 푸는 일이다.
5부. HTTP는 소켓 위에 얹은 대화 규칙이다
20. 회선과 대화규칙은 다른 층에 있다
이제 HTTP로 넘어간다. 그리고 이 비유 하나만 잡으면 5부와 6부가 전부 풀린다.
전화 통화를 두 부분으로 나눠 생각해 보자.
하나는 회선이다. 물리적으로 목소리가 오갈 길이 열려 있는가.
다른 하나는 대화 규칙이다. 누가 먼저 말하고, 어떻게 인사하고, 언제 끊을지.
이 둘은 완전히 별개다. 회선이 열려 있어도 대화 규칙은 상황마다 다를 수 있다. 업무 통화의 규칙과 친구와의 수다 규칙이 다른 것처럼.
소켓과 TCP 연결이 회선이다. 그리고 HTTP는 그 회선 위에서 오가는 대화 규칙이다.
HTTP ← 대화 규칙 (어떻게 요청하고 어떻게 답할지)
──────────
TCP ← 회선 (순서대로, 빠짐없이 전달)
──────────
IP ← 경로 (어느 컴퓨터로)
브라우저가 웹페이지를 열 때 순서는 이렇다. 먼저 소켓을 만든다. 그 소켓으로 서버와 TCP 연결을 맺는다. 세 번 악수한다. 연결이 되면 그 위에 HTTP 형식으로 쓴 요청을 흘려보낸다. 서버는 같은 연결로 HTTP 형식의 응답을 돌려준다.
즉 HTTP는 소켓 없이는 성립하지 않는다. 1번에서 말한 "HTTP도 소켓을 쓴다"가 이 뜻이다.
21. HTTP/1.0 — 한 마디 하고 끊는다
초기 HTTP는 아주 단순하게 동작했다.
연결한다
↓
요청 하나 보낸다
↓
응답 하나 받는다
↓
연결을 끊는다
한 번 통화해서 한 마디 하고 끊는 셈이다. 문서 하나만 받아 오면 되던 시절에는 이걸로 충분했다.
문제는 웹페이지가 복잡해지면서 생겼다. 요즘 웹페이지 하나를 열면 HTML만 오는 게 아니다. 스타일 파일, 자바스크립트 파일, 이미지 여러 장, 글꼴 파일이 줄줄이 따라온다. 수십 개일 때도 있다.
이걸 초기 방식으로 하면 파일 하나마다 연결을 새로 맺어야 한다. 13번에서 본 세 번 악수를 수십 번 반복해야 한다는 뜻이다. 악수 한 번에 오가는 왕복 시간이 붙으니, 파일이 많아질수록 페이지가 느려진다. 실제 데이터를 받는 시간보다 연결을 맺고 끊는 데 쓰는 시간이 더 클 수도 있다.
전화로 치면 이렇다. 할 말이 열 가지인데 한 마디 하고 끊고, 다시 걸어서 한 마디 하고 끊고를 열 번 반복하는 것이다. 누가 봐도 비효율적이다.
22. Keep-Alive — 끊지 않고 여러 번 주고받는다
그래서 방식이 바뀌었다. 요청과 응답이 한 번 끝나도 연결을 끊지 않고 그대로 두는 것이다. 그 위에 다음 요청을 또 보낸다.
연결한다
↓
요청 → 응답
요청 → 응답
요청 → 응답 ← 같은 연결을 계속 쓴다
요청 → 응답
↓
이제 끊는다
이걸 지속 연결이라고 하고, HTTP/1.1부터는 이게 기본 동작이다. 예전 방식처럼 매번 끊고 싶으면 오히려 "끊어 달라"고 따로 알려 줘야 한다.
전화 비유로는 이렇다. 한 번 통화를 걸어서 할 말을 다 하는 것이다. 걸고 끊기를 반복하지 않는다.
23. 그런데 이건 소켓의 기능이 아니다
여기가 이 글의 핵심 논점이다.
22번에서 연결이 유지된 건 맞다. 그런데 무엇이 그렇게 만들었나.
소켓은 아무것도 하지 않았다. 소켓은 창구일 뿐이다. 창구는 원래 닫으라고 하기 전까지 닫히지 않는다. 초기 HTTP에서 연결이 매번 끊어졌던 건, 소켓이 스스로 닫혀서가 아니라 HTTP 규칙이 "응답을 보냈으면 닫는다"고 정해 뒀기 때문이다.
지속 연결은 그 규칙을 "닫지 않는다"로 바꾼 것이다. 소켓 쪽에서는 아무 변화가 없다. 애초에 소켓에는 "한 번만 쓰고 닫히는 모드" 같은 게 없다.
정리하면 이렇다.
- 연결을 유지하는 능력은 원래부터 있었다. 소켓과 TCP는 닫으라고 할 때까지 열려 있다.
- 초기 HTTP는 그 능력을 안 썼다. 규칙상 매번 닫았다.
- 지속 연결은 그 능력을 쓰기로 규칙을 바꾼 것이다.
24. 그래서 "지속 연결을 위한 소켓"은 순서가 뒤바뀐 말이다
이제 도입부에서 적어 둔 그 설명으로 돌아가 보자.
"HTTP 연결을 계속 유지하기 위한 것이 소켓"이라는 문장은 원인과 결과가 뒤집혀 있다.
소켓은 연결을 유지하기 위해 등장한 개념이 아니다. 소켓은 네트워크 통신을 하려면 무조건 있어야 하는 창구다. 연결을 오래 유지하든 곧바로 끊든 소켓은 쓰인다. 한 번 쓰고 끊는 통신에도 소켓이 있다.
바꿔 말하면, 소켓은 "얼마나 오래 연결을 유지할지"에 대해 아무 입장이 없다. 그건 위층에서 정한다.
이 문장을 기억하면 6부가 쉬워진다. 웹소켓도 결국 위층에서 정한 규칙 하나이기 때문이다.
6부. WebSocket은 소켓이 아니다 — 이름이 문제다
25. HTTP의 진짜 한계 — 서버가 먼저 말을 걸 수 없다
22번에서 HTTP도 연결을 유지할 수 있다는 걸 봤다. 그러면 채팅도 그냥 HTTP로 만들면 되지 않나. 연결이 유지되는데 왜 웹소켓이 필요한가.
여기서 HTTP의 진짜 제약이 드러난다. 연결이 끊기는 게 문제가 아니었다.
HTTP는 반드시 클라이언트가 먼저 물어야 서버가 답하는 구조다. 요청이 있어야 응답이 있다. 서버가 먼저 말을 걸 방법이 없다.
이게 채팅에서 왜 치명적인가. 상대가 나에게 메시지를 보냈다는 사실을 서버는 알고 있다. 그런데 서버가 나에게 그걸 알려 줄 수 없다. 내가 "새 메시지 있어요?"라고 물어야 한다.
전화 비유로 하면 이렇다. 회선은 열려 있는데, 규칙상 내가 질문할 때만 상대가 대답할 수 있다. 상대가 하고 싶은 말이 있어도 내가 물어 줄 때까지 기다려야 한다. 회선 문제가 아니라 대화 규칙 문제다.
26. 그래서 예전에는 계속 물어봤다
그럼 어떻게 해결했나. 계속 물어봤다.
클라이언트: "새 메시지 있어요?" → 서버: "없어요"
(1초 뒤)
클라이언트: "새 메시지 있어요?" → 서버: "없어요"
(1초 뒤)
클라이언트: "새 메시지 있어요?" → 서버: "하나 있어요"
이걸 폴링(polling)이라고 부른다. 동작은 한다. 그런데 낭비가 심하다. 대부분의 질문에 "없다"는 답이 돌아온다. 사용자가 만 명이면 1초에 만 번의 헛질문이 서버로 몰린다. 그리고 아무리 자주 물어도 최대 1초는 늦게 안다.
이걸 개선한 방법들도 있었지만, 근본적으로는 "클라이언트가 물어야 한다"는 제약 안에서의 우회였다. 필요한 건 서버가 먼저 말을 걸 수 있는 규칙이었다.
27. Upgrade — 회선은 그대로 두고 규칙만 바꾼다
웹소켓의 아이디어는 여기서 나온다.
이미 열려 있는 TCP 연결을 그대로 쓰되, 그 위에서 쓰는 대화 규칙을 HTTP에서 다른 것으로 바꾸는 것이다.
시작은 평범한 HTTP 요청이다. 다만 헤더에 특별한 부탁을 담는다. "이 연결에서 쓰는 규칙을 웹소켓으로 바꾸고 싶다"는 요청이다.
클라이언트 서버
HTTP 요청
Upgrade: websocket ─────────►
Connection: Upgrade
◄───────── HTTP 응답
101 Switching Protocols
─────── 이 지점부터 규칙이 바뀐다 ───────
웹소켓 메시지 ─────────►
◄───────── 웹소켓 메시지
◄───────── 웹소켓 메시지 ← 서버가 먼저 보낸다
웹소켓 메시지 ─────────►
여기서 눈여겨볼 곳은 가운데 선이다. 그 선을 지나면 같은 연결에서 오가는 메시지의 형식이 달라진다. 그리고 서버가 먼저 보내는 게 가능해진다.
전화 비유로 하면, 통화 중에 "이제부터 무전기처럼 씁시다"라고 합의하는 것이다. 회선을 새로 걸지 않는다. 이미 연결된 통화에서 규칙만 바꾼다.
28. 101 Switching Protocols
응답 코드 101은 실무에서 거의 볼 일이 없어서 낯설다. 뜻은 이름 그대로다. 프로토콜을 바꾸겠다.
우리가 흔히 아는 응답 코드들과 성격이 다르다. 200은 요청을 잘 처리했다는 뜻이고, 404는 없다는 뜻이다. 둘 다 "이 요청에 대한 결과"를 말한다. 101은 결과를 말하는 게 아니라 "지금부터 이 연결의 사용법이 달라진다"고 선언한다.
그래서 101 응답 이후에는 이 연결에 HTTP 요청을 보낼 수 없다. HTTP는 자기 역할을 끝내고 물러난 상태다. HTTP가 한 일은 문을 열어 준 것까지다.
29. 여기서 새 소켓이 생겼나
이제 이 글의 두 번째 핵심 질문이다.
웹소켓으로 전환하는 과정에서 소켓이 새로 만들어졌나.
아니다. 하나도 새로 만들어지지 않았다.
처음 HTTP 요청을 보낼 때 이미 소켓이 있었고, TCP 연결도 이미 맺어져 있었다. 3부에서 본 세 번 악수도 그때 이미 끝났다. 27번의 전환은 그 연결 위에서 쓰는 데이터 형식을 바꾼 것뿐이다.
계층으로 보면 이렇다.
전환 전 전환 후
HTTP WebSocket ← 이 층만 바뀌었다
───────── ─────────
TCP TCP ← 그대로
───────── ─────────
IP IP ← 그대로
───────── ─────────
소켓 소켓 ← 그대로, 같은 것
바뀐 건 맨 위 한 층이다. 아래는 전부 그대로다. 번호표도 그대로다. 프로그램이 들고 있던 FD가 3번이었다면 전환 후에도 3번이다.
30. 이름이 만든 오해
그럼 왜 웹소켓이라는 이름이 붙었을까.
소켓 프로그래밍으로 얻던 것과 비슷한 걸 브라우저에서도 쓸 수 있게 해 준다는 의미로 보인다. 양방향으로 자유롭게 메시지를 주고받는 감각이 소켓과 닮았으니까.
그런데 이 이름이 오해를 만들었다. 이름에 소켓이 들어 있으니 웹소켓이 소켓의 한 종류처럼 보인다. 그래서 "웹소켓을 쓰면 소켓이 열린다"거나 "HTTP는 소켓을 안 쓰고 웹소켓은 쓴다"는 식으로 이해하게 된다.
정확한 관계는 이렇다.
소켓 ← 운영체제가 주는 통신 창구. 모든 통신이 여기를 지난다
↑
│ 이 창구를 통해 TCP 연결을 맺는다
│
TCP 연결 ← 두 소켓 사이에 걸린 연결
↑
│ 이 연결 위에서 쓸 규칙을 고른다
│
HTTP / WebSocket ← 규칙. 둘은 같은 층의 형제 관계다
웹소켓은 소켓의 종류가 아니다. HTTP와 같은 층에 있는 형제다. 소켓 위에 얹히는 규칙 중 하나다.
31. 그래서 "채팅은 소켓 통신"은 어디가 맞고 어디가 틀렸나
1번에서 던진 질문으로 돌아가자.
틀린 부분은 소켓을 쓰는 게 채팅의 특징이라고 본 것이다. 웹페이지를 여는 것도 소켓을 쓴다. 소켓은 채팅의 특징이 아니다.
맞는 부분은 그 말이 실제로 가리키려 한 것이다. 현장에서 "소켓 통신"이라고 할 때 사람들이 뜻하는 건 보통 두 가지다. 웹소켓처럼 연결을 유지하고 양쪽이 자유롭게 메시지를 보내는 방식, 또는 HTTP를 쓰지 않고 TCP 위에 직접 데이터 형식을 정해서 주고받는 방식. 둘 다 "요청하면 응답이 오고 끝"이 아닌 통신이라는 공통점이 있다.
그러니까 그 말은 용어가 부정확한 것이지 감각이 틀린 건 아니다. 다만 정확하게 말하면 이렇게 된다.
채팅이 특별한 이유는 소켓을 쓴다는 데 있지 않다. 연결을 유지하면서 서버도 먼저 말을 걸 수 있는 규칙을 쓴다는 데 있다.
7부. 한 장으로 정리
32. 다 잊어도 이 세 문장만 남으면 된다
첫째, 소켓은 통신 창구다. 채팅이든 웹페이지든 모든 네트워크 통신이 이 창구를 지난다. 소켓은 특별한 기능이 아니라 기본 설비다.
둘째, 소켓을 만드는 것과 연결되는 것은 다른 사건이다. 창구를 열었다고 상대와 이어지지 않는다. 그리고 연결은 소켓 안에 있는 게 아니라 두 소켓 사이에 걸쳐 있다.
셋째, 연결을 얼마나 오래 유지할지, 누가 먼저 말할 수 있는지는 소켓이 정하지 않는다. 그건 위층 규칙이 정한다. HTTP 지속 연결도 웹소켓도 그 위층 이야기다.
이 세 문장이면 도입부에서 짚은 두 설명이 왜 부정확한지 스스로 설명할 수 있게 된다. 소켓이 지속 연결을 위한 개념이라는 말은 층을 헷갈린 것이고, 채팅이 소켓 통신이라는 말은 모든 통신이 소켓을 쓴다는 사실을 놓친 것이다. 누가 물어도 이 선을 그어 줄 수 있으면 이 글의 목적은 달성된 셈이다.
33. 더 알아보고 싶다면
여기서 한 걸음 더 나가고 싶다면 방향을 두 가지로 나눌 수 있다.
직접 만들어 보는 쪽이라면, 서버 소켓을 열고 손님을 받는 코드를 써 보는 게 가장 빠르다. 17번의 자바 코드 정도로 시작해서, 손님을 받으면 문자열을 하나 돌려주는 프로그램을 만들어 보면 된다. 16번의 네 단계가 코드에서 그대로 보인다.
개념을 더 파는 쪽이라면 다음 질문들이 자연스러운 다음 단계다. 손님이 여러 명일 때 서버는 어떻게 동시에 상대하나. 데이터가 아직 도착하지 않았을 때 프로그램은 기다려야 하나. 접속이 만 개가 되면 무엇이 먼저 한계에 부딪히나. 이 질문들이 블로킹과 논블로킹, 그리고 하나의 흐름으로 여러 연결을 감시하는 방식으로 이어진다.
어느 쪽이든 출발점은 같다. 소켓은 창구이고, 연결은 그 사이에 걸린 관계이고, 대화 규칙은 그 위에 얹힌 층이라는 구조다. 이 구조를 손에 쥐고 있으면 새로 배우는 것들이 어디에 놓이는지 헷갈리지 않는다.
'인프라 · 데브옵스' 카테고리의 다른 글
| 메시징·스트리밍 인프라 비교 — Amazon SQS·SNS, Apache Kafka, Amazon MSK, RabbitMQ (0) | 2026.07.26 |
|---|
