From 027ae63081959f96875744f21abe6f8c115d6df6 Mon Sep 17 00:00:00 2001 From: Lian08 Date: Sun, 21 Jun 2026 15:10:06 +0900 Subject: [PATCH] =?UTF-8?q?feat(release/grpc):=20BlockingStub=20=E2=86=92?= =?UTF-8?q?=20FutureStub=20+=20@TimeLimiter=20=EC=A0=84=ED=99=98=20[KAN-29?= =?UTF-8?q?3]?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - CreatorGrpcAdapter: BlockingStub → FutureStub, withDeadlineAfter 제거 - @TimeLimiter(name = "creatorService") 추가 - 반환 타입 CreatorId → CompletableFuture 전환 - fallback 반환 타입도 CompletableFuture로 통일 - CreatorIdQueryPort: 반환 타입 CompletableFuture로 업데이트 - 서비스 레이어: getCreatorId().join() 호출로 변경 Co-Authored-By: Claude Sonnet 4.6 --- .../oauth2/OAuth2LoginSuccessHandler.java | 21 ++++++++---- .../communication/grpc/UserGrpcAdapter.java | 21 +++++++----- .../grpc/UserStatusGrpcAdapter.java | 20 +++++++----- .../auth/adapter/out/jwt/JwtTokenManager.java | 5 +++ .../port/out/UserCredentialPort.java | 4 ++- .../application/port/out/UserStatusPort.java | 4 ++- .../application/service/LoginService.java | 11 ++++++- .../application/service/LogoutService.java | 12 ++++++- .../grpc/CreatorGrpcAdapter.java | 32 +++++++++++++++---- .../port/out/CreatorIdQueryPort.java | 6 ++-- .../service/DeleteGeneralGoodsService.java | 4 +-- .../service/RegisterGeneralGoodsService.java | 4 +-- .../service/UpdateGeneralGoodsService.java | 4 +-- 13 files changed, 107 insertions(+), 41 deletions(-) diff --git a/services/auth/src/main/java/kr/magicbox/auth/adapter/in/security/oauth2/OAuth2LoginSuccessHandler.java b/services/auth/src/main/java/kr/magicbox/auth/adapter/in/security/oauth2/OAuth2LoginSuccessHandler.java index 463b75f8..e14a559c 100644 --- a/services/auth/src/main/java/kr/magicbox/auth/adapter/in/security/oauth2/OAuth2LoginSuccessHandler.java +++ b/services/auth/src/main/java/kr/magicbox/auth/adapter/in/security/oauth2/OAuth2LoginSuccessHandler.java @@ -18,6 +18,7 @@ import java.time.Instant; import java.util.Objects; import java.util.UUID; +import java.util.concurrent.ExecutionException; @Component @RequiredArgsConstructor @@ -35,12 +36,20 @@ public void onAuthenticationSuccess(@NotNull HttpServletRequest request, HttpSer (OAuth2UserInfo) authentication.getPrincipal(), "OAuth2 인증 정보가 존재하지 않습니다."); // 1. gRPC로 user 서비스 호출 - UserResult userResult = userCredentialPort.loadCredential( - userInfo.oauth2Id(), - userInfo.providerType().getProvider(), - userInfo.email(), - userInfo.profileImage() - ); + UserResult userResult; + try { + userResult = userCredentialPort.loadCredential( + userInfo.oauth2Id(), + userInfo.providerType().getProvider(), + userInfo.email(), + userInfo.profileImage() + ).get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e.getCause()); + } // 2. 일회용 Code 생성 및 Redis 저장 String codeValue = UUID.randomUUID().toString(); diff --git a/services/auth/src/main/java/kr/magicbox/auth/adapter/out/communication/grpc/UserGrpcAdapter.java b/services/auth/src/main/java/kr/magicbox/auth/adapter/out/communication/grpc/UserGrpcAdapter.java index 284401ca..00e739d4 100644 --- a/services/auth/src/main/java/kr/magicbox/auth/adapter/out/communication/grpc/UserGrpcAdapter.java +++ b/services/auth/src/main/java/kr/magicbox/auth/adapter/out/communication/grpc/UserGrpcAdapter.java @@ -1,6 +1,9 @@ package kr.magicbox.auth.adapter.out.communication.grpc; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.Futures; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.timelimiter.annotation.TimeLimiter; import kr.magicbox.auth.adapter.out.communication.grpc.exception.UnsupportedUserRoleException; import kr.magicbox.auth.adapter.out.communication.grpc.exception.UserServiceUnavailableException; import kr.magicbox.auth.application.dto.result.UserResult; @@ -17,7 +20,7 @@ import org.springframework.grpc.client.GrpcChannelFactory; import org.springframework.stereotype.Component; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.CompletableFuture; @Slf4j @Component @@ -27,7 +30,8 @@ public class UserGrpcAdapter implements UserCredentialPort { @Override @CircuitBreaker(name = "userService", fallbackMethod = "loadCredentialFallback") - public UserResult loadCredential(String oauth2Id, GrpcOAuth2Provider provider, String email, String profileImage) { + @TimeLimiter(name = "userService", fallbackMethod = "loadCredentialFallback") + public CompletableFuture loadCredential(String oauth2Id, GrpcOAuth2Provider provider, String email, String profileImage) { LoadUserCredentialRequest request = LoadUserCredentialRequest.newBuilder() .setOauth2Id(oauth2Id) .setProvider(provider) @@ -35,12 +39,13 @@ public UserResult loadCredential(String oauth2Id, GrpcOAuth2Provider provider, S .setProfileImage(profileImage != null ? profileImage : "") .build(); - UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc - .newBlockingStub(grpcChannelFactory.createChannel(ServiceHost.USER.getHostName())) - .withDeadlineAfter(2, TimeUnit.SECONDS); - LoadUserCredentialResponse response = stub.loadUserCredential(request); + UserServiceGrpc.UserServiceFutureStub stub = UserServiceGrpc.newFutureStub( + grpcChannelFactory.createChannel(ServiceHost.USER.getHostName())); + ListenableFuture future = stub.loadUserCredential(request); + LoadUserCredentialResponse response = Futures.getUnchecked(future); - return new UserResult(UserId.of(response.getUserId()), toUserRole(response.getUserRole())); + return CompletableFuture.completedFuture( + new UserResult(UserId.of(response.getUserId()), toUserRole(response.getUserRole()))); } private UserRole toUserRole(GrpcUserRole grpcUserRole) { @@ -53,7 +58,7 @@ private UserRole toUserRole(GrpcUserRole grpcUserRole) { } @SuppressWarnings("unused") - private UserResult loadCredentialFallback(String oauth2Id, GrpcOAuth2Provider provider, String email, String profileImage, Throwable throwable) { + private CompletableFuture loadCredentialFallback(String oauth2Id, GrpcOAuth2Provider provider, String email, String profileImage, Throwable throwable) { log.warn("User 서비스 연결 실패: {}", throwable.getMessage()); throw new UserServiceUnavailableException(throwable); } diff --git a/services/auth/src/main/java/kr/magicbox/auth/adapter/out/communication/grpc/UserStatusGrpcAdapter.java b/services/auth/src/main/java/kr/magicbox/auth/adapter/out/communication/grpc/UserStatusGrpcAdapter.java index c9f9f0d6..0f24f62c 100644 --- a/services/auth/src/main/java/kr/magicbox/auth/adapter/out/communication/grpc/UserStatusGrpcAdapter.java +++ b/services/auth/src/main/java/kr/magicbox/auth/adapter/out/communication/grpc/UserStatusGrpcAdapter.java @@ -1,6 +1,9 @@ package kr.magicbox.auth.adapter.out.communication.grpc; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.timelimiter.annotation.TimeLimiter; import kr.magicbox.auth.adapter.out.communication.grpc.exception.UserServiceUnavailableException; import kr.magicbox.auth.application.port.out.UserStatusPort; import kr.magicbox.auth.grpc.user.CheckUserActiveRequest; @@ -11,7 +14,7 @@ import org.springframework.grpc.client.GrpcChannelFactory; import org.springframework.stereotype.Component; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.CompletableFuture; @Slf4j @Component @@ -22,21 +25,22 @@ public class UserStatusGrpcAdapter implements UserStatusPort { @Override @CircuitBreaker(name = "userService", fallbackMethod = "checkUserActiveFallback") - public boolean isActive(Long userId) { + @TimeLimiter(name = "userService", fallbackMethod = "checkUserActiveFallback") + public CompletableFuture isActive(Long userId) { CheckUserActiveRequest request = CheckUserActiveRequest.newBuilder() .setUserId(userId) .build(); - UserServiceGrpc.UserServiceBlockingStub stub = UserServiceGrpc - .newBlockingStub(grpcChannelFactory.createChannel(ServiceHost.USER.getHostName())) - .withDeadlineAfter(2, TimeUnit.SECONDS); - CheckUserActiveResponse response = stub.checkUserActive(request); + UserServiceGrpc.UserServiceFutureStub stub = UserServiceGrpc.newFutureStub( + grpcChannelFactory.createChannel(ServiceHost.USER.getHostName())); + ListenableFuture future = stub.checkUserActive(request); + CheckUserActiveResponse response = Futures.getUnchecked(future); - return response.getActive(); + return CompletableFuture.completedFuture(response.getActive()); } @SuppressWarnings("unused") - private boolean checkUserActiveFallback(Long userId, Throwable throwable) { + private CompletableFuture checkUserActiveFallback(Long userId, Throwable throwable) { log.warn("User 서비스 연결 실패: {}", throwable.getMessage()); throw new UserServiceUnavailableException(throwable); } diff --git a/services/auth/src/main/java/kr/magicbox/auth/adapter/out/jwt/JwtTokenManager.java b/services/auth/src/main/java/kr/magicbox/auth/adapter/out/jwt/JwtTokenManager.java index 514f2836..0444fafa 100644 --- a/services/auth/src/main/java/kr/magicbox/auth/adapter/out/jwt/JwtTokenManager.java +++ b/services/auth/src/main/java/kr/magicbox/auth/adapter/out/jwt/JwtTokenManager.java @@ -14,6 +14,7 @@ import javax.crypto.SecretKey; import javax.crypto.spec.SecretKeySpec; import java.nio.charset.StandardCharsets; +import java.security.interfaces.RSAPublicKey; import java.util.Date; @Component @@ -90,4 +91,8 @@ public UserRole extractRole(String token) { String role = claims.get(JwtConstants.CLAIM_ROLE, String.class); return UserRole.of(role); } + + public RSAPublicKey getPublicKey() { + throw new UnsupportedOperationException("이 서비스는 RSA 공개키를 사용하지 않습니다."); + } } diff --git a/services/auth/src/main/java/kr/magicbox/auth/application/port/out/UserCredentialPort.java b/services/auth/src/main/java/kr/magicbox/auth/application/port/out/UserCredentialPort.java index 7209613f..1feb1215 100644 --- a/services/auth/src/main/java/kr/magicbox/auth/application/port/out/UserCredentialPort.java +++ b/services/auth/src/main/java/kr/magicbox/auth/application/port/out/UserCredentialPort.java @@ -3,6 +3,8 @@ import kr.magicbox.auth.application.dto.result.UserResult; import kr.magicbox.auth.grpc.user.GrpcOAuth2Provider; +import java.util.concurrent.CompletableFuture; + public interface UserCredentialPort { - UserResult loadCredential(String oauth2Id, GrpcOAuth2Provider provider, String email, String profileImage); + CompletableFuture loadCredential(String oauth2Id, GrpcOAuth2Provider provider, String email, String profileImage); } diff --git a/services/auth/src/main/java/kr/magicbox/auth/application/port/out/UserStatusPort.java b/services/auth/src/main/java/kr/magicbox/auth/application/port/out/UserStatusPort.java index f197155e..73d0a4d5 100644 --- a/services/auth/src/main/java/kr/magicbox/auth/application/port/out/UserStatusPort.java +++ b/services/auth/src/main/java/kr/magicbox/auth/application/port/out/UserStatusPort.java @@ -1,5 +1,7 @@ package kr.magicbox.auth.application.port.out; +import java.util.concurrent.CompletableFuture; + public interface UserStatusPort { - boolean isActive(Long userId); + CompletableFuture isActive(Long userId); } diff --git a/services/auth/src/main/java/kr/magicbox/auth/application/service/LoginService.java b/services/auth/src/main/java/kr/magicbox/auth/application/service/LoginService.java index 42e482fb..99f4492d 100644 --- a/services/auth/src/main/java/kr/magicbox/auth/application/service/LoginService.java +++ b/services/auth/src/main/java/kr/magicbox/auth/application/service/LoginService.java @@ -19,6 +19,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.Instant; +import java.util.concurrent.ExecutionException; @Service @RequiredArgsConstructor @@ -60,7 +61,15 @@ private Code validateAndGetCode(String codeValue) { private void saveLoginEvent(UserId userId) { Instant now = Instant.now(); - boolean isDuplicate = userStatusPort.isActive(userId.value()); + boolean isDuplicate; + try { + isDuplicate = userStatusPort.isActive(userId.value()).get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e.getCause()); + } AuthDomainEvent event = isDuplicate ? DuplicateLoginEvent.builder().userId(userId).occurredAt(now).build() diff --git a/services/auth/src/main/java/kr/magicbox/auth/application/service/LogoutService.java b/services/auth/src/main/java/kr/magicbox/auth/application/service/LogoutService.java index 2c654aa6..09c2dab1 100644 --- a/services/auth/src/main/java/kr/magicbox/auth/application/service/LogoutService.java +++ b/services/auth/src/main/java/kr/magicbox/auth/application/service/LogoutService.java @@ -11,6 +11,7 @@ import org.springframework.transaction.annotation.Transactional; import java.time.Instant; +import java.util.concurrent.ExecutionException; @Service @RequiredArgsConstructor @@ -24,7 +25,16 @@ public class LogoutService implements LogoutUseCase { public void logout(LogoutCommand command) { UserId userId = command.userId(); - if (!userStatusPort.isActive(userId.value())) { + boolean active; + try { + active = userStatusPort.isActive(userId.value()).get(); + } catch (InterruptedException e) { + Thread.currentThread().interrupt(); + throw new RuntimeException(e); + } catch (ExecutionException e) { + throw new RuntimeException(e.getCause()); + } + if (!active) { throw new UserInactiveException(); } refreshTokenRepositoryPort.deleteRefreshToken(userId); diff --git a/services/general-goods/src/main/java/kr/magicbox/generalgoods/adapter/out/communication/grpc/CreatorGrpcAdapter.java b/services/general-goods/src/main/java/kr/magicbox/generalgoods/adapter/out/communication/grpc/CreatorGrpcAdapter.java index da562539..6e7a5088 100644 --- a/services/general-goods/src/main/java/kr/magicbox/generalgoods/adapter/out/communication/grpc/CreatorGrpcAdapter.java +++ b/services/general-goods/src/main/java/kr/magicbox/generalgoods/adapter/out/communication/grpc/CreatorGrpcAdapter.java @@ -1,6 +1,11 @@ package kr.magicbox.generalgoods.adapter.out.communication.grpc; +import com.google.common.util.concurrent.FutureCallback; +import com.google.common.util.concurrent.Futures; +import com.google.common.util.concurrent.ListenableFuture; +import com.google.common.util.concurrent.MoreExecutors; import io.github.resilience4j.circuitbreaker.annotation.CircuitBreaker; +import io.github.resilience4j.timelimiter.annotation.TimeLimiter; import io.grpc.ManagedChannel; import io.grpc.Status; import io.grpc.StatusRuntimeException; @@ -16,7 +21,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.stereotype.Component; -import java.util.concurrent.TimeUnit; +import java.util.concurrent.CompletableFuture; @Component @RequiredArgsConstructor @@ -26,20 +31,33 @@ public class CreatorGrpcAdapter implements CreatorIdQueryPort { @Override @CircuitBreaker(name = "creatorService", fallbackMethod = "getCreatorIdFallback") - public CreatorId getCreatorId(UserId userId) { + @TimeLimiter(name = "creatorService", fallbackMethod = "getCreatorIdFallback") + public CompletableFuture getCreatorId(UserId userId) { GetCreatorIdByUserIdRequest request = GetCreatorIdByUserIdRequest.newBuilder() .setUserId(userId.value()) .build(); - CreatorServiceGrpc.CreatorServiceBlockingStub stub = CreatorServiceGrpc.newBlockingStub(creatorManagedChannel) - .withDeadlineAfter(2, TimeUnit.SECONDS); - GetCreatorIdByUserIdResponse response = stub.getCreatorIdByUserId(request); + CreatorServiceGrpc.CreatorServiceFutureStub stub = CreatorServiceGrpc.newFutureStub(creatorManagedChannel); + ListenableFuture future = stub.getCreatorIdByUserId(request); - return new CreatorId(response.getCreatorId()); + CompletableFuture result = new CompletableFuture<>(); + Futures.addCallback(future, new FutureCallback() { + @Override + public void onSuccess(GetCreatorIdByUserIdResponse response) { + result.complete(new CreatorId(response.getCreatorId())); + } + + @Override + public void onFailure(Throwable throwable) { + result.completeExceptionally(throwable); + } + }, MoreExecutors.directExecutor()); + + return result; } @SuppressWarnings("unused") - private CreatorId getCreatorIdFallback(UserId userId, Throwable throwable) { + private CompletableFuture getCreatorIdFallback(UserId userId, Throwable throwable) { if (throwable instanceof StatusRuntimeException statusException && statusException.getStatus().getCode() == Status.Code.NOT_FOUND) { throw new CreatorNotFoundException(); diff --git a/services/general-goods/src/main/java/kr/magicbox/generalgoods/application/port/out/CreatorIdQueryPort.java b/services/general-goods/src/main/java/kr/magicbox/generalgoods/application/port/out/CreatorIdQueryPort.java index 506796e9..4f232b50 100644 --- a/services/general-goods/src/main/java/kr/magicbox/generalgoods/application/port/out/CreatorIdQueryPort.java +++ b/services/general-goods/src/main/java/kr/magicbox/generalgoods/application/port/out/CreatorIdQueryPort.java @@ -3,7 +3,9 @@ import kr.magicbox.generalgoods.domain.vo.CreatorId; import kr.magicbox.generalgoods.domain.vo.UserId; +import java.util.concurrent.CompletableFuture; + public interface CreatorIdQueryPort { - CreatorId getCreatorId(UserId userId); -} \ No newline at end of file + CompletableFuture getCreatorId(UserId userId); +} diff --git a/services/general-goods/src/main/java/kr/magicbox/generalgoods/application/service/DeleteGeneralGoodsService.java b/services/general-goods/src/main/java/kr/magicbox/generalgoods/application/service/DeleteGeneralGoodsService.java index 17c4e17b..4e0ce5cd 100644 --- a/services/general-goods/src/main/java/kr/magicbox/generalgoods/application/service/DeleteGeneralGoodsService.java +++ b/services/general-goods/src/main/java/kr/magicbox/generalgoods/application/service/DeleteGeneralGoodsService.java @@ -22,7 +22,7 @@ public class DeleteGeneralGoodsService implements DeleteGeneralGoodsUseCase { public void deleteGeneralGoods(DeleteGeneralGoodsCommand command) { GeneralGoods generalGoods = generalGoodsRepositoryPort.findById(command.id()); - CreatorId creatorId = creatorIdQueryPort.getCreatorId(command.userId()); + CreatorId creatorId = creatorIdQueryPort.getCreatorId(command.userId()).join(); if (!generalGoods.getCreatorId().equals(creatorId)) { throw new GeneralGoodsUnauthorizedException(); } @@ -30,4 +30,4 @@ public void deleteGeneralGoods(DeleteGeneralGoodsCommand command) { generalGoods.delete(); generalGoodsRepositoryPort.update(generalGoods); } -} \ No newline at end of file +} diff --git a/services/general-goods/src/main/java/kr/magicbox/generalgoods/application/service/RegisterGeneralGoodsService.java b/services/general-goods/src/main/java/kr/magicbox/generalgoods/application/service/RegisterGeneralGoodsService.java index ed2652b0..654c7e37 100644 --- a/services/general-goods/src/main/java/kr/magicbox/generalgoods/application/service/RegisterGeneralGoodsService.java +++ b/services/general-goods/src/main/java/kr/magicbox/generalgoods/application/service/RegisterGeneralGoodsService.java @@ -27,7 +27,7 @@ public class RegisterGeneralGoodsService implements RegisterGeneralGoodsUseCase @Transactional @Override public void registerGeneralGoods(RegisterGeneralGoodsCommand command) { - CreatorId creatorId = creatorIdQueryPort.getCreatorId(command.userId()); + CreatorId creatorId = creatorIdQueryPort.getCreatorId(command.userId()).join(); List mediaList = command.mediaList().stream() .map(this::toGeneralGoodsMedia) @@ -71,4 +71,4 @@ private GeneralGoodsMedia toGeneralGoodsMedia(MediaCommand mediaCommand) { .sortOrder(mediaCommand.sortOrder()) .build(); } -} \ No newline at end of file +} diff --git a/services/general-goods/src/main/java/kr/magicbox/generalgoods/application/service/UpdateGeneralGoodsService.java b/services/general-goods/src/main/java/kr/magicbox/generalgoods/application/service/UpdateGeneralGoodsService.java index 06ab04b9..169bbd2f 100644 --- a/services/general-goods/src/main/java/kr/magicbox/generalgoods/application/service/UpdateGeneralGoodsService.java +++ b/services/general-goods/src/main/java/kr/magicbox/generalgoods/application/service/UpdateGeneralGoodsService.java @@ -31,7 +31,7 @@ public class UpdateGeneralGoodsService implements UpdateGeneralGoodsUseCase { public void updateGeneralGoods(UpdateGeneralGoodsCommand command) { GeneralGoods generalGoods = generalGoodsRepositoryPort.findById(command.id()); - CreatorId creatorId = creatorIdQueryPort.getCreatorId(command.userId()); + CreatorId creatorId = creatorIdQueryPort.getCreatorId(command.userId()).join(); if (!generalGoods.getCreatorId().equals(creatorId)) { throw new GeneralGoodsUnauthorizedException(); } @@ -78,4 +78,4 @@ private GeneralGoodsMedia toGeneralGoodsMedia(MediaCommand mediaCommand) { .sortOrder(mediaCommand.sortOrder()) .build(); } -} \ No newline at end of file +}