1.  서비스 계층과 DTO의 구현

BoardRepository의 모든 메서드는 서비스 계층을 통해서 DTO로 변환되어 처리되도록 구성
엔티티 객체는 영속 컨텍스트에서 관리되므로 가능하면 많은 계층에서 사용되지 않는 것이 좋음

'서비스 계층에서 엔티티 객체를 DTO로 변환하거나 반대의 작업'을 처리하도록 함.
  ➡️  이 때 ModelMapper를 이용


1)  ModleMapper 설정

DTO와 엔티티 간의 변환 처리를 간단히 처리하기 위해서 ModelMapper를 이용할 것이므로 build.gradle 파일에 ModelMapper 라이브러리가 존재하는지 우선 확인

implementation 'org.modelmapper:modelmapper:3.1.0'

 

  • 프로젝트에 config 패키지를 구성하고 RootConfig 클래스를 구성
  • @Confuguration을 이용해서 해당 클래스가 스프링의 설정 클래스임을 명시
  • @Bean을 이용해서 ModelMapper를 스프링의 빈으로 설정
@Configuration
public class RootConfig {
    @Bean
    public ModelMapper getMapper() {
        ModelMapper modelMapper = new ModelMapper();
        modelMapper.getConfiguration()
                .setFieldMatchingEnabled(true)
                .setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE)
                .setMatchingStrategy(MatchingStrategies.STRICT);
        return modelMapper;
    }
}

 


 

2) CRUD 작업 처리

프로젝트에 dto 패키지를 추가하고 dto 패키지에 BoardDTO 클래스를 추가
@Data
@Builder
@AllArgsConstructor
@NoArgsConstructor
public class BoardDTO {

    private Long bno;
    private String title;
    private String content;
    private String writer;
    private LocalDateTime regDate;
    private LocalDateTime modDate;

}

 


프로젝트에 service 패키지를 추가하고 BoardService 인터페이스와 BoardServiceImpl 클래스를 추가

 

    📍  등록 작업 처리       -  BoardService 인터페이스에 register()를 선언

public interface BoardService {

    Long register(BoardDTO boardDTO);
}

 

    📍  BoardServiceImpl은 ModelMapper와 BoardRepository를 주입 받도록 구현

@Service
@Log4j2
@RequiredArgsConstructor
@Transactional
public class BoardServiceImpl implements BoardService {

    private final ModelMapper modelMapper;

    private final BoardRepository boardRepository;

    @Override
    public Long register(BoardDTO boardDTO) {
        Board board = modelMapper.map(boardDTO, Board.class);
        Long bno = boardRepository.save(board).getBno();
        return bno;
    }
}

 

  ✓  BoardServiceImpl 클래스에는 의존성 주입 외에도 @Transactional 어노테이션을 적용
         ➡️  @Transactional을 적용하면 스프링은 해당 객체를 감싸는 별도의 클래스를 생성해 내는데, 간혹 여러 번의 데이터베이스 연결이 있을 수도 있으므로 트랜잭션 처리는 기본적으로 적용해 두는 것이 좋음

 

    📍  test 폴더에 service 패키지를 추가하고 BoardServiceTests 클래스를 추가

@SpringBootTest
@Log4j2
class BoardServiceImplTest {
    @Autowired
    private BoardService boardService;

    @Test
    public void registerTest() {
        log.info(boardService.getClass().getName());
    }
}

 

  ✓  실제 boardService 변수가 가르키는 객체의 클래스명을 출력하는데 실행해 보면 BoardServiceImpl이 나오지 않고 스프링에서 BoardServiceImpl을 감싸서 만든 클래스 정보가 출력

 

 

    📍  등록처리의 구현은 다음과 같이 작성  -  콘솔에서 insert문이 동작하는지 확인하고 최종적으로 데이터 베이스에서 확인

    @Test
    public void registerTest() {
        log.info(boardService.getClass().getName());

        BoardDTO boardDTO = BoardDTO.builder()
                .title("test...")
                .content("test...")
                .writer("user")
                .build();

        Long bno = boardService.register(boardDTO);
        log.info("bno: " + bno);
    }

 


 

조회 작업 처리


    📍  조회 작업 처리는 특정한 게시물의 번호를 이용하므로 BoardService와 BoardServiceImpl에 코드를 추가

BoardDTO readOne(Long bno);
   @Override
    public BoardDTO readOne(Long bno) {

        Optional<Board> optionalBoard = boardRepository.findById(bno);
        Board board = optionalBoard.orElseThrow();

        return modelMapper.map(board, BoardDTO.class);
    }

 

Optional<Board> optionalBoard = boardRepository.findById(bno);:

boardRepository.findById(bno)는 주어진 bno를 사용하여 데이터베이스에서 Board 객체를 찾음

이 메서드는 Optional<Board>를 반환 (Optional은 값이 존재할 수도 있고, 존재하지 않을 수도 있는 컨테이너)

