본 포스팅은 음악을 쉽게 담을 수 있게 도와주는 'Sqoop 스쿱' 프로젝트의 트러블 슈팅 내용을 기록했습니다.
YTPlaylistExtractor
모듈의 핵심 비즈니스 로직인, 영상 플레이리스트 추출을 위해 가장 중요한 단계는 YouTube의 Description, Comment 문자열을 정규표현식과 매칭해 음악 리스트 [YTMusicInfo]
로 추출(1차 스쿱)하는 것입니다.
Sqoop 서비스는 ShazamKit
을 이용해 정확도가 높은 2차 스쿱(깊은 추출)을 제공하고 있지만, 최소 1분 이상의 긴 추출 시간, 예상보다 큰 데이터 사용량 등의 문제점을 상호보완하기 위해 본 모듈의 1차 스쿱(얉은 추출) 기능을 우선적으로 제공하고 있습니다.
즉, 영상 내 음악 정보가 존재한다면 2차 스쿱으로 넘어가지 않더라도 빠르고 정확하게 사용자에게 음악 리스트를 제공해야하는 비즈니스 목적이 있었습니다.
이를 달성하기 위해 정규표현식의 정확도를 높이고자 했습니다. 정확도 개선의 핵심 조건은, 다양한 케이스에 대한 대응이였습니다. YouTube에는 셀 수 없을만큼 많은 플레이리스트가 있고 그와 비례할 정도의 문자열 케이스가 존재합니다. 이는 음악 정보가 영상 내 있음에도 불구하고, 1차 스쿱에 실패할 확률이 높다는 뜻이기도 합니다.
다양한 케이스에 대응하기 위해 그에 맞는 정규표현식을 코드로 구현해야 했습니다. 하지만 기존의 코드를 확장해 정규표현식 케이스를 추가하는 것은 문제가 있었습니다. 애플의 전통적인 정규표현식 사용 방법인 NSRegularExpression
을 이용한 기존 코드와 함께 설명을 이어가겠습니다.
// 1. 정규 표현식 케이스 정의(Regex Literal)
let regexPatternList = [
"(\\d{1,2}:\\d{2}(?::\\d{2})?)\\s*[-]?\\s*(.+)",
"\\[?(\\d{1,2}:\\d{2}(?::\\d{2})?)\\]?\\s*[-]?\\s*(.+)",
// ...
]
// 2. 정규표현식 패턴(문자열)을 이용해 NSRegularExpression 생성
let regex = try? NSRegularExpression(pattern: pattern, options: [])
// 3. 정규표현식을 순환하며 매칭
let range = NSRange(text.startIndex..., in: text)
regex.enumerateMatches(in: text, options: [], range: range) { match, _, _ in
if let match = match {
if match.range(at: 1).location != NSNotFound,
let timeRange = Range(match.range(at: 1), in: text),
let contentRange = Range(match.range(at: 2), in: text) {
print("시간: \(String(timeRange))")
print("제목 및 아티스트: \(String(contentRange))")
}
}
}
기존 코드는 정규표현식 케이스를 관리하기 위해 슬래시 /.../
사이에 정규표현식을 작성하는 형태의 Regex Literal
을 사용하고 있습니다. 정규표현식은 일종의 별개 언어로 동작하고, 이는 다른 언어에서도 가장 보편적으로 사용되기 때문에 간결하고, 어디서나 사용이 가능합니다. 하지만 확장, 재사용성, 가독성의 관점에서 이는 제한 사항이 존재합니다.
- 케이스 대응 및 추가의 어려움
- 다양한 케이스에 대응하기 위해선, 복잡하고 어려운
Regex Literal
형태에 대한 학습 및 리서치가 필요합니다.
- 다양한 케이스에 대응하기 위해선, 복잡하고 어려운
- 재사용성의 어려움
- 비슷하지만 조금씩 다른 패턴을 위한 재사용이 어렵습니다.
- 이를 위해 반복되는
Regex Literal
을 변수 등에 담아 활용할 수 있지만, 구조적인 사용 및 관리에 제한 사항이 존재합니다.
- 결과값 예측의 어려움
Regex Literal
만으로 어떤 케이스에 매칭되는지 예측하기 어렵습니다.- 정규표현식 케이스가 많아질 수록 관리의 어려움을 만들 수 있습니다.
- 에러 발생 시 디버깅의 어려움을 만들기도 합니다.
다양한 케이스의 발생은 도메인 상 제어할 수 없는 영역이었고, 이를 해결하는 것은 중요한 비즈니스 요구사항이었기 때문에, 기존의 Regex Literal
방식은 개선이 필요했습니다. 여러 방법을 모색하며 결정하게 된 현재의 방식은 Swift Regex
의 도입입니다.
애플은 정규표현식 활용의 문제점을 고민하고 있었고, WWDC22에서 Swift Regex
를 발표했습니다. 애플이 소개하는 Swift Regex
의 핵심 키워드는 ‘구조화’와 ‘조직화’입니다. 아래 간단한 예시와 함께 Swift Regex
가 가져다줄 수 있는 이점에 대해 살펴보겠습니다.
// Regex Literal
let regexLiteral1 = /\d+/
let regexLiteral2 = /[a-zA-Z][a-zA-Z0-9]*/
// Swift Regex(Regex builders)
let regexBuilder1 = OneOrMore(.digit)
let regexBuilder2 = Regex {
CharacterClass("a"..."z", "A"..."Z")
ZeroOrMore {
CharacterClass("a"..."z", "A"..."Z", "0"..."9"
}
}
위 코드의 Regex Literal
방식과 Swift Regex
모두 같은 결과를 내는데 사용될 수 있습니다. Regex Literal
은 간결하고, 어디서나 사용이 가능하지만 Swift Regex
는 구조적이고, 직관적으로 동작한다는 것을 파악할 수 있습니다. 애플 또한 이와 같은 사실을 염두에 두어 두 표현 방식을 적절히 사용해 균형을 찾을 것을 제시합니다. Regex Builder 내부에 Regex Literal
을 포함할 수 있기 때문에, 이는 점진적 마이그레이션의 관점에서도 유용하게 활용이 가능할 것입니다.
최종적으로, Swift Regex
방식을 도입해 코드를 리팩토링했습니다.
- [PR/#101] YTPlaylistExtractor 정규표현식 로직 업데이트 - https://github.com/DeveloperAcademy-POSTECH/2024-MacC-M14-Medio/pull/104
import RegexBuilder
// MARK: - RegexBuilder를 이용한 정규표현식 케이스 생성
// 시간을 나타내는 패턴
let timePattern = Regex {
OneOrMore(.digit)
":"
Repeat(.digit, count: 2)
Optionally {
":"
Repeat(.digit, count: 2)
}
}
// 공백 및 분리 기호를 나타내는 패턴
let separatorPattern = Regex {
ZeroOrMore(.whitespace)
Optionally(.anyOf("-~"))
ZeroOrMore(.whitespace)
}
// 음악 제목 및 아티스트를 나타내는 패턴
let titleOrArtistPattern = Regex {
OneOrMore(.anyNonNewline)
}
// 구조적으로 작성한 RegexBuilder
let firstCaseRegex = Regex {
Capture { timePattern }
separatorPattern
Capture { titleOrArtistPattern }
}
// MARK: - Regex Builder 활용
// Regex Builder 배열
let regexPatternList = [
firstCaseRegex,
secondCaseRegex,
// ...
]
// 정규표현식을 순환해 매칭
for match in text.matches(of: regex) {
let (_, timeRegex, titleOrArtistRegex) = match.output
print("시간: \(String(timeRange))")
print("제목 및 아티스트: \(String(contentRange))")
}
Swift Regex
도입 후의 경험 및 기대 효과를 정리하겠습니다.
- 케이스 대응의 유연성 증가
Swift Regex
가 제공하는 다양한 Parser를 활용할 수 있습니다.- 자주 사용하는 패턴을 캡슐화 후 재조립 해 케이스 대응에 유연성을 더할 수 있습니다.
- 가독성 향상
- 간결하고 직관적인 구조로 인해 코드를 구조화할 수 있습니다.
OneOrMore
,Repeat
,Capture
,Optionally
와 같은 이해하기 쉬운 객체들로 표현이 가능합니다.- 패턴이 복잡해지더라도 기존 방식 대비 빠른 이해 및 문제 파악이 가능합니다.
Ref.
'iOS > 프로젝트 일지' 카테고리의 다른 글
스쿱 트러블 슈팅 - 음악 추정 시간으로 정확도 개선하기 (1) | 2025.02.17 |
---|---|
스쿱 트러블 슈팅 - API로부터 도메인을 안전하게 지키기 (1) | 2025.02.17 |
캐플 리팩토링 세 번째 이야기 - 트러블 슈팅 (2) | 2025.02.07 |
캐플 리팩토링 두 번째 이야기 - 프로젝트 세팅하기 (0) | 2025.01.30 |
캐플 리팩토링 첫 번째 이야기 - 방향성 설정하기 (0) | 2025.01.22 |