berryberrybin / board-project

0 stars 0 forks source link

좋아요 count 구현 및 리팩토링 #18

Open berryberrybin opened 1 year ago

berryberrybin commented 1 year ago

기존 코드

Domain

}


- PostLike (좋아요)

```java

@Getter
@Entity
@Table(uniqueConstraints = { @UniqueConstraint(name = "POST_LIKE_UNIQUE", columnNames = {"user_id", "post_id"})})
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PostLike {

    @Id
    @GeneratedValue(generator = "UUID")
    @Column(name = "post_like_id", columnDefinition = "BINARY(16)")
    @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
    private UUID id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;

    @Builder
    public PostLike(Post post, User user) {
        this.post = post;
        this.user = user;
    }
}

Service

@Service
@Transactional
@RequiredArgsConstructor
public class PostCommandService {
    private final TagCommandService tagCommandService;
    private final PostRepository postRepository;
    private final BoardRepository boardRepository;
    private final UserRepository userRepository;
    private final PostLikeRepository postLikeRepository;

public void likePost(PostCommand.LikePostCommand likePostCommand) {
        if (postLikeRepository.existsByUserIdAndPostId(likePostCommand.getUserId(), likePostCommand.getPostId())) {
            throw new BusinessException(ErrorCode.DUPLICATE, "postLike", null);
        }

        UUID userId = likePostCommand.getUserId();
        User user = userRepository.findById(userId)
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "user.byId", List.of(userId.toString())));

        UUID postId = likePostCommand.getPostId();
        Post post = postRepository.findById(postId)
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "post.byId", List.of(postId.toString())));

        PostLike postLike = PostLike.builder()
                .user(user)
                .post(post)
                .build();

        post.addLike(postLike);
    }

    public void cancelLikePost(PostCommand.CancelLikePostCommand cancelLikePostCommand) {
        UUID userId = cancelLikePostCommand.getUserId();
        UUID postId = cancelLikePostCommand.getPostId();

        Post post = postRepository.findById(postId)
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "post.byId", List.of(postId.toString())));

        PostLike postLike = postLikeRepository.findByUserIdAndPostId(userId, postId)
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "postLike", null));

        post.cancelLike(postLike);
    }

}
@Slf4j
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class PostQueryService {
    private final PostRepository postRepository;

    public PostInfo retrievePost(PostQuery.RetrievePostByPostId retrievePostQuery) {
        UUID postId = retrievePostQuery.getPostId();
        Post post = postRepository.findById(postId)
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "post.byId", List.of(postId.toString())));

        return PostInfoMapper.INSTANCE.of(post);
    }
}

Mapper

@Setter
@Getter
@ToString
@EqualsAndHashCode
@AllArgsConstructor
public class PostInfo {
    private UUID userId;
    private UUID postId;
    private String title;
    private String content;
    private Integer likeCount;
}
@Mapper
public interface PostInfoMapper {
    PostInfoMapper INSTANCE = Mappers.getMapper(PostInfoMapper.class);

    @Mapping(target = "userId", source = "user.id")
    @Mapping(target = "postId", source = "id")
    @Mapping(target = "likeCount", expression = "java(post.getLikeCount())")
    PostInfo of(Post post);
}

Controller

@RestController
@RequestMapping("/api/v1/users/{userId}")
@RequiredArgsConstructor
public class PostCommandController {
    private final PostCommandService postCommandService;
@PostMapping("/like/posts/{postId}")
    public ResponseEntity<CommonResponse<?>> likePost(@PathVariable UUID userId,
                                                      @PathVariable UUID postId) {
        PostCommand.LikePostCommand command = PostCommand.LikePostCommand.builder()
                .userId(userId)
                .postId(postId)
                .build();

        postCommandService.likePost(command);

        return ResponseEntity
                .status(HttpStatus.OK)
                .body(CommonResponse.success("포스트 좋아요 성공"));
    }

    @DeleteMapping("/like/posts/{postId}")
    public ResponseEntity<CommonResponse<?>> cancelLikePost(@PathVariable UUID userId,
                                                            @PathVariable UUID postId) {
        PostCommand.CancelLikePostCommand command = PostCommand.CancelLikePostCommand.builder()
                .userId(userId)
                .postId(postId)
                .build();

        postCommandService.cancelLikePost(command);

        return ResponseEntity
                .status(HttpStatus.OK)
                .body(CommonResponse.success("포스트 좋아요 취소 성공"));
    }
}
berryberrybin commented 1 year ago

리팩토링 1

Domain

}

- PostLike : 기존과 동일
```java
@Getter
@Entity
@Table(uniqueConstraints = { @UniqueConstraint(name = "POST_LIKE_UNIQUE", columnNames = {"user_id", "post_id"})})
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class PostLike {

    @Id
    @GeneratedValue(generator = "UUID")
    @Column(name = "post_like_id", columnDefinition = "BINARY(16)")
    @GenericGenerator(name = "UUID", strategy = "org.hibernate.id.UUIDGenerator")
    private UUID id;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "user_id")
    private User user;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "post_id")
    private Post post;

    @Builder
    public PostLike(Post post, User user) {
        this.post = post;
        this.user = user;
    }
}

