자바 스프링 부트 11일차 b01Upload /첨부파일 이미지와 댓글 조회 및 처리

2026. 4. 21. 20:12대우개발원 수업 내용/spring boot, framework

반응형

DTO와 Querydsl을 활용한 첨부파일 및 댓글 통합 처리 시스템 구현

1. 통합 데이터 구조 설계 (BoardImageDTO, BoardListAllDTO)

BoardImageDTO:

게시글에 첨부된 개별 이미지의 정보(UUID, 파일명, 정렬 순서)를 관리하기 위한 전용 데이터 객체임

 

BoardListAllDTO:

목록 화면에서 게시글 기본 정보는 물론, 댓글 개수(replyCount)와 첨부 이미지 리스트(boardImages)를

한 번에 담아 전달하는 통합 DTO임

 

효율적 데이터 전달:

여러 번의 API 호출 없이 한 번의 요청으로 화면 구성에 필요한 모든 데이터를 응답할 수 있도록 구조를 최적화함

 

2. Querydsl Tuple과 BooleanBuilder를 활용한 동적 검색 조회 (BoardSearchImpl.java)

Tuple 기반 조회: 게시글 엔티티와 댓글의 개수(countDistinct)를 동시에 조회하기 위해 Querydsl의 Tuple 기능을 활용하여 복합적인 결과 세트를 구성함

 

동적 조건 생성: BooleanBuilder를 통해 사용자가 선택한 검색 타입(제목, 내용, 작성자)과 키워드에 맞춰 WHERE 절을 유연하게 생성하여 동적 쿼리를 완성함

 

데이터 변환 및 페이징: 조회된 튜플 리스트를 스트림을 통해 BoardListAllDTO로 변환하고, PageImpl을 사용하여 스프링 데이터 표준 페이징 객체로 반환함

3. 계층 간 데이터 변환 및 매핑 로직 커스텀 (BoardService.java)

dtoToEntity: BoardDTO의 평면적인 파일명 리스트를 파싱하여 엔티티 내부의 addImage 메서드를 통해 실제 BoardImage 엔티티 객체로 변환하여 저장 준비를 마침

 

entityToDto: DB에서 읽어온 엔티티와 이미지 셋(imageSet)을 다시 문자열 형태의 파일명으로 재가공하고, 등록일/수정일 등 메타데이터를 포함한 DTO로 역변환함

 

정밀한 제어: ModelMapper와 같은 라이브러리 대신 직접 매핑 메서드를 정의함으로써 이미지 추가/삭제와 같은 복잡한 연관 관계 비즈니스 로직을 정확하게 제어함

 

4. 첨부파일을 포함한 CRUD 기능 고도화 (BoardServiceImpl.java)

등록 및 상세 조회: 게시글 저장 시 이미지도 함께 영속화하며, 상세 조회 시에는 findByIdWithImages를 통해 이미지까지 조인 처리하여 LazyInitializationException을 방지함

 

수정 로직 최적화: 기존 첨부파일을 clearImages()로 모두 제거한 뒤 새로운 파일 리스트를 추가하는 방식으로 연관 관계를 갱신하며 데이터 일관성을 유지함

 

성능 최적화: 게시글 목록 조회 시 연관된 이미지를 가져올 때 @BatchSize 설정을 통해 여러 게시물의 이미지를 IN 절로 묶어 조회함으로써 N+1 문제를 해결함

 

5. 영속성 전이(Cascade) 기반의 연관 데이터 관리 (Board.java)

CascadeType.ALL: 부모인 Board 엔티티의 상태 변화가 자식인 BoardImage에도 그대로 전파되어, 게시글 저장/수정/삭제 시 이미지 정보도 일괄 처리되도록 설정함

 

orphanRemoval = true: 게시글 엔티티 내의 이미지 리스트에서 특정 이미지 객체가 제거되면, DB에서도 해당 행(row)이 자동으로 삭제되도록 고아 객체 제거 기능을 적용함

 

무결성 검증: 테스트 코드를 통해 게시글 삭제 시 연관된 이미지와 댓글 등이 외래키 제약 조건을 위반하지 않고 정상적으로 삭제되는지 최종 확인함


