| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | |||
| 5 | 6 | 7 | 8 | 9 | 10 | 11 |
| 12 | 13 | 14 | 15 | 16 | 17 | 18 |
| 19 | 20 | 21 | 22 | 23 | 24 | 25 |
| 26 | 27 | 28 | 29 | 30 | 31 |
- AOP
- 냄새라도
- Transaction
- OOP
- JPA
- Ajax
- 전전긍긍
- 이벤트핸들러등록
- 로그백
- 디스크i/o
- DDD
- MVC
- string
- index
- 사면초가
- springsecurity
- 클린아키텍처
- 설정세팅
- SpringLegacy
- 이벤트핸들러this
- TDD
- 문제선택근거
- 공통규약
- 공통기술
- SEQUENCE
- MVC요청플로우
- 커뮤니티서버
- 단위테스트
- model
- JDBC
- Today
- Total
개발이군고구마
자바 I/O (2) - (문자) 파싱 구현체 만들기 본문
1. 학습 내용
데이터가 들어오면, 메모리(Buffer)에 올리고, 쪼개고(Split), 적절한 **그릇(Map)**에 담는다
텍스트 파일
↓
문자 스트림 (Reader)
↓
토큰 단위 해석
↓
구조화된 데이터 (Map / List / Object)
↓
매핑 / 조합 로직
- Java I/O: FileInputStream -> InputStreamReader -> BufferedReader로 이어지는 보조 스트림
- String 조작: split, substring, indexOf 만으로 원하는 문자열을 발라내기
- CSV → 구조화 (row, column)
- JSON → 구조화 (object, array, key-value)
- 자료구조 개념
- 두 구조를 어떤 공통 모델로 표현하고
- 어디서 매핑/조합할지 사고할 수 있는가
2. 주어진 상황
구현 결과
- 리스트에 담진 전화번호부 정보에 이름일 때 번호를 출력하고 싶음
- 이름 중복 허용하지 않는 전화번호 리스트의 총 개수를 구하고 싶음
public class MainTest {
public static void main(String[] args) {
PhoneList phoneList = new PhoneList();
// CSV 로드
phoneList.loadData(new CsvParser(), "data.csv");
// JSON 로드 (같은 리스트에 추가됨, 중복 이름은 자동 갱신)
phoneList.loadData(new JsonParser(), "data.json");
System.out.println("총 인원: " + phoneList.getTotalCount());
}
}
주어진 자료
- csv 자료
name : john, phone: 010-1234-1234,
name : peter, age: 010-1234-1234
....
- json 자료
[
{
name : "jeeho",
phone: "010-1234-1234"
},
{
name : "jeesu",
phone: "010-1234-1234"
},
....
]
구체 설계 포인트
Parser는 구현체로 분리하여 csv/json 파서에 공통으로 적용한다.
3. 핵심 원리
[A. 대용량 처리를 위한 I/O: BufferedReader]
대용량 파일은 한 번에 메모리에 올리면 OutOfMemoryError가 발생할 수 있음. 따라서 **버퍼(Buffer)**를 사용하여 데이터를 조각내어 읽어와야 함.
- Scanner: 사용이 쉽지만, 정규식을 사용하고 파싱 과정이 있어 대용량 처리에 느림.
- BufferedReader: 버퍼링을 통해 HDD/SSD 접근 횟수를 줄여 훨씬 빠름. (필수 선택)
[B. 검색과 중복 제거를 위한 자료구조: HashMap]
문제의 요구사항은 "이름으로 번호 찾기"와 "이름 중복 불허".
- ArrayList: 검색 시 처음부터 끝까지 훑어야 하므로 속도가 느림 ($O(N)$)
- HashMap: Key(이름)에 대한 해시값을 통해 즉시 접근하므로 검색 속도가 매우 빠름 ($O(1)$). 또한, Key는 중복될 수 없으므로 중복 제거 요구사항을 자동으로 만족
[C. 파싱 (Parsing) 전략]
라이브러리 없이 파싱하려면 문자열을 '토큰(Token)' 단위로 잘라야 함.
- String 메서드: split(), trim(), replace(), substring()
- 전략: 의미 없는 기호({, }, [, ], ")를 제거하고, 구분자(:, ,)를 기준으로 데이터를 추출
▶ (파일) CSV와 JSON의 공통점 / 차이점
| CSV | JSON | |
| 기본 단위 | 줄(Line) | 토큰(Token) |
| 구조 | 행/열 | 객체/배열 |
| 표현 | 위치 기반 | 키 기반 |
| 내부 표현 | List<String> | Map<String, Object> |
4.전략 패턴(Strategy Pattern)으로 파서 구현하기
[코드 설계]
- 핵심 개념 : 역할과 책임의 분리과 다형성 → 유지보수 쉬운 코드
- PhoneList : 데이터를 저장하고 조회하는 역할에만 집중
- Parser : 파일 형식이 CSV/JSON에 따라 달라지는 파싱 로직
1️⃣ Parser 인터페이스
PhoneList는 구체적으로 어떻게 파싱하는지 몰라도 되며, 주입받은 Parser에게 일을 **위임** 하기만 하면 됨
import java.util.Map;
// 파싱 기능을 추상화한 인터페이스
public interface Parser {
// 대용량 처리를 위해 Map을 인자로 받아 직접 채워넣는 방식 (Call by Reference 활용)
// 반환값을 List로 할 경우, 중간 단계 메모리 소모가 커지므로 Map을 전달받아 채우는 것이 효율적
void parse(String filePath, Map<String, String> phoneBook);
}
▶ 왜 인터페이스를 사용하였나? 👉 https://sweetjhpotato.tistory.com/530
2️⃣ 구현체: CsvParser, JsonParser
import java.io.BufferedReader;
import java.io.FileReader;
import java.io.IOException;
import java.util.Map;
// CSV 파싱 구현체
public class CsvParser implements Parser {
@Override
public void parse(String filePath, Map<String, String> phoneBook) {
// try-with-resources 구문 (AutoCloseable)
try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
String line;
while ((line = br.readLine()) != null) {
// 한 줄 예시: "name : john, phone: 010-1234-1234"
parseLine(line, phoneBook);
}
} catch (IOException e) {
throw new RuntimeException("CSV 파일 읽기 실패: " + filePath, e);
}
}
private void parseLine(String line, Map<String, String> phoneBook) {
String[] parts = line.split(","); // 쉼표로 1차 분리
String name = null;
String phone = null;
for (String part : parts) {
String[] keyValue = part.split(":"); // 콜론으로 2차 분리
if (keyValue.length < 2) continue;
String key = keyValue[0].trim();
String value = keyValue[1].trim();
if ("name".equals(key)) name = value;
else if ("phone".equals(key)) phone = value;
}
if (name != null && phone != null) {
phoneBook.put(name, phone);
}
}
}
// JSON 파싱 구현체
public class JsonParser implements Parser {
@Override
public void parse(String filePath, Map<String, String> phoneBook) {
try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {
String line;
String currentName = null;
String currentPhone = null;
while ((line = br.readLine()) != null) {
line = line.trim();
// 1. 이름 추출
if (line.contains("\"name\"")) {
currentName = extractValue(line);
}
// 2. 전화번호 추출
else if (line.contains("\"phone\"")) {
currentPhone = extractValue(line);
}
// 3. 객체 닫힘(},) 감지 -> 저장
if (line.startsWith("}")) {
if (currentName != null && currentPhone != null) {
phoneBook.put(currentName, currentPhone);
}
// 상태 초기화
currentName = null;
currentPhone = null;
}
}
} catch (IOException e) {
throw new RuntimeException("JSON 파일 읽기 실패: " + filePath, e);
}
}
// "key" : "value" 형태에서 value만 뽑아내는 헬퍼 메서드
private String extractValue(String line) {
int colonIndex = line.indexOf(":");
if (colonIndex == -1) return null;
String valuePart = line.substring(colonIndex + 1).trim();
// 앞뒤 따옴표(")와 뒤쪽 콤마(,) 제거
return valuePart.replaceAll("[\",]", "");
}
}
✴ BufferedReader의 readLine():
- 파일 전체를 String 변수 하나에 담으면 메모리가 터짐
- readLine()은 한 줄씩 읽어서 처리하고 버리므로, 수 기가바이트(GB)의 로그 파일도 처리할 수 있는 방식 (스트림 처리 방식)
✴ JSON 수동 파싱 (State Machine 아이디어):
- JSON 라이브러리 없이 구현할 때는 **'한 줄씩 읽으면서 상태를 기억'**하는 것이 요령
- 위 코드에서는 currentName, currentPhone이라는 임시 변수를 두고, }(닫는 괄호)가 나올 때 Map에 저장하고 변수를 비우는 방식을 사용. 이것이 simple-json이나 Jackson 같은 라이브러리가 내부적으로 동작하는 기초 원리
✴ Map<String, String> 사용 이유:
- 리스트(List)에 담고 매번 contains로 중복을 체크하면 데이터가 쌓일수록 속도가 급격히 느려짐
- Map의 Key는 유일성이 보장되므로, put("john", ...)을 두 번 호출해도 알아서 마지막 데이터로 갱신되어 중복 처리를 별도 로직 없이 해결
3️⃣ PhoneList 클래스 (사용자)
import java.util.HashMap;
import java.util.Map;
public class PhoneList {
private Map<String, String> phoneBook;
public PhoneList() {
this.phoneBook = new HashMap<>(); // 검색 속도 O(1) 및 중복 방지
}
// 다형성 활용: 어떤 Parser가 들어오든 상관없음
public void loadData(Parser parser, String filePath) {
parser.parse(filePath, this.phoneBook);
}
public String getPhoneNumber(String name) {
return phoneBook.getOrDefault(name, "번호 없음");
}
public int getTotalCount() {
return phoneBook.size();
}
}
5. JSON 파싱 로직 심층 분석
[State Machine 원리]
- 라이브러리 없이 JSON을 파싱할 때 가장 중요한 개념은 **"상태(State)를 기억하는 것"**. 이것은 컴파일러 이론의 어휘 분석(Lexical Analysis) 기초와 닿아 있음
- BufferedReader로 읽는 것은 한 번에 한 줄뿐. 따라서 이전 줄에서 읽은 정보를 어딘가에 저장해둬야 온전한 데이터 하나를 만들 수 있음
[논리 흐름]
임시 변수에 값을 모으고 닫는 괄호가 나올 때 맵에 저장 후 변수를 플러시(Flush)하는 방식
- 변수 선언 (바구니 준비):
- currentName, currentPhone이라는 변수를 루프 밖듬. 이 변수들은 **"지금 조립 중인 사람의 부품"**을 임시 보관하는 역할.
- 데이터 발견 (재료 손질):
- line.contains("\"name\"")이 참이면, 값을 발라내어 currentName에 담음. 아직 phone이 없으므로 Map에 넣을 수 없음. (대기 상태)
- 객체 종료 감지 (조립 완료):
- } (닫는 중괄호)를 만나는 순간이 바로 **"한 사람의 데이터가 끝났다"**는 신호
- 이때 비로소 Map.put(currentName, currentPhone)을 수행하여 확정
- 초기화 (다음 사람 준비):
- 저장이 끝났으므로 currentName, currentPhone을 다시 null로 만들어 다음 데이터를 받을 준비. 만약 초기화하지 않으면, 다음 데이터에 이름이 누락되었을 때 이전 사람의 이름이 섞여 들어가는 버그가 발생
6. 예외처리
자바 I/O는 Checked Exception을 강제하므로 처리가 필수
단순히 try-catch를 썼느냐보다 **"리소스를 안전하게 닫았는가?"**가 중요함
[ A. try-with-resources (Java 7+)]
// 권장 방식
try (BufferedReader br = new BufferedReader(new FileReader(path))) {
// ... 작업 수행 ...
} catch (IOException e) {
// 예외 처리
}
// 여기서 br.close()가 자동으로 호출됨 (성공하든 실패하든)
원리: BufferedReader가 AutoCloseable 인터페이스를 구현하고 있기 때문에, 블록을 빠져나갈 때 자동으로 자원을 반납
[ B. 예외의 종류와 처리 전략]
- FileNotFoundException: 파일 경로가 틀렸거나 파일이 없을 때 발생. IOException의 하위 클래스이므로, 별도로 잡고 싶다면 catch 블록을 분리해서 위에 작성해야
- IOException: 읽기 권한이 없거나, 디스크 오류 등으로 읽다 끊겼을 때 발생
- 파싱 예외 (논리적 오류): 파일은 읽었으나 형식이 엉망인 경우(예: JSON인데 닫는 괄호가 없음).
- 실무: 로그를 남기고 해당 라인만 스킵(Skip)한 뒤 계속 진행하는 것이 좋음. (100만 개 중 1개가 깨졌다고 프로그램이 죽으면 인되기 때문)
[C. 예외 전환]
위 코드의 catch 블록을 보면 IOException을 잡아서 RuntimeException으로 감싸서 던지고 있음.
catch (IOException e) {
throw new RuntimeException("파일 읽기 실패: " + filePath, e);
}
- 이유: Parser 인터페이스의 메서드 시그니처에 throws IOException을 덕지덕지 붙이면, 이를 호출하는 PhoneList와 Main까지 모두 예외 처리를 강제받아 코드가 지저분해짐.
- 전략: 복구 불가능한 시스템 오류(파일 없음 등)는 **Unchecked Exception(런타임 예외)**으로 포장(Wrap)하여 던져서, 최상단에서 한 번에 에러 로그를 출력하는 방식이 깔끔. 이를 **"예외 전환(Exception Translation)"**이라고 함
'SERVER > Java' 카테고리의 다른 글
| build gradle의 상세 과정 (0) | 2026.05.13 |
|---|---|
| 응집도/결합도 분석으로 본 '인터페이스를 사용하는 이유' (0) | 2026.01.09 |
| 자바 I/O (1) - 원리 (0) | 2025.12.29 |
| [자료구조] LIST 원리 (0) | 2025.12.26 |
| [자료구조] MAP 원리 (0) | 2025.12.19 |