[Spring] 예제 2 - 파일 업로드
DB에서 데이터는 날짜, 숫자, 문자만 저장할 수 있다. 이미지 자체는 DB에 저장할 수 없다.
이미지는 WAS에 연결된 HDD에 저장한다.
파일 자체는 WAS 하드디스크에 저장, 파일 연결고리는 DB에 넣는다
글쓰기에서 파일 업로드 구현
✍ writeContentPage.jsp


만약 파일을 업로드 해야하는 경우 form 태그에 enctype="multipart/form-data"을 추가해주어야 한다.
클라이언트 처리
✍ writeContentPage.jsp
multiple="multiple" : 여러 개를 선택할 수 있다.
accept="image/*" : default를 이미지 파일로 설정함.
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>게시글 쓰기</h1>
<!-- 만약 파일을 보내야되는 경우 - enctype="multipart/form-data" 로 꼭 변경해야 한다. 무조건 post 방식-->
<form action="./writeContentProcess" method="post" enctype="multipart/form-data">
작성자 : ${sessionUser.nickname} <br>
제목 : <input type="text" name="title"><br>
내용 : <br>
<textarea rows="10" cols="60" name="content"></textarea>
<br>
<input name="boardFiles" type="file" multiple="multiple" accept="image/*">
<br>
<button>등록</button>
</form>
</body>
</html>
파일 업로드 작업을 수행하기 위해서는 해당 외부 라이브러리를 pom.xml에 추가하여야 한다.
- 외부 라이브러리 pom.xml에 추가
<!-- 업로드용 파일 파서... -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>
여러 라이브러리 추가
<!-- Mail 관련 라이브러리 -->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context-support</artifactId>
<version>5.0.7.RELEASE</version>
</dependency>
<dependency>
<groupId>javax.mail</groupId>
<artifactId>mail</artifactId>
<version>1.4.7</version>
</dependency>
<!-- 업로드용 파일 파서... -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.3.3</version>
</dependency>
<!-- json 변환기 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>2.9.5</version>
</dependency>
파알 업로드를 위한 servlet-context.xml에 bean 등록
<!-- ja : 파일 업로드 관련.. 빈 등록.. -->
<beans:bean id="multipartResolver"
class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<beans:property name="maxUploadSize" value="100000000"></beans:property>
</beans:bean>
- 참고로 jsp에서 파일을 한 개도 선택하지 않은 채 submit을 하여도 null값이 날라오지 않는다.
✍ BoardController
테스트 코드
@RequestMapping("writeContentProcess") // boardFiles : 변수명 jsp에 있는 name과 같아야함
// 파일이 여러 개 전송될 수 있으므로 배열로 받는다. -> 반복문 적용하여 저장
public String writeContentProcess(HttpSession session, BoardDto params, MultipartFile [] boardFiles) {
// 파일 저장 로직
if (boardFiles != null) {
for(MultipartFile file : boardFiles) {
// 이미지가 존재하지 않을 때
if (file.isEmpty()) {
continue;
}
System.out.println("파일명: " + file.getOriginalFilename());
try {
// java.io.file 불러오기, 폴더를 포함한 파일명을 넣는다
file.transferTo(new File("c:\\uploadFiles\\" + file.getOriginalFilename()));
} catch (Exception e) {
e.printStackTrace();
}
}
}
- 참고: 경로에서 uploadFiles는 위 소스코드 작성 이전에 c드라이브에서 임의로 생성한 폴더이다.
위 소스코드를 확정하고 코드를 실행하면 이미 존재한 파일명과 똑같은 파일명으로 새 파일을 업로드하였을 때
uploadFiles 폴더에는 새 파일이 기존에 동일한 파일명을 가진 파일을 덮어씌우게 된다.
그러므로 코드를 아래와 같이 수정해주어야 한다.
@RequestMapping("writeContentProcess") // boardFiles : 변수명 jsp에 있는 name과 같아야함
// 파일이 여러 개 전송될 수 있으므로 배열로 받는다. -> 반복문 적용하여 저장
public String writeContentProcess(HttpSession session, BoardDto params, MultipartFile [] boardFiles) {
// 파일 저장 로직
if (boardFiles != null) {
for(MultipartFile multipartFile : boardFiles) {
// 이미지가 존재하지 않을 때
if (multipartFile.isEmpty()) {
continue;
}
System.out.println("파일명: " + multipartFile.getOriginalFilename());
String rootFolder = "C:/uploadFiles/";
// 날짜별 폴더 생성 로직
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); // 날짜를 문자로 바꿔주는 역할
String today = sdf.format(new Date()); // new Date(): 오늘 날짜 출력
File targetFolder = new File(rootFolder + today); // C:/uploadFolder/2023/05/23
if(!targetFolder.exists()) {
targetFolder.mkdirs(); // 폴더 생성
}
// 저장 파일명 만들기. 핵심은 파일명 충돌 방지 = 랜덤 + 시간
String fileName = UUID.randomUUID().toString();
fileName += "_" + System.currentTimeMillis();
// 확장자 추출
String originalFileName = multipartFile.getOriginalFilename(); // originalFileName : 사용자가 컴퓨터에 올리는 파일명
String ext = originalFileName.substring(originalFileName.lastIndexOf(".")); // lastindexof: 뒤에서부터 .의 위치를 찾아서 숫자를 반환
// 슬래시 주의할 것 기억하기
String saveFileName = rootFolder + today + "/" + fileName + ext;
try {
// java.io.file 불러오기, 폴더를 포함한 파일명을 넣는다
// 다른 이미지이지만 파일명이 같은 경우, 충돌을 피하려면
multipartFile.transferTo(new File(saveFileName));
} catch (Exception e) {
e.printStackTrace();
}
}
}
// 데이터 저장 로직
MemberDto sessionUser = (MemberDto) session.getAttribute("sessionUser"); // key로 값을 뽑아온다.
int memberId = sessionUser.getId(); // 로그인한 회원 아이디 추출
params.setMember_id(memberId);
boardService.writeContent(params);
return "redirect:./mainPage";
}
업로드 한 이미지를 db와 연결시키는 작업을 해야한다,
게시판 이미지 테이블 생성
하나의 게시판에는 여러 개의 이미지를 업로드할 수 있도록 위에서 설계해두었기 때문에
1:N 관계로 설정한다.

