Problem
금일 팀 프로젝트 진행 중 맞이한 문제는 Post 의 Like 상태를 User 에 맞춰 다르게 반환해주는 것이었습니다.
예시를 들어 문제를 설명해 보면
A 라는 유저가 있고 애플리케이션 내 로그인을 하여 접속한 후 피드를 구경할 때
A 유저가 좋아요를 누른 게시글은 True 를, 좋아요를 누르지 않은 게시글은 False 를 반환합니다.
사실 PostLike 관련 비즈니스 로직은 정말 단순하다고 생각 했으나, 의외로 생각해야 하는 부분들이 정말 많았습니다.
"Post 내 좋아요를 누른 후 취소 할 때, CRUD 중 어떤 Annotation 을 활용하는 것이 가장 적절한 방법일까?"
"Post 전체 조회, 상세 조회 시 좋아요 수를 보여주는 코드를 어떻게, 어느 도메인의 서비스에 작성하는 것이 좋을까?"
이 중 금일 자세히 다루어 볼 문제는 HeartStatus 관련 문제입니다!
접근 방식
작성해야 할 코드를 먼저 글로 정리해 보면,
1. True, False 를 User 에게 보여주는 곳은 Post Entity 입니다.
2. Post Entity 에 HeartStatus Column 을 Boolean Type 으로 추가하고, 기본 값을 False 로 설정합니다.
3. PostId 내 좋아요를 누른 UserId 를 체크, 현재 사용자의 UserId 가 포함된다면 True 를 반환합니다.
"JpaRepository 를 상속받은 PostLikeRepository Interface 안 'findByPostIdAndUserId' 메소드를 정의한 후,
PostService 내 해당 메소드를 활용하여 Post 내 좋아요를 누른 UserId 를 찾아 True 를 반환해주자!"
작성한 코드 및 풀이
fun getAllPosts(authUser: AuthUser): List<PostsResponse> {
val posts = postRepository.findAllByOrderByCreatedAtDesc()
posts.forEach { post ->
val like = postLikeRepository.findByPostIdAndUserId(post.id!!, authUser.id)
post.heartStatus = like != null
}
return posts.map { PostsResponse.from(it) }
}
모든 Post 들을 리스트 형태로 가져와 애플리케이션 유저들에게 보여줄 때 HeartStatus 도 함께 반환할 것이기 때문에 'getAllPosts' 메소드 코드를 가져온 점 참고 부탁드리며, getAllPosts 메서드를 자세히 풀어보려 합니다.
parameter
'authUser: AuthUser' 현재 인증된 사용자 객체를 의미합니다.
Return Type
'List<PostsResponse>' 모든 Posts 를 유저들에게 보여줘야 하기 때문에, 최종적으로 PostsResponse 객체들의 리스트를 반환합니다.
Get Posts
'val posts = postRepository.findAllByOrderByCreatedAtDesc(): postRepository' 를 사용하여 모든 게시물을 조회합니다. 이 때 게시물들은 생성일자(createdAt)를 기준 내림차순으로 정렬됩니다.
좋아요 상태 확인 및 설정
'posts.forEach { post -> ... }' 각 게시물에 대해 반복 작업을 수행,
'val like = postLikeRepository.findByPostIdAndUserId(post.id!!, authUser.id): postLikeRepository' 를 사용하여 '현재 사용자(authUser.id)가 현재 확인중인 게시물(post.id)에 좋아요를 눌렀는지 조회'합니다.
(like가 null이 아니라면 현재 사용자가 반복문을 통해 받아온 Post 를 Like 한 것입니다.)
'post.heartStatus = like != null' like 가 null이 아니라면 post의 heartStatus를 true로 설정합니다. 그렇지 않으면 false 로 설정합니다.
(해당 코드를 if 문으로 풀어쓰면 다음과 같습니다!)
if (like != null) {
post.heartStatus = true
} else {
post.heartStatus = false
}
PostsResponse 변환 및 반환
'return posts.map { PostsResponse.from(it) }' posts 리스트의 각 post 객체를 PostsResponse 로 변환하여 새로운 리스트로 반환합니다. map 함수는 리스트의 각 요소를 변환하여 새로운 리스트를 생성합니다.
자체 리팩토링
Post Entity 에서 Id 를 아래와 같이 받다 보니 위 코드에서 '!!' 연산자를 사용하여 강제로 'nullable' 타입을 'non-nullable' 타입으로 변환을 시켜야만 했습니다..
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
val id: Long? = null
그런데 해당 연산자 사용은 최대한 지양하라고 익히 들어왔기에 자세히 찾아보니 이는 런타임에 'NullPointerException' 을 유발할 수 있으므로 안전하지 않다고 합니다.
그래서 생각한 방법이 'null 값이 없는 것만 받아온다는 조건을 걸 수 없을까?' 였고, if (variable != null) 과 같은 표현으로 쓰인다는 let 을 활용하여 코드를 리팩토링 해보았습니다.
posts.forEach { post ->
// post 중 Id가 있는 것들만 let을 실행
post.id?.let { postId ->
// 각 postId 좋아요 목록에 현재 사용자의 Id 가 있는지 확인하는 작업
val like = postLikeRepository.findByPostIdAndUserId(postId, authUser.id)
// like 가 null 이 아닐 경우 true, null 일 경우 false
post.heartStatus = like != null
} ?: throw IllegalStateException("Post ID should not be null")
}
return posts.map { PostsResponse.from(it) }
위의 코드에서는 foundPost.id가 null이 아닌 경우에만 let 이 실행되며, 그렇지 않은 경우에는 예외가 발생합니다.
이렇게 하면 '!!' 연산자를 사용하지 않고도 foundPost.id가 null 이 아님을 보장하여 해당 연산자를 사용하지 않아도 컴파일이 가능해졌습니다.
그런데 이게 잘 맞는지는 모르겠습니다.. 혹여나 더 좋은 코드가 있다면 조언 부탁드리겠습니다.