Java/Spring Boot

[Java / Spring Boot] 파일 첨부

2025. 2. 10. 16:50

파일 업로드

multipart configuration

#multipart configuration
spring.servlet.multipart.enabled=true
spring.servlet.multipart.max-file-size=1MB
spring.servlet.multipart.max-request-size=10MB

enabled

파일 업로드 기능 활성화

 

max-file-size

업로드할 수 있는 파일 최대 크기

 

max-request-size

`HTTP` 요청의 최대 크기

 

attachment 테이블 생성

  • 파일 번호, 이름, 타입, 크기 컬럼 생성
create table attachment(
attachment_no number primary key,
attachment_name varchar2(255) not null,
attachment_type varchar2(255) not null,
attachment_size number not null,
check(attachment_size >= 0)
);

 

AttachmentDto

@Data
public class AttachmentDto {
	private int attachmentNo; // 시퀀스 번호
	private String attachmentName; // 업로드한 파일명
	private String attachmentType; // 업로드한 파일유형
	private long attachmentSize; // 업로드한 파일크기
}

 

AttachmentDao

@Repository
public class AttachmentDao {
	
	@Autowired
	private JdbcTemplate jdbcTemplate;
	
	@Autowired
	private AttachmentMapper attachmentMapper;
	
	public int sequence() {
		String sql = "select attachment_seq.nextval from dual";
		return jdbcTemplate.queryForObject(sql, int.class);
	}
	
	public void insert(AttachmentDto attachmentDto) {
		String sql = "insert into attachment("
				+ "attachment_no, attachment_name, "
				+ "attachment_type, attachment_size"
				+ ") "
				+ "values(?, ?, ?, ?)";
		Object[] data = {
				attachmentDto.getAttachmentNo(),
				attachmentDto.getAttachmentName(),
				attachmentDto.getAttachmentType(),
				attachmentDto.getAttachmentSize()
		};
		jdbcTemplate.update(sql, data);
	}
}

 

AttachmentService

  • 파일이 비어있으면 -1 반환
  • 지정한 경로에 디렉터리 생성
  • `sequence` 생성 후 실제 파일 생성
  • db에 파일 정보 추가
@Service
public class AttachmentService {
	
	@Autowired
	private AttachmentDao attachmentDao;
	
	public int save(MultipartFile attach) throws IllegalStateException, IOException {
		if (attach.isEmpty()) return -1;
		File dir = new File("D:/upload");
		dir.mkdirs();
		
		int attachmentNo = attachmentDao.sequence();
		File target = new File(dir, String.valueOf(attachmentNo));
		attach.transferTo(target);
		
		AttachmentDto attachmentDto = new AttachmentDto();
		attachmentDto.setAttachmentNo(attachmentNo);
		attachmentDto.setAttachmentName(attach.getOriginalFilename());
		attachmentDto.setAttachmentType(attach.getContentType());
		attachmentDto.setAttachmentSize(attach.getSize());
		attachmentDao.insert(attachmentDto);
		
		return attachmentNo;
	}
}

MultipartFile

파일 업로드를 처리하기 위해 제공되는 인터페이스

.isEmpty()

파일이 비어있는지 확인하는 메서드

 

.transferTo()

파일을 지정된 위치로 전송하는 메서드

 

.getOriginalFilename()

파일의 실제 이름을 반환하는 메서드

 

.getContentType()

파일의 컨텐츠 타입을 반환하는 메서드

 

.getSize()

파일의 크기(바이트)를 반환하는 메서드

 

AttachmentController

@Autowired
private AttachmentService attachmentService;

@RequestMapping("/test")
public String test() {
    return "/WEB-INF/views/fileupload/test.jsp";
}

@PostMapping("/upload")
public String upload(@RequestParam String uploader, 
        @RequestParam MultipartFile attach) {
    attachmentService.save(attach);
    return "redirect:test";
}

 

test.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<h1>파일 업로드 테스트</h1>

<form action="upload3" method="post" 
	enctype="multipart/form-data">
	<input type="text" name="uploader"> <br> <br>
    <input type="file" name="attach" accept=".png, .gif, .jpg"> <br> <br>
	<!-- <input type="file" name="attach" accept="image/*"> <br> <br> -->
	<button>업로드</button>
</form>

enctype

데이터의 인코딩 방식을 지정하는 속성

application/x-www-form-urlencoded

  • `get`, `post` 방식으로는 파일의 이름만 전송

 

multipart/form-data

  • `post` 방식으로는 파일 전송 가능

