자동화를 위해서 애플 앱스토어의 여러 기능을 개발할 수 있습니다.(애플에서 API일부를 제공해줘서)

 

그 중 앱의 IAP 리스트를 조회하는 API샘플입니다.

 - 주의: API는 호출양 제한이 걸려있습니다.(링크 참고)

 

List All In-App Purchases for an App

  - https://developer.apple.com/documentation/appstoreconnectapi/list_all_in-app_purchases_for_an_app

 

java 샘플 소스

 - git 주소: https://github.com/oshnew/spring-boot-ver2-study/commit/b04e4293e983bf7425be17a97aaac6c80f70a26b

import okhttp3.HttpUrl;
import okhttp3.OkHttpClient;
import okhttp3.Request;
import okhttp3.Response;
import sample.apple.AppStoreConnectApi.AppStoreConnectApiJWT;

import java.io.IOException;
import java.util.concurrent.TimeUnit;

/**
 * 특정 애플 앱의 IAP로 등록된 상품 리스트 조회
 *  - 참고: https://developer.apple.com/documentation/appstoreconnectapi/list_all_in-app_purchases_for_an_app
 *
 * @author
 */
public class ListInAppPurchasesForAnApp {

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

		final String issuer = "입력필요";
		final String keyIdentifier = "입력필요"; //전달받은 사설키의 키 ID
		final String privateKeyFilePath = "입력필요"; //사설키 경로(제품개발 권한 액세스). 애플 앱스토어 웹 콘솔에서 다운로드 받음. 키에 허용권한(ACCOUNT_HOLDER, ADMIN, APP_MANAGER 중 1가지) 필요

		int expireAfterMinutes = 20; //20분 넘으면 안됨

		AppStoreConnectApiJWT clzJWT = new AppStoreConnectApiJWT();
		final String jwt = clzJWT.makeApiAuthJwt(issuer, keyIdentifier, expireAfterMinutes, privateKeyFilePath); //인증용 JWT생성

		System.out.println("인증용 JWT: " + jwt);

		OkHttpClient client = new OkHttpClient.Builder().readTimeout(4, TimeUnit.SECONDS).connectTimeout(1, TimeUnit.SECONDS).writeTimeout(5, TimeUnit.SECONDS).build();

		final String id = "입력필요"; //애플 앱스토어 웹 콘솔에서 'Apple ID'로 표시되는 부분, 앱의 구분용ID로 추정됨
		String prefixUri = String.format("https://api.appstoreconnect.apple.com/v1/apps/%s/inAppPurchasesV2", id);

		HttpUrl.Builder httpBuilder = HttpUrl.get(prefixUri).newBuilder();
		httpBuilder.addQueryParameter("limit", "200"); //최대 200건까지 API에서 제약됨
		httpBuilder.addQueryParameter("filter[inAppPurchaseType]", "NON_CONSUMABLE"); //비 소모성만 조회하기 필터링

		final String fullUrl = httpBuilder.build().toString();
		System.out.println("fullrUrl: " + fullUrl);

		Request request = new Request.Builder().addHeader("Authorization", "Bearer " + jwt).url(fullUrl).build();
		Response response = client.newCall(request).execute();

		if (response.isSuccessful()) {
			String rtnMsg = response.body().string();
			System.out.println(String.format("성공. 응답 msg => '%s", rtnMsg));
		} else {
			System.out.println(String.format("응답에러(200이 아님). 응답 code:'%s'\n errMsg:'%s'", response.code(), response.body().string()));
		}

	}
}

 

import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.SignatureAlgorithm;
import org.apache.tomcat.util.codec.binary.Base64;

import java.io.BufferedReader;
import java.io.FileReader;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;

/**
 * 애플 App Store Connect API에서 사용할 인증용 JWT토큰을 생성
 *
 * @author
 */
public class AppStoreConnectApiJWT {

