관리 메뉴

거니의 velog

231207_SPRING 2 (12-2) 본문

대덕인재개발원_웹기반 애플리케이션

231207_SPRING 2 (12-2)

Unlimited00 2023. 12. 7. 08:24

package kr.or.ddit.controller.transaction;

public class TransactionController {

	/*
	 * [ 15장. 트랜잭션 ]
	 * 
	 * 		1. 트랜잭션 설명
	 * 
	 * 			- 한 번에 이루어지는 작업의 단위를 의미한다.
	 * 
	 * 				# 트랜잭션 성격(ACID 원칙)
	 * 
	 * 				- 원자성(Actomicity)
	 * 					> 하나의 트랜잭션은 모두 하나의 단위로 처리되어야 한다.
	 * 				- 일관성(Consistency)
	 * 					> 트랜잭션이 성공했다면 모든 데이터는 일관성을 유지해야 한다.
	 * 				- 격리성(Isolation)
	 * 					> 트랜잭션으로 처리되는 동안에는 외부에서의 간섭이 없어야 한다.
	 * 				- 영속성(Durability)
	 * 					> 트랜잭션이 성공적으로 처리되면 그 결과는 영속적으로 보관되어야 한다.
	 * 
	 * 		2. 트랜잭션 설정
	 * 
	 * 			# 스프링 설정
	 * 
	 * 				- root-context.xml 설정
	 * 					> 네임스페이스, 스키마 추가
	 * 				- transactionManager 빈 등록
	 * 				- 어노테이션 기반의 트랜잭션 제어 활성화
	 * 
	 * 		3. 트랜잭션 적용
	 * 
	 * 			[테스트는 AOP가 적용된 CrudBoardController와 연계된 ServiceImpl에서 진행합니다.]
	 * 			[테스트는 AOP가 적용된 CrudMemberController와 연계된 ServiceImpl에서 진행합니다.]
	 * 
	 * 			# 트랜잭션 상황 부여
	 * 
	 * 			예) 회원은 반드시 하나의 권한을 가진다는 비즈니스 규칙이 있다.
	 * 				회원과 회원권한 테이블이 각각 개별로 존재하지만 회원 정보를 저장할 때 반드시 회원권한 정보를 동시에 저장해야 한다.
	 * 				클래스나 메소드에 대해 @Transactional 어노테이션을 부여하여 트랜잭션을 적용할 수 있다.
	 * 
	 * 			# @Transactional 어노테이션의 예외처리 견해
	 * 
	 * 			@Transactional을 적용하고 중간에 에러를 발생시킨 후, 롤백된 상태를 확인하려고 했으나 롤백이 되지 않고
	 * 			데이터베이스에 데이터가 등록된다. 그 이유는 스프링 프레임워크에서 @Transactional은 기본적으로 
	 * 			Checked Exception에 대해서는 롤백 처리를 하지 않도록 설계되어 있다.
	 * 			기본적으로 스프링에서 트랜잭션 처리는 RuntimeException계열(Unchecked Exception)이라면
	 * 			Rollback 처리를 한다.
	 * 
	 *			여기서, 트랜잭션으로 국한된 롤백 정책이 아니라 스프링 프레임워크에서의 기본 정책에 대한 내용일 뿐이다.
	 *			실제로 '트랜잭션 롤백 처리', 'Checked Exception vs Unchecked Exception'등 검색해 보면
	 *			잘못된 정보들로 복사/붙여넣기 되어 있는 내용이 수두룩하여 많은 에러 정보를 공유하고 있다.
	 *			(모든 블로그기 그렇진 않지만, 대표적인 표가 무수히 많이 공유되고 있음을 알 수 있다)
	 *
	 *			***
	 *			'예외처리 시 트랜잭션 처리'에 대한 정보가 잘못 표기된 경우가 많은데...
	 *			이럴 때 되고 저럴 때 되는 것이 아니라, 기본적인 스프링 트랜잭션 정책이 있고 그 안에서 정책 이외의 옵션으로
	 *			특정 에러가 발생했을 때 롤백을 진행할 수 있도록 제공한다. (많은 블로그들을 100% 믿지 말길 바랄게요...)
	 *			혹여나 블로그를 관리하는 사람이 있다면 검증된 내용으로만 적어주길...
	 *
	 *			# Exception(예외)와 Error(에러)
	 *
	 *				- Exception : 개발 알고리즘에서 발생하는 오류로 개발자가 작성한 코드에서 발생하므로
	 *								예외를 상황에 맞춰 처리할 수 있다.
	 *				- Error : 시스템에서 발생하는 심각한 수준의 에러로 개발자가 미리 예측하여 대응할 수 없기 때문에 예외처리에
	 *							대한 부분을 신경쓰지 않아도 된다.
	 *
	 *				CheckedException과 UncheckedException
	 *				- RuntimeException의 상속 여부에 따라 Checked Exception 과 Unchecked Exception 으로 나누어진다.
	 *
	 *			------------------------------------------------------------------------------------
	 *						|	CheckedException				|	UncheckedException
	 *			------------------------------------------------------------------------------------
	 *			예외처리 여부	|	반드시 예외 처리 코드가 있어야 한다.			|	강제로 예외 처리는 아니다.
	 *			예외 확인 시점	|	컴파일 단계에서부터 컴파일이 되지 않는다.		|	런타임중 예외가 확인된다.
	 *			클래스		|	IOException, SQLException...	|	NullPointException, IndexOutOf...
	 *			------------------------------------------------------------------------------------
	 *
	 *			*** 트랜잭션도 AOP의 개념이 반영된 관점 지향 프로그래밍이라 할 수 있다.
	 *
	 *			# RuntimeException 계열의 종류
	 *			- ArithmeticException
	 *			- ArraysStoreException
	 *			- ArrayIndexOutOfBoundsException
	 *			- ClassCastException
	 *			- NullPointerException
	 *			- NegativeArraySizeException
	 *			- NoClassDefFoundException
	 *			- OutOfMemoryException
	 *			- IndexOutOfBoundsException
	 *			- IllegalArgumentException
	 *			- IllegalMonitorStateException
	 *
	 *		# 선언적 트랜잭션 @Transactional
	 *
	 *			- 컨트롤러 메소드 각 단위로 세밀한 트랜잭션 속성 제어가 가능
	 *			- 해당 어노테이션이 클래스 수준에서 선언되면 선언 클래스 및 해당 하위 클래스의 모든 메서드에 기본값으로 적용된다.
	 *			- RuntimeException 계열, Error 예외에 대해서는 Rollback이 가능하다. (공식문서 내용)
	 *
	 *			1) isolation (격리 수준)
	 *				- 각 트랜잭션이 존재할 때, 트랜잭션들 끼리 서로 고립된 수준을 나타내며 서로 간에 가용된 데이터를 컨트롤 할지에
	 *					대한 부분들을 설정할 수 있다.
	 *				- isolation 기본 값은 DEFAULT 이다.
	 *				- 새롭게 시작된 트랜잭션에만 적용되므로, Propagation.REQUIRED 또는 Propagation.REQUIRED_NEW
	 *					와 함께 사용되도록 독점 설계되었다.
	 *
	 *				1-1) 옵션
	 *
	 *				*** 용어
	 *				# Dirth read
	 *				- Commit이 이뤄지지 않은 다른 트랜잭션의 데이터를 읽는 것을 의미
	 *				# Non-repeatable Read(데이터의 수정/삭제)
	 *				- 처리 중인 트랜잭션에서 다른 트랜잭션이 Commit한 데이터를 읽을 수 있는 것을 의미
	 *				(처음 조회에 대한 트랜잭션과 두 번째 조회에 대한 트랜잭션 결과가 다를 수 있다.)
	 *				# Phantom Read
	 *				- 자신이 처리중인 트랜잭션에서 처리했던 내용 안에서 다른 트랜잭션이 데이터를 수정 후 Commit 하더라도 자신의
	 *					트랜잭션에서 처리한 내용만 사용하는 것을 의미
	 *
	 *				- DEFAULT : 기본 데이터 저장소의 기본 격리 수준을 사용한다.
	 *				- READ_COMMITED : Dirth read 가 방지됨을 나타내는 상수이다.
	 *									Non-repeatable Read 및 Phantom Read가 발생할 수 있다.
	 *					> 하나의 트랜잭션 처리가 이루어진 변경 내용이 Commit 된 후, 다른 트랜잭션에서 조회가 가능하다.
	 *					> A 트랜잭션이 데이터를 변경하고 B 트랜잭션이 조회를 진행할 때 B 트랜잭션은 Shared lock이 걸린다.
	 *				- READ_UNCOMMITED : Dirth read, Non-repeatable Read 및 Phantom Read가 발생할 수 있음
	 *									을 나타내는 상수이다.
	 *					> 다른 트랜잭션의 내용이 Commit 또는 Rollback 되거나 되지 않아도 다른 트랜잭션에서 조회가 가능
	 *				- REPEATABLE_READ : Dirth read, Non-repeatable Read가 방지됨을 나타내는 상수이다.
	 *									Phantom Read가 발생할 수 있다.
	 *					> 트랜잭션 Commit이 일어난 데이터에 대해서 조회가 가능
	 *						(트랜잭션 완료 시까지 조회에 대한 Shared lock이 걸리지 않음)
	 *				- SERIALIZABLE : Dirth read, Non-repeatable Read 및 Phantom Read가 방지됨을 나타냄.
	 *					> Phantom Read가 발생하지 않는다. (거의 사용하지 않는 옵션임)
	 *
	 *			2) propagation (전파 옵션)
	 *				- 기존 진행중인 트랜잭션 외에 추가적으로 진행중인 트랜잭션이 존재할 때 추가적인 트랜잭션에 대해서 어떻게
	 *					처리할 지에 대한 설정
	 *				- 추가적인 트랜잭션을 기존 트랜잭션에 포함 시켜 함께 처리할 수도 있고, 추가적인 트랜잭션처럼 별도의 트랜잭션으로
	 *					추가할 수도 있고 다른 트랜잭션처럼 진행되다 에러를 발생 시킬 수도 있다.
	 *
	 *				2-1) 옵션
	 *
	 *				- REQUIRED : 현재 트랜잭션을 지원하고 존재하지 않는 경우 새 트랜잭션을 만든다.
	 *					> propagation 기본 default 옵션
	 *					> 부모/자식간에 상관관계에서 자식부분의 트랜잭션이 rollback 처리 시, 부모까지 영향이 가서
	 *						rollback 처리가 된다.
	 *				- REQUIRED_NEW : 새로운 트랜잭션을 생성한다.
	 *					> rollback은 각각 이루어 진다.
	 *				- SUPPORTS : 트랜잭션이 있으면 현재 트랜잭션을 지원하고 트랜잭션이 없으면 트랜잭션이 아닌 방식으로 실행한다.
	 *				- MANDATORY : 현재 트랜잭션을 지원하고, 없으면 예외를 발생시킨다.
	 *					> 독립적인 트랜잭션으로 진행하면 안 되는 경우 사용
	 *				- NESTED : 현재 트랜잭션이 있는 경우 중첩된(부모-자식) 트랜잭션 내에서 실행하고 그렇지 않은 경우
	 *							REQUIRED와 같이 동작한다.
	 *					> 부모에서 예외가 발생했을 때, 자식까지 영향이 가서 Commit 되지 않는다.
	 *				- NEVER : 트랜잭션이 아닌 방식으로 실행하고 트랜잭션이 있으면 예외를 발생시킨다.
	 *					> 실행 자체가 트랜잭션을 필요로 하지 않고, 트랜잭션이 존재한다면 예외를 발생시키도록 한다.
	 *					> Existing transition found for transaction marked with propagation
	 *						'never' 에러
	 *
	 *			3) readOnly (읽기 전용 설정)
	 *				- 읽기 전용인 경우 설정할 수 있는 Bool Flag, 런타임시 최적화를 허용한다.
	 *				- readOnly 속성을 설정했다고 해서, 읽기 전용으로 무조건 설정된다는 보장이 없음
	 *					(쓰기와 같은 트랜잭션이 실행될 수도 있음)
	 *					> 읽기 전용에 대한 힌트를 분석할 수 없는 트랜잭션인 경우 throw를 던지지 않고 조용히 힌트를 무시한다.
	 *
	 *			4) timeout (트랜잭션 제한시간)
	 *				- 기본값은 -1로 무제한이다.
	 *				- timeout은 클라이언트와 서버와의 통신 중, 서버측 문제로 다음 처리를 이어나가지 못하는 'DeadLock'을
	 *					방지할 수 있는 속성이다.
	 *				- 클라이언트와 서버간의 Restful API를 개발 시 고려해볼 속성
	 *
	 *			5) rollbackFor
	 *				- 트랜잭션 롤백을 유발해야 하는 예외 유형을 나타내는 0개 이상의 예외 유형을 정의한다.
	 *				- 기본적으로 트랜잭션은 롤백 되지만, CheckedException 계열의 에러를 롤백되지 않는다.
	 *					> 공식 문서에서 제공하는 것처럼 RuntimeException 계열과 Error에 대해서는 기본적으로 롤백 가능
	 *				- 기본적인 정책 이외에 에러를 처리할 경우, 해당 에러를 선언하여 롤백 정책을 추가한다.
	 */
	
}

