본문 바로가기

학원/복기

[Spring] TranscationManager

예제를 통해 TranscationManager 클래스를 이용해 트랜잭션 처리해야할 필요성에 대해 알아볼 것이다.

 

예제)

 

테이블 생성

테이블 생성
create table pointuser(id varchar2(20) primary key, name varchar2(30), point number);
create table pointboard(idx number primary key, writer varchar2(20), subject varchar2(100));

시퀀스 생성
create sequence pointboard_seq;

 


POINTUSER 

 

DTO 선언

PointUser

@Data
@Builder//테스트 프로그램 작성 위해 선언
public class PointUser {
	private String id;
	private String name;
	private int point;
}

 

Mapper 생성

PointUserMapper.xml

 

PointUserMapper.java

public interface PointUserMapper {
	int insertPointUser(PointUser user);
	int updatePlusPointUser(String id);
	int updateMinusPointUser(String id);
	PointUser selectPointUser(String id);
}

 

 

DAO 선언

PointUserDAOImpl

//상속 받을 인터페이스
public interface PointUserDAO {
	int insertPointUser(PointUser user);
	int updatePlusPointUser(String id);
	int updateMinusPointUser(String id);
	PointUser selectPointUser(String id);
}

@Repository
@RequiredArgsConstructor
public class PointUserDAOImpl implements PointUserDAO {
	private final SqlSession sqlSession;
	
	@Override
	public int insertPointUser(PointUser user) {
		return sqlSession.getMapper(PointUserMapper.class).insertPointUser(user);
	}

	@Override
	public int updatePlusPointUser(String id) {
		return sqlSession.getMapper(PointUserMapper.class).updatePlusPointUser(id);
	}

	@Override
	public int updateMinusPointUser(String id) {
		return sqlSession.getMapper(PointUserMapper.class).updateMinusPointUser(id);
	}

	@Override
	public PointUser selectPointUser(String id) {
		return sqlSession.getMapper(PointUserMapper.class).selectPointUser(id);
	}
	
}

 

 

Service 클래스 선언

PointUserServiceImpl

//상속받을 인터페이스 
public interface PointUserService {
	PointUser addPointUser(PointUser user) throws Exception;
}

@Service
@RequiredArgsConstructor
public class PointUserServiceImpl implements PointUserService {
	private final PointUserDAO pointUserDAO;
	
	//매개변수로 회원정보를 전달받아 POINTUSER 테이블에 회원 정보를 삽입 처리하고 삽입된 
	//회원정보를 검색하여 반환하는 메소드
	@Override
	public PointUser addPointUser(PointUser user) throws Exception {
		//매개변수로 전달받은 회원정보의 id가 POINTUSER 테이블에 저장된 기존 회원정보의
		//아이디와 중복될 경우 인위적 예외 발생
		if(pointUserDAO.selectPointUser(user.getId()) !=null) {
			throw new Exception("이미 사용중인 아이디입니다.");
		}
		pointUserDAO.insertPointUser(user);
		return pointUserDAO.selectPointUser(user.getId());
	}
}

 


POINTBOARD

 

DTO 선언

 

PointBoard

@Data
@Builder
public class PointBoard {
	private int idx;//글번호
	private String writer;
	private String subject;
}

 

Mapper 생성

 

PointBoardMapper.xml

 

PointBoardMapper.java

public interface PointBoardMapper {
	int insertPointBoard(PointBoard board);
	int deletePointBoard(int idx);
	PointBoard selectPointBoard(int idx);
	List<PointBoard> selectPointBoardList();
}

 

DAO 선언

PointBoardDAOImpl

//상속받을 인터페이스
public interface PointBoardDAO {
	int insertPointBoard(PointBoard board);
	int deletePointBoard(int idx);
	PointBoard selectPointBoard(int idx);
	List<PointBoard> selectPointBoardList();
}

@Repository
@RequiredArgsConstructor
public class PointBoardDAOImpl implements PointBoardDAO {
	private final SqlSession sqlSession;
	
	@Override
	public int insertPointBoard(PointBoard board) {
		return sqlSession.getMapper(PointBoardMapper.class).insertPointBoard(board);
	}

	@Override
	public int deletePointBoard(int idx) {
		return sqlSession.getMapper(PointBoardMapper.class).deletePointBoard(idx);
	}

	@Override
	public PointBoard selectPointBoard(int idx) {
		return sqlSession.getMapper(PointBoardMapper.class).selectPointBoard(idx);
	}

	@Override
	public List<PointBoard> selectPointBoardList() {
		return sqlSession.getMapper(PointBoardMapper.class).selectPointBoardList();
	}
	
}

 