[요약]

DTO와 Querydsl을 활용한 첨부파일 및 댓글 통합 처리 시스템 구현

1. 통합 데이터 구조 설계 (BoardImageDTO, BoardListAllDTO)

2. Querydsl Tuple과 BooleanBuilder를 활용한 동적 검색 조회 구현

3. 계층 간 데이터 변환 및 매핑 로직 커스텀 (BoardService.java)

4. 첨부파일을 포함한 CRUD 기능 고도화 (BoardServiceImpl.java)

5. 영속성 전이(Cascade) 기반의 연관 데이터 관리 (Board.java)

 

BoardImageDTO

BoardListAllDTO

두개를 만들어줌

BoardImageDTO

BoardImageDTO 클래스는 게시글에 첨부된 이미지 정보를 시스템 계층 간에 전달하기 위해 정의된 데이터 객체

더보기
더보기
package com.example.b01.dto;


import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder

public class BoardImageDTO {
    private String uuid;
    private String fileName;
    private int ord;
}

BoardListAllDTO

BoardListAllDTO 클래스는 게시글 목록 화면에서 필요한 모든 정보(게시글 기본 정보, 댓글 수, 첨부 이미지 목록)를 한 번에 담아 전달하는 통합 데이터 객체

더보기
더보기
package com.example.b01.dto;


import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.time.LocalDateTime;
import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder

public class BoardListAllDTO {
    private Long bno;
    private String title;
    private String writer;
    private LocalDateTime regDate;
    private Long replyCount;
    private List<BoardImageDTO> boardImages;
}

 


 게시글의 이미지와 댓글의 숫자까지 처리 가능하도록 서비스 코드를 수정

BoardService interface에 코드 추가

더보기
더보기
// 게시글의 이미지와 댓글의 숫자까지 처리
PageResponseDTO<BoardListAllDTO> listWithAll(PageRequestDTO pageRequestDTO);

BoardServiceImpl

코드에 추가

BoardListAllDTO를 활용하여 게시글 정보, 댓글 개수,

첨부 이미지 전체를 포함한 목록 데이터를 페이징하여 반환하기 위한 서비스 인터페이스 구현 메서드.

더보기
더보기

 

  1. @Override: 부모 인터페이스(BoardService)에 정의된 메서드를 실제로 구현하여 비즈니스 로직을 실행함을 명시함.
  2. PageResponseDTO<BoardListAllDTO>: 단순 리스트가 아닌 페이징 정보(현재 페이지, 전체 개수 등)와 모든 정보를 담은 DTO 리스트를 결합하여 최종 반환 타입으로 지정함.
  3. listWithAll: 게시글뿐만 아니라 이미지와 댓글 카운트 등 연관된 모든 데이터를 한 번에 처리하겠다는 의도를 담은 메서드명임.
  4. PageRequestDTO: 프론트엔드로부터 전달된 페이지 번호, 사이즈, 검색 조건 등의 파라미터를 담고 있는 객체를 인자로 받아 처리함.
  5. 현재 상태: 반환값이 null로 설정되어 있어, 실제 작동을 위해서는 리포지토리의 조회 메서드를 호출하고 결과를 DTO로 변환하는 추가 로직 작성이 필요함.
@Override
public PageResponseDTO<BoardListAllDTO> listWithAll(PageRequestDTO pageRequestDTO) {
    return null;
}

BoardSearch

코드에 추가

기존에 댓글 개수만 포함하던 기능에서 확장하여, 첨부 이미지 목록까지 포함된

BoardListAllDTO를 반환하도록 변경된 리포지토리 인터페이스 메서드

더보기
더보기
    //    Page<BoardListReplyCountDTO> searchWithAll(String[] types,
    Page<BoardListAllDTO> searchWithAll(String[] types,
                                        String keyword,
                                        Pageable pageable);

BoardSearchImpl

코드에 추가

Querydsl Tuple 기능을 활용하여 게시글 엔티티와 댓글 개수를 동시에 조회하고 

BoardListAllDTO로 변환하여 페이징 결과를 반환하는 구현 로직.

