이 글의 배경이 되는 이야기는 9화: 괄호 안의 괄호 안의 괄호에서 읽을 수 있습니다.
이 글을 읽으면 알 수 있는 것
- 중첩 괄호를 깊이 추적 방식으로 처리하는 원리
- 괄호 4종류(소·대·중·겹낫)를 동시에 다루는 방법
- “사전 먼저, 분해는 나중에” 전략이 왜 필요한지
문제: 괄호 안에 괄호가 있다
이전 글에서 구분자로 텍스트를 분리할 때, 괄호 안의 구분자는 무시해야 한다고 했습니다. 원리 자체는 단순합니다. 그런데 실제 성분표에는 이런 데이터가 있었습니다:
쇠고기맛베이스시즈닝(정제소금, 쇠고기엑기스(쇠고기:호주산), 설탕, 마늘분)
괄호가 두 겹입니다. 시즈닝이라는 복합 원재료 안에 쇠고기엑기스가 있고, 그 안에 다시 쇠고기의 원산지가 괄호로 감싸져 있습니다.
더 깊은 경우도 있었습니다:
혼합양념(간장(대두:수입산, 밀:미국산), 설탕)
세 겹. 혼합양념 → 간장 → 원산지.
괄호가 한 겹이면 “안에 있다/밖에 있다”의 이진 판단이지만, 여러 겹이 되면 깊이를 숫자로 추적해야 합니다.
깊이 추적의 원리
글자를 하나씩 읽으면서, 현재 괄호 깊이를 세는 방식입니다.
쇠 고 기 맛 베 이 스 시 즈 닝 ( 정 제 소 금 , 쇠 고 기 엑 기 스 ( 쇠 고 기 ) , 설 탕 )
0 0 0 0 0 0 0 0 0 0 1 1 1 1 1 1 1 1 1 1 1 1 1 2 2 2 2 1 1 1 1 0
- 여는 괄호를 만나면 깊이 +1
- 닫는 괄호를 만나면 깊이 -1
- 깊이가 0일 때의 구분자만 텍스트를 자르는 기준으로 사용
이 로직은 코드로 보면 20줄 남짓입니다. 한 글자씩 읽고, 여는 괄호인지 닫는 괄호인지 구분자인지 판단하고, 깊이에 따라 행동을 결정합니다.
실전에서 마주친 문제들
문제 1: 괄호 종류가 4가지
성분표에 등장하는 괄호가 소괄호 () 하나가 아니었습니다.
| 괄호 | 여는 | 닫는 | 사용 예 |
|---|---|---|---|
| 소괄호 | ( | ) | 밀가루(밀:미국산) |
| 대괄호 | [ | ] | 합성향료[바닐린] |
| 중괄호 | { | } | 드물지만 존재 |
| 겹낫괄호 | 【 | 】 | 일부 수입 제품 |
네 종류 모두 “여는 괄호” 그룹과 “닫는 괄호” 그룹으로 묶어서, 어떤 종류든 깊이를 같은 방식으로 추적합니다.
문제 2: 닫히지 않는 괄호
제조사가 입력하다가 괄호를 빠뜨린 데이터가 있었습니다. “밀가루(밀:미국산, 설탕, 소금” — 닫는 괄호가 없습니다.
이런 경우 깊이가 1인 채로 끝까지 가버려서, 이후의 모든 구분자가 무시됩니다. 결과적으로 원재료가 하나도 분리되지 않습니다.
대응으로, 닫는 괄호가 나왔을 때 깊이가 이미 0이면 음수로 빠지지 않도록 최소값을 0으로 고정했습니다. 완벽한 해결은 아니지만, 괄호 불균형 데이터에서 파싱이 완전히 멈추는 것은 방지합니다.
깊이 = max(0, 깊이 - 1) # 음수 방지
문제 3: 괄호가 이름의 일부인 경우
합성향료(2-Methyl-2-pentenoic acid)
이건 하위 원재료가 아니라, 합성향료의 화학명입니다. 괄호 안의 내용을 별도 원재료로 추출하면 안 됩니다.
이 문제는 분리 단계가 아니라, 그 이전에 해결해야 했습니다.
갈림길: 괄호를 언제 열 것인가
| 전략 | 동작 | 문제 |
|---|---|---|
| 항상 열기 | 모든 괄호 내용을 하위 원재료로 추출 | 화학명, 기능 분류명까지 원재료로 잡힘 |
| 항상 보존 | 괄호를 아예 건드리지 않음 | 복합 원재료의 하위 성분을 놓침 |
| 사전 매칭 후 결정 | 괄호 포함 이름이 사전에 있으면 보존, 없으면 분해 | 사전 로딩 필요 |
세 번째를 선택했습니다.
원재료 표준 사전에 “합성향료(바닐린)“이 통째로 등록되어 있다면, 그건 하나의 원재료로 취급합니다. 사전에 없는 “쇠고기맛베이스시즈닝(정제소금, 쇠고기엑기스…)”은 괄호를 열어서 내부를 분석합니다.
이 전략의 핵심은 순서입니다. 사전 조회가 분해보다 먼저 일어나야 합니다.
1. 이 텍스트가 사전에 통째로 있는가? → 있으면 끝
2. 없으면 → 괄호를 열고 안쪽을 분석
순서가 바뀌면 이미 쪼개진 후라서 복원할 수 없습니다. 이 “사전 먼저, 분해는 나중에” 원칙은 처음부터 있었던 것이 아니라, 오매칭을 하나씩 잡아가면서 도달한 결론이었습니다.
괄호 안 내용의 분류
괄호를 열기로 결정했을 때, 안에 있는 것이 무엇인지도 판단해야 합니다. 크게 세 가지 경우가 있었습니다.
| 괄호 안 내용 | 예시 | 처리 |
|---|---|---|
| 하위 원재료 | 밀가루, 설탕, 정제소금 | 각각 원재료로 추출 |
| 원산지 | 미국산, 호주산 | 필터링으로 제거 |
| 기능 분류명 | 향미증진제, 유화제 | 필터링으로 제거 |
이 판단은 괄호 파싱기 자체가 하는 것이 아니라, 이후 단계의 필터(skip_patterns)가 담당합니다. 괄호 파싱기는 “안에 있는 것을 꺼내는 것”까지만 책임지고, “꺼낸 것이 원재료인지 아닌지”는 다음 단계에 맡깁니다.
역할을 나눈 이유는, 한 곳에서 전부 판단하면 로직이 복잡해지고 규칙 충돌이 생기기 때문입니다.
결과
| 항목 | 수치 |
|---|---|
| 처리하는 괄호 종류 | 4종 ( (), [], {}, 【】 ) |
| 최대 중첩 깊이 | 3단계 (실제 데이터 기준) |
| 사전 기반 보존 대상 | 괄호 포함 사전 이름 전체 |
한계
괄호 종류 간 짝 맞춤을 하지 않습니다. (로 열고 ]로 닫아도 깊이가 감소합니다. 실제 데이터에서 괄호 종류가 뒤섞인 경우가 있었고, 엄격한 짝 맞춤보다 관대한 처리가 더 나은 결과를 보였습니다.
닫히지 않는 괄호를 “감지”하지는 않습니다. 음수 방지는 있지만, “이 데이터는 괄호가 불균형하다”고 경고하지는 않습니다. 파싱 결과가 예상과 다르면 원본을 직접 확인해야 합니다.
한 줄 교훈
괄호 처리에서 어려운 것은 “여는 것과 닫는 것을 세는 것”이 아니라, “이 괄호를 열어야 하는가 말아야 하는가”를 판단하는 것입니다.