MVP 기능 구현 완료 이후 프로젝트의 코드를 일괄적으로 튜터님께 리뷰 받았다. 칭찬도 받았지만 지적도 왕창 받았다. 그래서 그 부분에 대해 분석하고 리팩토링 했다.
지적받은 내용은 크게 다음과 같다.
Optional로 반환하는 것은 지양해야 함코드를 전부 가져오기는 많아서 맡은 도메인의 주요 포인트만 작성했다.
@Transactional
public ReplyResponse createReply(
	Long problemId,
	Long discussionId,
	ReplyCreateRequest request,
	Long userId
) {
	User user = userDomainService.getUserById(userId);
	Discussion discussion = discussionDomainService.getDiscussionById(discussionId);
	discussionDomainService.validateProblemMatches(discussion, problemId);
	Reply parent = null;
	if (request.parentReplyId() != null) {
		parent = replyDomainService.getReplyById(request.parentReplyId());
		replyDomainService.validateDiscussionMatches(parent, discussion);
	}
	Reply reply = replyDomainService.createReply(
		ReplyCreateRequest.toEntity(discussion, user, parent, request)
	);
	User discussionAuthor = discussion.getUser();	// TODO: get depth 하나로 줄이기
	User parentAuthor = reply.getParent() != null ? reply.getParent().getUser() : null;
	notify(user, discussionAuthor, reply);
	notify(user, parentAuthor, reply);
	return ReplyResponse.fromEntity(reply);
}
...
@Transactional
public ReplyResponse modifyReply(
	Long problemId,
	Long discussionId,
	Long replyId,
	ReplyModifyRequest request,
	Long userId
) {
	Discussion discussion = discussionDomainService.getDiscussionById(discussionId);
	discussionDomainService.validateProblemMatches(discussion, problemId);
	Reply reply = replyDomainService.getReplyById(replyId);
	replyDomainService.validateDiscussionMatches(reply, discussion);
	replyDomainService.validateIsAuthor(reply, userId);
	replyDomainService.modify(reply, request.content());
	return ReplyResponse.fromEntity(reply);
}@Transactional
public ReplyResponse createReply(
	Long problemId,
	Long discussionId,
	ReplyCreateRequest request,
	Long userId
) {
	User user = userDomainService.getUserById(userId);
	Discussion discussion = discussionDomainService.getDiscussionForProblem(discussionId, problemId);
	Reply reply = replyDomainService.createReply(discussion, user, request.parentReplyId(), request.content());
	List<User> notificationTargets = reply.generateNotificationTargets();
	if (!notificationTargets.isEmpty()) {
		for (User target : notificationTargets) {
			NotificationCreateEvent notificationEvent = replyDomainService.createReplyNotification(target, reply);
			notificationEventService.saveAndNotify(notificationEvent);
		}
	}
	return ReplyResponse.fromEntity(reply);
}
...
@Transactional
public ReplyResponse modifyReply(
	Long problemId,
	Long discussionId,
	Long replyId,
	ReplyModifyRequest request,
	Long userId
) {
	Discussion discussion = discussionDomainService.getDiscussionForProblem(discussionId, problemId);
	Reply reply = replyDomainService.modify(replyId, discussion, userId, request.content());
	return ReplyResponse.fromEntity(reply);
}위와 같이 코드를 개선함으로써, createReply 메서드의 유저 엔티티 조회 - 토론글 엔티티 조회 - 댓글 엔티티 생성 및 저장 - 알림 발행라는 비즈니스 로직 흐름을 쉽게 파악할 수 있게 됐다. 
그리고 중요하지 않은 동작의 코드들은 모두 도메인 서비스에게 위임하면서 가독성도 좋아졌다.
기존에 적용했던 알림 이벤트 객체 생성을 구현한 방식 자체는 잘 만들었지만, 단순히 컨버팅, 매핑 역할만 해야 할 클래스들이 아래처럼 필요 이상의 책임을 가지고 있다고 튜터님이 말씀하셨다.
@Override
public NotificationCreateEvent map(ReplyCreateEvent event) {
	return new NotificationCreateEvent(
		event.principalName(),
		NotificationType.COMMUNITY_REPLY,
		"새로운 댓글이 달렸습니다.",
		new ReplyCreatePayload(event.replyId(), event.discussionId(), event.content()),
		"/redirect",
		false,
		LocalDateTime.now()
	);
}또한 고정된 값들로 필드에 할당하고 있는데, 추후 여러 파라미터에 따라 redirect url, 메시지 등을 수정할 수 있도록 해야 한다.
public NotificationCreateEvent createReplyNotification(User target, Reply reply) {
	ReplyCreatePayload payload = new ReplyCreatePayload(
		reply.getProblemId(),
		reply.getId(),
		reply.getDiscussionId(),
		reply.getContent()
	);
	return NotificationCreateEvent.of(
		target.getEmail(),
		NotificationType.COMMUNITY_REPLY,
		payload
	);
}기존의 mapper, converter 파일들을 삭제하고 알림 이벤트 객체 생성은 각자 도메인 서비스에서 처리하도록 위임했다.
이제 애플리케이션 서비스에서는 도메인 서비스를 통해 알림 이벤트 객체를 생성하고 알림 서비스(port)를 통해 이벤트를 발행한다.
알림 이벤트 enum에 message, redirect url 등의 필드를 추가해 관련 정보를 담을 수 있도록 했다.
@Getter
public enum NotificationType {
	/* 커뮤니티 */
	COMMUNITY_REPLY("댓글"),					// 작성한 discussion, reply, solution 등에 댓글 달림
	COMMUNITY_DISCUSSION_VOTED_UP("자유글 추천"),					// 작성한 discussion, reply, solution 등에 추천 받음
	COMMUNITY_REPLY_VOTED_UP("댓글 추천"),					// 작성한 discussion, reply, solution 등에 추천 받음
	COMMUNITY_MENTIONED("멘션"),				// 누군가 discussion, reply에서 멘션함
	/* 코딩 문제 */
	SUBMISSION_COMPLETED("제출 완료"),	// 제출 및 채점 완료
	;
	private final String description;
	NotificationType(String description) {
		this.description = description;
	}
}
public record NotificationCreateEvent(
	String principalName,
	NotificationType notificationType,
	String message,
	NotificationPayload payload,
	String redirectUrl,
	boolean isRead,
	LocalDateTime createdAt
) {
}@Getter
public enum NotificationType {
	/* 커뮤니티 */
	COMMUNITY_REPLY("새로운 댓글이 달렸습니다.", "/problems/{problemId}/discussions/{discussionId}"),
	COMMUNITY_DISCUSSION_VOTED_UP("토론글에 추천을 받았습니다.", "/problems/{problemId}/discussions/{discussionId}"),
	COMMUNITY_REPLY_VOTED_UP("댓글에 추천을 받았습니다.", "/problems/{problemId}/discussions/{discussionId}/replies/{replyId}"),
	COMMUNITY_MENTIONED("멘션", ""),
	;
	private final String message;
	private final String redirectUrl;
	NotificationType(String message, String redirectUrl) {
		this.message = message;
		this.redirectUrl = redirectUrl;
	}
}
public record NotificationCreateEvent(
	String principalName,
	NotificationType notificationType,
	NotificationPayload payload,
	boolean isRead,
	LocalDateTime createdAt
) {
	public static NotificationCreateEvent of(String principalName, NotificationType notificationType, NotificationPayload payload) {
		return new NotificationCreateEvent(
			principalName,
			notificationType,
			payload,
			false,
			LocalDateTime.now()
		);
	}
}