[spring kotlin] 게시판 좋아요 api 설계
[spring kotlin] 게시판 좋아요 api 설계 — #스프링코틀린 #코틀린 #api설계 #좋아요api #개발자의도구들 🚨📝 📖 📒✏️💡🔍 AI스쿨 ms...
#스프링코틀린 #코틀린 #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)
실제 상황을 가정하고 정확한 기능을 정의합니다. 클라이언트측, 서버측을 나눠서 설계합니다.
클라이언트 기능
- 클라이언트는 게시글에 좋아요를 누릅니다.
- 클라이언트는 누가 해당 게시글에 좋아요를 눌렀는지 볼 수 있습니다.
서버 기능
- 클라이언트로 부터 게시글 id 및 user정보를 받습니다.
- 1번의 정보를 바탕으로 like table에 해당 데이터를 저장합니다.
DB Design
| 💡 이번 기능 부터는 외래키를 직접 지정하지 않습니다. 개념적 외래키를 사용합니다. (성능 & 보안 이슈) |
|---|
| id | board\_id | user\_id | type | |
|---|---|---|---|---|
| 1 | 1 | 1 | 1 | |
| 2 | 1 | 2 | 1 | |
| 3 | 2 | 1 | 1 | |
| 4 | 2 | 2 | 1 |
테이블 설계를 표로 나타내었습니다.
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에 대한 인덱스를 설정하였습니다.
CREATE INDEX idx_board_id
ON likes(board_id);
CREATE INDEX idx_user_id
ON likes(board_id);
Code
\* 전체적인 맥락을 이해하기 위해 구체적인 코드 설계는 담지 않았습니다
| 🧑💻 Entiy |
|---|
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 |
|---|
// 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는 세가지 입니다.
좋아요한 유저 목록 예시
- save: 클라이언트가 좋아요를 입력할 때 실행되는 api.
- boardId를 전달받고, jwt 토큰 정보를 통해 userId를 추출합니다.
- GET like users:
- 사용자가 게시글의 좋아요 누른 유저 목록을 볼때 호출되는 api
- boardId를 전달받고, 목록을 return합니다.
- Delete Like
- boardId를 입력받아 like를 취소합니다.
- 아래 Service에서 자세하게 설명합니다.
| 🧑💻 Service |
|---|
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 |
|---|
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>
}
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 |
|---|
package com.example.liveApiServer.enum
enum class LikeType {
NONE,
LIKE,
SMILE,
SAD,
AMAZING
}
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 값을 명시하기 |
|---|

