Skip to content

Commit

Permalink
feat: 트랜잭션 아이디 기반 가상 쓰레드 활용 다건 요청 기능 구현 (#15)
Browse files Browse the repository at this point in the history
* feat: 함수형 인터페이스를 활용한 http 호출 추상화 (#9)

* docs: update README.md

* refactor: 기존 requestProduct 로직 개선

* feat: 함수형 인터페이스를 활용한 http 호출 추상화

* feat: httpClient5 호출부 로직 각 클래스 단위로 분리

* chore: github action 불필요 JDK initialize 로직 삭제

* feat: requestSimpleAuthCertification() TransactionId 기반 간편인증 상품 호출 (#11)

* docs: update README.md

* chore: Verify 액션이 브랜치 분기 전략에 맞게 동작하도록 수정

* refactor: 상수 -> Constants 리팩토링

* docs: 라이센스 오탈자 수정

* feat: 트랜잭션 아이디 기반 간편인증 완료 요청

* chore: migrate JDK 17 to 21

* docs: update README.md

* refactor: 트랜잭션 아이디 기반 단건 간편인증

* chore: path/requestUrl 오탈자 수정

Closes #10

* release: 2.0.0-alpha-004

* chore: 예외처리 및 getter 메소드 추가

* feat: 가상 쓰레드 기반 다건요청 기능 구현 (#14)

* feat: 트랜잭션 아이디 기반 간편인증 다건요청 구현

* feat: 예외처리 개선

* feat: 다건요청 응답 값을 List 형식의 반환값으로 받을 수 있도록 개선

* feat: static import 제거

* refactor: 객체 책임 및 관심사 분리

* docs/chore: update README.md & update release ver to 005

* feat: 단건, 다건 분리

* chore: 코드 포맷팅

Closes #13
  • Loading branch information
h-beeen authored Nov 19, 2024
1 parent 1037fae commit ac4545d
Show file tree
Hide file tree
Showing 15 changed files with 461 additions and 123 deletions.
20 changes: 8 additions & 12 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,27 +24,23 @@
<p align="center">
<a href="https://github.com/codef-io/easycodef-java-v2/actions?query=branch%3Amaster"><img align="center" src="https://img.shields.io/github/actions/workflow/status/codef-io/easycodef-java-v2/publish.yml?style=for-the-badge&logo=gradle&color=02303A" alt="Build Status"/></a>
<a href="https://github.com/codef-io/easycodef-java-v2"><img align="center" src="https://img.shields.io/github/last-commit/codef-io/easycodef-java-v2/master?style=for-the-badge&label=LAST%20BUILD&logo=Github&color=181717" alt="Last Commit"/></a>
<a href="https://central.sonatype.com/artifact/io.codef.api/easycodef-java-v2"><img align="center" src="https://img.shields.io/maven-central/v/io.codef.api/easycodef-java-v2.svg?style=for-the-badge&label=Maven%20Central&logo=apache-maven&color=C71A36" alt="Maven Central"/></a>
<a href="https://central.sonatype.com/artifact/io.codef.api/easycodef-java-v2/2.0.0-alpha-005"><img align="center" src="https://img.shields.io/maven-central/v/io.codef.api/easycodef-java-v2.svg?style=for-the-badge&label=Maven%20Central&logo=apache-maven&color=C71A36" alt="Maven Central"/></a>
</p>

<br><br>

### easycodef-java-v2

- **[EasyCodef V2 Wiki](https://github.com/codef-io/easycodef-java-v2/wiki)**

Latest version support feature
본 라이브러리는 현재 알파 버전 개발중인 버전으로, 2025년 상반기 정식 릴리즈 예정입니다.

→ EasyCodef.requestProduct(EasyCodefRequest request) : 비인증 상품 호출
→ EasyCodef.requestSimpleAuthCertification() : 사용자 본인 인증(간편 인증) 완료 호출
> [!TIP]
> - **[EasyCodef V2 Wiki](https://github.com/codef-io/easycodef-java-v2/wiki)**
<br>

- [Codef Homepage](https://codef.io/)
- [Codef API Developer Guide](https://developer.codef.io/)
- [Hectodata Homepage](https://hectodata.co.kr/)
- [Hecto Tech Blog](https://blog.hectodata.co.kr/)
> [!NOTE]
> - [Codef Homepage](https://codef.io/)
> - [Codef API Developer Guide](https://developer.codef.io/)
> - [Hectodata Homepage](https://hectodata.co.kr/)
> - [Hecto Tech Blog](https://blog.hectodata.co.kr/)
<br>
<br>
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ plugins {


group = "io.codef.api"
version = "2.0.0-alpha-004"
version = "2.0.0-alpha-005"

signing {
useInMemoryPgpKeys(
Expand Down
10 changes: 8 additions & 2 deletions src/main/java/io/codef/api/CodefValidator.java
Original file line number Diff line number Diff line change
Expand Up @@ -10,12 +10,18 @@ public class CodefValidator {
private CodefValidator() {
}

public static <T> T requireNonNullElseThrow(T object, CodefError codefError) {
public static <T> T requireNonNullElseThrow(
T object,
CodefError codefError
) {
return Optional.ofNullable(object)
.orElseThrow(() -> CodefException.from(codefError));
}

public static void requireValidUUIDPattern(String uuid, CodefError codefError) {
public static void requireValidUUIDPattern(
String uuid,
CodefError codefError
) {
final String UUID_REGULAR_EXPRESSION = "^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$";

Optional.ofNullable(uuid)
Expand Down
226 changes: 190 additions & 36 deletions src/main/java/io/codef/api/EasyCodef.java
Original file line number Diff line number Diff line change
Expand Up @@ -5,19 +5,26 @@
import io.codef.api.dto.CodefSimpleAuth;
import io.codef.api.dto.EasyCodefRequest;
import io.codef.api.dto.EasyCodefResponse;
import io.codef.api.error.CodefError;
import io.codef.api.error.CodefException;
import io.codef.api.storage.MultipleRequestStorage;
import io.codef.api.storage.SimpleAuthStorage;
import io.codef.api.util.RsaUtil;

import java.security.PublicKey;
import java.util.HashMap;
import java.util.Optional;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.*;
import java.util.stream.IntStream;

import static io.codef.api.dto.EasyCodefRequest.SSO_ID;
import static io.codef.api.dto.EasyCodefRequest.TRUE;

public class EasyCodef {
private final HashMap<String, CodefSimpleAuth> simpleAuthRequestStorage = new HashMap<>();

private static final long REQUEST_DELAY_MS = 700L;
private final SimpleAuthStorage simpleAuthStorage;
private final MultipleRequestStorage multipleRequestStorage;
private final PublicKey publicKey;
private final CodefClientType clientType;
private final EasyCodefToken easyCodefToken;
Expand All @@ -26,60 +33,207 @@ protected EasyCodef(EasyCodefBuilder builder, EasyCodefToken easyCodefToken) {
this.publicKey = RsaUtil.generatePublicKey(builder.getPublicKey());
this.clientType = builder.getClientType();
this.easyCodefToken = easyCodefToken;
this.simpleAuthStorage = new SimpleAuthStorage();
this.multipleRequestStorage = new MultipleRequestStorage();
}

/**
* 단일 상품 요청
*/
public EasyCodefResponse requestProduct(EasyCodefRequest request) throws CodefException {
final String requestUrl = clientType.getHost() + request.path();
final EasyCodefToken validToken = easyCodefToken.validateAndRefreshToken();
String requestUrl = buildRequestUrl(request);
EasyCodefToken validToken = easyCodefToken.validateAndRefreshToken();

final EasyCodefResponse easyCodefResponse = EasyCodefConnector.requestProduct(request, validToken, requestUrl);
EasyCodefResponse response = EasyCodefConnector.requestProduct(request, validToken, requestUrl);
simpleAuthStorage.storeIfRequired(request, response, requestUrl);

storeIfSimpleAuthResponseRequired(request, easyCodefResponse, requestUrl);
return easyCodefResponse;
return response;
}

/**
* 다중 상품 요청
*/
public EasyCodefResponse requestMultipleProduct(List<EasyCodefRequest> requests) throws CodefException {
validateRequests(requests);

String uuid = UUID.randomUUID().toString();
assignSsoId(requests, uuid);

var executors = createExecutors();
try {
return processMultipleRequests(requests, executors);
} finally {
cleanupExecutors(executors);
}
}

/**
* 단건 간편인증 완료 요청
*/
public EasyCodefResponse requestSimpleAuthCertification(String transactionId) throws CodefException {
final CodefSimpleAuth codefSimpleAuth = simpleAuthRequestStorage.get(transactionId);
CodefValidator.requireNonNullElseThrow(codefSimpleAuth, CodefError.SIMPLE_AUTH_FAILED);
CodefSimpleAuth simpleAuth = simpleAuthStorage.get(transactionId);

EasyCodefRequest enrichedRequest = enrichRequestWithTwoWayInfo(simpleAuth);
EasyCodefResponse response = executeSimpleAuthRequest(enrichedRequest, simpleAuth.requestUrl());

simpleAuthStorage.updateIfRequired(
simpleAuth.requestUrl(),
enrichedRequest,
response,
transactionId
);

return response;
}

/**
* 다건 간편인증 완료 요청
*/
public List<EasyCodefResponse> requestMultipleSimpleAuthCertification(String transactionId) throws CodefException {
CodefSimpleAuth simpleAuth = simpleAuthStorage.get(transactionId);

EasyCodefRequest enrichedRequest = enrichRequestWithTwoWayInfo(simpleAuth);
EasyCodefResponse firstResponse = executeSimpleAuthRequest(enrichedRequest, simpleAuth.requestUrl());

simpleAuthStorage.updateIfRequired(
simpleAuth.requestUrl(),
enrichedRequest,
firstResponse,
transactionId
);

return isSuccessful(firstResponse)
? combineWithRemainingResponses(firstResponse, transactionId)
: List.of(firstResponse);
}


// Private helper methods

private String buildRequestUrl(EasyCodefRequest request) {
return clientType.getHost() + request.path();
}

private EasyCodefRequest enrichRequestWithTwoWayInfo(CodefSimpleAuth simpleAuth) {
EasyCodefRequest request = simpleAuth.request();
Map<String, Object> body = request.requestBody();

body.put(EasyCodefRequest.IS_TWO_WAY, true);
body.put(EasyCodefRequest.SIMPLE_AUTH, TRUE);
body.put(EasyCodefRequest.TWO_WAY_INFO, simpleAuth.response().data());

return request;
}

private EasyCodefResponse executeSimpleAuthRequest(EasyCodefRequest request, String requestUrl)
throws CodefException {
EasyCodefToken validToken = easyCodefToken.validateAndRefreshToken();
return EasyCodefConnector.requestProduct(request, validToken, requestUrl);
}

final String requestUrl = codefSimpleAuth.requestUrl();
final EasyCodefRequest request = codefSimpleAuth.request();
private boolean isSuccessful(EasyCodefResponse response) {
return CodefResponseCode.CF_00000.equals(response.code());
}

private List<EasyCodefResponse> combineWithRemainingResponses(
EasyCodefResponse firstResponse,
String transactionId
) throws CodefException {
List<EasyCodefResponse> remainingResponses = multipleRequestStorage.getRemainingResponses(transactionId);
List<EasyCodefResponse> allResponses = new ArrayList<>(remainingResponses);
allResponses.add(firstResponse);
return allResponses;
}

private void validateRequests(List<EasyCodefRequest> requests) {
if (requests == null || requests.isEmpty()) {
throw new IllegalArgumentException("Requests cannot be null or empty");
}
}

private void assignSsoId(List<EasyCodefRequest> requests, String uuid) {
requests.forEach(request -> request.requestBody().put(SSO_ID, uuid));
}

private CodefExecutors createExecutors() {
return new CodefExecutors(
Executors.newScheduledThreadPool(1),
Executors.newThreadPerTaskExecutor(Thread.ofVirtual().factory())
);
}

private EasyCodefResponse processMultipleRequests(
List<EasyCodefRequest> requests,
CodefExecutors codefExecutors
) throws CodefException {
List<CompletableFuture<EasyCodefResponse>> futures = scheduleRequests(requests, codefExecutors);

addTwoWayInfo(request, codefSimpleAuth);
CompletableFuture<EasyCodefResponse> firstCompleted = CompletableFuture.anyOf(
futures.toArray(new CompletableFuture[0])
).thenApply(result -> (EasyCodefResponse) result);

final EasyCodefToken validToken = easyCodefToken.validateAndRefreshToken();
final EasyCodefResponse easyCodefResponse = EasyCodefConnector.requestProduct(request, validToken, requestUrl);
EasyCodefResponse result = firstCompleted.join();
multipleRequestStorage.store(result.transactionId(), futures);

updateSimpleAuthResponseRequired(requestUrl, request, easyCodefResponse, transactionId);
return easyCodefResponse;
return result;
}

private void addTwoWayInfo(
private List<CompletableFuture<EasyCodefResponse>> scheduleRequests(
List<EasyCodefRequest> requests,
CodefExecutors codefExecutors
) {
return IntStream.range(0, requests.size())
.mapToObj(i -> scheduleRequest(requests.get(i), i * REQUEST_DELAY_MS, codefExecutors))
.toList();
}

private CompletableFuture<EasyCodefResponse> scheduleRequest(
EasyCodefRequest request,
CodefSimpleAuth codefSimpleAuth
long delayMs,
CodefExecutors codefExecutors
) {
request.requestBody().put(EasyCodefRequest.IS_TWO_WAY, true);
request.requestBody().put(EasyCodefRequest.SIMPLE_AUTH, TRUE);
request.requestBody().put(EasyCodefRequest.TWO_WAY_INFO, codefSimpleAuth.response().data());
CompletableFuture<EasyCodefResponse> future = new CompletableFuture<>();

codefExecutors.scheduler.schedule(
() -> executeRequest(request, codefExecutors.virtualThreadExecutor, future),
delayMs,
TimeUnit.MILLISECONDS
);

return future;
}

private void storeIfSimpleAuthResponseRequired(EasyCodefRequest request, EasyCodefResponse easyCodefResponse, String requestUrl) {
Optional.ofNullable(easyCodefResponse.code()).filter(code -> code.equals(CodefResponseCode.CF_03002)).ifPresent(code -> {
CodefSimpleAuth codefSimpleAuth = new CodefSimpleAuth(requestUrl, request, easyCodefResponse);
simpleAuthRequestStorage.put(easyCodefResponse.transactionId(), codefSimpleAuth);
private void executeRequest(
EasyCodefRequest request,
Executor executor,
CompletableFuture<EasyCodefResponse> future
) {
CompletableFuture.supplyAsync(() -> {
try {
return requestProduct(request);
} catch (Exception e) {
throw new RuntimeException(e);
}
}, executor).whenComplete((response, ex) -> {
if (ex != null) {
future.completeExceptionally(ex);
} else {
future.complete(response);
}
});
}

private void updateSimpleAuthResponseRequired(String path, EasyCodefRequest request, EasyCodefResponse easyCodefResponse, String transactionId) {
Optional.ofNullable(easyCodefResponse.code())
.filter(code -> code.equals(CodefResponseCode.CF_03002))
.ifPresentOrElse(code -> {
CodefSimpleAuth newCodefSimpleAuth = new CodefSimpleAuth(path, request, easyCodefResponse);
simpleAuthRequestStorage.put(transactionId, newCodefSimpleAuth);
}, () -> simpleAuthRequestStorage.remove(transactionId));
private void cleanupExecutors(CodefExecutors codefExecutors) {
codefExecutors.scheduler.shutdown();
}

public PublicKey getPublicKey() {
return publicKey;
}

private record CodefExecutors(
ScheduledExecutorService scheduler,
Executor virtualThreadExecutor
) {
}
}
2 changes: 1 addition & 1 deletion src/main/java/io/codef/api/EasyCodefBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@ public static EasyCodefBuilder builder() {
}

public EasyCodefBuilder publicKey(String publicKey) {
this.publicKey = CodefValidator.requireNonNullElseThrow(publicKey, CodefError.INVALID_PUBLIC_KEY);
this.publicKey = CodefValidator.requireNonNullElseThrow(publicKey, CodefError.NULL_PUBLIC_KEY);
return this;
}

Expand Down
Loading

0 comments on commit ac4545d

Please sign in to comment.