728x90
320x100
🎯 오늘의 목표
- 북(Book) 도메인 개발
⚙️ 진행한 작업
- 북(Book) 엔티티 생성
- 북(Book) 레파지토리 생성
- 북(Book) 서비스 생성
- 북(Book) 관련 예외 생성
- 북(Book) 도메인 단위 테스트
🛠️ 개발내용
📌 북(Book) 엔티티 생성
/**
* 책(Book) 엔티티
* - 외부 책 API에서 검색된 책 정보를 저장
*/
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Book {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
/** 책 제목 */
@Column(nullable = false)
private String title;
/** 저자 */
private String author;
/** 출판사 */
private String publisher;
/** ISBN */
@Column(unique = true)
private String isbn;
/** 썸네일 이미지 URL */
@Column(length = 1000)
private String thumbnailUrl;
/**
* Book 생성 정적 메서드
*/
public static Book create(String title, String author, String publisher, String isbn, String thumbnailUrl) {
Book book = new Book();
book.title = title;
book.author = author;
book.publisher = publisher;
book.isbn = isbn;
book.thumbnailUrl = thumbnailUrl;
return book;
}
}
📌 북(Book) 레파지토리 생성
package ddururi.bookbookclub.domain.book.repository;
import ddururi.bookbookclub.domain.book.entity.Book;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
/**
* Book 데이터 액세스 레포지토리
*/
public interface BookRepository extends JpaRepository<Book, Long> {
Optional<Book> findByIsbn(String isbn);
Optional<Book> findByTitleAndAuthor(String title, String author);
}
📌 북(Book) 서비스 생성
package ddururi.bookbookclub.domain.book.service;
import ddururi.bookbookclub.domain.book.dto.BookRequest;
import ddururi.bookbookclub.domain.book.dto.BookResponse;
import ddururi.bookbookclub.domain.book.entity.Book;
import ddururi.bookbookclub.domain.book.repository.BookRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
/**
* 책 관련 비즈니스 로직 처리
*/
@Service
@RequiredArgsConstructor
@Transactional
public class BookService {
private final BookRepository bookRepository;
/**
* 책 등록
* @param request 책 등록 요청 데이터
* @return 등록된 책 정보
*/
public BookResponse createBook(BookRequest request) {
Book book = Book.create(
request.getTitle(),
request.getAuthor(),
request.getPublisher(),
request.getIsbn(),
request.getThumbnailUrl()
);
Book savedBook = bookRepository.save(book);
return new BookResponse(savedBook);
}
}
📌 북(Book) 컨트롤러 생성
package ddururi.bookbookclub.domain.book.controller;
import ddururi.bookbookclub.domain.book.dto.BookRequest;
import ddururi.bookbookclub.domain.book.dto.BookResponse;
import ddururi.bookbookclub.domain.book.service.BookService;
import ddururi.bookbookclub.global.common.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
/**
* 책 관련 API 컨트롤러
* - 등록
*/
@RestController
@RequestMapping("/api/books")
@RequiredArgsConstructor
public class BookController {
private final BookService bookService;
/**
* 책 등록 API
* @param request 책 등록 요청 데이터
* @return 등록된 책 정보
*/
@PostMapping
public ResponseEntity<ApiResponse<BookResponse>> createBook(
@RequestBody @Valid BookRequest request
) {
BookResponse bookResponse = bookService.createBook(request);
return ResponseEntity.ok(ApiResponse.success(bookResponse));
}
}
📌 BookRequest 생성
package ddururi.bookbookclub.domain.book.dto;
import jakarta.validation.constraints.NotBlank;
import lombok.Getter;
import lombok.NoArgsConstructor;
/**
* 책 등록 요청 DTO
*/
@Getter
@NoArgsConstructor
public class BookRequest {
@NotBlank(message = "제목은 비어 있을 수 없습니다.")
private String title;
@NotBlank(message = "저자는 비어 있을 수 없습니다.")
private String author;
private String publisher;
private String isbn;
private String thumbnailUrl;
}
📌 BookResponse 생성
package ddururi.bookbookclub.domain.book.dto;
import ddururi.bookbookclub.domain.book.entity.Book;
import lombok.Getter;
@Getter
public class BookResponse {
private final Long id;
private final String title;
private final String author;
private final String publisher;
private final String isbn;
private final String thumbnailUrl;
public BookResponse(Book book) {
this.id = book.getId();
this.title = book.getTitle();
this.author = book.getAuthor();
this.publisher = book.getPublisher();
this.isbn = book.getIsbn();
this.thumbnailUrl = book.getThumbnailUrl();
}
}
📌 단위 테스트
package ddururi.bookbookclub.domain.book.service;
import ddururi.bookbookclub.domain.book.dto.BookRequest;
import ddururi.bookbookclub.domain.book.dto.BookResponse;
import ddururi.bookbookclub.domain.book.entity.Book;
import ddururi.bookbookclub.domain.book.repository.BookRepository;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.ArgumentCaptor;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.junit.jupiter.MockitoExtension;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.Mockito.*;
import org.junit.jupiter.api.extension.ExtendWith;
/**
* BookService 단위 테스트
*/
@ExtendWith(MockitoExtension.class)
class BookServiceTest {
@Mock
private BookRepository bookRepository;
@InjectMocks
private BookService bookService;
@Test
@DisplayName("책 등록 성공")
void createBook_success() {
// given
BookRequest request = new BookRequest();
setField(request, "title", "Clean Code");
setField(request, "author", "Robert C. Martin");
setField(request, "publisher", "Prentice Hall");
setField(request, "isbn", "9780132350884");
setField(request, "thumbnailUrl", "https://example.com/cleancode.jpg");
Book savedBook = Book.create(
request.getTitle(),
request.getAuthor(),
request.getPublisher(),
request.getIsbn(),
request.getThumbnailUrl()
);
setField(savedBook, "id", 1L); // 저장된 책에 id 설정 (mock용)
when(bookRepository.save(any(Book.class))).thenReturn(savedBook);
// when
BookResponse response = bookService.createBook(request);
// then
ArgumentCaptor<Book> captor = ArgumentCaptor.forClass(Book.class);
verify(bookRepository, times(1)).save(captor.capture());
Book capturedBook = captor.getValue();
assertThat(response).isNotNull();
assertThat(response.getId()).isEqualTo(1L);
assertThat(capturedBook.getTitle()).isEqualTo(request.getTitle());
assertThat(capturedBook.getAuthor()).isEqualTo(request.getAuthor());
assertThat(capturedBook.getPublisher()).isEqualTo(request.getPublisher());
assertThat(capturedBook.getIsbn()).isEqualTo(request.getIsbn());
assertThat(capturedBook.getThumbnailUrl()).isEqualTo(request.getThumbnailUrl());
}
// 테스트용 private util (setter 없는 필드 강제 설정)
private void setField(Object target, String fieldName, Object value) {
try {
var field = target.getClass().getDeclaredField(fieldName);
field.setAccessible(true);
field.set(target, value);
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
728x90
320x100
'💻 뚝딱뚝딱 > 북북클럽' 카테고리의 다른 글
[개발일지 #018] 피드(Feed) 도메인 개발 및 단위테스트 (0) | 2025.04.29 |
---|---|
[개발일지 #017] 북(Book) 도메인 API 구현 및 테스트 (0) | 2025.04.29 |
[개발일지 #015] 이메일 인증 실패 시도 횟수 제한 기능 (0) | 2025.04.25 |
[개발일지 #014] 글로벌 예외처리 및 API 응답 포맷 통일 (0) | 2025.04.24 |
[개발일지 #013] 회원 프로필 사진 등록 기능 구현 (0) | 2025.04.24 |