	/**
	 * 애플 App Store Connect API에서 사용할 인증용 JWT토큰을 생성하는 샘플 소스
	 * - 참고: https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests
	 *
	 * @param issuer Your issuer ID from the API Keys page in App Store Connect; for example, 57246542-96fe-1a63-e053-0824d011072a.
	 * @param keyIdentifier
	 * @param expireAfterMinutes
	 * @param privateKeyFilePath
	 * @return
	 */
	public String makeApiAuthJwt(String issuer, String keyIdentifier, int expireAfterMinutes, String privateKeyFilePath) {

		if (expireAfterMinutes > 20) {
			throw new IllegalArgumentException(
				"expireAfterMinutes는 20보다 작아야합니다. 참고: https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests");
		}

		final String strPrivateKey = readPrivateKeyBody(privateKeyFilePath);
		PrivateKey privateKeyObj = convertToPrivateKeyObj(strPrivateKey);

		Map<String, Object> header = new HashMap<>();
		header.put("alg", SignatureAlgorithm.ES256);
		header.put("kid", keyIdentifier);
		header.put("typ", "JWT");

		final Date now = new Date();

		//주의: 일반적으로 만료시간이 20분보다 크면 애플에서 거부함. https://developer.apple.com/documentation/appstoreconnectapi/generating_tokens_for_api_requests#3878467
		LocalDateTime expLdt = LocalDateTime.ofInstant(now.toInstant(), ZoneId.systemDefault()).plusMinutes(10);
		Date expiredAt = java.sql.Timestamp.valueOf(expLdt);

		// @formatter:off
		String jwt =  Jwts.builder()
		.setHeader(header)
		.setIssuer(issuer)
		.setAudience("appstoreconnect-v1") //애플 API정의서
		.setIssuedAt(now)  //발행 시간
		.setExpiration(expiredAt) //만료시간
		.signWith(SignatureAlgorithm.ES256, privateKeyObj)
		.compact();

		// @formatter:on

		return jwt;
	}

	private static PrivateKey convertToPrivateKeyObj(String strPrivateKey) {

		try {
			byte[] encodedKey = Base64.decodeBase64(strPrivateKey);
			return KeyFactory.getInstance("EC").generatePrivate(new PKCS8EncodedKeySpec(encodedKey));
		} catch (NoSuchAlgorithmException | InvalidKeySpecException e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}

	/**
	 * private key 내용을 얻어옴
	 *  -  -----BEGIN PRIVATE KEY----- 또는 -----END PRIVATE KEY----- 와 같은 가이드라인 줄은 제외하고 실제 사용하는 부분만 파일에서 가져옴
	 *
	 * @param filePath
	 * @return
	 */
	private String readPrivateKeyBody(String filePath) {

		try (BufferedReader br = new BufferedReader(new FileReader(filePath))) {

			String line;
			StringBuilder sb = new StringBuilder();
			while ((line = br.readLine()) != null) {
				if (line.contains("PRIVATE KEY")) { //guard line은 pass
					continue;
				}
				sb.append(line);
			}

			return sb.toString();

		} catch (Exception e) {
			throw new RuntimeException(e.getMessage(), e);
		}
	}
}

 

java 11 vs 17 성능 관련 자료 정리(작성 중)

 

 

참고 자료

  1. https://www.optaplanner.org/blog/2021/09/15/HowMuchFasterIsJava17.html

Java에서 Optional이라는 기능이 존재합니다.

그런데 제대로, 문제없이, 읽기 좋은 소스로 작성하려면 생각보다 조금 신경 써야할 부분들이 있습니다.

