필요한 일이 생겨서 간단히 프로토타이핑한 구글 수익보고서 다운로드하는 java프로그램

 

필요 라이브러리(아래 google-api-services-storage외에도 import된 라이브러리 추가 필요)

<dependency>
    <groupId>com.google.apis</groupId>
    <artifactId>google-api-services-storage</artifactId>
    <version>v1-rev171-1.25.0</version>
</dependency>

import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.extensions.java6.auth.oauth2.AuthorizationCodeInstalledApp;
import com.google.api.client.extensions.jetty.auth.oauth2.LocalServerReceiver;
import com.google.api.client.googleapis.auth.oauth2.GoogleAuthorizationCodeFlow;
import com.google.api.client.googleapis.auth.oauth2.GoogleClientSecrets;
import com.google.api.client.googleapis.javanet.GoogleNetHttpTransport;
import com.google.api.client.http.HttpTransport;
import com.google.api.client.json.JsonFactory;
import com.google.api.client.json.jackson2.JacksonFactory;
import com.google.api.client.util.store.DataStoreFactory;
import com.google.api.services.storage.Storage;
import com.google.api.services.storage.StorageScopes;
import com.google.api.services.storage.model.Objects;
import com.google.api.services.storage.model.StorageObject;
import com.google.common.collect.Iterables;
import com.google.common.collect.Lists;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;

import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.StringReader;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;

/**
 * 구글 재무보고서 중에서 '수익'보고서를 다운로드하는 샘플 프로그램
 *  - 참고: https://support.google.com/googleplay/android-developer/answer/6135870#zippy=%2C%EC%88%98%EC%9D%B5
 *  - 2021년 11월 기준 구글스토어에서 제공하는 재무보고서는 3가지가 존재
 *    1. 예상 판매실적
 *    2. 수익
 *    3. 대한민국 Play 잔액 차감
 *
 *  - 추가 참고사항
 *  1) 수익 보고서를 사용하여 판매 대금과 거래 내역를 파악할 수 있습니다.
 *     보고서의 각 행은 거래 유형(예: 고객에게 대금을 청구하거나 Google에 수수료를 지급하는 시기)과 함께 원래 금액 및 변환된 금액을 나타냅니다.
 *  2) 수익 보고서에는 이전 달에 발생한 인보이스가 포함됩니다. 최소 지급액에 도달하면 인보이스를 받게 됩니다. 수익 보고서를 사용할 수 있게 되면 몇 주 후에판매 대금을 수령할 수 있습니다.
 *  3) 수익 보고서는 한 달에 한 번 생성되며 일반적으로 다음 달 5일에 제공됩니다.
 *  	경우에 따라 Google에서 계산 오류를 바로잡기 위해 수익을 조정할 수 있습니다.
 *  	이 경우 Google에서 문제에 관해 알려 드리며 개발자 기록용으로 조정된 거래만 포함되어 있는 추가 수입 파일을 생성합니다.
 *	4) Google은 유럽 경제 지역(EEA)의 사용자에게 판매되는 상품의 등록된 판매자이므로, 영향을 받는 국가에서 이루어진 판매가 주문당 한 줄씩 표시됩니다.
 *		또한 거래 유형은 '청구'로 표시됩니다. 다른 국가에서 이루어진 판매에는 'Google 수수료' 거래 유형도 포함됩니다.
 * 	5) 수익 보고서에는 지불 거절이 포함되지 않습니다
 *
 * @author eomsh
 */
@Slf4j
public class GetEarningsReport {

	final static String encStoredCredential = "입력필요";
	final static String encClientSecretJson = "입력필요";

	//다운로드 저장 디렉토리 경로
	final static String saveDirStr = "c:\\temp\\google_earnigns";

	//버킷
	final static String BUCKET_NAME = "pubsite_prod_rev_블라블라"; //버킷명(Google Play Console -> 보고서 다운로드 -> 재무 -> Cloud Storage URI복사를 통해서 확인 가능)

	//수익보고서 object name
	final static String OBJECT_NAME = "earnings/earnings_블라블라.zip"; //object name 샘플. earnings/earnings_202110_블라블라숫자-숫자.zip