더보기
더보기

 

  1. groupBy: 게시글(board)을 기준으로 그룹화하여 각 게시글당 연관된 데이터들을 집계할 수 있는 상태로 만듦.
  2. Tuple: 서로 다른 타입인 Board 엔티티와 Long 타입의 댓글 개수를 한 번에 담기 위해 Querydsl에서 제공하는 복합 결과 객체를 사용함.
  3. countDistinct: 댓글의 중복을 제거한 개수를 계산하여 정확한 댓글 수를 추출하며, 이를 튜플의 두 번째 인자로 저장함.
  4. Stream Map: 조회된 tupleList를 순회하며 엔티티 정보를 추출하고, 빌더 패턴을 통해 최종 화면 전달용 BoardListAllDTO 객체로 변환함.
  5. PageImpl: 변환된 DTO 리스트와 페이지 정보, 전체 데이터 개수(totalCount)를 결합하여 스프링 데이터의 표준 페이지 객체를 생성 및 반환
//    @Override
//    public Page<BoardListReplyCountDTO> searchWithAll(String[] types, String keyword, Pageable pageable) {
//
//            QBoard board = QBoard.board;
//            QReply reply = QReply.reply;
//            // Board 엔터티를 기준으로 JPQLQuery 생성
//            JPQLQuery<Board> boardJPQLQuery = from(board);
//            // Board와 Reply를 left join
//            boardJPQLQuery.leftJoin(reply).on(reply.board.eq(board));
//            //페이지네이션 적용
//            getQuerydsl().applyPagination(pageable, boardJPQLQuery);
//            //쿼리 실행하여 Board 리스트 가져오기
//        List<Board> boardList = boardJPQLQuery.fetch();
//        //가져온 Board 리스트의 각 요소 출력
//        boardList.forEach(board1 -> {
//            System.out.println(board1.getBno()); // 게시글 번호 출력
//            System.out.println(board1.getImageSet()); //이미지 정보 출력
//            System.out.println("--------------"); //구분선 출력
//        });
//        return null; //현재는 반환값이 없으므로 null 반환
//    }

    //튜플처리 추가
    @Override
    public Page<BoardListAllDTO> searchWithAll(String[] types,
                                               String keyword,
                                               Pageable pageable) {
        QBoard board = QBoard.board;
        QReply reply = QReply.reply;
        // Board 엔터티를 기준으로 JPQLQuery 생성
        JPQLQuery<Board> boardJPQLQuery = from(board);
        //Board와 Reply를 left join
        boardJPQLQuery.leftJoin(reply).on(reply.board.eq(board));
        //그룹화 (게시글 기준)
        boardJPQLQuery.groupBy(board);
        //페이지네이션 적용
        getQuerydsl().applyPagination(pageable, boardJPQLQuery);
        //게시글과 댓글 개수를 가져오는 튜플 쿼리 생성
        JPQLQuery<Tuple> tupleJPQLQuery = boardJPQLQuery.select(board,reply.countDistinct());
        //쿼리 실행하여 튜플 리스트 가져오기
        List<Tuple> tupleList = tupleJPQLQuery.fetch();
        // 튜플 리스트를 DTO 리스트로 변환
        List<BoardListAllDTO> dtoList = tupleList.stream().map(tuple -> {
            Board board1 = (Board) tuple.get(board);//게시글 엔터티 가져오기
            long replyCount = tuple.get(1,Long.class); //댓글 개수 가져오기
            //게시글 정보를 DTO로 변환
            BoardListAllDTO dto = BoardListAllDTO.builder()
                    .bno(board1.getBno()) //게시글 번호
                    .title(board1.getTitle()) //제목
                    .writer(board1.getWriter()) //작성자
                    .regDate(board1.getRegDate()) // 등록일
                    .replyCount(replyCount)//댓글 개수
                    .build();

            return dto;
        }).collect(Collectors.toList());
        //전체 게시글 수 가져오기
        long totalCount = boardJPQLQuery.fetchCount();
        // 페이지 객체 생성 후 반환
        return new PageImpl<>(dtoList, pageable, totalCount);
    }
}

BoardRepositoryTests

코드에 추가