 - 실제 프로덕트 코드에서 개발자들이 실수하는 경우를 많이 봤음 << 중요, 실수가 잦다면 코드 퀄리티를 관리해야하는 관리자 입장에서는 프로젝트 컨벤션으로 사용 금지로 해야할 수도 있음

 

그리고 개인적으로는 fast-fail형태가 맞다고 생각하기에 파라미터 검사(데이터 검사)를 앞쪽에서 잘 해야한다고 생각합니다. 

이후 코드는 최대한 읽기 좋게 작성하고, 성능을 생각하면 불필요하게 컴퓨팅 파워를 쓰는 코드 자체를 없애는게 맞다고 생각합니다.(임베디드 개발했던 경험을 떠올려봐도.... 컴퓨터에게 불필요한 일 자체를 시키면 안됨)

 

 

검색해서 메모 목적으로 몇가지 참고 내용을 작성해둡니다.


Java Optional의 API Note를 보면 Optional을 개발한 Brian Goetz도 의도한 형태로 사용하지 않는 경우가 많아서인지 Note까지 작성해뒀습니다.(사실 언어 설계자가 의도한데로만 사용하도록 개발/설계해뒀으면 하는 아쉬움이..)

 

1. Java Optional API Note

2. 기타 참고 링크들

import org.apache.commons.lang3.StringUtils;
import org.joda.time.DateTime;
import org.joda.time.Period;
import org.joda.time.format.DateTimeFormat;
import org.joda.time.format.DateTimeFormatter;

import java.util.Locale;

/**
 * 월 달력 생성기
 *
 * @author
 */
public class Test {

	public static void main(String[] args) {

		final int addMonth = 2; //2개월 후 데이터 생성

		DateTimeFormatter fmtMonth = DateTimeFormat.forPattern("yyyyMM");
		DateTimeFormatter fmtDay = DateTimeFormat.forPattern("yyyyMMdd");

		//대상 월
		DateTime now = DateTime.now();
		String strTargetMonth = now.plus(Period.months(addMonth)).toString(fmtMonth);

		DateTime dt = DateTime.parse(strTargetMonth, fmtMonth);

		int startDay = dt.dayOfMonth().getMinimumValue();
		int endDay = dt.dayOfMonth().getMaximumValue();

		String strDay;
		String dayOfWeek;
		//boolean isHoliDay = false;
		for (int i = startDay; i <= endDay; i++) {
			strDay = strTargetMonth + StringUtils.leftPad(Integer.toString(i), 2, "0");
			dayOfWeek = DateTime.parse(strDay, fmtDay).dayOfWeek().getAsShortText(Locale.KOREA);

			System.out.println(String.format("%s | %s", strDay, dayOfWeek));

		}

	}
}

Java의 java.time.LocalDateTime관련 자주 사용하는 몇 가지를 메모 목적으로 정리합니다.

  • 예전 java 1.7까지는 joda를 많이 사용했고, Java기본 패키지에 포함된 이후에도 바쁘면 joda를 사용했습니다.
  • 가능하면 java 기본 패키지를 사용하는게 좋습니다.
  • 계속해서 내용은 추가할 예정입니다.

 

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.Date;

/**
 * Java의 java.time.LocalDateTime 관련 몇가지 사용 예 - 작성중
 * - joda를 많이 썻던 개발자들을 위해 샘플 작성
 *
 * @author
 */
public class LocalDateTimeSample {

	public static ZoneId pstZoneId = ZoneId.of("America/Los_Angeles"); //PST는 ZoneId가 'America/Los_Angeles' 임
	public static ZoneId pdtZoneId = ZoneId.of("GMT-07:00"); //PDT는 ZoneId가 'GMT-07:00' 임. 참고: https://savvytime.com/converter/pdt-to-kst-utc/aug-1-2021/3am

	public static DateTimeFormatter FMT_YMDHIS = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");

