[개발일지] 첨부파일 기능 추가

zwon·2023년 10월 19일
0

개발일지

목록 보기
18/23

게시글을 작성할 때 이미지 파일도 업로드 가능하게 기능을 추가하였다.
참고로 파일 업로드를 구현할 때 폼에 enctype="multipart/form-data"를 추가해줘야한다.

해시태그는 구현중이다........

application.yaml

  • 이름엔 제 이름이 들어있어서 우선 저렇게 작성했습니다.
file:
  dir: /Users/이름/Desktop/file-upload/

Board

@Entity
@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class Board extends BaseTimeEntity {

  @Id @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String title;

  @Lob
  private String content;

  @ManyToOne(fetch = FetchType.LAZY) "user" 추가해주면 됨.
  @JoinColumn(name = "user_id")
  private User user;

  @OneToMany(mappedBy = "board", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE) // 글이 삭제되면 댓글 모두 삭제
  @OrderBy("createdDate")
  private List<Reply> replies = new ArrayList<>();

  @OneToMany(mappedBy = "board",fetch = FetchType.LAZY, cascade = CascadeType.ALL)
  private List<ImgFile> imgFiles = new ArrayList<>(); 

  @OneToMany(mappedBy = "board", fetch = FetchType.LAZY, cascade = CascadeType.REMOVE)
  private List<Tag> tags = new ArrayList<>();

  public void setUser(User user){
    this.user = user;
  }

  public void setImgFile(List<ImgFile> imgFiles){
    this.imgFiles = imgFiles;
    for (ImgFile file : imgFiles) {
      file.setBoard(this);
    }
  }

  public void addTag(Tag tag){
    tags.add(tag);
    tag.setBoard(this);
  }
  
  public void update(String title, String content) {
    this.title = title;
    this.content = content;
  }

  @Builder
  public Board(String title, String content) {
    this.title = title;
    this.content = content;
  }
}
  • Board엔티티인데 Board랑 imgFiles는 1:N 관계이다.
  • Board랑 imgFiles는 양방향 관계라서 연관관계 편의 메서드인 setImgFile(...)을 작성해줬는데 뭔가 아쉽다.....좀 더 연관관계 편의 메서드에 대해서 찾아봐야겠다.

ImgFile

@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor(access = AccessLevel.PROTECTED) // 이거 추가해주니까 되네... 이유가..
public class ImgFile extends BaseTimeEntity {

  @Id
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  private Long id;

  private String originFilename; // 업로드된 파일 이름
  private String storeFilename; // 저장한 파일명 ex) 132-5sdf-23451-1as.png -> UUID로 식별자 생성 + 확장자

  @ManyToOne(fetch = FetchType.LAZY)
  @JoinColumn(name = "board_id")
  private Board board;

  @Builder
  public ImgFile(String originFilename, String storeFilename) {
    this.originFilename = originFilename;
    this.storeFilename = storeFilename;
  }

  public void setBoard(Board board) {
    this.board = board;
  }
}

FileStore

  • 파일 처리 관련 코드만을 모아두었다.
  • 파일을 업로드한 파일명은 중복될 수 있기때문에 UUID를 활용하여 DB엔 "UUID + 확장자"로 파일명을 변경하여 DB에 파일명만 저장한다.
  • 파일 자체를 DB에 저장하지않고 보통 스토리지에 저장한다. AWS로 예를 들자면 S3에 저장한다.
  • 그래서 DB엔 파일 자체를 저장하지 않는다.
@Component
public class FileStore {

  @Value("${file.dir}") // application.yaml or application.properties에 지정한 파일 경로
  private String fileDir;
  
  // 파일 저장 경로 Full
  public String getFullPath(String filename){
    return fileDir + filename;
  }
  
  public ImgFile storeFile(MultipartFile multipartFile) throws IOException {
    if (multipartFile.isEmpty()){
      return null;
    }
    String originalFileName = multipartFile.getOriginalFilename();
    String ext = extractedExt(originalFileName);
    String storeFileName = createStoreFileName(ext);
    multipartFile.transferTo(new File(getFullPath(storeFileName)));

    return ImgFile.builder()
        .originFilename(originalFileName)
        .storeFilename(storeFileName)
        .build();
  }
  // 여러 개 업로드 하는 경우
  public List<ImgFile> storeFiles(List<MultipartFile> multipartFiles) throws IOException{
    List<ImgFile> files = new ArrayList<>();
    for (MultipartFile multipartFile : multipartFiles) {
      if (multipartFile.isEmpty()) {
        return null;
      }
      files.add(storeFile(multipartFile));
    }
    return files;
  }
  
