Skip to content

Commit 3a04b69

Browse files
authored
Merge pull request #2 from FlipNoteTeam/feat/impl-grpc
Feat: 카드셋 GRPC 연동
2 parents a742653 + 8ef3d71 commit 3a04b69

14 files changed

Lines changed: 292 additions & 48 deletions

File tree

build.gradle.kts

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
1+
import com.google.protobuf.gradle.id
2+
13
plugins {
24
java
35
id("org.springframework.boot") version "3.5.10"
46
id("io.spring.dependency-management") version "1.1.7"
7+
id("com.google.protobuf") version "0.9.4"
58
}
69

710
group = "flipnote"
@@ -24,12 +27,23 @@ repositories {
2427
mavenCentral()
2528
}
2629

30+
val grpcVersion = "1.68.0"
31+
val protocVersion = "4.28.2"
32+
2733
dependencies {
2834
implementation("org.springframework.boot:spring-boot-starter-actuator")
2935
implementation("org.springframework.boot:spring-boot-starter-data-jpa")
3036
implementation("org.springframework.boot:spring-boot-starter-web")
3137
implementation("org.springframework.boot:spring-boot-starter-validation")
3238
implementation("org.springframework.boot:spring-boot-starter-amqp")
39+
40+
// gRPC
41+
implementation("io.grpc:grpc-netty-shaded:$grpcVersion")
42+
implementation("io.grpc:grpc-protobuf:$grpcVersion")
43+
implementation("io.grpc:grpc-stub:$grpcVersion")
44+
implementation("com.google.protobuf:protobuf-java:$protocVersion")
45+
implementation("javax.annotation:javax.annotation-api:1.3.2")
46+
3347
compileOnly("org.projectlombok:lombok")
3448
runtimeOnly("com.mysql:mysql-connector-j")
3549
annotationProcessor("org.projectlombok:lombok")
@@ -38,6 +52,24 @@ dependencies {
3852
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
3953
}
4054

55+
protobuf {
56+
protoc {
57+
artifact = "com.google.protobuf:protoc:$protocVersion"
58+
}
59+
plugins {
60+
id("grpc") {
61+
artifact = "io.grpc:protoc-gen-grpc-java:$grpcVersion"
62+
}
63+
}
64+
generateProtoTasks {
65+
all().forEach { task ->
66+
task.plugins {
67+
id("grpc")
68+
}
69+
}
70+
}
71+
}
72+
4173
tasks.withType<Test> {
4274
useJUnitPlatform()
4375
}

src/main/java/flipnote/reaction/bookmark/model/response/BookmarkResponse.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@
22

33
import java.time.LocalDateTime;
44

5+
import cardset.Cardset.CardSetSummary;
56
import flipnote.reaction.bookmark.entity.Bookmark;
67

78
public record BookmarkResponse(
89
String targetType,
910
Long targetId,
11+
Long targetGroupId,
1012
LocalDateTime bookmarkedAt
1113
) {
12-
public static BookmarkResponse from(Bookmark bookmark) {
14+
public static BookmarkResponse from(Bookmark bookmark, CardSetSummary summary) {
1315
return new BookmarkResponse(
1416
bookmark.getTargetType().name(),
1517
bookmark.getTargetId(),
18+
summary != null ? summary.getGroupId() : null,
1619
bookmark.getCreatedAt()
1720
);
1821
}
Lines changed: 29 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,20 +1,26 @@
11
package flipnote.reaction.bookmark.service;
22

3-
import org.springframework.amqp.rabbit.core.RabbitTemplate;
3+
import java.util.List;
4+
import java.util.Map;
5+
46
import org.springframework.dao.DataIntegrityViolationException;
57
import org.springframework.data.domain.Page;
68
import org.springframework.stereotype.Service;
79
import org.springframework.transaction.annotation.Transactional;
810

11+
import cardset.Cardset;
12+
import cardset.Cardset.CardSetSummary;
913
import flipnote.reaction.bookmark.entity.Bookmark;
1014
import flipnote.reaction.bookmark.entity.BookmarkTargetType;
1115
import flipnote.reaction.bookmark.exception.BookmarkErrorCode;
1216
import flipnote.reaction.bookmark.model.request.BookmarkSearchRequest;
1317
import flipnote.reaction.bookmark.model.response.BookmarkResponse;
1418
import flipnote.reaction.bookmark.repository.BookmarkRepository;
1519
import flipnote.reaction.common.config.RabbitMqConfig;
20+
import flipnote.reaction.common.event.ReactionEventPublisher;
1621
import flipnote.reaction.common.exception.BizException;
17-
import flipnote.reaction.common.model.event.ReactionMessage;
22+
import flipnote.reaction.common.exception.CommonErrorCode;
23+
import flipnote.reaction.common.grpc.CardSetGrpcClient;
1824
import flipnote.reaction.common.model.response.IdResponse;
1925
import flipnote.reaction.common.model.response.PagingResponse;
2026
import lombok.RequiredArgsConstructor;
@@ -28,11 +34,16 @@ public class BookmarkService {
2834

2935
private final BookmarkRepository bookmarkRepository;
3036
private final BookmarkReader bookmarkReader;
31-
private final RabbitTemplate rabbitTemplate;
37+
private final ReactionEventPublisher eventPublisher;
38+
private final CardSetGrpcClient cardSetGrpcClient;
3239

3340
@Transactional
3441
public IdResponse addBookmark(BookmarkTargetType targetType, Long targetId, Long userId) {
35-
// TODO: gRPC로 대상 존재 여부 검증 (CardSet/Group 서비스 호출)
42+
if (targetType == BookmarkTargetType.CARD_SET) {
43+
if (!cardSetGrpcClient.isCardSetViewable(targetId, userId)) {
44+
throw new BizException(CommonErrorCode.TARGET_NOT_VIEWABLE);
45+
}
46+
}
3647

3748
if (bookmarkReader.isBookmarked(targetType, targetId, userId)) {
3849
throw new BizException(BookmarkErrorCode.ALREADY_BOOKMARKED);
@@ -50,7 +61,8 @@ public IdResponse addBookmark(BookmarkTargetType targetType, Long targetId, Long
5061
throw new BizException(BookmarkErrorCode.ALREADY_BOOKMARKED);
5162
}
5263

53-
publishEvent(RabbitMqConfig.ROUTING_KEY_BOOKMARK_ADDED, "BOOKMARK_ADDED", targetType, targetId, userId);
64+
eventPublisher.publish(RabbitMqConfig.ROUTING_KEY_BOOKMARK_ADDED, "BOOKMARK_ADDED",
65+
targetType.name(), targetId, userId);
5466

5567
return IdResponse.from(bookmark.getId());
5668
}
@@ -60,7 +72,8 @@ public void removeBookmark(BookmarkTargetType targetType, Long targetId, Long us
6072
Bookmark bookmark = bookmarkReader.findByTargetAndUserId(targetType, targetId, userId);
6173
bookmarkRepository.delete(bookmark);
6274

63-
publishEvent(RabbitMqConfig.ROUTING_KEY_BOOKMARK_REMOVED, "BOOKMARK_REMOVED", targetType, targetId, userId);
75+
eventPublisher.publish(RabbitMqConfig.ROUTING_KEY_BOOKMARK_REMOVED, "BOOKMARK_REMOVED",
76+
targetType.name(), targetId, userId);
6477
}
6578

6679
public PagingResponse<BookmarkResponse> getBookmarks(BookmarkTargetType targetType, Long userId,
@@ -69,23 +82,17 @@ public PagingResponse<BookmarkResponse> getBookmarks(BookmarkTargetType targetTy
6982
targetType, userId, request.getPageRequest()
7083
);
7184

72-
// TODO: gRPC로 대상 상세 정보 fetch (CardSet 서비스 호출)
85+
List<Long> targetIds = bookmarkPage.getContent().stream()
86+
.map(Bookmark::getTargetId)
87+
.toList();
7388

74-
Page<BookmarkResponse> responsePage = bookmarkPage.map(BookmarkResponse::from);
75-
return PagingResponse.from(responsePage);
76-
}
89+
Map<Long, CardSetSummary> summaryMap = targetIds.isEmpty()
90+
? Map.of()
91+
: cardSetGrpcClient.getCardSetsByIds(targetIds, userId);
7792

78-
private void publishEvent(String routingKey, String eventType,
79-
BookmarkTargetType targetType, Long targetId, Long userId) {
80-
try {
81-
rabbitTemplate.convertAndSend(
82-
RabbitMqConfig.EXCHANGE,
83-
routingKey,
84-
new ReactionMessage(eventType, targetType.name(), targetId, userId)
85-
);
86-
} catch (Exception e) {
87-
log.error("북마크 이벤트 발행 실패: eventType={}, targetType={}, targetId={}, userId={}",
88-
eventType, targetType, targetId, userId, e);
89-
}
93+
Page<BookmarkResponse> responsePage = bookmarkPage.map(
94+
bookmark -> BookmarkResponse.from(bookmark, summaryMap.get(bookmark.getTargetId()))
95+
);
96+
return PagingResponse.from(responsePage);
9097
}
9198
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
package flipnote.reaction.common.event;
2+
3+
import org.springframework.amqp.rabbit.core.RabbitTemplate;
4+
import org.springframework.stereotype.Component;
5+
6+
import flipnote.reaction.common.config.RabbitMqConfig;
7+
import flipnote.reaction.common.model.event.ReactionMessage;
8+
import lombok.RequiredArgsConstructor;
9+
import lombok.extern.slf4j.Slf4j;
10+
11+
@Slf4j
12+
@Component
13+
@RequiredArgsConstructor
14+
public class ReactionEventPublisher {
15+
16+
private final RabbitTemplate rabbitTemplate;
17+
18+
public void publish(String routingKey, String eventType,
19+
String targetType, Long targetId, Long userId) {
20+
try {
21+
rabbitTemplate.convertAndSend(
22+
RabbitMqConfig.EXCHANGE,
23+
routingKey,
24+
new ReactionMessage(eventType, targetType, targetId, userId)
25+
);
26+
} catch (Exception e) {
27+
log.error("이벤트 발행 실패: eventType={}, targetType={}, targetId={}, userId={}",
28+
eventType, targetType, targetId, userId, e);
29+
}
30+
}
31+
}

src/main/java/flipnote/reaction/common/exception/CommonErrorCode.java

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@
77
@RequiredArgsConstructor
88
public enum CommonErrorCode implements ErrorCode {
99
INTERNAL_SERVER_ERROR(500, "COMMON_001", "예기치 않은 오류가 발생했습니다."),
10-
INVALID_INPUT_VALUE(400, "COMMON_002", "입력값이 올바르지 않습니다.");
10+
INVALID_INPUT_VALUE(400, "COMMON_002", "입력값이 올바르지 않습니다."),
11+
TARGET_NOT_VIEWABLE(403, "COMMON_003", "접근할 수 없는 대상입니다."),
12+
GRPC_CALL_FAILED(502, "COMMON_004", "외부 서비스 호출에 실패했습니다.");
1113

1214
private final int status;
1315
private final String code;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
package flipnote.reaction.common.grpc;
2+
3+
import java.util.List;
4+
import java.util.Map;
5+
import java.util.function.Function;
6+
import java.util.stream.Collectors;
7+
8+
import org.springframework.stereotype.Component;
9+
10+
import cardset.Cardset.CardSetSummary;
11+
import cardset.Cardset.GetCardSetsByIdsRequest;
12+
import cardset.Cardset.GetCardSetsByIdsResponse;
13+
import cardset.Cardset.IsCardSetViewableRequest;
14+
import cardset.Cardset.IsCardSetViewableResponse;
15+
import cardset.CardsetServiceGrpc;
16+
import flipnote.reaction.common.exception.BizException;
17+
import flipnote.reaction.common.exception.CommonErrorCode;
18+
import io.grpc.ManagedChannel;
19+
import io.grpc.StatusRuntimeException;
20+
import lombok.extern.slf4j.Slf4j;
21+
22+
@Slf4j
23+
@Component
24+
public class CardSetGrpcClient {
25+
26+
private final CardsetServiceGrpc.CardsetServiceBlockingStub stub;
27+
28+
public CardSetGrpcClient(ManagedChannel cardSetChannel) {
29+
this.stub = CardsetServiceGrpc.newBlockingStub(cardSetChannel);
30+
}
31+
32+
public boolean isCardSetViewable(Long cardSetId, Long userId) {
33+
try {
34+
IsCardSetViewableRequest request = IsCardSetViewableRequest.newBuilder()
35+
.setCardSetId(cardSetId.intValue())
36+
.setUserId(userId.intValue())
37+
.build();
38+
39+
IsCardSetViewableResponse response = stub.isCardSetViewable(request);
40+
return response.getViewable();
41+
} catch (StatusRuntimeException e) {
42+
log.error("gRPC call failed: IsCardSetViewable, cardSetId={}, userId={}", cardSetId, userId, e);
43+
throw new BizException(CommonErrorCode.GRPC_CALL_FAILED);
44+
}
45+
}
46+
47+
public Map<Long, CardSetSummary> getCardSetsByIds(List<Long> cardSetIds, Long userId) {
48+
try {
49+
GetCardSetsByIdsRequest request = GetCardSetsByIdsRequest.newBuilder()
50+
.addAllCardSetIds(cardSetIds)
51+
.setUserId(userId.intValue())
52+
.build();
53+
54+
GetCardSetsByIdsResponse response = stub.getCardSetsByIds(request);
55+
return response.getCardSetsList().stream()
56+
.collect(Collectors.toMap(
57+
cs -> (long) cs.getId(),
58+
Function.identity()
59+
));
60+
} catch (StatusRuntimeException e) {
61+
log.error("gRPC call failed: GetCardSetsByIds, cardSetIds={}, userId={}", cardSetIds, userId, e);
62+
throw new BizException(CommonErrorCode.GRPC_CALL_FAILED);
63+
}
64+
}
65+
}
Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,22 @@
1+
package flipnote.reaction.common.grpc;
2+
3+
import org.springframework.beans.factory.annotation.Value;
4+
import org.springframework.context.annotation.Bean;
5+
import org.springframework.context.annotation.Configuration;
6+
7+
import io.grpc.ManagedChannel;
8+
import io.grpc.ManagedChannelBuilder;
9+
10+
@Configuration
11+
public class GrpcConfig {
12+
13+
@Bean
14+
public ManagedChannel cardSetChannel(
15+
@Value("${grpc.cardset.host}") String host,
16+
@Value("${grpc.cardset.port}") int port
17+
) {
18+
return ManagedChannelBuilder.forAddress(host, port)
19+
.usePlaintext()
20+
.build();
21+
}
22+
}

src/main/java/flipnote/reaction/like/model/response/LikeResponse.java

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,20 @@
22

33
import java.time.LocalDateTime;
44

5+
import cardset.Cardset.CardSetSummary;
56
import flipnote.reaction.like.entity.Like;
67

78
public record LikeResponse(
89
String targetType,
910
Long targetId,
11+
Long targetGroupId,
1012
LocalDateTime likedAt
1113
) {
12-
public static LikeResponse from(Like like) {
14+
public static LikeResponse from(Like like, CardSetSummary summary) {
1315
return new LikeResponse(
1416
like.getTargetType().name(),
1517
like.getTargetId(),
18+
summary != null ? summary.getGroupId() : null,
1619
like.getCreatedAt()
1720
);
1821
}

0 commit comments

Comments
 (0)