Zig 개발 로그에 Partial Operator Precedence, 우리말로 하면 부분 연산자 우선순위라는 항목이 올라왔어요. 이름은 어렵게 들리지만, 알고 보면 개발자가 매일 겪는 문제와 맞닿아 있는 주제거든요.
먼저 한 가지 밝혀둘게요. 이 글은 Zig가 정확히 어떤 규칙을 정했는지 옮긴 글이 아니에요. 세부 규칙은 원문 개발 로그에서 확인해 주시고, 여기서는 이 개념이 무엇이고 왜 언어 설계자들이 관심을 갖는지 풀어볼게요.
순위표 하나로 다 정하면 생기는 문제
대부분의 언어는 연산자마다 순위를 매긴 표를 갖고 있어요. 곱셈이 덧셈보다 먼저라는 건 누구나 알죠. 문제는 표의 나머지 부분이에요. C 코드를 하나 볼게요.
if (flags & MASK == 0) { ... }
언뜻 보면 flags와 MASK를 AND 연산한 결과가 0인지 확인하는 코드 같아요. 그런데 C에서는 ==가 &보다 먼저 계산돼서 flags & (MASK == 0)으로 해석되거든요. 컴파일은 되는데 의도와 다르게 동작하는, 아주 찾기 싫은 종류의 버그예요.
이렇게 된 데에는 역사적인 이유가 있어요. 초기 C에는 &&와 ||가 없어서 &와 |가 논리 연산까지 맡았거든요. 나중에 &&가 추가됐지만, 이미 작성된 코드와의 호환성 때문에 순위를 고치지 못했어요. Dennis Ritchie도 C의 역사를 돌아보는 글에서 이 점을 언급한 적이 있고요.
더 근본적인 문제는 모든 연산자 쌍에 순서를 정해야 한다는 점이에요. <<와 + 중 뭐가 먼저인지 바로 답할 수 있는 사람이 얼마나 될까요? 이런 조합은 코드를 쓰는 사람도, 읽는 사람도 헷갈리는데 언어는 어쨌든 답을 하나 골라둬야 해요. GCC가 -Wparentheses 경고를 제공하는 것도 이런 코드가 실수로 이어지기 쉽기 때문이에요.
부분 우선순위가 뭐냐면
이름은 수학의 부분 순서(partial order)에서 왔어요. 키 순서로 줄을 세우면 어떤 두 사람이든 비교할 수 있죠. 이게 전체 순서예요. 반면 회사 조직도에서는 팀장이 팀원보다 위에 있지만, 다른 부서 팀원끼리는 위아래를 따질 수 없어요. 이렇게 일부 쌍만 비교할 수 있는 게 부분 순서예요.
연산자에 적용하면 이렇게 돼요. 곱셈이 덧셈보다 먼저라는, 모두가 아는 규칙은 그대로 둬요. 하지만 &와 ==처럼 헷갈리는 쌍은 순서를 아예 정하지 않아요. 그러면 괄호 없이 섞어 쓴 코드는 컴파일 에러가 되죠. 아래는 이해를 돕기 위한 가상의 언어 예시예요.
a + b c // OK: 가 +보다 강하다는 건 모두가 아는 규칙
(a & b) == c // OK: 괄호로 의도를 밝힘
a & b == c // 컴파일 에러: 둘 사이에 순서가 없음
핵심은 컴파일러가 조용히 추측하지 않고 사람에게 의도를 물어본다는 거예요. 런타임에 숨어 있던 버그가 컴파일 에러로 올라오고, 고치는 방법도 괄호 한 쌍이면 끝이에요. 코드를 읽는 사람도 순위표를 외울 필요가 없어지고요.
구현도 생각보다 어렵지 않아요. 연산자 파싱에 많이 쓰는 프랫 파서(Pratt parser)는 연산자마다 결합력 숫자를 두고 크기를 비교하는 방식인데요, 숫자 비교 대신 관계를 조회하도록 바꾸면 돼요.
def check(left_op, right_op):
order = relation(left_op, right_op) # TIGHTER, LOOSER, NONE 중 하나
if order == NONE:
raise SyntaxError('괄호로 순서를 밝혀 주세요')
return order
다른 언어들은 어떻게 풀었을까
사실 이 아이디어는 이미 여러 곳에서 쓰이고 있어요. Swift는 연산자를 우선순위 그룹으로 묶고 higherThan, lowerThan으로 그룹 사이의 관계를 선언해요. 관계가 없는 그룹의 연산자를 괄호 없이 섞으면 컴파일 에러가 나고요. 구글 엔지니어들이 시작한 C++ 후속 실험 언어 Carbon도 설계 문서에서 부분 우선순위를 채택했어요.
자바스크립트 개발자라면 이미 경험했을지도 몰라요. ?? 연산자는 ||, &&와 괄호 없이 섞을 수 없거든요.
a ?? b || c // SyntaxError
(a ?? b) || c // OK
Rust와 Go는 다른 길을 택했어요. 전체 순위표는 유지하되 C의 실수를 고친 거죠. 두 언어에서는 a & b == c가 (a & b) == c로 읽혀요. 표를 고치는 방식은 코드가 간결하지만, 언어마다 순서가 달라서 여러 언어를 오가는 사람은 여전히 헷갈릴 수 있어요. 반대로 부분 우선순위는 의도가 분명해지는 대신 괄호가 늘어나고, 이미 쓰이는 언어에 도입하면 기존 코드가 컴파일되지 않을 수 있다는 부담이 있어요.
Zig는 원래도 a == b == c처럼 비교 연산자를 이어 쓰는 코드를 문법에서 허용하지 않았어요. 애매한 코드는 처음부터 막겠다는 성향이 있는 언어라서, 이 주제를 다루는 게 어색하지 않아요.
한국 개발자에게 주는 시사점
Zig를 쓰지 않아도 오늘 바로 적용할 수 있는 게 있어요. C나 C++ 프로젝트라면 -Wall을 켜서 -Wparentheses 경고를 받아보세요. 자바스크립트나 타입스크립트 프로젝트라면 ESLint의 no-mixed-operators 규칙이 있어요. 이 규칙은 사실상 부분 우선순위를 린터로 구현한 셈이에요. 팀 컨벤션으로 비트 연산이나 &&, ||를 섞을 때 괄호를 필수로 정하는 것도 좋은 방법이고요.
언어가 어떻게 만들어지는지 궁금한 분이라면 직접 작은 파서를 만들어 보는 걸 추천해요. Crafting Interpreters 같은 책을 따라가며 표현식 파서를 만든 다음, 순위표를 관계표로 바꿔보면 이 개념이 금방 손에 익을 거예요.
마무리
한 줄로 정리하면, 부분 우선순위는 헷갈리는 조합을 언어가 추측하지 않고 개발자에게 명시하게 만드는 설계예요.
여러분은 어떻게 생각하세요? 괄호를 강제하는 언어가 더 안전하다고 느끼시나요, 아니면 표를 외워서 간결하게 쓰는 쪽이 낫다고 보시나요? a && b || c 같은 코드를 리뷰에서 만나면 괄호를 요청하시는지도 궁금해요.
🔗 출처: Hacker News