	public static void main(String[] args) throws Exception {

		LocalDateTime start = LocalDateTime.now();
		log.info("\n\n==== Start: {} ====", start);

		HttpTransport httpTransport = GoogleNetHttpTransport.newTrustedTransport();
		JsonFactory jsonFactory = JacksonFactory.getDefaultInstance();

		//@formatter:off
			Credential credential = authorize(
				httpTransport,
				jsonFactory,
				new CustomByteDataStoreFactory(GoogleAuthUtils.getDecStoredCredential(encStoredCredential)),
				GoogleClientSecrets.load(jsonFactory,
				new StringReader(GoogleAuthUtils.getDecClientSecretsJson(encClientSecretJson)))
			);

		//@formatter:on

		final String surfixAppVer = ".0.0.1"; //ApplicationName을 관리할때 버전을 추가하여 관리하기 위함.
		final String applicationName = "블라블라" + surfixAppVer;

		Storage storage = new Storage.Builder(httpTransport, jsonFactory, credential).setApplicationName(applicationName).build();
		Storage.Objects.Get getObject = storage.objects().get(BUCKET_NAME, OBJECT_NAME);

		final StorageObject storageObject = getObject.execute();
		log.info("storageObject 정보 ==>\n\t{}", storageObject.toPrettyString());

		FileUtils.forceMkdir(new File(saveDirStr)); //디렉토리 생성
		log.info("저장 디렉토리:{}", saveDirStr);

		String objectNameSplitArray[] = StringUtils.split(OBJECT_NAME, "/");
		String saveFileFullPathStr = saveDirStr + File.separator + objectNameSplitArray[1];

		log.info("저장 파일 full path:{}", saveFileFullPathStr);

		final File saveFile = new File(saveFileFullPathStr);
		FileUtils.deleteQuietly(saveFile); //기존 다운로드된 파일이 존재할 수 있어서 선 삭제 진행

		FileOutputStream out = new FileOutputStream(saveFile);
		getObject.getMediaHttpDownloader().setDirectDownloadEnabled(true); //true 설정 필요
		getObject.executeMediaAndDownloadTo(out);

		LocalDateTime end = LocalDateTime.now();
		log.info("\n\n==== End: {} ==== 소요시간: {}(second)", end, Duration.between(start, end).getSeconds());
	}
    
    /**
	 * Storage의 object들 중에서, name prefix로 필터링하여 가져옴
	 *
	 * @param storage
	 * @param bucketName
	 * @param namePrefix
	 * @return
	 * @throws IOException
	 */
	public static Iterable<StorageObject> listObject(Storage storage, String bucketName, String namePrefix) throws IOException {

		List<List<StorageObject>> pagedList = Lists.newArrayList();
		Storage.Objects.List listObjects = storage.objects().list(bucketName).setPrefix(namePrefix);
		Objects objects;
		do {
			objects = listObjects.execute();
			List<StorageObject> items = objects.getItems();
			if (items != null) {
				pagedList.add(objects.getItems());
			}
			listObjects.setPageToken(objects.getNextPageToken());
		} while (objects.getNextPageToken() != null);

		return Iterables.concat(pagedList);
	}


	/**
	 * 구글 인증
	 *
	 * @param httpTransport
	 * @param jsonFactory
	 * @param dataStoreFactory
	 * @param clientSecrets
	 * @return
	 * @throws Exception
	 */
	public static Credential authorize(
		//@formatter:off
		HttpTransport httpTransport,
		JsonFactory jsonFactory,
		DataStoreFactory dataStoreFactory,
		GoogleClientSecrets clientSecrets
		//@formatter:on
	) throws Exception {

		if (clientSecrets.getDetails().getClientId() == null || clientSecrets.getDetails().getClientSecret() == null) {
			throw new Exception("client_secrets not well formed.");
		}

		//@formatter:off
		GoogleAuthorizationCodeFlow flow = new GoogleAuthorizationCodeFlow
			.Builder(
				httpTransport,
				jsonFactory,
				clientSecrets,
				Collections.singleton(StorageScopes.DEVSTORAGE_FULL_CONTROL)
			).setDataStoreFactory(dataStoreFactory)
				.build();
		//@formatter:on

		return new AuthorizationCodeInstalledApp(flow, new LocalServerReceiver()).authorize("user");
	}
}

