Skip to content

Commit

Permalink
refactor: transactional outbox pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
parksey authored Apr 22, 2024
2 parents 21fb037 + 7954ac1 commit a199231
Show file tree
Hide file tree
Showing 40 changed files with 710 additions and 124 deletions.
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

0 comments on commit a199231

Please sign in to comment.