728x90
320x100
🎯 오늘의 목표
- 좋아요(Like) 도메인 개발
⚙️ 진행한 작업
- 좋아요(Like) 엔티티 생성
- 좋아요(Like) 레파지토리 생성
- 좋아요(Like) 서비스 생성
- 좋아요(Like) 관련 예외 생성
- 좋아요(Like) 도메인 단위테스트
🛠️ 개발내용
📌 좋아요(Like) 엔티티 생성
package ddururi.bookbookclub.domain.like.entity;
import ddururi.bookbookclub.domain.feed.entity.Feed;
import ddururi.bookbookclub.domain.user.entity.User;
import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
import java.time.LocalDateTime;
/**
* 좋아요(Like) 엔티티
* - 사용자가 특정 피드에 좋아요를 누른 기록을 저장
* - 사용자(User)와 피드(Feed)와 다대일 관계
*/
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@EntityListeners(AuditingEntityListener.class)
@Table(name = "likes", uniqueConstraints = {
@UniqueConstraint(columnNames = {"user_id", "feed_id"})
})
public class Like {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 좋아요를 누른 사용자 */
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id", nullable = false)
private User user;
/** 좋아요를 누른 피드 */
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "feed_id", nullable = false)
private Feed feed;
/** 좋아요 누른 시간 */
@CreatedDate
@Column(nullable = false, updatable = false)
private LocalDateTime createdAt;
/**
* 좋아요 생성 정적 메서드
*/
public static Like create(User user, Feed feed) {
Like like = new Like();
like.user = user;
like.feed = feed;
return like;
}
}
📌 좋아요(Like) 레파지토리 생성
package ddururi.bookbookclub.domain.like.repository;
import ddururi.bookbookclub.domain.like.entity.Like;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
/**
* 좋아요(Like) 관련 데이터 액세스 레포지토리
*/
public interface LikeRepository extends JpaRepository<Like, Long> {
boolean existsByUserIdAndFeedId(Long userId, Long feedId);
Optional<Like> findByUserIdAndFeedId(Long userId, Long feedId);
long countByFeedId(Long feedId);
}
📌 좋아요(Like) 서비스 생성
package ddururi.bookbookclub.domain.like.service;
import ddururi.bookbookclub.domain.feed.entity.Feed;
import ddururi.bookbookclub.domain.feed.repository.FeedRepository;
import ddururi.bookbookclub.domain.like.entity.Like;
import ddururi.bookbookclub.domain.like.exception.LikeException;
import ddururi.bookbookclub.domain.like.repository.LikeRepository;
import ddururi.bookbookclub.domain.user.entity.User;
import ddururi.bookbookclub.global.exception.ErrorCode;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 좋아요(Like) 관련 비즈니스 로직 처리 서비스
*/
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class LikeService {
private final LikeRepository likeRepository;
private final FeedRepository feedRepository;
/**
* 피드 좋아요
* @param user 좋아요 누른 사용자
* @param feedId 좋아요 누를 피드 ID
*/
@Transactional
public void likeFeed(User user, Long feedId) {
// 이미 좋아요를 눌렀는지 확인
boolean alreadyLiked = likeRepository.existsByUserIdAndFeedId(user.getId(), feedId);
if (alreadyLiked) {
throw new LikeException(ErrorCode.ALREADY_LIKED);
}
// 피드 존재 여부 확인
Feed feed = feedRepository.findById(feedId)
.orElseThrow(() -> new LikeException(ErrorCode.FEED_NOT_FOUND));
// 좋아요 저장
Like like = Like.create(user, feed);
likeRepository.save(like);
}
/**
* 피드 좋아요 취소
* @param user 좋아요 취소할 사용자
* @param feedId 좋아요 취소할 피드 ID
*/
@Transactional
public void unlikeFeed(User user, Long feedId) {
// 좋아요 존재 여부 확인
Like like = likeRepository.findByUserIdAndFeedId(user.getId(), feedId)
.orElseThrow(() -> new LikeException(ErrorCode.LIKE_NOT_FOUND));
likeRepository.delete(like);
}
/**
* 피드의 좋아요 수 조회
* @param feedId 피드 ID
* @return 좋아요 수
*/
public long getLikeCount(Long feedId) {
return likeRepository.countByFeedId(feedId);
}
/**
* 사용자가 특정 피드에 좋아요를 눌렀는지 여부 조회
* @param userId 사용자 ID
* @param feedId 피드 ID
* @return 좋아요 여부 (true/false)
*/
@Transactional(readOnly = true)
public boolean hasUserLiked(Long userId, Long feedId) {
return likeRepository.existsByUserIdAndFeedId(userId, feedId);
}
}
📌 좋아요(Like) 관련 예외 클래스 생성
package ddururi.bookbookclub.domain.like.exception;
import ddururi.bookbookclub.global.exception.ErrorCode;
import lombok.Getter;
/**
* 좋아요 관련 비즈니스 예외
*/
@Getter
public class LikeException extends RuntimeException {
private final ErrorCode errorCode;
public LikeException(ErrorCode errorCode) {
super(errorCode.getMessage());
this.errorCode = errorCode;
}
}
📌 좋아요(Like) 관련 예외 핸들러 수정
package ddururi.bookbookclub.global.exception;
/**
* 전역 예외 처리 핸들러
* - 각 예외 상황에 맞는 HTTP 상태코드 및 응답 구조 반환
* - ApiResponse.fail(...) 구조 사용
*/
@RestControllerAdvice
public class GlobalExceptionHandler {
//코드생략//
@ExceptionHandler(LikeException.class)
public ResponseEntity<ApiResponse<Void>> handleLikeException(LikeException e) {
return ResponseEntity.status(HttpStatus.BAD_REQUEST)
.body(ApiResponse.fail(e.getErrorCode()));
}
//코드생략//
}
📌 좋아요(Like) 단위테스트
package ddururi.bookbookclub.domain.like.service;
class LikeServiceTest {
@Mock
private LikeRepository likeRepository;
@Mock
private FeedRepository feedRepository;
@InjectMocks
private LikeService likeService;
@BeforeEach
void setUp() {
MockitoAnnotations.openMocks(this);
}
private User createUser(Long id) {
User user = User.create("test@example.com", "encrypted", "nickname");
ReflectionTestUtils.setField(user, "id", id);
return user;
}
private Feed createFeed(Long id) {
// 테스트용 유저와 책은 임시 더미 객체
User dummyUser = User.create("dummy@email.com", "pw", "닉네임");
Book dummyBook = mock(Book.class); // 실제 동작 필요 없음
Feed feed = Feed.create(dummyUser, dummyBook, "내용");
ReflectionTestUtils.setField(feed, "id", id);
return feed;
}
@Nested
@DisplayName("likeFeed() 테스트")
class LikeFeedTest {
@Test
@DisplayName("정상적으로 좋아요를 누를 수 있다")
void likeFeed_success() {
// given
User user = createUser(1L);
Feed feed = createFeed(1L);
given(likeRepository.existsByUserIdAndFeedId(user.getId(), feed.getId())).willReturn(false);
given(feedRepository.findById(feed.getId())).willReturn(Optional.of(feed));
given(likeRepository.save(any(Like.class))).willReturn(Like.create(user, feed));
// when & then
assertDoesNotThrow(() -> likeService.likeFeed(user, feed.getId()));
}
@Test
@DisplayName("이미 좋아요를 누른 경우 예외 발생")
void likeFeed_alreadyLiked() {
User user = createUser(1L);
Long feedId = 1L;
given(likeRepository.existsByUserIdAndFeedId(user.getId(), feedId)).willReturn(true);
assertThatThrownBy(() -> likeService.likeFeed(user, feedId))
.isInstanceOf(LikeException.class)
.hasMessage(ErrorCode.ALREADY_LIKED.getMessage());
}
@Test
@DisplayName("존재하지 않는 피드에 좋아요 시 예외 발생")
void likeFeed_feedNotFound() {
User user = createUser(1L);
Long feedId = 1L;
given(likeRepository.existsByUserIdAndFeedId(user.getId(), feedId)).willReturn(false);
given(feedRepository.findById(feedId)).willReturn(Optional.empty());
assertThatThrownBy(() -> likeService.likeFeed(user, feedId))
.isInstanceOf(LikeException.class)
.hasMessage(ErrorCode.FEED_NOT_FOUND.getMessage());
}
}
@Nested
@DisplayName("unlikeFeed() 테스트")
class UnlikeFeedTest {
@Test
@DisplayName("정상적으로 좋아요 취소 가능")
void unlikeFeed_success() {
User user = createUser(1L);
Feed feed = createFeed(1L);
Like like = Like.create(user, feed);
given(likeRepository.findByUserIdAndFeedId(user.getId(), feed.getId()))
.willReturn(Optional.of(like));
assertDoesNotThrow(() -> likeService.unlikeFeed(user, feed.getId()));
}
@Test
@DisplayName("좋아요 기록 없을 경우 예외 발생")
void unlikeFeed_notFound() {
User user = createUser(1L);
Long feedId = 1L;
given(likeRepository.findByUserIdAndFeedId(user.getId(), feedId))
.willReturn(Optional.empty());
assertThatThrownBy(() -> likeService.unlikeFeed(user, feedId))
.isInstanceOf(LikeException.class)
.hasMessage(ErrorCode.LIKE_NOT_FOUND.getMessage());
}
}
@Nested
@DisplayName("getLikeCount() 테스트")
class GetLikeCountTest {
@Test
@DisplayName("피드의 좋아요 수 정상 조회")
void getLikeCount_success() {
Long feedId = 1L;
given(likeRepository.countByFeedId(feedId)).willReturn(10L);
long result = likeService.getLikeCount(feedId);
assertThat(result).isEqualTo(10L);
}
}
@Nested
@DisplayName("hasUserLiked() 테스트")
class HasUserLikedTest {
@Test
@DisplayName("사용자가 좋아요 눌렀는지 여부 조회")
void hasUserLiked_success() {
Long userId = 1L;
Long feedId = 1L;
given(likeRepository.existsByUserIdAndFeedId(userId, feedId)).willReturn(true);
boolean result = likeService.hasUserLiked(userId, feedId);
assertThat(result).isTrue();
}
}
}
728x90
320x100
'💻 뚝딱뚝딱 > 북북클럽' 카테고리의 다른 글
[개발일지 #022] 댓글(Comment) 도메인 구현 및 단위테스트 (0) | 2025.04.30 |
---|---|
[개발일지 #021] 좋아요(Like) 도메인 API 구현 및 테스트 (0) | 2025.04.30 |
[개발일지 #019] 피드(Feed) 도메인 API 구현 및 테스트 (1) | 2025.04.29 |
[개발일지 #018] 피드(Feed) 도메인 개발 및 단위테스트 (1) | 2025.04.29 |
[개발일지 #017] 북(Book) 도메인 API 구현 및 테스트 (0) | 2025.04.29 |