구글에서는 개발자에게 재무보고서를 제공하고 있습니다.

 

'예상판매실적 보고서', '수익 보고서', '대한민국 Play 잔액 차감 보고서'를 제공하는데, 이 중 실제 회계처리는 수익 보고서를 이용해야합니다.

 

그런데 수익 보고서 CSV파일을 분석해보니 조금 귀찮은 문제(타임존 처리, 필드 포맷 등)가 있습니다.

이에 간단히 프로그램을 만들었습니다.

 

프로그램은 한국 개발자에게 익숙한 java로 만들었습니다.

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;
import org.apache.commons.lang3.StringUtils;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.time.Duration;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.Locale;
import java.util.Map;

/**
 * 구글 재무보고서 중에서 '수익'보고서 CSV파일을 읽어서 파싱
 *  - 구글 '재무보고서 수익'에 대한 설명: https://support.google.com/googleplay/android-developer/answer/6135870#zippy=%2C%EC%88%98%EC%9D%B5
 *  - apache commons-csv를 이용해서 CSV파일을 파싱( https://mvnrepository.com/artifact/org.apache.commons/commons-csv/1.9.0 )
 *
 * @author eomsh
 */
@Slf4j
public class EarningsReportFileParser {

	private static String targetFilePath = "파싱할 CSV파일 경로"; //파싱할 CSV파일

	private static DateTimeFormatter parseFMT = DateTimeFormatter.ofPattern("MMM d, yyyy hh:mm:ss a z", Locale.ENGLISH); //CSV 일시 파싱을 위한 포맷(예. Sep 1, 2021 12:00:20 AM PST)

	//private static ZoneId kstZoneId = ZoneId.of("Asia/Seoul");
	private static ZoneId pstZoneId = ZoneId.of("America/Los_Angeles"); //PST는 ZoneId가 'America/Los_Angeles' 임
	private static ZoneId pdtZoneId = ZoneId.of("GMT-07:00"); //PDT는 ZoneId가 'GMT-07:00' 임.

	private static Map<String, Integer> csvHeadMap = null; //CSV 헤더 맵

	/**
	 * 구글 개발자콘솔에서 다운로드한 재무보고서-수익 CSV파일을 파싱하는 프로그램
	 *
	 * @param args
	 * @throws Exception
	 */
	public static void main(String[] args) throws Exception {

		LocalDateTime start = LocalDateTime.now();
		log.info("\n\n==== Start: {} ====", start);

		List<CSVRecord> records = getCsvRecords(new File(targetFilePath)); //파싱하여 데이터 records를 얻음

		log.info("CSV 데이터 records 수: {}", records.size());
		//log.debug("records 0번째 데이터:{}", records.get(0));

		for (CSVRecord record : records) {

			//Transaction 일시를 유닉스타임스탬프로 변환(CSV문서의 데이터는 PDT, PST 등의 일치하지 않는 타임존 정보로 되어 있기에 내부 처리의 용이함을 위해 유닉스타임스탬프로 변환
			getUnixTs(record.get("Transaction Date"), record.get("Transaction Time"));
		}

		LocalDateTime end = LocalDateTime.now();
		log.info("\n\n==== End: {} ==== 소요시간: {}(second)", end, Duration.between(start, end).getSeconds()); //로컬 PC에서 50만건 대략 10초 소요됨

	}

