가끔 Jit 컴파일러 튜닝해야하는 경우 참고하는 내용
 - https://man.archlinux.org/man/java-openjdk11.1.en#Advanced_JIT_Compiler_Options

요즘 가장 관심이 있는 java쪽 기능/피쳐

모든 기존 프로덕트를 재 개발할수도 없고, 리액티브 스타일로 개발시 여러 허들과 트러블 슈팅도 있어서..

java virtual thread의 draft(2023/03/06 created)
https://openjdk.org/jeps/8303683

 

 

노이즈가 좀 있는 내용 같지만 추가 참고 글

 - https://news.hada.io/topic?id=9250

 

 

jjwt를 사용해서 보통 JWT 처리를 하는데 혹시 secretKey 기존에 대충 만들었다면(길이가 짧게) 0.10.0 버전부터 에러가 발생함

 - https://github.com/jwtk/jjwt/issues/334

https://github.com/jwtk/jjwt/blob/6b980553cfb4435ec14b7842af5b529735acbb2d/impl/src/main/java/io/jsonwebtoken/impl/DefaultJwtParser.java#L364

 

 

이에 간단히 사용하는 SecretKey 생성 소스

import io.jsonwebtoken.SignatureAlgorithm;
import io.jsonwebtoken.io.Encoders;
import io.jsonwebtoken.security.Keys;

import javax.crypto.SecretKey;

public class JwtSecretKeyMaker {

	/**
	 * JJWT secretKey를 생성할때 알고리즘에 맞는 length로 만듬
	 *  - https://github.com/jwtk/jjwt#jws-key-create
	 *  - jjwt 0.10.0 부터는 length가 작으면 에러가 발생함: https://github.com/jwtk/jjwt/issues/334
	 *
	 * @param args
	 */
	public static void main(String[] args) {

		//Creating Safe Keys(length: 256bit))
		SecretKey key = Keys.secretKeyFor(SignatureAlgorithm.HS256); // HS256: HMAC-SHA256
		String secretString = Encoders.BASE64.encode(key.getEncoded());

		System.out.println(secretString);

	}
}
  1. 사전 필요사항
    1. AWS CloudFront 설정
1. 메뉴
 - AWS 웹 콘솔 -> CloudFront -> 대상 CloudFront ID(도메인에 맵핑된) -> Behaviors -> 해당 Behaviors 선택 -> Edit

2.  Cache key and origin requests 영역 설정
 1) Cache policy and origin request policy (recommended) 선택
 2) Cache policy 를  CachingDisabled 로 설정
  - CloudFront를 동적 컨텐츠 전송 목적과 위치정보 헤더 값등 필요 헤더를 획득할 목적이기 때문에
  - 참고: https://aws.amazon.com/ko/cloudfront/dynamic-content/
  - 참고: https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/using-managed-cache-policies.html
 3) Origin request policy - optional 에 커스텀 policy추가해서 반영
  - 샘플) CloudFront에서 추가되는 헤더를 모두 선택한 경우

설정샘플(주의: 뒷단 was까지 Query Strings와 Cookies를 넘겨줘야하기 때문에 ALL로 설정)

  1. Java 샘플소스
/**
	 * 클라이언트 요청자의 IP를 가져옴(WAS 앞단에 로드밸런서 등이 존재할 수 있음을 감안한 메소드)
	 *  - AWS CloudFront -> ALB(LB) -> Nginx-> 톰캣을 사용하는 케이스를 감안한 메소드
	 *  - AWS CloudFront사용시 해당 헤더의 IP정보를 최우선으로 신뢰함(즉, IP를 얻어오는 순서에 의미가 있음)
	 *  - 주의: Spring webflux는 해당 메소드를 사용불가(대신 ServerWebExchange exchange.getRequest().getRemoteAddress().getAddress().getHostAddress() 같은 메소드를 사용해야함)
	 *
	 * @param request HttpServletRequest
	 * @return
	 */
	public static String getClientIp(HttpServletRequest request) {

		//참고 https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/using-cloudfront-headers.html#cloudfront-headers-viewer-location
		//참고 https://docs.aws.amazon.com/ko_kr/AmazonCloudFront/latest/DeveloperGuide/WhatsNew.html
		final String awsCfAddress = request.getHeader("CloudFront-Viewer-Address"); //ex) 123.45.67.89:46532
		if (StringUtils.isNotBlank(awsCfAddress)) {
			log.trace("\nIP획득 출처: AWS CloudFront-Viewer-Address\n\t요청 http header => {}", HttpRequestUtil.getRequestHeaderToMap(request));
			return awsCfAddress.split(":")[0].trim(); //구분자 :
		}

		//프록시 레이어를 감안한 XFF값
		//TODO: XFF는 spoofed(속일 수 있음)가 가능하기 때문에 오른쪽부터 IP를 가져오도록 수정해야함. 이때 신뢰하는 프록시IP들은 제외하는 로직 작업도 필요 https://news.hada.io/topic?id=6098
		//https://en.wikipedia.org/wiki/X-Forwarded-For
		//https://blog.lael.be/post/8989
		final String xForwardedFor = request.getHeader("X-Forwarded-For"); //ex) 123.456.78.99,15.162.1.11
		if (StringUtils.isNotBlank(xForwardedFor)) {
			log.trace("\nIP획득 출처: X-Forwarded-For\n\t요청 http header => {}", HttpRequestUtil.getRequestHeaderToMap(request));
			return xForwardedFor.split(",")[0].trim(); //구분자 ,
		}

		final String realIp = request.getHeader("X-Real-IP"); //일반적으로는 인프라(프록시) 설정 들이 잘되어 있다면 다른 언어 및 프레임웤에서는 보안문제 때문에 Real IP가 XFF보다 우선순위 높음
		if (StringUtils.isNotBlank(realIp)) {
			log.trace("\nIP획득 출처: X-Real-IP\n\t요청 http header => {}", HttpRequestUtil.getRequestHeaderToMap(request));
			return realIp;
		}

		log.trace("\nIP획득 출처: request.getRemoteAddr\n\t요청 http header => {}", HttpRequestUtil.getRequestHeaderToMap(request));
		return request.getRemoteAddr();
	}

자동화를 위해서 애플 앱스토어의 여러 기능을 개발할 수 있습니다.(애플에서 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. 기타 참고 링크들

아직 Jsp를 쓰는 프로젝트에서 Maven multi module 프로젝트를 셋팅했는데, JSP를 못 찾는 문제가 발생했습니다.

IntelliJ Working directory에 $MODULE_WORKING_DIR$ 를 셋팅해주면 문제가 해결됩니다.
- 참고 링크

샘플

+ Recent posts