From feadb3f2557e55faeb8267111fffd0ddc0e8220f Mon Sep 17 00:00:00 2001 From: JiwonDev Date: Fri, 8 Oct 2021 15:23:37 +0900 Subject: [PATCH] =?UTF-8?q?ADD=20Comment=20domain,=20ADD=20Article.existsB?= =?UTF-8?q?yId()=20-=20comment=20=ED=85=8C=EC=8A=A4=ED=8A=B8=EC=9A=A9=20DB?= =?UTF-8?q?=20DML=20=EC=B6=94=EA=B0=80=20-=20Article=20=EB=AA=A8=EB=93=88?= =?UTF-8?q?=EC=97=90=20existsById()=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../yam/app/article/domain/ArticleReader.java | 2 + .../MybatisArticleRepository.java | 9 +- .../domain/ArticleNotFoundException.java | 11 +++ .../com/yam/app/comment/domain/Comment.java | 56 +++++++++++ .../domain/CommentNotFoundException.java | 11 +++ .../app/comment/domain/CommentProcessor.java | 54 +++++++++++ .../yam/app/comment/domain/CommentReader.java | 13 +++ .../app/comment/domain/CommentRepository.java | 11 +++ .../CommentModuleConfiguration.java | 29 ++++++ .../MybatisCommentRepository.java | 63 ++++++++++++ .../mapper/xml/ArticleQueryMapper.xml | 6 ++ .../mapper/xml/CommentCommandMapper.xml | 25 +++++ .../mapper/xml/CommentQueryMapper.xml | 25 +++++ src/main/resources/sql/dml.sql | 3 + .../article/domain/FakeArticleRepository.java | 5 + .../comment/domain/CommentProcessorTest.java | 95 +++++++++++++++++++ .../comment/domain/FakeCommentRepository.java | 53 +++++++++++ .../MybatisCommentRepositoryTest.java | 44 +++++++++ 18 files changed, 513 insertions(+), 2 deletions(-) create mode 100644 src/main/java/com/yam/app/comment/domain/ArticleNotFoundException.java create mode 100644 src/main/java/com/yam/app/comment/domain/Comment.java create mode 100644 src/main/java/com/yam/app/comment/domain/CommentNotFoundException.java create mode 100644 src/main/java/com/yam/app/comment/domain/CommentProcessor.java create mode 100644 src/main/java/com/yam/app/comment/domain/CommentReader.java create mode 100644 src/main/java/com/yam/app/comment/domain/CommentRepository.java create mode 100644 src/main/java/com/yam/app/comment/infrastructure/CommentModuleConfiguration.java create mode 100644 src/main/java/com/yam/app/comment/infrastructure/MybatisCommentRepository.java create mode 100644 src/main/resources/mapper/xml/CommentCommandMapper.xml create mode 100644 src/main/resources/mapper/xml/CommentQueryMapper.xml create mode 100644 src/test/java/com/yam/app/comment/domain/CommentProcessorTest.java create mode 100644 src/test/java/com/yam/app/comment/domain/FakeCommentRepository.java create mode 100644 src/test/java/com/yam/app/comment/infrastructure/MybatisCommentRepositoryTest.java diff --git a/src/main/java/com/yam/app/article/domain/ArticleReader.java b/src/main/java/com/yam/app/article/domain/ArticleReader.java index d33947c..4e1e304 100644 --- a/src/main/java/com/yam/app/article/domain/ArticleReader.java +++ b/src/main/java/com/yam/app/article/domain/ArticleReader.java @@ -7,4 +7,6 @@ public interface ArticleReader { Article findByTitle(String title); Optional
findById(Long articleId); + + boolean existsById(Long articleId); } diff --git a/src/main/java/com/yam/app/article/infrastructure/MybatisArticleRepository.java b/src/main/java/com/yam/app/article/infrastructure/MybatisArticleRepository.java index e05f09e..ced6d7a 100644 --- a/src/main/java/com/yam/app/article/infrastructure/MybatisArticleRepository.java +++ b/src/main/java/com/yam/app/article/infrastructure/MybatisArticleRepository.java @@ -8,9 +8,8 @@ import org.mybatis.spring.SqlSessionTemplate; public final class MybatisArticleRepository implements ArticleReader, ArticleRepository { - private final SqlSessionTemplate template; - private static final String SAVE_FQCN = "com.yam.app.article.domain.ArticleRepository.save"; + private final SqlSessionTemplate template; public MybatisArticleRepository(SqlSessionTemplate template) { this.template = template; @@ -34,4 +33,10 @@ public final class MybatisArticleRepository implements ArticleReader, ArticleRep public Optional
findById(Long articleId) { return template.getMapper(ArticleReader.class).findById(articleId); } + + @Override + public boolean existsById(Long articleId) { + return template.getMapper(ArticleReader.class).existsById(articleId); + } + } diff --git a/src/main/java/com/yam/app/comment/domain/ArticleNotFoundException.java b/src/main/java/com/yam/app/comment/domain/ArticleNotFoundException.java new file mode 100644 index 0000000..be2a65d --- /dev/null +++ b/src/main/java/com/yam/app/comment/domain/ArticleNotFoundException.java @@ -0,0 +1,11 @@ +package com.yam.app.comment.domain; + +import com.yam.app.common.EntityNotFoundException; + +public final class ArticleNotFoundException extends EntityNotFoundException { + + public ArticleNotFoundException(Long id) { + super("Article could not be found, (id : %s)", id); + } +} + diff --git a/src/main/java/com/yam/app/comment/domain/Comment.java b/src/main/java/com/yam/app/comment/domain/Comment.java new file mode 100644 index 0000000..a41dfd6 --- /dev/null +++ b/src/main/java/com/yam/app/comment/domain/Comment.java @@ -0,0 +1,56 @@ +package com.yam.app.comment.domain; + +import static java.time.LocalDateTime.now; + +import com.yam.app.common.EntityStatus; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.EqualsAndHashCode; +import lombok.Getter; +import lombok.NoArgsConstructor; +import lombok.ToString; + +@Getter +@ToString +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@EqualsAndHashCode(of = "id") +public final class Comment { + + private Long id; + private String content; + private LocalDateTime createdAt; + private LocalDateTime modifiedAt; + private EntityStatus status = EntityStatus.ALIVE; + private Long articleId; + private Long memberId; + + public Comment(String content, LocalDateTime createdAt, LocalDateTime modifiedAt, + Long articleId, Long memberId) { + this.content = content; + this.createdAt = createdAt; + this.modifiedAt = modifiedAt; + this.articleId = articleId; + this.memberId = memberId; + } + + public static Comment of(String content, Long articleId, Long memberId) { + return new Comment(content, now(), now(), articleId, memberId); + } + + public void update(String content) { + this.content = content; + this.modifiedAt = now(); + } + + public void delete() { + this.status = EntityStatus.DELETED; + } + + public boolean isAlive() { + return this.status == EntityStatus.ALIVE; + } + + void setId(Long id) { + this.id = id; + } +} diff --git a/src/main/java/com/yam/app/comment/domain/CommentNotFoundException.java b/src/main/java/com/yam/app/comment/domain/CommentNotFoundException.java new file mode 100644 index 0000000..426940f --- /dev/null +++ b/src/main/java/com/yam/app/comment/domain/CommentNotFoundException.java @@ -0,0 +1,11 @@ +package com.yam.app.comment.domain; + +import com.yam.app.common.EntityNotFoundException; + +public final class CommentNotFoundException extends EntityNotFoundException { + + public CommentNotFoundException(Long id) { + super("Comment could not be found, It may have been deleted.(id : %s)", id); + } +} + diff --git a/src/main/java/com/yam/app/comment/domain/CommentProcessor.java b/src/main/java/com/yam/app/comment/domain/CommentProcessor.java new file mode 100644 index 0000000..6aacf80 --- /dev/null +++ b/src/main/java/com/yam/app/comment/domain/CommentProcessor.java @@ -0,0 +1,54 @@ +package com.yam.app.comment.domain; + +import com.yam.app.article.domain.ArticleReader; + +public final class CommentProcessor { + + CommentReader commentReader; + CommentRepository commentRepository; + ArticleReader articleReader; + + public CommentProcessor(CommentReader commentReader, + CommentRepository commentRepository, ArticleReader articleReader) { + this.commentReader = commentReader; + this.commentRepository = commentRepository; + this.articleReader = articleReader; + } + + public void create(String content, Long articleId, Long memberId) { + if (!articleReader.existsById(articleId)) { + throw new ArticleNotFoundException(articleId); + } + + commentRepository.save(Comment.of(content, articleId, memberId)); + } + + public void update(String content, Long commentId) { + Comment comment = commentReader.findById(commentId) + .orElseThrow(() -> new CommentNotFoundException(commentId)); + + if (!articleReader.existsById(comment.getArticleId())) { + throw new ArticleNotFoundException(comment.getArticleId()); + } + + if (!comment.isAlive()) { + throw new CommentNotFoundException(commentId); + } + + comment.update(content); + commentRepository.update(comment); + } + + public void delete(Long commentId) { + Comment comment = commentReader.findById(commentId) + .orElseThrow(() -> new CommentNotFoundException(commentId)); + + if (!articleReader.existsById(comment.getArticleId())) { + throw new ArticleNotFoundException(comment.getArticleId()); + } + + comment.delete(); + commentRepository.delete(comment); + } + +} diff --git a/src/main/java/com/yam/app/comment/domain/CommentReader.java b/src/main/java/com/yam/app/comment/domain/CommentReader.java new file mode 100644 index 0000000..e2f8b54 --- /dev/null +++ b/src/main/java/com/yam/app/comment/domain/CommentReader.java @@ -0,0 +1,13 @@ +package com.yam.app.comment.domain; + +import java.util.List; +import java.util.Optional; + +public interface CommentReader { + + Optional findById(Long commentId); + + List findByArticleId(Long articleId); + + boolean existsById(Long commentId); +} diff --git a/src/main/java/com/yam/app/comment/domain/CommentRepository.java b/src/main/java/com/yam/app/comment/domain/CommentRepository.java new file mode 100644 index 0000000..3d5045e --- /dev/null +++ b/src/main/java/com/yam/app/comment/domain/CommentRepository.java @@ -0,0 +1,11 @@ +package com.yam.app.comment.domain; + +public interface CommentRepository { + + void save(Comment entity); + + void update(Comment entity); + + void delete(Comment entity); + +} diff --git a/src/main/java/com/yam/app/comment/infrastructure/CommentModuleConfiguration.java b/src/main/java/com/yam/app/comment/infrastructure/CommentModuleConfiguration.java new file mode 100644 index 0000000..d5e28c0 --- /dev/null +++ b/src/main/java/com/yam/app/comment/infrastructure/CommentModuleConfiguration.java @@ -0,0 +1,29 @@ +package com.yam.app.comment.infrastructure; + +import com.yam.app.article.domain.ArticleReader; +import com.yam.app.comment.domain.CommentProcessor; +import com.yam.app.comment.domain.CommentReader; +import com.yam.app.comment.domain.CommentRepository; +import org.mybatis.spring.SqlSessionTemplate; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class CommentModuleConfiguration { + + @Bean + public CommentReader commentReader(SqlSessionTemplate template) { + return new MybatisCommentRepository(template); + } + + @Bean + public CommentRepository commentRepository(SqlSessionTemplate template) { + return new MybatisCommentRepository(template); + } + + @Bean + public CommentProcessor commentProcessor(CommentReader commentReader, + CommentRepository commentRepository, ArticleReader articleReader) { + return new CommentProcessor(commentReader, commentRepository, articleReader); + } +} diff --git a/src/main/java/com/yam/app/comment/infrastructure/MybatisCommentRepository.java b/src/main/java/com/yam/app/comment/infrastructure/MybatisCommentRepository.java new file mode 100644 index 0000000..faaf850 --- /dev/null +++ b/src/main/java/com/yam/app/comment/infrastructure/MybatisCommentRepository.java @@ -0,0 +1,63 @@ +package com.yam.app.comment.infrastructure; + +import com.yam.app.comment.domain.Comment; +import com.yam.app.comment.domain.CommentReader; +import com.yam.app.comment.domain.CommentRepository; +import java.util.List; +import java.util.Optional; +import org.mybatis.spring.SqlSessionTemplate; + +public final class MybatisCommentRepository implements CommentReader, CommentRepository { + + private static final String SAVE_FQCN = "com.yam.app.comment.domain.CommentRepository.save"; + private static final String UPDATE_FQCN = "com.yam.app.comment.domain.CommentRepository.update"; + private static final String DELETE_FQCN = "com.yam.app.comment.domain.CommentRepository.delete"; + private final SqlSessionTemplate template; + + public MybatisCommentRepository(SqlSessionTemplate template) { + this.template = template; + } + + @Override + public void save(Comment entity) { + int result = template.insert(SAVE_FQCN, entity); + if (result != 1) { + throw new RuntimeException( + String.format("There was a problem saving the object : %s", entity)); + } + } + + @Override + public void update(Comment entity) { + int result = template.update(UPDATE_FQCN, entity); + if (result != 1) { + throw new RuntimeException( + String.format("There was a problem updating the object : %s", entity)); + } + } + + @Override + public void delete(Comment entity) { + int result = template.update(DELETE_FQCN, entity); + if (result != 1) { + throw new RuntimeException( + String.format("There was a problem soft deleting the object : %s", entity)); + } + } + + @Override + public Optional findById(Long commentId) { + return template.getMapper(CommentReader.class).findById(commentId); + } + + @Override + public List findByArticleId(Long articleId) { + return template.getMapper(CommentReader.class).findByArticleId(articleId); + } + + @Override + public boolean existsById(Long commentId) { + return template.getMapper(CommentReader.class).existsById(commentId); + } + +} diff --git a/src/main/resources/mapper/xml/ArticleQueryMapper.xml b/src/main/resources/mapper/xml/ArticleQueryMapper.xml index aec9b0a..49d96ca 100644 --- a/src/main/resources/mapper/xml/ArticleQueryMapper.xml +++ b/src/main/resources/mapper/xml/ArticleQueryMapper.xml @@ -3,6 +3,12 @@ + + + SELECT * + FROM COMMENT + WHERE id = #{id} + + + + + + + diff --git a/src/main/resources/sql/dml.sql b/src/main/resources/sql/dml.sql index ab5b238..bdd06ac 100644 --- a/src/main/resources/sql/dml.sql +++ b/src/main/resources/sql/dml.sql @@ -27,3 +27,6 @@ insert into article_tag(article_id, tag_id) values (1, 1), (1, 2), (1, 3); + +INSERT INTO comment(content, created_at, modified_at, status, article_id, member_id) +VALUES ('sample content1', now(), now(), 'ALIVE', 1, 1); diff --git a/src/test/java/com/yam/app/article/domain/FakeArticleRepository.java b/src/test/java/com/yam/app/article/domain/FakeArticleRepository.java index 6a6f986..abb4836 100644 --- a/src/test/java/com/yam/app/article/domain/FakeArticleRepository.java +++ b/src/test/java/com/yam/app/article/domain/FakeArticleRepository.java @@ -30,4 +30,9 @@ public final class FakeArticleRepository implements ArticleRepository, ArticleRe entity.setId(idGenerator.incrementAndGet()); data.put(entity.getId(), entity); } + + @Override + public boolean existsById(Long articleId) { + return data.containsKey(articleId); + } } diff --git a/src/test/java/com/yam/app/comment/domain/CommentProcessorTest.java b/src/test/java/com/yam/app/comment/domain/CommentProcessorTest.java new file mode 100644 index 0000000..eda9c92 --- /dev/null +++ b/src/test/java/com/yam/app/comment/domain/CommentProcessorTest.java @@ -0,0 +1,95 @@ +package com.yam.app.comment.domain; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatExceptionOfType; + +import com.yam.app.article.domain.Article; +import com.yam.app.article.domain.FakeArticleRepository; +import java.util.Arrays; +import java.util.Collection; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.DynamicTest; +import org.junit.jupiter.api.TestFactory; + +@DisplayName("댓글 작성 도메인 서비스") +final class CommentProcessorTest { + + @TestFactory + @DisplayName("댓글 시나리오") + Collection comment_scenarios() { + final var fakeCommentRepository = new FakeCommentRepository(); + final var fakeArticleRepository = new FakeArticleRepository(); + final var processor = new CommentProcessor(fakeCommentRepository, + fakeCommentRepository, fakeArticleRepository); + + final var memberId = 1L; + final var articleId = 1L; + final var commentId = 1L; + final var deletedCommentId = 2L; + final var wrongCommentId = 3L; + + fakeArticleRepository.save(Article.write(1L, "title", "content", "image.png")); + fakeArticleRepository.save(Article.write(2L, "title", "content", "image.png")); + fakeCommentRepository.save(Comment.of("sample content", articleId, memberId)); + fakeCommentRepository.save(Comment.of("deleted content", articleId, memberId)); + fakeCommentRepository.save(Comment.of("article not found", 4321L, memberId)); + + return Arrays.asList( + DynamicTest.dynamicTest("댓글 작성에 성공한다.", + () -> { + // Act + processor.create("hello", 2L, memberId); + var comments = fakeCommentRepository.findByArticleId(2L); + + // Assert + assertThat(comments.get(0).getId()).isEqualTo(4L); + }), + DynamicTest.dynamicTest("댓글 수정에 성공한다.", + () -> { + // Act + var modifiedAt = fakeCommentRepository.findById(commentId).get() + .getModifiedAt(); + + processor.update("new content", commentId); + var comment = fakeCommentRepository.findById(commentId).get(); + + // Assert + assertThat(comment.getContent()).isEqualTo("new content"); + assertThat(comment.getModifiedAt().isAfter(modifiedAt)).isTrue(); + }), + DynamicTest.dynamicTest("댓글 작성시 유효한 게시글이 존재하지 않는 경우 예외를 반환한다.", + () -> { + // Act & Assert + assertThatExceptionOfType(ArticleNotFoundException.class) + .isThrownBy(() -> processor.create("sample content", 1234L, memberId)); + }), + DynamicTest.dynamicTest("댓글 수정시 유효한 게시글이 존재하지 않는 경우 예외를 반환한다.", + () -> { + // Act & Assert + assertThatExceptionOfType(ArticleNotFoundException.class) + .isThrownBy(() -> processor.update("new content", wrongCommentId)); + }), + DynamicTest.dynamicTest("댓글 삭제시 유효한 게시글이 존재하지 않는 경우 예외를 반환한다.", + () -> { + // Act & Assert + assertThatExceptionOfType(ArticleNotFoundException.class) + .isThrownBy(() -> processor.delete(wrongCommentId)); + }), + + DynamicTest.dynamicTest("댓글 수정시 유효한 댓글이 존재하지 않는 경우 예외를 반환한다.", + () -> { + // Act & Assert + assertThatExceptionOfType(CommentNotFoundException.class) + .isThrownBy(() -> processor.update("new content", 1234L)); + }), + DynamicTest.dynamicTest("삭제된 댓글을 수정하려고 하는 경우 예외를 반환한다.", + () -> { + // Act & Assert + processor.delete(deletedCommentId); + assertThatExceptionOfType(CommentNotFoundException.class) + .isThrownBy( + () -> processor.update("sample content", deletedCommentId)); + }) + ); + } +} diff --git a/src/test/java/com/yam/app/comment/domain/FakeCommentRepository.java b/src/test/java/com/yam/app/comment/domain/FakeCommentRepository.java new file mode 100644 index 0000000..319d353 --- /dev/null +++ b/src/test/java/com/yam/app/comment/domain/FakeCommentRepository.java @@ -0,0 +1,53 @@ +package com.yam.app.comment.domain; + +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.atomic.AtomicLong; +import java.util.stream.Collectors; + +public final class FakeCommentRepository implements CommentRepository, CommentReader { + + private final Map data = new ConcurrentHashMap<>(); + private final AtomicLong idGenerator = new AtomicLong(); + + @Override + public Optional findById(Long commentId) { + return data.values().stream() + .filter(c -> c.getId().equals(commentId)) + .findAny(); + } + + @Override + public List findByArticleId(Long articleId) { + return data.values().stream() + .filter(c -> c.getArticleId().equals(articleId)) + .collect(Collectors.toList()); + } + + @Override + public boolean existsById(Long commentId) { + return data.containsKey(commentId); + } + + @Override + public void save(Comment entity) { + entity.setId(idGenerator.incrementAndGet()); + data.put(entity.getId(), entity); + } + + @Override + public void update(Comment entity) { + var comment = data.get(entity.getId()); + comment.update(entity.getContent()); + data.put(entity.getId(), comment); + } + + @Override + public void delete(Comment entity) { + var comment = data.get(entity.getId()); + comment.delete(); + data.put(entity.getId(), comment); + } +} diff --git a/src/test/java/com/yam/app/comment/infrastructure/MybatisCommentRepositoryTest.java b/src/test/java/com/yam/app/comment/infrastructure/MybatisCommentRepositoryTest.java new file mode 100644 index 0000000..886a10b --- /dev/null +++ b/src/test/java/com/yam/app/comment/infrastructure/MybatisCommentRepositoryTest.java @@ -0,0 +1,44 @@ +package com.yam.app.comment.infrastructure; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.fail; + +import com.yam.app.comment.domain.CommentReader; +import org.junit.jupiter.api.Disabled; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.ActiveProfiles; + +@SpringBootTest +@ActiveProfiles("test") +@Disabled +final class MybatisCommentRepositoryTest { + + @Autowired + CommentReader commentReader; + + @Test + void findById() { + var comment = commentReader.findById(1L); + if (comment.isPresent()) { + assertThat(comment.get()).isNotNull(); + } else { + fail("Comment Could not find"); + } + + } + + @Test + void findByArticleId() { + var comments = commentReader.findByArticleId(1L); + assertThat(comments).extracting("articleId", Long.class) + .containsOnly(1L); + } + + @Test + void existsById() { + var isPresent = commentReader.existsById(1L); + assertThat(isPresent).isTrue(); + } +}