Search

2025-02-11 버그 해결

엣지 케이스 발견
문제 데이터
"0홍길""동","0 동 ""번지""",0,0.2,2024-11-20,"0"" 한글","0"" 한글","0""\n 한글","0"" 한글","0"" 한글"\n "1홍길""동","1 동 ""번지""",1,2.2,2024-11-19,"1"" 한글","1"" 한글","1""\n 한글","1"" 한글","1"" 한글"\n "2홍길""동","2 동 ""번지""",2,4.2,2024-11-18,"2"" 한글","2"" 한글","2""\n 한글","2"" 한글","2"" 한글"\n "3홍길""동","3 동 ""번지""",3,6.2,2024-11-17,"3"" 한글","3"" 한글","3""\n 한글","3"" 한글","3"" 한글"\n "4홍길""동","4 동 ""번지""",4,8.2,2024-11-16,"4"" 한글","4"" 한글","4""\n 한글","4"" 한글","4"" 한글"\n
Plain Text
복사
문제 상황
해당 데이터에서 \n 이 buf의 buf[buf.length-1]에 딱 맞아 떨어질 경우 해당 위치를 row의 delimiter로 판단해버리는 케이스 발견. 정상적 출력의 경우 1) 과 같이 되어야 하지만 2)와 같은 형태로 출력됨. 이 때문에 이후 데이터가 모두 밀려서 출력 되어지는 현상 발생.
1)"3홍길""동","3 동 ""번지""",3,6.2,2024-11-17,"3"" 한글","3"" 한글","3""\n 한글","3"" 한글","3"" 한글"\n 2) "3홍길""동","3 동 ""번지""",3,6.2,2024-11-17,"3"" 한글","3"" 한글","3""\n
Plain Text
복사
해결 방식
1.
pos, begin, limit를 활용하여 csvConfiglineDelimiter의 길이를 가져와 해당 길이를 더한 만큼의 길이를 extendAndLoad() 호출.
기각 - 해당 방식의 경우 CsvConfig class 의 종속성 발생 및 pos, begin 관리 시 유지 보수성 떨어짐.
2.
해당 엣지 발생 사유 분석 - 결국 상태 inQuote, matchIndex가 지역변수로 선언되어 fill() 이후 초기화됨으로 인하여 delimiter를 인식못함.
해결 - inQuote, matchIndex 인스턴스 변수로 추출.
전체 코드 package parser3; import java.io.Reader; import java.util.ArrayList; import java.util.List; import java.util.Optional; import java.util.function.Function; import java.util.function.IntPredicate; import java.util.function.Predicate; public class RecordParser implements CSVParser<List<Record>> { private final InternalBuffer2 buffer; private final CSVConfig csvConfig; // TODO 차후 추가 private final CSVParser<List<String>> fieldParser; private int skipLine; private boolean inQuote = false; private int matchIndex = 0; public RecordParser(Reader reader, int bufferCapacity ,CSVConfig csvConfig) { if (bufferCapacity <= 0) throw new IllegalArgumentException("Buffer capacity cannot be less than 1."); this.buffer = new InternalBuffer2(reader); this.csvConfig = csvConfig; // TODO 차후 추가 this.fieldParser = new FieldParser(buffer, csvConfig); this.skipLine = csvConfig.getSkipLine(); } @Override public List<Record> parse(){ final List<String> list = new ArrayList<>(); final StringBuilder builder = new StringBuilder(); while (buffer.fill()){ normalParse(buffer,list, builder); } return null; } private void normalParse(InternalBuffer2 buffer, List<String> record, StringBuilder builder){ while (buffer.notBeginAtLimit()){ char c = buffer.getBeginAndIncrement(); if (c == csvConfig.getQuote()) { inQuote = !inQuote; }else if (!inQuote){ matchIndex = incrementIfMatch(csvConfig.getLineDelimiter(), c, matchIndex); if (matchIndex == csvConfig.getLineDelimiterLength()){ parse(record, builder); matchIndex = 0; inQuote = false; } } } } private void parse(List<String> record, StringBuilder builder){ int length = (matchIndex == 1 ? buffer.getBegin() + matchIndex : buffer.getBegin() + matchIndex - 1) - buffer.getPos() - 1; record.add(builder.append(buffer.getBuf(), buffer.getPos(), length).toString()); builder.setLength(0); buffer.setPos(buffer.getBegin()); } /** * lastRow의 경우는 pos 부터 limit까지 넘겨버리면 됨 * 어차피 row 단위로 하기때문에 row가 존재함은 무조건 보장됨 * * normalParse -> return List<Record> * boolean inQuote = false; * int matchIndex = 0; * * -> while 조건 * char c = getCharAtBegin(); * if -> csvConfig.getQoute() == c * inQuote = !inQuote * * if -> !inQuote && mathcIndex == 1 * fieldParser -> 넘김 * matchIndex = 0; 초기화 * buffer.newPos(buffer.getIncrementBegin()); * break; * buffer.getIncrementBegin(); * */ @Override public boolean canParse() { if (skipLine > 0){ skipLine--; return false; } return true; } }
Java
복사
package parser3; import lombok.AccessLevel; import lombok.Getter; import java.io.IOException; import java.io.Reader; import java.io.UncheckedIOException; import java.util.Optional; import java.util.function.Function; @Getter public class InternalBuffer2 { @Getter(AccessLevel.NONE) private final Reader reader; private static final int DEFAULT_BUFFER_SIZE = 8192; private static final int READ_SIZE = DEFAULT_BUFFER_SIZE; private BufferState state = BufferState.INITIAL; private char[] buf = new char[DEFAULT_BUFFER_SIZE]; private int pos, begin, limit; private enum BufferState { INITIAL, PROGRESSING, EXTEND, RELOCATE, LAST_LINE; } public InternalBuffer2(Reader reader) { this.reader = reader; } /** * TODO isLastLine 추가 * RecordParser내에서 마지막 줄 여부 판단을 위해서 필요함 * <p> * 상태 전이 [] 반복, () 선택, {} 종료 * INITIAL -> [PROGRESSING <-> (EXTEND | RELOCATE)] -> LAST_LINE{종료} */ boolean fill() { while (true) { switch (state) { case INITIAL: int cnt = read(buf, pos, READ_SIZE); state = BufferState.PROGRESSING; if (cnt == -1){ return false; } limit = cnt; return true; case PROGRESSING: /** * if -> begin >= limit 일 경우 즉, 커서가 끝까지 갔는데 아무 변화 없을때 * state = (pos == 0) ? EXTEND : RELOCATE; * continue; * retrun true; */ if (begin >= limit) { state = pos == 0 ? BufferState.EXTEND : BufferState.RELOCATE; continue; } return true; case EXTEND: /** * 확장 및 read * extend() 확장 * cnt = read(begin) -> READ_SIZE 만큼 읽기 * if -> cnt == -1 LAST_LINE * else -> PROGRESSING and limit += cnt * continue; */ extendAndLoad(); return true; case RELOCATE: /** * 재배치 및 read * relocate() * begin = limit - pos; * pos = 0; * cnt = read(begin) // READ_SIZE 만큼 데이터 로드 * if -> cnt == -1 LAST_LINE * else -> PROGRESSING */ relocateAndLoad(); return true; case LAST_LINE: //TODO 라스트 삭제 예정 return false; } } } static <T, R> Optional<Function<T, R>> ifLastRowOrElse(InternalBuffer2 buffer, Function<T, R> action, Function<T, R> elseAction) { if (buffer.getPos() == buffer.getLimit()){ return Optional.empty(); } boolean isLastLine = buffer.getState() == BufferState.LAST_LINE && buffer.getPos() < buffer.getLimit(); return Optional.of(isLastLine ? action : elseAction); } char getCharAtBegin(){ return buf[begin]; } int getIncrementBegin(){ return ++begin; } char getBeginAndIncrement(){ return buf[begin++]; } void setPos(int newPos){ this.pos = newPos; } boolean notBeginAtLimit(){ return begin < limit; } private int read(char[] buf, int off, int len) { try { return reader.read(buf, off, len); } catch (IOException e) { throw new UncheckedIOException(e); } } private void extendAndLoad() { // TODO max size 예외처리 char[] newBuf = new char[buf.length * 2]; System.arraycopy(buf, 0, newBuf, 0, buf.length); buf = newBuf; int cnt = read(buf, begin, READ_SIZE); stateAndLimitUpdate(cnt, limit + cnt); } private void relocateAndLoad() { final int copyLength = begin - pos; System.arraycopy(buf, pos, buf, 0, copyLength); int cnt = read(buf, copyLength, READ_SIZE - copyLength); pos = 0; begin = copyLength; stateAndLimitUpdate(cnt, copyLength + cnt); } private void stateAndLimitUpdate(int readSize, int newLimit){ state = readSize == -1 ? BufferState.LAST_LINE : BufferState.PROGRESSING; limit = readSize == -1 ? limit : newLimit; } }
Java
복사