프로젝트를 데이터 중심 설계에서 책임 주도 설계로 변경해보기(3)

taehee kim·2023년 4월 12일
0

0. 리팩토링 과정에서 느낀점.

  • 데이터 중심 설계를 책임 주도 설계로 변경하여 객체가 같은 메시지에 대해서 각자가 다르게 책임지는 방식으로 코드를 구성하면 변경에 유리한 코드를 작성할 수 있다.
  • 하지만, 실제에서 개발할 때에는 완벽하게 처음부터 객체지향적인 설계를 하는 것은 현실적으로 쉽지 않고 때로는 데이터 중심 설계를 통해 구현하는 것이 간편하고 유리한 경우도 있는 것 같다.
  • 따라서, 책임 주도 설계를 통해 객체지향적으로 코드를 작성하는것을 지향하는 하는 것은 바람직하지만, 너무 이를 집착하면 오히려 시간, 비용면에서 비효율적일 수도 있다는 생각이 들었다.
  • 한번에 완벽한 설계나 코드를 작성하려 하기 보다는 경험에 따라서 미래에 변경이 될 가능성이 많은 부분은 처음부터 신경써서 다형성과 책임을 잘 고려하여 설계하도록 노력하고 그렇지 않은 부분들은 간단하게 구현하고 그때그때 상황에 맞게 리팩토링을 진행해나가는 것이 현실적으로 더 맞는 방법이 아닐까 하는 생각이 들었다.
  • 그리고 Domain Model의 순수한 자바 코드에서의 객체지향 적인 설계를 아무리 잘하더라도 Controller에서 들어오는 외부 요청이나 DAO를 통해 메모리 내의 객체가 Serialize되고 Deserialize되는 과정이 Application에는 포함되어있다. 이렇게 연동되는 부분에서 객체의 순수한 성격이 변형되기 때문에 이러한 부분에서 객체지향적으로 코드를 작성할 수 있도록 하는 기술들도 잘 익혀야 전체 Application code에서 객체지향적인 코드를 작성할 수 있다는 점도 알게 되었다.

1. 리팩토링 하려는 API 기능 설명.

매칭 신청 API

  • 사용자가 매칭 조건을 입력하여 매칭을 신청하면 관련 데이터가 DB에 저장되고 Kafka에 Event를 매칭 알고리즘 수행을 위한 이벤트를 produce한다.
  • Kafka가 Event를 Consume하면 매칭 알고리즘을 수행하여 매칭이 맺어질 수 있는지 확인하고 매칭이 가능하면 사용자에게 알림을 보내준다.

매칭 알고리즘

  • 매칭 신청 내역을 조회하여 같은 조건의 신청이 특정 수 이상만큼 존재하면 매칭을 맺어준다.

RandomMatch

  • 매칭을 신청한 신청 정보가 저장되는 Entity이다.

2. 리팩토링 내용.

RandomMatch Entity의 매칭 조건을 다른 객체로 분리하여 Composition의 형태로 구성한다. Composition 되는 객체인 매칭 조건에 따라 로직이 분기를 활용하지 않고 변경에 유리한 코드를 작성할 수 있다.

기존 코드

  • 기존 RandomMatch는 field에 매칭 조건을 포함한 형태이다.
  • RandomMatch자체를 상속으로 하여 매칭 조건에 따라 필드가 달라지도록 하였다.

@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 {}

기존 코드 문제점

  • RandomMatch 자체에서 타입 계층을 생성하는 것이 큰 문제를 발생시킨다.
    • 타입 계층을 통해 다형성을 활용하고 싶다면 매칭 조건에 대해서 객체를 분리하는 것이 맞을 것이다.
  • RandomMatchService, RandomMatchRepository에서 ContentCategory의 개수만큼 분기가 발생한다.
  • 다른 ContentCategory가 추가될 경우 모든 경우에 분기문이 추가되어야한다.
@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();
    }

리팩토링

  • RandomMatch의 상속을 제거한다.
  • 매칭 조건은 RandomMatchCondition이라는 객체로 분리하고 이를 RandomMatch가 Composition하도록 한다.
  • RandomMatchCondition은 매칭 조건을 비교하고 결과를 반환하는 책임만 수행하면 된다. 따라서 그에 필요한 상태인 매칭 조건과 비교 메서드인 compareTo를 가지고 있다.