게시글, 댓글 개수, 이미지 정보를 통합 조회하는 searchWithAll 메서드의 최종 동작을 확인하기 위한 테스트 코드임.

더보기
더보기

 

  1. PageRequest: 0번 페이지부터 10개씩 조회를 설정하며,
    게시글 번호(bno) 기준 내림차순으로 정렬하여 최신 글이 먼저 나오도록 파라미터를 구성함.
  2. searchWithAll 호출: 검색 조건과 키워드 없이 페이징 정보만 전달하여, 모든 데이터가 포함된 Page<BoardListAllDTO> 형태의 결과를 수신함.
  3. getTotalElements: 전체 데이터 개수를 로그로 출력하여 페이징 처리를 위한 전체 카운트 쿼리가 정상적으로
    실행되었는지 확인함.
  4. forEach 루프: 결과 리스트를 순회하며 각 BoardListAllDTO 객체 내부의 게시글 정보, 댓글 수,
    이미지 목록이 올바르게 담겼는지 로그로 검증함.
  5. @Transactional: 테스트 환경에서 데이터베이스 연결 세션을 유지시켜,
    지연 로딩 관계에 있는 이미지 데이터 등에 접근할 때 예외가 발생하지 않도록 보장
    @Transactional
    @Test
    public void testSearchImageReplyCount() {
        Pageable pageable = PageRequest.of(0, 10,
                Sort.by("bno").descending());
//        boardRepository.searchWithAll(null, null, pageable);
        Page<BoardListAllDTO> result
                = boardRepository.searchWithAll(null, null, pageable);
        log.info("-----------------------------");
        log.info(result.getTotalElements());
        result.getContent().forEach(boardListAllDTO 
                -> log.info(boardListAllDTO));
    }

실행하면


BoardSearchImpl

코드에 추가

Querydsl을 사용하여 게시글 엔티티 정보뿐만 아니라 이미지 목록을 내부에 포함시켜 최종 DTO로 변환하는 상세 과정 요약.

더보기
더보기
  1. 이미지 리스트 변환: board1.getImageSet()을 통해 가져온 엔티티 리스트를 Stream을 사용하여 BoardImageDTO 리스트로 변환함.
  2. sorted: 게시글 이미지 엔티티에 정의된 정렬 기준(순번 등)에 따라 데이터를 재배치하여 화면 출력 순서를 보장함.
  3. DTO 맵핑: 엔티티 내부의 uuid, fileName, ord 값을 꺼내어 BoardImageDTO 빌더에 할당함으로써 데이터 전송 최적화 구조를 생성함.
  4. setBoardImages: 앞서 생성한 게시글 기본 정보 DTO에 변환 완료된 이미지 리스트를 최종적으로 주입하여 통합 DTO를 완성함.
  5. N+1 문제 처리: 이 과정에서 이미지를 조회할 때 앞서 설정한 @BatchSize가 작동하여, 20개 게시물 분량의 이미지를 IN 절 쿼리 한 번으로 효율적으로 로딩
    //튜플처리 추가
    @Override
    public Page<BoardListAllDTO> searchWithAll(String[] types,
                                               String keyword,
                                               Pageable pageable) {
        QBoard board = QBoard.board;
        QReply reply = QReply.reply;
        // Board 엔터티를 기준으로 JPQLQuery 생성
        JPQLQuery<Board> boardJPQLQuery = from(board);
        //Board와 Reply를 left join
        boardJPQLQuery.leftJoin(reply).on(reply.board.eq(board));
        //그룹화 (게시글 기준)
        boardJPQLQuery.groupBy(board);
        //페이지네이션 적용
        getQuerydsl().applyPagination(pageable, boardJPQLQuery);
        //게시글과 댓글 개수를 가져오는 튜플 쿼리 생성
        JPQLQuery<Tuple> tupleJPQLQuery = boardJPQLQuery.select(board,reply.countDistinct());
        //쿼리 실행하여 튜플 리스트 가져오기
        List<Tuple> tupleList = tupleJPQLQuery.fetch();
        // 튜플 리스트를 DTO 리스트로 변환
        List<BoardListAllDTO> dtoList = tupleList.stream().map(tuple -> {
            Board board1 = (Board) tuple.get(board);//게시글 엔터티 가져오기
            long replyCount = tuple.get(1,Long.class); //댓글 개수 가져오기
            //게시글 정보를 DTO로 변환
            BoardListAllDTO dto = BoardListAllDTO.builder()
                    .bno(board1.getBno()) //게시글 번호
                    .title(board1.getTitle()) //제목
                    .writer(board1.getWriter()) //작성자
                    .regDate(board1.getRegDate()) // 등록일
                    .replyCount(replyCount)//댓글 개수
                    .build();
            //게시글 이미지 정보를 DTO로 변환
            List<BoardImageDTO> imageDTOS = board1.getImageSet().stream()
                    .sorted() //정렬
                    .map(boardImage -> BoardImageDTO.builder()
                            .uuid(boardImage.getUuid()) //이미지 UUID
                            .fileName(boardImage.getFileName()) //파일 이름
                            .ord(boardImage.getOrd())//정렬 순서
                            .build()
                    ).collect(Collectors.toList());
            //변환한 이미지 리스트를 DTO에 설정
            dto.setBoardImages(imageDTOS);
            return dto;
        }).collect(Collectors.toList());
        //전체 게시글 수 가져오기
        long totalCount = boardJPQLQuery.fetchCount();
        // 페이지 객체 생성 후 반환
        return new PageImpl<>(dtoList, pageable, totalCount);
    }
}
 

