domain -> entity
package hello.board.entity;
import lombok.AccessLevel;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Board {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "board_id")
private Long id;
private String title;
private String content;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
private LocalDateTime registerDate;
@Builder
public Board(String title, String content, User user, LocalDateTime registerDate) {
this.title = title;
this.content = content;
this.user = user;
this.registerDate = registerDate;
}
//==생성 메서드==//
public static Board createBoard(String title, String content, User user) {
return Board.builder()
.title(title).content(content).user(user)
.registerDate(LocalDateTime.now())
.build();
}
//==비즈니스 메서드==//
public void update(String title, String content) {
this.title = title;
this.content = content;
}
}
@ManyToOne(fetch = FetchType.LAZY)
@XToOne(OneToOne, ManyToOne)
관계는 기본이 즉시로딩이므로 직접 지연로딩으로 설정해야 한다.EAGER
)은 예측이 어렵고, 어떤 SQL이 실행될지 추적하기 어렵다. 특히 JPQL을 실행할 때 N+1 문제가 자주 발생한다.@Builder
createBoard
registerDate
를 LocalDateTime.now()
로 설정하여, 게시글 작성 일자를 객체 생성 시점으로 정한다.update
게시글의 제목과 내용을 변경하기 위해 도메인 모델 패턴을 사용했다. 비즈니스 로직 대부분이 엔티티에 있으며, 서비스 계층은 단순히 엔티티에 필요한 요청을 위임하는 역할이다.
엔티티가 비즈니스 로직을 가지고 객체 지향의 특성을 적극 활용하는 것을 도메인 모델 패턴이라 한다. 반대로 엔티티에는 비즈니스 로직이 거의 없고 서비스 계층에서 대부분의 비즈니스 로직을 처리하는 것을 트랜잭션 스크립트 패턴이라 한다.
→ 어떤 패턴이 유지보수 하기 좋은지 고민해야 한다.
package hello.board.repository;
import hello.board.domain.Board;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
@Repository
public interface BoardRepository extends JpaRepository<Board, Long> {
}
JpaRepository
를 보고 구현체를 자동으로 만들고 스프링 빈으로 등록한다.package hello.board.service;
import hello.board.entity.Board;
import hello.board.entity.User;
import hello.board.repository.BoardRepository;
import hello.board.repository.UserRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class BoardService {
private final BoardRepository boardRepository;
private final UserRepository userRepository;
/**
* 게시글 생성
*/
@Transactional
public Long register(String title, String content, Long userId) {
User user = userRepository.findOne(userId).orElseThrow();
Board board = Board.createBoard(title, content, user);
boardRepository.save(board);
return board.getId();
}
/**
* 게시판 전체 조회
*/
public List<Board> findAll() {
return boardRepository.findAll();
}
/**
* 게시글 단건 조회
*/
public Optional<Board> findOne(Long id) {
return boardRepository.findById(id);
}
/**
* 게시글 수정
*/
@Transactional
public void updateBoard(Long id, String title, String content) {
Board board = boardRepository.findById(id).orElseThrow();
board.update(title, content);
}
/**
* 게시글 삭제
*/
@Transactional
public void deleteById(Long id) {
boardRepository.deleteById(id);
}
}
register
userRepository
에서 해당 회원을 찾는다.createBoard
를 호출해 새로운 Board
엔티티를 생성한다.boardRepository.save(board)
를 호출findAll
: 모든 게시글을 조회한다.findOne
: 해당 아이디를 가진 게시글을 조회한다.updateBoard
: 해당 아이디를 가진 게시글의 제목과 내용을 변경한다.deleteById
: 해당 아이디를 가진 게시글을 삭제한다.package hello.board.service;
import hello.board.entity.Board;
import hello.board.entity.User;
import hello.board.repository.BoardRepository;
import hello.board.repository.UserRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import javax.persistence.EntityManager;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
@Transactional
class BoardServiceTest {
@Autowired BoardService boardService;
@Autowired BoardRepository boardRepository;
@Autowired EntityManager em;
@Test
public void register() {
//given
User user = User.builder().loginId("userA").build();
em.persist(user);
//when
Long registerId = boardService.register("AAA", "BBB", user.getId());
//then
Board board = boardRepository.findById(registerId).orElseThrow();
assertThat(board.getTitle()).isEqualTo("AAA");
assertThat(board.getContent()).isEqualTo("BBB");
assertThat(board.getUser()).isEqualTo(user);
}
@Test
public void updateBoard() {
//given
User user = User.builder().loginId("userA").build();
em.persist(user);
Long registerId = boardService.register("AAA", "BBB", user.getId());
//when
boardService.updateBoard(registerId, "CCC", "DDD");
//then
Board board = boardRepository.findById(registerId).orElseThrow();
assertThat(board.getTitle()).isEqualTo("CCC");
assertThat(board.getContent()).isEqualTo("DDD");
assertThat(board.getUser()).isEqualTo(user);
}
}
쿼리 파라미터 로그를 남기기 위해 외부 라이브러리를 사용한다. ex) p6spy
build.gradle
에 다음 코드를 추가한다.
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.8.0'
p6spy
를 추가하면 테스트를 실행한 후 로그에서 다음과 같이 쿼리를 확인할 수 있다.
insert into users (age, login_id, name, password) values (?, ?, ?, ?)
insert into users (age, login_id, name, password) values (0, 'userA', NULL, NULL);
insert into board (content, register_date, title, user_id) values (?, ?, ?, ?)
insert into board (content, register_date, title, user_id) values ('BBB', '${date}', 'AAA', 17);
createBoard
)를 엔티티에 따로 추가한 다음, 필요한 매개변수만을 받는 생성자와 @Builder
를 사용했다. User
엔티티에도 같은 방법을 사용했다.update
비즈니스 메서드를 추가하는 방식을 사용했다.(도메인 모델 패턴을 사용)