Service 클래스 선언

PointBoardServiceImpl

//서비스 클래스가 상속받을 인터페이스 
public interface PointBoardService {
	PointUser addPointBoard(PointBoard board) throws Exception;
	PointUser removePointBoard(int idx) throws Exception;
	List<PointBoard> getPointBoardList();
}


@Service
@RequiredArgsConstructor
public class PointBoardServiceImpl implements PointBoardService {
	private final PointUserDAO pointUserDAO;
	private final PointBoardDAO pointBoardDAO;
	
	//매개변수로 게시글을 전달받아 POINTBOARD 테이블에 게시글로 삽입하고 게시글 작성에 대한 
	//회원정보를 POINTUSER 테이블에서 검색하여 반환하는 메소드
	// => POINTUSER 테이블에 저장된 회원정보 중 게시글 작성자에 대한 회원정보의 포인트가 증가되도록 변경 처리 
	@Override
	public PointUser addPointBoard(PointBoard board) throws Exception {
		pointBoardDAO.insertPointBoard(board);//게시글 삽입
		//게시글 작성자에 대한 회원정보가 없는 경우 인위적 예외 발생
		// => 예외가 발생된 경우 하단에 작성된 명령은 실행되지 않고 메소드 강제 종료
		if(pointUserDAO.selectPointUser(board.getWriter()) == null) {
			throw new Exception("게시글 작성자가 존재하지 않습니다.");
		}
		pointUserDAO.updatePlusPointUser(board.getWriter());//회원정보의 포인트 증가
		return pointUserDAO.selectPointUser(board.getWriter());//회원정보를 검색하여 반환
	}
	
	//매개변수로 글번호를 전달받아 POINTBOARD 테이블에 저장된 게시글을 삭제하고 게시글 작성자에 대한
	//회원정보를 POINTUSER 테이블에서 검색하여 반환하는 메소드
	// => POINTUSER 테이블에 저장된 회원정보 중 게시글 작성자에 대한 회원정보의 포인트가 감소되도록 변경 처리 
	@Override
	public PointUser removePointBoard(int idx) throws Exception {
		PointBoard board=pointBoardDAO.selectPointBoard(idx);//게시글 검색
		//글번호에 대한 게시글이 검색되지 않은 경우 인위적 예외 발생
		if(board == null) {
			throw new Exception("게시글이 존재하지 않습니다.");
		}
		
		pointBoardDAO.deletePointBoard(idx);//게시글 삭제
		
		//게시글 작성자에 대한 회원정보가 없는 경우 인위적 예외 발생
		if(pointUserDAO.selectPointUser(board.getWriter()) == null) {
			throw new Exception("게시글 작성자가 존재하지 않습니다.");
		}
		
		pointUserDAO.updateMinusPointUser(board.getWriter());//회원정보의 포인트 감소
		
		return pointUserDAO.selectPointUser(board.getWriter());//회원정보를 검색하여 반환
	}
	
	
	//POINTBOARD 테이블에 저장된 모든 게시글을 검색하여 반환하는 메소드
	@Override
	public List<PointBoard> getPointBoardList() {
		return pointBoardDAO.selectPointBoardList();
	}
}

 


 

테스트 프로그램을 작성해 서비스 클래스의 메소드가 잘 동작되는지 확인해보자

 

 

PointUserServiceTest

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/*.xml"})
@Slf4j
public class PointUserServiceTest {
	@Autowired
	private PointUserService pointUserService;
	
	@Test
	public void testAddPointUser() throws Exception {
		PointUser user=PointUser.builder().id("abc123").name("홍길동").build();
		PointUser addUser=pointUserService.addPointUser(user);
		
		log.info(addUser.toString());
	}
}

 

PointBoardServiceTest

@RunWith(SpringJUnit4ClassRunner.class)
@WebAppConfiguration
@ContextConfiguration(locations = {"file:src/main/webapp/WEB-INF/spring/**/*.xml"})
@FixMethodOrder(MethodSorters.NAME_ASCENDING)//메소드 호출 순서 정의
@Slf4j
public class PointBoardServiceTest {
	@Autowired
	private PointBoardService pointBoardService;
	
