From e711510e28fda9b3888699d63936312e6a76811e Mon Sep 17 00:00:00 2001 From: jiwon Date: Fri, 8 Aug 2025 22:32:28 +0900 Subject: [PATCH 1/4] =?UTF-8?q?refactor:=20post=5Ftag=20bulk=20insert=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=ED=95=98=EB=8F=84=EB=A1=9D=20=EC=88=98?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jiwon/mylog/domain/post/entity/Post.java | 2 +- .../domain/post/service/PostService.java | 32 ++++- .../posttag/PostTagJdbcRepository.java | 37 ++++++ .../{ => posttag}/PostTagRepository.java | 2 +- .../repository/{ => tag}/TagRepository.java | 5 +- .../{ => tag}/TagRepositoryCustom.java | 2 +- .../{ => tag}/TagRepositoryImpl.java | 2 +- .../mylog/domain/tag/service/TagService.java | 55 ++++----- .../common/config/JDBCTemplateConfig.java | 15 +++ .../global/schedular/CountScheduler.java | 2 +- .../domain/post/service/PostServiceTest.java | 2 +- .../tag/repository/PostTagBatchTest.java | 109 ++++++++++++++++++ 12 files changed, 220 insertions(+), 45 deletions(-) create mode 100644 src/main/java/com/jiwon/mylog/domain/tag/repository/posttag/PostTagJdbcRepository.java rename src/main/java/com/jiwon/mylog/domain/tag/repository/{ => posttag}/PostTagRepository.java (82%) rename src/main/java/com/jiwon/mylog/domain/tag/repository/{ => tag}/TagRepository.java (89%) rename src/main/java/com/jiwon/mylog/domain/tag/repository/{ => tag}/TagRepositoryCustom.java (81%) rename src/main/java/com/jiwon/mylog/domain/tag/repository/{ => tag}/TagRepositoryImpl.java (97%) create mode 100644 src/main/java/com/jiwon/mylog/global/common/config/JDBCTemplateConfig.java create mode 100644 src/test/java/com/jiwon/mylog/domain/tag/repository/PostTagBatchTest.java diff --git a/src/main/java/com/jiwon/mylog/domain/post/entity/Post.java b/src/main/java/com/jiwon/mylog/domain/post/entity/Post.java index 0454f4c..55e0542 100644 --- a/src/main/java/com/jiwon/mylog/domain/post/entity/Post.java +++ b/src/main/java/com/jiwon/mylog/domain/post/entity/Post.java @@ -79,7 +79,7 @@ public class Post extends BaseEntity { private List images = new ArrayList<>(); @Builder.Default - @OneToMany(mappedBy = "post", cascade = CascadeType.ALL, orphanRemoval = true) + @OneToMany(mappedBy = "post") private List postTags = new ArrayList<>(); @Builder.Default diff --git a/src/main/java/com/jiwon/mylog/domain/post/service/PostService.java b/src/main/java/com/jiwon/mylog/domain/post/service/PostService.java index fb637cc..bbe3c58 100644 --- a/src/main/java/com/jiwon/mylog/domain/post/service/PostService.java +++ b/src/main/java/com/jiwon/mylog/domain/post/service/PostService.java @@ -10,6 +10,8 @@ import com.jiwon.mylog.domain.post.dto.response.PostDetailResponse; import com.jiwon.mylog.domain.post.dto.response.PostNavigationResponse; import com.jiwon.mylog.domain.post.dto.response.RelatedPostResponse; +import com.jiwon.mylog.domain.tag.entity.PostTag; +import com.jiwon.mylog.domain.tag.repository.posttag.PostTagJdbcRepository; import com.jiwon.mylog.global.common.entity.PageResponse; import com.jiwon.mylog.domain.post.dto.response.PostSummaryResponse; import com.jiwon.mylog.domain.post.entity.Post; @@ -22,7 +24,8 @@ import com.jiwon.mylog.domain.category.repository.CategoryRepository; import com.jiwon.mylog.domain.user.repository.UserRepository; -import java.time.LocalDateTime; +import jakarta.persistence.EntityManager; +import java.util.Comparator; import java.util.List; import com.jiwon.mylog.domain.tag.service.TagService; @@ -43,8 +46,10 @@ @Service public class PostService { + private final EntityManager em; private final ApplicationEventPublisher eventPublisher; private final TagService tagService; + private final PostTagJdbcRepository postTagJdbcRepository; private final PostRepository postRepository; private final UserRepository userRepository; private final CategoryRepository categoryRepository; @@ -63,15 +68,23 @@ public class PostService { public PostDetailResponse createPost(Long userId, PostRequest postRequest) { User user = getUserById(userId); Category category = getCategoryById(userId, postRequest.getCategoryId()); - List tags = tagService.getTagsById(user, postRequest.getTagRequests()); + List tags = tagService.getOrCreateTags(user, postRequest.getTagRequests()); Post post = Post.create(postRequest, user, category, tags); Post savedPost = postRepository.save(post); + if (tags != null && !tags.isEmpty()) { + List postTags = tags.stream() + .map(tag -> PostTag.createPostTag(savedPost, tag)) + .toList(); + postTagJdbcRepository.saveAll(postTags); + } + increaseRelatedPostInfo(category, tags); eventPublisher.publishEvent(new PostCreatedEvent(userId, savedPost.getId(), savedPost.getCreatedAt())); - return PostDetailResponse.fromPost(savedPost); + return postRepository.findPostDetail(savedPost.getId()) + .orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_POST)); } @Caching( @@ -91,11 +104,14 @@ public PostDetailResponse updatePost(Long userId, Long postId, PostRequest postR decreaseRelatedPostInfo(post); Category category = getCategoryById(userId, postRequest.getCategoryId()); - List tags = tagService.getTagsById(post.getUser(), postRequest.getTagRequests()); + List tags = tagService.getOrCreateTags(post.getUser(), postRequest.getTagRequests()); increaseRelatedPostInfo(category, tags); post.update(postRequest, category, tags); - return PostDetailResponse.fromPost(post); + em.flush(); + + return postRepository.findPostDetail(post.getId()) + .orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_POST)); } @Caching(evict = { @@ -127,7 +143,11 @@ private void increaseRelatedPostInfo(Category category, List tags) { if (category != null) { category.incrementUsage(); } - tags.forEach(Tag::incrementUsage); + + List sortedTags = tags.stream() + .sorted(Comparator.comparing(Tag::getId)) + .toList(); + sortedTags.forEach(Tag::incrementUsage); } /** diff --git a/src/main/java/com/jiwon/mylog/domain/tag/repository/posttag/PostTagJdbcRepository.java b/src/main/java/com/jiwon/mylog/domain/tag/repository/posttag/PostTagJdbcRepository.java new file mode 100644 index 0000000..2ea7d3e --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/tag/repository/posttag/PostTagJdbcRepository.java @@ -0,0 +1,37 @@ +package com.jiwon.mylog.domain.tag.repository.posttag; + +import com.jiwon.mylog.domain.tag.entity.PostTag; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Repository +public class PostTagJdbcRepository { + + private final JdbcTemplate jdbcTemplate; + + @Transactional + public void saveAll(List postTags) { + jdbcTemplate.batchUpdate("insert into post_tag (post_id, tag_id) values (?, ?)", + new BatchPreparedStatementSetter() { + + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + PostTag postTag = postTags.get(i); + ps.setLong(1, postTag.getPost().getId()); + ps.setLong(2, postTag.getTag().getId()); + } + + @Override + public int getBatchSize() { + return postTags.size(); + } + }); + } +} diff --git a/src/main/java/com/jiwon/mylog/domain/tag/repository/PostTagRepository.java b/src/main/java/com/jiwon/mylog/domain/tag/repository/posttag/PostTagRepository.java similarity index 82% rename from src/main/java/com/jiwon/mylog/domain/tag/repository/PostTagRepository.java rename to src/main/java/com/jiwon/mylog/domain/tag/repository/posttag/PostTagRepository.java index dbee9f3..c1d0dd0 100644 --- a/src/main/java/com/jiwon/mylog/domain/tag/repository/PostTagRepository.java +++ b/src/main/java/com/jiwon/mylog/domain/tag/repository/posttag/PostTagRepository.java @@ -1,4 +1,4 @@ -package com.jiwon.mylog.domain.tag.repository; +package com.jiwon.mylog.domain.tag.repository.posttag; import com.jiwon.mylog.domain.tag.entity.PostTag; import org.springframework.data.jpa.repository.JpaRepository; diff --git a/src/main/java/com/jiwon/mylog/domain/tag/repository/TagRepository.java b/src/main/java/com/jiwon/mylog/domain/tag/repository/tag/TagRepository.java similarity index 89% rename from src/main/java/com/jiwon/mylog/domain/tag/repository/TagRepository.java rename to src/main/java/com/jiwon/mylog/domain/tag/repository/tag/TagRepository.java index 4aed817..4f8e4b4 100644 --- a/src/main/java/com/jiwon/mylog/domain/tag/repository/TagRepository.java +++ b/src/main/java/com/jiwon/mylog/domain/tag/repository/tag/TagRepository.java @@ -1,6 +1,5 @@ -package com.jiwon.mylog.domain.tag.repository; +package com.jiwon.mylog.domain.tag.repository.tag; -import com.jiwon.mylog.domain.category.entity.Category; import com.jiwon.mylog.domain.tag.entity.Tag; import com.jiwon.mylog.domain.user.entity.User; import java.util.List; @@ -18,6 +17,8 @@ public interface TagRepository extends JpaRepository, TagRepositoryCu @Query("select t from Tag t where t.user.id = :userId") List findAllByUserId(@Param("userId") Long userId); + Optional findByUserAndName(User user, String name); + @Modifying(clearAutomatically = true) @Query("update Tag t set t.usageCount = (select coalesce(count(pt.id), 0) from PostTag pt where pt.tag.id = t.id and pt.post.deletedAt is null)") void updateAllPostCounts(); diff --git a/src/main/java/com/jiwon/mylog/domain/tag/repository/TagRepositoryCustom.java b/src/main/java/com/jiwon/mylog/domain/tag/repository/tag/TagRepositoryCustom.java similarity index 81% rename from src/main/java/com/jiwon/mylog/domain/tag/repository/TagRepositoryCustom.java rename to src/main/java/com/jiwon/mylog/domain/tag/repository/tag/TagRepositoryCustom.java index 5ea17b1..08e16a7 100644 --- a/src/main/java/com/jiwon/mylog/domain/tag/repository/TagRepositoryCustom.java +++ b/src/main/java/com/jiwon/mylog/domain/tag/repository/tag/TagRepositoryCustom.java @@ -1,4 +1,4 @@ -package com.jiwon.mylog.domain.tag.repository; +package com.jiwon.mylog.domain.tag.repository.tag; import com.jiwon.mylog.global.common.entity.PageResponse; import org.springframework.data.domain.Pageable; diff --git a/src/main/java/com/jiwon/mylog/domain/tag/repository/TagRepositoryImpl.java b/src/main/java/com/jiwon/mylog/domain/tag/repository/tag/TagRepositoryImpl.java similarity index 97% rename from src/main/java/com/jiwon/mylog/domain/tag/repository/TagRepositoryImpl.java rename to src/main/java/com/jiwon/mylog/domain/tag/repository/tag/TagRepositoryImpl.java index dcd4845..37e5988 100644 --- a/src/main/java/com/jiwon/mylog/domain/tag/repository/TagRepositoryImpl.java +++ b/src/main/java/com/jiwon/mylog/domain/tag/repository/tag/TagRepositoryImpl.java @@ -1,4 +1,4 @@ -package com.jiwon.mylog.domain.tag.repository; +package com.jiwon.mylog.domain.tag.repository.tag; import com.jiwon.mylog.global.common.entity.PageResponse; import com.jiwon.mylog.domain.tag.dto.response.TagCountResponse; diff --git a/src/main/java/com/jiwon/mylog/domain/tag/service/TagService.java b/src/main/java/com/jiwon/mylog/domain/tag/service/TagService.java index 1b3411f..2811b7b 100644 --- a/src/main/java/com/jiwon/mylog/domain/tag/service/TagService.java +++ b/src/main/java/com/jiwon/mylog/domain/tag/service/TagService.java @@ -4,16 +4,16 @@ import com.jiwon.mylog.domain.tag.dto.request.TagRequest; import com.jiwon.mylog.domain.tag.dto.response.TagResponse; import com.jiwon.mylog.domain.tag.entity.Tag; -import com.jiwon.mylog.domain.tag.repository.TagRepository; +import com.jiwon.mylog.domain.tag.repository.tag.TagRepository; import com.jiwon.mylog.domain.user.entity.User; -import java.util.Collection; import java.util.List; import java.util.stream.Collectors; -import java.util.stream.Stream; import lombok.RequiredArgsConstructor; +import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @@ -36,39 +36,32 @@ public PageResponse getAllTagsWithCount(Long userId, Pageable pageable) { } @Transactional - public List getTagsById(User user, List tagRequests) { - + public List getOrCreateTags(User user, List tagRequests) { List names = tagRequests.stream() .map(TagRequest::getName) .distinct() - .collect(Collectors.toList()); - - // 이미 존재하는 태그 - List existingTags = tagRepository.findAllByUserAndNameIn(user, names); - - List existNames = existingTags.stream() - .map(Tag::getName) - .collect(Collectors.toList()); + .toList(); - // 새로운 태그 - List createdTags = names.stream() - .filter(name -> !existNames.contains(name)) - .map(name -> createTag(user, name)) - .collect(Collectors.toList()); - - tagRepository.saveAll(createdTags); - - return Stream.of(existingTags, createdTags) - .flatMap(Collection::stream) - .collect(Collectors.toList()); + return names.stream() + .map(name -> findOrCreateTag(user, name)) + .toList(); } - private Tag createTag(User user, String tagName) { - Tag tag = Tag.builder() - .name(tagName) - .usageCount(0L) - .user(user) - .build(); - return tag; + @Transactional(propagation = Propagation.REQUIRES_NEW) + public Tag findOrCreateTag(User user, String name) { + return tagRepository.findByUserAndName(user, name) + .orElseGet(() -> { + try { + Tag tag = Tag.builder() + .name(name) + .usageCount(0L) + .user(user) + .build(); + return tagRepository.save(tag); + } catch (DataIntegrityViolationException e) { + return tagRepository.findByUserAndName(user, name) + .orElseThrow(() -> new IllegalStateException("Tag should exist but not found: " + name)); + } + }); } } diff --git a/src/main/java/com/jiwon/mylog/global/common/config/JDBCTemplateConfig.java b/src/main/java/com/jiwon/mylog/global/common/config/JDBCTemplateConfig.java new file mode 100644 index 0000000..a1507a5 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/global/common/config/JDBCTemplateConfig.java @@ -0,0 +1,15 @@ +package com.jiwon.mylog.global.common.config; + +import javax.sql.DataSource; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcTemplate; + +@Configuration +public class JDBCTemplateConfig { + + @Bean + public JdbcTemplate jdbcTemplate(DataSource dataSource) { + return new JdbcTemplate(dataSource); + } +} diff --git a/src/main/java/com/jiwon/mylog/global/schedular/CountScheduler.java b/src/main/java/com/jiwon/mylog/global/schedular/CountScheduler.java index df32e0e..d164d61 100644 --- a/src/main/java/com/jiwon/mylog/global/schedular/CountScheduler.java +++ b/src/main/java/com/jiwon/mylog/global/schedular/CountScheduler.java @@ -1,7 +1,7 @@ package com.jiwon.mylog.global.schedular; import com.jiwon.mylog.domain.category.repository.CategoryRepository; -import com.jiwon.mylog.domain.tag.repository.TagRepository; +import com.jiwon.mylog.domain.tag.repository.tag.TagRepository; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.scheduling.annotation.Scheduled; diff --git a/src/test/java/com/jiwon/mylog/domain/post/service/PostServiceTest.java b/src/test/java/com/jiwon/mylog/domain/post/service/PostServiceTest.java index 2b1c6be..89ceec0 100644 --- a/src/test/java/com/jiwon/mylog/domain/post/service/PostServiceTest.java +++ b/src/test/java/com/jiwon/mylog/domain/post/service/PostServiceTest.java @@ -12,7 +12,7 @@ import com.jiwon.mylog.domain.tag.dto.request.TagRequest; import com.jiwon.mylog.domain.tag.entity.PostTag; import com.jiwon.mylog.domain.tag.entity.Tag; -import com.jiwon.mylog.domain.tag.repository.TagRepository; +import com.jiwon.mylog.domain.tag.repository.tag.TagRepository; import com.jiwon.mylog.domain.tag.service.TagService; import com.jiwon.mylog.domain.user.entity.User; import com.jiwon.mylog.domain.user.repository.UserRepository; diff --git a/src/test/java/com/jiwon/mylog/domain/tag/repository/PostTagBatchTest.java b/src/test/java/com/jiwon/mylog/domain/tag/repository/PostTagBatchTest.java new file mode 100644 index 0000000..d9b9b59 --- /dev/null +++ b/src/test/java/com/jiwon/mylog/domain/tag/repository/PostTagBatchTest.java @@ -0,0 +1,109 @@ +package com.jiwon.mylog.domain.tag.repository; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +import com.jiwon.mylog.config.EmbeddedRedisConfig; +import com.jiwon.mylog.domain.post.entity.Post; +import com.jiwon.mylog.domain.tag.entity.PostTag; +import com.jiwon.mylog.domain.tag.entity.Tag; +import com.jiwon.mylog.domain.tag.repository.posttag.PostTagJdbcRepository; +import java.util.ArrayList; +import java.util.List; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.context.annotation.Import; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.test.context.ActiveProfiles; +import org.springframework.transaction.support.TransactionTemplate; + +@Import(EmbeddedRedisConfig.class) +@ActiveProfiles("test") +@SpringBootTest +class PostTagBatchTest { + + @Autowired + TransactionTemplate tx; + + @Autowired + JdbcTemplate jdbcTemplate; + + @Autowired + PostTagJdbcRepository postTagJdbcRepository; + + private static final Long[] TAGS = {1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L}; + + @BeforeEach + void clean() { + jdbcTemplate.update("TRUNCATE TABLE post_tag"); + } + + private List buildPostTags(Long postId, int tagCount) { + List list = new ArrayList<>(tagCount); + for (int i = 0; i < tagCount; i++) { + Post post = Post.builder().id(postId).build(); + Tag tag = Tag.builder().id(TAGS[i]).build(); + + PostTag pt = new PostTag(); + pt.setPost(post); + pt.setTag(tag); + list.add(pt); + } + return list; + } + + private long measureMillis(Runnable r) { + long start = System.nanoTime(); + r.run(); + return (System.nanoTime() - start) / 1_000_000; + } + + private void insertIndividually(List postTags) { + final String sql = "INSERT INTO post_tag (post_id, tag_id) VALUES (?, ?)"; + for (PostTag pt : postTags) { + jdbcTemplate.update(sql, pt.getPost().getId(), pt.getTag().getId()); + } + } + + private void insertBatch(List postTags) { + postTagJdbcRepository.saveAll(postTags); + } + + @Test + void compare_single_post_with_max_tags() { + final int TAG_COUNT = 10; + final Long POST_ID = 1L; + + List postTags = buildPostTags(POST_ID, TAG_COUNT); + long ms1 = measureMillis(() -> tx.executeWithoutResult(s -> insertIndividually(postTags))); + + jdbcTemplate.update("DELETE FROM post_tag WHERE post_id = ?", POST_ID); + + List postTags2 = buildPostTags(POST_ID, TAG_COUNT); + long ms2 = measureMillis(() -> tx.executeWithoutResult(s -> insertBatch(postTags2))); + + System.out.println("단일 포스트 10개 태그"); + System.out.printf("개별 INSERT: %d ms%n", ms1); + System.out.printf("배치 INSERT: %d ms%n", ms2); + + assertTrue(ms1 > 0 && ms2 > 0); + } + + @ParameterizedTest + @ValueSource(ints = {1, 3, 5, 7, 10}) + void test_different_tag_counts(int tagCount) { + List postTags = buildPostTags(1L, tagCount); + + long ms1 = measureMillis(() -> tx.executeWithoutResult(s -> insertIndividually(postTags))); + jdbcTemplate.update("DELETE FROM post_tag WHERE post_id = 1"); + + List postTags2 = buildPostTags(1L, tagCount); + long ms2 = measureMillis(() -> tx.executeWithoutResult(s -> insertBatch(postTags2))); + jdbcTemplate.update("DELETE FROM post_tag WHERE post_id = 1"); + + System.out.printf("태그 %d개: 개별=%dms, 배치=%dms%n", tagCount, ms1, ms2); + } +} \ No newline at end of file From abe1ba60ad2c9d003fb6dc54177c9d090167575d Mon Sep 17 00:00:00 2001 From: jiwon Date: Sat, 9 Aug 2025 12:35:21 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=EC=A2=8B=EC=95=84=EC=9A=94=20delete?= =?UTF-8?q?=20=EC=9E=98=EB=AA=BB=EB=90=9C=20=EC=BA=90=EC=8A=A4=ED=8C=85=20?= =?UTF-8?q?=EB=B2=84=EA=B7=B8=20=ED=95=B4=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../mylog/domain/like/LikeNotificationDetails.java | 8 ++++++++ .../com/jiwon/mylog/domain/like/LikeRepository.java | 12 +++++------- .../com/jiwon/mylog/domain/like/LikeService.java | 8 +++----- 3 files changed, 16 insertions(+), 12 deletions(-) create mode 100644 src/main/java/com/jiwon/mylog/domain/like/LikeNotificationDetails.java diff --git a/src/main/java/com/jiwon/mylog/domain/like/LikeNotificationDetails.java b/src/main/java/com/jiwon/mylog/domain/like/LikeNotificationDetails.java new file mode 100644 index 0000000..ed6de08 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/like/LikeNotificationDetails.java @@ -0,0 +1,8 @@ +package com.jiwon.mylog.domain.like; + +import java.time.LocalDateTime; + +public interface LikeNotificationDetails { + Long getReceiverId(); + LocalDateTime getCreatedAt(); +} diff --git a/src/main/java/com/jiwon/mylog/domain/like/LikeRepository.java b/src/main/java/com/jiwon/mylog/domain/like/LikeRepository.java index 2b0d5cf..0c7e4fe 100644 --- a/src/main/java/com/jiwon/mylog/domain/like/LikeRepository.java +++ b/src/main/java/com/jiwon/mylog/domain/like/LikeRepository.java @@ -6,18 +6,16 @@ import org.springframework.data.repository.query.Param; import org.springframework.stereotype.Repository; -import java.util.Objects; - @Repository public interface LikeRepository extends JpaRepository { boolean existsByUserIdAndPostId(Long userId, Long postId); - @Query(value = "SELECT p.user_id, l.created_at " + - "FROM likes l " + - "INNER JOIN posts p ON l.post_id = p.id " + - "WHERE l.user_id = :userId AND l.post_id = :postId", + @Query(value = "select p.user_id as receiverId, l.created_at as createdAt " + + "from likes l " + + "inner join post p on l.post_id = p.id " + + "where l.user_id = :userId and p.id = :postId", nativeQuery = true) - Object[] findLikeDetails(@Param("userId") Long userId, @Param("postId") Long postId); + LikeNotificationDetails findLikeNotificationDetails(@Param("userId") Long userId, @Param("postId") Long postId); @Modifying @Query(value = "insert ignore into likes(user_id, post_id) values(:userId, :postId)", nativeQuery = true) diff --git a/src/main/java/com/jiwon/mylog/domain/like/LikeService.java b/src/main/java/com/jiwon/mylog/domain/like/LikeService.java index 6bf20a8..07a4120 100644 --- a/src/main/java/com/jiwon/mylog/domain/like/LikeService.java +++ b/src/main/java/com/jiwon/mylog/domain/like/LikeService.java @@ -20,7 +20,6 @@ import org.springframework.transaction.annotation.Transactional; import java.time.LocalDateTime; -import java.util.Objects; @RequiredArgsConstructor @@ -63,9 +62,8 @@ public void deleteLike(Long userId, Long postId) { validateUserExists(userId); validatePostExists(postId); - Object[] likeDetails = likeRepository.findLikeDetails(userId, postId); - Long receiverId = ((Number) likeDetails[0]).longValue(); - LocalDateTime createdAt = (LocalDateTime) likeDetails[1]; + LikeNotificationDetails likeDetails = likeRepository.findLikeNotificationDetails(userId, postId); + Long receiverId = likeDetails.getReceiverId(); likeRepository.deleteLike(userId, postId); @@ -75,7 +73,7 @@ public void deleteLike(Long userId, Long postId) { postId, receiverId, userId, - createdAt + likeDetails.getCreatedAt() ) ); } From 8cbd8f63b9f055e3fde12772ff1a8f21ace5a738 Mon Sep 17 00:00:00 2001 From: jiwon Date: Sat, 9 Aug 2025 16:46:48 +0900 Subject: [PATCH 3/4] =?UTF-8?q?refactor:=20Like=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EC=A0=95=EB=A6=AC=20=EB=B0=8F?= =?UTF-8?q?=20base,=20interface=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../event/NotificationEventListener.java | 6 +- .../domain/event/UserStatsEventListener.java | 4 +- .../event/dto/like/LikeCreatedEvent.java | 5 +- .../event/dto/like/LikeDeletedEvent.java | 5 +- .../domain/like/LikeNotificationDetails.java | 8 -- .../mylog/domain/like/LikeRepository.java | 30 ------- .../like/{ => controller}/LikeController.java | 24 ++--- .../mylog/domain/like/entity/BaseLike.java | 34 ++++++++ .../like/{Like.java => entity/PostLike.java} | 40 +++------ .../like/repository/PostLikeRepository.java | 23 +++++ .../domain/like/service/LikeService.java | 12 +++ .../PostLikeService.java} | 87 ++++++++----------- 12 files changed, 141 insertions(+), 137 deletions(-) delete mode 100644 src/main/java/com/jiwon/mylog/domain/like/LikeNotificationDetails.java delete mode 100644 src/main/java/com/jiwon/mylog/domain/like/LikeRepository.java rename src/main/java/com/jiwon/mylog/domain/like/{ => controller}/LikeController.java (66%) create mode 100644 src/main/java/com/jiwon/mylog/domain/like/entity/BaseLike.java rename src/main/java/com/jiwon/mylog/domain/like/{Like.java => entity/PostLike.java} (50%) create mode 100644 src/main/java/com/jiwon/mylog/domain/like/repository/PostLikeRepository.java create mode 100644 src/main/java/com/jiwon/mylog/domain/like/service/LikeService.java rename src/main/java/com/jiwon/mylog/domain/like/{LikeService.java => service/PostLikeService.java} (55%) diff --git a/src/main/java/com/jiwon/mylog/domain/event/NotificationEventListener.java b/src/main/java/com/jiwon/mylog/domain/event/NotificationEventListener.java index f1539ea..4744eee 100644 --- a/src/main/java/com/jiwon/mylog/domain/event/NotificationEventListener.java +++ b/src/main/java/com/jiwon/mylog/domain/event/NotificationEventListener.java @@ -51,9 +51,9 @@ public void handleCommentCreated(CommentCreatedEvent event) { public void handleLikeCreated(LikeCreatedEvent event) { try { handleNotification( - event.postWriterId(), - event.likeWriterName() + "왹이 외계 수집물에 푸 딩을 달았다!", - "/posts/" + event.postId(), + event.receiverId(), + event.senderName() + "왹이 외계 수집물에 푸 딩을 달았다!", + "/posts/" + event.targetId(), NotificationType.LIKE ); } catch (Exception e) { diff --git a/src/main/java/com/jiwon/mylog/domain/event/UserStatsEventListener.java b/src/main/java/com/jiwon/mylog/domain/event/UserStatsEventListener.java index eaadeb3..a1089ad 100644 --- a/src/main/java/com/jiwon/mylog/domain/event/UserStatsEventListener.java +++ b/src/main/java/com/jiwon/mylog/domain/event/UserStatsEventListener.java @@ -55,7 +55,7 @@ public void handleLikeCreated(LikeCreatedEvent event) { if (!validateIsToday(event.createdAt())) return; try { - userStatsService.updateReceivedLikes(event.postWriterId(), 1); + userStatsService.updateReceivedLikes(event.receiverId(), 1); } catch (Exception e) { log.warn("좋아요 생성 이벤트 통계 처리 실패: {}", event, e); } @@ -66,7 +66,7 @@ public void handleLikeDeleted(LikeDeletedEvent event) { if (!validateIsToday(event.createdAt())) return; try { - userStatsService.updateReceivedLikes(event.postWriterId(), -1); + userStatsService.updateReceivedLikes(event.receiverId(), -1); } catch (Exception e) { log.warn("좋아요 삭제 이벤트 통계 처리 실패: {}", event, e); } diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/like/LikeCreatedEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/like/LikeCreatedEvent.java index 92a816a..194da6e 100644 --- a/src/main/java/com/jiwon/mylog/domain/event/dto/like/LikeCreatedEvent.java +++ b/src/main/java/com/jiwon/mylog/domain/event/dto/like/LikeCreatedEvent.java @@ -3,6 +3,9 @@ import java.time.LocalDateTime; public record LikeCreatedEvent( - Long postId, Long postWriterId, Long likeWriterId, String likeWriterName, + Long targetId, + Long receiverId, + Long senderId, + String senderName, LocalDateTime createdAt) { } diff --git a/src/main/java/com/jiwon/mylog/domain/event/dto/like/LikeDeletedEvent.java b/src/main/java/com/jiwon/mylog/domain/event/dto/like/LikeDeletedEvent.java index 27c0f2c..eb60d5d 100644 --- a/src/main/java/com/jiwon/mylog/domain/event/dto/like/LikeDeletedEvent.java +++ b/src/main/java/com/jiwon/mylog/domain/event/dto/like/LikeDeletedEvent.java @@ -3,5 +3,8 @@ import java.time.LocalDateTime; public record LikeDeletedEvent( - Long postId, Long postWriterId, Long likeWriterId, LocalDateTime createdAt) { + Long targetId, + Long receiverId, + Long senderId, + LocalDateTime createdAt) { } diff --git a/src/main/java/com/jiwon/mylog/domain/like/LikeNotificationDetails.java b/src/main/java/com/jiwon/mylog/domain/like/LikeNotificationDetails.java deleted file mode 100644 index ed6de08..0000000 --- a/src/main/java/com/jiwon/mylog/domain/like/LikeNotificationDetails.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.jiwon.mylog.domain.like; - -import java.time.LocalDateTime; - -public interface LikeNotificationDetails { - Long getReceiverId(); - LocalDateTime getCreatedAt(); -} diff --git a/src/main/java/com/jiwon/mylog/domain/like/LikeRepository.java b/src/main/java/com/jiwon/mylog/domain/like/LikeRepository.java deleted file mode 100644 index 0c7e4fe..0000000 --- a/src/main/java/com/jiwon/mylog/domain/like/LikeRepository.java +++ /dev/null @@ -1,30 +0,0 @@ -package com.jiwon.mylog.domain.like; - -import org.springframework.data.jpa.repository.JpaRepository; -import org.springframework.data.jpa.repository.Modifying; -import org.springframework.data.jpa.repository.Query; -import org.springframework.data.repository.query.Param; -import org.springframework.stereotype.Repository; - -@Repository -public interface LikeRepository extends JpaRepository { - boolean existsByUserIdAndPostId(Long userId, Long postId); - - @Query(value = "select p.user_id as receiverId, l.created_at as createdAt " - + "from likes l " - + "inner join post p on l.post_id = p.id " - + "where l.user_id = :userId and p.id = :postId", - nativeQuery = true) - LikeNotificationDetails findLikeNotificationDetails(@Param("userId") Long userId, @Param("postId") Long postId); - - @Modifying - @Query(value = "insert ignore into likes(user_id, post_id) values(:userId, :postId)", nativeQuery = true) - void saveLike(@Param("userId") Long userId, @Param("postId") Long postId); - - @Modifying - @Query(value = "delete from likes where user_id = :userId and post_id = :postId", nativeQuery = true) - void deleteLike(@Param("userId") Long userId, @Param("postId") Long postId); - - @Query(value = "select count(*) from likes where post_id = :postId", nativeQuery = true) - long countByPostId(@Param("postId") Long postId); -} diff --git a/src/main/java/com/jiwon/mylog/domain/like/LikeController.java b/src/main/java/com/jiwon/mylog/domain/like/controller/LikeController.java similarity index 66% rename from src/main/java/com/jiwon/mylog/domain/like/LikeController.java rename to src/main/java/com/jiwon/mylog/domain/like/controller/LikeController.java index 79f580e..4b1ced7 100644 --- a/src/main/java/com/jiwon/mylog/domain/like/LikeController.java +++ b/src/main/java/com/jiwon/mylog/domain/like/controller/LikeController.java @@ -1,7 +1,7 @@ -package com.jiwon.mylog.domain.like; +package com.jiwon.mylog.domain.like.controller; +import com.jiwon.mylog.domain.like.service.PostLikeService; import com.jiwon.mylog.global.common.entity.PageResponse; -import com.jiwon.mylog.global.common.entity.SliceResponse; import com.jiwon.mylog.global.security.auth.annotation.LoginUser; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Pageable; @@ -21,29 +21,29 @@ @RestController public class LikeController { - private final LikeService likeService; + private final PostLikeService postLikeService; @PostMapping("/posts/{postId}/likes") - public ResponseEntity createLike(@LoginUser Long userId, @PathVariable Long postId) { - likeService.createLike(userId, postId); + public ResponseEntity likePost(@LoginUser Long userId, @PathVariable Long postId) { + postLikeService.like(userId, postId); return new ResponseEntity<>(HttpStatus.CREATED); } @DeleteMapping("/posts/{postId}/likes") - public ResponseEntity deleteLike(@LoginUser Long userId, @PathVariable Long postId) { - likeService.deleteLike(userId, postId); + public ResponseEntity unlikePost(@LoginUser Long userId, @PathVariable Long postId) { + postLikeService.unlike(userId, postId); return new ResponseEntity<>(HttpStatus.NO_CONTENT); } @GetMapping("/posts/{postId}/likes/count") - public ResponseEntity getLikeCount(@PathVariable Long postId) { - long likeCount = likeService.getLikeCount(postId); + public ResponseEntity countPostLike(@PathVariable Long postId) { + long likeCount = postLikeService.countLikes(postId); return ResponseEntity.ok(likeCount); } @GetMapping("/posts/{postId}/likes") - public ResponseEntity getLikeStatus(@LoginUser Long userId, @PathVariable Long postId) { - boolean likeStatus = likeService.getLikeStatus(userId, postId); + public ResponseEntity isLikedPost(@LoginUser Long userId, @PathVariable Long postId) { + boolean likeStatus = postLikeService.isLiked(userId, postId); return ResponseEntity.ok(likeStatus); } @@ -51,7 +51,7 @@ public ResponseEntity getLikeStatus(@LoginUser Long userId, @PathVariab public ResponseEntity getUserLikes( @PathVariable Long userId, @PageableDefault(size = 10, sort="createdAt", direction = Sort.Direction.DESC) Pageable pageable) { - PageResponse response = likeService.getUserLikes(userId, pageable); + PageResponse response = postLikeService.getUserLikes(userId, pageable); return ResponseEntity.ok(response); } } diff --git a/src/main/java/com/jiwon/mylog/domain/like/entity/BaseLike.java b/src/main/java/com/jiwon/mylog/domain/like/entity/BaseLike.java new file mode 100644 index 0000000..3301093 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/like/entity/BaseLike.java @@ -0,0 +1,34 @@ +package com.jiwon.mylog.domain.like.entity; + +import com.jiwon.mylog.domain.user.entity.User; +import jakarta.persistence.Column; +import jakarta.persistence.EntityListeners; +import jakarta.persistence.FetchType; +import jakarta.persistence.JoinColumn; +import jakarta.persistence.ManyToOne; +import jakarta.persistence.MappedSuperclass; +import java.time.LocalDateTime; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.NoArgsConstructor; +import org.springframework.data.annotation.CreatedDate; +import org.springframework.data.jpa.domain.support.AuditingEntityListener; + +@NoArgsConstructor(access = AccessLevel.PROTECTED) +@Getter +@MappedSuperclass +@EntityListeners(AuditingEntityListener.class) +public class BaseLike { + + @ManyToOne(fetch = FetchType.LAZY) + @JoinColumn(name = "user_id", nullable = false) + private User user; + + @CreatedDate + @Column(nullable = false, updatable = false) + private LocalDateTime createdAt; + + BaseLike(User user) { + this.user = user; + } +} diff --git a/src/main/java/com/jiwon/mylog/domain/like/Like.java b/src/main/java/com/jiwon/mylog/domain/like/entity/PostLike.java similarity index 50% rename from src/main/java/com/jiwon/mylog/domain/like/Like.java rename to src/main/java/com/jiwon/mylog/domain/like/entity/PostLike.java index 4d10f53..26466da 100644 --- a/src/main/java/com/jiwon/mylog/domain/like/Like.java +++ b/src/main/java/com/jiwon/mylog/domain/like/entity/PostLike.java @@ -1,10 +1,8 @@ -package com.jiwon.mylog.domain.like; +package com.jiwon.mylog.domain.like.entity; import com.jiwon.mylog.domain.post.entity.Post; import com.jiwon.mylog.domain.user.entity.User; -import jakarta.persistence.Column; import jakarta.persistence.Entity; -import jakarta.persistence.EntityListeners; import jakarta.persistence.FetchType; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; @@ -13,47 +11,35 @@ import jakarta.persistence.ManyToOne; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; -import lombok.AllArgsConstructor; +import lombok.AccessLevel; import lombok.Getter; import lombok.NoArgsConstructor; -import org.springframework.data.annotation.CreatedDate; -import org.springframework.data.jpa.domain.support.AuditingEntityListener; - -import java.time.LocalDateTime; @Getter -@EntityListeners(AuditingEntityListener.class) -@NoArgsConstructor +@NoArgsConstructor(access = AccessLevel.PROTECTED) @Entity @Table( - name = "likes", + name = "post_like", uniqueConstraints = @UniqueConstraint( - name = "like_uk", + name = "post_like_uk", columnNames = {"user_id", "post_id"} ) ) -public class Like { - +public class PostLike extends BaseLike { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; - @ManyToOne(fetch = FetchType.LAZY) - @JoinColumn(name = "user_id", nullable = false) - private User user; - @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "post_id", nullable = false) private Post post; - @CreatedDate - @Column(nullable = false, updatable = false) - private LocalDateTime createdAt; + PostLike(User user, Post post) { + super(user); + this.post = post; + } - public static Like toLike(User user, Post post) { - Like like = new Like(); - like.user = user; - like.post = post; - return like; + public static PostLike toPostLike(User user, Post post) { + return new PostLike(user, post); } -} +} \ No newline at end of file diff --git a/src/main/java/com/jiwon/mylog/domain/like/repository/PostLikeRepository.java b/src/main/java/com/jiwon/mylog/domain/like/repository/PostLikeRepository.java new file mode 100644 index 0000000..6d253ad --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/like/repository/PostLikeRepository.java @@ -0,0 +1,23 @@ +package com.jiwon.mylog.domain.like.repository; + +import com.jiwon.mylog.domain.like.entity.PostLike; +import java.util.Optional; +import org.springframework.data.jpa.repository.JpaRepository; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.query.Param; +import org.springframework.stereotype.Repository; + +@Repository +public interface PostLikeRepository extends JpaRepository { + + @Query("select pl from PostLike pl " + + "join fetch pl.user " + + "join fetch pl.post " + + "where pl.user.id = :userId and pl.post.id = :postId") + Optional findWithDetails(@Param("userId") Long userId, @Param("postId") Long postId); + + boolean existsByUserIdAndPostId(Long userId, Long postId); + + @Query(value = "select count(*) from post_like where post_id = :postId", nativeQuery = true) + long countByPostId(@Param("postId") Long postId); +} diff --git a/src/main/java/com/jiwon/mylog/domain/like/service/LikeService.java b/src/main/java/com/jiwon/mylog/domain/like/service/LikeService.java new file mode 100644 index 0000000..bee41b4 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/like/service/LikeService.java @@ -0,0 +1,12 @@ +package com.jiwon.mylog.domain.like.service; + +public interface LikeService { + + boolean isLiked(Long userId, Long targetId); + + void like(Long userId, Long targetId); + + void unlike(Long userId, Long targetId); + + Long countLikes(Long targetId); +} diff --git a/src/main/java/com/jiwon/mylog/domain/like/LikeService.java b/src/main/java/com/jiwon/mylog/domain/like/service/PostLikeService.java similarity index 55% rename from src/main/java/com/jiwon/mylog/domain/like/LikeService.java rename to src/main/java/com/jiwon/mylog/domain/like/service/PostLikeService.java index 07a4120..0ce5c0b 100644 --- a/src/main/java/com/jiwon/mylog/domain/like/LikeService.java +++ b/src/main/java/com/jiwon/mylog/domain/like/service/PostLikeService.java @@ -1,7 +1,9 @@ -package com.jiwon.mylog.domain.like; +package com.jiwon.mylog.domain.like.service; import com.jiwon.mylog.domain.event.dto.like.LikeCreatedEvent; import com.jiwon.mylog.domain.event.dto.like.LikeDeletedEvent; +import com.jiwon.mylog.domain.like.repository.PostLikeRepository; +import com.jiwon.mylog.domain.like.entity.PostLike; import com.jiwon.mylog.domain.post.dto.response.PostSummaryResponse; import com.jiwon.mylog.domain.post.entity.Post; import com.jiwon.mylog.domain.post.repository.PostRepository; @@ -11,93 +13,84 @@ import com.jiwon.mylog.global.common.error.ErrorCode; import com.jiwon.mylog.global.common.error.exception.NotFoundException; import lombok.RequiredArgsConstructor; -import org.springframework.cache.annotation.CacheEvict; -import org.springframework.cache.annotation.Cacheable; import org.springframework.context.ApplicationEventPublisher; import org.springframework.data.domain.Page; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; -import java.time.LocalDateTime; - @RequiredArgsConstructor @Service -public class LikeService { +public class PostLikeService implements LikeService { private final ApplicationEventPublisher eventPublisher; - private final LikeRepository likeRepository; + private final PostLikeRepository postLikeRepository; private final UserRepository userRepository; private final PostRepository postRepository; - @CacheEvict(value = "like::count", key = "'postId:' + #postId", condition = "#postId != null") + @Override + @Transactional(readOnly = true) + public boolean isLiked(Long userId, Long postId) { + return postLikeRepository.existsByUserIdAndPostId(userId, postId); + } + + @Override + @Transactional(readOnly = true) + public Long countLikes(Long postId) { + return postLikeRepository.countByPostId(postId); + } + + @Override @Transactional - public void createLike(Long userId, Long postId) { + public void like(Long userId, Long postId) { User user = userRepository.findById(userId) .orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_USER)); - Post post = postRepository.findById(postId) + Post post = postRepository.findByIdWithUser(postId) .orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND_POST)); - Long receiverId = post.getUser().getId(); - Like like = Like.toLike(user, post); - Like savedLike = likeRepository.save(like); + PostLike savedPostLike = postLikeRepository.save(PostLike.toPostLike(user, post)); + + Long receiverId = post.getUser().getId(); if (!receiverId.equals(userId)) { eventPublisher.publishEvent( new LikeCreatedEvent( postId, receiverId, - userId, + user.getId(), user.getUsername(), - savedLike.getCreatedAt() + savedPostLike.getCreatedAt() ) ); } } - @CacheEvict(value = "like::count", key = "'postId:' + #postId", condition = "#postId != null") + @Override @Transactional - public void deleteLike(Long userId, Long postId) { - validateUserExists(userId); - validatePostExists(postId); - - LikeNotificationDetails likeDetails = likeRepository.findLikeNotificationDetails(userId, postId); - Long receiverId = likeDetails.getReceiverId(); + public void unlike(Long userId, Long postId) { + PostLike postLike = postLikeRepository.findWithDetails(userId, postId) + .orElseThrow(() -> new NotFoundException(ErrorCode.NOT_FOUND)); + postLikeRepository.delete(postLike); - likeRepository.deleteLike(userId, postId); + Long receiverId = postLike.getUser().getId(); + Long targetId = postLike.getPost().getId(); if (!receiverId.equals(userId)) { eventPublisher.publishEvent( new LikeDeletedEvent( - postId, + targetId, receiverId, userId, - likeDetails.getCreatedAt() + postLike.getCreatedAt() ) ); } } - @Cacheable(value = "like::count", key = "'postId:' + #postId", condition = "#postId != null") - @Transactional(readOnly = true) - public long getLikeCount(Long postId) { - validatePostExists(postId); - return likeRepository.countByPostId(postId); - } - - @Transactional(readOnly = true) - public boolean getLikeStatus(Long userId, Long postId) { - validateUserExists(userId); - validatePostExists(postId); - return likeRepository.existsByUserIdAndPostId(userId, postId); - } - @Transactional(readOnly = true) public PageResponse getUserLikes(Long userId, Pageable pageable) { - validateUserExists(userId); Page postPage = postRepository.findLikedPosts(userId, pageable); - return PageResponse.from( postPage.getContent(), postPage.getNumber(), @@ -106,16 +99,4 @@ public PageResponse getUserLikes(Long userId, Pageable pageable) { postPage.getTotalElements() ); } - - private void validateUserExists(Long userId) { - if (!userRepository.existsById(userId)) { - throw new NotFoundException(ErrorCode.NOT_FOUND_USER); - } - } - - private void validatePostExists(Long postId) { - if (!postRepository.existsById(postId)) { - throw new NotFoundException(ErrorCode.NOT_FOUND_POST); - } - } } From 11485dcd8855f695bff749850076d4529f11a4a0 Mon Sep 17 00:00:00 2001 From: jiwon Date: Sat, 9 Aug 2025 16:56:58 +0900 Subject: [PATCH 4/4] =?UTF-8?q?refactor:=20tag=20=EC=A0=80=EC=9E=A5=20?= =?UTF-8?q?=EC=8B=9C=20upsert=20bulk=20update=20=EB=B0=A9=EC=8B=9D?= =?UTF-8?q?=EC=9C=BC=EB=A1=9C=20=EB=B3=80=EA=B2=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../jiwon/mylog/domain/post/entity/Post.java | 9 +---- .../post/repository/PostRepository.java | 3 ++ .../post/repository/PostRepositoryImpl.java | 18 ++++----- .../domain/post/service/PostService.java | 4 +- .../jiwon/mylog/domain/tag/entity/Tag.java | 17 ++++----- .../tag/repository/tag/TagJdbcRepository.java | 38 +++++++++++++++++++ .../mylog/domain/tag/service/TagService.java | 28 ++++---------- 7 files changed, 67 insertions(+), 50 deletions(-) create mode 100644 src/main/java/com/jiwon/mylog/domain/tag/repository/tag/TagJdbcRepository.java diff --git a/src/main/java/com/jiwon/mylog/domain/post/entity/Post.java b/src/main/java/com/jiwon/mylog/domain/post/entity/Post.java index 55e0542..e4b3029 100644 --- a/src/main/java/com/jiwon/mylog/domain/post/entity/Post.java +++ b/src/main/java/com/jiwon/mylog/domain/post/entity/Post.java @@ -86,7 +86,7 @@ public class Post extends BaseEntity { @OneToMany(mappedBy = "post") private List comments = new ArrayList<>(); - public static Post create(PostRequest request, User user, Category category, List tags) { + public static Post create(PostRequest request, User user, Category category) { Post post = Post.builder() .title(request.getTitle()) .content(request.getContent()) @@ -98,7 +98,6 @@ public static Post create(PostRequest request, User user, Category category, Lis .pinned(request.isPinned()) .type(PostType.fromString(request.getType())) .build(); - post.setTags(tags); return post; } @@ -112,12 +111,6 @@ public void update(PostRequest request, Category category, List tags) { updateTags(tags); } - private void setTags(List tags) { - this.postTags = tags.stream() - .map(tag -> PostTag.createPostTag(this, tag)) - .toList(); - } - private void updateTags(List tags) { this.postTags.clear(); this.postTags.addAll( diff --git a/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepository.java b/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepository.java index f414d49..f6cfbe2 100644 --- a/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepository.java +++ b/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepository.java @@ -53,4 +53,7 @@ public interface PostRepository extends JpaRepository, PostRepositor "from Post p " + "where p.user.id = :userId and p.pinned = true and p.deletedAt is null") List findPinnedPostsByUserId(@Param("userId") Long userId); + + @Query("select p from Post p join fetch p.user where p.id = :postId") + Optional findByIdWithUser(@Param("postId") Long postId); } diff --git a/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepositoryImpl.java b/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepositoryImpl.java index 50f7f4d..91d64a4 100644 --- a/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepositoryImpl.java +++ b/src/main/java/com/jiwon/mylog/domain/post/repository/PostRepositoryImpl.java @@ -5,7 +5,7 @@ import com.jiwon.mylog.domain.comment.dto.response.CommentResponse; import com.jiwon.mylog.domain.comment.entity.QComment; import com.jiwon.mylog.domain.image.entity.QProfileImage; -import com.jiwon.mylog.domain.like.QLike; +import com.jiwon.mylog.domain.like.entity.QPostLike; import com.jiwon.mylog.domain.post.dto.response.PostDetailResponse; import com.jiwon.mylog.domain.post.dto.response.PostNavigationResponse; import com.jiwon.mylog.domain.post.dto.response.PostSummaryResponse; @@ -56,7 +56,7 @@ public class PostRepositoryImpl implements PostRepositoryCustom { private static final QPostTag POST_TAG = QPostTag.postTag; private static final QTag TAG = QTag.tag; private static final QComment COMMENT = QComment.comment; - private static final QLike LIKE = QLike.like; + private static final QPostLike POST_LIKE = QPostLike.postLike; @Override public Page findLikedPosts(Long userId, Pageable pageable) { @@ -79,8 +79,8 @@ public Page findLikedPosts(Long userId, Pageable pageable) POST.createdAt ) ) - .from(LIKE) - .join(LIKE.post, POST) + .from(POST_LIKE) + .join(POST_LIKE.post, POST) .join(POST.user, USER) .leftJoin(POST.category, CATEGORY) .leftJoin(USER.profileImage, PROFILE_IMAGE) @@ -89,13 +89,13 @@ public Page findLikedPosts(Long userId, Pageable pageable) ) .offset(pageable.getOffset()) .limit(pageable.getPageSize()) - .orderBy(LIKE.createdAt.desc()) + .orderBy(POST_LIKE.createdAt.desc()) .fetch(); Long total = jpaQueryFactory - .select(LIKE.count()) - .from(LIKE) - .join(LIKE.post, POST) + .select(POST_LIKE.count()) + .from(POST_LIKE) + .join(POST_LIKE.post, POST) .where(likeUserIdEq(userId), postDeletedAtIsNull()) .fetchOne(); @@ -458,7 +458,7 @@ private BooleanExpression postUserIdEq(Long userId) { } private BooleanExpression likeUserIdEq(Long userId) { - return userId != null ? LIKE.user.id.eq(userId) : null; + return userId != null ? POST_LIKE.user.id.eq(userId) : null; } private BooleanExpression categoryIdEq(Long categoryId) { diff --git a/src/main/java/com/jiwon/mylog/domain/post/service/PostService.java b/src/main/java/com/jiwon/mylog/domain/post/service/PostService.java index bbe3c58..8b98d7f 100644 --- a/src/main/java/com/jiwon/mylog/domain/post/service/PostService.java +++ b/src/main/java/com/jiwon/mylog/domain/post/service/PostService.java @@ -69,9 +69,7 @@ public PostDetailResponse createPost(Long userId, PostRequest postRequest) { User user = getUserById(userId); Category category = getCategoryById(userId, postRequest.getCategoryId()); List tags = tagService.getOrCreateTags(user, postRequest.getTagRequests()); - - Post post = Post.create(postRequest, user, category, tags); - Post savedPost = postRepository.save(post); + Post savedPost = postRepository.save(Post.create(postRequest, user, category)); if (tags != null && !tags.isEmpty()) { List postTags = tags.stream() diff --git a/src/main/java/com/jiwon/mylog/domain/tag/entity/Tag.java b/src/main/java/com/jiwon/mylog/domain/tag/entity/Tag.java index d8b057d..90fbdd7 100644 --- a/src/main/java/com/jiwon/mylog/domain/tag/entity/Tag.java +++ b/src/main/java/com/jiwon/mylog/domain/tag/entity/Tag.java @@ -1,7 +1,6 @@ package com.jiwon.mylog.domain.tag.entity; import com.jiwon.mylog.domain.user.entity.User; -import jakarta.persistence.CascadeType; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.FetchType; @@ -10,7 +9,6 @@ import jakarta.persistence.Id; import jakarta.persistence.JoinColumn; import jakarta.persistence.ManyToOne; -import jakarta.persistence.OneToMany; import jakarta.persistence.Table; import jakarta.persistence.UniqueConstraint; import lombok.AllArgsConstructor; @@ -18,9 +16,6 @@ import lombok.Getter; import lombok.NoArgsConstructor; -import java.util.ArrayList; -import java.util.List; - @AllArgsConstructor @NoArgsConstructor @Builder @@ -48,10 +43,6 @@ public class Tag { @JoinColumn(name = "user_id", nullable = false) private User user; - @Builder.Default - @OneToMany(mappedBy = "tag", cascade = CascadeType.ALL, orphanRemoval = true) - private List postTags = new ArrayList<>(); - public void incrementUsage() { this.usageCount++; } @@ -59,4 +50,12 @@ public void incrementUsage() { public void decrementUsage() { this.usageCount = Math.max(0, this.usageCount - 1); } + + public static Tag create(User user, String name) { + return Tag.builder() + .user(user) + .name(name) + .usageCount(0L) + .build(); + } } diff --git a/src/main/java/com/jiwon/mylog/domain/tag/repository/tag/TagJdbcRepository.java b/src/main/java/com/jiwon/mylog/domain/tag/repository/tag/TagJdbcRepository.java new file mode 100644 index 0000000..97944c6 --- /dev/null +++ b/src/main/java/com/jiwon/mylog/domain/tag/repository/tag/TagJdbcRepository.java @@ -0,0 +1,38 @@ +package com.jiwon.mylog.domain.tag.repository.tag; + +import com.jiwon.mylog.domain.tag.entity.Tag; +import java.sql.PreparedStatement; +import java.sql.SQLException; +import java.util.List; +import lombok.RequiredArgsConstructor; +import org.springframework.jdbc.core.BatchPreparedStatementSetter; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Repository; +import org.springframework.transaction.annotation.Transactional; + +@RequiredArgsConstructor +@Repository +public class TagJdbcRepository { + + private final JdbcTemplate jdbcTemplate; + + @Transactional + public void upsert(List tags) { + jdbcTemplate.batchUpdate("insert into tag (name, usage_count, user_id) values (?, ?, ?) on duplicate key update id = LAST_INSERT_ID(id)", + new BatchPreparedStatementSetter() { + + @Override + public void setValues(PreparedStatement ps, int i) throws SQLException { + Tag tag = tags.get(i); + ps.setString(1, tag.getName()); + ps.setLong(2, tag.getUsageCount()); + ps.setLong(3, tag.getUser().getId()); + } + + @Override + public int getBatchSize() { + return tags.size(); + } + }); + } +} diff --git a/src/main/java/com/jiwon/mylog/domain/tag/service/TagService.java b/src/main/java/com/jiwon/mylog/domain/tag/service/TagService.java index 2811b7b..b19524a 100644 --- a/src/main/java/com/jiwon/mylog/domain/tag/service/TagService.java +++ b/src/main/java/com/jiwon/mylog/domain/tag/service/TagService.java @@ -1,5 +1,6 @@ package com.jiwon.mylog.domain.tag.service; +import com.jiwon.mylog.domain.tag.repository.tag.TagJdbcRepository; import com.jiwon.mylog.global.common.entity.PageResponse; import com.jiwon.mylog.domain.tag.dto.request.TagRequest; import com.jiwon.mylog.domain.tag.dto.response.TagResponse; @@ -10,16 +11,15 @@ import java.util.List; import java.util.stream.Collectors; import lombok.RequiredArgsConstructor; -import org.springframework.dao.DataIntegrityViolationException; import org.springframework.data.domain.Pageable; import org.springframework.stereotype.Service; -import org.springframework.transaction.annotation.Propagation; import org.springframework.transaction.annotation.Transactional; @RequiredArgsConstructor @Service public class TagService { + private final TagJdbcRepository tagJdbcRepository; private final TagRepository tagRepository; @Transactional(readOnly = true) @@ -42,26 +42,12 @@ public List getOrCreateTags(User user, List tagRequests) { .distinct() .toList(); - return names.stream() - .map(name -> findOrCreateTag(user, name)) + List tags = names.stream() + .map(name -> Tag.create(user, name)) .toList(); - } - @Transactional(propagation = Propagation.REQUIRES_NEW) - public Tag findOrCreateTag(User user, String name) { - return tagRepository.findByUserAndName(user, name) - .orElseGet(() -> { - try { - Tag tag = Tag.builder() - .name(name) - .usageCount(0L) - .user(user) - .build(); - return tagRepository.save(tag); - } catch (DataIntegrityViolationException e) { - return tagRepository.findByUserAndName(user, name) - .orElseThrow(() -> new IllegalStateException("Tag should exist but not found: " + name)); - } - }); + tagJdbcRepository.upsert(tags); + + return tagRepository.findAllByUserAndNameIn(user, names); } }