	/**
	 * 수익 CSV파일을 파싱하여 List데이를 얻음
	 *
	 * @param targetFile
	 * @return
	 * @throws IOException
	 */
	private static List<CSVRecord> getCsvRecords(File targetFile) throws IOException {

		//구글 개발자콘솔의 재무보고서 기능을 통해서 내려받은 '수익.csv파일'

		int sampleDataRow = 0; //샘플 데이터 row번호
		try (BufferedReader bufferedReader = new BufferedReader(new FileReader(targetFile))) {

			
            //구글 수익보고서를 분석하여 필요에 맞게 적절하게 CSV포맷 지정
			//@formatter:off
			CSVFormat csvFormat = CSVFormat.EXCEL.builder() //기본 엑셀타입 포맷
				.setHeader().setSkipHeaderRecord(true) //헤더 포함&skip
				.setQuote('"') //쌍따옴표 escape처리
				.setNullString("") //empty는 null처리
				.build();
			//@formatter:on
			CSVParser parser = csvFormat.parse(bufferedReader); //파서 처리
            
			//CSVParser parser = CSVFormat.EXCEL.withFirstRecordAsHeader().withQuote('"').parse(bufferedReader); //엑셀타입 & 쌍따옴표 escape처리
			List<CSVRecord> records = parser.getRecords();

			csvHeadMap = parser.getHeaderMap();

			// @formatter:off
			log.debug("\n"
					+ "CSV 데이터 records 수: {}\n"
					+ "CSV 헤더 정보\n\t"
						+ "CSV헤더 필드수: {}\n\t"
						+ "헤더 필드리스트: {}\n"
					+ "{}번째 row 데이터 정보\n\t"
						+ "데이터 필드수: {}\n\t"
						+ "데이터: {}\n",
						records.size(),
						parser.getHeaderMap().size(),
						parser.getHeaderMap(),
						sampleDataRow,
						records.get(sampleDataRow).size(),
						records.get(sampleDataRow)
			);

			// @formatter:on

			return records;
		}
	}

	/**
	 * transaction 일시 정보를 유닉스타임스탬프로 변환하여 리턴
	 *
	 * @param transactionDate CSV의 Transaction Date 필드의 값
	 * @param transactionTime CSV의 Transaction Time 필드의 값
	 * @return
	 */
	private static long getUnixTs(String transactionDate, String transactionTime) {

		String timeField = transactionTime;

		String timeArray[] = StringUtils.split(transactionTime, ":");
		if (timeArray[0].length() != 2) { //1:01:30 AM PDT 와 같이 시간필드에 0이 누락된 경우는 0을 붙여줘서 파싱 포맷에 맞게 변환
			timeField = "0" + timeField;
		}

		final String targetStr = transactionDate + " " + timeField; // 예) Sep 1, 2021 12:00:20 AM PDT

		if (StringUtils.contains(transactionTime, "PST")) {

			LocalDateTime targetDT = LocalDateTime.parse(targetStr, parseFMT); //파싱
			ZonedDateTime pstZDT = targetDT.atZone(pstZoneId);
			LocalDateTime pstDT = pstZDT.withZoneSameInstant(pstZoneId).toLocalDateTime();

			return pstDT.toEpochSecond(pstZoneId.getRules().getOffset(pstDT));

		} else if (StringUtils.contains(transactionTime, "PDT")) {

			LocalDateTime targetDT = LocalDateTime.parse(targetStr, parseFMT); //파싱
			ZonedDateTime pdtZDT = targetDT.atZone(pdtZoneId);
			LocalDateTime pdtDT = pdtZDT.withZoneSameInstant(pdtZoneId).toLocalDateTime();

			return pdtDT.toEpochSecond(pdtZoneId.getRules().getOffset(pdtDT));
		}

		throw new IllegalStateException(String.format("Not supported timezone data. transactionDate:'%s' | transactionTime:'%s'", transactionDate, transactionTime));
	}
}

java 8버전 이상 필요


import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.ZonedDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Locale;

/**
 * 타임존 변환 샘플 소스
 *
 * @author
 */
public class TimeZoneConvert {

	public static void main(String[] args) {
		pstData();
	}

	/**
	 * PST로 변환 샘플
	 *  - 참고: mysql 기준 쿼리 샘플 SELECT CONVERT_TZ( NOW(), 'UTC', 'Asia/Seoul') AS kst, CONVERT_TZ(NOW(), 'UTC', 'America/Los_Angeles') AS PST
	 */
	public static void pstData() {

		DateTimeFormatter strFMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); //문자 출력용 포맷