  // UUID를 활용해 저장용 파일명
  private String createStoreFileName(String ext) {
    String uuid = UUID.randomUUID().toString();
    String storedFileName = uuid + "." + ext;
    return storedFileName;
  }

  // 파일 확장자 추출
  private String extractedExt(String originalFileName) {
    int pos = originalFileName.lastIndexOf(".");
    String ext = originalFileName.substring(pos + 1);
    return ext;
  }
}

FileService

@Service
@RequiredArgsConstructor
public class FileService {
  private final FileStore fileStore;
  private final FileRepository fileRepository; 

  public List<ImgFile> transferImgFile(List<MultipartFile> imgFile) throws IOException {
    List<ImgFile> imgFiles = fileStore.storeFiles(imgFile);
    return imgFiles;
  }

  public void deleteBeforeFile(Board board){
    if (!board.getImgFiles().isEmpty()) {
      // 이전 첨부파일 삭제
      if (!board.getImgFiles().isEmpty()) {
        for (ImgFile imgFile : board.getImgFiles()) {
          // 파일 시스템에서 실제 이미지 파일 삭제
          File imgFileOnDisk = new File(fileStore.getFullPath(imgFile.getStoreFilename()));
          if (imgFileOnDisk.exists()) {
            imgFileOnDisk.delete();
          }
          // 데이터베이스에서 이미지 파일 레코드 삭제
          this.delete(imgFile);
        }
      }
    }
  }
  public void delete(ImgFile imgFile){
    fileRepository.delete(imgFile);
  }
  
}

FileRepository

public interface FileRepository extends JpaRepository<ImgFile, Long> {

}

BoardSaveRequestDto

@Getter
@Setter
@NoArgsConstructor
@AllArgsConstructor
public class BoardSaveRequestDto {
  private String title;
  private String content;
  private List<MultipartFile> imgFiles;

  public Board toEntity() {
    return Board.builder()
        .title(title)
        .content(content)
        .imgFiles(new ArrayList<>())
        .build();
  }
}

BoardController

  • 게시글 등록 관련한 부분만 가져왔다.
...
// 글 등록
  @PostMapping("/board/form")
  public String boardSave(@ModelAttribute BoardSaveRequestDto boardSaveRequestDto,
                          @RequestBody TagSaveRequestDto tagSaveRequestDto, // 해시태그
                          HttpSession session) throws IOException {
    User loginUser = (User) session.getAttribute("loginUser");
    boardService.save(boardSaveRequestDto, loginUser);
    return  "redirect:/boards";
  }
...

BoardService

  • 파일 처리 관련 부분만 가져왔다.
// 게시판 등록
  private final BoardRepository boardRepository;
  private final FileService FileService;
  
  ...
  @Transactional
  public Long save(BoardSaveRequestDto request, User user) throws IOException {
    List<ImgFile> imgFiles = FileService.transferImgFile(request.getImgFiles());// List<multipartfile> -> List<ImgFile>
    Board board = request.toEntity(); // title, content
    board.setUser(user);
    if (imgFiles == null){
      board.setImgFile(new ArrayList<>()); // 첨부파일이 없더라도 게시글이 저장이 되도록하기 위해서 추가함. 
    } else {
      board.setImgFile(imgFiles); //
    }
    Board savedBoard = boardRepository.save(board);
    return savedBoard.getId();
  }

다음은 게시글을 수정할 때의 로직이다.

BoardService

  • 파일 처리 관련 부분만 가져왔다.
// 게시판 등록
 // 게시글 수정
  @Transactional
  public void update(Long id, BoardUpdateRequestDto request) throws IOException {
    Board board = boardRepository.findById(id)
        .orElseThrow(() -> new IllegalArgumentException("존재하지 않는 포스트입니다."));

    List<ImgFile> imgFiles = FileService.transferImgFile(request.getImgFiles()); // 새로 업로드한 파일
    FileService.deleteBeforeFile(board); // 이전에 업로드 한 파일 삭제

    // 수정한 첨부파일로 연관관계 매핑
    board.setImgFile(imgFiles);
    board.update(request.getTitle(), request.getContent());

실행 화면

  • 화면에 출력하는 html 코드는 생략하겠다.

스토리지

데이터베이스

뭔가 부족함이 많은 코드인거같다. 계속 공부하면서 리팩토링 해야겠다.
피드백을 주시면 감사하겠습니다.........

profile
Backend 관련 지식을 정리하는 Back과사전

0개의 댓글