지난 편에서 *, +, ?로 반복을 표현하는 법을 배웠어요. 그런데 이 셋만으로는 아쉬운 게 있어요. "숫자가 정확히 네 개", "두 개에서 네 개 사이"처럼 구체적인 개수는 못 정하거든요. +는 그냥 "하나 이상"일 뿐 몇 개까지인지는 안 정해주잖아요. 이럴 때 쓰는 게 오늘의 주인공, 중괄호 수량자 {n}{n,m}이에요. 연도, 전화번호, 우편번호처럼 자릿수가 정해진 데이터를 다룰 때 없어서는 안 될 도구니까 확실히 익혀볼게요.

08.1 정확한 개수는 어떻게 정하나요?

방법은 아주 직관적이에요. 반복시키고 싶은 요소 뒤에 중괄호({ })를 붙이고 그 안에 숫자를 적으면 돼요. 세 가지 형태가 있어요. {n}정확히 n번, {n,m}n번에서 m번까지, {n,}n번 이상(상한 없음)이에요. 콤마 하나로 "딱 정해진 개수"와 "범위"가 갈리는 거죠.


사실 지난 편의 수량자들도 이 중괄호로 바꿔 쓸 수 있어요. +{1,}과 같고, *{0,}과 같으며, ?{0,1}과 같아요. 그러니까 중괄호는 반복을 가장 세밀하게 조절하는 도구라고 보면 돼요. 자주 쓰는 개수는 *, +, ?로 짧게 쓰고, 정확한 자릿수가 필요할 땐 중괄호를 꺼내는 거죠.

08.2 딱 N개만 어떻게 잡나요?

먼저 정확히 몇 개, {n}부터 볼게요. 네 자리 연도만 찾고 싶다고 해봐요. 숫자 \d 뒤에 {4}를 붙이면 "숫자 정확히 네 개"라는 뜻이 돼요.


import re


re.findall(r"\d{4}", "year 2024 code 12 id 98765")


# ['2024', '9876']


결과를 뜯어볼게요. 2024는 딱 네 자리라 그대로 잡혔어요. 그런데 "code 12"의 12는 두 자리뿐이라 네 개를 못 채워서 빠졌죠. 재밌는 건 마지막 "id 98765"예요. 다섯 자리인데 9876까지, 즉 앞 네 자리만 잡혔어요. {4}는 정확히 네 개씩만 떼어 가니까, 다섯 자리 중 앞에서 네 개만 가져가고 남은 5는 버린 거예요. "정확히 N개"라는 게 이렇게 칼같이 동작한다는 걸 기억해두세요.


여기서 "왜 뒤 네 자리가 아니라 앞 네 자리지?" 하고 궁금할 수 있어요. 정규식은 문자열을 왼쪽에서 오른쪽으로 훑으면서 매치를 찾거든요. 그래서 98765를 만나면 맨 앞의 9부터 세어 9876까지 네 개를 집고, 그 뒤에 남은 5는 이미 지나간 자리라 다음 매치를 위해 남겨둬요. 이 "왼쪽부터 훑는다"는 원리는 정규식 전반에 깔린 기본 동작이라, 결과가 예상과 다를 때 이걸 떠올리면 대부분 설명이 돼요.

08.3 몇 개에서 몇 개까지는요?

이번엔 범위를 정하는 {n,m}이에요. "두 자리에서 네 자리 사이의 숫자"를 찾고 싶다면 \d{2,4}라고 쓰면 돼요. 최소 두 개, 최대 네 개라는 뜻이에요.


re.findall(r"\d{2,4}", "1 12 123 1234 12345")


# ['12', '123', '1234', '1234']


하나씩 볼게요. 한 자리인 1은 최소 두 개를 못 채워서 빠졌어요. 12(두 자리), 123(세 자리), 1234(네 자리)는 범위 안이라 통째로 잡혔고요. 마지막 "12345"가 흥미로워요. 다섯 자리인데 최대가 네 개라서 1234까지만 잡혔어요. 여기서 한 가지 성질이 드러나요. 정규식은 기본적으로 가능한 한 많이 가져가려고 해요. 그래서 2~4개 중에서 2개나 3개가 아니라 최대치인 4개를 먼저 집은 거예요. 이 "최대한 많이 먹으려는" 성질은 다음에 배울 중요한 주제로 이어지니까, 지금은 "범위를 주면 상한까지 욕심낸다" 정도로 기억해두면 돼요.

08.4 최소 개수만 정할 수도 있나요?

