apache commons의 fileupload를 사용했다.
pom.xml
라이브러리를 추가한다.
<!-- https://mvnrepository.com/artifact/commons-fileupload/commons-fileupload -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
servlet-context.xml
1. 인코딩, maxUploadSize, maxInMemorySize를 설정한다. 이 외에도 설정할 수 있다. (api문서 참고)
- maxUploadSize : 한 요청당 업로드가 허용되는 최대 용량. 바이트 단위로 설정. 기본값(-1)은 용량에 제한이 없다.
- maxInMemorySize : 디스크에 저장하지 않고 메모리에 유지하도록 허용하는 바이트 단위의 최대 용량. 사이즈가 이보다 클 경우 이 이상의 데이터는 파일에 저장된다. 기본값은 10240바이트다.
<beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<beans:property name="defaultEncoding" value="UTF-8"/>
<beans:property name="maxUploadSize" value="5242880"/> <!-- 5MB -->
<beans:property name="maxInMemorySize" value="1048576"/> <!-- 1MB -->
</beans:bean>
ver. SpringBoot
스프링부트에서는 application.properties에 다음을 추가한다. 계산할 필요 없이 그대로 적으면 된다.
spring.servlet.multipart.max-file-size=5MB
spring.servlet.multipart.max-request-size=1MB
.jsp
파일 추가하는 부분만 남겼다.
1. form method는 꼭 post여야 한다.
2. form enctype을 "multipart/form-data"로 지정한다.
3. input type은 "file"이다.
4. 파일을 여러 개 저장할 수 있도록 하려면 multiple="multiple"을 지정한다.
5. 특정한 타입의 파일만 저장하려면 accept에 설정한다. 확장자를 제한할 수도 있다.
<form id="writeform" class="text-left mb-3" method="post" enctype="multipart/form-data" action="주소">
<div class="form-group">
<label for="___">파일:</label>
<input type="file" class="form-control-file border" name="___" multiple="multiple" accept="image/*">
</div>
</form>
Controller
1. 파일은 MultipartFile타입으로 들어온다. 파일이 여러 개라면 배열을 사용한다.
2. file이 null이 아니고, 첫 번째 파일이 비어있지 않다면 다음을 수행한다.
2-1. ServletContext를 이용해 실제로 서버가 사용하는 주소를 가져온다. (sts 등에서 사용하는 가상주소이긴하다.) workspace/프로젝트명 주소가 아니다. 아파치 톰캣 기준으로 workspace위치/.metadata/.plugins/org.eclipse.wst.Server.core/tmp0/wtpwebapps/프로젝트명이 실제 위치다. sts VM server는 sts설치위치/pivotal-tc-server/instances/base-instance/wtpwebapps/프로젝트명이 실제 위치다.
2-2. 날짜별로 파일을 분류하기 위해 오늘 날짜로 폴더명을 만든다. 해당 위치가 존재하지 않는다면 File의 mkdirs을 이용해서 새 폴더를 만든다. path를 만들 때, File.separator을 사용해서 운영체제별로 다르게 설정되도록 한다.
2-3. 파일 정보들을 저장할 리스트를 만든다. 파일 정보 객체는 1) 서버의 저장 폴더, 2) 원본 파일 이름, 3) 실제 저장 파일 이름을 담고 있다.
2-4. 파일 개수에 맞춰서 파일 정보 객체를 만든 뒤 set 한다. 이때, 실제 저장 파일 명은 UUID를 사용해 유니크 identifier을 원본 파일명 앞에 붙여서 만든다. 원본 파일명은 확장자 이전까지로 한다.
2-5. 현재 파일을 지정된 위치에 새 이름으로 전송한다.
2-6. 전송까지 끝났다면 현재의 파일 정보 객체를 리스트에 추가한다. 이 리스트는 나중에 한꺼번에 게시글 dto에 추가한다. (당연히 게시글 dto에 이 리스트를 담을 멤버 변수가 있어야 한다.)
@Autowired
private ServletContext servletContext;
@PostMapping("주소")
public String register(게시글Dto dto, @RequestParam("___") MultipartFile[] files, Model model,
HttpSession session, RedirectAttributes redirectAttributes)
throws Exception {
//..생략..
if (files != null && !files[0].isEmpty()) {
String realPath = servletContext.getRealPath("/upload"); //2-1번
String today = new SimpleDateFormat("yyMMdd").format(new Date());
String saveFolder = realPath + File.separator + today;
File folder = new File(saveFolder);
if (!folder.exists()) folder.mkdirs();
List<FileInfoDto> fileInfos = new ArrayList<FileInfoDto>();
for (MultipartFile mfile : files) {
FileInfoDto fileInfoDto = new FileInfoDto();
String originalFileName = mfile.getOriginalFilename();
if (!originalFileName.isEmpty()) {
String saveFileName = UUID.randomUUID().toString()
+ originalFileName.substring(originalFileName.lastIndexOf('.'));
fileInfoDto.setSaveFolder(today); //서버 저장폴더명
fileInfoDto.setOriginFile(originalFileName); //원본 파일 이름
fileInfoDto.setSaveFile(saveFileName); //실제 저장 파일 이름
mfile.transferTo(new File(folder, saveFileName)); //2-5번
}
fileInfos.add(fileInfoDto);
}
//list를 게시글dto에 추가 생략
}
//..생략..
DB insert - DAO
전체적인 순서는 아래 3단계다. 게시글 dto가 가진 file목록이 없다면 2),3)을 실행하지 않고 바로 commit 한다.
1) 게시글 insert
2) insert 된 AUTO_INCREMENT column값 가져오기
3) 2)를 fk로 파일 테이블에 하나씩 저장
중요한 점은, connection의 autocommit 설정을 false로 해서 자동 커밋이 안되게 해야 한다는 것이다. 1)부터 3)까지 중 하나라도 오류가 났을 때 커밋되면 안 되기 때문이다. 모든 작업이 정상적으로 끝났을 때 commit()한다. 예외가 발생하면 rollback()한다.
public void registerArticle(게시글dto dto) throws Exception {
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
conn.setAutoCommit(false);// 자동커밋못하게 해야함
StringBuilder registerArticle = new StringBuilder();
registerArticle.append("insert into 게시글테이블 (컬럼명, 컬럼명, ,..) \n");
registerArticle.append("values (?, ?, ... , ?)");
pstmt = conn.prepareStatement(registerArticle.toString());
pstmt.setString(번호, sql에 넣을 값);
pstmt.executeUpdate();
pstmt.close();//여기까지가 게시글 테이블에 insert
if (dto.getFileInfos() != null) {
String lastNo = "select last_insert_id()";
pstmt = conn.prepareStatement(lastNo);
rs = pstmt.executeQuery();
rs.next();
int articleno = rs.getInt(1);
pstmt.close();//여기까지가 fk 가져오는 부분(AUTO_INCREMENT column 값)
List<FileInfoDto> fileInfos = guestBookDto.getFileInfos();
if (!fileInfos.isEmpty()) {
StringBuilder reigsterFile = new StringBuilder();
reigsterFile.append("insert into file_info (articleno, savefolder, originfile, savefile) \n");
reigsterFile.append("values");
int size = fileInfos.size();
for (int i = 0; i < size; i++) {// 파일 개수만큼 (?,?,..) 늘리기
reigsterFile.append("(?, ?, ?, ?)");
if (i != fileInfos.size() - 1)
reigsterFile.append(",");
}
pstmt = conn.prepareStatement(reigsterFile.toString());
int idx = 0;
for (int i = 0; i < size; i++) {// 가져온 파일 sql에 setting
FileInfoDto fileInfo = fileInfos.get(i);
pstmt.setInt(++idx, articleno);
pstmt.setString(++idx, fileInfo.getSaveFolder());
pstmt.setString(++idx, fileInfo.getOriginFile());
pstmt.setString(++idx, fileInfo.getSaveFile());
}
pstmt.executeUpdate();//여기까지가 게시글 insert
}
}
conn.commit();// 전부 커밋
} catch (SQLException e) {
e.printStackTrace();
conn.rollback();// 예외생겼으면 롤백
} finally {
dbUtil.close(rs, pstmt, conn);
}
}
DB selectAll - DAO
1. 모든 게시글을 select해서 가져온다.
2. 게시글 하나를 가져올 때마다 해당 게시글 번호로 다시 파일테이블을 검색한다. 저장된 파일이 있다면 file list에 더해서 한꺼번에 dto에 set한다.
3. 한 게시글 정보를 파일까지 모두 설정했다면 list에 더한다.
4. 마지막으로 list를 반환한다.
public List<게시글dto> listArticle(Map<String, Object> map) throws Exception {
List<게시글dto> list = new ArrayList<게시글dto>();
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = dataSource.getConnection();
StringBuilder listArticle = new StringBuilder();
listArticle.append("select 게시글정보 \n");
listArticle.append("from 게시글 테이블 \n");
pstmt = conn.prepareStatement(listArticle.toString());
rs = pstmt.executeQuery();
while (rs.next()) {
게시글dto dto = new 게시글dto();
//게시글dto set하기 생략
//---여기서부터 해당 게시글 넘버로 검색한 파일들 가져오기
PreparedStatement pstmt2 = null;
ResultSet rs2 = null;
try {
StringBuilder fileInfos = new StringBuilder();
fileInfos.append("select savefolder, originfile, savefile \n");
fileInfos.append("from file_info \n");
fileInfos.append("where 게시글번호 = ?");
pstmt2 = conn.prepareStatement(fileInfos.toString());
pstmt2.setInt(1, 게시글번호);
rs2 = pstmt2.executeQuery();
List<FileInfoDto> files = new ArrayList<FileInfoDto>();
while (rs2.next()) {
FileInfoDto fileInfoDto = new FileInfoDto();
fileInfoDto.setSaveFolder(rs2.getString("savefolder"));
fileInfoDto.setOriginFile(rs2.getString("originfile"));
fileInfoDto.setSaveFile(rs2.getString("savefile"));
files.add(fileInfoDto);
}
dto.setFileInfos(files);//파일목록 dto에 set
} finally {
dbUtil.close(rs2, pstmt2);
}
list.add(dto);//게시글 목록에 해당 dto 저장
}
} finally {
dbUtil.close(rs, pstmt, conn);
}
return list;
}
DB insert - MyBatis
mapper.xml
1. 게시글을 insert한 뒤에 selectKey를 이용해 마지막 AUTO_INCREMENT 값을 가져온다. 그 값을 다시 dto에 있는 변수에 저장한다.
2. 파일을 insert할 때 게시글 dto를 받아와서 foreach로 반복해 집어넣는다.
2-1. collection에는 반복할 객체명을 넣는다. 여기서 fileInfos는 dto변수명이다.
2-2. item은 현재 반복하고 있는 객체다. 간단하게 쓰려면 미리 별칭을 설정해야한다.
2-3. separator는 반복하면서 뒤에 붙일 문자열을 저장한다. 알아서 마지막 반복에는 붙이지 않는다.
<insert id="registerArticle" parameterType="게시글dto">
insert into 테이블명 (컬럼명, 컬럼명, 컬럼명)
values (#{dto변수명}, #{dto변수명}, #{dto변수명})
<selectKey resultType="int" keyProperty="dto변수명" order="AFTER">
select last_insert_id()
</selectKey>
</insert>
<insert id="registerFile" parameterType="게시글dto">
insert into file_info (articleno, savefolder, originfile, savefile)
values
<foreach collection="fileInfos" item="fileinfo" separator=" , ">
(#{articleNo}, #{fileinfo.saveFolder}, #{fileinfo.originFile}, #{fileinfo.saveFile})
</foreach>
</insert>
Dao
먼저 registerArticle을 해서 fk로 쓸 값이 dto에 저장되도록 한다. 값이 변경된 dto로 file도 insert한다.
@Override
public void registerArticle(게시글Dto dto) throws SQLException {
try (SqlSession sqlSession = SqlMapConfig.getSqlSession()) {
sqlSession.insert(NAMESPACE + "registerArticle", dto);
List<FileInfoDto> fileInfos = dto.getFileInfos();
if (fileInfos != null && !fileInfos.isEmpty()) {
sqlSession.insert(NAMESPACE + "registerFile", dto);
}
sqlSession.commit();
}
}
DB selectAll - MyBatis
mapper.xml
1. resultMap을 설정했다. 칼럼명과 프로퍼티명이 다른 경우에 대해 데이터베이스 별칭을 사용하는 것과 다른 방법으로 명시적인 resultMap을 선언하는 방법이다.
2. collection을 사용해서 List<fileInfo>를 채웠다. 각 요소의 설명은 MyBatis User Guide에서 따왔다.
property | 결과 칼럼에 매핑하기 위한 필드나 프로퍼티. 자바빈 프로퍼티가 해당 이름과 일치한다면, 그 프로퍼티가 사용될 것이다. 반면에 MyBatis는 해당 이름이 필드를 찾을 것이다. 점 표기를 사용하여 복잡한 프로퍼티 검색을 사용할 수 있다. 예를 들어, “username”과 같이 간단하게 매핑될 수 있거나 “address.street.number” 처럼 좀더 복잡하게 매핑될수도 있다. |
column | 데이터베이스의 칼럼명이나 별칭된 칼럼 라벨. resultSet.getString(columnName) 에 전달되는 같은 문자열이다. Note: 복합키를 다루기 위해서, column=”{prop1=col1,prop2=col2}” 문법을 사용해서 여러개의 칼럼명을 내포된 select 구문에 명시할 수 있다. 이것은 대상의 내포된 select 구문의 파라미터 객체에 prop1, prop2 형태로 셋팅하게 될 것이다. |
javaType | 패키지 경로를 포함한 클래스 전체명이거나 타입 별칭. 자바빈을 사용한다면 MyBatis는 타입을 찾아낼 수 있다. 반면에 HashMap 으로 매핑한다면, 기대하는 처리를 명확히 하기 위해 javaType을 명시해야 한다. |
ofType | 리스트의 타입. 미리 별칭을 설정해야 간단하다. |
select | 다른 매핑된 구문의 ID는 이 프로퍼티 매핑이 필요로 하는 복잡한 타입을 로드할 것이다. column속성의 칼럼으로 부터 가져온 값은 대상 select 구문에 파라미터로 전달될 것이다. Note: 복합키를 다루기 위해서, column=”{prop1=col1,prop2=col2}” 문법을 사용해서 여러개의 칼럼명을 내포된 select 구문에 명시할 수 있다. 이것은 대상의 내포된 select 구문의 파라미터 객체에 prop1, prop2 형태로 셋팅하게 될 것이다. |
<resultMap type="게시글dto" id="articleList">
<result column="컬럼명" property="dto변수명"/>
<result column="컬럼명" property="dto변수명"/>
<collection property="fileInfos(dto변수명)" column="articleno=articleno" javaType="list" ofType="fileinfo" select="fileInfoList(파일리스트select할id)"/>
</resultMap>
<select id="listArticle" parameterType="map" resultMap="articleList(위에서_지정한_이름)">
select 가져올컬럼명
from 가져올테이블
</select>
<select id="fileInfoList" resultType="fileinfo">
select savefolder, originfile, savefile
from file_info
where articleno = #{articleno}(조건)
</select>
Dao
mapper보다 dao가 훨씬 간단하다. selectList한 값을 return한다.
@Override
public List<게시글dto> listArticle(Map<String, Object> map) throws SQLException {
try (SqlSession sqlSession = SqlMapConfig.getSqlSession()) {
return sqlSession.selectList(NAMESPACE + "listArticle", map);
}
}
'기록 > BACKEND' 카테고리의 다른 글
[Spring] Interceptor (0) | 2022.04.24 |
---|---|
[Spring] File Download (0) | 2022.04.23 |
[Spring] 예외처리하기 - ControllerAdvice (0) | 2022.04.21 |
[Spring] DI (0) | 2022.04.20 |
[Spring] JNDI 설정하기 (0) | 2022.04.19 |
댓글