추상클래스로 모호한 도메인 구체화시키기

공병주(Chris)·2023년 4월 19일
0

2023 글로벌미디어학부 졸업프로젝트 Dandi에서 알림(Notification)도메인의 불명확함을 해결하기 위해서 고민한 기록입니다.

알림 도메인

알림은 성격에 따라 아래와 같이 구분됩니다.

  • 게시글 좋아요 알림
  • 게시글 댓글 알림
  • 날씨 알림

성격에 따라, 필요한 값들도 다릅니다.

공통적으로 필요한 값

  • pk(id)
  • 어떤 사용자의 알림인지(memberId)
  • 어떤 성격의 알림인지(type)
  • 생성일자(createdAt)

다르게 필요로 하는 값

게시글 좋아요 알림

  • 어떤 게시글에 대한 알림인지(postId)

게시글 댓글 알림

  • 어떤 게시글에 대한 알림인지(postId)
  • 댓글 pk(commentId)
  • 댓글 내용(commentContent)

날씨 알림

  • 어떤 날짜의 날씨인지(weatherDate)

하지만, 이들은 모두 알림이라는 공통점이 있습니다. 또한, 이렇게 다른 성격의 알림을 한번에 응답해줘야 하기 때문에 하나의 객체로 관리해야합니다.

모호한 도메인 객체

public class Notification {

    private final Long id;
    private final Long memberId;
    private final NotificationType type;
    private final LocalDate createdAt;
    private final Long postId;
    private final Long commentId;
    private final Long commentContent;
    private final LocalDate weatherDate;
}

따라서, 하나의 Notification으로 관리하게 되면 위와 같은 형태로 도메인을 설계할 수 있습니다.

이 객체는 상당히 모호하다고 생각했습니다. 객체를 파악할 때, 객체의 이름과 필드 값들로 객체의 특성을 파악한다고 생각합니다. 그런데, 필드의 값을 모두 조합했을 때, 어떤 객체인지 바로 파악이 되지 않는다고 생각합니다.

다른 성격의 필드 값들이 한 곳에 나열되있기 떄문입니다.

계층화로 해결

따라서, 이를 계층화 시켜야겠다는 생각이 들었습니다. 알림들의 특성에 따라 아래와 같이 계층화 시켰습니다.

이전의 Notification에 모든 필드를 다 선언해둔 방식보다 알림이라는 도메인을 더 파악하기 쉬운 구조가 되었다고 생각합니다.

또한, 이전에는 알림의 특성에 따라 null 값들이 할당되는 경우가 많았습니다. 하지만, 추상클래스를 통한 계층화를 시켜서 필요한 필드들만 가지게 하니 null이 할당되는 경우가 없어졌습니다.

PostLikeNotificaion은 PostNotificaion과 다른 필드가 없지만, 그렇다고 PostLikeNotification과 PostCommentNotification은 is의 관계가 아니기 때문에 상하위 타입으로 구조화할 수 없습니다.

문제점

기존에 하나의 Notificaion 객체로 관리했을 때는 persistence 영역에서 NotificationJpaEntity를 Notification으로 변환할 때, 아래와 같이 모든 필드를 다 할당해주면 되었습니다.

public class NotificationPersistenceAdapter {

    // ...

    private Notification convertToNotificaion(NotificationJpaEntity notificationJpaEntity) {
        return new PostCommentNotification(
                notificationJpaEntity.getId(),
                notificationJpaEntity.getMemberId(),
                notificationJpaEntity.getNotificationType(),
                notificationJpaEntity.getCreatedAt().toLocalDate(),
                notificationJpaEntity.isChecked(),
                notificationJpaEntity.getPostId(),
                notificationJpaEntity.commentId(),
                notificationJpaEntity.getCommentContent()
        );
    }
}

하지만, 상속 구조에서는 notificationType에 맞는 타입의 Notification 객체를 아래와 같이 생성해줘야 합니다.

public class NotificationPersistenceAdapter {

    // ...

