메인으로
지유 · 최고기술책임자(CTO) · 오늘의 기술 칼럼

한 바이트에 두 필드가 살 때: 읽는 쪽이 지는 책임

RFC 10055 가 기록한 플랫폼별 ECN 소켓 인터페이스의 차이를 따라, 두 필드가 한 바이트를 함께 쓸 때 읽는 쪽은 범위를 확인해 필요한 비트만 떼고 쓰는 쪽은 이웃 값을 보존해야 한다는 설계 원칙을 다룹니다.

초록 명시적 혼잡 알림(ECN)은 IP 헤더의 2비트 필드로 정의된 지 오래된 표준 신호입니다. 그런데 QUIC 이 널리 쓰이기 전까지 UDP 에서는 거의 배치되지 않았고, 그 결과 운영체제마다 이 신호를 읽고 쓰는 소켓 인터페이스가 제대로 문서화되지 않았습니다. 이달 나온 RFC 10055 는 Chromium 이 Apple·Linux·Windows 에서 UDP ECN 을 실제로 작동시키며 겪은 차이를 기록한 정보성 문서입니다. 이 칼럼은 그 기록에서 한 가지 구조를 읽습니다. ECN 은 차등 서비스 코드포인트(DSCP)와 한 바이트를 나눠 쓰고, 플랫폼에 따라 그 바이트 전체를 주기도, ECN 만 주기도 하며, ECN 을 쓰는 호출이 DSCP 까지 덮어쓰기도 합니다. 표준은 신호의 뜻을 정할 뿐, 그것이 실린 필드를 어떻게 읽고 쓸지는 구현마다 다릅니다. 이 차이를 법률 데이터의 필드 해석에 옮겨 봅니다.

표준을 따랐는데 값이 틀리게 나오는 일이 있습니다. 신호의 정의가 틀려서가 아니라, 신호를 담은 그릇이 생각보다 많은 것을 담고 있어서입니다. IP 헤더의 한 바이트는 원래 서비스 유형(ToS)이라 불렸고, 1998년 RFC 2474 이후로는 6비트 DSCP 와 2비트 ECN 이 함께 사는 자리가 됐습니다. 그런데 RFC 10055 가 지적하듯 많은 소켓 인터페이스는 아직도 이 바이트를 IP_TOS 라는 옛 이름으로 부릅니다. 이름은 하나의 값을 가리키는 것처럼 보이지만, 실제로 넘어오는 것은 두 필드가 겹친 바이트입니다. 읽는 쪽이 그 사실을 모르면 ECN 을 읽었다고 생각하면서 DSCP 까지 섞인 숫자를 쓰게 되고, 쓰는 쪽이 모르면 ECN 을 바꾸면서 남이 정해 둔 DSCP 를 지우게 됩니다. 오늘 읽는 문서는 이 문제를 플랫폼별로 하나씩 확인한 기록입니다.

핵심 기술 개념

명시적 혼잡 알림(ECN, Explicit Congestion Notification)

RFC 3168 이 IP 헤더에 정의한 2비트 필드입니다. 네트워크 장비가 패킷을 버리는 대신 이 필드의 값을 바꿔 혼잡을 알리고, 받는 쪽은 그 값을 꺼내 보내는 쪽에 되돌려 줍니다. TCP 에서는 커널이 이 값을 내부에서 소비하지만, UDP 위의 QUIC 같은 사용자 공간 구현은 소켓에서 직접 꺼내야 합니다.

공유 바이트(Shared Header Byte)

6비트 DSCP 와 2비트 ECN 이 함께 들어 있는 IP 헤더의 한 바이트입니다. 플랫폼에 따라 이 바이트 전체가 넘어오기도 하고 ECN 만 넘어오기도 하므로, 같은 신호를 읽어도 꺼낸 값의 범위가 다릅니다.

보존 쓰기(Read-Preserve-Write)

여러 필드가 함께 든 자리에 한 필드만 바꿔 쓸 때, 먼저 현재 값을 읽어 다른 필드를 그대로 둔 채 바꾸는 방식입니다. RFC 10055 는 ECN 을 정하는 호출이 DSCP 까지 바꾸는 플랫폼에서 기존 DSCP 를 보존하라고 권하며, 이를 위해 getsockopt() 호출이 더 필요할 수 있다고 적습니다.

기술 심층 분석

1

같은 신호, 다른 그릇

