본문 바로가기
Develop Study/Java 2025. 2. 19.

(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 .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교환의 웹 서비스에서 유용하게 사용할 수 있음

주요 기능

  1. Java 객체를 JSON 문자열로 변환 (Serialization)
    • toJson() 메서드를 사용하여 객체를 JSON 문자열로 변환
  2. JSON 문자열을 Java 객체로 변환 (Deserialization)
    • fromJson() 메서드를 사용하여 JSON 문자열을 Java 객체로 변환
  3. 복잡한 객체 구조 (중첩된 객체나 리스트 등)도 처리
  4. 커스터마이징 가능한 직렬화 및 역직렬화 기능 제공 (필드 이름 변경, 특정 필드 제외 등)
  5. 다른 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이외의 기본적인 파일 역시 바이트 타입으로 읽고 내보내기를 해야하는데 기본적인 로직의 틀을 알 수 있었기 때문에

필요에 의해서는 실제 서비스를 구성하면서 바로 사용할 수 있도록 해야할 것이다.