	@Test
	public void test1() throws Exception {
		//게시글(PointBoard 객체) 생성 
		PointBoard board=PointBoard.builder().writer("abc123").subject("테스트").build();
		
		//PointBoardService 클래스의 addPointBoard() 메소드를 호출하여 POINTBOARD 테이블에 게시글 등록
		// => POINTUSER 테이블에 저장된 회원정보 중 게시글을 작성한 회원의 포인트 증가 
		// => POINTUSER 테이블에 저장된 회원정보 중 게시글 작성자의 회원정보를 검색하여 반환
		PointUser user=pointBoardService.addPointBoard(board);
		
		//게시글 작성자에 대한 회원정보를 기록 
		log.info(user.toString());
		
		//PointBoardService 클래스의 getPointBoardList() 메소드를 호출하여 게시글 목록을 반환받아 출력
		log.info(pointBoardService.getPointBoardList().toString());
	}
	
	@Test
	public void test2 () throws Exception {
		//PointBoardService 클래스의 addPointBoard() 메소드를 호출하여 POINTBOARD 테이블에 저장된 게시글 삭제
		// => POINTUSER 테이블에 저장된 회원정보 중 게시글을 작성한 회원의 포인트 감소
		// => POINTUSER 테이블에 저장된 회원정보 중 게시글 작성자의 회원정보를 검색하여 반환
		PointUser user=pointBoardService.removePointBoard(1);
		
		//게시글 작성자에 대한 회원정보를 기록 
		log.info(user.toString());
				
		//PointBoardService 클래스의 getPointBoardList() 메소드를 호출하여 게시글 목록을 반환받아 출력
		log.info(pointBoardService.getPointBoardList().toString());
	}
}

 


 

게시글 작성자가 없는 경우 예외가 발생한다.

 

PointBoardServiceTEst 

@Test
public void test1() throws Exception {
		//게시글(PointBoard 객체) 생성 
		//PointBoard board=PointBoard.builder().writer("abc123").subject("테스트").build();
		PointBoard board=PointBoard.builder().writer("xyz789").subject("테스트").build();
	
		//매개변수로 전달받은 게시글에서 게시글 작성자가 없는 경우 예외 발생 
		PointUser user=pointBoardService.addPointBoard(board);
		
		//게시글 작성자에 대한 회원정보를 기록 
		log.info(user.toString());
		
		//PointBoardService 클래스의 getPointBoardList() 메소드를 호출하여 게시글 목록을 반환받아 출력
		log.info(pointBoardService.getPointBoardList().toString());
	}

 

 

 

문제점)

 

예외가 발생하기 전에 실행된 게시글 삽입에 대한 SQL 명령은 이미 DBMS 서버에 전달되어 실행된 상태이므로 POINTBOARD 테이블에는 비정상적인 게시글이 저장된다.

즉, POINTBOARD 테이블에 게시글 작성자가 존재하지 않는 게시글이 저장되어 버리는 것이다.

 

때문에 게시글을 검색하여 출력할 경우 비정상적인 출력 결과가 제공된다.

 

게시글 작성자가 저장되어 버린것을 확인할 수 있다

 

 

해결법) 

 

예외가 발생되기 전에 실행된 SQL 명령에 대해 모두 롤백 처리 되도록 설정해야한다.

  • Spring 프레임워크에서 제공하는 트랜잭션 관리 기능을 사용하여 트랜잭션 처리해주면 된다.
  • TranscationManager 관련 클래스를 이용하여 일관성 있는 트랜잭션 처리 기능을 제공한다.

 


Spring 프레임워크의 TransactionManager 관련 클래스를 이용하여 트랜잭션 처리하는 방법

 

 

1. spring-tx 라이브러리를 프로젝트에 빌드 처리한다. (메이븐 이용:pom.xml)

  • spring-jdbc 라이브러리를 빌드 처리하면 의존 관계에 의해 자동으로 빌드 처리된다.

 

2. Spring Bean Configuration File(root-context.xml)에 TranscationManager 관련 클래스를 Spring Bean으로 등록한다.

 

root-context.xml

 

 

3. Spring Bean Configuration File(servlet-context.xml)에 트랜잭션 처리를 위한 AOP 설정

 

 

servlet-context.xml

 

TransactionManager 객체를 사용하여 트랜잭션 처리 하기 위해서는 tx 네임스페이스에 spring-tx.csd 파일을 제공받아 엘리먼트를 사용할 수 있도록 설정해주어야 한다. 

 

tx 네임스페이스를 추가

 

 

advice 엘리먼트로 TransactionManger 객체(Spring Bean)를 Advisor로 설정한다.

  • Advisor는 삽입위치(JoinPoint)가 정해져 있는 횡단관심코드의 메소드가 작성된 Advice 클래스로 생성된 객체(Srping Bean)을 의미한다.
  • id 속성에는 advice 엘리먼트를 구분하기 위한 식별자를 속성값으로 설정한다.
  • transaction-manager 속성에는 TranscationManager 관련 클래스의 Spring Bean에 대한 식별자(beanName)을 속성값으로 설정한다.
    • -> TransactionManager 객체(Spring Bean)를 이용하여 커밋처리 또는 롤백처리를 제공한다.

