From 8ef3d7186f72329760f76ff849cbb1c41b68b63a Mon Sep 17 00:00:00 2001 From: dungbik Date: Sun, 15 Mar 2026 14:12:07 +0900 Subject: [PATCH] =?UTF-8?q?Feat:=20=EC=B9=B4=EB=93=9C=EC=85=8B=20GRPC=20?= =?UTF-8?q?=EC=97=B0=EB=8F=99?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle.kts | 32 +++++++++ .../model/response/BookmarkResponse.java | 5 +- .../bookmark/service/BookmarkService.java | 51 ++++++++------- .../common/event/ReactionEventPublisher.java | 31 +++++++++ .../common/exception/CommonErrorCode.java | 4 +- .../common/grpc/CardSetGrpcClient.java | 65 +++++++++++++++++++ .../reaction/common/grpc/GrpcConfig.java | 22 +++++++ .../like/model/response/LikeResponse.java | 5 +- .../reaction/like/service/LikeService.java | 50 +++++++------- src/main/proto/cardset.proto | 37 +++++++++++ src/main/resources/application.yml | 5 ++ .../reaction/ReactionApplicationTests.java | 3 +- .../reaction/config/TestGrpcConfig.java | 23 +++++++ src/test/resources/application.yml | 7 ++ 14 files changed, 292 insertions(+), 48 deletions(-) create mode 100644 src/main/java/flipnote/reaction/common/event/ReactionEventPublisher.java create mode 100644 src/main/java/flipnote/reaction/common/grpc/CardSetGrpcClient.java create mode 100644 src/main/java/flipnote/reaction/common/grpc/GrpcConfig.java create mode 100644 src/main/proto/cardset.proto create mode 100644 src/test/java/flipnote/reaction/config/TestGrpcConfig.java diff --git a/build.gradle.kts b/build.gradle.kts index c6e5f38..3a4f7ed 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -1,7 +1,10 @@ +import com.google.protobuf.gradle.id + plugins { java id("org.springframework.boot") version "3.5.10" id("io.spring.dependency-management") version "1.1.7" + id("com.google.protobuf") version "0.9.4" } group = "flipnote" @@ -24,12 +27,23 @@ repositories { mavenCentral() } +val grpcVersion = "1.68.0" +val protocVersion = "4.28.2" + dependencies { implementation("org.springframework.boot:spring-boot-starter-actuator") implementation("org.springframework.boot:spring-boot-starter-data-jpa") implementation("org.springframework.boot:spring-boot-starter-web") implementation("org.springframework.boot:spring-boot-starter-validation") implementation("org.springframework.boot:spring-boot-starter-amqp") + + // gRPC + implementation("io.grpc:grpc-netty-shaded:$grpcVersion") + implementation("io.grpc:grpc-protobuf:$grpcVersion") + implementation("io.grpc:grpc-stub:$grpcVersion") + implementation("com.google.protobuf:protobuf-java:$protocVersion") + implementation("javax.annotation:javax.annotation-api:1.3.2") + compileOnly("org.projectlombok:lombok") runtimeOnly("com.mysql:mysql-connector-j") annotationProcessor("org.projectlombok:lombok") @@ -38,6 +52,24 @@ dependencies { testRuntimeOnly("org.junit.platform:junit-platform-launcher") } +protobuf { + protoc { + artifact = "com.google.protobuf:protoc:$protocVersion" + } + plugins { + id("grpc") { + artifact = "io.grpc:protoc-gen-grpc-java:$grpcVersion" + } + } + generateProtoTasks { + all().forEach { task -> + task.plugins { + id("grpc") + } + } + } +} + tasks.withType { useJUnitPlatform() } diff --git a/src/main/java/flipnote/reaction/bookmark/model/response/BookmarkResponse.java b/src/main/java/flipnote/reaction/bookmark/model/response/BookmarkResponse.java index 4a0c94b..c9fc570 100644 --- a/src/main/java/flipnote/reaction/bookmark/model/response/BookmarkResponse.java +++ b/src/main/java/flipnote/reaction/bookmark/model/response/BookmarkResponse.java @@ -2,17 +2,20 @@ import java.time.LocalDateTime; +import cardset.Cardset.CardSetSummary; import flipnote.reaction.bookmark.entity.Bookmark; public record BookmarkResponse( String targetType, Long targetId, + Long targetGroupId, LocalDateTime bookmarkedAt ) { - public static BookmarkResponse from(Bookmark bookmark) { + public static BookmarkResponse from(Bookmark bookmark, CardSetSummary summary) { return new BookmarkResponse( bookmark.getTargetType().name(), bookmark.getTargetId(), + summary != null ? summary.getGroupId() : null, bookmark.getCreatedAt() ); } diff --git a/src/main/java/flipnote/reaction/bookmark/service/BookmarkService.java b/src/main/java/flipnote/reaction/bookmark/service/BookmarkService.java index 4b1db83..ca7f7f7 100644 --- a/src/main/java/flipnote/reaction/bookmark/service/BookmarkService.java +++ b/src/main/java/flipnote/reaction/bookmark/service/BookmarkService.java @@ -1,11 +1,15 @@ package flipnote.reaction.bookmark.service; -import org.springframework.amqp.rabbit.core.RabbitTemplate; +import java.util.List; +import java.util.Map; + import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import cardset.Cardset; +import cardset.Cardset.CardSetSummary; import flipnote.reaction.bookmark.entity.Bookmark; import flipnote.reaction.bookmark.entity.BookmarkTargetType; import flipnote.reaction.bookmark.exception.BookmarkErrorCode; @@ -13,8 +17,10 @@ import flipnote.reaction.bookmark.model.response.BookmarkResponse; import flipnote.reaction.bookmark.repository.BookmarkRepository; import flipnote.reaction.common.config.RabbitMqConfig; +import flipnote.reaction.common.event.ReactionEventPublisher; import flipnote.reaction.common.exception.BizException; -import flipnote.reaction.common.model.event.ReactionMessage; +import flipnote.reaction.common.exception.CommonErrorCode; +import flipnote.reaction.common.grpc.CardSetGrpcClient; import flipnote.reaction.common.model.response.IdResponse; import flipnote.reaction.common.model.response.PagingResponse; import lombok.RequiredArgsConstructor; @@ -28,11 +34,16 @@ public class BookmarkService { private final BookmarkRepository bookmarkRepository; private final BookmarkReader bookmarkReader; - private final RabbitTemplate rabbitTemplate; + private final ReactionEventPublisher eventPublisher; + private final CardSetGrpcClient cardSetGrpcClient; @Transactional public IdResponse addBookmark(BookmarkTargetType targetType, Long targetId, Long userId) { - // TODO: gRPC로 대상 존재 여부 검증 (CardSet/Group 서비스 호출) + if (targetType == BookmarkTargetType.CARD_SET) { + if (!cardSetGrpcClient.isCardSetViewable(targetId, userId)) { + throw new BizException(CommonErrorCode.TARGET_NOT_VIEWABLE); + } + } if (bookmarkReader.isBookmarked(targetType, targetId, userId)) { throw new BizException(BookmarkErrorCode.ALREADY_BOOKMARKED); @@ -50,7 +61,8 @@ public IdResponse addBookmark(BookmarkTargetType targetType, Long targetId, Long throw new BizException(BookmarkErrorCode.ALREADY_BOOKMARKED); } - publishEvent(RabbitMqConfig.ROUTING_KEY_BOOKMARK_ADDED, "BOOKMARK_ADDED", targetType, targetId, userId); + eventPublisher.publish(RabbitMqConfig.ROUTING_KEY_BOOKMARK_ADDED, "BOOKMARK_ADDED", + targetType.name(), targetId, userId); return IdResponse.from(bookmark.getId()); } @@ -60,7 +72,8 @@ public void removeBookmark(BookmarkTargetType targetType, Long targetId, Long us Bookmark bookmark = bookmarkReader.findByTargetAndUserId(targetType, targetId, userId); bookmarkRepository.delete(bookmark); - publishEvent(RabbitMqConfig.ROUTING_KEY_BOOKMARK_REMOVED, "BOOKMARK_REMOVED", targetType, targetId, userId); + eventPublisher.publish(RabbitMqConfig.ROUTING_KEY_BOOKMARK_REMOVED, "BOOKMARK_REMOVED", + targetType.name(), targetId, userId); } public PagingResponse getBookmarks(BookmarkTargetType targetType, Long userId, @@ -69,23 +82,17 @@ public PagingResponse getBookmarks(BookmarkTargetType targetTy targetType, userId, request.getPageRequest() ); - // TODO: gRPC로 대상 상세 정보 fetch (CardSet 서비스 호출) + List targetIds = bookmarkPage.getContent().stream() + .map(Bookmark::getTargetId) + .toList(); - Page responsePage = bookmarkPage.map(BookmarkResponse::from); - return PagingResponse.from(responsePage); - } + Map summaryMap = targetIds.isEmpty() + ? Map.of() + : cardSetGrpcClient.getCardSetsByIds(targetIds, userId); - private void publishEvent(String routingKey, String eventType, - BookmarkTargetType targetType, Long targetId, Long userId) { - try { - rabbitTemplate.convertAndSend( - RabbitMqConfig.EXCHANGE, - routingKey, - new ReactionMessage(eventType, targetType.name(), targetId, userId) - ); - } catch (Exception e) { - log.error("북마크 이벤트 발행 실패: eventType={}, targetType={}, targetId={}, userId={}", - eventType, targetType, targetId, userId, e); - } + Page responsePage = bookmarkPage.map( + bookmark -> BookmarkResponse.from(bookmark, summaryMap.get(bookmark.getTargetId())) + ); + return PagingResponse.from(responsePage); } } diff --git a/src/main/java/flipnote/reaction/common/event/ReactionEventPublisher.java b/src/main/java/flipnote/reaction/common/event/ReactionEventPublisher.java new file mode 100644 index 0000000..92729c0 --- /dev/null +++ b/src/main/java/flipnote/reaction/common/event/ReactionEventPublisher.java @@ -0,0 +1,31 @@ +package flipnote.reaction.common.event; + +import org.springframework.amqp.rabbit.core.RabbitTemplate; +import org.springframework.stereotype.Component; + +import flipnote.reaction.common.config.RabbitMqConfig; +import flipnote.reaction.common.model.event.ReactionMessage; +import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +@RequiredArgsConstructor +public class ReactionEventPublisher { + + private final RabbitTemplate rabbitTemplate; + + public void publish(String routingKey, String eventType, + String targetType, Long targetId, Long userId) { + try { + rabbitTemplate.convertAndSend( + RabbitMqConfig.EXCHANGE, + routingKey, + new ReactionMessage(eventType, targetType, targetId, userId) + ); + } catch (Exception e) { + log.error("이벤트 발행 실패: eventType={}, targetType={}, targetId={}, userId={}", + eventType, targetType, targetId, userId, e); + } + } +} diff --git a/src/main/java/flipnote/reaction/common/exception/CommonErrorCode.java b/src/main/java/flipnote/reaction/common/exception/CommonErrorCode.java index 2c13ba5..349b6f4 100644 --- a/src/main/java/flipnote/reaction/common/exception/CommonErrorCode.java +++ b/src/main/java/flipnote/reaction/common/exception/CommonErrorCode.java @@ -7,7 +7,9 @@ @RequiredArgsConstructor public enum CommonErrorCode implements ErrorCode { INTERNAL_SERVER_ERROR(500, "COMMON_001", "예기치 않은 오류가 발생했습니다."), - INVALID_INPUT_VALUE(400, "COMMON_002", "입력값이 올바르지 않습니다."); + INVALID_INPUT_VALUE(400, "COMMON_002", "입력값이 올바르지 않습니다."), + TARGET_NOT_VIEWABLE(403, "COMMON_003", "접근할 수 없는 대상입니다."), + GRPC_CALL_FAILED(502, "COMMON_004", "외부 서비스 호출에 실패했습니다."); private final int status; private final String code; diff --git a/src/main/java/flipnote/reaction/common/grpc/CardSetGrpcClient.java b/src/main/java/flipnote/reaction/common/grpc/CardSetGrpcClient.java new file mode 100644 index 0000000..ab5104c --- /dev/null +++ b/src/main/java/flipnote/reaction/common/grpc/CardSetGrpcClient.java @@ -0,0 +1,65 @@ +package flipnote.reaction.common.grpc; + +import java.util.List; +import java.util.Map; +import java.util.function.Function; +import java.util.stream.Collectors; + +import org.springframework.stereotype.Component; + +import cardset.Cardset.CardSetSummary; +import cardset.Cardset.GetCardSetsByIdsRequest; +import cardset.Cardset.GetCardSetsByIdsResponse; +import cardset.Cardset.IsCardSetViewableRequest; +import cardset.Cardset.IsCardSetViewableResponse; +import cardset.CardsetServiceGrpc; +import flipnote.reaction.common.exception.BizException; +import flipnote.reaction.common.exception.CommonErrorCode; +import io.grpc.ManagedChannel; +import io.grpc.StatusRuntimeException; +import lombok.extern.slf4j.Slf4j; + +@Slf4j +@Component +public class CardSetGrpcClient { + + private final CardsetServiceGrpc.CardsetServiceBlockingStub stub; + + public CardSetGrpcClient(ManagedChannel cardSetChannel) { + this.stub = CardsetServiceGrpc.newBlockingStub(cardSetChannel); + } + + public boolean isCardSetViewable(Long cardSetId, Long userId) { + try { + IsCardSetViewableRequest request = IsCardSetViewableRequest.newBuilder() + .setCardSetId(cardSetId.intValue()) + .setUserId(userId.intValue()) + .build(); + + IsCardSetViewableResponse response = stub.isCardSetViewable(request); + return response.getViewable(); + } catch (StatusRuntimeException e) { + log.error("gRPC call failed: IsCardSetViewable, cardSetId={}, userId={}", cardSetId, userId, e); + throw new BizException(CommonErrorCode.GRPC_CALL_FAILED); + } + } + + public Map getCardSetsByIds(List cardSetIds, Long userId) { + try { + GetCardSetsByIdsRequest request = GetCardSetsByIdsRequest.newBuilder() + .addAllCardSetIds(cardSetIds) + .setUserId(userId.intValue()) + .build(); + + GetCardSetsByIdsResponse response = stub.getCardSetsByIds(request); + return response.getCardSetsList().stream() + .collect(Collectors.toMap( + cs -> (long) cs.getId(), + Function.identity() + )); + } catch (StatusRuntimeException e) { + log.error("gRPC call failed: GetCardSetsByIds, cardSetIds={}, userId={}", cardSetIds, userId, e); + throw new BizException(CommonErrorCode.GRPC_CALL_FAILED); + } + } +} diff --git a/src/main/java/flipnote/reaction/common/grpc/GrpcConfig.java b/src/main/java/flipnote/reaction/common/grpc/GrpcConfig.java new file mode 100644 index 0000000..d131067 --- /dev/null +++ b/src/main/java/flipnote/reaction/common/grpc/GrpcConfig.java @@ -0,0 +1,22 @@ +package flipnote.reaction.common.grpc; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +import io.grpc.ManagedChannel; +import io.grpc.ManagedChannelBuilder; + +@Configuration +public class GrpcConfig { + + @Bean + public ManagedChannel cardSetChannel( + @Value("${grpc.cardset.host}") String host, + @Value("${grpc.cardset.port}") int port + ) { + return ManagedChannelBuilder.forAddress(host, port) + .usePlaintext() + .build(); + } +} diff --git a/src/main/java/flipnote/reaction/like/model/response/LikeResponse.java b/src/main/java/flipnote/reaction/like/model/response/LikeResponse.java index 509448d..5de2b62 100644 --- a/src/main/java/flipnote/reaction/like/model/response/LikeResponse.java +++ b/src/main/java/flipnote/reaction/like/model/response/LikeResponse.java @@ -2,17 +2,20 @@ import java.time.LocalDateTime; +import cardset.Cardset.CardSetSummary; import flipnote.reaction.like.entity.Like; public record LikeResponse( String targetType, Long targetId, + Long targetGroupId, LocalDateTime likedAt ) { - public static LikeResponse from(Like like) { + public static LikeResponse from(Like like, CardSetSummary summary) { return new LikeResponse( like.getTargetType().name(), like.getTargetId(), + summary != null ? summary.getGroupId() : null, like.getCreatedAt() ); } diff --git a/src/main/java/flipnote/reaction/like/service/LikeService.java b/src/main/java/flipnote/reaction/like/service/LikeService.java index 479aac5..ed46aba 100644 --- a/src/main/java/flipnote/reaction/like/service/LikeService.java +++ b/src/main/java/flipnote/reaction/like/service/LikeService.java @@ -1,14 +1,19 @@ package flipnote.reaction.like.service; -import org.springframework.amqp.rabbit.core.RabbitTemplate; +import java.util.List; +import java.util.Map; + import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.domain.Page; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import cardset.Cardset.CardSetSummary; import flipnote.reaction.common.config.RabbitMqConfig; +import flipnote.reaction.common.event.ReactionEventPublisher; import flipnote.reaction.common.exception.BizException; -import flipnote.reaction.common.model.event.ReactionMessage; +import flipnote.reaction.common.exception.CommonErrorCode; +import flipnote.reaction.common.grpc.CardSetGrpcClient; import flipnote.reaction.common.model.response.IdResponse; import flipnote.reaction.common.model.response.PagingResponse; import flipnote.reaction.like.entity.Like; @@ -28,11 +33,16 @@ public class LikeService { private final LikeRepository likeRepository; private final LikeReader likeReader; - private final RabbitTemplate rabbitTemplate; + private final ReactionEventPublisher eventPublisher; + private final CardSetGrpcClient cardSetGrpcClient; @Transactional public IdResponse addLike(LikeTargetType targetType, Long targetId, Long userId) { - // TODO: gRPC로 대상 존재 여부 검증 (CardSet/Group 서비스 호출) + if (targetType == LikeTargetType.CARD_SET) { + if (!cardSetGrpcClient.isCardSetViewable(targetId, userId)) { + throw new BizException(CommonErrorCode.TARGET_NOT_VIEWABLE); + } + } if (likeReader.isLiked(targetType, targetId, userId)) { throw new BizException(LikeErrorCode.ALREADY_LIKED); @@ -50,7 +60,8 @@ public IdResponse addLike(LikeTargetType targetType, Long targetId, Long userId) throw new BizException(LikeErrorCode.ALREADY_LIKED); } - publishEvent(RabbitMqConfig.ROUTING_KEY_LIKE_ADDED, "LIKE_ADDED", targetType, targetId, userId); + eventPublisher.publish(RabbitMqConfig.ROUTING_KEY_LIKE_ADDED, "LIKE_ADDED", + targetType.name(), targetId, userId); return IdResponse.from(like.getId()); } @@ -60,7 +71,8 @@ public void removeLike(LikeTargetType targetType, Long targetId, Long userId) { Like like = likeReader.findByTargetAndUserId(targetType, targetId, userId); likeRepository.delete(like); - publishEvent(RabbitMqConfig.ROUTING_KEY_LIKE_REMOVED, "LIKE_REMOVED", targetType, targetId, userId); + eventPublisher.publish(RabbitMqConfig.ROUTING_KEY_LIKE_REMOVED, "LIKE_REMOVED", + targetType.name(), targetId, userId); } public PagingResponse getLikes(LikeTargetType targetType, Long userId, LikeSearchRequest request) { @@ -68,23 +80,17 @@ public PagingResponse getLikes(LikeTargetType targetType, Long use targetType, userId, request.getPageRequest() ); - // TODO: gRPC로 대상 상세 정보 fetch (CardSet 서비스 호출) + List targetIds = likePage.getContent().stream() + .map(Like::getTargetId) + .toList(); - Page responsePage = likePage.map(LikeResponse::from); - return PagingResponse.from(responsePage); - } + Map summaryMap = targetIds.isEmpty() + ? Map.of() + : cardSetGrpcClient.getCardSetsByIds(targetIds, userId); - private void publishEvent(String routingKey, String eventType, - LikeTargetType targetType, Long targetId, Long userId) { - try { - rabbitTemplate.convertAndSend( - RabbitMqConfig.EXCHANGE, - routingKey, - new ReactionMessage(eventType, targetType.name(), targetId, userId) - ); - } catch (Exception e) { - log.error("좋아요 이벤트 발행 실패: eventType={}, targetType={}, targetId={}, userId={}", - eventType, targetType, targetId, userId, e); - } + Page responsePage = likePage.map( + like -> LikeResponse.from(like, summaryMap.get(like.getTargetId())) + ); + return PagingResponse.from(responsePage); } } diff --git a/src/main/proto/cardset.proto b/src/main/proto/cardset.proto new file mode 100644 index 0000000..aca48d6 --- /dev/null +++ b/src/main/proto/cardset.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +package cardset; + +service CardsetService { + rpc IsCardSetViewable (IsCardSetViewableRequest) returns (IsCardSetViewableResponse); + rpc GetCardSetsByIds (GetCardSetsByIdsRequest) returns (GetCardSetsByIdsResponse); +} + +message IsCardSetViewableRequest { + int64 card_set_id = 1; + int64 user_id = 2; +} + +message IsCardSetViewableResponse { + bool viewable = 1; +} + +message GetCardSetsByIdsRequest { + repeated int64 card_set_ids = 1; + int64 user_id = 2; +} + +message CardSetSummary { + int64 id = 1; + string name = 2; + int64 group_id = 3; + string visibility = 4; + string category = 5; + string hashtag = 6; + optional int64 image_ref_id = 7; + int64 card_count = 8; +} + +message GetCardSetsByIdsResponse { + repeated CardSetSummary card_sets = 1; +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f42c0e2..d347ef3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,6 +19,11 @@ spring: hibernate: dialect: org.hibernate.dialect.MySQLDialect +grpc: + cardset: + host: ${GRPC_CARDSET_HOST:cardset-service} + port: ${GRPC_CARDSET_PORT:9095} + server: port: 8083 diff --git a/src/test/java/flipnote/reaction/ReactionApplicationTests.java b/src/test/java/flipnote/reaction/ReactionApplicationTests.java index 2a2c715..4faa0f6 100644 --- a/src/test/java/flipnote/reaction/ReactionApplicationTests.java +++ b/src/test/java/flipnote/reaction/ReactionApplicationTests.java @@ -4,10 +4,11 @@ import org.springframework.boot.test.context.SpringBootTest; import org.springframework.context.annotation.Import; +import flipnote.reaction.config.TestGrpcConfig; import flipnote.reaction.config.TestRabbitMqConfig; @SpringBootTest -@Import(TestRabbitMqConfig.class) +@Import({TestRabbitMqConfig.class, TestGrpcConfig.class}) class ReactionApplicationTests { @Test diff --git a/src/test/java/flipnote/reaction/config/TestGrpcConfig.java b/src/test/java/flipnote/reaction/config/TestGrpcConfig.java new file mode 100644 index 0000000..f9846df --- /dev/null +++ b/src/test/java/flipnote/reaction/config/TestGrpcConfig.java @@ -0,0 +1,23 @@ +package flipnote.reaction.config; + +import org.springframework.boot.test.context.TestConfiguration; +import org.springframework.context.annotation.Bean; + +import flipnote.reaction.common.grpc.CardSetGrpcClient; +import io.grpc.ManagedChannel; + +import static org.mockito.Mockito.mock; + +@TestConfiguration +public class TestGrpcConfig { + + @Bean + public ManagedChannel cardSetChannel() { + return mock(ManagedChannel.class); + } + + @Bean + public CardSetGrpcClient cardSetGrpcClient() { + return mock(CardSetGrpcClient.class); + } +} diff --git a/src/test/resources/application.yml b/src/test/resources/application.yml index 284ac37..f99db3a 100644 --- a/src/test/resources/application.yml +++ b/src/test/resources/application.yml @@ -1,4 +1,6 @@ spring: + main: + allow-bean-definition-overriding: true autoconfigure: exclude: org.springframework.boot.autoconfigure.amqp.RabbitAutoConfiguration datasource: @@ -13,3 +15,8 @@ spring: properties: hibernate: dialect: org.hibernate.dialect.H2Dialect + +grpc: + cardset: + host: localhost + port: 9090