Day 92

ChangWoo·2023년 1월 12일
1

중앙 HTA

목록 보기
36/51
post-thumbnail

스프링의 트랜잭션 처리

  • spring framework는 선언적 트랜잭션처리, 프로그래밍적 트랜잭션 처리를 지원한다.
  • spring-tx.jar 라이브러리는 트랜잭션처리를 지원하는 라이브러리다.

선언적 트랜잭션 처리

  • 별도의 트랜잭션처리 코드없이 간단한 설정과 어노테이션을 이용해서 트랜잭션처리를 수행하는 것이다.
  • 스프링의 선언적 트랜잭션 처리
    • @Transactional을 이용한 트랜잭션 처리
      1. TransactionManager 객체를 스프링 컨테인어의 빈으로 등록시킨다.
        • 스프링은 PlatformTransactionManager를 구현한 다양한 트랜잭션매니저를 제공한다.
          • PlatformTransactionManager의 주요 API
            TransactionStatus getTransaction(TransactionDefinition definition)
            현재 진행중인 트랜잭션을 조회한다.
            void commit(TransactionStauts stauts)
            트랜잭션내의 모든 데이터베이스 엑세스 작업을 영구적으로 데이터베이스에 반영시킨다.
            void rollback(TransactionStauts stauts)
            * 트랜잭션내의 모든 데이터베이스 엑세스 작업의 데이터베이스 반영을 전부 취소시킨다.
          • PlatformTransactionManager 인터페이스의 주요 구현 클래스
            DataSourceTransactionManager
            - spring-jdbc, iatis, mybatis 등의 데이터베이스 엑세스 기술을 사용했을 때 사용되는 트랜잭션매니저
            HibernateTransactionManager
            - hibernate를 사용해서 데이터베이스 엑세스 작업을 수행했을 때 사용된다.
            JpaTransactionManager
            - Jpa를 사용해서 데이터베이스 엑세스 작업을 수행했을 때 사용된다.
            JtaTransactionManager
            - Jta(글로벌/분산 트랜잭션)에 대한 트랜잭션처리를 지원한다.
          • 선언적 트랜잭션처리가 적용된 메소드가 실행될 트랜잭션 매니저가 같이 실행된다.
            1. 트랜잭션 매니저가 실행된다.
            1. 새로운 트랜잭션이 실행된다.
              1. 메소드 내에서 데이터베이스 엑세스 작업을 수행할 때마다 해당 작업을 트랜잭션에 포함시킨다.
              2. 메소드 실행이 완료되면 commit을 호출해서 트랜잭션에 포함된 모든 데이터베이스 엑세스 작업을 데이터베이스에 영구적으로 반영시킨다.
                메소드 실행 중 예외(RuntimeException)가 발생하면 rollback을 호출해서 트랜잭션에 포함된 모든 데이터베이스 엑세스 작업을 취소시킨다.
              3. 트랜잭션 매니저가 종료된다.
          • 트랜잭션 매니저를 스프링 컨테이너의 빈으로 등록시키기
<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개 추가했을 때)

  • 위와 같이 댓글 수는 증가되지 않지만, 조회수가 증가한다.
  • 그러나 댓글이 추가되어 있지 않다.
  • @Transactional의 주요 속성
    • isolation
      격리레벨
    • propagation
      전파규칙
    • rollbackFor
      • 롤백 대상이 되는 예외 클래스
      • RuntimeException의 자손이 아닌 예외클래스를 롤백 대상 예외클래스로 지정한다.
    • noRollbackFor

  • Required : 하나가 예외를 발생하면 다른 하나도 영향을 받는다.
  • Requires_new : 하나가 예외를 발생하더라도 다른 하나는 영향을 받지 않는다.
* AOP를 이용한 트랜잭션처리
  • @Transactional은 Transaction으로 연결만 해주는 역할을 하며, PlatformTransactionManager가 실제로 Transaction을 실행한다.
  • PlatformTransactionManager는 자동 commit 기능을 끄고 중간에 예외가 발생하면 rollback을 실행하고, 메소드의 실행이 완전히 종료되었다면 commit을 실행한다.

@Transactional을 이용한 트랜잭션 처리

  • 데이터의 일관성을 위해서 실행된다.
  • 여러 번의 DB엑세스 작업을 통해 한 번이라도 통과하지 못하면 실행X
  • DB 엑세스 작업을 다 통과하면 실행O
  • 항상 서비스에서 트랜잭션 처리가 실행된다.
  • DB 엑세스 작업은 Mapper 혹은 Service에서 하기 때문이다.
  • 트랜잭션 처리를 위해 업무를 Controller에 작성하지 않는 것이다.
  • 클래스 혹은 메소드 안에 spring이 제공하는 어노테이션을 붙인다.
  • 트랜잭션 처리가 필요한 이유 : 데이터 값을 변경/저장/삭제 작업이 2번 이상 있는 경우, 두번째 이상의 작업이 실패하는 경우에 모든 작업을 취소하기 위해서
  • 트랜잭션 처리는 부분적인 허용을 허용하지 않는다.

태그기능

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>                

실행결과

첨부파일기능

스프링에서 첨부파일 업로드 구현하기(multipart 요청 처리하기)

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 번호와 파일명이 입력되었다.

  • 저장 전에는 post번호가 들어가지 않았다가
  • 저장 후에 selectKey로 인해 post 번호가 들어간다.
profile
한 걸음 한 걸음 나아가는 개발자

0개의 댓글