BoardSearchImpl

BooleanBuilder를 사용하여 전달된 검색 타입(types)과 키워드에 따라 동적으로 SQL의 WHERE 절을 구성하는 로직.

더보기
더보기
  1. BooleanBuilder: 쿼리의 조건(Predicate)을 유연하게 결합하기 위한 객체로, 조건이 없을 때는 쿼리에 영향을 주지 않고 조건이 있을 때만 WHERE 절을 생성함.
  2. 동적 루프 처리: types 배열을 순회하며 사용자가 선택한 검색 범위(제목, 내용, 작성자)를 하나씩 확인하여 조건을 누적함.
  3. OR 결합: booleanBuilder.or()를 사용하여 제목 또는 내용 또는 작성자 중 하나라도 키워드를 포함하고 있으면 검색 결과에 포함되도록 설정함.
  4. contains: SQL의 LIKE %keyword% 연산과 동일하게 동작하여, 필드 값 내에 검색어가 포함되어 있는지를 확인하는 부분 일치 검색을 수행함.
  5. where 적용: 최종적으로 생성된 모든 조건 뭉치를 boardJPQLQuery에 주입하여 실제 데이터베이스 쿼리에 반영
// 검색 조건이 있는 경우 처리
if( (types != null && types.length > 0) && keyword != null ){
    BooleanBuilder booleanBuilder = new BooleanBuilder(); // 검색 조건을 담을 BooleanBuilder
    // 검색 타입에 따라 검색 조건 추가
    for(String type: types){
        switch (type){
            case "t": // 제목 검색
                booleanBuilder.or(board.title.contains(keyword));
                break;
            case "c": // 내용 검색
                booleanBuilder.or(board.content.contains(keyword));
                break;
            case "w": // 작성자 검색
                booleanBuilder.or(board.writer.contains(keyword));
                break;
        }
    }//end for
    boardJPQLQuery.where(booleanBuilder);
}//end if

코드를 추가


BoardDTO에 아래 코드 추가

더보기
더보기
private List<String> fileNames;

BoardService

에 코드 추가

 

BoardDTO에 담긴 평면적인 데이터를 데이터베이스 저장에 적합한 Board 엔티티 구조로 변환하는 로직임.

더보기
더보기

 

  1. 빌더 패턴 활용: **Board.builder()**를 사용하여 DTO의 기본 정보(번호, 제목, 내용, 작성자)를 엔티티 객체의 초기 값으로 설정함.
  2. 문자열 파싱: 파일명 배열(fileNames)을 순회하며 split("_")을 통해 UUID실제 파일 이름을 분리 추출함.
  3. 이미지 추가 로직: 분리된 정보를 엔티티 내부의 addImage 메서드에 전달하여, 단순 문자열 데이터를 BoardImage 객체로 생성하고 관리 리스트에 포함시킴.
  4. 계층 간 변환: 서비스 계층에서 전달받은 DTO를 영속 계층(JPA)이 이해할 수 있는 엔티티 형태로 가공하여 데이터 저장 준비를 마침.
  5. 객체 지향적 설계: 이미지 객체를 엔티티 내부에서 직접 생성하고 관리하게 함으로써 게시글과 이미지 사이의 응집도를 높임