[root-context.xml]

	<!-- 트랜잭션 관리자의 빈을 정의 -->
	<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource" />
	</bean>
	
	<!-- 어노테이션 기반의 트랜잭션 제어를 활성화 -->
	<tx:annotation-driven />

package kr.or.ddit.service;

import java.io.IOException;
import java.util.List;

import kr.or.ddit.vo.Board;

public interface IBoardService {

	public void register(Board board) throws IOException;
	public List<Board> list();
	public Board read(int boardNo);
	public void modify(Board board);
	public void remove(int boardNo);
	public List<Board> search(Board board);

}
package kr.or.ddit.service.impl;

import java.io.IOException;
import java.util.List;

import javax.inject.Inject;

import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import kr.or.ddit.mapper.IBoardMapper;
import kr.or.ddit.service.IBoardService;
import kr.or.ddit.vo.Board;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Service
public class BoardServiceImpl implements IBoardService {

	@Inject
	private IBoardMapper mapper;
	
	@Transactional
	@Override
	public void register(Board board) throws IOException {
		log.info("BoardServiceImpl register 실행...!");
		mapper.create(board);
		
		if(true)
			throw new IOException();
	}

	@Override
	public List<Board> list() {
		return mapper.list();
	}

	@Override
	public Board read(int boardNo) {
		return mapper.read(boardNo);
	}