파일 삭제

AttachmentDao

public boolean delete(int attachmentNo) {
    String sql = "delete attachment where attachment_no=?";
    Object[] data = {attachmentNo};
    return jdbcTemplate.update(sql, data) > 0;
}

 

AttachmentService

  • `target`이 파일이 아니면 `return`
  • 실제 파일 삭제
  • db에 파일 정보 삭제
public void delete(int attachmentNo) {
    File dir = new File("D:/upload");
    File target = new File(dir, String.valueOf(attachmentNo));
    if(target.isFile() == false) return;

    target.delete();

    attachmentDao.delete(attachmentNo);
}

 

AttachmentController

  • 예외처리를 하여 파일 삭제
@RequestMapping("/delete")
public String delete(@RequestParam int attachmentNo) {
    try {
        attachmentService.delete(attachmentNo);
    }
    catch(Exception e) {}

    return "redirect:list";
}

 

좀비데이터란?

실제 파일은 삭제되었지만, db에는 삭제되지 않고 남아있는 데이터이다. 이러한 경우 예외처리를 해주지 않아서 발생한다.


파일 다운로드

AttachmentMapper

@Component
public class AttachmentMapper implements RowMapper<AttachmentDto> {

	@Override
	public AttachmentDto mapRow(ResultSet rs, int rowNum) throws SQLException {
		AttachmentDto attachmentDto = new AttachmentDto();
		attachmentDto.setAttachmentNo(rs.getInt("attachment_no"));
		attachmentDto.setAttachmentName(rs.getString("attachment_name"));
		attachmentDto.setAttachmentType(rs.getString("attachment_type"));
		attachmentDto.setAttachmentSize(rs.getLong("attachment_size"));
		return attachmentDto;
	}
	
}

 

AttachmentDao

public AttachmentDto selectOne(int attachmentNo) {
    String sql = "select * from attachment where attachment_no=?";
    Object[] data = {attachmentNo};
    List<AttachmentDto> list = jdbcTemplate.query(sql, attachmentMapper, data);
    return list.isEmpty() ? null : list.get(0);
}

 

apache commons io

파일 제어 라이브러리

https://mvnrepository.com/artifact/commons-io/commons-io/2.18.0

<!-- https://mvnrepository.com/artifact/commons-io/commons-io -->
<dependency>
    <groupId>commons-io</groupId>
    <artifactId>commons-io</artifactId>
    <version>2.18.0</version>
</dependency>

 

AttachmentService

  • db에서 파일 정보를 가져옴
  • db 파일 정보와 실제 파일이 존재하지 않는 경우 예외처리
  • 파일을 `byte[]`로 읽어옴
  • `data` 반환
public byte[] load(int attachmentNo) throws IOException {
	AttachmentDto attachmentDto = attachmentDao.selectOne(attachmentNo);
	if (attachmentDto == null) {
		throw new TargetNotFoundException("존재하지 않는 파일번호");
	}
	
	File dir = new File("D:/upload");
	File target = new File(dir, String.valueOf(attachmentNo));
	if (!target.isFile()) {
		throw new TargetNotFoundException("파일이 존재하지 않습니다");
	}
	
	byte[] data = FileUtils.readFileToByteArray(target);
	
	return data;
}

FileUtils

파일 처리와 관련된 다양한 기능을 제공하는 유틸리티 클래스

.readFileToByteArray()

파일의 내용을 바이트 배열로 읽어들이는 메서드

 

FileDownloadController - 기존 방식

  • HttpServletResponse 객체를 직접 사용하여 응답 처리
  • response.setHeader()로 HTTP 헤더를 설정하고 response.getOutputStream().write(data)로 실제 파일 데이터를 전달
  • 직접적인 스트림 제어가 이뤄짐
@RequestMapping("/download1")
public void download1(@RequestParam int attachmentNo,
        HttpServletResponse response) throws IOException {
    byte[] data = attachmentService.load(attachmentNo);
    AttachmentDto attachmentDto = attachmentDao.selectOne(attachmentNo);

    response.setHeader("Content-Encoding", "UTF-8");
    response.setHeader("Content-Type", attachmentDto.getAttachmentType());
    // response.setHeader("Content-Type", "application/octet-stream");
    response.setHeader("Content-Length", String.valueOf(attachmentDto.getAttachmentSize()));
    response.setHeader("Content-Disposition", "attachment; filename=\"" + attachmentDto.getAttachmentName() + "\"");

    response.getOutputStream().write(data);
}

 

