← 기술 블로그

괄호 깊이 3단계 파싱기 만들기

Phase: 2 — 파싱 시리즈: 공공데이터 레시피 대응 스토리: 운영 블로그 9화

이 글의 배경이 되는 이야기는 9화: 괄호 안의 괄호 안의 괄호에서 읽을 수 있습니다.

이 글을 읽으면 알 수 있는 것


문제: 괄호 안에 괄호가 있다

이전 글에서 구분자로 텍스트를 분리할 때, 괄호 안의 구분자는 무시해야 한다고 했습니다. 원리 자체는 단순합니다. 그런데 실제 성분표에는 이런 데이터가 있었습니다:

쇠고기맛베이스시즈닝(정제소금, 쇠고기엑기스(쇠고기:호주산), 설탕, 마늘분)

괄호가 두 겹입니다. 시즈닝이라는 복합 원재료 안에 쇠고기엑기스가 있고, 그 안에 다시 쇠고기의 원산지가 괄호로 감싸져 있습니다.

더 깊은 경우도 있었습니다:

혼합양념(간장(대두:수입산, 밀:미국산), 설탕)

세 겹. 혼합양념 → 간장 → 원산지.

괄호가 한 겹이면 “안에 있다/밖에 있다”의 이진 판단이지만, 여러 겹이 되면 깊이를 숫자로 추적해야 합니다.


깊이 추적의 원리

글자를 하나씩 읽으면서, 현재 괄호 깊이를 세는 방식입니다.

쇠 고 기 맛 베 이 스 시 즈 닝 (  정 제 소 금 ,  쇠 고 기 엑 기 스 (  쇠 고 기  )  ,  설 탕  )
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

이 로직은 코드로 보면 20줄 남짓입니다. 한 글자씩 읽고, 여는 괄호인지 닫는 괄호인지 구분자인지 판단하고, 깊이에 따라 행동을 결정합니다.


실전에서 마주친 문제들

문제 1: 괄호 종류가 4가지

성분표에 등장하는 괄호가 소괄호 () 하나가 아니었습니다.

괄호여는닫는사용 예
소괄호()밀가루(밀:미국산)
대괄호[]합성향료[바닐린]
중괄호{}드물지만 존재
겹낫괄호일부 수입 제품

네 종류 모두 “여는 괄호” 그룹과 “닫는 괄호” 그룹으로 묶어서, 어떤 종류든 깊이를 같은 방식으로 추적합니다.

문제 2: 닫히지 않는 괄호

제조사가 입력하다가 괄호를 빠뜨린 데이터가 있었습니다. “밀가루(밀:미국산, 설탕, 소금” — 닫는 괄호가 없습니다.

이런 경우 깊이가 1인 채로 끝까지 가버려서, 이후의 모든 구분자가 무시됩니다. 결과적으로 원재료가 하나도 분리되지 않습니다.

대응으로, 닫는 괄호가 나왔을 때 깊이가 이미 0이면 음수로 빠지지 않도록 최소값을 0으로 고정했습니다. 완벽한 해결은 아니지만, 괄호 불균형 데이터에서 파싱이 완전히 멈추는 것은 방지합니다.

깊이 = max(0, 깊이 - 1)  # 음수 방지

문제 3: 괄호가 이름의 일부인 경우

합성향료(2-Methyl-2-pentenoic acid)

이건 하위 원재료가 아니라, 합성향료의 화학명입니다. 괄호 안의 내용을 별도 원재료로 추출하면 안 됩니다.

이 문제는 분리 단계가 아니라, 그 이전에 해결해야 했습니다.


갈림길: 괄호를 언제 열 것인가

전략동작문제
항상 열기모든 괄호 내용을 하위 원재료로 추출화학명, 기능 분류명까지 원재료로 잡힘
항상 보존괄호를 아예 건드리지 않음복합 원재료의 하위 성분을 놓침
사전 매칭 후 결정괄호 포함 이름이 사전에 있으면 보존, 없으면 분해사전 로딩 필요

세 번째를 선택했습니다.

원재료 표준 사전에 “합성향료(바닐린)“이 통째로 등록되어 있다면, 그건 하나의 원재료로 취급합니다. 사전에 없는 “쇠고기맛베이스시즈닝(정제소금, 쇠고기엑기스…)”은 괄호를 열어서 내부를 분석합니다.

이 전략의 핵심은 순서입니다. 사전 조회가 분해보다 먼저 일어나야 합니다.

1. 이 텍스트가 사전에 통째로 있는가? → 있으면 끝
2. 없으면 → 괄호를 열고 안쪽을 분석

순서가 바뀌면 이미 쪼개진 후라서 복원할 수 없습니다. 이 “사전 먼저, 분해는 나중에” 원칙은 처음부터 있었던 것이 아니라, 오매칭을 하나씩 잡아가면서 도달한 결론이었습니다.


괄호 안 내용의 분류

괄호를 열기로 결정했을 때, 안에 있는 것이 무엇인지도 판단해야 합니다. 크게 세 가지 경우가 있었습니다.

괄호 안 내용예시처리
하위 원재료밀가루, 설탕, 정제소금각각 원재료로 추출
원산지미국산, 호주산필터링으로 제거
기능 분류명향미증진제, 유화제필터링으로 제거

이 판단은 괄호 파싱기 자체가 하는 것이 아니라, 이후 단계의 필터(skip_patterns)가 담당합니다. 괄호 파싱기는 “안에 있는 것을 꺼내는 것”까지만 책임지고, “꺼낸 것이 원재료인지 아닌지”는 다음 단계에 맡깁니다.

역할을 나눈 이유는, 한 곳에서 전부 판단하면 로직이 복잡해지고 규칙 충돌이 생기기 때문입니다.


결과

항목수치
처리하는 괄호 종류4종 ( (), [], {}, 【】 )
최대 중첩 깊이3단계 (실제 데이터 기준)
사전 기반 보존 대상괄호 포함 사전 이름 전체

한계

괄호 종류 간 짝 맞춤을 하지 않습니다. (로 열고 ]로 닫아도 깊이가 감소합니다. 실제 데이터에서 괄호 종류가 뒤섞인 경우가 있었고, 엄격한 짝 맞춤보다 관대한 처리가 더 나은 결과를 보였습니다.

닫히지 않는 괄호를 “감지”하지는 않습니다. 음수 방지는 있지만, “이 데이터는 괄호가 불균형하다”고 경고하지는 않습니다. 파싱 결과가 예상과 다르면 원본을 직접 확인해야 합니다.

한 줄 교훈

괄호 처리에서 어려운 것은 “여는 것과 닫는 것을 세는 것”이 아니라, “이 괄호를 열어야 하는가 말아야 하는가”를 판단하는 것입니다.


다음 글: 걸러내야 할 것들 — skip_patterns 설계기