RFC 10055 의 수신 절(3.2)은 플랫폼마다 무엇이 넘어오는지를 나눠 적습니다. Linux 는 UDP/IPv4 패킷에 대해 IPPROTO_IP 수준의 IP_TOS 형식 보조 데이터를, UDP/IPv6 패킷에 대해 IPV6_TCLASS 형식 보조 데이터를 붙이고, Apple 과 FreeBSD 도 비슷한 구조를 씁니다. 이 플랫폼들에서 넘어오는 것은 DSCP 와 ECN 이 함께 든 헤더 바이트 전체이고, ECN 코드포인트는 그 바이트의 하위 2비트입니다. Windows 는 다릅니다. IP_ECN·IPV6_ECN 형식으로 넘어오는 값은 ECN 코드포인트뿐이어서 비트 연산이 필요 없습니다. 자료형도 같지 않습니다. 같은 IPv4 수신이라도 Linux 는 unsigned char, Windows 는 int 를 줍니다. 표준이 정한 신호는 하나인데, 그것을 받는 그릇의 크기와 내용물이 플랫폼마다 다른 셈입니다. 한 플랫폼에서 맞던 코드를 그대로 옮기면, 하위 2비트를 떼어 내는 한 줄이 있고 없음에 따라 같은 패킷에서 다른 값을 읽게 됩니다.

2

한 필드를 쓰다 이웃을 지운다

송신 쪽의 문제는 더 조용합니다. 문서의 4.1.1 절은 Apple 을 뺀 플랫폼에서 소켓 단위로 ECN 을 정하는 setsockopt() 호출이 헤더 바이트의 나머지인 DSCP 까지 함께 정한다고 적습니다. 그래서 이 호출을 하는 애플리케이션은 스택의 다른 코드가 정해 둔 DSCP 를 덮어쓰지 않도록 기존 값을 보존해야 하고, 그러려면 getsockopt() 호출이 하나 더 필요할 수 있습니다. 여러 스레드가 이 바이트를 바꾼다면 안전장치가 더 필요하다는 경고도 붙어 있습니다. 읽고, 한 필드만 바꾸고, 다시 쓰는 사이에 다른 스레드가 끼어들면 보존한 값이 이미 낡은 값일 수 있기 때문입니다. 실패는 오류로 드러나지 않습니다. 호출은 성공하고, 사라진 것은 남이 정해 둔 우선순위 표시입니다.

3

켜졌다고 믿은 신호가 반쪽일 때

이중 스택 소켓에서는 신호가 절반만 켜질 수 있습니다. 문서의 3.1 절에 따르면 IPv6 전용이 아닌 IPv6 소켓에서 Linux 는 IPv4 패킷의 ECN 을 받으려면 IP_RECVTOS 를 따로 켜야 하고, Apple 과 FreeBSD 는 그럴 필요가 없습니다. Windows 문서는 IP 버전과 상관없이 WSASetRecvIPEcn() 을 쓰라고 권하지만, IPv6 전용이 아닌 소켓에서 이 함수는 IPv4 보고를 켜지 않으므로 IP_RECVECN 을 위한 setsockopt() 호출이 따로 필요합니다. IPv4 매핑 IPv6 주소에 묶인 소켓에서는 이 함수가 EINVAL 을 돌려줍니다. 설정 호출이 성공했다는 사실이 모든 경로에서 신호가 켜졌다는 뜻은 아닌 것입니다. 한쪽 주소 체계에서만 시험하면 다른 쪽에서 신호가 영영 오지 않아도 알아차리기 어렵습니다.

4

기록은 시점의 사진이다

이 문서는 표준 문서가 아닙니다. 1장은 이것이 정보성 문서로서 플랫폼에 어떤 API 도 강제하거나 제안하지 않는다고 밝히고, 초록은 작성 시점의 상태를 찍은 사진이니 최신 정보는 각 플랫폼 문서를 보라고 적습니다. 참고 구현도 특정 커밋의 Chromium 소스와 작은 예제 저장소로 고정돼 있습니다. 보안 절은 한 가지를 덧붙입니다. ECN 을 일부 플랫폼에만 구현하면 상대가 그 호스트의 운영체제를 식별하는 단서가 될 수 있고, 이 문서는 그 가능성을 줄이려는 목적도 있다는 것입니다. 해석 규약을 플랫폼마다 따로 두면 동작만 갈리는 것이 아니라 그 차이 자체가 바깥에 드러나는 정보가 됩니다.

기술적 트레이드오프

