@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "dtype")
@Table(name = "RANDOM_MATCHES"
, indexes = @Index(name = "idx__created_at", columnList = "createdAt")
)
public abstract class RandomMatch extends BaseEntity implements Serializable {
protected ContentCategory contentCategory;
protected Place place;
}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
//@Table(name = "MEAL_RANDOM_MATCH")
public class MealRandomMatch extends RandomMatch {}
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
//@Table(name = "STUDY_RANDOM_MATCH")
public class StudyRandomMatch extends RandomMatch {}
@Transactional
public void deleteRandomMatch(String username,
RandomMatchCancelRequest request) {
Long memberId = getUserByUsernameOrException(username).getMember().getId();
List<RandomMatch> randomMatches = new ArrayList<>();
LocalDateTime now = LocalDateTime.now();
//생성된 지 RandomMatch.MAX_WAITING_TIME분 이내 + 취소되지 않은 신청 내역 있는지 확인.
ContentCategory contentCategory = request.getContentCategory();
if (contentCategory == ContentCategory.MEAL) {
randomMatches.addAll(
randomMatchRepository.findMealPessimisticWriteByCreatedAtBeforeAndIsExpiredAndMemberId(
now.minusMinutes(RandomMatch.MAX_WAITING_TIME),
memberId, false));
} else if (contentCategory == ContentCategory.STUDY) {
randomMatches.addAll(
randomMatchRepository.findStudyPessimisticWriteByCreatedAtBeforeAndIsExpiredAndMemberId(
now.minusMinutes(RandomMatch.MAX_WAITING_TIME),
memberId, false));
}
// 활성화된 randomMatch가 db에 없으면 취소할 매치가 없는 경우 exception
if (randomMatches.isEmpty()) {
throw new InvalidInputException(ErrorCode.ALREADY_CANCELED_RANDOM_MATCH);
}
//db상에서 만료
randomMatches
.forEach(RandomMatch::expire);
}
public RandomMatchDto readRandomMatchCondition(String username,
RandomMatchSearch randomMatchCancelRequest) {
Member member = getUserByUsernameOrException(username).getMember();
Long memberId = member.getId();
LocalDateTime now = LocalDateTime.now();
List<RandomMatch> randomMatches = new ArrayList<>();
MatchConditionRandomMatchDtoBuilder builder = MatchConditionRandomMatchDto.builder();
if (randomMatchCancelRequest.getContentCategory() == ContentCategory.MEAL) {
randomMatches = randomMatchRepository.findMealByCreatedAtBeforeAndIsExpiredAndMemberId(
now.minusMinutes(RandomMatch.MAX_WAITING_TIME), memberId, false);
builder = builder.wayOfEatingList(new ArrayList<>(randomMatches.stream()
.map(randomMatch -> ((MealRandomMatch) randomMatch).getWayOfEating())
.collect(Collectors.toSet())));
} else if (randomMatchCancelRequest.getContentCategory() == ContentCategory.STUDY) {
randomMatches = randomMatchRepository.findStudyByCreatedAtBeforeAndIsExpiredAndMemberId(
now.minusMinutes(RandomMatch.MAX_WAITING_TIME), memberId, false);
builder = builder.typeOfStudyList(new ArrayList<>(randomMatches.stream()
.map(randomMatch -> ((StudyRandomMatch) randomMatch).getTypeOfStudy())
.collect(Collectors.toSet())));
}
MatchConditionRandomMatchDto matchConditionRandomMatchDto = builder.placeList(
new ArrayList<>(randomMatches.stream()
.map(RandomMatch::getPlace)
.collect(Collectors.toSet())))
.build();
return RandomMatchDto.builder()
.contentCategory(randomMatchCancelRequest.getContentCategory())
.matchConditionRandomMatchDto(matchConditionRandomMatchDto)
.build();
}
RandomMatchCondition이라는 객체로 분리하고 이를 RandomMatch가 Composition하도록 한다.
@Entity
@Table(name = "RANDOM_MATCHES"
, indexes = @Index(name = "idx__created_at", columnList = "createdAt")
)
public class RandomMatch extends BaseEntity implements Serializable {
@Embedded
private RandomMatchCondition randomMatchCondition;
}
@Embeddable
@EqualsAndHashCode
public class RandomMatchCondition implements Comparable<RandomMatchCondition>, Serializable {
private static final long serialVersionUID = 1L;
@Enumerated(value = EnumType.STRING)
@Column(nullable = false, updatable = false)
private ContentCategory contentCategory;
@Enumerated(value = EnumType.STRING)
@Column(nullable = false, updatable = false)
private Place place;
@Builder.Default
@Enumerated(value = EnumType.STRING)
@Column(updatable = false)
private WayOfEating wayOfEating = null;
@Builder.Default
@Enumerated(value = EnumType.STRING)
@Column(updatable = false)
private TypeOfStudy typeOfStudy = null;
public static RandomMatchCondition of(Place place, TypeOfStudy typeOfStudy) {
return RandomMatchCondition.builder()
.contentCategory(ContentCategory.STUDY)
.place(place)
.typeOfStudy(typeOfStudy)
.build();
}
public static RandomMatchCondition of(Place place, WayOfEating wayOfEating) {
return RandomMatchCondition.builder()
.contentCategory(ContentCategory.MEAL)
.place(place)
.wayOfEating(wayOfEating)
.build();
}
@Override
public int compareTo(RandomMatchCondition rmc) {
if (contentCategory != rmc.contentCategory) {
return Comparator.<ContentCategory>nullsFirst(Comparator.naturalOrder())
.compare(contentCategory, rmc.getContentCategory());
} else if (place != rmc.place) {
return Comparator.<Place>nullsFirst(Comparator.naturalOrder())
.compare(place, rmc.getPlace());
} else if (wayOfEating != rmc.wayOfEating) {
return Comparator.<WayOfEating>nullsFirst(Comparator.naturalOrder())
.compare(wayOfEating, rmc.getWayOfEating());
} else if (typeOfStudy != rmc.typeOfStudy) {
return Comparator.<TypeOfStudy>nullsFirst(Comparator.naturalOrder())
.compare(typeOfStudy, rmc.getTypeOfStudy());
} else {
return 0;
}
}
}
@Transactional
public List<RandomMatch> createRandomMatch(String username,
RandomMatchDto randomMatchDto, LocalDateTime now) {
Member member = getUserByUsernameOrException(username).getMember();
//"2020-12-01T00:00:00"
//이미 RandomMatch.MAX_WAITING_TIME분 이내에 랜덤 매칭 신청을 한 경우 인지 체크
verifyAlreadyApplied(randomMatchDto.getContentCategory(), member, now);
//요청 dto로 부터 랜덤 매칭 모든 경우의 수 만들어서 RandomMatch 여러개로 변환
List<RandomMatch> randomMatches = randomMatchDto.makeAllAvailRandomMatchesFromRandomMatchDto(member);
//랜덤 매칭 신청한 것 DB에 기록.
randomMatchRepository.saveAll(randomMatches);
return randomMatches;
}
@Transactional
public void deleteRandomMatch(String username,
RandomMatchCancelRequest request, LocalDateTime now) {
Long memberId = getUserByUsernameOrException(username).getMember().getId();
//생성된 지 RandomMatch.MAX_WAITING_TIME분 이내 + 취소되지 않은 신청 내역 있는지 확인.
List<RandomMatch> randomMatches = randomMatchRepository.findByCreatedAtAfterAndIsExpiredAndMemberIdAndContentCategory(
RandomMatchSearch.builder()
.contentCategory(request.getContentCategory())
.memberId(memberId)
.isExpired(false)
.createdAt(RandomMatch.getValidTime(now))
.build());
// 활성화된 randomMatch가 db에 없으면 취소할 매치가 없는 경우
if (randomMatches.isEmpty()) {
throw new InvalidInputException(ErrorCode.ALREADY_CANCELED_RANDOM_MATCH);
}
randomMatchRepository.bulkUpdateOptimisticLockIsExpiredToTrueByIds(randomMatches.stream()
.map(rm -> RandomMatchBulkUpdateDto.builder()
.id(rm.getId())
.version(rm.getVersion())
.build())
.collect(Collectors.toSet()));
}
public RandomMatchDto readRandomMatchCondition(String username,
RandomMatchParam randomMatchCancelRequest, LocalDateTime now) {
Member member = getUserByUsernameOrException(username).getMember();
Long memberId = member.getId();
List<RandomMatch> randomMatches = randomMatchRepository.findByCreatedAtAfterAndIsExpiredAndMemberIdAndContentCategory(
RandomMatchSearch.builder()
.contentCategory(randomMatchCancelRequest.getContentCategory())
.memberId(memberId)
.isExpired(false)
.createdAt(RandomMatch.getValidTime(now))
.build());
return randomMatchDtoFactory.createRandomMatchDto(
randomMatchCancelRequest.getContentCategory(), randomMatches);
}
@Getter
@Setter
@Builder
@NoArgsConstructor
@AllArgsConstructor(access = AccessLevel.PRIVATE)
public class RandomMatchDto {
@Schema(name = "contentCategory", example = "MEAL or STUDY", description = "식사, 공부 글인지 여부")
@NotNull
private ContentCategory contentCategory;
@Builder.Default
@Schema(name = "place", example = "SEOCHO(서초 클러스터), GAEPO(개포 클러스터), OUT_OF_CLUSTER(클러스터 외부)", description = "앞에 영어를 배열로 보내면 됨.")
private List<Place> placeList = new ArrayList<>();
@Builder.Default
@Schema(name = "timeOfEatingList", example = "BREAKFAST(아침 식사), LUNCH(점심 식사), DUNCH(점저), DINNER(저녁 식사), MIDNIGHT(야식)", description = "앞에 영어를 배열로 보내면 됨.")
private List<TimeOfEating> timeOfEatingList = new ArrayList<>();
@Builder.Default
@Schema(name = "wayOfEatingList", example = " DELIVERY(배달), EATOUT(외식), TAKEOUT(포장)", description = "앞에 영어를 배열로 보내면 됨.")
private List<WayOfEating> wayOfEatingList = new ArrayList<>();
@Builder.Default
@Schema(name = "typeofOfStudyList", example = " INNER_CIRCLE(본 과정), NOT_INNER_CIRCLE(비본 과정)", description = "앞에 영어를 배열로 보내면 됨.")
private List<TypeOfStudy> typeOfStudyList = new ArrayList<>();
}
Validation로직이 모두 달라짐.
@Validated를통해 validation할 수 없는 로직이 생성됨.
private void verifyRandomMatchDtoHasEmptyField(RandomMatchDto randomMatchDto) {
ContentCategory contentCategory = randomMatchDto.getContentCategory();
MatchConditionRandomMatchDto matchConditionRandomMatchDto = randomMatchDto.getMatchConditionRandomMatchDto();
if (matchConditionRandomMatchDto.getPlaceList().isEmpty() ||
(contentCategory == ContentCategory.MEAL && matchConditionRandomMatchDto.getWayOfEatingList().isEmpty()) ||
(contentCategory == ContentCategory.STUDY && matchConditionRandomMatchDto.getTypeOfStudyList().isEmpty())) {
throw new InvalidInputException(ErrorCode.MATCH_CONDITION_EMPTY);
}
}
RandomMatch Entity 조회, 생성로직이 달라지기 때문에 이를 위해 ServiceLayer에 ContentCategory만큼의 분기가 생성되어야함.
/**
* 요청 dto로 부터 랜덤 매칭 모든 경우의 수 만들어서 RandomMatch 여러개로 변환
*
* @param randomMatchDto
* @return
*/
private List<RandomMatch> makeAllAvailRandomMatchesFromRandomMatchDto(
RandomMatchDto randomMatchDto, Member member, LocalDateTime now) {
//아무 matchCondition필드에 값이 없는 경우 모든 조건으로 변환.
List<RandomMatch> randomMatches = new ArrayList<>();
MatchConditionRandomMatchDto matchConditionRandomMatchDto = randomMatchDto.getMatchConditionRandomMatchDto();
if (randomMatchDto.getContentCategory().equals(ContentCategory.STUDY) &&
matchConditionRandomMatchDto.getTypeOfStudyList().isEmpty()) {
matchConditionRandomMatchDto.getTypeOfStudyList()
.addAll(List.of(TypeOfStudy.values()));
} else if (randomMatchDto.getContentCategory().equals(ContentCategory.MEAL) &&
matchConditionRandomMatchDto.getWayOfEatingList().isEmpty()) {
matchConditionRandomMatchDto.getWayOfEatingList()
.addAll(List.of(WayOfEating.values()));
}
// redis 조건에 따라 여러 데이터 생성
for (Place place : matchConditionRandomMatchDto.getPlaceList()) {
if (randomMatchDto.getContentCategory().equals(ContentCategory.STUDY)) {
for (TypeOfStudy typeOfStudy : matchConditionRandomMatchDto.getTypeOfStudyList()) {
randomMatches.add(new StudyRandomMatch(ContentCategory.STUDY,
place, member, typeOfStudy));
}
} else if (randomMatchDto.getContentCategory().equals(ContentCategory.MEAL)) {
for (WayOfEating wayOfEating : matchConditionRandomMatchDto.getWayOfEatingList()) {
randomMatches.add(new MealRandomMatch(ContentCategory.MEAL,
place, member, wayOfEating));
}
}
}
return randomMatches;
}
@Getter
@Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED)
@JsonTypeInfo(
include = JsonTypeInfo.As.EXISTING_PROPERTY,
property = "contentCategory",
use = JsonTypeInfo.Id.NAME,
visible = true
)
@JsonSubTypes({
@JsonSubTypes.Type(value = MealRandomMatchDto.class, name = "MEAL"),
@JsonSubTypes.Type(value = StudyRandomMatchDto.class, name = "STUDY")
})
public abstract class RandomMatchDto {
@Schema(name = "contentCategory", example = "MEAL or STUDY", description = "식사, 공부 글인지 여부")
@NotNull
protected ContentCategory contentCategory;
@Schema(name = "place", example = "SEOCHO(서초 클러스터), GAEPO(개포 클러스터), OUT_OF_CLUSTER(클러스터 외부)", description = "앞에 영어를 배열로 보내면 됨.")
@NotNull
protected List<Place> placeList = new ArrayList<>();
protected void init(){
if (placeList.isEmpty()){
placeList = new ArrayList<>(List.of(Place.values()));
}
}
public abstract List<RandomMatch> makeAllAvailRandomMatchesFromRandomMatchDto(Member member);
public abstract MatchMakingEvent createMatchMakingEvent(LocalDateTime now);
}
@Getter
@Setter
@NoArgsConstructor
@ToString
public class MealRandomMatchDto extends RandomMatchDto{
// @Builder.Default
@Schema(name = "wayOfEatingList", example = " DELIVERY(배달), EATOUT(외식), TAKEOUT(포장)", description = "앞에 영어를 배열로 보내면 됨.")
@NotNull
private List<WayOfEating> wayOfEatingList = new ArrayList<>();
@Builder
private MealRandomMatchDto(ContentCategory contentCategory, List<Place> placeList, List<WayOfEating> wayOfEatingList) {
super(contentCategory, placeList);
this.wayOfEatingList = wayOfEatingList;
}
@Override
protected void init(){
super.init();
if (wayOfEatingList.isEmpty()){
wayOfEatingList = new ArrayList<>(List.of(WayOfEating.values()));
}
}
@Override
public final List<RandomMatch> makeAllAvailRandomMatchesFromRandomMatchDto(Member member){
//아무 matchCondition필드에 값이 없는 경우 모든 조건으로 변환.
this.init();
List<RandomMatch> randomMatches = new ArrayList<>();
for (Place place : placeList) {
for (WayOfEating wayOfEating : wayOfEatingList) {
randomMatches.add(
RandomMatch.of(RandomMatchCondition.of(
place, wayOfEating), member));
}
}
return randomMatches;
}
@Override
public final MatchMakingEvent createMatchMakingEvent(LocalDateTime now){
return new MatchMakingEvent(now, contentCategory, placeList, wayOfEatingList, null);
}
}
@Getter
@Setter
@NoArgsConstructor
public class StudyRandomMatchDto extends RandomMatchDto {
// @Builder.Default
@NotNull
@Schema(name = "typeofOfStudyList", example = " INNER_CIRCLE(본 과정), NOT_INNER_CIRCLE(비본 과정)", description = "앞에 영어를 배열로 보내면 됨.")
private List<TypeOfStudy> typeOfStudyList = new ArrayList<>();
@Builder
private StudyRandomMatchDto(ContentCategory contentCategory, List<Place> placeList, List<TypeOfStudy> typeOfStudyList) {
super(contentCategory, placeList);
this.typeOfStudyList = typeOfStudyList;
}
@Override
protected void init() {
super.init();
if (typeOfStudyList.isEmpty()) {
typeOfStudyList = new ArrayList<>(List.of(TypeOfStudy.values()));
}
}
@Override
public final List<RandomMatch> makeAllAvailRandomMatchesFromRandomMatchDto(Member member) {
//아무 matchCondition필드에 값이 없는 경우 모든 조건으로 변환.
this.init();
List<RandomMatch> randomMatches = new ArrayList<>();
for (Place place : placeList) {
for (TypeOfStudy typeOfStudy : typeOfStudyList) {
randomMatches.add(
RandomMatch.of(RandomMatchCondition.of(
place, typeOfStudy), member));
}
}
return randomMatches;
}
@Override
public final MatchMakingEvent createMatchMakingEvent(LocalDateTime now){
return new MatchMakingEvent(now, contentCategory, placeList, null, typeOfStudyList);
}
}