AWS에서 Redis(Elastic cache)사용시 모니터링 등에 사용하는 커맨드
 - 예) 리얼 환경에서 사용하면 안되는 커맨드가 가끔 개발자 실수로 반영될 수 있음(예. keys)

-- redis에 keys 커맨드가 실행되고 있는지 모니터링하는 커맨드
 redis-cli -h 도메인혹은IP monitor | grep keys
  - 예) redis-cli -h 블라블라.apn2.cache.amazonaws.com monitor


-- redis에 접속되어있는 유니크한 클라이언 IP 리스트 확인 커맨드
redis-cli -h redis서버호스트 CLIENT LIST | awk '{print $2}' | sed s/:.*//g | sort -u

 


AWS ec2에 redis-cli설치하는 방법

# make 하기 위해서 gcc 다운
sudo yum install -y gcc

# redis-cli 설치 및 make
wget http://download.redis.io/redis-stable.tar.gz && tar xvzf redis-stable.tar.gz && cd redis-stable && make

#src/redis-cli 에서 사용 가능
예) ./redis-cli -h 'redis 호스트 도메인' -p '포트번호'

 

git 링크

 - https://github.com/oshnew/study-modern-java/blob/master/src/main/java/stream/StreamSorted.java

 

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.Arrays;
import java.util.Comparator;
import java.util.List;
import java.util.stream.Collectors;

/**
 * 내부 개발자 교육을 위한 간단한 java Stream 샘플 소스
 *  - 정렬
 *
 * @author
 */
public class StreamSorted {

	public static void main(String[] args) {

		//테스트 데이터 셋팅
		List<StatVO> statList = Arrays.asList(new StatVO("a", 3), new StatVO("b_a", 2), new StatVO("b_c", 1), new StatVO("d", 7));

		System.out.println("=== cnt 오름차순 정렬");
		statList.stream().sorted(Comparator.comparing(StatVO::getCnt)).forEach(System.out::println);

		System.out.println("\n\n");
		System.out.println("=== cnt 내림차순 정렬");
		statList.stream().sorted(Comparator.comparing(StatVO::getCnt).reversed()).forEach(System.out::println);

		System.out.println("\n\n");
		System.out.println("=== stat name 오름차순 후에 cnt 내림차순정렬"); //다수 필드 정렬
		statList.stream().sorted(Comparator.comparing(StatVO::getStat).thenComparing(StatVO::getCnt).reversed()).forEach(System.out::println);

		System.out.println("\n\n");
		List<StatVO> descList = statList.stream().sorted(Comparator.comparing(StatVO::getCnt)).collect(Collectors.toList());
		System.out.println("=== 오름차순 정렬하여 collect한 리스트");
		descList.stream().forEach(System.out::println);

	}

	@Data
	@AllArgsConstructor
	public static class StatVO {
		private String stat;
		private int cnt;
	}

}
  1. 인증
    1. 서비스 계정으로 인증: https://cloud.google.com/docs/authentication/production?hl=ko

1. 목적/배경

 - 가끔 대용량 데이터 처리(예. DB에 100만건 insert values로 bulk insert할때) 부하 분산을 위해 적절한 데이터 사이즈로 분할하여 처리할 필요가 있음

   (간단한 수학 계산인데 필요할때마다 다시 코딩하는게 번거롭고 헷갈려서 메모 목적으로 작성해둡니다.

 

2. 처리 방법

 - 적절한 데이터 사이즈만큼 잘라서 loop처리

 

 

샘플 소스

/**
	 * N개의 데이터 리스트를 자르는 간단한 소스
	 *   - 대용량 DB 저장 등의 기능 구현때 부하 분산 목적
	 *
	 * @param args
	 */
	public static void main(String[] args) {

		//테스트 데이터 셋팅
		int dataSize = 103;
		ArrayList<Integer> dataList = new ArrayList<>(dataSize);
		for (int i = 1; i <= dataSize; i++) {
			dataList.add(i);
		}

		//처리 시작
		final int dataCnt = dataList.size();
		final int PER_CNT = 10; //1회 처리 건수

		int fromIndex = 0;
		int toIndex = 0;

		boolean isNeedNextLoop = true;
		int loopCompleteCnt = 0; //loop처리 완료된 횟수
		do {

			fromIndex = loopCompleteCnt * PER_CNT;  //최초 시작은 0, 이후 PER_SAVE_CNT개 만큼씩 fromIndex는 증가
			toIndex = fromIndex + PER_CNT; //from index에서 1회 최대 건수만큼까지 toIndex는 증가
			if (toIndex >= dataCnt) { //증가한 toIndex가 데이터 전체 갯수보다 클때는 toIndex 값을 변경(마지막 loop에서는 데이터 element갯수가 1회 처리량보다 적거나 같음)
				toIndex = dataCnt;
				isNeedNextLoop = false;
			}		

			//비즈니스 로직 작성
			System.out.println(String.format("%s회 data subList: %s", loopCompleteCnt+1, dataList.subList(fromIndex, toIndex)));
            loopCompleteCnt++;

		} while (isNeedNextLoop);

	}

 

샘플소스 결과 

1회 data subList: [1, 2, 3, 4, 5, 6, 7, 8, 9, 10]
2회 data subList: [11, 12, 13, 14, 15, 16, 17, 18, 19, 20]
3회 data subList: [21, 22, 23, 24, 25, 26, 27, 28, 29, 30]
4회 data subList: [31, 32, 33, 34, 35, 36, 37, 38, 39, 40]
5회 data subList: [41, 42, 43, 44, 45, 46, 47, 48, 49, 50]
6회 data subList: [51, 52, 53, 54, 55, 56, 57, 58, 59, 60]
7회 data subList: [61, 62, 63, 64, 65, 66, 67, 68, 69, 70]
8회 data subList: [71, 72, 73, 74, 75, 76, 77, 78, 79, 80]
9회 data subList: [81, 82, 83, 84, 85, 86, 87, 88, 89, 90]
10회 data subList: [91, 92, 93, 94, 95, 96, 97, 98, 99, 100]
11회 data subList: [101, 102, 103]

필요한 일이 생겨서 간단히 프로토타이핑한 구글 수익보고서 다운로드하는 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));
	}
}

