11. 표준 API 활용: 파일 입출력, 날짜·시간, 정규식

문법 다음에 오는 진짜 실전
자바 문법을 익혔다고 해서 바로 프로그램을 잘 만들 수 있는 것은 아닙니다. 변수, 조건문, 클래스, 컬렉션을 아는 것은 기본 체력을 갖춘 것에 가깝습니다. 하지만 실제 업무에서는 파일을 읽고, 로그를 저장하고, 날짜를 계산하고, 문자열에서 원하는 정보만 뽑아내는 일이 훨씬 자주 등장합니다.
예를 들어 이런 상황을 떠올려 봅시다. 사용자가 업로드한 텍스트 파일을 읽어야 할 수도 있고, 오늘 날짜를 기준으로 만료일을 계산해야 할 수도 있으며, 이메일 형식이 맞는지 검사해야 할 수도 있습니다. 이때 필요한 것이 바로 자바의 표준 API입니다. 표준 API는 자바가 기본으로 제공하는 도구 상자라고 생각하면 됩니다.
이 장에서는 세 가지 축을 중심으로 실전을 준비합니다. 첫째는 파일 입출력, 둘째는 날짜와 시간 처리, 셋째는 정규표현식입니다. 이 셋을 익히면 "자바 문법을 아는 사람"에서 "실제 데이터를 다룰 수 있는 사람"으로 한 단계 올라가게 됩니다.
파일 입출력의 큰 그림
파일 입출력은 말 그대로 프로그램이 파일과 데이터를 주고받는 작업입니다. 읽기는 외부 데이터를 프로그램 안으로 가져오는 일이고, 쓰기는 프로그램의 결과를 파일로 남기는 일입니다.
자바에서 파일 입출력을 이해할 때는 먼저 두 가지 관점을 구분하면 좋습니다. 하나는 데이터의 종류이고, 다른 하나는 파일을 다루는 방식입니다.
데이터의 종류는 크게 바이트와 문자로 나뉩니다.
바이트 스트림은 이미지, 영상, 실행 파일처럼 "있는 그대로의 데이터"를 다룹니다.
문자 스트림은 텍스트처럼 글자를 다룹니다.
텍스트 파일이라도 내부적으로는 바이트로 저장되지만, 사람이 읽는 글자 단위로 처리하려면 문자 스트림이 더 편합니다.
파일을 다루는 방식은 전통적인 I/O 방식과 NIO 방식으로 생각할 수 있습니다. 전통적인 I/O는 InputStream, OutputStream, Reader, Writer 같은 클래스를 중심으로 동작합니다. 반면 NIO는 Path, Files, Paths 같은 클래스를 이용해 파일 시스템을 더 직관적으로 다룰 수 있게 해 줍니다.
실무에서는 두 방식이 완전히 따로 노는 것이 아닙니다. 서로 연결되기도 하고, 상황에 따라 적절한 도구를 고르면 됩니다. 초보자에게는 보통 "스트림의 개념"을 이해한 뒤, 실제 파일 작업은 Path와 Files를 중심으로 시작하는 것이 가장 편합니다.
스트림이란 무엇인가
스트림은 데이터가 흐르는 통로입니다. 이름만 보면 강물 같지만, 비유가 꽤 잘 맞습니다. 데이터가 한쪽에서 다른 쪽으로 흘러갑니다.
입력 스트림은 외부에서 프로그램으로 데이터가 들어오는 통로입니다. 출력 스트림은 프로그램에서 외부로 데이터가 나가는 통로입니다.
예를 들어 파일에서 내용을 읽는다면 입력 스트림을 사용하고, 파일에 내용을 저장한다면 출력 스트림을 사용합니다.
자바에서는 다음 계열을 자주 봅니다.
InputStream,OutputStream: 바이트 기반Reader,Writer: 문자 기반BufferedInputStream,BufferedReader같은 버퍼 보조 클래스
여기서 버퍼는 한 번에 조금씩 처리하지 않고, 어느 정도 모아서 처리하도록 도와주는 임시 저장 공간입니다. 버퍼를 사용하면 성능이 좋아지는 경우가 많습니다. 물컵으로 수영장을 채우는 것보다 양동이를 쓰는 편이 낫다는 느낌으로 이해하면 됩니다.
바이트 스트림과 문자 스트림
텍스트 파일을 읽는다면 문자 스트림이 더 자연스럽습니다. 반대로 이미지 파일이나 PDF 파일 같은 바이너리 데이터는 바이트 스트림을 사용해야 안전합니다.
먼저 바이트 스트림 예제를 봅시다.
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteStreamExample {
public static void main(String[] args) {
try (FileInputStream in = new FileInputStream("input.bin");
FileOutputStream out = new FileOutputStream("output.bin")) {
int data;
while ((data = in.read()) != -1) {
out.write(data);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}이 코드는 파일을 1바이트씩 읽어서 그대로 다른 파일에 씁니다. 동작은 이해하기 쉽지만, 실제로는 너무 조금씩 처리하므로 비효율적일 수 있습니다. 그래서 보통은 바이트 배열을 사용합니다.
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
public class ByteArrayCopyExample {
public static void main(String[] args) {
try (FileInputStream in = new FileInputStream("input.bin");
FileOutputStream out = new FileOutputStream("output.bin")) {
byte[] buffer = new byte[1024];
int length;
while ((length = in.read(buffer)) != -1) {
out.write(buffer, 0, length);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}이제 문자 스트림 예제를 봅시다.
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
public class CharStreamExample {
public static void main(String[] args) {
try (FileReader reader = new FileReader("input.txt");
FileWriter writer = new FileWriter("output.txt")) {
int ch;
while ((ch = reader.read()) != -1) {
writer.write(ch);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}문자 스트림은 텍스트를 다루기 쉽지만, 문자 인코딩을 신경 써야 하는 경우가 있습니다. 특히 한글이 포함되면 더 중요합니다. 그래서 실무에서는 FileReader, FileWriter보다는 문자 집합을 명시할 수 있는 방식이 더 자주 쓰입니다. 이 부분은 뒤에서 Files와 함께 다시 보겠습니다.
try-with-resources로 안전하게 닫기
파일과 연결된 스트림은 사용이 끝나면 반드시 닫아야 합니다. 닫지 않으면 자원이 낭비되고, 어떤 운영체제에서는 파일이 잠겨 다음 작업이 실패할 수도 있습니다.
자바는 이를 편하게 처리하려고 try-with-resources 문법을 제공합니다.
try (BufferedReader br = Files.newBufferedReader(path)) {
// 사용
}괄호 안에 선언한 자원은 블록이 끝날 때 자동으로 닫힙니다. 예외가 발생해도 닫히므로 매우 안전합니다. 파일, 네트워크, 데이터베이스 연결처럼 "열고 나중에 닫아야 하는 것"에는 거의 습관처럼 적용하는 편이 좋습니다.
NIO와 Path, Files
요즘 자바에서 파일을 다룰 때는 java.nio.file 패키지의 Path와 Files를 중심으로 배우는 것이 훨씬 실용적입니다. 코드가 읽기 쉽고 기능도 풍부합니다.
File 클래스는 오래전부터 사용된 전통적인 방식입니다. 물론 지금도 많이 보이지만, 새로운 코드에서는 Path와 Files가 더 자주 선택됩니다.
먼저 Path는 파일이나 디렉터리의 경로를 표현합니다.
import java.nio.file.Path;
public class PathExample {
public static void main(String[] args) {
Path path = Path.of("data", "memo.txt");
System.out.println(path);
}
}Path.of("data", "memo.txt")는 운영체제에 맞는 경로 구분자를 알아서 처리합니다. 문자열을 직접 이어 붙이는 것보다 안전하고 깔끔합니다.
파일 존재 여부를 확인하는 것도 간단합니다.
import java.nio.file.Files;
import java.nio.file.Path;
public class ExistsExample {
public static void main(String[] args) {
Path path = Path.of("data", "memo.txt");
if (Files.exists(path)) {
System.out.println("파일이 있습니다.");
} else {
System.out.println("파일이 없습니다.");
}
}
}이런 코드는 설정 파일, 업로드 파일, 로그 파일을 다룰 때 자주 사용됩니다.
텍스트 파일 읽고 쓰기
텍스트 파일을 읽고 쓰는 가장 간단한 방법 중 하나는 Files.readString, Files.writeString입니다.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class ReadWriteStringExample {
public static void main(String[] args) {
Path path = Path.of("note.txt");
try {
Files.writeString(path, "안녕하세요n자바 표준 API 연습 중입니다.");
String content = Files.readString(path);
System.out.println(content);
} catch (IOException e) {
e.printStackTrace();
}
}
}작은 텍스트 파일이라면 이 방법이 아주 편합니다. 다만 파일이 매우 크면 한 번에 메모리로 읽어들이는 방식이 부담이 될 수 있습니다. 큰 파일은 한 줄씩 읽는 방식이 더 좋습니다.
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.List;
public class ReadLinesExample {
public static void main(String[] args) {
Path path = Path.of("names.txt");
try {
List<String> lines = Files.readAllLines(path, StandardCharsets.UTF_8);
for (String line : lines) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}여기서 StandardCharsets.UTF_8을 명시한 점이 중요합니다. 텍스트 파일은 인코딩이 맞지 않으면 글자가 깨질 수 있습니다. 한글이 포함된 텍스트를 다룰 때는 문자 집합을 명시하는 습관이 큰 사고를 줄여 줍니다.
큰 파일이라면 BufferedReader를 이용해 한 줄씩 읽는 편이 좋습니다.
import java.io.BufferedReader;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class BufferedReadExample {
public static void main(String[] args) {
Path path = Path.of("app.log");
try (BufferedReader reader = Files.newBufferedReader(path, StandardCharsets.UTF_8)) {
String line;
while ((line = reader.readLine()) != null) {
System.out.println(line);
}
} catch (IOException e) {
e.printStackTrace();
}
}
}쓰기 역시 버퍼를 이용할 수 있습니다.
import java.io.BufferedWriter;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class BufferedWriteExample {
public static void main(String[] args) {
Path path = Path.of("result.txt");
try (BufferedWriter writer = Files.newBufferedWriter(path, StandardCharsets.UTF_8)) {
writer.write("첫 번째 줄");
writer.newLine();
writer.write("두 번째 줄");
} catch (IOException e) {
e.printStackTrace();
}
}
}파일 생성, 복사, 이동, 삭제
실무에서 파일 내용만 읽고 쓰는 것은 절반입니다. 파일 자체를 생성하거나 옮기고 지우는 일도 정말 많습니다.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.StandardCopyOption;
public class FileOperationExample {
public static void main(String[] args) {
try {
Path dir = Path.of("backup");
Path source = Path.of("report.txt");
Path target = dir.resolve("report_backup.txt");
if (Files.notExists(dir)) {
Files.createDirectories(dir);
}
if (Files.notExists(source)) {
Files.writeString(source, "보고서 내용");
}
Files.copy(source, target, StandardCopyOption.REPLACE_EXISTING);
Files.move(target, dir.resolve("final_report.txt"), StandardCopyOption.REPLACE_EXISTING);
} catch (IOException e) {
e.printStackTrace();
}
}
}여기서 createDirectories는 중간 디렉터리가 없어도 한 번에 만들어 줍니다. 실무에서는 폴더 구조가 아직 없는 상태에서 로그 폴더, 업로드 폴더, 백업 폴더를 생성하는 일이 자주 있으므로 알아두면 유용합니다.
삭제는 조심해야 합니다. 파일이 없거나 디렉터리가 비어 있지 않으면 예외가 발생할 수 있기 때문입니다.
Files.delete(path);삭제할 대상이 없을 수도 있다면 다음처럼 더 안전하게 처리할 수 있습니다.
Files.deleteIfExists(path);코드는 짧지만, 실제 결과는 되돌리기 어렵습니다. 삭제 코드는 늘 한 번 더 확인하는 습관이 필요합니다. 개발자의 손끝은 생각보다 강력합니다.
디렉터리 탐색과 파일 목록 읽기
특정 폴더 안에 어떤 파일이 있는지 살펴보는 일도 흔합니다. 예를 들어 업로드 폴더를 검사하거나, 로그 디렉터리에서 오래된 파일만 골라야 할 수 있습니다.
import java.io.IOException;
import java.nio.file.DirectoryStream;
import java.nio.file.Files;
import java.nio.file.Path;
public class DirectoryExample {
public static void main(String[] args) {
Path dir = Path.of("data");
try (DirectoryStream<Path> stream = Files.newDirectoryStream(dir)) {
for (Path path : stream) {
System.out.println(path.getFileName());
}
} catch (IOException e) {
e.printStackTrace();
}
}
}하위 디렉터리까지 재귀적으로 순회하고 싶다면 Files.walk가 편리합니다.
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
public class WalkExample {
public static void main(String[] args) {
Path dir = Path.of("data");
try {
Files.walk(dir)
.filter(Files::isRegularFile)
.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
}
}이 코드는 스트림 API와도 자연스럽게 연결됩니다. 10장에서 배운 람다와 스트림이 여기서 갑자기 살아 움직이는 느낌이 들 것입니다. 이쯤 되면 표준 API가 문법 수업의 연장선이 아니라, 실제 일을 처리하는 주인공이라는 사실이 보이기 시작합니다.
파일 처리에서 자주 만나는 실수
파일 입출력은 처음엔 쉬워 보여도 작은 실수로 문제가 생기기 쉽습니다. 특히 다음 세 가지를 꼭 기억해 두면 좋습니다.
텍스트 파일에서는 인코딩을 명시하자.
큰 파일은 한 번에 읽기보다 나누어 처리하자.
스트림과 리더는 반드시 닫자.
여기에 하나를 더 보태면, 경로를 문자열로 직접 이어 붙이기보다 Path와 resolve를 쓰는 편이 좋습니다. 운영체제마다 경로 형식이 다를 수 있기 때문입니다.
날짜와 시간은 왜 어려운가
날짜와 시간은 언뜻 단순해 보이지만, 실제로는 아주 까다로운 주제입니다. 월마다 날짜 수가 다르고, 윤년이 있으며, 시차가 있고, 여름 시간제 같은 변수도 있습니다. 그래서 날짜를 숫자로 대충 계산하면 나중에 큰 실수를 부를 수 있습니다.
예전 자바에는 Date와 Calendar가 있었지만 사용이 불편하고 실수하기 쉬운 부분이 많았습니다. 그래서 현대 자바에서는 java.time 패키지를 사용하는 것이 표준에 가깝습니다.
java.time은 역할이 분명하게 나뉘어 있습니다.
LocalDate: 날짜만 표현LocalTime: 시간만 표현LocalDateTime: 날짜와 시간 표현ZonedDateTime: 시간대까지 포함한 날짜와 시간Duration,Period: 시간 차이와 날짜 차이 표현DateTimeFormatter: 포맷팅과 파싱 담당
이름만 봐도 훨씬 정리된 느낌이 들지요. 날짜 처리의 혼란을 줄이기 위해 설계된 패키지라고 이해하면 됩니다.
LocalDate, LocalTime, LocalDateTime
가장 먼저 익힐 클래스는 LocalDate, LocalTime, LocalDateTime입니다.
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
public class DateTimeBasicExample {
public static void main(String[] args) {
LocalDate today = LocalDate.now();
LocalTime nowTime = LocalTime.now();
LocalDateTime now = LocalDateTime.now();
System.out.println(today);
System.out.println(nowTime);
System.out.println(now);
}
}특정 날짜를 직접 만들 수도 있습니다.
LocalDate birthDay = LocalDate.of(2000, 5, 15);
LocalTime meetingTime = LocalTime.of(14, 30);
LocalDateTime exam = LocalDateTime.of(2026, 4, 25, 9, 0);이 API의 장점 중 하나는 불변 객체라는 점입니다. 불변 객체란 한 번 만들어진 뒤 내부 값이 바뀌지 않는 객체입니다. 그래서 날짜를 더하거나 빼더라도 원본을 수정하지 않고 새로운 객체를 반환합니다.
LocalDate today = LocalDate.now();
LocalDate nextWeek = today.plusWeeks(1);
LocalDate yesterday = today.minusDays(1);이 방식은 실수를 줄입니다. 원본이 슬쩍 바뀌어 버리는 버그가 적기 때문입니다.
날짜 계산하기
날짜 계산은 실무에서 정말 자주 등장합니다. 만료일 계산, 배송 예정일 계산, 예약 가능일 계산, 월말 정산일 계산 등 끝이 없습니다.
import java.time.LocalDate;
public class DateCalcExample {
public static void main(String[] args) {
LocalDate today = LocalDate.now();
LocalDate after30Days = today.plusDays(30);
LocalDate nextMonth = today.plusMonths(1);
LocalDate lastYear = today.minusYears(1);
System.out.println("오늘: " + today);
System.out.println("30일 후: " + after30Days);
System.out.println("다음 달: " + nextMonth);
System.out.println("작년: " + lastYear);
}
}plusDays(30)처럼 의미가 드러나는 메서드 이름 덕분에 코드가 읽기 쉽습니다. 이것이 표준 API를 잘 설계했다고 느끼는 순간 중 하나입니다.
요일도 구할 수 있습니다.
import java.time.DayOfWeek;
import java.time.LocalDate;
public class DayOfWeekExample {
public static void main(String[] args) {
LocalDate date = LocalDate.of(2026, 4, 19);
DayOfWeek dayOfWeek = date.getDayOfWeek();
System.out.println(dayOfWeek);
}
}스케줄링, 영업일 계산, 주말 제외 처리 같은 기능의 출발점이 되는 정보입니다.
기간과 시간 차이
두 날짜 사이의 차이를 다룰 때는 Period와 Duration을 구분하는 것이 중요합니다.
Period는 날짜 단위의 차이입니다. 예를 들어 "며칠 차이", "몇 개월 차이"를 계산할 때 어울립니다. Duration은 시간 단위의 차이입니다. 예를 들어 "몇 초 차이", "몇 시간 차이"를 계산할 때 적합합니다.
import java.time.Duration;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.Period;
public class PeriodDurationExample {
public static void main(String[] args) {
LocalDate startDate = LocalDate.of(2026, 4, 1);
LocalDate endDate = LocalDate.of(2026, 4, 19);
Period period = Period.between(startDate, endDate);
System.out.println(period.getDays());
LocalDateTime startTime = LocalDateTime.of(2026, 4, 19, 9, 0);
LocalDateTime endTime = LocalDateTime.of(2026, 4, 19, 11, 30);
Duration duration = Duration.between(startTime, endTime);
System.out.println(duration.toMinutes());
}
}날짜와 시간을 한꺼번에 다루는데 무조건 하나의 클래스만 쓰려고 하면 오히려 헷갈립니다. "날짜 중심인가, 시간 길이 중심인가"를 먼저 생각하면 적절한 클래스를 고르기 쉬워집니다.
날짜와 시간 포맷팅
사람에게 보여줄 날짜 문자열은 보기 좋게 가공해야 합니다. 반대로 문자열로 입력된 날짜를 프로그램이 이해할 수 있는 객체로 바꾸는 일도 필요합니다. 이때 사용하는 것이 DateTimeFormatter입니다.
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
public class FormatterExample {
public static void main(String[] args) {
LocalDateTime now = LocalDateTime.now();
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = now.format(formatter);
System.out.println(formatted);
}
}문자열을 날짜로 파싱하는 것도 가능합니다.
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
public class ParseExample {
public static void main(String[] args) {
String input = "2026-04-19";
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd");
LocalDate date = LocalDate.parse(input, formatter);
System.out.println(date.plusDays(7));
}
}패턴은 자주 쓰이는 만큼 익숙해질 필요가 있습니다. yyyy는 연도, MM은 월, dd는 일, HH는 24시간 형식의 시, mm은 분, ss는 초입니다. 대소문자를 잘못 쓰면 전혀 다른 뜻이 되므로 주의해야 합니다. 예를 들어 MM은 월이고 mm은 분입니다. 월과 분이 한 글자 차이로 서로 뒤바뀌면, 디버깅 중에 한숨이 길어질 수 있습니다.
시간대와 ZonedDateTime
국내 서비스만 다루면 시간대가 잘 드러나지 않을 수 있습니다. 하지만 해외 사용자, 클라우드 서버, 글로벌 로그 시스템과 연결되면 시간대는 매우 중요한 문제가 됩니다.
이럴 때 ZonedDateTime이 필요합니다.
import java.time.ZoneId;
import java.time.ZonedDateTime;
public class ZoneExample {
public static void main(String[] args) {
ZonedDateTime seoulTime = ZonedDateTime.now(ZoneId.of("Asia/Seoul"));
ZonedDateTime newYorkTime = ZonedDateTime.now(ZoneId.of("America/New_York"));
System.out.println(seoulTime);
System.out.println(newYorkTime);
}
}서버 시간이 UTC로 저장되고, 화면에는 사용자 지역 시간으로 보여 주는 구조는 실무에서 매우 흔합니다. 처음에는 조금 낯설어도, "저장 시간"과 "표시 시간"을 구분한다는 생각을 가지면 이해가 빨라집니다.
정규표현식은 문자열의 탐정 도구
정규표현식은 문자열에서 특정 패턴을 찾고 검사하고 바꾸는 규칙입니다. 처음 보면 기호가 많아서 외계어처럼 느껴질 수 있습니다. 하지만 자주 쓰는 패턴만 익혀도 효율이 크게 올라갑니다.
예를 들어 다음 작업들은 정규표현식이 잘 어울립니다.
이메일 형식이 대략 맞는지 확인하기
전화번호에서 숫자만 추출하기
로그 문자열에서 날짜나 코드 찾기
여러 공백을 하나로 바꾸기
정규표현식은 복잡한 문자열 문제를 짧게 풀 수 있게 도와줍니다. 다만 너무 욕심내서 한 줄에 모든 것을 해결하려고 하면 오히려 읽기 어려워질 수 있습니다. 강력하지만 다루는 사람의 절제도 필요한 도구입니다.
가장 기본적인 패턴 이해하기
정규표현식에서 자주 보는 기호부터 익혀 봅시다.
.: 아무 문자 하나d: 숫자 하나w: 영문자, 숫자, 밑줄 중 하나s: 공백 문자 하나+: 한 번 이상 반복*: 0번 이상 반복?: 0번 또는 1번^: 문자열 시작$: 문자열 끝
예를 들어 d+는 숫자가 하나 이상 이어지는 패턴입니다. ^d+$는 문자열 전체가 숫자로만 이루어졌는지 검사하는 패턴입니다.
String 메서드로 간단히 쓰기
정규표현식은 꼭 Pattern과 Matcher를 써야만 하는 것은 아닙니다. 간단한 작업은 String 메서드만으로도 할 수 있습니다.
public class RegexStringExample {
public static void main(String[] args) {
String input = "12345";
System.out.println(input.matches("^\d+$"));
String phone = "010-1234-5678";
String onlyNumbers = phone.replaceAll("[^\d]", "");
System.out.println(onlyNumbers);
String text = "java,python,javascript";
String[] arr = text.split(",");
for (String s : arr) {
System.out.println(s);
}
}
}matches는 전체 문자열이 패턴에 맞는지 검사합니다. 일부만 포함되어도 되는 것이 아니라, 문자열 전체가 기준에 맞아야 true가 됩니다. 이 점은 초보자가 자주 헷갈리는 부분입니다.
Pattern과 Matcher로 본격적으로 다루기
복잡한 정규표현식 작업은 Pattern과 Matcher를 사용하는 편이 낫습니다. 특히 반복적으로 재사용하거나, 매칭된 일부를 추출해야 할 때 유용합니다.
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class PatternMatcherExample {
public static void main(String[] args) {
String text = "문의 이메일: [email protected]";
Pattern pattern = Pattern.compile("[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}");
Matcher matcher = pattern.matcher(text);
if (matcher.find()) {
System.out.println("찾은 이메일: " + matcher.group());
}
}
}여기서 find()는 문자열 안에서 패턴에 맞는 부분을 찾습니다. group()은 찾은 결과를 가져옵니다. 로그에서 에러 코드만 추출하거나, 문장 속 URL만 뽑는 작업에 아주 잘 어울립니다.
그룹으로 필요한 부분만 뽑아내기
정규표현식의 강력함은 그룹에 있습니다. 그룹은 괄호 ()로 묶은 부분입니다. 그룹을 사용하면 패턴 전체가 아니라 일부 조각만 따로 꺼낼 수 있습니다.
예를 들어 날짜 문자열 2026-04-19에서 연도, 월, 일을 각각 분리해 보겠습니다.
import java.util.regex.Matcher;
import java.util.regex.Pattern;
public class GroupExample {
public static void main(String[] args) {
String text = "주문일: 2026-04-19";
Pattern pattern = Pattern.compile("(\d{4})-(\d{2})-(\d{2})");
Matcher matcher = pattern.matcher(text);
if (matcher.find()) {
System.out.println("연도: " + matcher.group(1));
System.out.println("월: " + matcher.group(2));
System.out.println("일: " + matcher.group(3));
}
}
}이 방법은 로그 분석, 데이터 정제, 문서 처리에서 특히 유용합니다. 문자열을 통째로 보는 대신 필요한 조각만 집어낼 수 있기 때문입니다.
자주 쓰는 실전 예제 세 가지
이제 실무에서 자주 만나는 상황을 짧게 묶어서 보겠습니다.
1. 로그 파일에서 ERROR 줄만 찾기
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
public class LogFilterExample {
public static void main(String[] args) {
Path path = Path.of("app.log");
try {
Files.lines(path, StandardCharsets.UTF_8)
.filter(line -> line.contains("ERROR"))
.forEach(System.out::println);
} catch (IOException e) {
e.printStackTrace();
}
}
}Files.lines는 파일을 스트림처럼 다룰 수 있게 해 줍니다. 큰 로그 파일에서 특정 패턴만 걸러낼 때 매우 편리합니다.
2. 파일명에 오늘 날짜 붙여 저장하기
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.time.LocalDate;
public class DailyReportExample {
public static void main(String[] args) {
LocalDate today = LocalDate.now();
String fileName = "report-" + today + ".txt";
Path path = Path.of(fileName);
try {
Files.writeString(path, "오늘의 보고서");
} catch (IOException e) {
e.printStackTrace();
}
}
}날짜 API와 파일 API가 만나는 전형적인 예입니다. 이런 작은 연결이 실전 프로그램의 기본 단위가 됩니다.
3. 사용자 입력에서 전화번호 숫자만 남기기
public class PhoneSanitizeExample {
public static void main(String[] args) {
String input = "010-1234-5678";
String normalized = input.replaceAll("[^\d]", "");
System.out.println(normalized);
}
}입력값 정리, 저장 전 정규화, 검색용 키 생성 등에 많이 쓰이는 패턴입니다.
세 기능을 연결해 생각하기
파일 입출력, 날짜·시간, 정규표현식은 따로 배우면 각각 다른 주제처럼 보입니다. 하지만 실제 업무에서는 자주 함께 등장합니다.
예를 들어 주문 내역 파일을 읽는다고 해 봅시다. 파일에서 한 줄씩 데이터를 읽고, 정규표현식으로 주문 번호를 검증하고, 주문 일자를 LocalDate로 파싱해 기간 계산을 할 수 있습니다. 또는 서버 로그 파일을 읽으면서 날짜 문자열을 분석하고, 특정 에러 코드만 추출해 리포트를 만들 수도 있습니다.
결국 중요한 것은 "API를 외우는 것"이 아니라 "문제에 맞는 도구를 고르는 것"입니다. 파일에서 가져오고, 문자열에서 걸러내고, 날짜로 계산한다는 흐름이 눈에 들어오기 시작하면 실전 감각이 붙기 시작한 것입니다.
초보자가 기억하면 좋은 선택 기준
무엇을 써야 할지 헷갈릴 때는 이렇게 정리해 보세요.
텍스트 파일은
Path와Files중심으로 시작한다.큰 파일은 버퍼나 스트림 방식으로 나누어 읽는다.
날짜는
java.time패키지로 처리한다.단순한 문자열 패턴 검사는
String메서드로 먼저 시도한다.정규표현식이 반복 사용되거나 복잡하면
Pattern과Matcher를 쓴다.
이 기준만 잡혀도 도구 선택이 훨씬 쉬워집니다.
마무리
이번 장에서 배운 내용은 자바 문법보다 덜 화려해 보일 수 있습니다. 하지만 실제 프로그램은 이런 표준 API 위에서 움직입니다. 파일을 읽고, 날짜를 계산하고, 문자열을 정리하는 기능이 모여서 업무 프로그램이 됩니다.
즉, 문법은 문장을 만드는 법이고, 표준 API는 그 문장으로 실제 일을 처리하는 도구입니다. 이제부터 자바는 단순한 학습용 언어가 아니라, 파일을 다루고 시간을 계산하고 데이터를 정리하는 실전 도구로 보이기 시작할 것입니다.
다음 장에서는 여러 작업이 동시에 돌아가는 세계, 즉 멀티스레딩과 동시성의 기초로 넘어갑니다. 지금까지는 프로그램이 한 줄로 움직였다면, 이제부터는 여러 줄이 함께 달리기 시작합니다. 물론 개발자의 머리도 같이 바빠집니다.
이 노트는 보도·비평·교육·연구 목적으로 작성되었습니다. 저작권 문의가 있으시면 에서 알려주세요.