조..좋아해요!! (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개의 댓글

관련 채용 정보