// BoardDTO를 Board 엔터티로 변환하는 메서드
default Board dtoToEntity(BoardDTO boardDTO){
    // Board 객체 생성(빌터 패턴 사용)
    Board board = Board.builder()
            .bno(boardDTO.getBno())
            .title(boardDTO.getTitle())
            .content(boardDTO.getContent())
            .writer(boardDTO.getWriter())
            .build();
    // 파일명이 존재하는 경우 이미지 정보를 Board에 추가
    if (boardDTO.getFileNames() != null) {
        boardDTO.getFileNames().forEach(fileName -> {
            // 파일명을"_" 기준으로 분리(UUID, 실제 파일명)
            String[] arr = fileName.split("_");
            // 분리된 정보를 이용하여 Board에 이미지 추가
            board.addImage(arr[0], arr[1]);
        });
    }
    return board; // 변환된 Board 객체 반환
}

BoardServiceImpl

코드 수정

 

전달받은 BoardDTO를 엔티티로 변환하여 데이터베이스에 저장하고 생성된 게시글 번호를 반환하는 등록(Create) 로직임.

더보기
더보기

 

  1. dtoToEntity: 단순한 데이터 묶음인 BoardDTO를 비즈니스 로직과 이미지 정보를 포함할 수 있는 Board 엔티티 객체로 변환함.
  2. 저장 로직: **boardRepository.save(board)**를 호출하여 변환된 엔티티를 데이터베이스에 영구적으로 기록함.
  3. 식별자 추출: 저장 직후 데이터베이스에서 자동 생성된 게시글 번호(bno)를 꺼내어 작업 결과를 확인하는 용도로 사용함.
  4. ModelMapper 대체: 주석 처리된 modelMapper.map 대신 직접 정의한 dtoToEntity를 사용함으로써, 이미지 추가와 같은 복잡한 변환 과정을 더욱 정교하게 제어함.
  5. 결과 반환: 등록이 성공적으로 완료되었음을 알리기 위해 생성된 게시글의 고유 번호(Long)를 최종 반환
    @Override
    public Long register(BoardDTO boardDTO) {
//        Board board = modelMapper.map(boardDTO , Board.class);
        Board board = dtoToEntity(boardDTO);
        Long bno = boardRepository.save(board).getBno();
        return bno;
    }

BoardServiceTests

코드에 추가

 

첨부파일이 포함된 게시글을 실제로 서비스 계층을 통해 등록하고, 

데이터베이스 저장  이미지 처리 로직이 정상적으로 작동하는지 확인하는 테스트 코드임.

더보기
더보기
  1. 서비스 구현체 확인: boardService.getClass().getName()을 로그로 찍어 실제 주입된 객체가 프록시 객체인지 혹은 의도한 구현체인지 체크함.
  2. DTO 데이터 구성: 빌더 패턴을 사용하여 제목, 내용, 작성자 등 기본 데이터를 채운 BoardDTO 객체를 준비함.
  3. 가상 파일 생성: UUID.randomUUID()를 활용하여 실제 파일 업로드 상황과 유사하게 중복되지 않는 파일명 리스트를 생성하고 DTO에 주입함.
  4. 등록 프로세스 실행: boardService.register(boardDTO)를 호출하여 앞서 분석한 DTO -> Entity 변환Repository 저장 과정을 한 번에 수행함.
  5. 결과 검증: 저장이 완료된 후 생성된 게시글 번호(bno)를 로그로 출력하여, DB 시퀀스나 Auto-increment가 정상적으로 작동했음을 확인