    private Notification convertToNotificaion(NotificationJpaEntity notificationJpaEntity) {
        if (notificationJpaEntity.hasType(NotificationType.POST_LIKE) {
            // PostLikeNotification 생성 후 반환
        } else if (notificationJpaEntity.hasType(NotificationType.POST_COMMENT) {
            // PostCommentNotification 생성 후 반환
        }
        return //WeatherNotificaion 생성 후 반환
    }
}

이렇게 분기문이 생기는데요. if 분기문이 가독성적으로 좋지 않고 추후에 알림의 종류가 다양해진다면 해당 if문 구문은 한없이 길어지기 때문에, 전략 패턴으로 이를 해결했습니다.

public interface NotificationConvertor {

    boolean canConvert(NotificationJpaEntity notificationJpaEntity);

    Notification convert(NotificationJpaEntity notificationJpaEntity);
}

위와 같은 인터페이스를 선언하고 아래처럼 알림의 유형에 따른 구현체들을 선언해주었습니다.

@Component
public class WeatherNotificationConvertor implements NotificationConvertor {

    @Override
    public boolean canConvert(NotificationJpaEntity notificationJpaEntity) {
        return notificationJpaEntity.hasNotificationType(NotificationType.WEATHER);
    }

    @Override
    public Notification convert(NotificationJpaEntity notificationJpaEntity) {
        return new WeatherNotification(
                notificationJpaEntity.getId(),
                notificationJpaEntity.getMemberId(),
                notificationJpaEntity.getNotificationType(),
                notificationJpaEntity.getCreatedAt().toLocalDate(),
                notificationJpaEntity.isChecked(),
                notificationJpaEntity.getWeatherDate()
        );
    }
}
@Component
public class PostLikeNotificationConvertor implements NotificationConvertor {

    @Override
    public boolean canConvert(NotificationJpaEntity notificationJpaEntity) {
        return notificationJpaEntity.hasNotificationType(NotificationType.POST_LIKE);
    }

    @Override
    public Notification convert(NotificationJpaEntity notificationJpaEntity) {
        return new PostLikeNotification(
                notificationJpaEntity.getId(),
                notificationJpaEntity.getMemberId(),
                notificationJpaEntity.getNotificationType(),
                notificationJpaEntity.getCreatedAt().toLocalDate(),
                notificationJpaEntity.isChecked(),
                notificationJpaEntity.getPostId()
        );
    }
}
@Component
public class PostCommentNotificationConvertor implements NotificationConvertor {

    private final CommentRepository commentRepository;

    public PostCommentNotificationConvertor(CommentRepository commentRepository) {
        this.commentRepository = commentRepository;
    }

    @Override
    public boolean canConvert(NotificationJpaEntity notificationJpaEntity) {
        return notificationJpaEntity.hasNotificationType(NotificationType.COMMENT);
    }

    @Override
    public Notification convert(NotificationJpaEntity notificationJpaEntity) {
        Long commentId = notificationJpaEntity.getCommentId();
        String commentContent = commentRepository.findById(commentId)
                .orElseThrow(() -> InternalServerException.notificationCommentNotFound(commentId))
                .getContent();
        return new PostCommentNotification(
                notificationJpaEntity.getId(),
                notificationJpaEntity.getMemberId(),
                notificationJpaEntity.getNotificationType(),
                notificationJpaEntity.getCreatedAt().toLocalDate(),
                notificationJpaEntity.isChecked(),
                notificationJpaEntity.getPostId(),
                commentId,
                commentContent
        );
    }
}

그리고 아래처럼 여러 NotificationConvertors를 관리하는 객체를 만들고

@Component
public class NotificationConvertors {

    private final List<NotificationConvertor> convertors;

    public NotificationConvertors(PostLikeNotificationConvertor postLikeNotificationConvertor,
                                  PostCommentNotificationConvertor postCommentNotificationConvertor,
                                  WeatherNotificationConvertor weatherNotificationConvertor) {
        this.convertors =
                List.of(postLikeNotificationConvertor, postCommentNotificationConvertor, weatherNotificationConvertor);
    }

    public Notification convert(NotificationJpaEntity notificationJpaEntity) {
        NotificationConvertor notificationConvertor = convertors.stream()
                .filter(convertor -> convertor.canConvert(notificationJpaEntity))
                .findAny()
                .orElseThrow(() ->
                        InternalServerException.notificationConvert(notificationJpaEntity.getNotificationType()));
        return notificationConvertor.convert(notificationJpaEntity);
    }
}

그리고 아래와 같이 NotificationPersistenceAdapter에서 NotificationJpaEntity를 Notification으로 변환할 때 사용했습니다.

@Component
public class NotificationPersistenceAdapter implements NotificationPersistencePort {

    private final NotificationRepository notificationRepository;
    private final NotificationConvertors notificationConvertors;

    public NotificationPersistenceAdapter(NotificationRepository notificationRepository,
                                          NotificationConvertors notificationConvertors) {
        this.notificationRepository = notificationRepository;
        this.notificationConvertors = notificationConvertors;
    }

    // ...

    @Override
    public Slice<Notification> findByMemberId(Long memberId, Pageable pageable) {
        Slice<NotificationJpaEntity> notificationJpaEntities =
                notificationRepository.findByMemberId(memberId, pageable);
        List<Notification> notifications = notificationJpaEntities.stream()
                .map(notificationConvertors::convert)
                .collect(Collectors.toUnmodifiableList());
        return new SliceImpl<>(notifications, pageable, notificationJpaEntities.hasNext());
    }
}
profile
self-motivation

0개의 댓글