		//PST(Pacific Standard Time). 태평양표준시로 UTC-8. PST는 흔히 LA타임으로 불리며, ZoneID는 'America/Los_Angeles' 임
		//PST와 KST는 17시간 차이가 남(PST가 17시간 늦음). KST->PST는 17시간 빼면됨
		//String targetStrPST = "Mar 1, 2021 11:42:23 PM PST"; //KST 기준으로 2021-03-02 16:42:23
		String targetStrPST = "Mar 4, 2021 05:32:33 PM PST"; //KST 기준으로 2021-03-02 16:42:23

		DateTimeFormatter targetFMT = DateTimeFormatter.ofPattern("MMM d, yyyy hh:mm:ss a z", Locale.ENGLISH); //대상 문자 PST의 포맷
		LocalDateTime targetPstDT = LocalDateTime.parse(targetStrPST, targetFMT);

		ZonedDateTime pstZDT = targetPstDT.atZone(ZoneId.of("America/Los_Angeles")); //PST는 ZoneId가 'America/Los_Angeles' 임
		System.out.println("PST ymdt=> " + pstZDT.toLocalDateTime().format(strFMT));

		ZoneId kstZoneId = ZoneId.of("Asia/Seoul");
		LocalDateTime kstDT = pstZDT.withZoneSameInstant(kstZoneId).toLocalDateTime(); //KST로 변환
		String kstStr = kstDT.format(strFMT);

		System.out.println("KST ymdt => " + kstStr);
		System.out.println("KST ymdt toEpochSecond(유닉스 타임스탬프) => " + kstDT.toEpochSecond(kstZoneId.getRules().getOffset(kstDT)));

	}
    
    /**
	 * PDT로 변환 샘플
	 */
	public static void pdtData() {

		final ZoneId pdtZoneId = ZoneId.of("GMT-07:00"); //PDT는 GMT-07:00
		final DateTimeFormatter strFMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"); //문자 출력용 포맷

		//참고링크: https://savvytime.com/converter/pdt-to-kst-utc/aug-1-2021/3am
		String targetStrPDT = "Aug 1, 2021 12:03:00 AM PDT"; //KST 기준으로 2021-08-01 16:03:00이고, UTC기준으로는 2021-08-01 07:03:00

		DateTimeFormatter targetFMT = DateTimeFormatter.ofPattern("MMM d, yyyy hh:mm:ss a z", Locale.ENGLISH); //대상 문자 포맷
		LocalDateTime targetPdtDT = LocalDateTime.parse(targetStrPDT, targetFMT); //PDT의 LocalDateTime 객체가 생성됨

		// @formatter:off
		System.out.println(
			String.format(
				"PDT 테스트 문자열: %s\n"
				+ "PDT LocalDateTime: %s\n"
				+ "PDT EpochSecond: %s\n"
				+ "PDT -> UTC LocalDateTime: %s\n"
				+ "PDT -> KST LocalDateTime: %s",
				targetStrPDT,
				targetPdtDT.format(strFMT),
				targetPdtDT.atZone(pdtZoneId).toEpochSecond(),
				targetPdtDT.atZone(pdtZoneId).withZoneSameInstant(ZoneId.of("UTC")).toLocalDateTime().format(strFMT),
				targetPdtDT.atZone(pdtZoneId).withZoneSameInstant(ZoneId.of("Asia/Seoul")).toLocalDateTime().format(strFMT)
			)
		);

	}
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.sql.Connection;
import java.sql.DriverManager;
import java.sql.PreparedStatement;
import java.sql.SQLException;
import java.util.List;

/**
 * CSV파일을 읽어서 파싱하여 DB(Mysql)에 저장하는 프로그램 샘플
 *  - apache commons-csv를 이용해서 CSV파일을 파싱(참고: https://mvnrepository.com/artifact/org.apache.commons/commons-csv/1.9.0 )
 *
 * @author
 */
@Slf4j
public class CsvFileParseSaveToDB {

	private static String targetFilePath = "CSV파일 전체 경로";

