From a8e9559e8b68433e06889b963919c2dac4acacfb Mon Sep 17 00:00:00 2001 From: co2plant Date: Thu, 20 Nov 2025 22:33:34 +0900 Subject: [PATCH 1/3] =?UTF-8?q?Chore=20:=20=EC=9E=84=EC=8B=9C=20=EB=94=94?= =?UTF-8?q?=EB=A0=89=ED=86=A0=EB=A6=AC=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- 76-Temporal/README.md | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 76-Temporal/README.md diff --git a/76-Temporal/README.md b/76-Temporal/README.md new file mode 100644 index 0000000..b9dbbce --- /dev/null +++ b/76-Temporal/README.md @@ -0,0 +1,6 @@ +## Temporal + +### 주제 +| 순서 | 발표자 | 주제 | 일자 +| :--- | :--- | :--- | :--- | +| 1 | 조영재 | 정규 표현식, ANT 표현식 | 2025-11-20 | From 25c89ecec80e7d37c35860c5998dd964e5185e97 Mon Sep 17 00:00:00 2001 From: co2plant Date: Sun, 30 Nov 2025 22:01:06 +0900 Subject: [PATCH 2/3] =?UTF-8?q?Feat=20:=20=EC=A0=95=EA=B7=9C=ED=91=9C?= =?UTF-8?q?=ED=98=84=EC=8B=9D=EA=B3=BC=20ANT=20=EC=A0=9C=EC=B6=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ANT_Style_Pattern.md" | 148 +++++ .../README.md" | 4 + .../Regular_Expression.md" | 512 +++++++++++++++ .../img/chomsky-hierarchy.png" | Bin 0 -> 70128 bytes .../Regular_Expression_Example.md" | 591 ++++++++++++++++++ 5 files changed, 1255 insertions(+) create mode 100644 "76-Temporal/01-\354\240\225\352\267\234\355\221\234\355\230\204\354\213\235&ANT\355\221\234\355\230\204\354\213\235/ANT_Style_Pattern.md" create mode 100644 "76-Temporal/01-\354\240\225\352\267\234\355\221\234\355\230\204\354\213\235&ANT\355\221\234\355\230\204\354\213\235/README.md" create mode 100644 "76-Temporal/01-\354\240\225\352\267\234\355\221\234\355\230\204\354\213\235&ANT\355\221\234\355\230\204\354\213\235/Regular_Expression.md" create mode 100644 "76-Temporal/01-\354\240\225\352\267\234\355\221\234\355\230\204\354\213\235&ANT\355\221\234\355\230\204\354\213\235/img/chomsky-hierarchy.png" create mode 100644 "76-Temporal/01-\354\240\225\352\267\234\355\221\234\355\230\204\354\213\235&ANT\355\221\234\355\230\204\354\213\235/presentation/Regular_Expression_Example.md" diff --git "a/76-Temporal/01-\354\240\225\352\267\234\355\221\234\355\230\204\354\213\235&ANT\355\221\234\355\230\204\354\213\235/ANT_Style_Pattern.md" "b/76-Temporal/01-\354\240\225\352\267\234\355\221\234\355\230\204\354\213\235&ANT\355\221\234\355\230\204\354\213\235/ANT_Style_Pattern.md" new file mode 100644 index 0000000..8eca5fe --- /dev/null +++ "b/76-Temporal/01-\354\240\225\352\267\234\355\221\234\355\230\204\354\213\235&ANT\355\221\234\355\230\204\354\213\235/ANT_Style_Pattern.md" @@ -0,0 +1,148 @@ +# Ant-style Pattern + +## 💡 핵심 요약 💡 + +> - **한 줄 정의** : 파일 경로(Path)나 URL을 매칭하기 위해 Apache Ant 빌드 도구에서 고안된 직관적인 **와일드카드 패턴** 입니다. +> +> - **핵심 키워드** : `와일드카드`, `경로 매칭(Path Matching)`, `Double Asterisk(**)`, `Glob Pattern` +> +> - **왜 중요한가?** : 정규표현식보다 문법이 훨씬 단순하여 디렉터리 구조 탐색에 최적화되어 있습니다. 특히 **Spring Framework** 의 URL 설정, 파일 검색 등에서 표준으로 사용됩니다. + +## 1. 개념 + +**Ant-style Pattern** 은 파일 시스템의 경로(Path)나 URL을 쉽고 간단하게 표현하기 위해 만들어진 규칙입니다. + +복잡한 문자열 처리를 위한 '정규표현식'과 달리, **'디렉터리(폴더)와 파일의 계층 구조'**를 매칭하는 데 특화되어 있습니다. 우리 수업에서는 Spring Framework에서 리소스 위치나 URL 패턴(`@RequestMapping`)을 정의할 때 주로 사용했습니다. + +----- + +## 2. 왜 필요한가? + +### 정규표현식의 복잡성 해소 + + - 파일 경로나 URL은 `/` (슬래시)로 구분된 계층 구조를 가집니다. + - 이를 정규표현식으로 표현하려면 이스케이프 처리(`\/`)와 복잡한 수량자 조합이 필요해 가독성이 떨어집니다. + - Ant 패턴은 `*`, `**`, `?` 세 가지만으로 직관적인 경로 표현이 가능합니다. + +### 디렉터리 재귀 탐색 (Recursive Search) + + - 가장 강력한 기능인 `**`를 통해 하위 디렉터리의 깊이(Depth)에 상관없이 모든 경로를 포함할 수 있습니다. + - 이는 웹 애플리케이션에서 "특정 경로 하위의 모든 페이지에 보안 필터를 적용"하는 등의 시나리오에 필수적입니다. + +----- + +## 3. 컴퓨터 과학 내에서 Ant 패턴의 위치 + +### Glob Pattern의 일종 + +Ant 패턴은 컴퓨터 과학에서 **Glob Pattern**의 확장된 형태입니다. + + - **Glob Pattern**: 유닉스 셸(Shell)에서 파일 이름 확장을 위해 사용하는 패턴 (예: `ls *.txt`) + - **Ant Pattern**: Glob 패턴에 **디렉터리 재귀 탐색(`**`)** 개념을 추가하여 확장한 것 + +### 정규표현식과의 관계 + +Ant 패턴은 정규표현식의 **부분집합(Subset)**처럼 동작하지만, 내부적으로는 정규표현식으로 변환되어 처리되거나 별도의 파서(Parser)를 통해 해석됩니다. + +| 특성 | Ant Pattern | 정규표현식 (Regex) | +| :--- | :--- | :--- | +| **주 목적** | 경로(Path), 파일, URL 매칭 | 범용 문자열 검색 및 조작 | +| **복잡도** | 매우 낮음 (단순) | 높음 (강력함) | +| **주요 심볼** | `?`, `*`, `**` | `.`, `*`, `+`, `^`, `$`, `[]` 등 다수 | +| **경계 기준** | `/` (디렉터리 구분자) 기준 | 문자 단위 기준 | + +----- + +## 4. Ant 패턴의 주요 구성 요소 (Syntax) + +Ant 패턴은 다음 3가지 특수 문자를 조합하여 사용합니다. + +### 4.1 `?` (물음표) + +**한 글자**와 매칭됩니다. (디렉터리 구분자 `/` 제외) + +```java +// 예시: t?st.txt +"test.txt" (O) // ? = e +"tast.txt" (O) // ? = a +"tst.txt" (X) // 문자가 있어야 함 +"teest.txt" (X) // 한 글자만 가능 +``` + +### 4.2 `*` (별표) + +**0개 이상의 문자** 와 매칭됩니다. 단, **디렉터리 구분자(`/`)를 넘어갈 수 없습니다.** (현재 디렉터리/파일 이름 내에서만 유효) + +```java +// 예시: *.txt +"file.txt" (O) +"a.txt" (O) +".txt" (O) // 이름 없는 경우도 매칭 (구현체에 따라 다름) +"dir/file.txt" (X) // / 를 넘어갈 수 없음 +``` + +### 4.3 `**` (더블 애스터리스크) + +**0개 이상의 디렉터리(패스)** 와 매칭됩니다. + +```java +// 예시: /project/**/test +"/project/test" (O) // 중간 디렉터리 0개 +"/project/a/test" (O) // 중간 디렉터리 1개 +"/project/a/b/c/d/test" (O) // 중간 디렉터리 다수 + +// 예시: /static/** +"/static/css/style.css" (O) +"/static/images/logo.png" (O) +``` + +----- + +## 5. 실전 비교: Ant Pattern vs Regex + +Spring Framework의 `AntPathMatcher`가 내부적으로 어떻게 동작하는지 이해하기 위해 같은 의도를 가진 패턴을 비교해 봅니다. + +### 시나리오 1: 모든 .jsp 파일 찾기 + + - **Ant Pattern**: `/**/*.jsp` + - **Regex**: `^/.*.jsp$` (또는 경로 구분자를 명확히 하려면 `^/.*[^/].jsp$`) + - *해석*: Ant 패턴이 훨씬 직관적입니다. + +### 시나리오 2: /admin 하위의 모든 URL (깊이 무관) + + - **Ant Pattern**: `/admin/**` + - **Regex**: `^/admin(/.*)?$` + - *해석*: Regex는 하위 경로가 아예 없는 경우와 있는 경우를 모두 고려하는 그룹화가 필요합니다. + +----- + +## 6. Java(Spring)에서의 활용 예시 + +Java Spring 생태계에서는 `AntPathMatcher` 유틸리티 클래스를 통해 이 패턴을 지원합니다. + +```java +import org.springframework.util.AntPathMatcher; + +public class AntPatternExample { + public static void main(String[] args) { + AntPathMatcher matcher = new AntPathMatcher(); + + // 1. ? 매칭 + System.out.println(matcher.match("t?st.jsp", "test.jsp")); // true + + // 2. * 매칭 (같은 레벨) + System.out.println(matcher.match("*.jsp", "hello.jsp")); // true + System.out.println(matcher.match("*.jsp", "a/hello.jsp")); // false (*은 /를 못 넘음) + + // 3. ** 매칭 (하위 경로 포함) + System.out.println(matcher.match("/**/api", "/v1/api")); // true + System.out.println(matcher.match("/app/**/*.html", "/app/views/home.html")); // true + } +} +``` + +### 주요 활용처 + +1. **URL 매핑**: `@RequestMapping("/api/**")`, `` +2. **Spring Security**: `antMatchers("/admin/**").hasRole("ADMIN")` +3. **파일 검색**: `PathMatchingResourcePatternResolver`를 통한 클래스패스 내 파일 로드 diff --git "a/76-Temporal/01-\354\240\225\352\267\234\355\221\234\355\230\204\354\213\235&ANT\355\221\234\355\230\204\354\213\235/README.md" "b/76-Temporal/01-\354\240\225\352\267\234\355\221\234\355\230\204\354\213\235&ANT\355\221\234\355\230\204\354\213\235/README.md" new file mode 100644 index 0000000..d455fdb --- /dev/null +++ "b/76-Temporal/01-\354\240\225\352\267\234\355\221\234\355\230\204\354\213\235&ANT\355\221\234\355\230\204\354\213\235/README.md" @@ -0,0 +1,4 @@ +# 정규 표현식과 ANT 표현식 +1.1 (정규 표현식(Regular Expression))[./Regular_Expression.md] +1.1.1 (정규 표현식 예제)[./Regular_Expression_Example.md] +1.2 (ANT 표현식(ANT Expression))[./ANT_Style_Pattern.md] diff --git "a/76-Temporal/01-\354\240\225\352\267\234\355\221\234\355\230\204\354\213\235&ANT\355\221\234\355\230\204\354\213\235/Regular_Expression.md" "b/76-Temporal/01-\354\240\225\352\267\234\355\221\234\355\230\204\354\213\235&ANT\355\221\234\355\230\204\354\213\235/Regular_Expression.md" new file mode 100644 index 0000000..4216d64 --- /dev/null +++ "b/76-Temporal/01-\354\240\225\352\267\234\355\221\234\355\230\204\354\213\235&ANT\355\221\234\355\230\204\354\213\235/Regular_Expression.md" @@ -0,0 +1,512 @@ + +# 정규표현식(Regular Expression : Regex, Regexp) + +## 💡 핵심 요약💡 +> - **한 줄 정의** : 문자열에서 특정 패턴을 찾거나 조작하기 위해 사용하는 일종의 형식화된 문자열입니다. +> +> - **핵심 키워드** : `패턴 매칭`, `메타문자`, `문자 클래스`, `수량자`, `앵커`, `그룹화` +> +> - **왜 중요한가?** : 텍스트 처리와 데이터 검증에서 강력한 도구로 활용되며, 복잡한 문자열 검색, 치환, 추출 작업을 효율적으로 수행할 수 있어 개발자와 데이터 과학자에게 필수적인 기술입니다. + +## 1. 개념 + +특정 패턴을 가진 문자열을 찾거나 조작하기 위한 형식 언어(Formal Language)입니다. + +정규표현식은 다양한 프로그래밍 언어와 도구에서 지원되며, 복잡한 문자열 검색, 치환, 추출 작업을 간단하게 수행할 수 있습니다. + +하지만 아쉽게도 **하나의 표준이 존재하지 않아 언어나 구현체마다 문법과 기능이 다릅니다.** + +만약 자바와 자바스크립트로 개발을 할 때, 같은 문법으로 정규 표현식을 작성하면 호환성 문제가 발생할 수 있습니다. + +--- + +## 2. 왜 필요한가? + +### 텍스트 처리의 복잡성 +- 대량의 텍스트 데이터에서 특정 패턴을 찾거나 변환하는 작업은 매우 복잡할 수 있습니다. +- 예를 들어, 이메일 주소, 전화번호, 우편번호 등 특정 형식을 가진 데이터를 추출하거나 검증하는 작업이 필요할 때 정규표현식이 유용합니다. + +### 효율적인 문자열 조작 +- 정규표현식을 사용하면 복잡한 문자열 조작 작업을 간결한 코드로 표현할 수 있습니다. +- 반복문과 조건문을 사용하지 않고도 패턴 매칭과 치환을 수행할 수 있어 코드의 가독성과 유지보수성이 향상됩니다. + +### 다양한 응용 분야 +- 데이터 검증: 사용자 입력이 특정 형식을 따르는지 확인 +- 로그 분석: 로그 파일에서 특정 이벤트나 오류 메시지 추출 +- 웹 스크래핑: HTML 문서에서 원하는 데이터 추출 + +--- + +## 3. 컴퓨터 과학 내에서 정규표현식의 위치 +### 촘스키 위계(Chomsky Hierarchy) + +노엄 촘스키(Avram Noam Chomsky)가 1956년에 제시한 형식 언어 분류 체계로, 언어의 생성 규칙에 따라 4가지 유형으로 나눕니다. + +![촘스키 위계](./img/chomsky-hierarchy.png) +- Type 0 : **무제한 문법(Unrestricted Grammar : UG)** + + Recognized By Turing Machine + + 생성 규칙 : αAβ → β (α, β는 임의의 문자열) + +- Type 1 : **문맥 민감 문법(Context-Sensitive Grammar : CSG)** + + Accepted By Linear Bounded Automaton + + 생성 규칙 : αAβ → αγβ (γ는 공백이 아닌 문자열) + +- Type 2 : **문맥 자유 문법 (Context-Free Grammar : CFG)** + + Accepted By Pushdown Automaton + + 생성 규칙 : A → α (A는 단일 비단말 기호, α는 임의의 문자열) + +- Type 3 : **정규 문법 (Regular Grammar : RG)** <- 정규표현식의 위치 + + Accepted By Finite Automaton + + 생성 규칙 : A → aB 또는 A → a (A, B는 단일 비단말 기호, a는 단일 단말 기호) + +### 정규 표현식과 유한 오토마타, 정규 언어 +- 정규 언어(Regular Language)는 정규 문법으로 생성되는 언어입니다. 다른말로 하면 정규 표현식으로 표현 가능한 언어라고도 할 수 있습니다. +- 유한 오토마타(Finite Automaton)는 정규 언어를 인식하는 추상 기계입니다. 정규 표현식과 유한 오토마타는 서로 변환이 가능하며, 동일한 언어를 표현할 수 있습니다. +``` +정규 표현식 <-> 유한 오토마타 <-> 정규 문법 +``` + +### 정규 표현식 엔진의 기본 개념 +- **NFA (Nondeterministic Finite Automaton)** : 비결정적 유한 오토마타 + - 여러 경로를 동시에 탐색 (백트래킹 사용) + - 느리지만 복잡한 패턴 처리에 유리 + - 백트래킹을 사용하기 때문에 최악의 경우 O(2^n) 시간 복잡도를 가질 수 있음 + - 이런 부분을 공격하면 ReDoS (Regular Expression Denial of Service) 공격이 발생할 수 있음 + - 캡처 그룹, 역참조 지원 + - Java, JavaScript, Python 등에서 사용 + +- **DFA (Deterministic Finite Automaton)** : 결정적 유한 오토마타 + - 단일 경로만 탐색 + - 빠르지만 복잡한 패턴 처리에 불리 + - 캡처 그룹, 역참조 미지원 + - grep, awk, sed 등에서 사용 + +--- + +## 4. 정규 표현식의 주요 구성 요소 + +정규 표현식은 크게 리터럴, 메타 문자, 문자 클래스, 수량자, 앵커, 그룹화 등으로 구성됩니다. 각 구성 요소는 패턴을 정의하는 데 고유한 역할을 합니다. +위에서 이야기 했던대로 하나의 표준이 존재하지는 않지만 알아두면 좋을 것 같아서 주요 구성 요소를 정리해봤습니다. + +### 4.1 리터럴 (Literals) +가장 기본적인 형태의 정규 표현식으로, 있는 그대로의 문자를 매칭합니다. + +```java +// 일반 문자열 매칭 +String pattern = "hello"; +"hello world".matches(".*hello.*"); // true +"Hello world".matches(".*hello.*"); // false (대소문자 구분) + +// 대소문자 무시 (Pattern.CASE_INSENSITIVE 플래그 사용) +Pattern p = Pattern.compile("hello", Pattern.CASE_INSENSITIVE); +p.matcher("Hello world").find(); // true +``` + +### 4.2 메타 문자 (Meta-characters) +정규 표현식에서 특별한 의미를 가지는 문자들입니다. 이 문자들을 리터럴로 사용하려면 백슬래시(`\`)로 이스케이프해야 합니다. +주요 메타 문자: `. ^ $ * + ? { } [ ] \ | ( )` + +### 4.2.1 `.` (Any character) +개행 문자(`\n`)를 제외한 모든 단일 문자와 매칭됩니다. + +```java +// . 은 개행 문자를 제외한 모든 문자 하나와 매칭 +"cat".matches("c.t"); // true +"cut".matches("c.t"); // true +"ct".matches("c.t"); // false (문자 하나 필요) +"c\\nt".matches("c.t"); // false (개행 제외) + +// DOTALL 모드로 개행도 포함 +Pattern.compile("c.t", Pattern.DOTALL).matcher("c\\nt").matches(); // true +``` + +### 4.2.2 `|` (Alternation / OR) +둘 중 하나를 선택하는 OR 연산자입니다. + +```java +// OR 연산 +"cat".matches("cat|dog"); // true +"dog".matches("cat|dog"); // true +"bird".matches("cat|dog"); // false + +// 그룹과 함께 사용 +"gmail.com".matches("(gmail|naver|daum)\\\\.com"); // true +``` + +### 4.2.3 `\` (Escape Character) +메타 문자를 일반 문자로 취급하게 하거나, 특수한 문자 클래스를 정의할 때 사용합니다. +Java 문자열에서는 백슬래시 자체를 이스케이프해야 하므로 `\\`로 작성해야 합니다. + +```java +// 메타 문자 . 을 일반 문자로 매칭 +"abc.def".matches("abc\\\\.def"); // true +"abc.def".matches("abc.def"); // true (.이 모든 문자와 매칭되므로) +"abcXdef".matches("abc\\\\.def"); // false +``` + +### 4.3 문자 클래스 (Character Classes) +대괄호 `[]` 안에 포함된 문자 중 하나와 매칭됩니다. + +```java +// [abc] : a, b, c 중 하나 +"a".matches("[abc]"); // true +"d".matches("[abc]"); // false + +// [^abc] : a, b, c를 제외한 모든 문자 (부정) +"d".matches("[^abc]"); // true +"a".matches("[^abc]"); // false + +// [a-z] : 범위 지정 (Range) +"m".matches("[a-z]"); // true (소문자) +"M".matches("[A-Z]"); // true (대문자) +"5".matches("[0-9]"); // true (숫자) + +// [a-zA-Z] : 조합 (Union) +"a".matches("[a-zA-Z]"); // true + +// [a-z&&[def]] : 교집합 (Intersection) - Java 정규식 특징 +"d".matches("[a-z&&[def]]"); // true (d, e, f 중 하나이면서 소문자) +"a".matches("[a-z&&[def]]"); // false +``` + +### 4.4 사전 정의된 문자 클래스 (Predefined Character Classes / Shorthands) +자주 사용되는 문자 클래스를 짧게 표현한 것입니다. + +```java +// \\d - Digit (숫자) = [0-9] +"5".matches("\\\\d"); // true +"a".matches("\\\\d"); // false + +// \\D - Non-Digit (숫자 아님) = [^0-9] +"a".matches("\\\\D"); // true + +// \\w - Word character (단어 문자) = [a-zA-Z0-9_] +"a".matches("\\\\w"); // true +"_".matches("\\\\w"); // true +"@".matches("\\\\w"); // false (특수문자 제외) + +// \\W - Non-Word character = [^a-zA-Z0-9_] +"@".matches("\\\\W"); // true + +// \\s - Whitespace (공백) = [ \\t\\n\\r\\f] +" ".matches("\\\\s"); // true +"\\t".matches("\\\\s"); // true + +// \\S - Non-Whitespace = [^\\s] +"a".matches("\\\\S"); // true +``` + +### 4.5 수량자 (Quantifiers) +앞의 요소가 얼마나 반복되는지를 지정합니다. + +#### 4.5.1 기본 수량자 +```java +// * : 0회 이상 ({0,}) +"".matches("a*"); // true +"aaa".matches("a*"); // true + +// + : 1회 이상 ({1,}) +"".matches("a+"); // false +"a".matches("a+"); // true + +// ? : 0회 또는 1회 ({0,1}) +"a".matches("a?"); // true +"aa".matches("a?"); // false + +// {n} : 정확히 n회 +"aa".matches("a{2}"); // true + +// {n,m} : n회 이상 m회 이하 +"aaa".matches("a{2,4}"); // true + +// {n,} : n회 이상 +"aaa".matches("a{2,}"); // true +``` + +#### 4.5.2 수량자의 종류 (Greedy, Lazy, Possessive) +| 종류 | 문법 | 설명 | 예시 (`aab` 에서 `a+` 매칭) | +|---|---|---|---| +| **Greedy** (탐욕적) | `*`, `+`, `?`, `{n,m}` | 가능한 한 **가장 많이** 매칭하려고 시도합니다. (기본값) | `aa` (전체 매칭) | +| **Reluctant** (Lazy, 게으른) | `*?`, `+?`, `??`, `{n,m}?` | 가능한 한 **가장 적게** 매칭하려고 시도합니다. | `a` (첫 번째 a만 매칭) | +| **Possessive** (소유적) | `*+`, `++`, `?+`, `{n,m}+` | Greedy처럼 많이 매칭하지만, **백트래킹을 허용하지 않습니다.** 성능 최적화에 사용됩니다. | `aa` (매칭 후 뒤로 돌아가지 않음) | + +```java +// Greedy vs Lazy +String text = "
Hello
"; +// Greedy:
Hello
전체 매칭 +text.matches("
.*
"); +// Lazy:
Hello
에서
,
각각 매칭 시도 가능 (find() 사용 시) +``` + +### 4.6 경계 (Anchors) +문자열의 특정 위치를 지정합니다. 문자를 소비하지 않고 위치만 확인합니다 (Zero-width assertion). + +```java +// ^ : 문자열(또는 라인)의 시작 +Pattern.compile("^hello").matcher("hello world").find(); // true + +// $ : 문자열(또는 라인)의 끝 +Pattern.compile("world$").matcher("hello world").find(); // true + +// \\b : 단어 경계 (Word Boundary) - 문자와 공백/특수문자 사이 +"cat".matches("\\\\bcat\\\\b"); // true +"cathedral".matches("\\\\bcat\\\\b"); // false (cat이 단어의 일부임) + +// \\B : 단어 경계가 아님 +"cathedral".matches(".*\\\\Bcat\\\\B.*"); // true (중간에 포함된 cat) + +// \\A : 입력의 시작 (무조건 문자열 전체의 시작) +// \\z : 입력의 끝 (무조건 문자열 전체의 끝) +// \\Z : 입력의 끝 (마지막 종결자(\\n)가 있으면 그 앞) +``` +### 4.7 그룹화 (Grouping) +여러 문자를 하나의 단위로 묶거나, 매칭된 부분을 추출할 때 사용합니다. + +#### 4.7.1 캡처 그룹 (Capturing Groups) - `(...)` +매칭된 부분을 메모리에 저장하여 나중에 참조할 수 있습니다. + +```java +String log = "2025-11-18 23:43:21 ERROR User login failed"; + +Pattern p = Pattern.compile("(\\\\d{4}-\\\\d{2}-\\\\d{2}) (\\\\d{2}:\\\\d{2}:\\\\d{2}) (\\\\w+) (.+)"); +Matcher m = p.matcher(log); + +if (m.find()) { + System.out.println("날짜: " + m.group(1)); // 2025-11-18 + System.out.println("시간: " + m.group(2)); // 23:43:21 + System.out.println("레벨: " + m.group(3)); // ERROR + System.out.println("메시지: " + m.group(4)); // User login failed +} +``` + +#### 4.7.2 비캡처 그룹 (Non-Capturing Groups) - `(?:...)` +그룹화는 필요하지만, 메모리에 저장할 필요가 없을 때 사용합니다. 성능상 이점이 있습니다. + +```java +// 그룹화는 필요하지만 캡처는 불필요한 경우 +String url = ""; + +// 비캡처 그룹 사용 (성능 향상) +Pattern p2 = Pattern.compile("(?:https|http)://(.+)"); +Matcher m2 = p2.matcher(url); +if (m2.find()) { + // m2.group(1)은 이제 www.example.com (프로토콜은 캡처 안 됨) + System.out.println(m2.group(1)); // www.example.com +} +``` + +#### 4.7.3 명명된 그룹 (Named Groups) - `(?...)` +인덱스 대신 이름으로 그룹을 참조할 수 있어 가독성이 좋아집니다. + +```java +String date = "2025-11-18"; +Pattern p = Pattern.compile("(?\\\\d{4})-(?\\\\d{2})-(?\\\\d{2})"); +Matcher m = p.matcher(date); + +if (m.find()) { + System.out.println("년도: " + m.group("year")); // 2025 + System.out.println("월: " + m.group("month")); // 11 + System.out.println("일: " + m.group("day")); // 18 +} +``` + +#### 4.7.4 역참조 (Back-reference) - `\\1`, `\\k` +앞서 매칭된 그룹을 다시 참조합니다. + +```java +// 중복 단어 찾기 +String text = "the the cat sat on the the mat"; +Pattern p = Pattern.compile("\\\\b(\\\\w+)\\\\s+\\\\1\\\\b"); +Matcher m = p.matcher(text); + +while (m.find()) { + System.out.println("중복 단어: " + m.group(1)); +} + +// HTML 태그 매칭 (여는 태그와 닫는 태그가 같아야 함) +String html = "

Title

Content

Subtitle

"; +Pattern tagPattern = Pattern.compile("<(\\\\w+)>.*?"); +Matcher tagMatcher = tagPattern.matcher(html); + +while (tagMatcher.find()) { + System.out.println(tagMatcher.group()); +} +``` + +### 4.8 플래그 (Flags) +정규 표현식의 동작 방식을 변경하는 옵션입니다. `Pattern.compile()`의 두 번째 인자로 전달하거나, 패턴 내에 `(?flags)` 형태로 포함할 수 있습니다. + +- `Pattern.CASE_INSENSITIVE` (`(?i)`): 대소문자를 구분하지 않습니다. +- `Pattern.MULTILINE` (`(?m)`): `^`와 `$`가 전체 문자열이 아닌 각 라인의 시작과 끝에 매칭됩니다. +- `Pattern.DOTALL` (`(?s)`): `.`이 개행 문자를 포함한 모든 문자와 매칭됩니다. +- `Pattern.COMMENTS` (`(?x)`): 패턴 내의 공백과 주석을 무시합니다. (가독성 향상) + +```java +// MULTILINE 예제 +String text = "First line\\nSecond line"; +Pattern p = Pattern.compile("^Second", Pattern.MULTILINE); // 각 줄의 시작에서 매칭 +Matcher m = p.matcher(text); +System.out.println(m.find()); // true +``` + +--- + +## 5. 심화 개념 (Advanced Concepts) + +### 5.1 Greedy vs. Lazy 상세 예제 + +#### Greedy (탐욕적) - 기본 동작 + +```java +String html = "
Hello
World
"; + +// Greedy: 최대한 많이 매칭 +Pattern greedy = Pattern.compile("
.*
"); +Matcher m = greedy.matcher(html); +if (m.find()) { + System.out.println(m.group()); + // 출력:
Hello
World
+ // (전체를 하나로 매칭) +} +``` + +#### Lazy (게으른) - `?` 추가 + +```java +String html = "
Hello
World
"; + +// Lazy: 최소한만 매칭 +Pattern lazy = Pattern.compile("
.*?
"); +Matcher m = lazy.matcher(html); +while (m.find()) { + System.out.println(m.group()); +} +// 출력: +//
Hello
+//
World
+// (각각 개별 매칭) +``` + +**수량자별 Lazy 버전**: + +```java +// Greedy → Lazy +// * → *? +// + → +? +// ? → ?? +// {n,m} → {n,m}? +// {n,} → {n,}? + +// 실무 예제: HTML 태그 추출 +String html = "

First

Second

"; + +// Greedy +html.replaceAll("

.*

", "[CONTENT]"); +// 결과: [CONTENT] + +// Lazy +html.replaceAll("

.*?

", "[CONTENT]"); +// 결과: [CONTENT][CONTENT] +``` + +## 📊 6. 성능 최적화 팁 + +### 6.1 Pattern 재사용 + +```java +// ❌ 나쁜 예: 매번 컴파일 +public boolean validateEmail(String email) { + return email.matches("^[\\\\w.-]+@[\\\\w.-]+\\\\.[a-zA-Z]{2,}$"); + // matches()는 내부에서 매번 Pattern.compile() 호출 +} + +// ✅ 좋은 예: Pattern 재사용 +public class EmailValidator { + private static final Pattern EMAIL_PATTERN = + Pattern.compile("^[\\\\w.-]+@[\\\\w.-]+\\\\.[a-zA-Z]{2,}$"); + + public boolean validateEmail(String email) { + return EMAIL_PATTERN.matcher(email).matches(); + } +} + +// 성능 차이: 약 3-5배 빠름 + +``` + +### 6.2 비캡처 그룹 사용 + +```java +// ❌ 느림: 불필요한 캡처 +Pattern slow = Pattern.compile("(https|http)://([\\\\w.]+)"); + +// ✅ 빠름: 비캡처 그룹 +Pattern fast = Pattern.compile("(?:https|http)://([\\\\w.]+)"); +// 도메인만 필요한 경우 프로토콜 캡처 불필요 + +``` + +### 6.3 Catastrophic Backtracking 방지 + + + +이 재앙적 백트래킹은 정규표현식을 이용한 공격인 ReDoS(Regular Expression Denial of Service)의 원인이 될 수 있습니다. +실제로 2019년에는 클라우드 플레어에서 ReDos 공격을 받았습니다. + +간단하게 해결 가능하지만 주의해서 사용하지 않으면 쉽게 공격을 받을 수 있는 부분입니다. + +```java +// ❌ 위험: 재앙적 백트래킹 가능 +Pattern dangerous = Pattern.compile("(a+)+b"); +String input = "aaaaaaaaaaaaaaaaaaaaaaaaaaac"; // 'b'가 없음 +// 이 경우 지수 시간 복잡도로 인해 매우 느려짐 (또는 스택 오버플로우) + +// ✅ 안전: Possessive 수량자 사용 +Pattern safe = Pattern.compile("(a++)b"); +// 또는 Atomic 그룹 사용 +Pattern safe2 = Pattern.compile("(?>a+)b"); + +// 실무에서는 타임아웃 설정 +public boolean matchesWithTimeout(String pattern, String input, long timeoutMs) { + ExecutorService executor = Executors.newSingleThreadExecutor(); + Future future = executor.submit(() -> { + return Pattern.compile(pattern).matcher(input).matches(); + }); + + try { + return future.get(timeoutMs, TimeUnit.MILLISECONDS); + } catch (TimeoutException e) { + future.cancel(true); + return false; + } catch (Exception e) { + return false; + } finally { + executor.shutdownNow(); + } +} + +``` + +--- + +## 📚 7. 참고 자료 및 학습 리소스 + +### 온라인 도구 + +- **Regex101** (https://regex101.com/) - 실시간 패턴 테스트 및 설명 +- **RegExr** (https://regexr.com/) - 시각적 디버깅 +- **RegexPlanet** (https://www.regexplanet.com/) - 다양한 언어별 테스트 + +### Java 공식 문서 + +- `java.util.regex.Pattern` JavaDoc +- `java.util.regex.Matcher` JavaDoc + diff --git "a/76-Temporal/01-\354\240\225\352\267\234\355\221\234\355\230\204\354\213\235&ANT\355\221\234\355\230\204\354\213\235/img/chomsky-hierarchy.png" "b/76-Temporal/01-\354\240\225\352\267\234\355\221\234\355\230\204\354\213\235&ANT\355\221\234\355\230\204\354\213\235/img/chomsky-hierarchy.png" new file mode 100644 index 0000000000000000000000000000000000000000..cdedf0e4aaa74f33fa9876f290b484a336fb246f GIT binary patch literal 70128 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 20) { + return new ValidationResult(false, "비밀번호는 8-20자여야 합니다."); + } + + // 소문자 포함 체크 + if (!password.matches(".*[a-z].*")) { + return new ValidationResult(false, "소문자를 최소 1개 포함해야 합니다."); + } + + // 대문자 포함 체크 + if (!password.matches(".*[A-Z].*")) { + return new ValidationResult(false, "대문자를 최소 1개 포함해야 합니다."); + } + + // 숫자 포함 체크 + if (!password.matches(".*\\\\d.*")) { + return new ValidationResult(false, "숫자를 최소 1개 포함해야 합니다."); + } + + // 특수문자 포함 체크 + if (!password.matches(".*[@$!%*?&].*")) { + return new ValidationResult(false, "특수문자(@$!%*?&)를 최소 1개 포함해야 합니다."); + } + + return new ValidationResult(true, "안전한 비밀번호입니다."); + } + + // 통합 패턴 사용 (한 번에 체크) + public static boolean isValidPasswordQuick(String password) { + return password != null && password.matches(PASSWORD_PATTERN); + } + + static class ValidationResult { + boolean valid; + String message; + + ValidationResult(boolean valid, String message) { + this.valid = valid; + this.message = message; + } + } + + // 테스트 + public static void main(String[] args) { + String[] passwords = { + "Abc123!@", // true + "Password123!", // true + "weak", // false (너무 짧음) + "alllowercase123!", // false (대문자 없음) + "ALLUPPERCASE123!", // false (소문자 없음) + "NoSpecialChar123", // false (특수문자 없음) + "NoNumber!@#", // false (숫자 없음) + }; + + for (String pw : passwords) { + ValidationResult result = validatePassword(pw); + System.out.printf("%s: %b - %s%n", pw, result.valid, result.message); + } + } +} + +``` + +### 한국 전화번호 검증 + +```java +public class PhoneValidator { + + // 휴대폰: 010-XXXX-XXXX + private static final String MOBILE_PATTERN = "^010-\\\\d{4}-\\\\d{4}$"; + + // 일반 전화: 0XX-XXX(X)-XXXX + private static final String PHONE_PATTERN = "^0\\\\d{1,2}-\\\\d{3,4}-\\\\d{4}$"; + + // 통합 패턴 + private static final String ALL_PHONE_PATTERN = + "^(010-\\\\d{4}-\\\\d{4}|0\\\\d{1,2}-\\\\d{3,4}-\\\\d{4})$"; + + public static boolean isValidMobile(String phone) { + return phone != null && phone.matches(MOBILE_PATTERN); + } + + public static boolean isValidPhone(String phone) { + return phone != null && phone.matches(ALL_PHONE_PATTERN); + } + + // 하이픈 자동 추가 + public static String formatPhone(String phone) { + if (phone == null) return null; + + // 숫자만 추출 + String numbers = phone.replaceAll("[^0-9]", ""); + + // 휴대폰 (11자리) + if (numbers.matches("^010\\\\d{8}$")) { + return numbers.replaceAll("(\\\\d{3})(\\\\d{4})(\\\\d{4})", "$1-$2-$3"); + } + + // 서울 (10자리: 02-XXXX-XXXX) + if (numbers.matches("^02\\\\d{8}$")) { + return numbers.replaceAll("(\\\\d{2})(\\\\d{4})(\\\\d{4})", "$1-$2-$3"); + } + + // 기타 지역 (10자리: 0XX-XXX-XXXX) + if (numbers.matches("^0\\\\d{9}$")) { + return numbers.replaceAll("(\\\\d{3})(\\\\d{3})(\\\\d{4})", "$1-$2-$3"); + } + + // 기타 지역 (11자리: 0XX-XXXX-XXXX) + if (numbers.matches("^0\\\\d{10}$")) { + return numbers.replaceAll("(\\\\d{3})(\\\\d{4})(\\\\d{4})", "$1-$2-$3"); + } + + return phone; // 포맷할 수 없으면 원본 반환 + } + + // 테스트 + public static void main(String[] args) { + System.out.println(formatPhone("01012345678")); // 010-1234-5678 + System.out.println(formatPhone("0212345678")); // 02-1234-5678 + System.out.println(formatPhone("0311234567")); // 031-123-4567 + System.out.println(formatPhone("03112345678")); // 031-1234-5678 + } +} + +``` + +### 1.2 데이터 파싱 (Parsing) + +### 로그 파일 파싱 + +```java +/** + * 로그 파일에서 정보 추출 + * 로그 형식: [2025-11-18 23:43:21] [ERROR] [UserService] User login failed - IP: 192.168.1.100 + */ +public class LogParser { + + private static final Pattern LOG_PATTERN = Pattern.compile( + "\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2} \\\\d{2}:\\\\d{2}:\\\\d{2})\\\\]\\\\s+" + // 타임스탬프 + "\\\\[(\\\\w+)\\\\]\\\\s+" + // 로그 레벨 + "\\\\[([\\\\w]+)\\\\]\\\\s+" + // 서비스명 + "(.+?)(?:\\\\s+-\\\\s+IP:\\\\s+(\\\\d+\\\\.\\\\d+\\\\.\\\\d+\\\\.\\\\d+))?" // 메시지 및 IP (옵션) + ); + + public static class LogEntry { + String timestamp; + String level; + String service; + String message; + String ipAddress; + + @Override + public String toString() { + return String.format("LogEntry{time='%s', level='%s', service='%s', message='%s', ip='%s'}", + timestamp, level, service, message, ipAddress); + } + } + + public static LogEntry parse(String logLine) { + Matcher m = LOG_PATTERN.matcher(logLine); + if (!m.find()) { + return null; + } + + LogEntry entry = new LogEntry(); + entry.timestamp = m.group(1); + entry.level = m.group(2); + entry.service = m.group(3); + entry.message = m.group(4); + entry.ipAddress = m.group(5); // null일 수 있음 + + return entry; + } + + // 특정 IP 주소 추출 + public static List extractIPs(String text) { + List ips = new ArrayList<>(); + Pattern ipPattern = Pattern.compile("\\\\b(?:\\\\d{1,3}\\\\.){3}\\\\d{1,3}\\\\b"); + Matcher m = ipPattern.matcher(text); + + while (m.find()) { + ips.add(m.group()); + } + + return ips; + } + + // 에러 로그만 필터링 + public static List filterErrorLogs(List logs) { + return logs.stream() + .filter(log -> log.matches(".*\\\\[ERROR\\\\].*")) + .collect(Collectors.toList()); + } + + // 테스트 + public static void main(String[] args) { + String log1 = "[2025-11-18 23:43:21] [ERROR] [UserService] User login failed - IP: 192.168.1.100"; + String log2 = "[2025-11-18 23:43:22] [INFO] [PaymentService] Payment processed successfully"; + + LogEntry entry1 = parse(log1); + LogEntry entry2 = parse(log2); + + System.out.println(entry1); + System.out.println(entry2); + + // IP 추출 + String text = "Connections from 192.168.1.1, 10.0.0.1, and 172.16.0.1"; + List ips = extractIPs(text); + System.out.println("추출된 IP: " + ips); + } +} + +``` + +### URL 파싱 + +```java +public class URLParser { + + private static final Pattern URL_PATTERN = Pattern.compile( + "^(?https?)://" + // 프로토콜 + "(?[^:/]+)" + // 도메인 + "(?::(?\\\\d+))?" + // 포트 (옵션) + "(?/[^?#]*)?" + // 경로 (옵션) + "(?:\\\\?(?[^#]*))?" + // 쿼리 스트링 (옵션) + "(?:#(?.*))?$" // 프래그먼트 (옵션) + ); + + public static class URLInfo { + String protocol; + String domain; + String port; + String path; + String query; + String fragment; + Map queryParams; + + @Override + public String toString() { + return String.format("URL{protocol='%s', domain='%s', port='%s', path='%s', query='%s', fragment='%s', params=%s}", + protocol, domain, port, path, query, fragment, queryParams); + } + } + + public static URLInfo parse(String url) { + Matcher m = URL_PATTERN.matcher(url); + if (!m.matches()) { + return null; + } + + URLInfo info = new URLInfo(); + info.protocol = m.group("protocol"); + info.domain = m.group("domain"); + info.port = m.group("port"); + info.path = m.group("path"); + info.query = m.group("query"); + info.fragment = m.group("fragment"); + + // 쿼리 파라미터 파싱 + if (info.query != null && !info.query.isEmpty()) { + info.queryParams = parseQueryString(info.query); + } + + return info; + } + + private static Map parseQueryString(String query) { + Map params = new HashMap<>(); + Pattern paramPattern = Pattern.compile("([^&=]+)=([^&]*)"); + Matcher m = paramPattern.matcher(query); + + while (m.find()) { + params.put(m.group(1), m.group(2)); + } + + return params; + } + + // 테스트 + public static void main(String[] args) { + String url = ""; + URLInfo info = parse(url); + System.out.println(info); + + // 출력: + // URL{protocol='https', domain='example.com', port='8080', + // path='/api/users', query='id=123&name=john', + // fragment='section1', params={id=123, name=john}} + } +} + +``` + +### CSV/TSV 파싱 + +```java +public class CSVParser { + + // 기본 CSV (쉼표로 구분, 따옴표 처리) + private static final Pattern CSV_PATTERN = Pattern.compile( + ",(?=(?:[^\\"]*\\"[^\\"]*\\")*[^\\"]*$)" // 따옴표 외부의 쉼표만 매칭 + ); + + // RFC 4180 준수 CSV 파서 + public static List parseCSVLine(String line) { + List fields = new ArrayList<>(); + + // 복잡한 케이스 처리: "field1","field with, comma","field with ""quotes""" + Pattern pattern = Pattern.compile( + "\\"([^\\"]*(?:\\"\\"[^\\"]*)*)\\"|([^,]+)|(?<=,)(?=,)|^(?=,)|(?<=,)$" + ); + + Matcher m = pattern.matcher(line); + while (m.find()) { + String field; + if (m.group(1) != null) { + // 따옴표로 감싸진 필드 + field = m.group(1).replace("\\"\\"", "\\""); + } else if (m.group(2) != null) { + // 일반 필드 + field = m.group(2); + } else { + // 빈 필드 + field = ""; + } + fields.add(field); + } + + return fields; + } + + // 간단한 버전 (따옴표 없는 경우) + public static List parseSimpleCSV(String line) { + return Arrays.asList(line.split(",")); + } + + // 테스트 + public static void main(String[] args) { + // 복잡한 케이스 + String line1 = "John,Doe,\\"123 Main St, Apt 4\\",\\"He said \\"\\"Hello\\"\\"\\""; + List fields1 = parseCSVLine(line1); + System.out.println(fields1); + // [John, Doe, 123 Main St, Apt 4, He said "Hello"] + + // 간단한 케이스 + String line2 = "apple,banana,cherry"; + List fields2 = parseSimpleCSV(line2); + System.out.println(fields2); + // [apple, banana, cherry] + } +} + +``` + +--- + +## 🔧 2. Spring Boot 실무 통합 예제 + +### 2.1 Validation with Bean Validation + +```java +import jakarta.validation.Constraint; +import jakarta.validation.ConstraintValidator; +import jakarta.validation.ConstraintValidatorContext; +import jakarta.validation.Payload; +import java.lang.annotation.*; + +// 커스텀 어노테이션 정의 +@Target({ElementType.FIELD, ElementType.PARAMETER}) +@Retention(RetentionPolicy.RUNTIME) +@Constraint(validatedBy = PhoneNumberValidator.class) +@Documented +public @interface PhoneNumber { + String message() default "유효하지 않은 전화번호 형식입니다"; + Class[] groups() default {}; + Class[] payload() default {}; +} + +// Validator 구현 +public class PhoneNumberValidator implements ConstraintValidator { + + private static final String PHONE_PATTERN = "^(010-\\\\d{4}-\\\\d{4}|0\\\\d{1,2}-\\\\d{3,4}-\\\\d{4})$"; + + @Override + public boolean isValid(String value, ConstraintValidatorContext context) { + if (value == null || value.isEmpty()) { + return true; // @NotNull과 함께 사용 + } + return value.matches(PHONE_PATTERN); + } +} + +// DTO 사용 +public class UserRegistrationDTO { + + @NotBlank(message = "이름은 필수입니다") + @Size(min = 2, max = 50, message = "이름은 2-50자여야 합니다") + private String name; + + @NotBlank(message = "이메일은 필수입니다") + @Email(message = "유효한 이메일 주소를 입력하세요") + private String email; + + @NotBlank(message = "전화번호는 필수입니다") + @PhoneNumber // 커스텀 검증 + private String phone; + + @NotBlank(message = "비밀번호는 필수입니다") + @Pattern( + regexp = "^(?=.*[a-z])(?=.*[A-Z])(?=.*\\\\d)(?=.*[@$!%*?&])[A-Za-z\\\\d@$!%*?&]{8,20}$", + message = "비밀번호는 8-20자이며, 대소문자, 숫자, 특수문자를 각각 1개 이상 포함해야 합니다" + ) + private String password; + + // getters, setters... +} + +// Controller +@RestController +@RequestMapping("/api/users") +public class UserController { + + @PostMapping("/register") + public ResponseEntity register(@Valid @RequestBody UserRegistrationDTO dto) { + // @Valid가 자동으로 검증 수행 + // 검증 실패 시 MethodArgumentNotValidException 발생 + + // 검증 통과 후 로직 + return ResponseEntity.ok("등록 성공"); + } +} + +// Exception Handler +@RestControllerAdvice +public class GlobalExceptionHandler { + + @ExceptionHandler(MethodArgumentNotValidException.class) + public ResponseEntity> handleValidationExceptions( + MethodArgumentNotValidException ex) { + + Map errors = new HashMap<>(); + ex.getBindingResult().getAllErrors().forEach((error) -> { + String fieldName = ((FieldError) error).getField(); + String errorMessage = error.getDefaultMessage(); + errors.put(fieldName, errorMessage); + }); + + return ResponseEntity.badRequest().body(errors); + } +} + +``` + +### 2.2 로깅 및 모니터링 + +```java +@Service +public class LogAnalysisService { + + private static final Pattern ERROR_PATTERN = Pattern.compile( + "\\\\[(\\\\d{4}-\\\\d{2}-\\\\d{2} \\\\d{2}:\\\\d{2}:\\\\d{2})\\\\].*\\\\[ERROR\\\\].*" + ); + + private static final Pattern IP_PATTERN = Pattern.compile( + "\\\\b(?:\\\\d{1,3}\\\\.){3}\\\\d{1,3}\\\\b" + ); + + /** + * 로그 파일에서 에러 발생 시간대 분석 + */ + public Map analyzeErrorsByHour(List logs) { + Pattern hourPattern = Pattern.compile("\\\\d{4}-\\\\d{2}-\\\\d{2} (\\\\d{2}):"); + + return logs.stream() + .filter(log -> log.contains("[ERROR]")) + .map(log -> { + Matcher m = hourPattern.matcher(log); + return m.find() ? Integer.parseInt(m.group(1)) : -1; + }) + .filter(hour -> hour != -1) + .collect(Collectors.groupingBy( + hour -> hour, + Collectors.counting() + )); + } + + /** + * 의심스러운 IP 주소 탐지 (단시간 내 많은 요청) + */ + public List detectSuspiciousIPs(List logs, int threshold) { + Map ipCounts = logs.stream() + .flatMap(log -> { + Matcher m = IP_PATTERN.matcher(log); + List ips = new ArrayList<>(); + while (m.find()) { + ips.add(m.group()); + } + return ips.stream(); + }) + .collect(Collectors.groupingBy( + ip -> ip, + Collectors.counting() + )); + + return ipCounts.entrySet().stream() + .filter(entry -> entry.getValue() > threshold) + .map(Map.Entry::getKey) + .collect(Collectors.toList()); + } +} + +``` + +--- From 2b673e5fc40ba875f9e13b7c18fb45e104a06c1a Mon Sep 17 00:00:00 2001 From: JO YOUNGJAE <93631841+co2plant@users.noreply.github.com> Date: Mon, 1 Dec 2025 09:31:52 +0900 Subject: [PATCH 3/3] Update 76-Temporal/README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --- 76-Temporal/README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/76-Temporal/README.md b/76-Temporal/README.md index b9dbbce..9c52f28 100644 --- a/76-Temporal/README.md +++ b/76-Temporal/README.md @@ -1,6 +1,6 @@ ## Temporal ### 주제 -| 순서 | 발표자 | 주제 | 일자 +| 순서 | 발표자 | 주제 | 일자 | | :--- | :--- | :--- | :--- | | 1 | 조영재 | 정규 표현식, ANT 표현식 | 2025-11-20 |