네, 상한 없이 최소 개수만 정하고 싶을 때가 있어요. 그럴 땐 콤마 뒤를 비워서 {n,}이라고 써요. "n개 이상, 위로는 제한 없음"이라는 뜻이에요. "두 자리 이상 숫자를 다 찾기"를 해볼게요.


re.findall(r"\d{2,}", "1 12 123 1234")


# ['12', '123', '1234']


한 자리 1은 최소 두 개를 못 채워 빠졌고, 나머지는 상한이 없으니 연속된 숫자를 끝까지 다 가져갔어요. 그래서 1234도 중간에서 안 끊기고 통째로 잡혔죠. 반대로 하한을 생략한 {,4}도 쓸 수 있는데, 이건 "0개에서 4개까지"라는 뜻이에요. 정리하면 콤마의 어느 쪽을 비우느냐에 따라 "최소만" 또는 "최대만" 정할 수 있는 거예요. 콤마 하나의 위치가 이렇게 다양한 뜻을 만들어내니 신기하죠?

08.5 전화번호 형식을 딱 맞게 잡아볼까요?

중괄호가 진짜 빛나는 순간은 자릿수가 고정된 형식을 잡을 때예요. 전화번호가 대표적이죠. "국번 세 자리 - 네 자리" 형식을 정확히 잡아볼게요. \d{3}으로 세 자리, -로 붙임표, \d{4}로 네 자리를 표현하면 돼요.


re.findall(r"\d{3}-\d{4}", "call 123-4567 now")


# ['123-4567']


깔끔하게 123-4567만 잡혔어요. 앞의 call이나 뒤의 now 같은 글자는 당연히 무시됐고요. 이렇게 {n}으로 각 자리의 개수를 딱 고정하면, 전화번호나 우편번호처럼 "형식이 정해진" 데이터를 아주 정확하게 표현할 수 있어요. 만약 여기에 +를 썼다면 "숫자 여러 개"라는 뜻이라 자릿수가 안 맞는 엉뚱한 숫자까지 잡혔을 거예요. 형식을 검사할 땐 이렇게 개수를 못 박는 게 훨씬 안전해요.


한 가지 덧붙이면, 이런 형식 패턴은 "형태"만 볼 뿐 "값의 옳고 그름"까지 봐주진 않아요. 예를 들어 \d{4}는 "9999"처럼 있을 수 없는 연도도 형식만 맞으면 통과시켜요. 그래서 자릿수 검사는 정규식으로 하되, "실제로 말이 되는 값인지"는 코드에서 따로 확인하는 게 좋아요. 이건 실무에서 정규식을 쓸 때 늘 염두에 둘 습관이에요.

08.6 중괄호를 정리하면요?

오늘 배운 중괄호 수량자를 마지막으로 정리해 볼게요. 셋 다 정확한 반복 개수를 다룬다는 공통점이 있어요.


{n}정확히 n번이에요. \d{4}가 다섯 자리에서 앞 네 개만 떼어 간 게 좋은 예였죠. {n,m}n번에서 m번까지인데, 기본적으로 상한까지 욕심내서 가져가요. \d{2,4}가 다섯 자리에서 앞 네 개를 집은 게 그 성질이었어요. {n,}n번 이상이라 상한 없이 끝까지 가져가고요. 참고로 +{1,}, *{0,}, ?{0,1}과 같다는 것도 기억해두면 수량자 전체가 하나로 연결돼요.


마지막으로 실전 팁 하나 드릴게요. 자릿수가 정해진 형식을 검사할 땐 {n}과 함께 문자열의 시작과 끝을 못 박는 앵커를 같이 쓰면 훨씬 정확해져요. 예를 들어 "문자열 전체가 딱 다섯 자리 숫자인가"를 볼 땐 앞뒤를 고정해줘야, 앞뒤에 엉뚱한 글자가 섞인 입력을 제대로 걸러낼 수 있거든요. 앵커 이야기는 따로 자세히 다루니, 지금은 "개수 지정과 앵커는 검증에서 단짝"이라는 것만 살짝 챙겨두세요.


중괄호는 자릿수가 정해진 데이터, 그러니까 연도, 전화번호, 우편번호, 카드번호 같은 걸 다룰 때 없어서는 안 될 도구예요. 오늘 예제들을 직접 쳐보면서, {4}가 다섯 자리에서 왜 네 개만 가져가는지, {2,4}가 왜 상한까지 욕심내는지 눈으로 꼭 확인해보세요. 개수를 정확히 다루는 감각이 손에 붙으면, 형식이 고정된 실전 데이터를 자신 있게 걸러낼 수 있게 될 거예요.