	@Override
	public void modify(Board board) {
		mapper.update(board);
	}

	@Override
	public void remove(int boardNo) {
		log.info("BoardServiceImpl remove 실행...!");
		mapper.delete(boardNo);
	}

	@Override
	public List<Board> search(Board board) {
		return mapper.search(board);
	}

}
package kr.or.ddit.controller.crud;

import java.io.IOException;
import java.util.List;

import javax.annotation.PostConstruct;
import javax.inject.Inject;

import org.springframework.aop.support.AopUtils;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import kr.or.ddit.service.IBoardService;
import kr.or.ddit.vo.Board;
import lombok.extern.slf4j.Slf4j;

@Slf4j
@Controller
@RequestMapping("/crud/board")
public class CrudBoardController {

	@Inject
	private IBoardService service;
	
	@PostConstruct
	public void init() {
		log.info("aopProxy 상태(interface 기반) : {}", AopUtils.isAopProxy(service));
		log.info("aopProxy 상태(클래스 상속 기반) : {}", AopUtils.isCglibProxy(service));
	}
	
	@RequestMapping(value = "/register", method = RequestMethod.GET)
	public String crudRegisterForm() {
		log.info("crudRegisterForm() 실행...!");
		
		return "crud/register";
	}
	
	@RequestMapping(value = "/register", method = RequestMethod.POST)
	public String crudRegister(Board board, Model model) throws IOException {
		log.info("crudRegister() 실행...!");
		
		service.register(board);
		// 게시글을 입력 후 최근 게시글 번호가 담겨있다(boardNo)
		log.info("게시글 등록 후 만들어진 최신 게시글 번호 : " + board.getBoardNo());
		
		model.addAttribute("msg", "등록이 완료되었습니다!");
		return "crud/success";
	}
	
