-
Notifications
You must be signed in to change notification settings - Fork 17
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
이벤트에 Transactional Outbox Pattern 적용 #757
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,27 @@ | ||
package hanglog.event; | ||
|
||
import java.util.Arrays; | ||
import lombok.AllArgsConstructor; | ||
import org.aspectj.lang.JoinPoint; | ||
import org.aspectj.lang.annotation.AfterReturning; | ||
import org.aspectj.lang.annotation.Aspect; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Aspect | ||
@Component | ||
@AllArgsConstructor | ||
public class CompletedEventChecker { | ||
|
||
private final OutboxRepository outboxRepository; | ||
|
||
@AfterReturning("@annotation(org.springframework.transaction.event.TransactionalEventListener)") | ||
public void check(final JoinPoint joinPoint) { | ||
System.out.println("CompletedEventChecker.check"); | ||
final Event event = Arrays.stream(joinPoint.getArgs()) | ||
.filter(Event.class::isInstance) | ||
.map(Event.class::cast) | ||
.findFirst() | ||
.orElseThrow(() -> new IllegalArgumentException("Event 타입이 아님.")); | ||
outboxRepository.deleteById(event.getOutboxId()); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,21 @@ | ||
package hanglog.event; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import lombok.Getter; | ||
import lombok.RequiredArgsConstructor; | ||
|
||
@Getter | ||
@RequiredArgsConstructor | ||
public class Event { | ||
|
||
@JsonProperty | ||
private Long outboxId; | ||
|
||
@JsonProperty | ||
private final EventType eventType; | ||
|
||
public Event(final Long outboxId, final EventType eventType) { | ||
this.outboxId = outboxId; | ||
this.eventType = eventType; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,39 @@ | ||
package hanglog.event; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import java.util.LinkedList; | ||
import java.util.List; | ||
import java.util.Queue; | ||
import lombok.RequiredArgsConstructor; | ||
import org.springframework.context.ApplicationEventPublisher; | ||
import org.springframework.scheduling.annotation.Scheduled; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
@RequiredArgsConstructor | ||
public class EventQueue { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 오홍 신기합니다!!!! |
||
|
||
private final Queue<Outbox> queue = new LinkedList<>(); | ||
private final OutboxRepository outboxRepository; | ||
private final List<OutboxToEventMapper> mappers; | ||
|
||
private final ApplicationEventPublisher publisher; | ||
|
||
@Scheduled(fixedRate = 2000) | ||
public void offerSavedEvent() { | ||
queue.addAll(outboxRepository.findAll()); | ||
} | ||
Comment on lines
+22
to
+25
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아래 댓글에 한 번에 써둠!!! |
||
|
||
@Scheduled(fixedRate = 2000) | ||
public void pollEvent() throws JsonProcessingException { | ||
if (queue.isEmpty()) { | ||
return; | ||
} | ||
|
||
final Outbox outbox = queue.poll(); | ||
final OutboxToEventMapper outboxToEventMapper = mappers.stream().filter(mapper -> mapper.is(outbox.getEventType())) | ||
.findFirst() | ||
.orElseThrow(() -> new IllegalArgumentException("이벤트 타입에 해당하는 매퍼가 없어요오")); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이대로 놔둘건가요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 레전드 There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. exception code 하나 만드시죠.! |
||
publisher.publishEvent(outboxToEventMapper.toEvent(outbox)); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,13 @@ | ||
package hanglog.event; | ||
|
||
public enum EventType { | ||
|
||
TRIP_DELETE(1), | ||
MEMBER_DELETE(2); | ||
|
||
private final int number; | ||
|
||
EventType(final int number) { | ||
this.number = number; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,47 @@ | ||
package hanglog.event; | ||
|
||
import static jakarta.persistence.EnumType.STRING; | ||
|
||
import com.fasterxml.jackson.annotation.JsonSubTypes.Type; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import hanglog.global.BaseEntity; | ||
import io.hypersistence.utils.hibernate.type.json.JsonType; | ||
import jakarta.persistence.Column; | ||
import jakarta.persistence.Entity; | ||
import jakarta.persistence.Enumerated; | ||
import jakarta.persistence.GeneratedValue; | ||
import jakarta.persistence.GenerationType; | ||
import jakarta.persistence.Id; | ||
import lombok.AccessLevel; | ||
import lombok.Getter; | ||
import lombok.NoArgsConstructor; | ||
import org.hibernate.annotations.SQLDelete; | ||
import org.hibernate.annotations.Where; | ||
|
||
@Getter | ||
@Entity | ||
@NoArgsConstructor(access = AccessLevel.PROTECTED) | ||
@SQLDelete(sql = "UPDATE outbox SET status = 'DELETED' WHERE id = ?") | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아웃박스 테이블도 soft delete를 하는 이유가 있을까요? 본래 아웃박스의 의도와는 조금 달라질 수도 있다는 생각이 들어서요.! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. status가 필요해서 재활용했는데, baseEntity의 status대신 아웃박스의 Status를 별도로 만드는 게 좋을까요?! There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 아 그럼 그대로 가도 될 듯 합니다 |
||
@Where(clause = "status = 'USABLE'") | ||
public class Outbox extends BaseEntity { | ||
|
||
public static final ObjectMapper objectMapper = new ObjectMapper(); | ||
|
||
@Id | ||
@GeneratedValue(strategy = GenerationType.IDENTITY) | ||
private Long id; | ||
|
||
@Column(nullable = false) | ||
@Enumerated(value = STRING) | ||
private EventType eventType; | ||
|
||
@Type(JsonType.class) | ||
@Column(name = "payload", columnDefinition = "json") | ||
private String eventPayload; | ||
|
||
public Outbox(final EventType eventType, final String eventPayload) { | ||
this.id = null; | ||
this.eventType = eventType; | ||
this.eventPayload = eventPayload; | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,5 @@ | ||
package hanglog.event; | ||
|
||
|
||
public @interface OutboxEventHandler { | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,7 @@ | ||
package hanglog.event; | ||
|
||
import org.springframework.data.jpa.repository.JpaRepository; | ||
|
||
public interface OutboxRepository extends JpaRepository<Outbox, Long> { | ||
|
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,9 @@ | ||
package hanglog.event; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
|
||
public interface OutboxToEventMapper<T extends Event> { | ||
|
||
boolean is(EventType type); | ||
T toEvent(Outbox outbox) throws JsonProcessingException; | ||
Comment on lines
+7
to
+8
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 여기 final 못붙이나요 |
||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package hanglog.event; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import com.fasterxml.jackson.databind.ObjectMapper; | ||
import java.io.Serializable; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class PayloadToEventMapper implements Serializable { | ||
|
||
private static final ObjectMapper objectMapper = new ObjectMapper(); | ||
|
||
public static Event toObject(final String payload, final Class<? extends Event> event) throws JsonProcessingException { | ||
return objectMapper.readValue(payload, event); | ||
} | ||
|
||
public static String toJson(final Event event) throws JsonProcessingException { | ||
return objectMapper.writeValueAsString(event); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,26 @@ | ||
package hanglog.event; | ||
|
||
import com.fasterxml.jackson.annotation.JsonProperty; | ||
import java.io.Serializable; | ||
import lombok.Getter; | ||
|
||
@Getter | ||
public class TripDeleteEvent extends Event implements Serializable { | ||
|
||
@JsonProperty | ||
private Long tripId; | ||
|
||
public TripDeleteEvent(final Long outboxId, final EventType eventType, final Long tripId) { | ||
super(outboxId, eventType); | ||
this.tripId = tripId; | ||
} | ||
|
||
public TripDeleteEvent(final Long tripId) { | ||
super(null, EventType.TRIP_DELETE); | ||
this.tripId = tripId; | ||
} | ||
|
||
public TripDeleteEvent() { | ||
super(EventType.TRIP_DELETE); | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
package hanglog.event; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import org.springframework.stereotype.Component; | ||
|
||
@Component | ||
public class TripDeleteEventMapper implements OutboxToEventMapper { | ||
|
||
@Override | ||
public boolean is(final EventType type) { | ||
return type.equals(EventType.TRIP_DELETE); | ||
} | ||
|
||
@Override | ||
public TripDeleteEvent toEvent(final Outbox outbox) throws JsonProcessingException { | ||
final String eventPayload = outbox.getEventPayload(); | ||
final TripDeleteEvent tripDeleteEvent = (TripDeleteEvent) PayloadToEventMapper.toObject(eventPayload, TripDeleteEvent.class); | ||
return new TripDeleteEvent(outbox.getId(), outbox.getEventType(), tripDeleteEvent.getTripId()); | ||
} | ||
} |
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -5,6 +5,12 @@ | |
import static hanglog.global.exception.ExceptionCode.NOT_FOUND_MEMBER_ID; | ||
import static hanglog.global.exception.ExceptionCode.NOT_FOUND_TRIP_ID; | ||
|
||
import com.fasterxml.jackson.core.JsonProcessingException; | ||
import hanglog.event.EventType; | ||
import hanglog.event.Outbox; | ||
import hanglog.event.OutboxRepository; | ||
import hanglog.event.PayloadToEventMapper; | ||
import hanglog.event.TripDeleteEvent; | ||
import hanglog.city.domain.City; | ||
import hanglog.city.domain.repository.CityRepository; | ||
import hanglog.global.exception.AuthException; | ||
|
@@ -17,7 +23,6 @@ | |
import hanglog.trip.domain.PublishEvent; | ||
import hanglog.trip.domain.SharedTrip; | ||
import hanglog.trip.domain.Trip; | ||
import hanglog.trip.domain.TripDeleteEvent; | ||
import hanglog.trip.domain.repository.CustomDayLogRepository; | ||
import hanglog.trip.domain.repository.CustomTripCityRepository; | ||
import hanglog.trip.domain.repository.SharedTripRepository; | ||
|
@@ -56,6 +61,7 @@ public class TripService { | |
private final CustomDayLogRepository customDayLogRepository; | ||
private final CustomTripCityRepository customTripCityRepository; | ||
private final ApplicationEventPublisher publisher; | ||
private final OutboxRepository outboxRepository; | ||
|
||
public void validateTripByMember(final Long memberId, final Long tripId) { | ||
if (!tripRepository.existsByMemberIdAndId(memberId, tripId)) { | ||
|
@@ -186,15 +192,17 @@ private Predicate<DayLog> getDayLogOutOfPeriod(final int currentPeriod, final in | |
return dayLog -> dayLog.getOrdinal() >= requestPeriod + 1 && dayLog.getOrdinal() <= currentPeriod; | ||
} | ||
|
||
public void delete(final Long tripId) { | ||
public void delete(final Long tripId) throws JsonProcessingException { | ||
if (!tripRepository.existsById(tripId)) { | ||
throw new BadRequestException(NOT_FOUND_TRIP_ID); | ||
} | ||
|
||
publisher.publishEvent(new PublishDeleteEvent(tripId)); | ||
sharedTripRepository.deleteByTripId(tripId); | ||
tripRepository.deleteById(tripId); | ||
publisher.publishEvent(new TripDeleteEvent(tripId)); | ||
|
||
final String payload = PayloadToEventMapper.toJson(new TripDeleteEvent(tripId)); | ||
outboxRepository.save(new Outbox(EventType.TRIP_DELETE, payload)); | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 이렇게 되면 결국 삭제 로직에서 DB 접근이 추가적으로 필요한 상황인데 트랜잭션 보장을 위해서 해당 방법이 가장 합리적일까요? 이 외에 다른 방식을 고민해 본게 있을까요? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 일단 외부 db에 저장한다는 것은 필수 조건으로 깔고 갔습니다! 아래 댓글 참고오오오 |
||
} | ||
|
||
private String generateInitialTitle(final List<City> cites) { | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
chk