Spring발행일 2024. 11. 6.원본 https://blog.naver.com/jword_/223649969690 ↗

[spring kotlin] 게시판 좋아요 api 설계

[spring kotlin] 게시판 좋아요 api 설계 — #스프링코틀린 #코틀린 #api설계 #좋아요api #개발자의도구들 🚨📝 📖 📒✏️💡🔍 AI스쿨 ms...

#Spring#Naver Blog

#스프링코틀린 #코틀린 #api설계 #좋아요api #개발자의도구들

🚨📝 📖 📒✏️💡🔍

​

AI스쿨 msa기반 java 백엔드 코스 중에 공부한 내용을 작성하였습니다

\* 현재 Spring x 프로젝트를 진행하고 있습니다. 전체 목차를 보시려면 여기를 눌러주세요.

\* 해당 프로젝트 이전에 공부한 내용을 보시려면 여기를 눌러주세요.

주제

이번에 만들 기능은 sns의 꽃이라고도 볼 수 있는 좋아요 기능입니다.

​

Index

  • Purpose
  • Idea(Define Functions)
  • DB Design
  • Code
  • Entity
  • Controller
  • Service
  • Repostiory
  • Question
  • Answer
  • Feedback
💡 Tips

Purpose

게시글 좋아요 기능을 구현합니다.

Idea(Define Functions)

실제 상황을 가정하고 정확한 기능을 정의합니다. 클라이언트측, 서버측을 나눠서 설계합니다.

​

클라이언트 기능

  1. 클라이언트는 게시글에 좋아요를 누릅니다.
  2. 클라이언트는 누가 해당 게시글에 좋아요를 눌렀는지 볼 수 있습니다.

​

서버 기능

  1. 클라이언트로 부터 게시글 id 및 user정보를 받습니다.
  2. 1번의 정보를 바탕으로 like table에 해당 데이터를 저장합니다.

DB Design

💡 이번 기능 부터는 외래키를 직접 지정하지 않습니다. 개념적 외래키를 사용합니다. (성능 & 보안 이슈)

​

idboard\_iduser\_idtype
1111
2121
3211
4221

테이블 설계를 표로 나타내었습니다.

sql 코드 예제
                                    CREATE TABLE LIKES(
    id BIGINT AUTO_INCREMENT PRIMARY KEY, -- 기본키
    board_id BIGINT NOT NULL,
    user_id BIGINT NOT NULL,
    -- 개념적 foreign key로 설정(실제 DB에는 외래키로 반영하지 읂음)
    type INT NOT NULL
)
💡 id값을 가지는 col의 경우 반드시 id라는 것을 명시해주세요. ex) board\_id, user\_id

요즘 x뿐 아니라 인스타그램, 카카오톡 모두 좋아요뿐 아니라 다양한 이모지 표현이 가능합니다.

이미지

이모지 타입을 지정하여서 번호를 넘겨주면, 프론트 쪽에서 적절하게 이모티콘으로 처리할 수 있도록 합니다.

​

예를들어 1을 넘겨준다면, 그냥 좋아요❤️ 표시를 하고, 2번이라면 good(👍)와 같이 처리할 수 있습니다.

💡 INDEX 설정

DB의 로직을 고려하여 INDEX를 미리 설정해 둡니다. 특정 COL로 검색을 하는 경우가 생긴다면, 그 COL에 대한 INDEX를 필수로 설정합니다.

​

저는 나중에 board\_id와 user\_id로 조회를 할거기 때문에 두 col에 대한 인덱스를 설정하였습니다.

sql 코드 예제
                                    CREATE INDEX idx_board_id
ON likes(board_id);
sql 코드 예제
                                    CREATE INDEX idx_user_id
ON likes(board_id);

Code

\* 전체적인 맥락을 이해하기 위해 구체적인 코드 설계는 담지 않았습니다

🧑‍💻 Entiy
kotlin 코드 예제
                                    package com.example.frontServer.entity

import jakarta.persistence.*
import org.springframework.data.annotation.CreatedBy

@Entity
@Table(
    name = "likes",
)
class Like(
    @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null,

    @Column(name = "board_id")
    val boardId: Long,

    @Column(name = "user_id")
    var userId: Long? = null,
)
🧑‍💻 Controller
kotlin 코드 예제
                                    // import 생략

@RestController
class LikeController(
    private val likeService: LikeService,
) {

    @PostMapping("/like")
    fun save(
        @RequestBody saveDto: LikeSaveDto
    ): ResponseEntity<ResponseToServerDto> {
        likeService.save(saveDto)
        return ResponseEntity.ok().body(
            ResponseToServerDto(
                error = null,
                userIds = null
            )
        )
    }

    @DeleteMapping("/like")
    fun delete(
        @RequestBody boardId: Long
    ): ResponseEntity<ResponseToServerDto> {
        likeService.delete(boardId)
        return ResponseEntity.ok().body(
            ResponseToServerDto(
                error = null,
                userIds = null
            )
        )
    }

    @GetMapping("/like/users")
    fun findUserIdsByBoardId(
        @RequestParam boardId: Long,
    ): ResponseEntity<ResponseToServerDto> {
        return ResponseEntity.ok().body(
            ResponseToServerDto(
                error = null,
                userIds = likeService.findUserIdsByBoardId(boardId)
            )
        )
    }
}