Mapper

Service

- retrievePost : 단건 게시글 조회시 mapper변경으로 파라미터 2개 
```java
    public PostInfo retrievePost(PostQuery.RetrievePostByPostId retrievePostQuery) {
        UUID postId = retrievePostQuery.getPostId();
        Post post = postRepository.findById(postId)
                .orElseThrow(() -> new BusinessException(ErrorCode.NOT_FOUND, "post.byId", List.of(postId.toString())));

        // mapper 수정으로 인해 변경
        // return PostInfoMapper.INSTANCE.of(post);
         long postLikeCount = postLikeRepository.findPostLikeCount(postId);
        return PostInfoMapper.INSTANCE.of(post, postLikeCount);
    }
    public Slice<SimplePostInfo> retrievePosts(PostQuery.RetrievePosts retrievePostsQuery) {
        UUID boardId = retrievePostsQuery.getBoardId();
        if (!boardRepository.existsById(boardId)) {
            throw new BusinessException(ErrorCode.NOT_FOUND, "board.byId", List.of(boardId.toString()));
        }

        if (StringUtils.hasText(retrievePostsQuery.getTag())) {
            return postRepository.findAllByBoardIdAndTag(retrievePostsQuery.getBoardId(),
                            retrievePostsQuery.getTag(),
                            retrievePostsQuery.getPageable())
                     //  .map(SimplePostInfoMapper.INSTANCE::of);
                    .map(post -> SimplePostInfoMapper.INSTANCE.of(post, postLikeRepository.findPostLikeCount(post.getId())));
        }

        return postRepository.findAllByBoardId(boardId, retrievePostsQuery.getPageable())
                // .map(SimplePostInfoMapper.INSTANCE::of);
                .map(post -> SimplePostInfoMapper.INSTANCE.of(post, postLikeRepository.findPostLikeCount(post.getId())));
    }

Repository

public interface PostLikeRepository extends JpaRepository<PostLike, UUID>, PostLikeRepositoryCustom {
    Optional<PostLike> findByUserIdAndPostId(UUID userId, UUID postId);
    boolean existsByUserIdAndPostId(UUID userId, UUID postId);
}
public class PostLikeRepositoryCustomImpl implements PostLikeRepositoryCustom {
    private final JPAQueryFactory jpaQueryFactory;

    public PostLikeRepositoryCustomImpl(EntityManager entityManager){
        this.jpaQueryFactory = new JPAQueryFactory(entityManager);
    }

    @Override
    public long findPostLikeCount(UUID postId){
        return jpaQueryFactory
            .select(postLike.count())
            .from(postLike)
            .where(postLike.post.id.eq(postId)).fetchOne();
    }
}

Controller

@RestController
@RequestMapping("/api/v1/users/{userId}")
@RequiredArgsConstructor
public class PostCommandController {
    private final PostCommandService postCommandService;

    // likePost와 cancleLikePost 1개로 통합
    @PostMapping("/like/posts/{postId}")
    public ResponseEntity<CommonResponse<?>> savePostLike(@PathVariable UUID userId,
        @PathVariable UUID postId) {
        PostCommand.LikePostCommand command = PostCommand.LikePostCommand.builder()
            .userId(userId)
            .postId(postId)
            .build();

        postCommandService.savePostLike(command);

        return ResponseEntity
            .status(HttpStatus.OK)
            .body(CommonResponse.success("포스트 좋아요 변경 성공"));
    }
}
berryberrybin commented 1 year ago

결과

berryberrybin commented 1 year ago

리팩토링 - 2

기존 size() 사용시 문제점

추가 발견한 문제점

2022-11-08 13:15:29.410 DEBUG 7298 --- [nio-8080-exec-4] org.hibernate.SQL                        : 
    select
        post0_.post_id as post_id1_4_0_,
        post0_.created_at as created_2_4_0_,
        post0_.updated_at as updated_3_4_0_,
        post0_.board_id as board_id7_4_0_,
        post0_.content as content4_4_0_,
        post0_.title as title5_4_0_,
        post0_.user_id as user_id8_4_0_,
        post0_.view_count as view_cou6_4_0_,
        board1_.board_id as board_id1_0_1_,
        board1_.created_at as created_2_0_1_,
        board1_.updated_at as updated_3_0_1_,
        board1_.name as name4_0_1_,
        user2_.user_id as user_id1_11_2_,
        user2_.email as email2_11_2_,
        user2_.nickname as nickname3_11_2_,
        user2_.role as role4_11_2_ 
    from
        post post0_ 
    left outer join
        board board1_ 
            on post0_.board_id=board1_.board_id 
    left outer join
        users user2_ 
            on post0_.user_id=user2_.user_id 
    where
        post0_.post_id=?
2022-11-08 13:15:29.432 DEBUG 7298 --- [nio-8080-exec-4] org.hibernate.SQL                        : 
    select
        count(postlike0_.post_like_id) as col_0_0_ 
    from
        post_like postlike0_ 
    where
        postlike0_.post_id=?

2022-11-08 13:15:29.438 DEBUG 7298 --- [nio-8080-exec-4] org.hibernate.SQL                        : 
    select
        comments0_.post_id as post_id7_1_1_,
        comments0_.comment_id as comment_1_1_1_,
        comments0_.comment_id as comment_1_1_0_,
        comments0_.created_at as created_2_1_0_,
        comments0_.updated_at as updated_3_1_0_,
        comments0_.content as content4_1_0_,
        comments0_.is_deleted as is_delet5_1_0_,
        comments0_.parent_comment_id as parent_c6_1_0_,
        comments0_.post_id as post_id7_1_0_,
        comments0_.user_id as user_id8_1_0_ 
    from
        comment comments0_ 
    where
        comments0_.post_id=? 
    order by
        comments0_.created_at asc

2022-11-08 13:15:29.443 DEBUG 7298 --- [nio-8080-exec-4] org.hibernate.SQL                        : 
    select
        posttags0_.post_id as post_id2_7_1_,
        posttags0_.post_tag_id as post_tag1_7_1_,
        posttags0_.post_tag_id as post_tag1_7_0_,
        posttags0_.post_id as post_id2_7_0_,
        posttags0_.tag_id as tag_id3_7_0_ 
    from
        post_tag posttags0_ 
    where
        posttags0_.post_id=?

2022-11-08 13:15:29.454 DEBUG 7298 --- [nio-8080-exec-4] org.hibernate.SQL                        : 
    select
        tag0_.tag_id as tag_id1_10_0_,
        tag0_.created_at as created_2_10_0_,
        tag0_.updated_at as updated_3_10_0_,
        tag0_.name as name4_10_0_ 
    from
        tag tag0_ 
    where
        tag0_.tag_id=?

해결 방법

@Entity(name = "Account")
public static class Account {

    @Id
    private Long id;

    private Double credit;

    private Double rate;

    @Formula(value = "credit * rate")
    private Double interest; 
}  
doInJPA(this::entityManagerFactory, entityManager -> {
    Account account = new Account();
    account.setId(1L);
    account.setCredit(5000d);
    account.setRate(1.25 / 100);
    entityManager.persist(account);
});

doInJPA(this::entityManagerFactory, entityManager -> {
    Account account = entityManager.find(Account.class, 1L);
    assertEquals(Double.valueOf(62.5d), account.getInterest()); //interest 값 = 5000 * (1.25/100) = 62.5가 들어감
});
INSERT INTO Account (credit, rate, id)
VALUES (5000.0, 0.0125, 1)

SELECT
    a.id as id1_0_0_,
    a.credit as credit2_0_0_,
    a.rate as rate3_0_0_,
    a.credit * a.rate as formula0_0_
FROM
    Account a
WHERE
    a.id = 1
berryberrybin commented 1 year ago

@Formula 적용 코드

}


### 결과 쿼리 
```Mysql
    select
        post0_.post_id as post_id1_4_0_,
        post0_.created_at as created_2_4_0_,
        post0_.updated_at as updated_3_4_0_,
        post0_.board_id as board_id7_4_0_,
        post0_.content as content4_4_0_,
        post0_.title as title5_4_0_,
        post0_.user_id as user_id8_4_0_,
        post0_.view_count as view_cou6_4_0_,
        (select
            count(*) 
        from
            post_like l 
        where
            l.post_id = post0_.post_id) as formula1_0_,
        board1_.board_id as board_id1_0_1_,
        board1_.created_at as created_2_0_1_,
        board1_.updated_at as updated_3_0_1_,
        board1_.name as name4_0_1_,
        user2_.user_id as user_id1_11_2_,
        user2_.email as email2_11_2_,
        user2_.nickname as nickname3_11_2_,
        user2_.role as role4_11_2_ 
    from
        post post0_ 
    left outer join
        board board1_ 
            on post0_.board_id=board1_.board_id 
    left outer join
        users user2_ 
            on post0_.user_id=user2_.user_id 
    where
        post0_.post_id=?
berryberrybin commented 1 year ago

JPA 엔터티 카운트 성능 개선하기

https://www.popit.kr/jpa-%EC%97%94%ED%84%B0%ED%8B%B0-%EC%B9%B4%EC%9A%B4%ED%8A%B8-%EC%84%B1%EB%8A%A5-%EA%B0%9C%EC%84%A0%ED%95%98%EA%B8%B0/