파일 업로드 모듈 추가 - Tomcat의 Module창 이동하여 아래와 같이 설정

add external web module 클릭 - 아래 경로로 작성하고 저장

위 작업까지 마치고 아래 링크로 request를 수행한다.

특정 글 번호에 위 링크를 저장해야한다.
2023/05/23/45e7c12e-37aa-4e6b-82b1-8de0f5f44fa3_1684819998645.jpg
위 링크를 복사
이미지 테이블 추가
-- 게시판 이미지 T
DROP TABLE fp_board_image;
CREATE TABLE fp_board_image(
id NUMBER PRIMARY KEY,
board_id NUMBER,
original_filename VARCHAR2(400),
link VARCHAR2(500)
);
DROP SEQUENCE fp_board_image_seq;
CREATE SEQUENCE fp_board_image_seq;
DTO 생성
✍BoardImageDto
package com.ja.finalproject.dto;
public class BoardImageDto {
private int id;
private int board_id;
private String original_filename;
private String link;
public BoardImageDto() {
super();
}
public BoardImageDto(int id, int board_id, String original_filename, String link) {
super();
this.id = id;
this.board_id = board_id;
this.original_filename = original_filename;
this.link = link;
}
public int getId() {
return id;
}
public void setId(int id) {
this.id = id;
}
public int getBoard_id() {
return board_id;
}
public void setBoard_id(int board_id) {
this.board_id = board_id;
}
public String getOriginal_filename() {
return original_filename;
}
public void setOriginal_filename(String original_filename) {
this.original_filename = original_filename;
}
public String getLink() {
return link;
}
public void setLink(String link) {
this.link = link;
}
}
이제 글쓰기 버튼을 누르면 두 개의 테이블에 insert 될 것이다.
✍ BoardSqlMapper
코드 추가
public int createPk();
두 테이블에 데이터가 동시에 insert되므로 primary key를 따로 생성하는 로직을 구현한다. (createPk)
✍ BoardSqlMapper.xml
<select id="createPk" resultType="int">
SELECT fp_board_seq.nextval FROM dual
</select>
<insert id="insert">
INSERT INTO fp_board VALUES(
#{id},
#{member_id},
#{title},
#{content},
0,
SYSDATE
)
</insert>
✍ BoardSqlMapper
// 이미지 등록
public void insertBoardImage(BoardImageDto boardImageDto);
✍ BoardSqlMapper.xml
<insert id="insertBoardImage">
INSERT INTO fp_board_image VALUES(
fp_board_image_seq.nextval,
#{board_id},
#{original_filename},
#{link}
)
</insert>
✍ BoardController
파일 저장 로직 하나를 돌때마다 boardimagedto를 만들 것이다.
각 dto를 ArrayList에 담아서 service에 넘기도록 구현할 것이다.
반복문 한 바퀴를 돌때마다 파일을 저장한다.
writeContent 메서드는 service에서 수정할 것이다.
@RequestMapping("writeContentProcess") // boardFiles : 변수명 jsp에 있는 name과 같아야함
// 파일이 여러 개 전송될 수 있으므로 배열로 받는다. -> 반복문 적용하여 저장
public String writeContentProcess(HttpSession session, BoardDto params, MultipartFile [] boardFiles) {
List<BoardImageDto> boardImageDtoList = new ArrayList<>();
// 파일 저장 로직
if (boardFiles != null) {
for(MultipartFile multipartFile : boardFiles) {
// 이미지가 존재하지 않을 때
if (multipartFile.isEmpty()) {
continue;
}
System.out.println("파일명: " + multipartFile.getOriginalFilename());
String rootFolder = "C:/uploadFiles/";
// 날짜별 폴더 생성 로직
SimpleDateFormat sdf = new SimpleDateFormat("yyyy/MM/dd"); // 날짜를 문자로 바꿔주는 역할
String today = sdf.format(new Date()); // new Date(): 오늘 날짜 출력
File targetFolder = new File(rootFolder + today); // C:/uploadFolder/2023/05/23
if(!targetFolder.exists()) {
targetFolder.mkdirs(); // 폴더 생성
}
// 저장 파일명 만들기. 핵심은 파일명 충돌 방지 = 랜덤 + 시간
String fileName = UUID.randomUUID().toString();
fileName += "_" + System.currentTimeMillis();
// 확장자 추출
String originalFileName = multipartFile.getOriginalFilename(); // originalFileName : 사용자가 컴퓨터에 올리는 파일명
String ext = originalFileName.substring(originalFileName.lastIndexOf(".")); // lastindexof: 뒤에서부터 .의 위치를 찾아서 숫자를 반환
// 슬래시 주의할 것 기억하기
String saveFileName = today + "/" + fileName + ext;
try {
// java.io.file 불러오기, 폴더를 포함한 파일명을 넣는다
// 다른 이미지이지만 파일명이 같은 경우, 충돌을 피하려면
multipartFile.transferTo(new File(rootFolder + saveFileName));
} catch (Exception e) {
e.printStackTrace();
}
BoardImageDto boardImageDto = new BoardImageDto();
boardImageDto.setOriginal_filename(originalFileName);
boardImageDto.setLink(saveFileName);
boardImageDtoList.add(boardImageDto);
}
}
// 데이터 저장 로직
MemberDto sessionUser = (MemberDto) session.getAttribute("sessionUser"); // key로 값을 뽑아온다.
int memberId = sessionUser.getId(); // 로그인한 회원 아이디 추출
params.setMember_id(memberId);
boardService.writeContent(params, boardImageDtoList);
return "redirect:./mainPage";
}
boardid는 set을 못한다.
글쓰기 작업을 수행하고 있을 때에는 게시글이 insert되기 전이므로 게시글 번호가 지정되지 않은 상태이기 때문이다.
service에서 boardid를 지정해줄 것이다.
✍ BoardServiceImpl
public void writeContent(BoardDto boardDto, List<BoardImageDto> boardImageDtoList) {
int boardid = boardSqlMapper.createPk();
boardDto.setId(boardid);
boardSqlMapper.insert(boardDto);
for(BoardImageDto boardImageDto : boardImageDtoList) {
boardImageDto.setBoard_id(boardid);
boardSqlMapper.insertBoardImage(boardImageDto);
}
}
쿼리 테스트