@Test
public void testRegisterWithImages() {
    // 현재 주입된 Service 구현체 확ㅇ;ㄴ
    log.info(boardService.getClass().getName());
    //BoardDTO 생성
    BoardDTO boardDTO = BoardDTO.builder()
            .title("File....Sample Title....")
            .content("Sample Content....")
            .writer("user00")
            .build();
    // 첨부파일 리스트 설정
        boardDTO.setFileNames(
                Arrays.asList(
                        UUID.randomUUID()+"_aaa.jpg",
                        UUID.randomUUID()+"_bbb.jpg",
                        UUID.randomUUID()+"_ccc.jpg"
        ));
    // 서비스 계층에 등록 요청
    Long bno = boardService.register(boardDTO);
    // 등록된 게시물 번호 출력
    log.info("bno: "+ bno);
}

실행하면

board와 boardimage에 정상적으로 들어간것을 확인


BoardService

에 코드 추가

데이터베이스의 Board 엔티티 객체를 화면이나 API 반환에 적합한 BoardDTO로 변환하는 역방향 매핑 로직임.

더보기
더보기

 

  1. 기본 필드 변환: **BoardDTO.builder()**를 사용하여 엔티티에 저장된 번호, 제목, 내용, 작성자뿐만 아니라 자동 생성된 **등록일(regDate)**과 **수정일(modDate)**까지 추출하여 DTO에 담음.
  2. 이미지 셋 처리: 엔티티 내부의 imageSetStream API를 활용하여 처리하며, 각 이미지 객체의 UUID파일 이름을 다시 _로 연결된 문자열 형태로 재가공함.
  3. 데이터 정렬: **sorted()**를 호출하여 이미지 엔티티의 기본 정렬 기준(예: ord 값)에 따라 파일명 리스트의 순서를 유지함으로써 일관된 화면 출력을 보장함.
  4. 리스트 변환: 가공된 문자열들을 **Collectors.toList()**로 묶어 DTO의 fileNames 필드에 주입함으로써, 클라이언트가 파일명만으로 이미지에 접근할 수 있게 함.
  5. 표준화된 응답: 데이터베이스 계층의 복잡한 연관 관계 객체들을 단순한 문자열과 기본 타입 위주의 DTO로 변환하여 시스템 간 결합도를 낮춤
// Board 엔티티를 BoardDTO로 변환하는 메서드
default BoardDTO entityToDto(Board board) {
    // BoardDTO 객체 생성(빌터 패턴 사용)
    BoardDTO boardDTO = BoardDTO.builder()
            .bno(board.getBno()) // 게시글 번호 설정
            .title(board.getTitle()) // 제목 설정
            .content(board.getContent()) // 내용 설정
            .writer(board.getWriter()) // 작성자 설정
            .regDate(board.getRegDate()) // 등록일 설정
            .modDate(board.getModDate()) // 수정일 설정
            .build();

    // 게시글에 포함된 이미지 파일명을 리스트로 변환
    List<String> fileNames = board.getImageSet().stream()
            .sorted() // 정렬
            .map(boardImage ->
            boardImage.getUuid() + "_" + boardImage.getFileName()
    ).collect(Collectors.toList());
    // 변환된 파일명 리스트를 DTO에 설정
    boardDTO.setFileNames(fileNames);

    return boardDTO; // 변환된 BoardDTO 객체 반환
}

 


BoardServiceImpl

 

코드를 수정

더보기
더보기
    @Override
    public BoardDTO readOne(Long bno) {
//        Optional<Board> result = boardRepository.findById(bno); //값이 있을수도 없을수도 있어서 Optional
        // Board_image 까지 조인 처리되는 findByImages()를 이용
        Optional<Board> result = boardRepository.findByIdWithImages(bno);
        Board board = result.orElseThrow(); //값 없으면 에러던짐
//        BoardDTO boardDTO = modelMapper.map(board, BoardDTO.class); //화면용 객체 BoardDTO로 이동
        BoardDTO boardDTO = entityToDto(board);
        return boardDTO;
    }

BoardServiceTests

코드에 추가

더보기
더보기
@Test
public void testReadAll() {
    Long bno = 101L;
    BoardDTO boardDTO = boardService.readOne(bno);
    log.info(boardDTO);
    for (String fileName : boardDTO.getFileNames()) {
        log.info(fileName);
    } // end for
}

