Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion .github/workflows/deploy.yml
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ permissions:

jobs:
build-deploy:
uses: PARADOX-BSSM/windeath44.manifest.cicd/.github/workflows/ci.yml@master
uses: PARADOX-BSSM/windeath44.manifest.cicd/.github/workflows/release.yml@master
with:
deploy_repo: 'PARADOX-BSSM/windeath44.manifest.values'
deploy_path_prefix: 'prod'
Expand Down
12 changes: 12 additions & 0 deletions src/main/avro/MemorialAppliedAvroSchema.avsc
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
{
"type": "record",
"name": "MemorialAppliedAvroSchema",
"namespace": "windeath44.server.application.avro",
"doc": "Event emitted when a user submits a memorial application.",
"fields": [
{"name": "memorialApplicationId", "type": "long", "doc": "Unique identifier for the memorial application"},
{"name": "applicantId", "type": "string", "doc": "User submitting the application"},
{"name": "characterId", "type": "long", "doc": "Character the memorial is requested for"},
{"name": "content", "type": "string", "doc": "Application content or message"}
]
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.data.domain.Slice;
import org.springframework.stereotype.Component;
import windeath44.server.application.avro.MemorialAppliedAvroSchema;
import windeath44.server.application.avro.MemorialApplicationAvroSchema;

import java.time.LocalDate;
Expand Down Expand Up @@ -47,6 +48,15 @@ public MemorialApplicationAvroSchema toMemorialApplicationAvroSchema(MemorialApp
.build();
}

public MemorialAppliedAvroSchema toMemorialAppliedAvroSchema(MemorialApplication memorialApplication) {
return MemorialAppliedAvroSchema.newBuilder()
.setMemorialApplicationId(memorialApplication.getMemorialApplicationId())
.setApplicantId(memorialApplication.getUserId())
.setCharacterId(memorialApplication.getCharacterId())
.setContent(memorialApplication.getContent())
.build();
}

public List<MemorialApplicationResponse> toMemorialApplicationListResponse(Slice<MemorialApplication> memorialApplicationSlice, String viewerId) {
return memorialApplicationSlice.getContent()
.stream()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import windeath44.server.application.avro.MemorialAppliedAvroSchema;
import windeath44.server.application.avro.MemorialApplicationAvroSchema;
import windeath44.server.memorial.avro.MemorialAvroSchema;

Expand Down Expand Up @@ -43,7 +44,10 @@ public void apply(String userId, MemorialApplicationRequest memorialApplicationR

// 만약 해당 캐릭터가 이미 추모중이라면 apply 실패
grpcClient.validateNotAlreadyMemorialized(characterId);
memorialApplicationRepository.save(memorialApplication);
MemorialApplication savedMemorialApplication = memorialApplicationRepository.save(memorialApplication);

MemorialAppliedAvroSchema memorialAppliedAvroSchema = memorialApplicationMapper.toMemorialAppliedAvroSchema(savedMemorialApplication);
kafkaProducer.send("memorial-apply-request", memorialAppliedAvroSchema);
Comment on lines +47 to +50
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

DB 저장과 Kafka 발행 간 데이터 일관성 고려 필요

apply() 메서드에서 DB 저장 후 Kafka 메시지를 발행합니다. 만약 kafkaProducer.send() 호출이 실패하면 DB에는 레코드가 저장되지만 이벤트는 발행되지 않아 XP가 누락될 수 있습니다.

다음 방안을 고려해 보세요:

  • Outbox 패턴 사용
  • 트랜잭션 내에서 이벤트 저장 후 별도 프로세스로 발행
  • 실패 시 재시도 로직 또는 보상 처리
🤖 Prompt for AI Agents
In
src/main/java/com/example/memorial_application/domain/service/MemorialApplicationCommandService.java
around lines 47–50, the code saves the entity to the DB then directly calls
kafkaProducer.send, which can lead to DB-event inconsistency if sending fails;
instead, persist the event atomically with the entity (Outbox pattern) by adding
an OutboxEvent entity/table and saving the memorialApplication and corresponding
OutboxEvent within the same transaction, then remove the direct
kafkaProducer.send call and implement/trigger a separate reliable dispatcher
that reads unsent OutboxEvent rows, publishes them to Kafka with
retries/backoff, and marks them sent (or use a transactional event log +
background publisher); alternatively implement transactional publish via a
two-phase approach or compensate on failure, but prefer the Outbox pattern for
guaranteed consistency.

}


Expand Down
4 changes: 2 additions & 2 deletions src/main/resources/application.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,8 @@ kafka:
schema.registry.url: ${SCHEMA_REGISTRY_URL}
auto.register.schemas: true
value.subject.name.strategy: io.confluent.kafka.serializers.subject.TopicNameStrategy
consumer:
auto-offset-reset: earliest
consumer:
auto-offset-reset: latest
key-deserializer: org.apache.kafka.common.serialization.StringDeserializer
value-deserializer: io.confluent.kafka.serializers.KafkaAvroDeserializer
specific-avro-reader: true
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import windeath44.server.application.avro.MemorialAppliedAvroSchema;
import windeath44.server.application.avro.MemorialApplicationAvroSchema;
import windeath44.server.memorial.avro.MemorialAvroSchema;

Expand Down Expand Up @@ -51,6 +52,7 @@ class MemorialApplicationApproveServiceTest {
private Long memorialApplicationId;
private MemorialApplication memorialApplication;
private MemorialApplicationAvroSchema memorialApplicationAvroSchema;
private MemorialAppliedAvroSchema memorialAppliedAvroSchema;
private MemorialAvroSchema memorialAvroSchema;
private MemorialApplicationRequest memorialApplicationRequest;

Expand All @@ -62,11 +64,14 @@ void setUp() {
memorialApplicationId = 1L;
memorialApplication = mock(MemorialApplication.class);
memorialApplicationAvroSchema = mock(MemorialApplicationAvroSchema.class);
memorialAppliedAvroSchema = mock(MemorialAppliedAvroSchema.class);
memorialAvroSchema = mock(MemorialAvroSchema.class);
memorialApplicationRequest = new MemorialApplicationRequest(1L, "example");
memorialApplicationRequest = new MemorialApplicationRequest(characterId, content);

when(memorialApplication.getMemorialApplicationId()).thenReturn(memorialApplicationId);
when(memorialApplication.getCharacterId()).thenReturn(characterId);
when(memorialApplication.getUserId()).thenReturn(userId);
when(memorialApplication.getContent()).thenReturn(content);
}

@Test
Expand All @@ -76,12 +81,14 @@ void apply_WhenUserHasNotAppliedForCharacter_ShouldSaveMemorialApplication() {
when(memorialApplicationMapper.toMemorialApplication(userId, characterId, content)).thenReturn(memorialApplication);
when(memorialApplicationRepository.existsByUserIdAndCharacterId(userId, characterId)).thenReturn(false);
doNothing().when(grpcClient).validateNotAlreadyMemorialized(characterId);
when(memorialApplicationMapper.toMemorialAppliedAvroSchema(memorialApplication)).thenReturn(memorialAppliedAvroSchema);

// Act
memorialApplicationApproveService.apply(userId, memorialApplicationRequest);

// Assert
verify(memorialApplicationRepository).save(memorialApplication);
verify(kafkaProducer).send("memorial-apply-request", memorialAppliedAvroSchema);
}

@Test
Expand Down Expand Up @@ -109,7 +116,7 @@ void approve_ById_ShouldSendKafkaMessage() {
memorialApplicationApproveService.approve(memorialApplicationId, userId);

// Assert
verify(kafkaProducer).send("memorial-application-", memorialApplicationAvroSchema);
verify(kafkaProducer).send("memorial-application-approved-request", memorialApplicationAvroSchema);
}

// @Test
Expand Down Expand Up @@ -171,4 +178,4 @@ void findApplicationByUserIdAndCharacterId_WhenApplicationNotFound_ShouldThrowEx
memorialApplicationApproveService.cancel(memorialAvroSchema);
});
}
}
}