게시글 상세보기할때도 이미지를 출력해야한다.
쿼리 작성
✍ BoardSqlMapper
외래키로 SELECT → List로 출력
public List<BoardImageDto> selectBoardImageByBoardId(int board_id);
✍ BoardSqlMapper.xml
<select id="selectBoardImageByBoardId" resultType="com.ja.finalproject.dto.BoardImageDto">
SELECT * FROM fp_board_image
WHERE board_id = #{board_id}
</select>
✍ BoardServiceImpl
// List일 필요없고 조회할 개시글은 한 개이므로 hashmap
public Map<String, Object> getBoard(int id) {
Map<String, Object> map = new HashMap<>();
BoardDto boardDto = boardSqlMapper.selectById(id);
MemberDto memberDto = memberSqlMapper.selectById(boardDto.getMember_id());
List<BoardImageDto> boardImageDtoList = boardSqlMapper.selectBoardImageByBoardId(id); // id = boardid
map.put("boardDto", boardDto);
map.put("memberDto", memberDto);
map.put("boardImageDtoList", boardImageDtoList);
return map;
}
✍ readContentPage.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
pageEncoding="UTF-8"%>
<%@ taglib uri = "http://java.sun.com/jsp/jstl/core" prefix="c" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<h1>게시글 상세 보기</h1>
제목 : ${data.boardDto.title} <br>
작성자 : ${data.memberDto.nickname} <br>
작성일 : ${data.boardDto.reg_date} <br>
조회수 : ${data.boardDto.read_count} <br>
<br>
<c:forEach items="${data.boardImageDtoList}" var="boardImageDto">
<%-- 절대 경로 --%>
<img src="/uploadFiles/${boardImageDto.link}"> <br>
</c:forEach>
내용 : <br>
${data.boardDto.content}
<br><br>
<a href="./mainPage">목록으로</a>
<c:if test="${!empty sessionUser && sessionUser.id == data.memberDto.id}">
<a href="./updatePage?id=${data.boardDto.id}">수정</a>
<a href="./deleteProcess?id=${data.boardDto.id}">삭제</a>
</c:if>
</body>
</html>
이미지 등록 결과