	/**
	 * Java의 java.time.LocalDateTime 관련 몇가지 샘플
	 *
	 * @param args
	 */
	public static void main(String[] args) {

		System.out.println("\n");

		final Date now = new Date();

		System.out.println(String.format("java.util.Date 타입의 현재 일시: %s\n\n", now));

		LocalDateTime ldt = LocalDateTime.ofInstant(now.toInstant(), ZoneId.systemDefault());
		System.out.println(String.format("java.util.Date를 java.time.LocalDateTime 로 변환:%s\n", ldt));

		//혹은 아래와 유사하하게 Date객체로 변환 가능
		//Instant instant = utcZonedDateTime.withZoneSameInstant(ZoneId.of("UTC")).toLocalDateTime().atZone(ZoneId.of("UTC")).toInstant();
		//java.util.Date.from(instant);

		final String pattern = "yyyy-MM-dd-HH:mm";
		System.out.println(String.format("LocalDateTime을 패턴 '%s'으로 포맷팅: %s\n", pattern, ldt.format(DateTimeFormatter.ofPattern(pattern))));

		System.out.println(String.format("1일 후는: %s", ldt.plusDays(1).format(FMT_YMDHIS)));
		System.out.println(String.format("3일전은: %s", ldt.minusDays(3).format(FMT_YMDHIS)));

		String ymd = "20220501";

		DateTimeFormatter fmtYmd = DateTimeFormatter.ofPattern("yyyyMMdd");
		LocalDate dateYmd = LocalDate.parse(ymd, fmtYmd);
		System.out.println(String.format("%s 문자열을 포맷팅 후 date객체 변환 결과: %s", ymd, dateYmd));

		LocalDate toDay = LocalDate.now();
		System.out.println(String.format("오늘은 '%s' 입니다.", toDay));
		System.out.println(String.format("이번달의 1일은 '%s' 입니다.", toDay.withDayOfMonth(1)));

		System.out.println("\n");
		ZoneId defaultZoneId = ZoneId.systemDefault();
		Date firstMonthDayDT = Date.from(toDay.atStartOfDay(defaultZoneId).toInstant());
		System.out.println(String.format("이번달의 1일의 java date타입은 '%s' 입니다.", firstMonthDayDT));

		System.out.println("\n");
		System.out.println("원본 LocalDateTime: " + ldt);
		System.out.println("\t초 미만 절삭: " + ldt.truncatedTo(ChronoUnit.SECONDS));
		System.out.println("\t분 미만 절삭: " + ldt.truncatedTo(ChronoUnit.MINUTES));
		System.out.println("\t시간 미만 절삭: " + ldt.truncatedTo(ChronoUnit.HOURS));
		System.out.println("\t일 미만 절삭: " + ldt.truncatedTo(ChronoUnit.DAYS));
	}
}

 

결과

코드 실행 결과

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. 목적/배경