	@RequestMapping(value = "/list", method = RequestMethod.GET)
	public String crudList(Model model) {
		log.info("crudList() 실행...!");
		List<Board> boardList = service.list();
		model.addAttribute("boardList", boardList);
		return "crud/list";
	}
	
	@RequestMapping(value = "/read", method = RequestMethod.GET)
	public String crudRead(int boardNo, Model model) {
		log.info("crudRead() 실행...!");
		Board board = service.read(boardNo);
		model.addAttribute("board", board);
		return "crud/read";
	}
	
	@RequestMapping(value = "/modify", method = RequestMethod.GET)
	public String crudModifyForm(int boardNo, Model model) {
		log.info("crudModifyForm() 실행...!");
		Board board = service.read(boardNo);
		model.addAttribute("board", board);
		model.addAttribute("status", "u"); // '수정을 진행합니다' 라는 flag
		return "crud/register";
	}
	
	@RequestMapping(value = "/modify", method = RequestMethod.POST)
	public String crudModify(Board board, Model model) {
		log.info("crudModify() 실행...!");
		service.modify(board);
		model.addAttribute("msg", "수정이 완료되었습니다.");
		return "crud/success";
	}
	
	@RequestMapping(value = "/remove", method = RequestMethod.POST)
	public String crudDelete(int boardNo, Model model) {
		log.info("crudDelete() 실행...!");
		service.remove(boardNo);
		model.addAttribute("msg", "삭제가 완료되었습니다.");
		return "crud/success";
	}
	
