스프링 부트 게시판 프로젝트 - 5 | 게시판 도메인 개발

seren-dev·2022년 8월 22일
0

패키지 이름 수정

domain -> entity


Board 엔티티 수정

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
    • Setter가 열려있으면 변경 포인트가 너무 많아서, 유지보수가 어렵다.
    • Setter 대신 비즈니스 메서드로 작성한다.
    • registerDateLocalDateTime.now()로 설정하여, 게시글 작성 일자를 객체 생성 시점으로 정한다.
  • 비즈니스 메서드 update
    • 게시글의 제목과 내용을 변경한다.

게시글의 제목과 내용을 변경하기 위해 도메인 모델 패턴을 사용했다. 비즈니스 로직 대부분이 엔티티에 있으며, 서비스 계층은 단순히 엔티티에 필요한 요청을 위임하는 역할이다.

엔티티가 비즈니스 로직을 가지고 객체 지향의 특성을 적극 활용하는 것을 도메인 모델 패턴이라 한다. 반대로 엔티티에는 비즈니스 로직이 거의 없고 서비스 계층에서 대부분의 비즈니스 로직을 처리하는 것을 트랜잭션 스크립트 패턴이라 한다.
어떤 패턴이 유지보수 하기 좋은지 고민해야 한다.


게시판 리포지토리 개발

BoardRepository

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> {
}
  • 스프링 데이터 JPA를 사용하면 더 간편하게 리포지토리를 개발할 수 있다.
    • 스프링 데이터 JPA가 JpaRepository를 보고 구현체를 자동으로 만들고 스프링 빈으로 등록한다.

게시판 서비스 개발

BoardService

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
    • 제목, 내용, 회원의 id가 넘어온다.
    • userRepository에서 해당 회원을 찾는다.
    • createBoard를 호출해 새로운 Board 엔티티를 생성한다.
    • boardRepository.save(board)를 호출
  • findAll : 모든 게시글을 조회한다.
  • findOne : 해당 아이디를 가진 게시글을 조회한다.
  • updateBoard : 해당 아이디를 가진 게시글의 제목과 내용을 변경한다.
  • deleteById : 해당 아이디를 가진 게시글을 삭제한다.

게시판 기능 테스트

테스트 요구사항

  • 게시글이 생성되어야 한다.
  • 게시글의 제목과 내용이 변경될 경우, 변경사항이 반영되어야 한다.

BoardServiceTest

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);

고민한 부분

1. 엔티티에 Setter 대신 생성 메서드 추가

  • 엔티티에는 가급적 Setter를 사용하지 않는것이 좋다. 그러면 테스트를 할 때 엔티티를 생성하려면 어떻게 해야하나 고민했고, 생성 메서드(createBoard)를 엔티티에 따로 추가한 다음, 필요한 매개변수만을 받는 생성자와 @Builder를 사용했다. User 엔티티에도 같은 방법을 사용했다.

2. 게시글 수정 기능

  • 게시글을 수정하는 기능을 추가할 때, 서비스 -> 리포지토리에서 엔티티를 수정할지, 아니면 Board 엔티티에서 수정할 지 고민했고, 결국 Board 엔티티에 update 비즈니스 메서드를 추가하는 방식을 사용했다.(도메인 모델 패턴을 사용)

0개의 댓글