[Spring Batch] Spring Batch 로 정산 시스템 만들어보기 (4)
🎯 정산 정보 CSV 파일 다운로드 API
요구사항을 다시 한번 살펴보면 다음과 같습니다.
정산 결과 파일 생성 및 다운로드
- 정산 결과 파일 생성: 정산 결과를 CSV 또는 Excel 파일로 생성하는 기능을 구현합니다. 파일에는 가게별 매출, 수수료, 최종 정산 금액 등이 포함되어야 합니다.
- 파일 저장소 관리: 생성된 파일을 AWS S3 또는 로컬 파일 시스템에 저장하는 기능을 구현합니다.
- 파일 다운로드 API: 저장된 정산 결과 파일을 다운로드할 수 있는 REST API를 제공합니다. 가게별로 파일을 조회하고 다운로드할 수 있어야 합니다.
요구사항에 맞춰 만들어야 하는 REST API 는 두개입니다.
- 가게별 일매출 정산 정보를 CSV 로 다운받는 API
- 가게별 월매출 정산 정보를 CSV 로 다운받는 API
현재 일매출, 월매출 정산 정보는 각각 다른 테이블에 저장되어 있으므로 DB 에서 조회해 CSV 로 다운받을 수 있는 형식으로만 보내주면 될거 같네요. (크게 어려운 부분은 없으니 가볍게 보셔도 괜찮을거 같습니다.)
즉, 배치를 실행해 DB 에 저장된 정산 정보를 REST API 를 통해 다운로드 할 수 있어야 합니다. 현재 프로젝트는 멀티 모듈로 구성되어 있고 REST API 와 관련된 로직들은 settlement-system-api 모듈에 만들어보겠습니다.
✅ SettlementRepository
package com.repository;
import com.entity.Settlement;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.Optional;
@Repository
public interface SettlementRepository extends JpaRepository<Settlement, Long> {
@Query("SELECT s FROM Settlement s WHERE s.shopId = :shopId AND FUNCTION('DATE', s.settlementDateTime) = :date")
Optional<Settlement> findByShopIdAndSettlementDate(@Param("shopId") Long shopId, @Param("date") LocalDate date);
}
Settlement 테이블은 가게별 일매출 정산 정보가 포함되어 있습니다. 따라서 파라미터로 shopId 와 날짜를 받아와 해당하는 Settlement 를 반환하는 메서드를 만들었습니다.
✅ MonthlySettlementRepository
package com.repository;
import com.entity.MonthlySettlement;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import org.springframework.data.repository.query.Param;
import org.springframework.stereotype.Repository;
import java.time.LocalDate;
import java.util.Optional;
@Repository
public interface MonthlySettlementRepository extends JpaRepository<MonthlySettlement, Long> {
@Query("SELECT m FROM MonthlySettlement m WHERE m.shopId = :shopId AND FUNCTION('MONTH', m.settlementDateTime) = FUNCTION('MONTH', :date) AND FUNCTION('YEAR', m.settlementDateTime) = FUNCTION('YEAR', :date)")
Optional<MonthlySettlement> findByShopIdAndSettlementMonthly(@Param("shopId") Long shopId, @Param("date") LocalDate date);
}
MonthlySettlement 테이블은 가게별 월매출 정산 정보가 포함되어 있습니다. 따라서 파라미터로 shopId 와 날짜를 받아와 해당하는 MonthlySettlement 를 반환하는 메서드를 만들었습니다.
✅ SettlementService
package com.service;
import com.entity.MonthlySettlement;
import com.entity.Settlement;
import com.repository.MonthlySettlementRepository;
import com.repository.SettlementRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import java.time.LocalDate;
import java.util.List;
import java.util.Optional;
@Service
@RequiredArgsConstructor
public class SettlementService {
private final SettlementRepository settlementRepository;
private final MonthlySettlementRepository monthlySettlementRepository;
public Settlement findByIdAndDate(Long shopId, LocalDate localDate){
Optional<Settlement> byShopIdAndSettlementDate = settlementRepository.findByShopIdAndSettlementDate(shopId, localDate);
return byShopIdAndSettlementDate.get();
}
public MonthlySettlement findByIdAndMonth(Long shopId, LocalDate localDate) {
Optional<MonthlySettlement> byShopIdAndSettlementMonthly = monthlySettlementRepository.findByShopIdAndSettlementMonthly(shopId, localDate);
return byShopIdAndSettlementMonthly.get();
}
}
Repository 에서 구현한 각각의 메서드를 이용해 조회된 정산 정보를 반환합니다.
✅ CsvHelper
package com.utils;
import com.entity.MonthlySettlement;
import com.entity.Settlement;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVPrinter;
import java.io.IOException;
import java.io.Writer;
public class CsvHelper {
public static void writeSettlementsToCsv(Writer writer, Settlement settlement) {
try (CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT.withHeader(
"ShopName", "SettlementDateTime", "TotalSales", "TotalRefunds", "NetSales"))) {
csvPrinter.printRecord(
settlement.getShopName(),
settlement.getSettlementDateTime(),
settlement.getTotalSales(),
settlement.getTotalRefunds(),
settlement.getNetSales()
);
} catch (IOException e) {
e.printStackTrace();
// 로그 처리 또는 예외 처리 로직 추가 가능
}
}
public static void writeMonthlySettlementsToCsv(Writer writer, MonthlySettlement settlement) {
try (CSVPrinter csvPrinter = new CSVPrinter(writer, CSVFormat.DEFAULT.withHeader(
"ShopName", "SettlementDateTime", "TotalSales", "TotalRefunds", "NetSales"))) {
csvPrinter.printRecord(
settlement.getShopName(),
settlement.getSettlementDateTime(),
settlement.getTotalSales(),
settlement.getTotalRefunds(),
settlement.getNetSales()
);
} catch (IOException e) {
e.printStackTrace();
// 로그 처리 또는 예외 처리 로직 추가 가능
}
}
}
객체를 CSV 형식으로 변환을 도와주는 클래스 입니다. 각각 Settlement, MonthlySettlement 정보를 원하는 CSV 형식으로 만들면 됩니다.
(CSV 변환 라이브러리를 사용하기 위해 build.gradle 을 아래와 같이 추가해야 합니다.)
implementation 'org.apache.commons:commons-csv:1.10.0'
✅ SettlementController
package com.controller;
import com.entity.MonthlySettlement;
import com.entity.Settlement;
import com.service.SettlementService;
import com.utils.CsvHelper;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.io.Writer;
import java.time.LocalDate;
@RestController
@RequiredArgsConstructor
@RequestMapping("/api/settlements")
public class SettlementController {
private final SettlementService settlementService;
@GetMapping("/daily/download/csv")
public void downloadDailySettlement(HttpServletResponse response, @RequestParam("shopId") Long shopId, @RequestParam("localDate") LocalDate localDate) throws IOException {
Settlement settlement = settlementService.findByIdAndDate(shopId, localDate);
response.setContentType("text/csv");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"settlements.csv\"");
try (Writer writer = response.getWriter()){
CsvHelper.writeSettlementsToCsv(writer, settlement);
}
}
@GetMapping("/monthly/download/csv")
public void downloadMonthlySettlement(HttpServletResponse response, @RequestParam("shopId") Long shopId, @RequestParam("localDate") LocalDate localDate) throws IOException {
response.setContentType("test/csv");
response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"settlements.csv\"");
MonthlySettlement monthlySettlement = settlementService.findByIdAndMonth(shopId, localDate);
try (Writer writer = response.getWriter()){
CsvHelper.writeMonthlySettlementsToCsv(writer, monthlySettlement);
}
}
}
downloadDailySettlement 와 downloadMonthlySettlement 는 각각 일별, 월별 정산 정보를 CSV 로 다운받기 위한 RESTAPI 입니다.
자! 이제 서버를 구동하고 CSV 파일이 잘 다운되는지 확인해보겠습니다.
http://localhost:8080/api/settlements/daily/download/csv?shopId=1&localDate=2024-09-01
다음과 같이 요청을 보내면 CSV 파일이 지정한 형식으로 잘 저장되는것을 확인할 수 있습니다.
지금까지 가게에 저장된 거래 정보를 이용해 정산 배치를 만들어 봤고, 이를 CSV 파일로 다운로드 할 수 있는 API 까지 만들어 봤습니다.
Spring Batch 를 이용해 간단한 정산 시스템 프로젝트를 만들데, 아직 효율적인 배치 프로그램을 만들기 위해선 공부해야할 부분이 많아 보이네요 ㅜ
(전체 코드는 아래 깃허브 주소를 참고해주세요.)
https://github.com/sinminseok/settlement-system
GitHub - sinminseok/settlement-system
Contribute to sinminseok/settlement-system development by creating an account on GitHub.
github.com