	public static void main(String[] args) {

		//한글 깨지 방지를 위해서 characterEncoding=UTF-8 처리
		final String jdbcURL = "jdbc:mysql://DB주소:3306/논리DB명?characterEncoding=UTF-8";
		final String username = "DB ID";
		final String password = "DB 암호";

		final int batchSize = 2_000; //bulk insert시 커밋 갯수

		Connection connection = null;

		try {

			connection = DriverManager.getConnection(jdbcURL, username, password);
			connection.setAutoCommit(false);

			String sql = "insert  into `temp_test`(`name_1`,`name_2`) " + "VALUES (?, ??)";

			PreparedStatement statement = connection.prepareStatement(sql);

			int columnSize = 2; //CSV 데이터 필드 컬럼 갯수

			List<CSVRecord> records = getCsvRecords();
			for (int row = 0; row < records.size(); row++) {

				CSVRecord data = records.get(row);
				for (int fieldIndex = 0; fieldIndex < columnSize; fieldIndex++) {
					statement.setString(fieldIndex + 1, data.get(fieldIndex));
				}

				statement.addBatch();
				if (row % batchSize == 0) {
					statement.executeBatch();
					System.out.println(String.format("statement.executeBatch ing row ==> %s", row));
					connection.commit(); //DB서버 부하분산을 원하는 대용량 처리시 중간중간 커밋

					sleep(1); //부하 분산
				}

			}

			//남아있는 데이터 처리
			System.out.println("나머지 데이터도 executeBatch ");
			statement.executeBatch();
			connection.commit();

			connection.close();

		} catch (IOException ex) {
			System.err.println(ex);
		} catch (SQLException ex) {
			ex.printStackTrace();

			try {
				connection.rollback();
			} catch (SQLException e) {
				e.printStackTrace();
			}
		}

	}

	private static void sleep(long millis) {

		try {
			Thread.sleep(millis);
		} catch (InterruptedException e) {
			e.printStackTrace();
		}
	}

	private static List<CSVRecord> getCsvRecords() throws IOException {

		File targetFile = new File(targetFilePath);

		int sampleDataRow = 0; //샘플 데이터 row번호
		try (BufferedReader bufferedReader = new BufferedReader(new FileReader(targetFile))) {

			CSVParser parser = CSVFormat.EXCEL.withFirstRecordAsHeader().withQuote('"').parse(bufferedReader); //엑셀타입 & 쌍따옴표 escape처리
			List<CSVRecord> records = parser.getRecords();

			log.debug("\nCSV 헤더\n\t{}\n데이터 샘플\n\t{}\n", parser.getHeaderMap(), records.get(sampleDataRow));
			log.info("\n\t헤더 필드 갯수 :{}\n\t데이터 갯수 :{}\n\t{}번째 row의 데이터 필드 갯수:{}\n\n", parser.getHeaderMap().size(), records.size(), sampleDataRow,
				records.get(sampleDataRow).size());

			return records;
		}
	}

}

1. 필요사항(maven dependency추가)

<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-csv -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-csv</artifactId>
            <version>1.9.0</version>
        </dependency>

 

2. 간단한 java CSV파일 파싱 샘플 소스

import lombok.extern.slf4j.Slf4j;
import org.apache.commons.csv.CSVFormat;
import org.apache.commons.csv.CSVParser;
import org.apache.commons.csv.CSVRecord;

import java.io.BufferedReader;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.util.List;

/**
 * CSV파일을 읽어서 파싱
 *  - apache commons-csv를 이용해서 CSV파일을 파싱: https://mvnrepository.com/artifact/org.apache.commons/commons-csv/1.9.0
 *
 * @author 엄승하
 */
@Slf4j
public class CsvFileParser {

	public static void main(String[] args) throws IOException {

		//파싱할 CSV파일
		File targetFile = new File("CSV파일 경로");

		int sampleDataRow = 0; //샘플 데이터 row번호
		try (BufferedReader bufferedReader = new BufferedReader(new FileReader(targetFile))) {

			CSVParser parser = CSVFormat.EXCEL.withFirstRecordAsHeader().withQuote('"').parse(bufferedReader); //엑셀타입 & 쌍따옴표 escape처리
			List<CSVRecord> records = parser.getRecords();

			log.debug("\nCSV 헤더\n\t{}\n데이터 샘플\n\t{}", parser.getHeaderMap(), records.get(sampleDataRow));

			//샘플 데이터의 필드 데이터를 개행하여 출력
			//			for (String field : records.get(sampleDataRow)) {
			//				System.out.println(field);
			//			}

			log.info("헤더 필드 갯수 :{} | {}번째 row의 데이터 필드 갯수:{}", parser.getHeaderMap().size(), sampleDataRow, records.get(sampleDataRow).size());
			log.info("헤더: {} ", parser.getHeaderMap());

		}

	}
}

 

