본문 바로가기
실습/리눅스 서버 + 스프링 부트

[JAVA] OpenCSV를 이용한 CSV 파싱

by 이민우 2024. 4. 1.
728x90
반응형

프로젝트 내 요구사항 중에는 아래와 같은 요구사항이 존재했다.

  • 사용자가 엑셀 혹은 csv 파일을 서버에 업로드할 수 있을 것
  • 사용자가 엑셀 혹은 csv 파일을 서버에서 다운로드할 수 있을 것

 

사실 위 요구사항이 들어간 프로그램의 개발은 첫 번째가 아니다. 이전에 비슷하지만 다른 프로그램을 개발할 때도 csv 기반 다운로드를 지원하라는 요구사항이 존재했다.

 

그 당시의 나는 아래와 같이 생각했었다.

CSV는 쉼표(,)를 기반으로 데이터가 나누어지니, 그냥 쉼표로 데이터를 구분하면 되겠지?

 

그런데 이는 틀린 생각이었다. 대표적인 예시로 아래 데이터를 들 수 있을 것 같다.

col_1 col_2 col_3
abc data: ["abs","vsad","scasd"] 1

 

위 데이터를 쉼표(,)로 나누면 각 row는 어떻게 나뉠까?

컬럼 로우는 세 개로 나뉠 것이다. 그렇다면 밸류 로우는?

 

당연히 쉼표에 따라 구문한다면 아래와 같이 나뉘게될 것이다.

  • abc
  • data: ["abc"
  • "vsad"
  • "scasd"]
  • 1

 

처음에는 이러한 부분을 하드 코딩으로 해결하려고 했었다. 그런데 문득 이런 생각이 들었다.

하드코딩을 굳이 해야하나? 누군가 만들어놓은 라이브러리가 있지 않을까?

 

이러한 생각이 들어 구글링을 해보았고 역시 OpenCSV라는 CSV 파서가 존재함을 확인했다. 이를 통해 이전에 프로그램을 개발했고, 이번 프로젝트에서도 개발을 성공적으로 수행했다.

 

그래서 복기겸 이번에는 OpenCSV를 이용해 CSV 파일을 파싱하는 코드를 공유하고자 한다.

 

 

프로젝트 설정

프로젝트는 직전까지 사용중인 프로젝트를 가져왔다.

https://123okk2.tistory.com/506

 

[JAVA] POI 라이브러리를 사용한 엑셀 파싱

직전 프로젝트 두 번째 복기를 해볼까 한다. 우선 간단하게 설명하자면, 나는 사용자에게 파일을 받아 이를 HTTPFS를 통해 HDFS에 저장하고, 내용을 파싱해서 DB에 저장한느 프로그램을 개발했다.

123okk2.tistory.com

 

프로젝트에서 OpenCSV를 사용하기 위한 pom.xml에 다음과 같은 디펜던시만 추가했다.

<dependency>
	<groupId>org.apache.poi</groupId>
	<artifactId>poi-ooxml</artifactId>
	<version>5.2.5</version>
</dependency>

 

그리고 아래는 CSV 파일을 파싱하기 위한 메서드이다. 별도의 설명은 주석으로 갈음하며, CommonCode의 경우에는 원래는 Service와 Controller 간 통신에 사용하기 위한 시그널 집합으로, 위 포스팅에 기재되어있다.

 

UploadService.java

package com.mwlee.test.service;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.text.DecimalFormat;
import java.util.ArrayList;
import java.util.List;

import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import com.mwlee.test.common.CommonCode;
import com.opencsv.CSVReader;
import com.opencsv.exceptions.CsvValidationException;