attributes 엘리먼트를 이용해 TransactionManager 객체에 의해 처리될 메소드 목록을 설정한다.

  • method 엘리먼트에는 TransactionManager 객체에 의해 처리될 메소드와 방식을 설정한다.
    • name 속성에는 TransactionManager 객체에 의해 처리될 메소드의 이름을 설정한다.
      • -> 메소드의 이름에 [*] 패턴문자를 사용해 설정할 수 있다. ex) [add*]은 add라는 이름으로 시작하는 메소드를 의미함 
    • rollback-for 속성에는 ROLLBACK 명령이 실행될 예외를 속성값으로 설정한다.
    • read-only 속성에는 false(기본값) 또는 true(읽기전용) 중 하나를 속성값으로 설정한다.

 

<tx:advice id="txAdvisor" transaction-manager="transactionManager">
	<tx:attributes>
		<tx:method name="add*" rollback-for="Exception"/>
		<tx:method name="modify*" rollback-for="Exception"/>
		<tx:method name="remove*" rollback-for="Exception"/>
		<tx:method name="get*" read-only="true"/>
	</tx:attributes>
</tx:advice>

 

 

SpringAOP 기능을 사용하기 위해서는 aop 네임스페이스에 spring-aop.xsd 파일을 제공받아 엘리먼트를 사용할 수 있도록 설정해주어야 한다.

 

 

SpringAOP 기능을 사용하여 타겟메소드 호출시 TransactionManager 객체가 동작될 수 있도록 설정해주는 것이다.

 

 

advisor 엘리먼트를 통해 Advisor(삽입위치가 지정된 Advice 객체)를 제공받아 사용할 수 있다.

  • advisor-ref 속성에는 advice 엘리먼트의 식별자를 속성값으로 설정한다.
<aop:config>
	<aop:advisor advice-ref="txAdvisor" pointcut="execution(* xyz.itwill10.service..*(..))"/>
</aop:config>

 

 

 


PointBoardServiceTest 를 다시 실행하면 ROLLBACK 처리 되는 것을 확인할 수 있다.

 

 


@Transactional 어노테이션

 

Spring Bean Configuration File의 AOP 설정을 이용하여 TransactionManager 객체를 사용하여 트랜잭션 처리를 할 수 있지만 @Transactional 어노테이션을 사용하여 트랜잭션 처리 하는 것도 가능하다.

 

TransactionManager 객체를 이용하여 트랜잭션 처리 기능을 제공받을 메소드에 @Transactional을 사용하면 메소드의 명령 실행시 예외(Exception)가 발생된 경우 자동으로 롤백 처리 된다.

  • ->  @Transactional을사용하기 위해서는 Spring Bean Configuration File(root-context.xml)에 tx 네임스페이스를 추가하여 spring-tx.xsd 파일을 제공받아 annotation-driven 엘리먼트를 반드시 설정해주어야 한다.

 

 

root-context.xml

 

tx 네임스페이스 추가

 

annotation-driven 엘리먼트를 사용해 @Transactional 어노테이션을 사용하여 TransactionManager 객체로 트랜잭션 처리 기능을 제공해줄 수 있도록 한다.

<tx:annotation-driven/>

 

 

서비스 클래스의 메소드에 @Transactional 어노테이션을 사용해줄 수 있다.

 

@Transactional : TransactionManager 객체에 의해 트렌젝션 처리 기능을 제공받기 위한 어노테이션

  • rollback 속성 : 예외 클래스(Class 객체)를 속성값으로 설정한다. - 예외가 발생되면 롤백 처리함 

예시)

@Transactional(rollbackFor = Exception.class)
@Override
	public PointUser addPointBoard(PointBoard board) throws Exception {
    ...
}

 


 

테스트 프로그램에도 @Transactional 어노테이션을 사용할 수 있다.

 

테스트 클래스의 테스트 메소드에 @Transactional 어노테이션을 사용하면 테스트 메소드의 명령 실행 후 무조건 롤백처리 된다.

 

 

PointUserServiceTest

@Transactional
@Test
public void testAddPointUser() throws Exception {
	//PointUser user=PointUser.builder().id("abc123").name("홍길동").build();
	PointUser user=PointUser.builder().id("xyz789").name("임꺽정").build();
	PointUser addUser=pointUserService.addPointUser(user);
		
	log.info(addUser.toString());
}

 

삽입 된 후 롤백처리 된다