이를 통해 NullPointerException을 피할 수 있음

 

Board board = optionalBoard.orElseThrow();:

optionalBoard.orElseThrow()는 Optional 객체가 비어있으면 예외를 던지고, 값이 존재하면 그 값을 반환

여기서는 Optional에 Board 객체가 없으면 기본적으로 NoSuchElementException이 발생

이렇게 함으로써, 해당 bno에 해당하는 Board가 없는 경우 예외가 발생하게 됩니다.

 

return modelMapper.map(board, BoardDTO.class);:

modelMapper.map(board, BoardDTO.class)는 Board 객체를 BoardDTO 객체로 변환

ModelMapper는 객체 변환을 쉽게 해주는 라이브러리로, 여기서는 Board 엔티티를 BoardDTO로 변환하는 데 사용

최종적으로 변환된 BoardDTO 객체를 반환

    @Test
    public void readOneTest() {
        log.info(boardService.readOne(100L));
    }

 

수정 작업 처리


    📍  수정 작업은 기존 엔티티 객체에서 필요한 부분만 변경하도록 작성

void modify(BoardDTO boardDTO);
    @Override
    public void modify(BoardDTO boardDTO) {
        Optional<Board> optionalBoard = boardRepository.findById(boardDTO.getBno());
        Board board = optionalBoard.orElseThrow();
        board.change(boardDTO.getTitle(), boardDTO.getContent());
        boardRepository.save(board);
    }

 

    📍  테스트 할 때는 반드시 실제 데이터베이스에 존재하는 번호를 이용해서 확인

    @Test
    public void testModify() {
        BoardDTO boardDTO = BoardDTO.builder()
                .bno(100L)
                .title("update...100")
                .content("update content 100")
                .build();
        boardService.modify(boardDTO);
    }

 


삭제 작업 처리


    📍  삭제 처리에는 게시물의 번호 (bno)만 필요

void remove(Long bno);
    @Override
    public void remove(Long bno) {
        boardRepository.deleteById(bno);
    }

 


 

3)  목록 / 검색 처리

dto 패키지에 PageRequestDTO, PageResponseDTO 클래스 추가

 

PageRequestDTO

 

    📍  페이징 관련 정보 외에 검색의 종류 type와 키워드 keyword를 추가해서 지정

@Builder
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageRequestDTO {

    @Builder.Default
    private int page = 1;

    @Builder.Default
    private int size = 10;

    private String type; // 검색의 종류 t, c, w, tc, tw, twc

    private String keyword;
}

 

  ✓  검색의 종류는 문자열 하나로 처리해서 나중에 각 문자를 분리하도록 구성

    📍  PageRequestDTO에는 몇 가지 필요한 기능들이 존재

  • 현재 검색 조건들을 BoardRepository에서 String[]로 처리하기 때문에 type이라는 문자열을 배열로 변환해 주는 기능이 필요.
  • 페이징 처리를 위해서 사용하는 Pageable 타입을 반환하는 기능도 필요.
    public String[] getTypes() {
        if (this.type == null || type.isEmpty()) {
            return null;
        }
        return this.type.split("");
    }

    public Pageable getPageable(String...props) {
        return PageRequest.of(this.page - 1, this.size, Sort.by(props).descending());
    }

    private String link;

    public String getLink() {
        if (link == null) {
            StringBuilder stringBuilder = new StringBuilder();
            stringBuilder.append("page=").append(this.page);
            stringBuilder.append("&size=").append(this.size);

            if (type != null && type.length() > 0) {
                stringBuilder.append("&type=").append(this.type);
            }
            if (keyword != null) {
                stringBuilder.append("&keyword=").append(URLEncoder.encode(this.keyword, StandardCharsets.UTF_8));
            }
            link = stringBuilder.toString();
        }
        return link;
    }

 

  • getTypes(): type 문자열을 분리하여 문자열 배열로 반환합니다. type이 null이거나 비어있으면 빈 배열을 반환
  • getPageable(String... props): 페이지 요청(PageRequest) 객체를 생성. 페이지 번호는 page - 1 (0부터 시작하기 때문), 페이지 크기는 size, 정렬 기준은 주어진 속성(props)을 기준으로 내림차순
  • getLink 메소드는 객체의 page, size, type, keyword 값을 기반으로 URL 쿼리 스트링을 동적으로 생성하고, 한 번 생성된 링크를 캐시하여 이후 호출 시 재사용. 이는 불필요한 문자열 연산을 피하기 위해 유용
  • if (type != null && type.length() > 0): type이 null이 아니고 비어 있지 않은 경우에만 type 파라미터를 추가. 예: &type=tc
  • if (keyword != null): keyword가 null이 아닌 경우에만 keyword 파라미터를 추가. 이때 URLEncoder.encode를 사용하여 keyword를 UTF-8로 인코딩. 이는 URL에 안전하게 포함시키기 위해 필요. 예: &keyword=example

 