import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class UploadService {
	
	private static final String tmpDir = "C:\\files\\";
	private DecimalFormat decimalFormat = new DecimalFormat("0");
	
	/**
	 * MultiPartFile을 File로 임시저장
	 * 
	 * @param file
	 * @return
	 * @throws IOException
	 */
	public File saveAndGetTmpFile(MultipartFile file) throws IOException {
		File tmpFile = new File(tmpDir + file.getOriginalFilename());
		
		file.transferTo(tmpFile);
		
		return tmpFile;
	}
	
	/**
	 * 임시파일 삭제
	 * 
	 * @param file
	 * @return
	 */
	public boolean deleteTmpFile(File file) {
		file.delete();
		if(file.exists()) {
			return false;
		}
		return true;
	}

	/**
	 * CSV 파일 파싱
	 * @param file
	 * @return
	 */
	public Integer uploadCsvFile(MultipartFile file) {
		File tmpFile = null;
		try {
			// 임시 파일 저장 : 파일 크기가 너무 크면 데이터를 못불러 오는 현상이 일부 발생하여 저장 후 처리
			tmpFile = saveAndGetTmpFile(file);
		}
		catch(IOException e) {
			log.error("@@ Error Occured While Save TmpFile : {}", e.getMessage());
			return CommonCode.ERR_INTERNAL;
		} 
		
		try {
			// 파일 불러오기
			BufferedReader reader = new BufferedReader(new FileReader(tmpFile));
			CSVReader csvReader = new CSVReader(reader);
			
			// 한 줄씩 read
			String[] nextRecord;
			List<String> keys = new ArrayList<>();
			List<List<String>> values = new ArrayList<>();
			while((nextRecord = csvReader.readNext()) != null) {
				List<String> arr = new ArrayList<>();
				// 한 칸씩 read
				for(String cell : nextRecord) {
					arr.add(cell);
				}
				
				if(keys.size() == 0) {
					// 첫째 줄이면 keys에 입력
					keys = arr;
				}
				else {
					while(arr.size() != keys.size()) {
						/**
						 * 중간 값이 비어있는건 문제가 없지만 마지막 값이 비어있는 건 문제가될 수 있음
						 * 예를 들어
						 * 
						 * col_1,	col_2,	col_3
						 * 1,		,		3
						 * 
						 * 이면 (1, null, 3)이지만
						 * 
						 * col_1,	col_2,	col_3
						 * 1,		2
						 * 이면 (1, 2)가 됨.
						 * 고로 size()가 맞지 않으면 뒤에 null들을 추가해줘야 함.
						 */
						arr.add(null);
					}
					values.add(arr);
				}
			}
			
			// 잘 입력됐나 출력해보기
			for(String key : keys) {
				System.out.print(key + "\t");
			}
			System.out.println();
			for(List<String> value : values) {
				for(String val : value) {
					System.out.print(val + "\t");
				}
				System.out.println();
			}
		} catch (CsvValidationException | IOException e) {
			
			log.error("@@ Error Occured While Open CSV : {}", e.getMessage());
			return CommonCode.INVALID_FILE;
			
		} finally {
			// 임시 파일 삭제
			if(!deleteTmpFile(tmpFile)) {
				log.warn("@@ Can't Delete TmpFile {}. Delete Manually.", tmpFile.getName());
			}
		}
		
		return CommonCode.SUCCESS;
	}
}

 

 

테스트

이제 테스트를 위해 아래의 두 개의 csv 파일을 생성했다.

하나는 모든 데이터가 정상적으로 삽입된 데이터이고, 다른 하나는 데이터를 하나씩 뺀 데이터이다.

 

이제 파일들을 돌려볼 test 코드를 작성한다.

@Test
void testUploadCsv() {
	try {
		// 사용자에게 업로드되었다고 가정하기 위해 MultipartFile 전송
		String[] filePaths = new String[] {
				"C://test-csv.csv",
				"C://test-csv-1.csv"
		};
		for(String filePath : filePaths) {
			File file = ResourceUtils.getFile(filePath);
			InputStream is = new FileInputStream(file);
			MultipartFile mFile = new MockMultipartFile("file", file.getName(), "application/octet-stream", is);
			
			Integer result = service.uploadCsvFile(mFile);
			
			if(result == CommonCode.SUCCESS) {
				log.info("@@ Test Scuuess.");
			}
			else if(result == CommonCode.INVALID_FILE) {
				log.info("@@ Test Fail. Invalid File");		
			}
			else if(result == CommonCode.ERR_INTERNAL) {
				log.info("@@ Test Fail. Error Internal");
			}
		}
	}
	catch(Exception e) {
		e.printStackTrace();
	}
}

 

 

이제 결과를 확인해보면 정상적으로 삽입이 완료됨을 확인할 수 있다.

728x90
반응형