Skip to content
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

refactor: transactional outbox pattern #5

Merged
merged 3 commits into from
Apr 22, 2024
Merged
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
3 changes: 3 additions & 0 deletions jtoon-core/core-api/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,9 @@ dependencies {
// OAuth2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

// Jakarta tx
implementation 'org.springframework:spring-tx'

// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.11.5'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package shop.jtoon.event;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;

import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import lombok.RequiredArgsConstructor;
import shop.jtoon.dto.ImagePublishData;
import shop.jtoon.event.domain.ImagePublish;
import shop.jtoon.event.service.EventDomainService;
import shop.jtoon.service.EventRedisService;
import shop.jtoon.webtoon.application.WebtoonClientService;
import shop.jtoon.webtoon.request.ImageEvent;
import shop.jtoon.webtoon.service.WebtoonDomainService;

@Service
@RequiredArgsConstructor
public class EventService {

private final EventDomainService eventDomainService;
private final WebtoonClientService webtoonClientService;
private final EventRedisService eventRedisService;
private final WebtoonDomainService webtoonDomainService;

@Scheduled(cron = "0/10 * * * * *")
@Transactional
public void publish() {
LocalDateTime now = LocalDateTime.now();

List<ImagePublish> publishes = eventDomainService.readRecentEvent(now).stream()
.map(imagePublish -> {
webtoonClientService.upload(ImageEvent.toImageEvent(imagePublish.getImagePayload()).toImageUpload());
imagePublish.updateStatus();

return imagePublish;
})
.toList();

eventDomainService.update(publishes);
}

@Scheduled(cron = "0/10 * * * * *")
@Transactional
public void eventExecute() {
List<Long> webtoonIds = new ArrayList<>();
List<ImagePublish> publishes = eventRedisService.consume().stream()
.parallel()
.map(imagePublishData -> updateEvent(imagePublishData, webtoonIds))
.filter(Objects::nonNull)
.map(ImageEvent::toImagePublish)
.toList();

eventDomainService.update(publishes);
webtoonDomainService.updateWebtoonStatus(webtoonIds);
}

private ImageEvent updateEvent(ImagePublishData imagePublishData, List<Long> wetoonIds) {
ImageEvent imageEvent = ImageEvent.toImageEvent(imagePublishData);

try {
webtoonClientService.upload(imageEvent.toImageUpload());
wetoonIds.add(imagePublishData.id());

return null;
} catch (Exception e) {
return imageEvent;
}
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package shop.jtoon.webtoon.application;

import static shop.jtoon.common.ImageType.*;
import static shop.jtoon.type.ErrorStatus.*;

import java.util.List;

Expand All @@ -10,17 +9,13 @@
import org.springframework.web.multipart.MultipartFile;

import lombok.RequiredArgsConstructor;

import shop.jtoon.dto.ImageUploadEvent;
import shop.jtoon.dto.MultiImageEvent;
import shop.jtoon.exception.InvalidRequestException;

import shop.jtoon.webtoon.request.MultiImageEvent;
import shop.jtoon.webtoon.domain.EpisodeMainInfo;
import shop.jtoon.webtoon.domain.EpisodeSchema;
import shop.jtoon.webtoon.entity.Webtoon;
import shop.jtoon.webtoon.presentation.WebtoonImageUploadEventListener;
import shop.jtoon.webtoon.request.CreateEpisodeReq;
import shop.jtoon.webtoon.request.GetEpisodesReq;
import shop.jtoon.webtoon.request.ImageEvent;
import shop.jtoon.webtoon.request.MultiImagesReq;
import shop.jtoon.webtoon.response.EpisodeInfoRes;
import shop.jtoon.webtoon.response.EpisodeItemRes;
Expand All @@ -44,18 +39,18 @@ public void createEpisode(
Webtoon webtoon = episodeDomainService.readWebtoon(webtoonId, memberId, request.no());

MultiImageEvent mainUploadEvents = MultiImageEvent.builder()
.imageUploadEvents(mainImages.toMultiImageEvent(request, webtoon.getTitle()))
.imageEvents(mainImages.toMultiImageEvent(request, webtoon.getTitle()))
.build();
List<String> mainUrls = mainUploadEvents.imageUploadEvents().stream()
.map(webtoonClientService::uploadUrl)
List<String> mainUrls = mainUploadEvents.imageEvents().stream()
.map(imageEvent -> webtoonClientService.parseUrl(imageEvent.toImageUpload()))
.toList();

ImageUploadEvent thumbnailUploadEvent = request.toUploadImageDto(
ImageEvent thumbnailUploadEvent = request.toUploadImageDto(
EPISODE_THUMBNAIL,
webtoon.getTitle(),
thumbnailImage
).toImageUploadEvent();
String thumbnailUrl = webtoonClientService.upload(thumbnailUploadEvent);
).toImageEvent();
String thumbnailUrl = webtoonClientService.parseUrl(thumbnailUploadEvent.toImageUpload());

EpisodeSchema episode = request.toEpisodeSchema();
episodeDomainService.createEpisode(episode, webtoon, mainUrls, thumbnailUrl);
Expand Down
Original file line number Diff line number Diff line change
@@ -1,34 +1,22 @@
package shop.jtoon.webtoon.application;



import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import lombok.RequiredArgsConstructor;
import shop.jtoon.common.ImageType;
import shop.jtoon.dto.ImageUploadEvent;
import shop.jtoon.dto.UploadImageDto;
import shop.jtoon.dto.ImageUpload;
import shop.jtoon.service.S3Service;
import shop.jtoon.webtoon.request.CreateWebtoonReq;

@Service
@RequiredArgsConstructor
public class WebtoonClientService {

private final S3Service s3Service;

public String upload(ImageType imageType, CreateWebtoonReq request, MultipartFile thumbnailImage) {
UploadImageDto uploadImageDto = request.toUploadImageDto(imageType, thumbnailImage);

return s3Service.uploadImage(uploadImageDto);
}

public String uploadUrl(ImageUploadEvent imageUpload) {
public String parseUrl(ImageUpload imageUpload) {
return s3Service.uploadUrl(imageUpload.key());
}

public String upload(ImageUploadEvent imageUpload) {
public ImageUpload upload(ImageUpload imageUpload) {
return s3Service.uploadImage(imageUpload);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,16 +5,16 @@
import java.util.List;
import java.util.Map;

import org.springframework.context.ApplicationEventPublisher;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

import lombok.RequiredArgsConstructor;
import shop.jtoon.dto.ImageUploadEvent;
import shop.jtoon.service.EventRedisService;
import shop.jtoon.webtoon.domain.WebtoonDetail;
import shop.jtoon.webtoon.entity.enums.DayOfWeek;
import shop.jtoon.webtoon.request.CreateWebtoonReq;
import shop.jtoon.webtoon.request.GetWebtoonsReq;
import shop.jtoon.webtoon.request.ImageEvent;
import shop.jtoon.webtoon.response.WebtoonInfoRes;
import shop.jtoon.webtoon.response.WebtoonItemRes;
import shop.jtoon.webtoon.service.WebtoonDomainService;
Expand All @@ -25,20 +25,19 @@ public class WebtoonService {

private final WebtoonClientService webtoonClientService;
private final WebtoonDomainService webtoonDomainService;
private final ApplicationEventPublisher publisher;
private final EventRedisService eventRedisService;

public void createWebtoon(Long memberId, MultipartFile thumbnailImage, CreateWebtoonReq request) {
ImageUploadEvent imageUploadEvent = request.toUploadImageDto(WEBTOON_THUMBNAIL, thumbnailImage)
.toImageUploadEvent();
String thumbnailUrl = webtoonClientService.uploadUrl(imageUploadEvent);
ImageEvent imageEvent = request.toUploadImageDto(WEBTOON_THUMBNAIL, thumbnailImage)
.toImageEvent();
String thumbnailUrl = webtoonClientService.parseUrl(imageEvent.toImageUpload());

webtoonDomainService.validateDuplicateTitle(request.title());
webtoonDomainService.createWebtoon(memberId,
Long webtoonId = webtoonDomainService.createWebtoon(memberId,
request.toWebtoonInfo(thumbnailUrl),
request.toWebtoonGenres(),
request.toWebtoonDayOfWeeks());

publisher.publishEvent(imageUploadEvent);
eventRedisService.publish(imageEvent.imagePublishData(webtoonId));
}

public Map<DayOfWeek, List<WebtoonItemRes>> getWebtoons(GetWebtoonsReq request) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,10 @@
import org.springframework.stereotype.Component;

import lombok.RequiredArgsConstructor;
import shop.jtoon.dto.ImageUploadEvent;
import shop.jtoon.dto.MultiImageEvent;
import shop.jtoon.dto.ImageUpload;
import shop.jtoon.global.util.AsyncEventListener;
import shop.jtoon.webtoon.application.WebtoonClientService;
import shop.jtoon.webtoon.request.MultiImageEvent;

@Component
@RequiredArgsConstructor
Expand All @@ -15,15 +15,14 @@ public class WebtoonImageUploadEventListener {
private final WebtoonClientService webtoonClientService;

@AsyncEventListener
public void uploadImage(ImageUploadEvent imageUploadEvent) {
webtoonClientService.upload(imageUploadEvent);
public void uploadImage(ImageUpload imageUpload) {
webtoonClientService.upload(imageUpload);
}

@AsyncEventListener
public void uploadMultiImages(MultiImageEvent multiImageEvent) {
multiImageEvent.imageUploadEvents().stream()
multiImageEvent.imageEvents().stream()
.parallel()
.forEach(webtoonClientService::upload);
.forEach(imageEvent -> webtoonClientService.upload(imageEvent.toImageUpload()));
}

}
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,7 @@
import lombok.Builder;
import shop.jtoon.common.FileName;
import shop.jtoon.common.ImageType;
import shop.jtoon.dto.ImageUploadEvent;
import shop.jtoon.dto.UploadImageDto;
import shop.jtoon.webtoon.domain.EpisodeSchema;
import shop.jtoon.webtoon.entity.Episode;
import shop.jtoon.webtoon.entity.Webtoon;

@Builder
public record CreateEpisodeReq(
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
package shop.jtoon.webtoon.request;

import java.util.List;
import java.util.Set;

import org.springframework.web.multipart.MultipartFile;
Expand All @@ -11,13 +10,9 @@
import lombok.Builder;
import shop.jtoon.common.FileName;
import shop.jtoon.common.ImageType;
import shop.jtoon.dto.UploadImageDto;
import shop.jtoon.webtoon.domain.WebtoonDayOfWeeks;
import shop.jtoon.webtoon.domain.WebtoonGenres;
import shop.jtoon.webtoon.domain.WebtoonInfo;
import shop.jtoon.webtoon.entity.DayOfWeekWebtoon;
import shop.jtoon.webtoon.entity.GenreWebtoon;
import shop.jtoon.webtoon.entity.Webtoon;
import shop.jtoon.webtoon.entity.enums.AgeLimit;
import shop.jtoon.webtoon.entity.enums.DayOfWeek;
import shop.jtoon.webtoon.entity.enums.Genre;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package shop.jtoon.webtoon.request;

import java.time.LocalDateTime;

import lombok.Builder;
import shop.jtoon.dto.ImagePublishData;
import shop.jtoon.dto.ImageUpload;
import shop.jtoon.event.domain.ImagePayload;
import shop.jtoon.event.domain.ImagePublish;
import shop.jtoon.event.entity.EventStatus;

@Builder
public record ImageEvent(
String key,
byte[] data
) {

public static ImageEvent toImageEvent(ImagePayload imagePayload) {
return ImageEvent.builder()
.key(imagePayload.key())
.data(imagePayload.data())
.build();
}

public static ImageEvent toImageEvent(ImagePublishData imagePublishData) {
return ImageEvent.builder()
.key(imagePublishData.key())
.data(imagePublishData.data())
.build();
}

public ImageUpload toImageUpload() {
return ImageUpload.builder()
.key(key)
.data(data)
.build();
}

public ImagePayload toImagePayload() {
return ImagePayload.builder()
.key(key)
.data(data)
.build();
}

public ImagePublish toImagePublish() {
return ImagePublish.builder()
.eventStatus(EventStatus.READY)
.imagePayload(this.toImagePayload())
.publishDate(LocalDateTime.now())
.build();
}

public ImagePublishData imagePublishData(Long id) {
return ImagePublishData.builder()
.id(id)
.key(key)
.data(data)
.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package shop.jtoon.dto;
package shop.jtoon.webtoon.request;

import java.util.List;

import lombok.Builder;

@Builder
public record MultiImageEvent(
List<ImageUploadEvent> imageUploadEvents
List<ImageEvent> imageEvents
) {

}
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,15 @@
import org.springframework.web.multipart.MultipartFile;

import lombok.Builder;
import shop.jtoon.dto.ImageUploadEvent;

@Builder
public record MultiImagesReq(
List<MultipartFile> mainImages
) {

public List<ImageUploadEvent> toMultiImageEvent(CreateEpisodeReq request, String webtoonTitle) {
public List<ImageEvent> toMultiImageEvent(CreateEpisodeReq request, String webtoonTitle) {
return mainImages.stream()
.map(mainImage -> request.toUploadImageDto(EPISODE_MAIN, webtoonTitle, mainImage).toImageUploadEvent())
.map(mainImage -> request.toUploadImageDto(EPISODE_MAIN, webtoonTitle, mainImage).toImageEvent())
.toList();
}
}
Loading
Loading