실행하면

정상적으로 실행되는 모습


수정처리

BoardServiceImpl

코드를 수정

더보기
더보기
@Override
public void modify(BoardDTO boardDTO){
    Optional<Board> result = boardRepository.findById(boardDTO.getBno());
    Board board = result.orElseThrow();
    board.change(boardDTO.getTitle(), boardDTO.getContent());
    // 기존 첨부 파일(이미지) 제거
    board.clearImages();
    // 새로운 첨부 파일이 존재하는 경우 추가
    if (boardDTO.getFileNames() != null) {
        for (String fileName : boardDTO.getFileNames()) {
            // 파일명을 "_" 기준으로 분리 (UUID, 실제 파일명)
            String[] arr = fileName.split("_");
            // 게시글에 이미지 추가
            board.addImage(arr[0], arr[1]);
        }
    }
    boardRepository.save(board);
}

BoardServiceTests

testModify() 부분 코드를 수정

더보기
더보기
    @Test
    public void testModify() {
        // 변경이 필요한 데이터만
        BoardDTO boardDTO = BoardDTO.builder()
                .bno(101L)
                .title("Update content 101....")
                .content("Update content 101......")
                .build();
        // 첨부파일을 하나 추가
        boardDTO.setFileNames(Arrays.asList(UUID.randomUUID()+"_zzz.jpg"));
        boardService.modify(boardDTO);
    }

실행하면

aaa,bbb,ccc.jpg 3개가 사라지고

zzz.jpg가 생긴 모습


삭제

Board.java

더보기
더보기
@OneToMany(mappedBy = "board",
            cascade = {CascadeType.ALL},
            fetch = FetchType.LAZY,
            orphanRemoval = true) // BoardImage 테이블의 board

게시물이 삭제되면 다른것도 삭제하도록 이미 코드가 되어 있기 때문에

테스트 코드만 작성해서 테스트를 해보면

BoardServiceTests

더보기
더보기
@Test
public void testRemoveAll() {
    Long bno = 101L;
    boardService.remove(bno);
}

실행하면

정상적으로 삭제된 것을 확인


조회

BoardServiceImpl

더보기
더보기
    @Override
    public PageResponseDTO<BoardListAllDTO> listWithAll(PageRequestDTO pageRequestDTO) {
        // 검색 타입 배열 가져오기 (제목, 내용, 작성자 등)
        String[] types= pageRequestDTO.getTypes();
        // 검색 키워드 가져오기
        String keyword = pageRequestDTO.getKeyword();
        // 페이지 정보를 생성 (정렬 기준 : 게시글 번호 "bno")
        Pageable pageable = pageRequestDTO.getPageable("bno");
        // 검색 조건과 페이지 정보를 이용하여 데이터 조회
        Page<BoardListAllDTO> result = boardRepository.searchWithAll(types,keyword,pageable);
        // 조회 결과를 PageRequestDTO 객체로 변환하여 반환
        return PageResponseDTO.<BoardListAllDTO>withAll()
                .pageRequestDTO(pageRequestDTO) // 요청 페이지 정보 설정
                .dtoList(result.getContent()) // 조회된 DTO 리스트 설정
                .total((int) result.getTotalElements()) // 전체 데이터 개수 설정
                .build();
//        return null;
    }

BoardServiceTests

더보기
더보기
@Test
public void testListWithAll() {
    PageRequestDTO pageRequestDTO = PageRequestDTO.builder()
            .page(1)
            .size(10)
            .build();
    PageResponseDTO<BoardListAllDTO>  responseDTO
            = boardService.listWithAll(pageRequestDTO);
    List<BoardListAllDTO> dtoList = responseDTO.getDtoList();
    dtoList.forEach(boardListAllDTO -> {
        log.info(boardListAllDTO.getBno()+ ":"
        + boardListAllDTO.getTitle());
        if (boardListAllDTO.getBoardImages() != null) {
            for (BoardImageDTO boardImageDTO :
            boardListAllDTO.getBoardImages()) {
                log.info(boardImage);
            }
        }
        log.info("-------------------------");
    });
}

실행하면

정상적으로 조회된다