PageResponseDTO

 

    📍  화면에 DTO 목록과 시작 페이지 / 끝 페이지 등에 대한 처리를 담당

@Getter
@ToString
public class PageResponseDTO<E> {
    private int page; // 페이지 번호
    private int size; // 페이지 크기
    private int total; // 전체 항목수

    private int startPage; // 시작 페이지 번호
    private int endPage; // 끝 페이지 번호

    private boolean prev; // 이전 페이지 존재 여부
    private boolean next; // 다음 페이지 존재 여부

    private List<E> dtoList; // 페이지에 포함된 데이터 목록

    @Builder(builderMethodName = "withAll")
    public PageResponseDTO(PageRequestDTO pageRequestDTO, List<E> dtoList, int total) {
        if (total <= 0) { // 총 항목 수가 0 이하인 경우, 생성자 실행을 중단
            return;
        }
        this.page = pageRequestDTO.getPage(); // 현재 페이지 번호
        this.size = pageRequestDTO.getSize(); // 한 페이지에 포함된 항목 수
        this.total = total;                   // 전체 항목 수
        this.dtoList = dtoList;               // 데이터 리스트

        this.endPage = (int)(Math.ceil(this.page / 10.0) * 10); // 끝 페이지 번호를 계산
        this.startPage = this.endPage - 9;   // 시작 페이지 번호를 계산
        int last = (int) (Math.ceil(this.total / (double)this.size)); // 실제 마지막 페이지 번호를 계산
        this.endPage = Math.min(endPage, last); // 더 작은 값을 사용
        this.prev = this.startPage > 1; // 시작 페이지 번호가 1보다 큰 경우 이전 페이지가 존재함
        this.next = total > this.endPage * this.size; 
        // 전체 항목 수가 끝 페이지 번호에 페이지 크기를 곱한 값보다 큰 경우 다음 페이지가 존재함

    }
}

 

  ✓  PageResponseDTO 클래스는 페이지네이션 정보와 함께 데이터를 전달하기 위한 데이터 전송 객체

  ✓  public PageResponseDTO(PageRequestDTO pageRequestDTO, List<E> dtoList, int total): 생성자로, PageRequestDTO, 데이터 리스트(dtoList), 전체 항목 수(total)를 매개변수로 받는다.

 

BoardService / BoardServiceImpl

 

    📍  list()라는 이름으로 목록 / 검색 기능을 선언

PageResponseDTO<BoardDTO> list(PageRequestDTO pageRequestDTO);
    @Override
    public PageResponseDTO<BoardDTO> list(PageRequestDTO pageRequestDTO) {
        String[] types = pageRequestDTO.getTypes(); // 검색 조건을 문자열 배열로 반환
        String keyword = pageRequestDTO.getKeyword(); // 검색 키워드를 반환
        Pageable pageable = pageRequestDTO.getPageable("bno"); // 페이지 요청 객체를 생성

        // 데이터베이스에서 Board 엔티티를 검색하고 페이징 결과를 Page<Board> 객체로 반환
        Page<Board> boardPage = boardRepository.searchAll(types, keyword, pageable);

        // BoardDTO 객체를 저장할 리스트를 생성
        List<BoardDTO> boardDTOList = new ArrayList<>();
        
        // Board 엔티티를 BoardDTO로 변환하여 boardDTOList에 추가
        for (Board board : boardPage.getContent()) {
            // Board 엔티티를 BoardDTO로 변환
            boardDTOList.add(modelMapper.map(board, BoardDTO.class));
        }
        return PageResponseDTO.<BoardDTO>withAll() // PageResponseDTO의 빌더를 생성
                .pageRequestDTO(pageRequestDTO)
                .dtoList(boardDTOList)
                .total((int)boardPage.getTotalElements())
                .build();
    }

 

  ✓  PageRequestDTO 객체를 사용하여 페이지 요청을 처리하고, 결과를 BoardDTO 객체로 변환하여 PageResponseDTO로 반환

 

테스트 코드로 목록 / 검색 기능 확인


    📍  BoardServiceTests에는 테스트 코드를 작성해서 페이징 처리나 쿼리문이 정상적으로 동작하는지 확인

    @Test
    public void testPaging() {
        PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
                .type("tcw")
                .keyword("user")
                .page(1)
                .size(10)
                .build();
        PageResponseDTO<BoardDTO> responseDTO = boardService.list(pageRequestDTO);
        log.info(responseDTO);
        log.info("1) 페이지 번호: " + responseDTO.getPage());
        log.info("2) 전체 게시물 수: " + responseDTO.getTotal());
        log.info("3) 현재 페이지에 출력될 게시물을 반복문을 이용해서 순서대로 출력: ");
        responseDTO.getDtoList().forEach(boardDTO -> {log.info(boardDTO);});
    }

 

 

 

 

 

 

[ 내용 참고 : IT 학원 강의 ]

+ Recent posts