FileDownloadController - ResponseEntity 방식

  • ResponseEntity를 사용하여 HTTP 응답을 포장, 스프링은 ResponseEntity로 응답을 처리할 때, 내부적으로 자동으로 HTTP 응답을 설정하고 처리함
  • ByteArrayResource를 사용하여 파일 데이터를 포장하고, 이를 ResponseEntity의 body로 설정하여 응답을 보냄
  • 스프링의 HTTP 응답 처리 기능을 활용하여 코드가 좀 더 선언적이고, 스프링 기능을 잘 활용하는 방식
@RequestMapping("/download2")
public ResponseEntity<ByteArrayResource> download2(
        @RequestParam int attachmentNo) throws IOException {
    byte[] data = attachmentService.load(attachmentNo);
    AttachmentDto attachmentDto = attachmentDao.selectOne(attachmentNo);

    ByteArrayResource resource = new ByteArrayResource(data);

    return ResponseEntity.ok()
            .header(HttpHeaders.CONTENT_ENCODING, "UTF-8")
            .header(HttpHeaders.CONTENT_TYPE, attachmentDto.getAttachmentType())
			.contentType(MediaType.APPLICATION_OCTET_STREAM)
            .contentLength(attachmentDto.getAttachmentSize())
            .header(HttpHeaders.CONTENT_DISPOSITION, 
                    ContentDisposition.attachment()
                    .filename(attachmentDto.getAttachmentName(),
                            StandardCharsets.UTF_8)
                    .build().toString()
            )
            .body(null);
}

ResponseEntity

사용자에게 내보낼 정보가 포장된 개체

ResponseEntity.ok()

HTTP 상태 코드 200 ok 응답을 생성

 

.header(HttpHeaders.CONTENT_ENCODING, "UTF-8")

응답의 콘텐츠 인코딩을 UTF-8로 설정

 

.header(HttpHeaders.CONTENT_TYPE, attachmentDto.getAttachmentType())

응답의 콘텐츠 타입을 설정

 

.contentType(MediaType.APPLICATION_OCTET_STREAM)

바이너리 데이터 전송을 위한 application/octet-stream을 지정

 

.contentLength(attachmentDto.getAttachmentSize())

응답의 콘텐츠 크기를 설정

 

.header(HttpHeaders.CONTENT_DISPOSITION, ...)

응답 헤더에 Content-Disposition을 설정하여 브라우저에서 파일을 다운로드하도록 유도

 

ContentDisposition.attachment()

파일을 다운로드 가능한 형태로 설정

 

.filename(attachmentDto.getAttachmentName(), StandardCharsets.UTF_8).build().toString()

파일 이름을 attachmentDto에서 가져오고 UTF-8로 인코딩하여 설정

 

body(null)

응답 본문에 실제 데이터는 없음을 의미

 

ByteArrayResource

바이트 배열을 기반으로 하는 리소스를 처리하는 객체

 

'Java > Spring Boot' 카테고리의 다른 글

[Java / Spring Boot] ControllerAdvice  (1) 2025.02.11
[Java / Spring Boot] Service  (0) 2025.02.10
[Java / Spring Boot] DBCP  (0) 2025.02.10
[Java / Spring Boot] AOP와 Interceptor, Configuration  (2) 2025.01.15
[Java / Spring Boot] Session  (0) 2025.01.15
'Java/Spring Boot' 카테고리의 다른 글
  • [Java / Spring Boot] ControllerAdvice
  • [Java / Spring Boot] Service
  • [Java / Spring Boot] DBCP
  • [Java / Spring Boot] AOP와 Interceptor, Configuration
개발하는 벌꿀오소리
개발하는 벌꿀오소리
겁없는 벌꿀오소리처럼 끊임없이 도전하자!
  • 글쓰기 관리
  • 개발하는 벌꿀오소리
    벌꿀오소리의 개발 노트
    개발하는 벌꿀오소리
  • 전체
    오늘
    어제
    • 분류 전체보기 (74)
      • Java (60)
        • 기본 (23)
        • 모듈 (8)
        • 자료구조 (5)
        • 알고리즘 (0)
        • 파일 입출력 (5)
        • JDBC (5)
        • Spring Boot (14)
      • Oracle (13)
      • Project (1)
  • 블로그 메뉴

    • 홈
    • 태그
    • 방명록
  • 인기 글

  • 공지사항

  • hELLO· Designed By정상우.v4.10.3
개발하는 벌꿀오소리
[Java / Spring Boot] 파일 첨부
상단으로

티스토리툴바