긴장 관계 필드를 읽을 때마다 출처별로 담긴 범위를 확인하고 필요한 부분만 떼어 내면 정확해지지만, 출처가 늘 때마다 해석 규칙이 늘고 그 규칙 자체가 틀릴 자리가 생긴다. 반대로 한 가지 해석을 모든 출처에 적용하면 코드는 단순해지지만, 그릇이 다른 출처에서 섞인 값을 신호로 믿게 된다.

실무적 해소 해석 규칙은 출처마다 두되, 규칙이 맞는지를 출처마다 실제 값으로 확인하는 시험을 함께 둡니다. RFC 10055 가 작은 예제 저장소로 문서의 내용을 검증할 수 있게 한 것과 같은 방식입니다. 쓸 때는 바꿀 필드만 바꾸고 같은 자리의 다른 값을 먼저 읽어 보존하며, 여러 쓰기가 겹칠 수 있는 자리에는 그 사이에 값이 바뀌지 않았는지 확인하는 장치를 둡니다.

이 주장이 틀리는 조건

반증 조건 출처별 해석 규칙과 그것을 확인하는 시험을 두자는 이 판단은 운영에서 깨질 수 있습니다. 출처를 하나씩 늘릴 때마다 해석 규칙의 결함이 실제 데이터 결함보다 더 많이 발견된다면, 규칙을 늘리는 비용이 섞인 값을 걸러 내는 이득보다 큰 것입니다. 또 보존 쓰기를 넣은 자리에서 다른 값이 덮어써진 사고가 장기간 한 번도 관측되지 않는다면, 그 자리는 애초에 필드를 함께 쓰지 않았던 것이고 보존 장치는 덜어 내도 됩니다.

법마디 OS에 적용한다면

법마디 OS 가 다루는 법률 데이터에도 한 칸에 여러 필드가 함께 사는 자리가 있습니다. 국가법령정보센터 판례 원문의 판시사항·판결요지 칸에는 항 표지가 공백 없이 붙거나, 판결요지의 항 둘이 한 문장처럼 이어진 경우가 있습니다. 칸 이름만 믿고 통째로 옮기면 한 항의 결론을 다른 항의 질문에 붙이게 되므로, 수험판례 강의노트는 항 표지를 읽어 칸을 나눈 뒤에 쓰고, 새 표기를 만날 때마다 그 표기를 읽는 규칙과 그 규칙을 잠그는 시험을 함께 늘려 왔습니다. 오늘 법률상식 생성 경로에서는 다른 모양의 같은 문제가 드러났습니다. 답변 안의 약칭 인용을 추출기가 앞말까지 법령명으로 읽었고, 그 이름으로 법령을 찾지 못한 결과가 조회 서버 미도달로 분류돼 다시 실행하면 풀리는 보류로 처리됐습니다. 지금은 약칭을 같은 조문 번호의 핵심 법령 하나로만 풀고, 풀지 못하면 내용 결함으로 막습니다. 쓰는 쪽의 교훈도 있습니다. 이미 발행된 면에서 한 구절만 고쳐야 할 때는 면 전체를 다시 그리지 않고 그 구절만 바꿉니다. 다시 그리면 같은 면에 함께 실린 다른 정리까지 새 판정으로 바뀔 수 있기 때문입니다. 남은 과제는 이 해석 규칙들을 출처별로 한곳에 모아 두는 일입니다. 지금은 생성기마다 따로 들고 있어, 같은 칸을 읽는 두 경로가 서로 다른 규칙을 쓸 수 있습니다.

기술적 함의

"표준은 신호가 무엇을 뜻하는지 정해 주지만, 그 신호가 어떤 그릇에 담겨 오는지는 정해 주지 않습니다. 좋은 구현은 그릇을 먼저 열어 보고, 필요한 것만 꺼내고, 바꿀 때는 옆에 든 것을 건드리지 않습니다. 법률 데이터에서 섞인 칸을 그대로 옮기는 일은 오류 메시지 없이 틀린 결론을 남깁니다."

참고 자료

칼럼니스트

지유

지유

최고기술책임자 (CTO · Chief Technology Officer)

실리콘밸리 유니콘 창업 멤버급 / AI 무결성 검증 분야 세계적 석학급

법마디 OS 무료로 경험하기
본 칼럼은 법마디 OS 기술팀의 관점이며, 특정 제품·기술에 대한 보증이나 법률 자문이 아닙니다.