 - 가끔 대용량 데이터 처리(예. 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프로그램
 - 2024-07-22 간단히 업데이트 진행

 

필요 라이브러리(기타 추가 import가 필요할 수도 있음)

<!-- https://mvnrepository.com/artifact/com.google.apis/google-api-services-storage -->
<dependency>
    <groupId>com.google.apis</groupId>
    <artifactId>google-api-services-storage</artifactId>
    <version>v1-rev20240706-2.0.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.google.api-client/google-api-client -->
<dependency>
    <groupId>com.google.api-client</groupId>
    <artifactId>google-api-client</artifactId>
    <version>2.6.0</version>
</dependency>

<!-- https://mvnrepository.com/artifact/com.google.http-client/google-http-client-jackson2 -->
<dependency>
    <groupId>com.google.http-client</groupId>
    <artifactId>google-http-client-jackson2</artifactId>
    <version>1.44.2</version>
</dependency>
import com.google.api.client.auth.oauth2.Credential;
import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
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.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 java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.time.LocalDateTime;
import java.util.Collections;
import java.util.List;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.io.FileUtils;
import org.apache.commons.lang3.StringUtils;

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

  //다운로드 저장 디렉토리 경로
  final static String saveDirStr = ".temp_google_earnings";

  //Google Cloud Platform에서 생성한 Service Account의 키 파일. 해당 키 파일의 유저(이메일주소) Google Play Console에 초대 후 권한 지급이 되어 있어야 함
  //일괄 보고서에 액세스하려면 '앱 정보 보기' 권한이 '전체'로 설정되어 있어야 합니다.
  //재무 보고서를 다운로드하려면 '재무 데이터 보기' 권한이 '전체'로 설정되어 있어야 합니다.
  //참고: https://support.google.com/googleplay/android-developer/answer/6135870#zippy=%2C%EC%88%98%EC%9D%B5%2C%ED%81%B4%EB%9D%BC%EC%9D%B4%EC%96%B8%ED%8A%B8-%EB%9D%BC%EC%9D%B4%EB%B8%8C%EB%9F%AC%EB%A6%AC-%EB%B0%8F-%EC%84%9C%EB%B9%84%EC%8A%A4-%EA%B3%84%EC%A0%95%EC%9D%84-%EC%82%AC%EC%9A%A9%ED%95%98%EC%97%AC-%EB%B3%B4%EA%B3%A0%EC%84%9C-%EB%8B%A4%EC%9A%B4%EB%A1%9C%EB%93%9C%ED%95%98%EA%B8%B0
  final static String SERVICE_ACCOUNT_KEY_FILE_PATH = "구글 Service Account 인증 파일 경로.json";

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

  //다운로드할 수익보고서 object name
  final static String DOWNLOAD_OBJECT_NAME = "earnings/earnings_202406_블라블라.zip"; //object name 샘플. earnings/earnings_202110_블라블라숫자-숫자.zip
  final static String applicationName = "test-app-0.0.1";

  //인증 스콥
  final static String GoogleCredential_SCOPE = StorageScopes.CLOUD_PLATFORM_READ_ONLY;


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

    Credential credential = authorizeWithServiceAccount(httpTransport, jsonFactory);
    Storage storage = new Storage.Builder(httpTransport, jsonFactory, credential).setApplicationName(applicationName).build();

    //테스트로 prefixName 기반으로 리스트 조회
    final String namePreifx = "earnings/earnings_2024"; //2024년 prefix로 전체 조회
    listObject(storage, BUCKET_NAME, namePreifx).forEach(storageObject -> {
      log.info("버킷의 {} prefix storageObject:{}", namePreifx, storageObject);
    });

    //버킷의 모든 object를 조회하는 경우: 오버헤드 주희
//    storage.objects().list(BUCKET_NAME).execute().getItems().forEach(storageObject -> {
//      log.info("storageObject:{}", storageObject);
//    });

    final Storage.Objects.Get downloartTargetObject = storage.objects().get(BUCKET_NAME, DOWNLOAD_OBJECT_NAME);

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

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

    String objectNameSplitArray[] = StringUtils.split(DOWNLOAD_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);
    downloartTargetObject.getMediaHttpDownloader().setDirectDownloadEnabled(true); //true 설정 필요
    downloartTargetObject.executeMediaAndDownloadTo(out);

    LocalDateTime end = LocalDateTime.now();
    log.info("\n\n==== End: {} ==== 소요시간: {}(second)", end, Duration.between(start, end).getSeconds());
  }

  /**
   * 서비스 account 인증
   *
   * @param httpTransport
   * @param jsonFactory
   * @return
   * @throws IOException
   */
  private static Credential authorizeWithServiceAccount(HttpTransport httpTransport, JsonFactory jsonFactory) throws IOException {

    final String privateKey = FileUtils.readFileToString(new File(SERVICE_ACCOUNT_KEY_FILE_PATH), "UTF-8");

    InputStream inputStream = new ByteArrayInputStream(privateKey.getBytes(StandardCharsets.UTF_8));

    GoogleCredential credential = GoogleCredential.fromStream(inputStream, httpTransport, jsonFactory);
    credential = credential.createScoped(Collections.singleton(GoogleCredential_SCOPE));

    return credential;
  }

  /**
   * 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); //name prefix로 필터링해서 가져오도록 설정 -> 속도 개선됨
    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);
  }

}

+ Recent posts