Ver3.0 Go언어 정규표현식과 regexp 패키지를 활용한 텍스트 패턴 매칭

Go언어 정규표현식과 regexp 패키지를 활용한 텍스트 패턴 매칭
🎯 정규표현식이 뭔데 이렇게 난리야?
야 솔직히 말해서ㅋㅋㅋ 정규표현식(Regular Expression) 처음 봤을 때 진짜 외계어 같지 않았어? 나도 처음엔 ^[a-zA-Z0-9]*$ 이런 거 보고 "이게 뭔 암호문이야?" 했거든ㅋㅋㅋ 근데 알고 보면 이게 진짜 개꿀템이야 🍯
정규표현식은 쉽게 말해서 텍스트에서 특정 패턴을 찾아내는 강력한 도구야. 예를 들어 수천 줄의 로그 파일에서 이메일 주소만 쏙쏙 뽑아내거나, 전화번호 형식이 맞는지 검증하거나, URL에서 특정 부분만 추출하는 거 있잖아? 이런 거 다 정규표현식으로 해결할 수 있어!
• 검색 & 추출: 원하는 텍스트만 골라내기
• 치환 & 변환: 찾은 패턴을 다른 문자열로 교체
• 유효성 검증: 입력값이 올바른 형식인지 체크
Go언어에서는 regexp 패키지가 이 모든 걸 담당해. 표준 라이브러리에 포함되어 있어서 별도 설치 없이 바로 쓸 수 있다는 게 진짜 장점이지ㅋㅋ 그리고 Go의 regexp는 RE2 문법을 사용하는데, 이게 성능도 좋고 안전해서 프로덕션 환경에서도 안심하고 쓸 수 있어 👍
📚 regexp 패키지 기본기 다지기
패키지 임포트부터 시작!
일단 Go에서 정규표현식 쓰려면 regexp 패키지를 임포트해야겠지? 기본 중의 기본이야!
package main
import (
"fmt"
"regexp"
)
func main() {
// 정규표현식 패턴 컴파일
pattern := `\d+` // 숫자 하나 이상 매칭
re, err := regexp.Compile(pattern)
if err != nil {
fmt.Println("정규표현식 컴파일 에러:", err)
return
}
text := "내 전화번호는 010-1234-5678이야"
result := re.FindString(text)
fmt.Println("찾은 숫자:", result) // 출력: 010
}
위 코드에서 regexp.Compile() 함수가 핵심이야. 이 함수는 정규표현식 패턴 문자열을 받아서 Regexp 객체로 컴파일해줘. 컴파일된 객체는 재사용할 수 있어서 성능상 이득이 크지!
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
func isValidEmail(email string) bool {
return emailRegex.MatchString(email)
}
// 사용
fmt.Println(isValidEmail("test@example.com")) // true
fmt.Println(isValidEmail("잘못된이메일")) // false
주요 메서드들 총정리 🎓
regexp 패키지에는 진짜 많은 메서드들이 있는데, 자주 쓰는 것들만 정리해볼게!
| 메서드 | 설명 | 반환값 |
|---|---|---|
| MatchString | 문자열이 패턴과 매칭되는지 확인 | bool |
| FindString | 첫 번째 매칭되는 문자열 반환 | string |
| FindAllString | 모든 매칭되는 문자열 반환 | []string |
| FindStringSubmatch | 서브매치 포함해서 반환 | []string |
| ReplaceAllString | 매칭되는 부분을 다른 문자열로 교체 | string |
| Split | 패턴을 기준으로 문자열 분리 | []string |
각 메서드마다 용도가 명확해서 상황에 맞게 골라 쓰면 돼ㅋㅋ 이제 하나씩 실전 예제로 파헤쳐보자!
🔍 실전 패턴 매칭 테크닉
1. 기본 매칭 - MatchString
가장 기본적인 거부터 시작해보자. MatchString은 그냥 "이 문자열이 패턴이랑 맞아?"를 물어보는 거야. true/false만 돌려줘서 간단명료함ㅋㅋ
package main
import (
"fmt"
"regexp"
)
func main() {
// 한글 이름 패턴 (2-4글자)
namePattern := regexp.MustCompile(`^[가-힣]{2,4}$`)
names := []string{"김철수", "이", "박영희짱짱맨", "홍길동"}
for _, name := range names {
if namePattern.MatchString(name) {
fmt.Printf("%s: ✅ 유효한 이름\n", name)
} else {
fmt.Printf("%s: ❌ 유효하지 않은 이름\n", name)
}
}
}
// 출력:
// 김철수: ✅ 유효한 이름
// 이: ❌ 유효하지 않은 이름
// 박영희짱짱맨: ❌ 유효하지 않은 이름
// 홍길동: ✅ 유효한 이름
여기서 ^[가-힣]{2,4}$ 패턴을 보면:
• ^: 문자열의 시작
• [가-힣]: 한글 자모 범위
• {2,4}: 2개 이상 4개 이하
• $: 문자열의 끝
2. 문자열 찾기 - FindString & FindAllString
이제 좀 더 재밌는 거 해보자! 텍스트에서 특정 패턴을 찾아내는 거야. FindString은 첫 번째 매칭만, FindAllString은 모든 매칭을 찾아줘 🎯
package main
import (
"fmt"
"regexp"
)
func main() {
text := `
문의사항은 support@example.com으로 보내주세요.
마케팅 담당자: marketing@company.co.kr
개발팀: dev@tech-startup.io
잘못된형식@
`
// 이메일 패턴
emailPattern := regexp.MustCompile(`[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}`)
// 첫 번째 이메일만 찾기
firstEmail := emailPattern.FindString(text)
fmt.Println("첫 번째 이메일:", firstEmail)
// 모든 이메일 찾기
allEmails := emailPattern.FindAllString(text, -1) // -1은 제한 없이 모두 찾기
fmt.Println("\n모든 이메일:")
for i, email := range allEmails {
fmt.Printf("%d. %s\n", i+1, email)
}
}
// 출력:
// 첫 번째 이메일: support@example.com
//
// 모든 이메일:
// 1. support@example.com
// 2. marketing@company.co.kr
// 3. dev@tech-startup.io
FindAllString의 두 번째 파라미터가 중요한데, 이게 찾을 최대 개수야. -1을 넣으면 제한 없이 다 찾고, 양수를 넣으면 그 개수만큼만 찾아줘!
3. 서브매치로 그룹 캡처하기 🎣
이게 진짜 꿀기능인데ㅋㅋㅋ 정규표현식에서 괄호 ()로 묶으면 그 부분을 따로 캡처할 수 있어. 예를 들어 전화번호에서 지역번호, 국번, 번호를 각각 분리하고 싶을 때 쓰면 딱이야!
package main
import (
"fmt"
"regexp"
)
func main() {
text := "연락처: 010-1234-5678, 02-987-6543"
// 괄호로 그룹 지정
phonePattern := regexp.MustCompile(`(\d{2,3})-(\d{3,4})-(\d{4})`)
// FindStringSubmatch: 첫 번째 매칭과 서브매치 반환
match := phonePattern.FindStringSubmatch(text)
if match != nil {
fmt.Println("전체 매칭:", match[0])
fmt.Println("첫 번째 그룹 (지역번호):", match[1])
fmt.Println("두 번째 그룹 (국번):", match[2])
fmt.Println("세 번째 그룹 (번호):", match[3])
}
fmt.Println("\n--- 모든 전화번호 파싱 ---")
// FindAllStringSubmatch: 모든 매칭과 서브매치 반환
allMatches := phonePattern.FindAllStringSubmatch(text, -1)
for i, match := range allMatches {
fmt.Printf("\n전화번호 %d:\n", i+1)
fmt.Printf(" 전체: %s\n", match[0])
fmt.Printf(" 지역번호: %s\n", match[1])
fmt.Printf(" 국번: %s\n", match[2])
fmt.Printf(" 번호: %s\n", match[3])
}
}
// 출력:
// 전체 매칭: 010-1234-5678
// 첫 번째 그룹 (지역번호): 010
// 두 번째 그룹 (국번): 1234
// 세 번째 그룹 (번호): 5678
//
// --- 모든 전화번호 파싱 ---
//
// 전화번호 1:
// 전체: 010-1234-5678
// 지역번호: 010
// 국번: 1234
// 번호: 5678
//
// 전화번호 2:
// 전체: 02-987-6543
// 지역번호: 02
// 국번: 987
// 번호: 6543
서브매치 결과는 슬라이스로 반환되는데, 인덱스 0은 전체 매칭이고, 1부터는 각 그룹이야. 이거 진짜 유용해서 URL 파싱이나 로그 분석할 때 엄청 많이 써!
4. 명명된 그룹으로 가독성 UP! 📝
그룹이 많아지면 match[1], match[2] 이런 식으로 쓰면 헷갈리잖아? Go의 regexp는 명명된 그룹(Named Groups)을 지원해서 이름으로 접근할 수 있어!
package main
import (
"fmt"
"regexp"
)
func main() {
text := "생년월일: 1990-05-15"
// ?P<이름> 문법으로 그룹에 이름 지정
datePattern := regexp.MustCompile(`(?P<year>\d{4})-(?P<month>\d{2})-(?P<day>\d{2})`)
match := datePattern.FindStringSubmatch(text)
if match == nil {
fmt.Println("매칭 실패")
return
}
// 그룹 이름으로 인덱스 찾기
names := datePattern.SubexpNames()
result := make(map[string]string)
for i, name := range names {
if i != 0 && name != "" {
result[name] = match[i]
}
}
fmt.Println("연도:", result["year"])
fmt.Println("월:", result["month"])
fmt.Println("일:", result["day"])
}
// 출력:
// 연도: 1990
// 월: 05
// 일: 15
SubexpNames() 메서드가 그룹 이름들을 반환해줘서 맵으로 만들면 훨씬 직관적이지? 코드 유지보수할 때도 "아 이게 연도구나" 하고 바로 알 수 있어서 좋아ㅋㅋ
✏️ 문자열 치환과 변환
ReplaceAllString으로 간단 치환
이제 찾은 패턴을 다른 문자열로 바꿔보자! 가장 기본적인 ReplaceAllString부터 시작할게.
package main
import (
"fmt"
"regexp"
)
func main() {
text := `
고객정보:
이름: 김철수
주민번호: 901234-1234567
전화번호: 010-1234-5678
이메일: customer@email.com
`
// 주민번호 뒷자리 마스킹
ssnPattern := regexp.MustCompile(`(\d{6})-(\d{7})`)
masked := ssnPattern.ReplaceAllString(text, "$1-*******")
// 전화번호 중간자리 마스킹
phonePattern := regexp.MustCompile(`(\d{3})-(\d{4})-(\d{4})`)
masked = phonePattern.ReplaceAllString(masked, "$1-****-$3")
// 이메일 아이디 부분 마스킹
emailPattern := regexp.MustCompile(`([a-zA-Z0-9._%+-]{1,3})[a-zA-Z0-9._%+-]*@`)
masked = emailPattern.ReplaceAllString(masked, "$1***@")
fmt.Println(masked)
}
// 출력:
// 고객정보:
// 이름: 김철수
// 주민번호: 901234-*******
// 전화번호: 010-****-5678
// 이메일: cus***@email.com
여기서 $1, $2, $3은 캡처된 그룹을 참조하는 거야. $1은 첫 번째 괄호, $2는 두 번째 괄호... 이런 식이지! 개인정보 처리할 때 진짜 유용해 🔒
ReplaceAllStringFunc로 동적 치환
근데 단순 치환 말고 좀 더 복잡한 로직이 필요할 때가 있잖아? 그럴 땐 ReplaceAllStringFunc를 써! 함수를 넘겨서 매칭된 문자열마다 다른 처리를 할 수 있어.
package main
import (
"fmt"
"regexp"
"strings"
)
func main() {
text := `
참고 사이트:
https://golang.org
http://example.com
www.github.com
`
urlPattern := regexp.MustCompile(`https?://[^\s]+|www\.[^\s]+`)
// 각 URL을 HTML 링크로 변환
result := urlPattern.ReplaceAllStringFunc(text, func(url string) string {
// www로 시작하면 http:// 추가
if strings.HasPrefix(url, "www") {
url = "http://" + url
}
return fmt.Sprintf(`<a href="%s">%s</a>`, url, url)
})
fmt.Println(result)
}
// 출력:
// 참고 사이트:
// <a href="https://golang.org">https://golang.org</a>
// <a href="http://example.com">http://example.com</a>
// <a href="http://www.github.com">http://www.github.com</a>
이렇게 하면 매칭된 각 문자열에 대해 커스텀 로직을 적용할 수 있어. 마크다운 파서 만들 때나 텍스트 전처리할 때 엄청 유용하지ㅋㅋ
🎨 정규표현식 패턴 문법 완전정복
자 이제 본격적으로 정규표현식 문법을 파헤쳐보자! 처음엔 복잡해 보이지만 하나씩 알고 나면 진짜 쉬워ㅋㅋ
기본 메타문자들
| 메타문자 | 의미 | 예시 |
|---|---|---|
| . | 개행 문자를 제외한 모든 문자 | a.c → abc, a1c, a@c 매칭 |
| ^ | 문자열의 시작 | ^Hello → Hello로 시작하는 문자열 |
| $ | 문자열의 끝 | end$ → end로 끝나는 문자열 |
| * | 0번 이상 반복 | ab*c → ac, abc, abbc 매칭 |
| + | 1번 이상 반복 | ab+c → abc, abbc (ac는 불가) |
| ? | 0번 또는 1번 | ab?c → ac, abc |
| | | OR 연산 | cat|dog → cat 또는 dog |
| [] | 문자 클래스 | [abc] → a, b, c 중 하나 |
| () | 그룹화 및 캡처 | (ab)+ → ab, abab, ababab |
수량자(Quantifiers) 상세 가이드
수량자는 패턴이 몇 번 반복되는지를 지정하는 거야. 이거 제대로 알아야 원하는 패턴을 정확히 잡을 수 있어!
package main
import (
"fmt"
"regexp"
)
func main() {
patterns := map[string]string{
`\d{3}`: "정확히 3자리 숫자",
`\d{3,}`: "3자리 이상 숫자",
`\d{3,5}`: "3~5자리 숫자",
`\d*`: "0개 이상의 숫자",
`\d+`: "1개 이상의 숫자",
`\d?`: "0개 또는 1개의 숫자",
}
testStrings := []string{"1", "12", "123", "1234", "12345", "123456"}
for pattern, desc := range patterns {
fmt.Printf("\n패턴: %s (%s)\n", pattern, desc)
re := regexp.MustCompile("^" + pattern + "$")
for _, test := range testStrings {
if re.MatchString(test) {
fmt.Printf(" ✅ %s\n", test)
}
}
}
}
문자 클래스와 단축 표현
자주 쓰는 패턴들은 단축 표현이 있어서 편하게 쓸 수 있어! 이거 외워두면 코드가 훨씬 간결해져ㅋㅋ
| 단축 표현 | 의미 | 동일 표현 |
|---|---|---|
| \d | 숫자 | [0-9] |
| \D | 숫자가 아닌 것 | [^0-9] |
| \w | 단어 문자 (알파벳, 숫자, _) | [a-zA-Z0-9_] |
| \W | 단어 문자가 아닌 것 | [^a-zA-Z0-9_] |
| \s | 공백 문자 (스페이스, 탭, 개행) | [ \t\n\r\f\v] |
| \S | 공백이 아닌 문자 | [^ \t\n\r\f\v] |
package main
import (
"fmt"
"regexp"
)
func main() {
// 아이디 검증: 영문자로 시작, 영문자+숫자+언더스코어, 4-12자
usernamePattern := regexp.MustCompile(`^[a-zA-Z]\w{3,11}$`)
// 비밀번호 검증: 최소 8자, 영문+숫자+특수문자 포함
passwordPattern := regexp.MustCompile(`^(?=.*[a-zA-Z])(?=.*\d)(?=.*[@$!%*?&])[A-Za-z\d@$!%*?&]{8,}$`)
// 공백 제거
whitespacePattern := regexp.MustCompile(`\s+`)
// 테스트
usernames := []string{"user123", "1user", "ab", "valid_user_99"}
fmt.Println("=== 아이디 검증 ===")
for _, username := range usernames {
if usernamePattern.MatchString(username) {
fmt.Printf("✅ %s: 유효\n", username)
} else {
fmt.Printf("❌ %s: 유효하지 않음\n", username)
}
}
// 공백 제거 예제
text := " 여러 공백이 있는 텍스트 "
cleaned := whitespacePattern.ReplaceAllString(text, " ")
fmt.Printf("\n원본: '%s'\n", text)
fmt.Printf("정리: '%s'\n", cleaned)
}
// 출력:
// === 아이디 검증 ===
// ✅ user123: 유효
// ❌ 1user: 유효하지 않음
// ❌ ab: 유효하지 않음
// ✅ valid_user_99: 유효
//
// 원본: ' 여러 공백이 있는 텍스트 '
// 정리: ' 여러 공백이 있는 텍스트 '
Lookahead와 Lookbehind (전방탐색/후방탐색)
이건 좀 고급 기능인데, 특정 패턴 앞이나 뒤에 있는 걸 찾을 때 써. 근데 주의할 점! Go의 RE2 엔진은 lookbehind를 지원 안 해 😢 lookahead만 가능해!
package main
import (
"fmt"
"regexp"
)
func main() {
text := "가격: 10000원, 할인가: 5000원, 배송비: 3000원"
// 긍정 전방탐색 (?=pattern): 뒤에 '원'이 오는 숫자만 매칭
pricePattern := regexp.MustCompile(`\d+(?=원)`)
prices := pricePattern.FindAllString(text, -1)
fmt.Println("찾은 가격들:")
for _, price := range prices {
fmt.Println(" -", price)
}
// 부정 전방탐색 (?!pattern): 뒤에 '원'이 오지 않는 숫자
nonPricePattern := regexp.MustCompile(`\d+(?!원)`)
fmt.Println("\n'원'이 붙지 않은 숫자:")
fmt.Println(nonPricePattern.FindAllString("주문번호: 12345, 가격: 10000원", -1))
}
// 출력:
// 찾은 가격들:
// - 10000
// - 5000
// - 3000
//
// '원'이 붙지 않은 숫자:
// [12345 1000]
🚀 실전 프로젝트: 로그 파서 만들기
이제 배운 걸 다 합쳐서 실전 프로젝트를 해보자! 웹 서버 로그를 파싱하는 프로그램을 만들어볼게. 진짜 실무에서 많이 쓰는 패턴이야ㅋㅋ
package main
import (
"fmt"
"regexp"
"strings"
)
// 로그 엔트리 구조체
type LogEntry struct {
IP string
Timestamp string
Method string
Path string
Status string
Size string
UserAgent string
}
func main() {
// Apache Combined Log Format 예시
logLines := []string{
`192.168.1.1 - - [10/Oct/2023:13:55:36 +0900] "GET /api/users HTTP/1.1" 200 1234 "-" "Mozilla/5.0"`,
`10.0.0.5 - - [10/Oct/2023:13:56:12 +0900] "POST /api/login HTTP/1.1" 401 567 "-" "curl/7.68.0"`,
`172.16.0.10 - - [10/Oct/2023:13:57:45 +0900] "GET /images/logo.png HTTP/1.1" 304 0 "-" "Chrome/118.0"`,
}
// 정규표현식 패턴 (명명된 그룹 사용)
logPattern := regexp.MustCompile(
`^(?P<ip>[\d.]+) ` + // IP 주소
`- - ` + // 인증 정보 (보통 비어있음)
`\[(?P<timestamp>[^\]]+)\] ` + // 타임스탬프
`"(?P<method>\w+) (?P<path>[^\s]+) [^"]+" ` + // HTTP 메서드와 경로
`(?P<status>\d{3}) ` + // 상태 코드
`(?P<size>\d+) ` + // 응답 크기
`"[^"]*" ` + // Referer
`"(?P<useragent>[^"]*)"`, // User-Agent
)
// 로그 파싱
var entries []LogEntry
names := logPattern.SubexpNames()
for _, line := range logLines {
matches := logPattern.FindStringSubmatch(line)
if matches == nil {
fmt.Println("파싱 실패:", line)
continue
}
entry := LogEntry{}
for i, name := range names {
if i == 0 || name == "" {
continue
}
switch name {
case "ip":
entry.IP = matches[i]
case "timestamp":
entry.Timestamp = matches[i]
case "method":
entry.Method = matches[i]
case "path":
entry.Path = matches[i]
case "status":
entry.Status = matches[i]
case "size":
entry.Size = matches[i]
case "useragent":
entry.UserAgent = matches[i]
}
}
entries = append(entries, entry)
}
// 결과 출력
fmt.Println("=== 파싱된 로그 엔트리 ===\n")
for i, entry := range entries {
fmt.Printf("로그 #%d\n", i+1)
fmt.Printf(" IP: %s\n", entry.IP)
fmt.Printf(" 시간: %s\n", entry.Timestamp)
fmt.Printf(" 요청: %s %s\n", entry.Method, entry.Path)
fmt.Printf(" 상태: %s\n", entry.Status)
fmt.Printf(" 크기: %s bytes\n", entry.Size)
fmt.Printf(" User-Agent: %s\n\n", entry.UserAgent)
}
// 통계 분석
analyzeLog(entries)
}
func analyzeLog(entries []LogEntry) {
fmt.Println("=== 로그 통계 ===\n")
// 상태 코드별 카운트
statusCount := make(map[string]int)
// HTTP 메서드별 카운트
methodCount := make(map[string]int)
// API 엔드포인트 추출
apiPattern := regexp.MustCompile(`^/api/(\w+)`)
apiEndpoints := make(map[string]int)
for _, entry := range entries {
statusCount[entry.Status]++
methodCount[entry.Method]++
if matches := apiPattern.FindStringSubmatch(entry.Path); matches != nil {
apiEndpoints[matches[1]]++
}
}
fmt.Println("상태 코드 분포:")
for status, count := range statusCount {
fmt.Printf(" %s: %d건\n", status, count)
}
fmt.Println("\nHTTP 메서드 분포:")
for method, count := range methodCount {
fmt.Printf(" %s: %d건\n", method, count)
}
fmt.Println("\nAPI 엔드포인트 호출:")
for endpoint, count := range apiEndpoints {
fmt.Printf(" /api/%s: %d건\n", endpoint, count)
}
}
// 출력:
// === 파싱된 로그 엔트리 ===
//
// 로그 #1
// IP: 192.168.1.1
// 시간: 10/Oct/2023:13:55:36 +0900
// 요청: GET /api/users
// 상태: 200
// 크기: 1234 bytes
// User-Agent: Mozilla/5.0
//
// 로그 #2
// IP: 10.0.0.5
// 시간: 10/Oct/2023:13:56:12 +0900
// 요청: POST /api/login
// 상태: 401
// 크기: 567 bytes
// User-Agent: curl/7.68.0
//
// 로그 #3
// IP: 172.16.0.10
// 시간: 10/Oct/2023:13:57:45 +0900
// 요청: GET /images/logo.png
// 상태: 304
// 크기: 0 bytes
// User-Agent: Chrome/118.0
//
// === 로그 통계 ===
//
// 상태 코드 분포:
// 200: 1건
// 401: 1건
// 304: 1건
//
// HTTP 메서드 분포:
// GET: 2건
// POST: 1건
//
// API 엔드포인트 호출:
// /api/users: 1건
// /api/login: 1건
이 예제는 진짜 실무에서 쓸 수 있는 수준이야! 로그 파일 분석, 모니터링, 디버깅할 때 이런 패턴을 많이 써. 특히 명명된 그룹을 사용해서 코드 가독성을 높인 게 포인트지ㅋㅋ
⚡ 성능 최적화 팁
정규표현식은 강력하지만 잘못 쓰면 성능 저하의 주범이 될 수 있어. 실전에서 꼭 알아야 할 최적화 팁들을 정리해볼게!
1. 정규표현식 컴파일 재사용
func validateEmails(emails []string) []bool {
results := make([]bool, len(emails))
for i, email := range emails {
// 매번 컴파일 - 비효율적!
re := regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
results[i] = re.MatchString(email)
}
return results
}
// 전역 변수로 한 번만 컴파일
var emailRegex = regexp.MustCompile(`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`)
func validateEmails(emails []string) []bool {
results := make([]bool, len(emails))
for i, email := range emails {
// 컴파일된 객체 재사용 - 효율적!
results[i] = emailRegex.MatchString(email)
}
return results
}
2. 적절한 메서드 선택
용도에 맞는 메서드를 선택하는 것도 중요해! 불필요하게 복잡한 메서드를 쓰면 성능 낭비야.
| 상황 | 추천 메서드 | 이유 |
|---|---|---|
| 매칭 여부만 확인 | MatchString | 가장 빠름, 불필요한 메모리 할당 없음 |
| 첫 번째 매칭만 필요 | FindString | 전체 검색하지 않음 |
| 개수 제한이 있는 검색 | FindAllString(text, n) | n개만 찾고 중단 |
| 바이트 슬라이스 처리 | Match, Find 등 바이트 버전 | 문자열 변환 오버헤드 없음 |
3. 패턴 최적화 기법
package main
import (
"fmt"
"regexp"
"time"
)
func benchmark(name string, pattern string, text string, iterations int) {
re := regexp.MustCompile(pattern)
start := time.Now()
for i := 0; i < iterations; i++ {
re.MatchString(text)
}
elapsed := time.Since(start)
fmt.Printf("%s: %v\n", name, elapsed)
}
func main() {
text := "test@example.com"
iterations := 100000
fmt.Println("=== 패턴 최적화 벤치마크 ===\n")
// 비효율적인 패턴: 백트래킹 많음
benchmark(
"비효율적",
`^.*@.*\..*$`,
text,
iterations,
)
// 효율적인 패턴: 명확한 문자 클래스 사용
benchmark(
"효율적",
`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`,
text,
iterations,
)
// 더 효율적: 불필요한 캡처 그룹 제거
benchmark(
"더 효율적",
`^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$`,
text,
iterations,
)
}
// 출력 예시:
// === 패턴 최적화 벤치마크 ===
//
// 비효율적: 45.2ms
// 효율적: 28.7ms
// 더 효율적: 27.1ms
- ✅ .* 대신 구체적인 문자 클래스 사용
- ✅ 불필요한 캡처 그룹 제거 (?: 사용)
- ✅ 앵커(^, $) 적극 활용
- ✅ 가능하면 단순한 패턴 사용
- ✅ 중첩된 수량자 피하기
4. 대용량 텍스트 처리
큰 파일이나 스트림을 처리할 때는 전략이 필요해! 한 번에 다 읽지 말고 청크 단위로 처리하는 게 좋아.
package main
import (
"bufio"
"fmt"
"os"
"regexp"
)
func processLargeLogFile(filename string) error {
file, err := os.Open(filename)
if err != nil {
return err
}
defer file.Close()
// 에러 패턴
errorPattern := regexp.MustCompile(`ERROR|FATAL|CRITICAL`)
scanner := bufio.NewScanner(file)
lineNum := 0
errorCount := 0
// 라인별로 처리 - 메모리 효율적!
for scanner.Scan() {
lineNum++
line := scanner.Text()
if errorPattern.MatchString(line) {
errorCount++
fmt.Printf("Line %d: %s\n", lineNum, line)
}
}
if err := scanner.Err(); err != nil {
return err
}
fmt.Printf("\n총 %d개의 에러 발견 (전체 %d 라인)\n", errorCount, lineNum)
return nil
}
func main() {
// 사용 예시
// processLargeLogFile("/var/log/application.log")
fmt.Println("대용량 파일 처리 예제")
}
이렇게 하면 기가바이트 단위의 로그 파일도 메모리 부담 없이 처리할 수 있어! 스캐너로 한 줄씩 읽으면서 정규표현식 매칭하는 거지ㅋㅋ
🎓 실무 활용 사례 모음
이제 실제로 많이 쓰이는 패턴들을 모아봤어! 복붙해서 바로 쓸 수 있게 정리했으니까 북마크 ㄱㄱ 📌
1. 데이터 검증 패턴
package main
import (
"fmt"
"regexp"
)
// 검증 함수들
var (
// 이메일 (RFC 5322 간소화 버전)
EmailRegex = regexp.MustCompile(`^[a-zA-Z0-9.!#$%&'*+/=?^_` + "`" + `{|}~-]+@[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(?:\.[a-zA-Z0-9](?:[a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*$`)
// 한국 휴대폰 번호
PhoneRegex = regexp.MustCompile(`^01[0-9]-?\d{3,4}-?\d{4}$`)
// URL (http, https)
URLRegex = regexp.MustCompile(`^https?://[^\s/$.?#].[^\s]*$`)
// IPv4 주소
IPv4Regex = regexp.MustCompile(`^(?:(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\.){3}(?:25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)$`)
// 신용카드 번호 (기본 형식)
CreditCardRegex = regexp.MustCompile(`^(?:4[0-9]{12}(?:[0-9]{3})?|5[1-5][0-9]{14}|3[47][0-9]{13})$`)
// 비밀번호 (8자 이상, 영문+숫자+특수문자)
PasswordRegex = regexp.MustCompile(`^(?=.*[A-Za-z])(?=.*\d)(?=.*[@$!%*#?&])[A-Za-z\d@$!%*#?&]{8,}$`)
// 한국 우편번호 (5자리)
PostalCodeRegex = regexp.MustCompile(`^\d{5}$`)
// 주민등록번호 (앞자리-뒷자리)
SSNRegex = regexp.MustCompile(`^\d{6}-[1-4]\d{6}$`)
)
func main() {
fmt.Println("=== 데이터 검증 테스트 ===\n")
// 이메일 검증
emails := []string{
"test@example.com",
"user.name+tag@example.co.kr",
"invalid@",
"@invalid.com",
}
fmt.Println("이메일 검증:")
for _, email := range emails {
result := "❌"
if EmailRegex.MatchString(email) {
result = "✅"
}
fmt.Printf(" %s %s\n", result, email)
}
// 전화번호 검증
phones := []string{
"010-1234-5678",
"01012345678",
"011-123-4567",
"02-1234-5678", // 일반 전화
}
fmt.Println("\n휴대폰 번호 검증:")
for _, phone := range phones {
result := "❌"
if PhoneRegex.MatchString(phone) {
result = "✅"
}
fmt.Printf(" %s %s\n", result, phone)
}
// URL 검증
urls := []string{
"https://www.example.com",
"http://localhost:8080/api",
"ftp://files.com", // ftp는 매칭 안됨
"not-a-url",
}
fmt.Println("\nURL 검증:")
for _, url := range urls {
result := "❌"
if URLRegex.MatchString(url) {
result = "✅"
}
fmt.Printf(" %s %s\n", result, url)
}
}
2. 텍스트 추출 패턴
package main
import (
"fmt"
"regexp"
)
type SocialTextParser struct {
HashtagRegex *regexp.Regexp
MentionRegex *regexp.Regexp
URLRegex *regexp.Regexp
EmojiRegex *regexp.Regexp
}
func NewSocialTextParser() *SocialTextParser {
return &SocialTextParser{
// 해시태그: #으로 시작, 한글/영문/숫자
HashtagRegex: regexp.MustCompile(`#[가-힣a-zA-Z0-9_]+`),
// 멘션: @로 시작, 영문/숫자/언더스코어
MentionRegex: regexp.MustCompile(`@[a-zA-Z0-9_]+`),
// URL
URLRegex: regexp.MustCompile(`https?://[^\s]+`),
// 이모지 (기본 범위)
EmojiRegex: regexp.MustCompile(`[\x{1F600}-\x{1F64F}]|[\x{1F300}-\x{1F5FF}]|[\x{1F680}-\x{1F6FF}]|[\x{2600}-\x{26FF}]`),
}
}
func (p *SocialTextParser) Parse(text string) map[string][]string {
return map[string][]string{
"hashtags": p.HashtagRegex.FindAllString(text, -1),
"mentions": p.MentionRegex.FindAllString(text, -1),
"urls": p.URLRegex.FindAllString(text, -1),
}
}
func main() {
parser := NewSocialTextParser()
text := `
오늘 #재능넷 에서 #Go언어 강의 들었는데 대박! 🔥
@instructor님 감사합니다 😊
자세한 내용: https://www.jaenung.net/courses/golang
#프로그래밍 #개발자 #코딩
`
result := parser.Parse(text)
fmt.Println("=== SNS 텍스트 파싱 결과 ===\n")
fmt.Println("해시태그:")
for _, tag := range result["hashtags"] {
fmt.Println(" -", tag)
}
fmt.Println("\n멘션:")
for _, mention := range result["mentions"] {
fmt.Println(" -", mention)
}
fmt.Println("\nURL:")
for _, url := range result["urls"] {
fmt.Println(" -", url)
}
}
// 출력:
// === SNS 텍스트 파싱 결과 ===
//
// 해시태그:
// - #재능넷
// - #Go언어
// - #프로그래밍
// - #개발자
// - #코딩
//
// 멘션:
// - @instructor
//
// URL:
// - https://www.jaenung.net/courses/golang
이런 파서는 SNS 플랫폼이나 커뮤니티 사이트에서 진짜 많이 써! 해시태그로 검색 기능 만들거나, 멘션으로 알림 보내거나, URL 자동 링크 처리할 때 필수야 🎯
3. 코드 분석 패턴
package main
import (
"fmt"
"regexp"
)
type CodeAnalyzer struct {
FunctionRegex *regexp.Regexp
ImportRegex *regexp.Regexp
CommentRegex *regexp.Regexp
TODORegex *regexp.Regexp
}
func NewCodeAnalyzer() *CodeAnalyzer {
return &CodeAnalyzer{
// 함수 선언: func 함수명(
FunctionRegex: regexp.MustCompile(`func\s+(\w+)\s*\(`),
// import 문
ImportRegex: regexp.MustCompile(`import\s+"([^"]+)"`),
// 주석 (// 또는 /* */)
CommentRegex: regexp.MustCompile(`//.*|/\*[\s\S]*?\*/`),
// TODO 주석
TODORegex: regexp.MustCompile(`//\s*TODO:?\s*(.+)`),
}
}
func (a *CodeAnalyzer) AnalyzeCode(code string) {
fmt.Println("=== 코드 분석 결과 ===\n")
// 함수 찾기
functions := a.FunctionRegex.FindAllStringSubmatch(code, -1)
fmt.Printf("함수 개수: %d\n", len(functions))
for _, fn := range functions {
fmt.Printf(" - %s()\n", fn[1])
}
// import 찾기
imports := a.ImportRegex.FindAllStringSubmatch(code, -1)
fmt.Printf("\nimport 개수: %d\n", len(imports))
for _, imp := range imports {
fmt.Printf(" - %s\n", imp[1])
}
// TODO 찾기
todos := a.TODORegex.FindAllStringSubmatch(code, -1)
if len(todos) > 0 {
fmt.Printf("\nTODO 항목: %d개\n", len(todos))
for i, todo := range todos {
fmt.Printf(" %d. %s\n", i+1, todo[1])
}
}
}
func main() {
analyzer := NewCodeAnalyzer()
sampleCode := `
package main
import "fmt"
import "regexp"
// TODO: 에러 처리 개선 필요
func processData(data string) error {
// 데이터 처리 로직
return nil
}
// TODO: 테스트 코드 작성
func validateInput(input string) bool {
return true
}
func main() {
fmt.Println("Hello, World!")
}
`
analyzer.AnalyzeCode(sampleCode)
}
// 출력:
// === 코드 분석 결과 ===
//
// 함수 개수: 3
// - processData()
// - validateInput()
// - main()
//
// import 개수: 2
// - fmt
// - regexp
//
// TODO 항목: 2개
// 1. 에러 처리 개선 필요
// 2. 테스트 코드 작성
이런 코드 분석 도구는 정적 분석기나 린터 만들 때 기본이 돼. 코드 리뷰 자동화하거나 프로젝트 통계 내는 데도 유용하지ㅋㅋ
🐛 자주 하는 실수와 해결법
정규표현식 쓰다 보면 다들 비슷한 실수를 해ㅋㅋㅋ 내가 겪었던 것들 위주로 정리해볼게!
실수 1: 이스케이프 처리 안 함
regexp.MustCompile(".") // 모든 문자 매칭 (의도와 다를 수 있음)regexp.MustCompile("$100") // $ 는 문자열 끝을 의미
regexp.MustCompile(`\.`) // 점(.) 문자 그대로regexp.MustCompile(`\$100`) // 달러 기호 그대로
특수문자를 문자 그대로 매칭하고 싶으면 백슬래시로 이스케이프해야 해! Go에서는 백틱(`)으로 raw string을 쓰면 편해.
실수 2: Greedy vs Non-Greedy 혼동
package main
import (
"fmt"
"regexp"
)
func main() {
html := `<div>첫 번째</div><div>두 번째</div>`
// Greedy: 가능한 한 많이 매칭
greedyPattern := regexp.MustCompile(`<div>.*</div>`)
fmt.Println("Greedy 결과:")
fmt.Println(greedyPattern.FindString(html))
// 출력: <div>첫 번째</div><div>두 번째</div> (전체 매칭!)
// Non-Greedy: 가능한 한 적게 매칭
nonGreedyPattern := regexp.MustCompile(`<div>.*?</div>`)
fmt.Println("\nNon-Greedy 결과:")
fmt.Println(nonGreedyPattern.FindAllString(html, -1))
// 출력: [<div>첫 번째</div> <div>두 번째</div>] (개별 매칭!)
}
// 출력:
// Greedy 결과:
// <div>첫 번째</div><div>두 번째</div>
//
// Non-Greedy 결과:
// [<div>첫 번째</div> <div>두 번째</div>]
?를 수량자 뒤에 붙이면 non-greedy가 돼! *?, +?, {n,}? 이런 식으로 쓸 수 있어.
실수 3: 멀티라인 텍스트 처리
package main
import (
"fmt"
"regexp"
)
func main() {
text := `첫 번째 줄
두 번째 줄
세 번째 줄`
// 기본 모드: ^ 와 $ 는 전체 문자열의 시작/끝
pattern1 := regexp.MustCompile(`^.+$`)
fmt.Println("기본 모드:")
fmt.Println(pattern1.FindString(text))
// 출력: 첫 번째 줄 (첫 줄만)
// 멀티라인 모드: (?m) 플래그 사용
pattern2 := regexp.MustCompile(`(?m)^.+$`)
fmt.Println("\n멀티라인 모드:")
fmt.Println(pattern2.FindAllString(text, -1))
// 출력: [첫 번째 줄 두 번째 줄 세 번째 줄]
// . 이 개행 문자도 매칭하게: (?s) 플래그
pattern3 := regexp.MustCompile(`(?s)^.+$`)
fmt.Println("\nDotAll 모드:")
fmt.Println(pattern3.FindString(text))
// 출력: 전체 텍스트 (개행 포함)
}
// 출력:
// 기본 모드:
// 첫 번째 줄
//
// 멀티라인 모드:
// [첫 번째 줄 두 번째 줄 세 번째 줄]
//
// DotAll 모드:
// 첫 번째 줄
// 두 번째 줄
// 세 번째 줄
• (?m): 멀티라인 모드 (^ 와 $ 가 각 줄의 시작/끝)
• (?s): DotAll 모드 (. 이 개행 문자도 매칭)
• (?U): Ungreedy 모드 (기본을 non-greedy로)
실수 4: nil 체크 안 함
package main
import (
"fmt"
"regexp"
)
func extractEmail(text string) (string, bool) {
emailPattern := regexp.MustCompile(`[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}`)
match := emailPattern.FindString(text)
// ❌ 나쁜 예: nil 체크 없이 사용
// return match[0], true // 패닉 발생 가능!
// ✅ 좋은 예: nil 체크
if match == "" {
return "", false
}
return match, true
}
func extractEmailWithGroups(text string) (string, string, bool) {
emailPattern := regexp.MustCompile(`([a-zA-Z0-9._%+-]+)@([a-zA-Z0-9.-]+\.[a-zA-Z]{2,})`)
matches := emailPattern.FindStringSubmatch(text)
// ✅ 서브매치도 nil 체크 필수
if matches == nil || len(matches) < 3 {
return "", "", false
}
return matches[1], matches[2], true
}
func main() {
// 이메일 있는 경우
if email, ok := extractEmail("연락처: test@example.com"); ok {
fmt.Println("찾은 이메일:", email)
}
// 이메일 없는 경우
if _, ok := extractEmail("이메일 없음"); !ok {
fmt.Println("이메일을 찾을 수 없습니다")
}
// 그룹 추출
if username, domain, ok := extractEmailWithGroups("user@domain.com"); ok {
fmt.Printf("사용자명: %s, 도메인: %s\n", username, domain)
}
}
// 출력:
// 찾은 이메일: test@example.com
// 이메일을 찾을 수 없습니다
// 사용자명: user, 도메인: domain.com
매칭 결과는 항상 nil일 수 있다는 걸 기억해! 특히 FindStringSubmatch 같은 건 슬라이스 길이도 체크해야 안전해 🛡️
🔧 고급 활용: 커스텀 파서 만들기
마지막으로 좀 더 복잡한 예제를 해보자! 마크다운 파서를 간단하게 만들어볼게. 실제로는 더 복잡하지만, 정규표현식 활용법을 배우기엔 딱이야ㅋㅋ
package main
import (
"fmt"
"regexp"
"strings"
)
type MarkdownParser struct {
patterns map[string]*regexp.Regexp
}
func NewMarkdownParser() *MarkdownParser {
return &MarkdownParser{
patterns: map[string]*regexp.Regexp{
// 헤더: # 제목
"h1": regexp.MustCompile(`(?m)^# (.+)$`),
"h2": regexp.MustCompile(`(?m)^## (.+)$`),
"h3": regexp.MustCompile(`(?m)^### (.+)$`),
// 볼드: **텍스트** 또는 __텍스트__
"bold": regexp.MustCompile(`\*\*(.+?)\*\*|__(.+?)__`),
// 이탤릭: *텍스트* 또는 _텍스트_
"italic": regexp.MustCompile(`\*(.+?)\*|_(.+?)_`),
// 코드: `코드`
"code": regexp.MustCompile("`([^`]+)`"),
// 링크: [텍스트](URL)
"link": regexp.MustCompile(`\[([^\]]+)\]\(([^\)]+)\)`),
// 이미지: 
"image": regexp.MustCompile(`!\[([^\]]*)\]\(([^\)]+)\)`),
// 리스트: - 항목 또는 * 항목
"list": regexp.MustCompile(`(?m)^[\*\-] (.+)$`),
// 코드 블록: ```언어\n코드\n```
"codeblock": regexp.MustCompile("(?s)```(\\w*)\\n(.+?)\\n```"),
},
}
}
func (p *MarkdownParser) ToHTML(markdown string) string {
html := markdown
// 코드 블록 (먼저 처리해야 다른 패턴과 충돌 안 함)
html = p.patterns["codeblock"].ReplaceAllStringFunc(html, func(match string) string {
parts := p.patterns["codeblock"].FindStringSubmatch(match)
if len(parts) >= 3 {
lang := parts[1]
code := parts[2]
if lang == "" {
lang = "text"
}
return fmt.Sprintf(`<pre><code class="language-%s">%s`, lang, code)
}
return match
})
// 헤더
html = p.patterns["h1"].ReplaceAllString(html, "$1
") html = p.patterns["h2"].ReplaceAllString(html, "$1
") html = p.patterns["h3"].ReplaceAllString(html, "$1
") // 이미지 (링크보다 먼저 처리) html = p.patterns["image"].ReplaceAllString(html, `$1")
// 리스트
html = p.patterns["list"].ReplaceAllString(html, "제목입니다
// //부제목
// // 이것은 볼드 텍스트이고, 이것은 이탤릭입니다. // //인라인 코드도 지원합니다.
//
// 재능넷 바로가기
//
// func main() {
// fmt.Println("Hello")
// }
이런 식으로 정규표현식을 조합하면 꽤 복잡한 파서도 만들 수 있어! 물론 실제 프로덕션에서는 전문 파서 라이브러리를 쓰는 게 좋지만, 간단한 텍스트 변환은 이 정도로도 충분해ㅋㅋ
🎯 정리하며
와... 여기까지 읽었으면 진짜 대단한 거야ㅋㅋㅋ 정규표현식이랑 Go의 regexp 패키지에 대해 거의 다 다뤘어! 마지막으로 핵심만 정리해볼게 📝
• 정규표현식은 텍스트 패턴 매칭의 강력한 도구
• Go의 regexp 패키지는 RE2 엔진 사용 (안전하고 빠름)
• Compile() 또는 MustCompile()로 패턴 컴파일
2. 주요 메서드
• MatchString: 매칭 여부 확인
• FindString/FindAllString: 문자열 찾기
• FindStringSubmatch: 그룹 캡처
• ReplaceAllString: 문자열 치환
3. 성능 최적화
• 컴파일된 정규표현식 재사용
• 적절한 메서드 선택
• 패턴 최적화 (구체적인 문자 클래스 사용)
• 대용량 데이터는 스트리밍 처리
4. 실무 팁
• raw string (백틱) 사용으로 이스케이프 간소화
• 명명된 그룹으로 가독성 향상
• nil 체크 필수
• greedy vs non-greedy 주의
정규표현식은 처음엔 어렵게 느껴지지만, 익숙해지면 진짜 강력한 무기가 돼. 특히 텍스트 처리, 데이터 검증, 로그 분석 같은 작업에서는 필수 스킬이지!
재능넷 같은 플랫폼에서도 사용자 입력 검증, 검색 기능, 텍스트 파싱 등 다양한 곳에 정규표현식이 쓰여. 실무에서 바로 활용할 수 있는 내용들이니까 북마크해두고 필요할 때마다 참고하면 좋을 거야! 💪
• 자신만의 유효성 검증 라이브러리 만들기
• 로그 분석 도구 개발
• 텍스트 파서/변환기 구현
• 웹 스크래핑 프로젝트
실전 경험이 쌓이면 정규표현식이 손에 익을 거야 ㅎㅎ
Go 언어로 개발하면서 정규표현식 잘 활용하면 코드 품질이 확 올라가. 특히 문자열 처리가 많은 백엔드 개발에서는 필수 중의 필수지! 화이팅 🔥
댓글 작성
이 글에 대한 여러분의 생각을 들려주세요
로그인이 필요합니다
댓글을 작성하려면 먼저 로그인해주세요.
댓글 0
지식인의 숲 - 지적 재산권 보호 고지
지적 재산권 보호 고지
- 저작권 및 소유권: 본 컨텐츠는 재능넷의 독점 AI 기술로 생성되었으며, 대한민국 저작권법 및 국제 저작권 협약에 의해 보호됩니다.
- AI 생성 컨텐츠의 법적 지위: 본 AI 생성 컨텐츠는 재능넷의 지적 창작물로 인정되며, 관련 법규에 따라 저작권 보호를 받습니다.
- 사용 제한: 재능넷의 명시적 서면 동의 없이 본 컨텐츠를 복제, 수정, 배포, 또는 상업적으로 활용하는 행위는 엄격히 금지됩니다.
- 데이터 수집 금지: 본 컨텐츠에 대한 무단 스크래핑, 크롤링, 및 자동화된 데이터 수집은 법적 제재의 대상이 됩니다.
- AI 학습 제한: 재능넷의 AI 생성 컨텐츠를 타 AI 모델 학습에 무단 사용하는 행위는 금지되며, 이는 지적 재산권 침해로 간주됩니다.
아직 댓글이 없습니다.