쉽고 재밌는 정규식 이야기
PC 주문접수 프로그램에서 개인정보 보호법을 적용하기 위해 취소된 주문의 개인정보를 숨겨야 할 필요가 생겼습니다.
‘앱의 주소검색 모양새를 보아하니 서버에서 두개의 필드를 받아서 처리해야겠군!’ 이라고 생각한 것이 저의 첫번째 착각.
하지만 그런거 없고 주소정보는 한 필드에 저장하고 있습니다!
그러면 주문접수 앱은 어떻게 처리하고 있을까?
- 행정동정보를 별도로 저장하여 숨김처리할 때 대신 사용한다.
문제는?
- 행정동 정보가 자신이 적은 주소의 동과 다른 경우가 있어 혼선이 있을 수 있다.
그러면 어떻게 해야 할까?
- 기존 주소정보의 패턴을 분석하여 상세정보를 추출하자. (왜그랬어!! 과거의 나!!)
- 한방에 깔끔하게 처리하도록 정규식을 사용하자.(왜 그랬어!! 과거의 나!!)
정규식은 가장 널리 사용되는 Perl정규식을 사용하기로 합니다. perl.or.kr
교훈: 사용자는 개발자가 의도하는 방향으로 프로그램을 사용하지 않는다.
일단 해당 필드에는 주소정보만 입력된다는 전제가 있어서 문제는 더 수월해집니다.
읍,면,동 위치를 찾아서 ‘산’ 주소가 있는지 찾은 다음 몇길, 몇로 정보를 찾으면 기준이 되는 위치를 찾을 수 있습니다.
추가로 ‘지하’ 번지도 찾아봅시다.
최초의 정규식
(
(([가-힣]+(\d{1,5}|)+(읍|면|동|가|리))( |)((\d{1,5}(~|-)\d{1,5}|\d{1,5})(가|리|)|))
([ ](산(\d{1,5}(~|-)\d{1,5}|\d{1,5}))|)|(([가-R]|(\d{1,5}(~|-)\d{1,5})|\d{1,5})+(로|길))
+[ ,]+
(
((\d{1,5})|(\d{1,5}번지)|(\d{1,5}(~|-)\d{1,5})|(\d{1,5}(~|-)\d{1,5}번지))|
(((지하)\d{1,5})|((지하)\d{1,5}번지)|((지하)\d{1,5}(~|-)\d{1,5})|((지하)\d{1,5}(~|-)\d{1,5}번지))
)
)
솔직히 이런 정규식을 보면 멘붕에 빠지는것이 정상입니다. 저도 한달정도 밖에 안됐는데 기억이 나지 않아 여러번 매뉴얼을 찾아봤습니다. 정규식의 특성상 식을 보고 의도를 파악하는 것은 매우 어렵습니다.
절대 무공의 정규식 고수가 아니라면 처음부터 다시 짜거나 잘 만들어진걸 베끼는 것을 추천합니다.(벤치마크!!!)
정규식의 특성상(대괄호, 중괄호, 소괄호가 혼재하는 구조에 많은 예약어) 복잡하게 보이는 것일뿐 찬찬히 뜯어보면 별로 어렵지 않습니다.
조금씩 뜯어서 살펴볼까요?
([가-힣]+(\d{1,5}|)+(읍|면|동|가|리))
한글로 시작하고 중간에 숫자가 아예 없거나 최대 다섯자리까지 있는 경우 끝이 읍,면,동,가,리로 끝나는 위치를 찾는다.
( |)((\d{1,5}(~|-)\d{1,5}|\d{1,5})(가|리|)|)
바로 빈칸이 있거나 혹은 없을 수도 있는데 그 뒤로 숫자-숫자 혹은 숫자만 있고 뒤에 가,리가 붙는 경우를 찾는다.
([ ](산(\d{1,5}(~|-)\d{1,5}|\d{1,5}))|)|(([가-R]|(\d{1,5}(~|-)\d{1,5})|\d{1,5})+(로|길))
‘산’주소를 찾거나 로, 길로 끝나는 주소를 찾는다.
샘플로 약 100개정도 주소를 돌려봅니다. 빠짐없이 잘 동작합니다.(천재가 아닐까 잠시 생각합니다.) 추가테스트를 해보고 배포하기로 합니다.
추가 테스트를 진행하자 문제가 발생합니다.
- 동이라는 글자가 포함된 구에서 파싱이 되지 않습니다.(ex. 남동구)
- 소수점이 들어가는 동이름이 있습니다. (ex. 인천광역시 동구 송림3.5동)
- 주소란에 동부터 쓰는 사람이 있습니다. (심지어 아파트 이름부터 쓰는 사람도 있습니다.)
이쯤되자 한번의 정규식으로 추출하는 것은 비효율적이라는 결론에 도달합니다.
시, 구까지 추출하는 정규식으로 한번 거르고 그것이 성공하면 성공한 위치부터 다시 추출하기로 합니다.
시, 군, 구를 추출하는 정규식
(([가-힣]+(시|도))( |)[가-힣]+(시|군|구))
해당 정규식을 추가했음에도 분석되지 않는 주소들이 발견됩니다. 의외의 패턴을 발견합니다. 문제가 특별시와 광역시에 집중됩니다. 특별시와 광역시는 시 이름의 뒤에 ‘시’를 붙이지 않고 주소를 쓰는 경향이 있었습니다. 이놈의 광역부심! 패턴을 조금 더 수정하기로 합니다.
시, 구를 추출하는 정규식 Ver.2
(([가-힣]+(시|도)|[서울]|[인천]|[대구]|[광주]|[부산]|[울산])( |)[가-힣]+(시|군|구))
이건 잘 되는 것 같으니 원래 정규식을 다듬습니다.
동이름에 ‘.’가 들어가는 것을 찾도록 합니다.
수정하는 김에 위의 시,구를 추출하는 공식에서 빠질것을 대비해 ‘남동구’도 제외할 수 있도록 정규식을 일부 수정합니다.
([가-힣]+(\d{1,5}|\d{1,5}(,|.)\d{1,5}|)+(읍|면|동|가|리))(^구|)
한글로 시작하고 중간에 ‘숫자’, ‘숫자.숫자’, ‘숫자,숫자’가 들어가는 읍,면,동,가,리뒤에 ‘구’가 붙지 않은 것
최종 정규식
(
(([가-힣]+(\d{1,5}|\d{1,5}(,|.)\d{1,5}|)+(읍|면|동|가|리))(^구|)((\d{1,5}(~|-)\d{1,5}|\d{1,5})(가|리|)|))([ ](산(\d{1,5}(~|-)\d{1,5}|\d{1,5}))|)|
(([가-힣]|(\d{1,5}(~|-)\d{1,5})|\d{1,5})+(로|길))
)
5000건쯤 데이터를 돌려봅니다. 다 의도한대로 동작하고 있는것을 확인합니다. 하지만 안걸리는게 있으면 어떡할까요? 물론 현재의 정규식으로 100% 걸러내는건 불가능합니다. 사람이 손으로 입력한 패턴이기에 어떤 문제가 발생할지 장담할 수 없습니다.
정규식으로 패턴검색을 하는 경우 늘 오탐과 미탐사이의 밸런스에 대한 고민이 있습니다. 오탐을 줄이려고 하면 미탐이 발생하고 미탐을 줄이려고 하면 오탐이 발생합니다. 이 경우에는 어떻게 해야 할까요? 저는 미탐지 되는 경우 전체 주소를 숨김처리했습니다.
왜냐하면 처리하는 않은 경우 법적문제가 발생하니까요.(주소가 사라졌다고 욕을 먹을 수는 있겠네요.)
알아두면 좋은 것
- 제약사항을 많이 지정할 수록 정규식은 간단해지고 품질은 좋아집니다.(여기는 주소만 입력, 영문으로 들어가는 주소는 없음 등)
- 선택한 괄호의 처음과 끝을 표시해 주는 에디터를 사용합니다.
- 에디터에서 ‘I’와 ‘ㅣ’ 와 ‘|‘가 구분되는 폰트를 사용합시다.
- 위의 정규식은 보기 편하라고 줄바꿈을 해놓은 겁니다. 저렇게 그냥 넣으면 파싱이 안될수도 있습니다.
2016.08.16 19:30 추가함
- 블로그 공개 후 CTO실(연구소)에서 여러가지 피드백을 받았습니다.(고수는 가까운 곳에 있습니다.)
- 도로명 주소를 처리하면서 남아있던 코드가 같이 추가된 것과 그 외에도 여러가지 버그가 있었습니다.
- 수정된 정규식을 다시 배포합니다.