<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" / > // Connection pool 객체를 의존성 주입시킨다.
</bean>
2. @Transactional 어노테이션을 활성화시키는 객체를 스프링 컨테이너의 빈을 등록시킨다.
<tx:annotation-driven />
3. 트랜잭션처리가 필요한 메소드를 포함하고 있는 인터페이스/클래스 혹은 해당 메소드에 @Transactional 어노테이션을 추가한다.
database-context.xml
<!--
@Transactional을 이용하는 선언적 트랜잭션 처리
- 트랜잭션매니저를 스프린컨테이너의 빈으로 등록한다.
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
- @Transactional 어노테이션을 활성화시킨다.
<tx:annotation-driven transaction-manager="transactionManager"/>
* <tx:annotation-driven /> 태그는
@Transactional 어노테이션을 감지하고,
해당 어노테이션이 사용된 클래스나 메소드가 실행될 때 자동으로 트랜잭션매니저를 실행시키는 객체를 스프링컨테이너의 빈으로 등록시킨다.
-->
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
<property name="dataSource" ref="dataSource" />
</bean>
<tx:annotation-driven transaction-manager="transactionManager"/>
</beans>
UserService.java
@Service
@Transactional
public class UserService {
PostService.java
@Service
@Transactional
public class PostService {
실행 전(@Transactional 사용 전 댓글을 1000000개 추가했을 때)
실행결과(@Transactional 사용 전 댓글을 1000000개 추가했을 때)
- 댓글 수는 증가되지 않지만, 조회수가 증가하고 댓글이 추가되어있다.
실행결과(@Transactional 사용 후 댓글을 1000000개 추가했을 때)
- 위와 같이 댓글 수는 증가되지 않지만, 조회수가 증가한다.
- 그러나 댓글이 추가되어 있지 않다.
* AOP를 이용한 트랜잭션처리
form.jsp
<div class="mb-2">
<label class="form-label">태그</label>
<!-- 태그를 입력하는 입력필드 -->
<input type="text" class="form-control" id="tag-input" />
<!-- 입력된 태그가 표시되는 곳 -->
<div id="tag-btn-box" class="me-3 pt-1 ps-1 mt-1"></div>
<!-- 입력된 태그를 서버로 전송하기 위해서 태그값을 가진 hidden 필드가 추가되는 곳 -->
<div id="tag-box" class="me-3 pt-1 ps-1"></div>
</div>
<script>
$(function() {
// 입력필드 엘리먼트 선택된 jQuery 객체
let $tagInput = $("#tag-input");
// 버튼모양 태그가 표시되는 엘리먼트 선택된 jQuery 객체
let $tagBtnBox = $("#tag-btn-box");
// 히든필드가 추가되는 엘리먼트 선택된 jQuery 객체
let $tagBox = $("#tag-box");
// $("#tag-input")는 $tagInput으로 적어도 된다.
// event.which는 입력된 키의 아스키코드값(유니코드값)을 반환한다.
// (event.which == 13) --> Enter키의 아스키코드값은 13이다.
// if (event.which == 13) { Enter키를 입력했을 때
// 입력필드의 값을 읽어온다.
// let value = $tagInput.val();
// 버튼 모양의 태그를 생성한다.
// let tagBtn = `<small>#\${value} <a href=""><i class="bi bi-x"></i></a></small>`;
// 히든필드 태그를 생성한다.
// let tag = `<input type="hidden" name="tags" value="\${value}">`;
// 각각의 div에 추가한다.
// $tagBtnBox.append(tagBtn);
// $tagBox.append(tag);
// 입력필드의 값을 삭제한다.
// $tagInput.val("");
// 입력필드에서 Enter키를 치면 submit이 된다. (그래서 return false를 적어준다.)
// 키 입력은 되어야 하기 때문에 return true / Enter키를 치면 submit이 되어야 하기 때문에 return false를 해준다.
// Enter 키 입력에 대한 기본 동작은 폼을 서버로 제출하는 것인데, 그 기본동작이 일어나지 않게 하기 위해서 false값을 반환한다.
// return false;
// Enter 키를 제외한 다른 키에 대해서는 기본동작이 일어나게 하기 위해서 true를 반환한다.
// return true;
// }
$("#tag-input").keydown(function(event) {
if (event.which == 13) {
let value = $tagInput.val();
if (value == "")
return false;
let tagBtn = `
<small class="border rounded bg-secondary p-1 text-white">#\${value} <a href="" class="text-white text-decoration-none"><i class="bi bi-x"></i></a></small>
`;
let tag = `
<input type="hidden" name="tags" value="\${value}">
`
$tagBtnBox.append(tagBtn);
$tagBox.append(tag);
$tagInput.val("");
return false;
}
return true;
})
$("#form-post").submit(function() {
let title = $("#form-post input[name=title]").val();
let content = $("#form-post textarea[name=content]").val();
if (title == "") {
alert("제목을 입력하세요");
return false;
}
if (content == "") {
alert("내용을 입력하세요");
return false;
}
return true;
});
});
</script>
실행결과
1. apache commons fileupload 라이브러리 의존성을 추가한다.
pom.xml
<!-- 파일 업로드를 지원하는 라이브러리 의존성을 추가한다. -->
<dependency>
<groupId>commons-fileupload</groupId>
<artifactId>commons-fileupload</artifactId>
<version>1.4</version>
</dependency>
2. MultipartResolver 객체를 스프링 컨테이너의 빈으로 등록시킨다.
web-context.xml
<!--
multipart 요청(첨부파일 업로드 요청)을 지원하는 MultipartResolver 객체를 스프링 컨테이너의 빈으로 등록시킨다.
반드시, id가 "multipartResolver"로 지정해야 한다. (꼭 지정된 id를 써야 하는 것이 몇개 있는데 그 중 하나다.)
-->
<bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
<property name="defaultEncoding" value="UTF-8"></property>
<!-- 한번에 업로드 할 수 있는 파일의 최대 사이즈 (-1은 무제한 / MB 구하는법: 1024*1024*N ) -->
<property name="maxUploadSize" value="10485760"></property>
<!-- 한 파일 당 업로드 할 수 있는 파일의 최대 사이즈 (-1은 무제한 / MB 구하는법: 1024*1024*N )-->
<property name="maxUploadSizePerFile" value="10485760"></property>
</bean>
3. 폼 작성하기
예시
<form method="oist" action="insert" cnctype="multipart/form-data">
제목
<input type="text" name="title" />
내용
<textarea name="content"></textarea>
첨부파일
<input type="file" name="upfile1" /> // name은 아무거나 해도 상관없다.
<input type="file" name="upfile2" disabled/>
</form>
form.jsp
<form id="form-post" class="border bg-light p-3" method="post" action="insert" enctype="multipart/form-data">
4. 폼 클래스 작성하기
예시
public class PostRegisterForm {
private String title;
private String content;
// 업로드된 첨부파일 정보를 표현하는 MultipartFile 객체가 대입되는 멤버변수
private MultipartFile upfile1; // 무조건 MultipartFile로 해야 한다! (첨부파일의 정보가 들어있다.), MultipartFile 객체가 들어있다.
private MultipartFile upfile1; // null이 들어있다.
// 업로드된 첨부파일의 이름이 대입되는 멤버변수
private String filename1;
private String filename2;
// Getter, Setter 메소드 작성
}
PostRegisterForm.java
private MultipartFile upfile; // <input type="file" name="upfile" / 업로드 할 파일이 여러개면 List로 하면 된다.>
private List<String> tags; // <input type="hidden" name="tags" value="스프링"><"input type="hidden" name="tags">
private String filename; // <form> 태그에는 없는 필드임. 첨부파일 이름을 저장하기 위해서 생성한 변수임
public MultipartFile getUpfile() {
return upfile;
}
public void setUpfile(MultipartFile upfile) {
this.upfile = upfile;
}
public List<String> getTags() {
return tags;
}
public void setTags(List<String> tags) {
this.tags = tags;
}
public String getFilename() {
return filename;
}
public void setFilename(String filename) {
this.filename = filename;
}
MultipartFile
- 업로드된 첨부파일의 정보를 표현하는 객체다.
- 파일이 업로드 되지 않아도 MultipartFile 객체는 생성된다.
- 주요 API
boolean isEmpty() - 업로드된 파일이 없으면 true를 반환한다. String getOriginalFilename() - 업로드된 파일명을 반환한다. String getContentType() - 업로드된 파일의 컨텐츠 타입을 반환한다. 예) text/plain, image/png long getSize() - 업로드된 파일의 크기를 바이트 단위로 반환한다. byte[] getBytes() - 업로드된 파일 데이터를 반환한다. InputStream getInputStream() - 업로드된 파일을 읽어오는 스트림을 반환한다. void transferTo(File dest) - 지정된 목적지에 업로드된 파일을 전송한다.
5. 요청핸들러 메소드에서 업로드된 첨부파일 처리하기
- 업로드된 첨부파일의 이름을 조회해서 테이블에 저장되게 한다.
- 업로드된 첨부파일을 지정된 위치에 저장시킨다.
예시
public class PostController {
//@value("${file.save.directory}")
//private String directory;
또는
private String directory = "c:/files";
@PostMapping("/insert")
public String insert(@LoginUser LoginUserInfo loginuserInfo, PostRegisterForm form) {
// 첨부파일 처리하기
MultipartFile upfile1 = form.getUpfile1();
MultipartFile upfile2 = fome.getUpfile2();
if (!upfile1.isEmpty()) {
// 파일이름을 조회해서 Form객체에 저장한다.
String filename1 = upfile1.getOriginalFilename();
form.setFilename1(filename1);
// 첨부파일을 지정된 위치에 복사한다.(=저장시킨다.)
FileCopyUtils.copy(upfile1.getInputStream(), new FileOutputStream(new File(directory, filename1)));
}
if (!upfile2.isEmpty()) {
String filename2 = upfile2.getOriginalFilename();
form.setFilename2(filename2);
FileCopyUtils.copy(upfile2.getInputStream(), new FileOutputStream(new File(directory, filename2)));
}
postService.insertPost(loginUserInfo.getUserId(), form);
}
}
PostController
@Controller
@RequestMapping("/post")
public class PostController {
// 업로드 될 위치를 지정한다.
private final String directory = "c:/files";
@PostMapping("/insert")
public String insertPost(@LoginUser LoginUserInfo loginUserInfo, PostRegisterForm form) throws IOException {
// 첨부파일 업로드 처리
MultipartFile upfile = form.getUpfile();
if (!upfile.isEmpty()) {
// 첨부파일 이름을 조회하고, PostRegisterForm 객체에 대입한다.
String filename = upfile.getOriginalFilename();
form.setFilename(filename); // postService에게 주기 위해 form으로 지정한다.
// 첨부파일을 지정된 디렉토리에 저장한다.
FileCopyUtils.copy(upfile.getInputStream(), new FileOutputStream(new File(directory, filename)));
}
postService.insertPost(loginUserInfo.getId(), form);
return "redirect:list";
}
실행결과
- C 디스크 밑 files 폴더에 업로드한 폴더가 저장되어 있다.
- 현재는 개인 컴퓨터라 폴더에 저장되지만, 클라우드나 서버에 저장된다.
첨부파일 기능 첫 번째 방법
첨부파일 기능 두 번째 방법(selectKey)
- selectKey를 통해 시퀀스번호를 발급받아서 post 번호에 들어간다.
- spring_post_attached_files 테이블의 post_no는 spring_posts의 post_no를 참조한다.
- spring_posts = 부모 테이블 / spring_post_attached_files = 자식 테이블인데,
부모 테이블과 자식 테이블이 같은 정보를 저장해야 할때는 selectKey를 사용하면 유리하다.post.xml <!-- void insertPost(Post post); <insert id="insertPost" parameterType="Post"> insert into spring_posts (post_no, post_title, post_user_id, post_content) values (spring_posts_seq.nextval, #{title}, #{userId}, #{content}) </insert> --> <insert id="insertPost" parameterType="Post"> <selectKey keyProperty="no" resultType="int" order="BEFORE"> select spring_posts_seq.nextval from dual </selectKey> insert into spring_posts (post_no, post_title, post_user_id, post_content) values (#{no}, #{title}, #{userId}, #{content}) </insert>
<insert id="insertAttachedFile" parameterType="AttachedFile">
insert into spring_post_attached_files
(post_no, file_name)
values
(#{postNo}, #{filename})
</insert>
- 위의 주석은 첫 번째 방법 밑에는 selectKey를 활용한 두번째 방법
AttachedFile.java
package com.sample.vo;
import org.apache.ibatis.type.Alias;
@Alias("AttachedFile")
public class AttachedFile {
private int postNo;
private String Filename;
public AttachedFile() {}
public int getPostNo() {
return postNo;
}
public void setPostNo(int postNo) {
this.postNo = postNo;
}
public String getFilename() {
return Filename;
}
public void setFilename(String filename) {
Filename = filename;
}
}
PostMapper.java
void insertAttachedFile(AttachedFile attachedFile);
PostService.java
// 게시글 등록 서비스
public void insertPost(String userId, PostRegisterForm form) {
Post post = new Post();
post.setUserId(userId);
BeanUtils.copyProperties(form, post);
postMapper.insertPost(post);
AttachedFile attachedFile = new AttachedFile();
attachedFile.setPostNo(post.getNo());
attachedFile.setFilename(form.getFilename());
postMapper.insertAttachedFile(attachedFile);
}
```
실행결과
- DB에 post 번호와 파일명이 입력되었다.