Skip to content

Commit cd6b2a1

Browse files
committed
Refactor: 리뷰 작성 시 Kafka 이벤트를 발행하도록 추가. Tour Service에서 해당 이벤트를 받아 Open API를 호출하고 결과를 DB에 저장.
1 parent 2e5943f commit cd6b2a1

14 files changed

Lines changed: 496 additions & 14 deletions
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package com.jocketdan.review.config;
2+
3+
import com.jocketdan.review.kafka.OpenApiInfoResponseEvent;
4+
import org.apache.kafka.clients.consumer.ConsumerConfig;
5+
import org.apache.kafka.common.serialization.StringDeserializer;
6+
import org.springframework.beans.factory.annotation.Value;
7+
import org.springframework.context.annotation.Bean;
8+
import org.springframework.context.annotation.Configuration;
9+
import org.springframework.kafka.config.ConcurrentKafkaListenerContainerFactory;
10+
import org.springframework.kafka.core.ConsumerFactory;
11+
import org.springframework.kafka.core.DefaultKafkaConsumerFactory;
12+
import org.springframework.kafka.support.serializer.JsonDeserializer;
13+
14+
import java.util.HashMap;
15+
import java.util.Map;
16+
17+
@Configuration
18+
public class KafkaConsumerConfig {
19+
20+
@Value("${spring.kafka.bootstrap-servers}")
21+
private String bootstrapServers;
22+
23+
@Bean
24+
public ConsumerFactory<String, OpenApiInfoResponseEvent> openApiInfoResponseConsumerFactory() {
25+
Map<String, Object> config = new HashMap<>();
26+
config.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
27+
config.put(ConsumerConfig.GROUP_ID_CONFIG, "review-service-group");
28+
config.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
29+
config.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
30+
config.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, JsonDeserializer.class);
31+
32+
config.put(JsonDeserializer.TRUSTED_PACKAGES, "*");
33+
config.put(JsonDeserializer.VALUE_DEFAULT_TYPE, OpenApiInfoResponseEvent.class.getName());
34+
config.put(JsonDeserializer.USE_TYPE_INFO_HEADERS, false);
35+
36+
return new DefaultKafkaConsumerFactory<>(config);
37+
}
38+
39+
@Bean
40+
public ConcurrentKafkaListenerContainerFactory<String, OpenApiInfoResponseEvent>
41+
openApiInfoResponseKafkaListenerContainerFactory() {
42+
ConcurrentKafkaListenerContainerFactory<String, OpenApiInfoResponseEvent> factory =
43+
new ConcurrentKafkaListenerContainerFactory<>();
44+
factory.setConsumerFactory(openApiInfoResponseConsumerFactory());
45+
return factory;
46+
}
47+
}

review/src/main/java/com/jocketdan/review/config/KafkaProducerConfig.java

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
package com.jocketdan.review.config;
22