상세코드는 최대한 줄이고 핵심만 남겼습니다. like에서 제공할 api는 세가지 입니다.

이미지

좋아요한 유저 목록 예시

  1. save: 클라이언트가 좋아요를 입력할 때 실행되는 api.
  • boardId를 전달받고, jwt 토큰 정보를 통해 userId를 추출합니다.

​

  1. GET like users:
  • 사용자가 게시글의 좋아요 누른 유저 목록을 볼때 호출되는 api
  • boardId를 전달받고, 목록을 return합니다.

​

  1. Delete Like
  • boardId를 입력받아 like를 취소합니다.
  • 아래 Service에서 자세하게 설명합니다.
🧑‍💻 Service
kotlin 코드 예제
                                    package com.example.liveApiServer.service

import com.example.liveApiServer.dto.LikeSaveDto
import com.example.liveApiServer.entity.Like
import com.example.liveApiServer.repository.LikeRepository
import org.springframework.stereotype.Service

@Service
class LikeService(
    private val likeRepository: LikeRepository,
) {
    fun save(saveDto: LikeSaveDto){
        likeRepository.save(
            Like(
                id = null, // 이렇게 직접 명시해주기.
                boardId = saveDto.boardId,
                userId = saveDto.userId,
                likeType = saveDto.likeType,
            )
        )
        // boardId userId가 없는 경우 어떻게 처리?
        // likeType이 없는 type인 경우도 있을 수 있다.
        // Exception?
    }

    fun delete(boardId: Long) {
        likeRepository.deleteLogically(boardId)
    }

    fun findUserIdsByBoardId(boardId: Long): List<Long> {
        return likeRepository.findByBoardId(boardId).map {it.userId}
    }
}

실제 DB에서 바로 제거하는 것이 아니라 논리적으로 제거된 것처럼 보이게 제거합니다.

🧑‍💻 Repository
kotlin 코드 예제
                                    package com.example.liveApiServer.repository

import com.example.liveApiServer.entity.Like
import org.springframework.data.jpa.repository.JpaRepository

interface LikeRepository: JpaRepository<Like, Long>, LikeQueryDslRepository {
    fun findByBoardId(boardId: Long): List<Like>
}
kotlin 코드 예제
                                    package com.example.liveApiServer.repository

import com.example.liveApiServer.entity.Like
import com.example.liveApiServer.entity.QLike
import com.example.liveApiServer.enum.LikeType
import com.querydsl.jpa.impl.JPAQueryFactory

class LikeQueryDslRepositoryImpl(
    private val queryFactory: JPAQueryFactory
):LikeQueryDslRepository {
    private val like = QLike.like
    override fun deleteLogically(boardId: Long) {
        queryFactory
            .update(like)
            .set(like.likeType, LikeType.NONE.ordinal)
            .where(like.boardId.eq(boardId))
            .execute()
    }
}

실제로 제거되는 것이 아니라 likeType정보가 NONE으로 변경되는 것 뿐입니다. 이유는 아래에 기술하였습니다.

🧑‍💻 Others
kotlin 코드 예제
                                    package com.example.liveApiServer.enum

enum class LikeType {
    NONE,

    LIKE,

    SMILE,

    SAD,

    AMAZING
}
kotlin 코드 예제
                                    package com.example.liveApiServer.dto

data class LikeSaveDto(
    val boardId: Long,
    val userId: Long,
    val likeType: Int,
)

Question

🤔 write Question​실제 데이터를 보내는 경우는 좋아요를 누른 유저 전체 목록을 보는 경우 밖에 없다.이때 Dto로 데이터를 만들지 않고 List<Long>으로 바로 보내도 되는건지??➡️ 넣을 데이터가 하나 밖에없어서 이렇게 판단하였음

Feedback

전체 글은 피드백을 반영한 이후 시점에서 작성한 글입니다. 따라서 코드가 피드백 이후 변경된 것으로 기록되어있습니다.

​

이 파트에서는 간단한 feedback 기록만을 남깁니다.

🙋‍♂️ 왜 MYSQL에서 PK를 의미 없는 값으러 설정하는것인가?

MYSQL에는 PK가 DISK 순서까지 결정합니다. 그럼 만약 순차적으로 증가하는 PK가아닌 랜덤 값으로 PK를 설정하면 어떻게 될까요? 매번 데이터가 들어올 때마다 MYSQL 내부에서는 DISK의 순서를 변경합니다. 이는 overhead를 유발할 수 있습니다.

​

위의 DB디자인에서 likeType을 설정해두었었는데, 이는 피드백 반영이후에 추가한 코드입니다. 추가하게 된 이유도 PK와 관련이 있습니다. 만약 사용자가 좋아요를 취소하는 경우, delete api가 호출되게 되는데, delete가 DB에서 발생하게 되는경우 MYSQL의 DISK상의 순서가 재정렬되어 overhead가 발생합니다(수 만, 수 억명이 삭제한다면 서버가 터지겠죠?).

​

이런 이유때문에 보통 서버에서는 상태를 관리하는 값을 따로 저장해두고, 평소에는 실제 DB에서 지우지 않고 있다가 점검시기에 like가 disable된 데이터를 모두 지웁니다.

​

🙋‍♂️ 그 외- createdBy 사용하지말기 : 실제로 잘 안쓰는 기능이야.- Entity에 NOT NULL 확실히 명시하고, 삽입할때 직접 null 값을 명시하기

​