조..좋아해요!! (LIKE 구현하기)

JUNHYUK CHANG·2024년 3월 4일
1

TIL

목록 보기
25/33

어떤 이벤트(공연)에 대해 사람들의 선호도를 확인하고, 인기 이벤트를 기록하기 위해 좋아요 기능을 구현하기로 했다.

기능 자체는 아주 간단한 것 같다. 좋아요를 누르면 True 다시 누르면 False 만 하면 되는거 아닌가?

아니다.

사용자 마다 좋아요 를 체크한 내용을 확인할 수 있어야 하고, 좋아요를 누른 이벤트에 대해선 누가/언제 눌렀는지. 좋아요 취소 누른 날짜는 어떤지? 좋아요 수는 어떻게 확인할지? 몇몇 고민이 필요했다.


메인 아이디어는 이렇다.

  1. 모든 사용자들의 좋아요 를 관리할 테이블 'likes' 를 생성하고 유저가 좋아요를 누를 때마다 이를 기록한다.

  2. Entity는 user / event 를 각각 N:1 관계로 설정하여 유저나 이벤트가 여러개의 좋아요를 갖는 것으로 구성한다.
    ( + BaseEntity 를 통해 CreatedDate 와 LastModifiedDate, isDeleted 를 상속받도록 함 )

  3. Event 에선 likeRepository 에서 해당 eventId 의 갯수를 세서 LikeCount 를 보여줄 수 있도록 한다.

[Like Entity]

@Entity
@SQLDelete(sql = "UPDATE likes SET is_deleted = true WHERE id = ?") // DELETE 쿼리 날아올 시 대신 실행
@SQLRestriction("is_deleted = false")
@Table(name = "likes")
class Like (

    @ManyToOne
    @JoinColumn(name = "member_id")
    val member: Member,

    @ManyToOne
    @JoinColumn(name = "event_id")
    val event : Event,


    ) : BaseEntity() {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    val id:Long? = null
}
  • @SQLDelete 메서드를 통해 softDelete 설정.
  • @SQLRestriction 메서드를 통해 삭제처리되지 않은 항목만 조회되도록 함.
  • ManyToOne 을 통해 다대일 관계를 설정.

Like Entity 는 간단히 만들고 이제 Controller 로 와서도 작은 고민이 있었다.

Like 를 생성하는건 Post. 지우는건 Delete. 수정(복원) 은 Put이나 Patch.
그런데 처음 좋아요를 누를 때는 생성이 되고 두 번째 누를 때는 삭제( isDelete = true )
세 번째 누를 때는 복원( isDelete = false ) 로 복합적인 기능의 메서드가 될 텐데 어떤 Mapping 으로 묶어야 할까? 고민이 되었다.

어쨌든 최초는 생성이니 Post? isDelete 항목을 계속 수정하고 있으니 Put 이나 Patch..?

다른 프로젝트들을 확인해보니 이는 FE 와 상의를 통해 어떤 메서드로 호출할지 정하기도 하고, 대부분 POST Mappiong 으로 진행하는 것 같다.


[LikeService > chkLike 메서드]

    override fun chkLike(memberId: Long, eventId: Long) {
        val member = memberRepository.findByIdOrNull(memberId)
            ?:throw NotFoundException()

        val event = eventRepository.findByIdOrNull(eventId)
            ?:throw NotFoundException()

       likeRepository.findLikeByMemberIdAndEventId(memberId, eventId)
           ?.let{
               it.isDeleted = !it.isDeleted
               event.likeCount += if(it.isDeleted) 1 else -1
               likeRepository.save(it)

           }
           ?:run{
               event.likeCount++
               likeRepository.save(Like(member,event))
           }

        eventRepository.save(event)
    }
  1. 매개변수로 memberId 와 eventId 를 입력받고, member 와 event 를 찾아온다.
  2. likeRepositorty 에서 혹시 이미 like 에 대한 기록이 있는지 확인한다. ( findLikeByMemberIdAndEventId )
  3. 만약 해당하는 데이터가 이미 있다면 isDeleted 항목을 반전하고, 해당 값에 따라 event의 likeCount 항목에 +=1 을 계산한 뒤 저장한다.
  4. 만약 해당 데이터가 없다면 likeCount +1 후 like 항목을 생성하여 저장한다.
  5. 어떤 상황이든 event 의 likeCount 의 변화가 있었기 때문에 event를 저장한다.
  • 사실 event의 likeCount 는 likes 테이블에 접근하여 해당 eventId 의 항목을 전부 가져온 뒤 count 쿼리를 전송하여 새로고침 하는 것이 가장 정확한 방법이다.
  • 하지만 이는 '저스틴-비버 문제(Justin Bieber Problem)' 라는 상황을 발생시키게 된다.
  • 너무 많은 유저가 좋아요 갯수가 많은 대상을 조회할 때, 매번 그 값을 표시하기 위해 쿼리를 발생시키고 계산하는 과정에서 과부하가 발생하는 문제를 말한다.
  • 정확한 좋아요 수치를 표현하기 위해선 주기적으로 (스케쥴링) 좋아요 값을 최신화해주는 메서드를 실행시키면 해결 된다.
    override fun updateLike() {
        // 이벤트 id 리스트를 Like 에서 가져와서
        likeRepository.getEventIdList().map{e_id ->
            // 각 id 마다 해당하는 like 가 몇개인지 확인하고   // 각 이벤트 객체의 count 를 저장
            val event = eventRepository.findByIdOrNull(e_id)
                ?.also {e ->
                    e.likeCount = likeRepository.countEventId(e.id!!).toInt()
                }
                ?: throw NotFoundException()
            eventRepository.save(event)
        }
    }

간단해보이는 내용이더라도 요청 데이터가 많아지면 예상치 못한 문제가 발생할 수 있으니 항상 주의해야 함을 다시 느끼게 되었다. 비슷한 예로 LIKE 쿼리를 통한 검색 문제로 한참 고생했던 일들이 떠오르기도 했다. 항상 다양한 상황을 고려하고 대비하는 개발자가 되어야겠다..!

0개의 댓글