@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;
        }
    }
}
  • RandomMatchService에서의 분기가 제거되었다.
@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);
    }

Controller Layer에서 Dto Mapping 시 다형성 활용

기존 코드 문제점

@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<>();

}
  • ContentCategory값에 따라 그 개수 만큼의 복잡한 분기로직이 발생.
    • 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;
          }

수정 코드

  • Dto의 타입을 계층화 하고 @JsonTypeInfo, @JsonSubTypes를 통해 Mapping과정에서 알맞은 객체를 생성하도록 설정함.
  • 기존코드에서의 문제를 다음 Dto들을 정의하는 것으로 모두 해결할 수 있었음.

@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);
    }
}

3. RandomMatchCondition을 타입 계층으로 구성하지 않은 이유

RandomMatchCondition은 타입 계층을 두어 구성하는 것이 객체지향 관점에서는 맞고 분리되어야하는 클래스이다.(오브젝트 책 내용중에 비슷한 사례가 있었다.)

  • 이 증거로는 RandomMatchCondition의 생성메서드를 보면 초기화 하는 필드가 ContentCategory에 따라 나뉘게 되고 초기화 시점이 다르다. 이것은 RandomMatchCondition가 분리되어야 하는 클래스임을 의미한다.
  • 실제로 비지니스 룰 적으로도 ContentCategory에 따라 특정 필드는 아예 사용되지 않거나 사용되어서는 않되고 존재하지 않는 필드여야한다.
  • 또한 ContentCategory라는 값을 가지고 있는 가지고 있는 데이터 중심설계의 성격을 가지고 있다.

그런데 왜 RandomMatchCondition타입 계층을 이루도록 구성하지 않고 데이터 중심 설계를 했을까(ContentCategory라는 특정 필드로 구분하는 형태로 되어있다.)?

  • 현재 로직에서는 RandomMatchCondition은 단지 가지고 있는 매칭 조건을 모두 활용하여 비교결과를 내놓기만 하면된다.
  • null field가 있더라도 Comparator.nullFirst를 활용하면 비교를 하는 것에 아무 문제가 없으며 ContentCategory가 추가되고 이에 따라 RandomMatchCondition의 필드가 증가하더라도 compareTo에 그 부분의 로직을 추가하기만 하면된다.
  • 오히려 이를 클래스를 분리해서 이것을 구성하면 더 객체지향적인 코드라고 볼 수는 있지만 오히려 이와 같이 코드를 구성하려다 보니 변경해야하는 부분이(코드 뿐만 아니라 적어도 table하나는 무조건 추가되어야한다.)적지 않음을 알게 되었고 향후 RandomMatchCondition의 책임이 단순히 비교로 유지될 가능성이 높은 상황에서 타입 계층을 형성하면 복잡성만 증가한다고 판단했다.
  • 객체가 수행하는 책임과 미래에 예상되는 변경을 예상할 수 있는 경우 무리하게 강박적으로 객체지향적인 코드로 만들려고 하기 보다는 유연하게 대처하는 것이 더 효율적일 수 있을 것 같다. 만약, 추후에 변경되어야 하는 이유가 꼭 생긴다면 그때 리팩토링을 해도 늦지 않을 것이다.

RandomMatchCondition을 타입 계층을 둔다면 코드가 어떻게 바뀌어야할까

  • ContentCategory 필드를 제거한다.
  • RandomMatchCondition을 추상 클래스로 두고 하위 클래스들을 정의한다.
  • 기존의 @Embeddable로 되어있는 것을 @Entity로 변경하고 @Inheritance를 활용한다. 이 경우 RandomMatchCondition 테이블이 추가로 생성되어야 한다.

Repository에서 RandomMatchCondition을 ContentCategory에 따라 조회 하고 싶은 경우에 ContentCategory가 없다면 어떻게 조회할 수 있을까?

  • JPQL은 Class type에 따라 where절에 조건을 걸 수 있다. 이를 활용하면 된다.
profile
Fail Fast

0개의 댓글