Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
13 commits
Select commit Hold shift + click to select a range
34d4c42
Docs: Spring Kafka 의존성 추가
Joonseok-Lee May 4, 2026
773b1b4
Feat: 동화 생성 요청이 들어올 때, 해당 요청을 DTO로 관리할 수 있게끔 객체 선언
Joonseok-Lee May 4, 2026
fc45113
Feat: Kafka Producer 설정, key - String, value - Json으로 파싱하도록 함. 또한, Ka…
Joonseok-Lee May 4, 2026
0c56107
Feat: KafkaTemplate의 메세지 객체를 동적 다형성으로 설계하기 위해 MessageInterface 인터페이스 …
Joonseok-Lee May 4, 2026
838f4ba
Feat: 동화 생성 요청을 로컬 FastAPI로 전파하기 위해 필요한 값을 메세지로 전파하도록 메세지 객체 설계
Joonseok-Lee May 4, 2026
1fcde03
Feat: Kafka에 메세지를 등록하도록 하는 서비스 객체와 동화 생성 이벤트를 전파하도록 하는 메소드 작성
Joonseok-Lee May 4, 2026
df6059c
Docs: Kafka 테스트를 위해 .env에 Kafka 실제 통신 서버 주소 기록 및 라이브러리를 통해 .env를 직접 읽…
Joonseok-Lee May 6, 2026
d228a43
Test: Kafka 통신 테스트 클래스, 메소드 작성
Joonseok-Lee May 6, 2026
383bcc3
Fix: kakfa 템플릿 제네릭을 선언한 Bean 타입과 일치하도록 수정
Joonseok-Lee May 6, 2026
2a477d3
Docs: Kafka 테스트를 위해 .env 내의 Kafka 경로를 직접 주입받게 하는 의존성 라이브러리를 등록
Joonseok-Lee May 7, 2026
5002fd1
Fix: DI받던 kafkaTemplate의 매개변수를 FairytaleGenerateReq에서 MessageInterfac…
Joonseok-Lee May 7, 2026
8bab140
Feat: 동화 생성 요청이 들어올 시, Kafka에 동화 생성 메세지 등록하는 컨트롤러 메소드를 구현
Joonseok-Lee May 7, 2026
603562e
Docs: 사용하지 않는 import문 최적화
Joonseok-Lee May 7, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,8 @@ dependencies {
implementation 'org.springframework.boot:spring-boot-starter-security'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.kafka:spring-kafka'
testImplementation 'org.springframework.kafka:spring-kafka-test'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
Expand All @@ -40,6 +42,9 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.3'
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.3'

// .env 파일 로딩
implementation 'me.paulschwarz:spring-dotenv:4.0.0'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import lombok.AllArgsConstructor;
import lombok.Getter;

import static com.capstone.kkumteul.global.constant.StaticValue.*;
import static com.capstone.kkumteul.global.constant.StaticValue.NOT_FOUND;

@Getter
@AllArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,13 @@
import com.capstone.kkumteul.domain.fairytale.entity.Island;
import com.capstone.kkumteul.domain.fairytale.service.FairytaleService;
import com.capstone.kkumteul.domain.fairytale.web.dto.FairytaleDetailRes;
import com.capstone.kkumteul.domain.fairytale.web.dto.FairytaleGenerateReq;
import com.capstone.kkumteul.domain.fairytale.web.dto.FairytaleListRes;
import com.capstone.kkumteul.domain.kafka.service.EventService;
import com.capstone.kkumteul.domain.user.entity.User;
import com.capstone.kkumteul.global.response.SuccessResponse;
import com.capstone.kkumteul.global.security.AuthUser;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
Expand All @@ -21,6 +24,19 @@
public class FairytaleController {

private final FairytaleService fairytaleService;
private final EventService eventService;

@PostMapping
public ResponseEntity<SuccessResponse<?>> createFairytale(
@AuthUser User user,
@Valid @RequestBody FairytaleGenerateReq request
) {
eventService.createFairytaleMessageSend(user.getId(), request);

return ResponseEntity
.status(HttpStatus.CREATED)
.body(SuccessResponse.empty());
}

@GetMapping("/my")
public ResponseEntity<SuccessResponse<Page<FairytaleListRes>>> getMyFairytales(
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.capstone.kkumteul.domain.fairytale.web.dto;

import com.capstone.kkumteul.domain.fairytale.entity.Background;
import com.capstone.kkumteul.domain.fairytale.entity.CharSpecies;
import com.capstone.kkumteul.domain.fairytale.entity.Morality;
import jakarta.validation.constraints.NotNull;
import lombok.AllArgsConstructor;
import lombok.Getter;

@Getter
@AllArgsConstructor
public class FairytaleGenerateReq {

@NotNull(message = "배경은 비어있을 수 없습니다.")
private final Background background;

@NotNull(message = "등장인물 종류는 비어있을 수 없습니다.")
private final CharSpecies charSpecies;

@NotNull(message = "교훈은 비어있을 수 없습니다.")
private final Morality morality;

}
Original file line number Diff line number Diff line change
@@ -1,10 +1,9 @@
package com.capstone.kkumteul.domain.game.entity;

import com.capstone.kkumteul.domain.game.exception.InvalidCategoryException;
import lombok.AllArgsConstructor;
import lombok.Getter;

import com.capstone.kkumteul.domain.game.exception.InvalidCategoryException;

import java.util.Arrays;

@Getter
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,8 @@
package com.capstone.kkumteul.domain.game.service;

import com.capstone.kkumteul.domain.fairytale.entity.Fairytale;
import com.capstone.kkumteul.domain.game.entity.GraphEdge;
import com.capstone.kkumteul.domain.game.entity.GraphNode;
import com.capstone.kkumteul.domain.game.entity.NodeCategory;
import com.capstone.kkumteul.domain.game.entity.EdgeChoice;
import com.capstone.kkumteul.domain.game.entity.GameResult;
import com.capstone.kkumteul.domain.fairytale.exception.FairytaleNotFoundException;
import com.capstone.kkumteul.domain.game.entity.*;
import com.capstone.kkumteul.domain.game.exception.*;
import com.capstone.kkumteul.domain.game.repository.EdgeChoiceRepository;
import com.capstone.kkumteul.domain.game.repository.GameResultRepository;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
package com.capstone.kkumteul.domain.kafka.dto;

import com.capstone.kkumteul.domain.fairytale.entity.Background;
import com.capstone.kkumteul.domain.fairytale.entity.CharSpecies;
import com.capstone.kkumteul.domain.fairytale.entity.Morality;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Getter;

@Getter
@Builder
@AllArgsConstructor
public class FairytaleGenerateMessage implements MessageInterface {

private final Long userId;

private final Background background;
private final CharSpecies charSpecies;
private final Morality morality;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package com.capstone.kkumteul.domain.kafka.dto;

public interface MessageInterface {

Long getUserId();
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package com.capstone.kkumteul.domain.kafka.service;

import com.capstone.kkumteul.domain.fairytale.web.dto.FairytaleGenerateReq;
import com.capstone.kkumteul.domain.kafka.dto.FairytaleGenerateMessage;
import com.capstone.kkumteul.domain.kafka.dto.MessageInterface;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.stereotype.Service;

/* 동화 생성 이벤트 전파 */

@Service
@Slf4j(topic = "event")
@RequiredArgsConstructor
public class EventService {

private final KafkaTemplate<String, MessageInterface> kafkaTemplate;

@Value("${FAIRYTALE_GENERATION}")
private String FAIRYTALE_GENERATION;

public void createFairytaleMessageSend(Long userId, FairytaleGenerateReq request) {

FairytaleGenerateMessage message = FairytaleGenerateMessage.builder()
.userId(userId)
.background(request.getBackground())
.charSpecies(request.getCharSpecie())
.morality(request.getMorality())
.build();

log.info("fairytale_generate userId={}, message={}", userId, message);

kafkaTemplate.send(FAIRYTALE_GENERATION, message)
.whenComplete((result, e) -> {
if (e != null) {
log.error("fairytale_generate failed", e);
}
});

}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package com.capstone.kkumteul.global.config;

import com.capstone.kkumteul.domain.kafka.dto.MessageInterface;
import org.apache.kafka.clients.producer.ProducerConfig;
import org.apache.kafka.common.serialization.StringSerializer;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.kafka.core.DefaultKafkaProducerFactory;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.core.ProducerFactory;
import org.springframework.kafka.support.serializer.JsonSerializer;

import java.util.HashMap;
import java.util.Map;

@Configuration
public class KafkaProducerConfig {

private final String KAFKA_URL;

public KafkaProducerConfig(
@Value("${KAFKA_URL}") String KAFKA_URL
) { this.KAFKA_URL = KAFKA_URL; }

@Bean

public ProducerFactory<String, MessageInterface> producerFactory() {

Map<String, Object> config = new HashMap<>();

// key - String Serialization
// value - Json Serialization
config.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG, KAFKA_URL);
config.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG, StringSerializer.class);
config.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, JsonSerializer.class);
config.put(JsonSerializer.ADD_TYPE_INFO_HEADERS, false);

return new DefaultKafkaProducerFactory<>(config);
}

@Bean
public KafkaTemplate<String, MessageInterface> kafkaTemplate() {
return new KafkaTemplate<>(producerFactory());
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,10 +8,10 @@
import org.springframework.validation.BindException;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.multipart.support.MissingServletRequestPartException;
import org.springframework.web.servlet.NoHandlerFoundException;
import org.springframework.web.servlet.resource.NoResourceFoundException;
Expand Down
52 changes: 52 additions & 0 deletions src/test/java/com/capstone/kkumteul/kafka/KafkaProducerTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
package com.capstone.kkumteul.kafka;

import com.capstone.kkumteul.domain.fairytale.entity.Background;
import com.capstone.kkumteul.domain.fairytale.entity.CharSpecies;
import com.capstone.kkumteul.domain.fairytale.entity.Morality;
import com.capstone.kkumteul.domain.kafka.dto.FairytaleGenerateMessage;
import com.capstone.kkumteul.domain.kafka.dto.MessageInterface;
import com.capstone.kkumteul.global.config.KafkaProducerConfig;
import lombok.extern.slf4j.Slf4j;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.kafka.core.KafkaTemplate;
import org.springframework.kafka.support.SendResult;

import java.util.concurrent.CompletableFuture;
import java.util.concurrent.TimeUnit;

import static org.assertj.core.api.Assertions.assertThat;

@Slf4j
@SpringBootTest(
classes = KafkaProducerConfig.class
)
class KafkaProducerTest {

@Autowired
private KafkaTemplate<String, MessageInterface> kafkaTemplate;

@Test
void messagePublicationTest() throws Exception {

FairytaleGenerateMessage message = FairytaleGenerateMessage.builder()
.userId(1L)
.background(Background.FOREST_NATURE)
.charSpecies(CharSpecies.ANIMAL)
.morality(Morality.KINDNESS_REWARDED)
.build();

CompletableFuture<SendResult<String, MessageInterface>> future =
kafkaTemplate.send("fairytale_generate", message);

SendResult<String, MessageInterface> result = future.get(10, TimeUnit.SECONDS);

assertThat(result.getRecordMetadata().topic()).isEqualTo("fairytale_generate");
assertThat(result.getRecordMetadata().offset()).isGreaterThanOrEqualTo(0);

log.info("토픽: {}", result.getRecordMetadata().topic());
log.info("파티션: {}", result.getRecordMetadata().partition());
log.info("오프셋: {}", result.getRecordMetadata().offset());
}
}