메모. 개인적으로 사용하는 logback file appender 설정

 

  • 서비스를 운영시 파일로 log를 남길때는 아래 사항을 꼭 고려해야함
    • 로그 파일 용량이 너무 커서 vi로 오픈이 힘든걸 막아야함
    • 로그파일을 삭제 없이 계속 저장하면 disk full로 서비스 장애가 발생하니 주의
    • 적당한 보관기간으로 압축해서 롤링

logback.properties 설정

#로그파일 저장 디렉토리(유저/logs 디렉토리 하위에 서비스코드별로 디렉토리 구분)
log.file.dir=/home/svc_user/logs/svc_cd

#로그 파일 명(prefix에 환경코드 추가, 1개 서버에 blue/green 배포하는 경우를 위해서 was port별로 파일명 구분)
log.file.name=real_svc_cd_${server.port}.log

log.file.full=${log.file.dir}/${log.file.name}

#백업
log.file.backup.fileNamePattern=${log.file.dir}/backup/${log.file.name}_%d{yyyy-MM-dd}_%i.zip

#로그 패턴
log.pattern=%d{yyyy-MM-dd HH:mm:ss} [${HOSTNAME}] [%-5level] %logger{35}:%L - %msg%n

 

logback-spring.xml 설정(참고 링크)

 

  • 로그파일 1개는 20M로 유지
  • 최대 50의 로그파일을 유지
  • 최대 1000MB
  • 압축해서 롤링
 <!-- 외부 설정파일을 사용 -->
    <property resource="properties/logback.properties"/>

    <appender name="FILE" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <File>${log.file.full}</File>
        <encoder>
            <pattern>${log.pattern}</pattern>
        </encoder>
        <rollingPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedRollingPolicy">
            <fileNamePattern>${log.file.backup.fileNamePattern}</fileNamePattern>
            <maxFileSize>20MB</maxFileSize>
            <maxHistory>50</maxHistory>
            <totalSizeCap>1000MB</totalSizeCap>
        </rollingPolicy>
    </appender>

 

간단한 Testcase로 확인용 작성

	@Test
	void 두글자국가코드_및_국가명_리스트조회() {

		String[] countries = Locale.getISOCountries();
		//Arrays.stream(countries).forEach(System.out::println); //2글자 국가코드 리스트 확인

		for (String country : countries) {

			Locale l = new Locale("en", country);
			System.out.println(String.format("2글자 국가코드(ISO 3166-1 alpha-2): %s | 영문 국가명: %s | 한글 국가명: %s ", country, l.getDisplayCountry(new Locale("en")),
				l.getDisplayCountry(new Locale("ko"))));

		}

		System.out.println("countries 갯수: " + countries.length);

		Assertions.assertNotNull(countries);
		Assertions.assertTrue(countries.length >= 200); //2021년기준 249개국이 존재.(200개 국가 이상을 assert 체크 기준으로 함)
	}

참고

 - site map 설명 링크

 

 

코드

 - 참고 링크

@RestController
public class TestController {

    @Autowired
    private RequestMappingHandlerMapping re;

    @GetMapping("/sitemap.xml")
    public String getSitemap() {
        Map<RequestMappingInfo, HandlerMethod> handlerMethods = re.getHandlerMethods();
        List<String> urls = new ArrayList<>();
        for (Entry<RequestMappingInfo, HandlerMethod> entry : handlerMethods.entrySet()) {
            urls.addAll((entry.getKey().getPatternsCondition().getPatterns()));
        }
        // Construct XML response from urls and return it
    }

}

+ Recent posts