33
import com.jocketdan.review.dto.ReviewRatingEvent;
4+
import com.jocketdan.review.kafka.OpenApiInfoRequestEvent;
45
import org.apache.kafka.clients.producer.ProducerConfig;
56
import org.apache.kafka.common.serialization.StringSerializer;
67
import org.springframework.beans.factory.annotation.Value;
@@ -21,7 +22,7 @@ public class KafkaProducerConfig {
2122
private String bootstrapServers;
2223

2324
@Bean
24-
public ProducerFactory<String, ReviewRatingEvent> producerFactory() {
25+
public ProducerFactory<String, ReviewRatingEvent> reviewRatingProducerFactory() {
2526
Map<String, Object> config = new HashMap<>();
2627
config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
2728
config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
@@ -30,7 +31,21 @@ public ProducerFactory<String, ReviewRatingEvent> producerFactory() {
3031
}
3132

3233
@Bean
33-
public KafkaTemplate<String, ReviewRatingEvent> kafkaTemplate() {
34-
return new KafkaTemplate<>(producerFactory());
34+
public KafkaTemplate<String, ReviewRatingEvent> reviewRatingKafkaTemplate() {
35+
return new KafkaTemplate<>(reviewRatingProducerFactory());
36+
}
37+
38+
@Bean
39+
public ProducerFactory<String, OpenApiInfoRequestEvent> openApiInfoRequestProducerFactory() {
40+
Map<String, Object> config = new HashMap<>();
41+
config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, bootstrapServers);
42+
config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
43+
config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
44+
return new DefaultKafkaProducerFactory<>(config);
45+
}
46+
47+
@Bean
48+
public KafkaTemplate<String, OpenApiInfoRequestEvent> openApiInfoRequestKafkaTemplate() {
49+
return new KafkaTemplate<>(openApiInfoRequestProducerFactory());
3550
}
3651
}

review/src/main/java/com/jocketdan/review/dto/ReviewWithPlaceResponseDTO.java

Lines changed: 45 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
package com.jocketdan.review.dto;
22

3+
import com.jocketdan.review.entity.OpenApiInfoCache;
34
import com.jocketdan.review.entity.Review;
45
import com.jocketdan.review.entity.ReviewImage;
56
import lombok.*;
@@ -15,17 +16,58 @@
1516
public class ReviewWithPlaceResponseDTO {
1617

1718
Long id;
18-
String contentId;
1919
String email;
2020
String content;
2121
int rating;
2222
List<String> imageUrls;
2323

24-
public static ReviewWithPlaceResponseDTO from(Review review) {
24+
String contentId;
25+
String placeTitle;
26+
String placeImage;
27+
String contentTypeId;
28+
String addr1;
29+
String addr2;
30+
String areaCode;
31+
String sigunguCode;
32+
33+
public static ReviewWithPlaceResponseDTO from(Review review, OpenApiInfoCache placeInfo) {
2534
List<String> urls = review.getImages().stream()
2635
.map(ReviewImage::getImageUrl)
2736
.toList();
2837

29-
return new ReviewWithPlaceResponseDTO(review.getId(), review.getContentId(), review.getEmail(), review.getContent(), review.getRating(), urls);
38+
if (placeInfo == null) {
39+
return new ReviewWithPlaceResponseDTO(
40+
review.getId(),
41+
review.getEmail(),
42+
review.getContent(),
43+
review.getRating(),
44+
urls,
45+
"[삭제되거나 알 수 없는 장소]",
46+
"[삭제되거나 알 수 없는 장소]",
47+
null,
48+
null,
49+
null,
50+
null,
51+
null,
52+
null
53+
);
54+
}
55+
56+
return new ReviewWithPlaceResponseDTO(
57+
review.getId(),
58+
review.getEmail(),
59+
review.getContent(),
60+
review.getRating(),
61+
urls,
62+
placeInfo.getContentId(),
63+
placeInfo.getTitle(),
64+
placeInfo.getFirstImage(),
65+
placeInfo.getContentTypeId(),
66+
placeInfo.getAddr1(),
67+
placeInfo.getAddr2(),
68+
placeInfo.getAreaCode(),
69+
placeInfo.getSigunguCode()
70+
);
3071
}
72+
3173
}
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
package com.jocketdan.review.entity;
2+
3+
import jakarta.persistence.Entity;
4+
import jakarta.persistence.Id;
5+
import lombok.AccessLevel;
6+
import lombok.AllArgsConstructor;
7+
import lombok.Getter;
8+
import lombok.NoArgsConstructor;
9+
import lombok.experimental.FieldDefaults;
10+
11+
import java.time.LocalDateTime;
12+
13+
@Entity
14+
@Getter
15+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
16+
@AllArgsConstructor
17+
@FieldDefaults(level = AccessLevel.PRIVATE)
18+
public class OpenApiInfoCache {
19+
20+
@Id
21+
String contentId;
22+
23+
String title;
24+
String firstImage;
25+
String contentTypeId;
26+
String addr1;
27+
String addr2;
28+
String areaCode;
29+
String sigunguCode;
30+
31+
LocalDateTime cachedAt;
32+
33+
public void update(String title, String firstImage, String contentTypeId, String addr1, String addr2, String areaCode, String sigunguCode) {
34+
this.title = title;
35+
this.firstImage = firstImage;
36+
this.contentTypeId = contentTypeId;
37+
this.addr1 = addr1;
38+
this.addr2 = addr2;
39+
this.areaCode = areaCode;
40+
this.sigunguCode = sigunguCode;
41+
this.cachedAt = LocalDateTime.now();
42+
}
43+
}
Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,64 @@
1+
package com.jocketdan.review.kafka;
2+
3+
import com.jocketdan.review.entity.OpenApiInfoCache;
4+
import com.jocketdan.review.repository.OpenApiInfoCacheRepository;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.kafka.annotation.KafkaListener;
8+
import org.springframework.stereotype.Component;
9+
import org.springframework.transaction.annotation.Transactional;
10+
11+
import java.time.LocalDateTime;
12+
13+
@Slf4j
14+
@Component
15+
@RequiredArgsConstructor
16+
public class OpenApiInfoCacheEventListener {
17+
18+
private final OpenApiInfoCacheRepository cacheRepository;
19+
20+
@KafkaListener(
21+
topics = "openapi-info-response",
22+
groupId = "review-service-group",
23+
containerFactory = "openApiInfoResponseKafkaListenerContainerFactory"
24+
)
25+
@Transactional
26+
public void handleOpenApiInfoResponse(OpenApiInfoResponseEvent event) {
27+
28+
if (event.isNotFound()) {
29+
log.warn("장소 정보를 찾을 수 없음: contentId={}", event.getContentId());
30+
return;
31+
}
32+
33+
log.info("장소 정보 수신: contentId={}, title={}", event.getContentId(), event.getTitle());
34+
35+
OpenApiInfoCache cache = cacheRepository
36+
.findById(event.getContentId())
37+
.map(existing -> {
38+
existing.update(
39+
event.getTitle(),
40+
event.getFirstImage(),
41+
event.getContentTypeId(),
42+
event.getAddr1(),
43+
event.getAddr2(),
44+
event.getAreaCode(),
45+
event.getSigunguCode()
46+
);
47+
return existing;
48+
})
49+
.orElseGet(() -> new OpenApiInfoCache(
50+
event.getContentId(),
51+
event.getTitle(),
52+
event.getFirstImage(),
53+
event.getContentTypeId(),
54+
event.getAddr1(),
55+
event.getAddr2(),
56+
event.getAreaCode(),
57+
event.getSigunguCode(),
58+
LocalDateTime.now()
59+
));
60+
61+
cacheRepository.save(cache);
62+
log.info("장소 정보 캐시 저장 완료: contentId={}, title={}", cache.getContentId(), cache.getTitle());
63+
}
64+
}
Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
package com.jocketdan.review.kafka;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
7+
@Getter
8+
@NoArgsConstructor
9+
@AllArgsConstructor
10+
public class OpenApiInfoRequestEvent {
11+
String contentId;
12+
}
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package com.jocketdan.review.kafka;
2+
3+
import lombok.AllArgsConstructor;
4+
import lombok.Getter;
5+
import lombok.NoArgsConstructor;
6+
7+
@Getter
8+
@NoArgsConstructor
9+
@AllArgsConstructor
10+
public class OpenApiInfoResponseEvent {
11+
private String contentId;
12+
private String title;
13+
private String firstImage;
14+
private String contentTypeId;
15+
private String addr1;
16+
private String addr2;
17+
private String areaCode;
18+
private String sigunguCode;
19+
private boolean notFound;
20+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package com.jocketdan.review.repository;
2+
3+
import com.jocketdan.review.entity.OpenApiInfoCache;
4+
import org.springframework.data.jpa.repository.JpaRepository;
5+
import org.springframework.data.jpa.repository.Query;
6+
import org.springframework.data.repository.query.Param;
7+
8+
import java.util.List;
9+
10+
public interface OpenApiInfoCacheRepository extends JpaRepository<OpenApiInfoCache, String> {
11+
boolean existsByContentId(String id);
12+
13+
@Query("SELECT o FROM OpenApiInfoCache o WHERE o.contentId IN :contentIds")
14+
List<OpenApiInfoCache> findByContentIdIn(@Param("contentIds") List<String> contentIds);
15+
}

0 commit comments

Comments
 (0)