	@RequestMapping(value = "/search", method = RequestMethod.POST)
	public String crudSearch(String title, Model model) {
		log.info("crudSearch() 실행...!");
		Board board = new Board();
		board.setTitle(title);
		
		List<Board> boardList = service.search(board);
		model.addAttribute("boardList", boardList);
		model.addAttribute("board", board);
		return "crud/list";
	}
	
}

- http://localhost/crud/board/register

	@Transactional
	@Override
	public void register(Board board) throws IOException {
		log.info("BoardServiceImpl register 실행...!");
		mapper.create(board);
		
		// CheckedException 계열로 롤백 처리가 되지 않는다.
//		if(true)
//			throw new IOException();
		
		if(true)
			throw new NullPointerException();
	}

롤백 4가 롤백되어 데이터베이스에 저장되지 않았다.


	@Transactional(rollbackFor = IOException.class)
	@Override
	public void register(Board board) throws IOException {
		log.info("BoardServiceImpl register 실행...!");
		mapper.create(board);
		
		// CheckedException 계열로 롤백 처리가 되지 않는다.
		if(true)
			throw new IOException();
		
		// RuntimeException 계열에 해당하는 에러는 롤백 처리가 가능하다.
//		if(true)
//			throw new NullPointerException();
	}

롤백 5 등록 안 됨


@Service
public class MemberServiceImpl implements IMemberService {

	@Inject
	private IMemberMapper mapper;
	
	@Transactional
	@Override
	public void register(CrudMember member) {
		// 회원 1명의 정보를 등록 시, 하나의 권한을 가질 수 있다.
		mapper.create(member); // 회원정보 1명의 데이터를 등록
		
		// 등록된 회원정보를 이용해서 권한을 등록
		CrudMemberAuth memberAuth = new CrudMemberAuth();
		memberAuth.setUserNo(member.getUserNo());
		memberAuth.setAuth("ROLE_USER");
		
		if(true) {
			throw new NullPointerException();
		}
		
		mapper.createAuth(memberAuth);
	}

- http://localhost/crud/member/register

데이터 제대로 안 들어감. 롤백 처리 완료.