1. 목적

 - Mysql은 날짜/시간과 관련된 여러 데이터 타입을 제공하는데, 글로벌 서비스를 개발시 여러 시간대 정보가 포함되었을때를 위해서 DB데이터 타입에 대한 확인

 

2. 확인을 위한 테스트 및 결과

 

-- 테스트 데이터 저장용 테이블 생성
CREATE TABLE `timezone_test` (
  `datetime` DATETIME DEFAULT NULL COMMENT 'datetime필드',
  `date` DATE DEFAULT NULL COMMENT 'date필드',
  `timestamp` TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT 'timestamp필드',
  `int_unixTS` INT(10) UNSIGNED DEFAULT NULL COMMENT 'unix TS를 저장하기 위한 int(고정값 저장)'
) ENGINE=INNODB DEFAULT CHARSET=utf8mb4 COMMENT='타임존 테스트';

-- 테스트 데이터 insert
INSERT INTO timezone_test VALUES (NOW(), NOW(), NOW(), UNIX_TIMESTAMP());

-- 타임존 변경 전 저장된 데이터 확인
SELECT t.*, UNIX_TIMESTAMP(t.datetime) AS convertUNIX_TS FROM timezone_test AS t;

-- 현재 타임존 정보 확인
SELECT @@GLOBAL.time_zone, @@SESSION.time_zone, @@system_time_zone;

-- 세션의 타임존 변경(예. +1시로 변경)
SET time_zone='+01:00';

-- 타임존 정보 변경 후 현재시간 확인하여 변경되었는지 확인
SELECT NOW();

-- 타임존 변경 후 저장된 값의 내용 조회
-- datetime, date필드는 DB타임존이 변경되어도 기존 값이 변경안됨, timestamp는 DB내부적으로 UTC로 저장 후 DB타임존 설정정보를 이용해서 변환하여 보여줌 -> 즉, timestmap만 타임존이 변경되었을때 영향을 받음
-- 여러 시간대를 사용한다면, 가능하면 timestamp타입으로 데이터를 저장하는게 좋음
SELECT t.*, UNIX_TIMESTAMP(t.datetime) AS convertUNIX_TS FROM timezone_test AS t;

 

 

3. 주의

 - timestamp는 2038년까지만 표시되기에 주의(예. admin시스템 만들때 무한이라는 미래값 처리할때 2999년 과 같은 값으로 처리 불가)

 

 


샘플: 저장된 동일 데이터를 DB타임존 변경 전과 후 비교

 - 타임존 변경 전

DB타임존 변경전의 데이터 확인

 - 타임존 변경 후

DB 타임존을 +9에서 +1로 변경 후 확인 내용. 기존 대비 timestamp만 8시간 전으로 표시됨

 

 

참고

 - https://dev.mysql.com/doc/refman/5.7/en/datetime.html

 

MySQL :: MySQL 5.7 Reference Manual :: 11.2.2 The DATE, DATETIME, and TIMESTAMP Types

11.2.2 The DATE, DATETIME, and TIMESTAMP Types The DATE, DATETIME, and TIMESTAMP types are related. This section describes their characteristics, how they are similar, and how they differ. MySQL recognizes DATE, DATETIME, and TIMESTAMP values in several f

dev.mysql.com

 

참고
 - 타임존 변환 사이트 : https://savvytime.com/converter/pdt-to-kst-utc/aug-1-2021/3am

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)
			)
		);

	}

+ Recent posts