Develop Study/Java
(25.02.19) Java Spring Boot 에서 파일 읽어오기 & 내보내기 (CSV)
항상 데이터를 JSON 형식으로만 주고 받는 RESTful API에 익숙할 수 있지만,
어플리케이션과 서비스에 따라서 인턴쉽회사에서 CSV기반의 파일로 여러 데이터를 한번에 DB에 옮기거나 읽으면서 처리를 해야하는 기능이 필요할 수도 있어, MongoDB 기반의 NoSQL기반의 DB와 연동한 상태에서
Download Upload 를 실습하고자 했다.
OpenCSV 라이브러리 의존성 추가 필수
dependencies {
implementation 'com.opencsv:opencsv:5.6' // OpenCSV 라이브러리 의존성 추가
}
- Java Spring Boot 내에서 CSV 파일을 읽는 것 외 쓰는 작업도 유용하게 할 수 있기 때문에 사용
- CSVReader나 CSVWriter 클래스를 사용해서 데이터의 매핑과 변환을 용이하게 할 수 있음
Request 의 파일 받아 읽어오기
Controller
@PostMapping("/upload/")
public ResponseEntity<String> uploadCsvFile(@RequestParam("file") MultipartFile file) {
try {
csvService.uploadUserCsv(file); // CSV 업로드 처리
return ResponseEntity.ok("CSV 파일 업로드 완료");
} catch (Exception e) {
return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body(e.getMessage());
}
}
MultipartFile 타입
- Spring 에서 파일 업로드를 처리할 때 사용하는 Interface
- HTTP 요청의 multipart/form-data 형식으로 전송했을 때, Spring이 이를 MultipartFile 객체로 변환 후, Controller에서 받을 수 있도록 함
- CSV 파일을 @RequestParam으로 MultipartFile타입으로 받아올 수 있음
- 주요 메서드
메서드이름 | 설명 |
getOriginalFilename() | 업로드된 파일의 원본 이름 반환 |
getSize() | 파일 크기(byte 단위) 반환 |
getContentType() | 파일의 MIME 타입 반환 (text/csv, image/png 등) |
getBytes() | 파일 내용을 byte 배열로 반환 |
getInputStream() | 파일 내용을 읽을 수 있는 InputStream 반환 |
transferTo(File dest) | 파일을 지정된 위치에 저장 |
- 지원 타입 유형 (MIME 타입 기준)
- 멀티미디어를 지원하기 때문에, byte배열로 변환해서 처리
파일 유형 | MIME 타입 (getContentType()) | 원본 파일 확장자 |
텍스트 파일 | text/plain | .txt |
CSV 파일 | text/csv | .csv |
JSON 파일 | application/json | .json |
XML 파일 | application/xml | .xml |
PDF 파일 | application/pdf | |
엑셀 파일 | application/vnd.ms-excel | .xls, .xlsx |
이미지 파일 | image/jpeg, image/png, image/gif | .jpg, .png, .gif |
오디오 파일 | audio/mpeg, audio/wav | .mp3, .wav |
비디오 파일 | video/mp4, video/x-matroska | .mp4, .mkv |
Service
public void uploadUserCsv(MultipartFile file) throws Exception {
try (CSVReader csvReader = new CSVReader(new InputStreamReader(file.getInputStream()))) {
String[] nextRecord;
List<User> users = new ArrayList<>();
// Pass Header
csvReader.readNext();
while ((nextRecord = csvReader.readNext()) != null) {
String name = nextRecord[0];
String email = nextRecord[1];
String purchases = nextRecord[2];
// JSON -> List<Purchase>
Type purchaseListType = new TypeToken<List<Purchase>>() {}.getType();
List<Purchase> purchaseList = gson.fromJson(purchases, purchaseListType);
User user = User.builder()
.name(name)
.email(email)
.purchases(purchaseList)
.build();
users.add(user);
}
userRepository.saveAll(users);
}
}
- MultipartFile 타입을 읽어오는 로직에서는 파일처리 단위에서 문제가 발생할 수 있기 때문에 항상 try - catch를 통해 예외처리를 안전하게 해줘야함
- 클라이언트에서 파일을 업로드하지 않을 경우
- 파일의 크기가 서버가 대응하기에 너무 클 경우
- 파일을 읽는 도중에 IOException처럼 읽기에 오류가 생겼을 경우
- 파일에 대해서 파싱이 진행되는 과정에서 오류가 발생한 경우
- etc
- 위에서 CSV파일을 String[] 으로 InputStreamReader로 읽어올 수 있고, 첫번째 읽어오는 것은 각 컬럼의 이름이나 설명인 Header이기 때문에 이를 넘기고 진행
- String[] nextRecord; 읽어오는 컬럼 수를 모르기 때문에 사이즈 정해서 배열 크기 고정을 하면 안되는 동적이기 때문에 미리 정해놓으면 안됨
파일 유형별 Reader & 처리 방법
파일 유형 | 사용 가능한 Reader / 라이브러리 | 반환 데이터 타입 | 예제 |
일반 텍스트 (.txt) | BufferedReader, InputStreamReader | String (라인별) | BufferedReader reader = new BufferedReader(new InputStreamReader(file.getInputStream())); String line = reader.readLine(); |
CSV (.csv) | BufferedReader, OpenCSV, Apache Commons CSV | String[] (행 단위) | CSVReader csvReader = new CSVReader(new InputStreamReader(file.getInputStream())); String[] row = csvReader.readNext(); |
JSON (.json) | BufferedReader, Gson, Jackson | Map<String, Object>, List<Object> | Map<String, Object> jsonMap = new ObjectMapper().readValue(file.getInputStream(), Map.class); |
XML (.xml) | BufferedReader, DOM Parser, SAX Parser, Jackson XML | Document (DOM 객체) | Document doc = DocumentBuilderFactory.newInstance().newDocumentBuilder().parse(file.getInputStream()); |
Excel (.xlsx, .xls) | Apache POI(XSSFWorkbook, HSSFWorkbook) | Workbook(XSSFWorkbook, HSSFWorkbook) | Workbook workbook = new XSSFWorkbook(file.getInputStream()); |
이미지 (.jpg, .png, .bmp) | ImageIO | BufferedImage | BufferedImage image = ImageIO.read(file.getInputStream()); |
오디오 (.mp3, .wav) | AudioSystem | AudioInputStream | AudioInputStream audioStream = AudioSystem.getAudioInputStream(file.getInputStream()); |
비디오 (.mp4, .avi, .mkv) | FFmpeg (외부 툴) | byte[] (파일 변환 필요) | ProcessBuilder pb = new ProcessBuilder("ffmpeg", "-i", "input.mp4", "output.mp3"); pb.start(); |
- 텍스트 계열 (TXT, CSV, JSON, XML) → String, Map, List, Document 등으로 변환 가능
- Excel 파일은 Workbook 객체로 다뤄야 함
- 이미지, 오디오, 비디오는 BufferedImage, AudioInputStream 등의 전용 객체로 변환 필요
- 비디오 파일은 Java에서 직접 처리하기 어렵고, FFmpeg 같은 외부 도구 필요
Gson의 사용
- Google에서 제공하는 Java 라이브러리
- Java 객체와 JSON 간의 변환 (Serialization/Deserialization)을 도와주는 역할로 JSON교환의 웹 서비스에서 유용하게 사용할 수 있음
주요 기능
- Java 객체를 JSON 문자열로 변환 (Serialization)
- toJson() 메서드를 사용하여 객체를 JSON 문자열로 변환
- JSON 문자열을 Java 객체로 변환 (Deserialization)
- fromJson() 메서드를 사용하여 JSON 문자열을 Java 객체로 변환
- 복잡한 객체 구조 (중첩된 객체나 리스트 등)도 처리
- 커스터마이징 가능한 직렬화 및 역직렬화 기능 제공 (필드 이름 변경, 특정 필드 제외 등)
- 다른 JSON 처리 라이브러리보다 가볍고 빠르게 동작
Gson에서의 제네릭 타입 처리 : TypeToken
- Java에서 제네릭 타입은 컴파일 시간에 구체적인 타입이 결정되기 때문에, 런타임에는 이 정보가 사라지게 되어버림 → 제네릭 타입을 써야 미리 JSON을 객체로 변환할때 Gson이 알수 있도록 명시적으로 해줄 필요가 있음
- 위에서 List<Purchase>는 컴파일 타임에는 정확한 타입이지만, 런타임에서는 List라는 일반적인 타입으로 밖에 알지 못함 → 정확하게 Purchase까지 명시
- new TypeToken<List<Purchase>>() {}은 익명 클래스
- 어떤 패턴이 아니라, 익명 클래스를 일단 새롭게 만들어 내는 간소화 한 것
- 바로 getType 을 사용 가능
동작 순서
- 의존성 주입 필요
implementation 'com.google.code.gson:gson:2.8.8' // 최신 버전 사용
- 객체 → JSON (Serialization)
- gson.toJson(대상 객체)
...
public static void main(String[] args) {
User user = new User("Alice", "alice@example.com");
Gson gson = new Gson();
// 객체를 JSON 문자열로 변환
String jsonString = gson.toJson(user);
System.out.println(jsonString); // {"name":"Alice","email":"alice@example.com"}
}
- JSON → 객체 (Deserialization)
- gson.fromJson(대상 타입, JSON으로 변환할 타입)
...
public static void main(String[] args) {
String jsonString = "{\\"name\\":\\"Alice\\",\\"email\\":\\"alice@example.com\\"}";
Gson gson = new Gson();
// JSON 문자열을 User 객체로 변환
User user = gson.fromJson(jsonString, User.class);
System.out.println(user.getName()); // Alice
System.out.println(user.getEmail()); // alice@example.com
}
Response 로 파일 내보내기(다운로드)
Controller
@GetMapping("/download")
public ResponseEntity<byte[]> downloadCsvFile() throws Exception{
byte[] response = csvService.downloadUserCsv();
if(response == null) {
return ResponseEntity.internalServerError().build();
}
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=users.csv");
return new ResponseEntity<>(response, headers, HttpStatus.OK);
}
- HttpHeaders.CONTENT_DISPOSITION
- Http 헤더에 Content-Disposition ****를 포함 시켜서 Client에게 파일 처리방식을 지정할 수 있도록 전달하는 방식
- Client가 파일을 다운로드 하려고 할 때 이 설정이 파일다운로드를 할 수 있도록 처리를 해줄 수 있음
- 바이트를 통해서 브라우저가 알아서 변환해서 파일을 다운로드할 수 있는 것
- "attachment; filename=users.csv"
- attachment: 파일을 첨부파일로 전송한다는 뜻
- filename=users.csv: 첨부파일 이름을 지정
Service
public byte[] downloadUserCsv() throws Exception {
try{
List<User> userList = userRepository.findAll();
ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
CSVWriter csvWriter = new CSVWriter(new OutputStreamWriter(outputStream));
// CSV Header
String[] header = {"id", "name", "email", "purchases"};
csvWriter.writeNext(header);
// 사용자 데이터로 CSV 작성
for (User user : userList) {
String[] data = {
user.getId(),
user.getName(),
user.getEmail(),
gson.toJson(user.getPurchases())
};
csvWriter.writeNext(data);
}
csvWriter.close();
// CSV -> byte[]
return outputStream.toByteArray();
} catch (Exception e) {
return null;
}
}
- 전체적인 로직은 업로드와 반대로만 작동
- 헤더를 설정
- 객체를 String[] 타입으로 변환하면됨
- 여기서 gson.toJson을 사용해서 JSON으로 바꿔줘야함
- csvWriter.close();
- CSVWriter는 파일이나 스트림에 데이터를 쓰는 작업이기 때문에 스트림을 꼭 닫아야지, 그렇지 않는다면 리소스가 계속 대기하면서 기록되지 않거자 리소스 누수 발생할 수 있음
- 내부에서 OutputStream이 일어나기 때문에
- 일종의 flush() 역할
매우 중요한 기능이 될 수도 있고,
특히 CSV이외의 기본적인 파일 역시 바이트 타입으로 읽고 내보내기를 해야하는데 기본적인 로직의 틀을 알 수 있었기 때문에
필요에 의해서는 실제 서비스를 구성하면서 바로 사용할 수 있도록 해야할 것이다.
'Develop Study > Java' 카테고리의 다른 글
(24.10.29) Comparator 인터페이스와 람다식 (0) | 2024.10.29 |
---|---|
(24.10.10) TDD & DDD / Filter & Interceptor & AOP 비교해보기 (0) | 2024.10.10 |
(24.08.09)[17주차] WebSocket과 실시간 메시지 구조 (0) | 2024.08.09 |
(24.07.09)[13주차] @DataJpaTest 슬라이스 테스트에서의 빈 등록 (0) | 2024.07.09 |
(24.07.03)[12주차] QueryDSL사용을 위한 Repository 분리 (0) | 2024.07.03 |