상세 컨텐츠

본문 제목

[스프링2] 8장. DB연동 (2)

22-23/22-23 Spring 2

by YUZ 유즈 2022. 11. 17. 10:01

본문

728x90

8장(2)은 목록 6 - 8까지 다루고 있습니다.


8-1장의 키워드

# 익셉션 변환 처리

# @Transactional 애노테이션

# 프록시를 이용한 트랜잭션 처리 과정


6. 스프링의 익셉션 변환 처리

jdbcTemplate.update("update MEMBER set NAME = ?, PASSWORD = ?, where" + 
	"EMAIL = ?", // 이 부분에서 오류 발생 : whereEmail = ? => where Email = ?
	member.getName(), member.getPassword(), member.getEmail());
  • 오류 발생: where " + "Email = ?" 로 변경해야한다.
org.springframework.jdbc.BadSqlGrammerException: ...생략
...생략
Caused by: com.mysql.jdbc.exception.jdbc4.MySQLSyntaxErrorException: ...생략
  • 발생 이유 : MySQLSyntaxErrorException이 발생했다.
  • 익셉션 클래스 : org.spring.framework.jdbs 패키지 > BadSqlGrammerException 클래스
  • BadSqlGrammerException을 발생한 메서드 : JdbTemplate 클래스의 update() 메서드이다.

exception 발생 과정

  1. JDBC API를 사용하는 과정에서 SQLException 발생한다.
  2. SQLException을 상속받은 MySQLSyntaxErrorException을 발생시킨다.
  3. convertSqlToException()을 통해 익셉션을 알맞은 DataAccessException으로 변환해서 발생시킨다.
  4. DataAccessException을 상속받은 BadSqlGrammerException으로 변환하여 오류 발생한다.
  • BadSqlGrammerException : 실행할 쿼리가 올바르지 않은 경우 발생
try{
	...JDBC 사용 코드
}	catch(SQLException ex){
		throw convertSQLToDataException(ex); //이 과정에서 SQLException이 DataAccessException으로 변경됨
}

SQLException을 DataAccessException으로 변환 시키는 이유

  • 연동 기술에 상관없이 동일하게 익셉션을 처리할 수 있도록 하기 위함이다.

JDBC (SQLException)

JPA (PersistenceException)                  convert      →         DataAccessException으로 동일하게 처리하기 위해

하이버네이트 (HibernateException)

try{
	//DB 연동 코드
}	catch(DataAcessException ex){ //동일하게 DataAcessException로 처리
	...
}

 

7. 트랜잭션 처리

  • 트랜잭션 : 두개 이상의 쿼리를 한 작업으로 실행해야 할 때 하나의 작업으로 묶어서 실행시키는 것
  • 롤백(rollback) : 쿼리 실행 결과를 취소하고 DB를 기존 상태로 되돌리는 것
  • 커밋(commit) : 모든 쿼리가 성공해서 쿼리 결과를 DB에 실제로 반영하는 것

JDBC의 트랜잭션 처리 방식

  • setAutoCommit(false) : 트랜잭션을 시작한다.
  • commit() : 트랜잭션 반영
  • rollback() : 트랜잭션 취소
Connection conn = null;
try{
	conn = DriverManager.getConnection(jdbcUrl, user, pw);
	conn.setAutoCommit(false);    //트랜잭션 범위 시작
	...쿼리 시작
	conn.commit();	//트랜잭션 범위 종료 : 커밋
}	catch(SQLException ex){
		if(conn != null)
            //트랜잭션 범위 종료 : 롤백
            try { conn.rollback(); } catch(SQLException e) { }
}	finally {
		if(conn != null)
			try { conn.close(); } catch(SQLException e) { }
}
  • 개발자가 트랜잭션 처리 코드를 누락하기 쉽다.

 

7-1. @Transactional을 이용한 트랜잭션 처리

  • 트랜잭션 범위에서 실행하고 싶은 메서드에 @Transactional 애노테이션을 붙인다.
import.org.springframework.transaction.annotation.Transactional;

public class ChangePasswordService {
    @Transcational
    public void changePassword(String email, String oldPwd, String newPwd){
        Member member = memberDao.selectByEmail(email);
        if(member != null)
            throw new MemberNotFoundException();
        member.changePassword(oldPwd, newPwd);

        memberDao.update(member);
}
  • memberDao.selectByEmail(), member.changePassword(), memberDao.update() 에서 실행하는 쿼리는 한 트랜잭션에 묶인다.

@Transactional을 사용하기 위한 스프링 설정

1. 플랫폼 트랜잭션 매니저(PlatformTransactionaManager) 설정

2. @Transactional 애노테이션 활성화 설정(@EnableTransactionManagement 애노테이션 사용)

 

@Configuration
@EnableTransactionManagement
public class AppCtx{
	@Bean(destoryMethod = "close")
	public DataSource dataSource(){
		DataSource ds = new DataSource();
		ds.setDriverClassName("com.mysql.jdbc.Driver");
		ds.setUrl("jdbc:mysql://localhost/spring5fs?characterEncoding=utf8");
		...생략
		reuturn ds;
	}

	@Bean
	public PlatformTransactionManager transactionManager(){
		DataSourceTransactionManager tm = new DataSourceTransactionManager();
		tm.setDataSource(dataSource()); //dataSource 프로퍼티를 이용하여 트랜잭션 연동에 사용할 DataSource 지정
		return tm;
	}
}

1. PlatformTransactionaManager : 트랜잭션 매니저 인터페이스

  • 스프링 구현기술에 상관없이 동일한 방식으로 트랜잭션을 처리하기 위한 인터페이스
  • JDBC : DataSourceTransactionManager가 PlatformTransactionaManager의 구현체
  • DataSourceTransactionManager : dataSource() 프로퍼티를 이용하여 트랜잭션 연동에 사용할 DataSource 지정하는 역할

2. @EnableTransactionManagement 애노테이션 : @Transactional 애노테이션이 붙은 메서드를 트랜잭션 범위에서 실행하는 기능을 활성화 한다.

  • PlatformTransactionaManager 빈을 사용하여 트랜잭션을 적용한다.

3. 트랜잭션 범위에서 실행하고 싶은 스프링 빈객체의 메서드에 @Transactional 애노테이션을 붙인다.

  • ChangePasswordService의 클래스의 changePassword() 매서드를 트랜잭션 범위에서 실행키기 위해 changePassword() 메서드에 @Transactional 애노테이션을 붙인다.
import.org.springframework.transaction.annotation.Transactional;

public class ChangePasswordService {
    @Transcational	//트랜잭션 범위에서 실행
    public void changePassword(String email, String oldPwd, String newPwd){
        Member member = memberDao.selectByEmail(email);
        if(member != null)
            throw new MemberNotFoundException();
        member.changePassword(oldPwd, newPwd);

        memberDao.update(member);
    }
}

4. ChangePasswordService 클래스를 빈으로 추가한다.

@Configuration
@EnableTransactionManagement
public class AppCtx{
	
	...생략

	@Bean
	public ChangePasswordService changePwdSvc(){
		ChangePasswordService pwdSvc = new ChangePasswordService();
		pwdSvc.setMemberDao(memberDao());
		return pwdSvc;
	}
}

5. changePwdSvc 빈을 이용하여 암호 변경 기능 실행하기

public class MainForCPS {
	
	public static void main(String[] arg){
		AnnotationConfigApplicationContext ctx = new AnnotationConfigApplicationContext(AppCtx.class);
		
		ChangePasswordService cps = ctx.getBean("changePwdSvc", ChangePasswordService.class); 

		try{
			cps.changePassword("madvirus@madvirus.net", "1234", "1111"); //changePwdSvc 빈을 이용하여 비밀번호 변경
			System.out.println("암호를 변경했습니다.");
		} 
		
		...생략
	}
}

6. 커밋이 실제로 시작, 커밋됐는지 알아보기 위해 Logback 사용하기

  • 사용법 : build.gradle 파일에 Logback 모듈 추가
  • 정상 실행됐다면 로그 메세지 출력
    • DataSourceTransationManager - Switching JDBC Connection  : JDBC로 연결
    • DataSourceTransationManager - Commiting transation commit  : commit 되었다
    • DataSourceTransationManager - Commiting JDBC transaction on Connection  : JDBC 트랜잭션에 commit 되었다.

 

7.2 @Transactional 과 프록시

  • 트랜잭션을 시작하고, 커밋하고, 롤백하는 것은 누가 하는 것일까? 프록시
  • @Transactional을 통해 내부적 AOP를 사용한다.
    • AOP는 프록시를 통해 구현된다 → 트랜잭션 처리도 프록시를 통해 이루어진다.

커밋 처리 과정

1. @EnableTransactionManagement 애노테이션 → @Transactional 애노테이션이 적용된 빈을 찾아간다.

2. 알맞은 프록시 객체를 생성한다.

  • ChangePasswordService의 메서드인 changePassword에 @Transactional 애노테이션이 적용되어있다.
  • ChangePasswordService cps = ctx.getBean("changePwdSvc", ChangePasswordService.class)를 통해
    트랜잭션 처리를 위해 changePwdSvc 프록시 객체를 생성한다. (ChangePasswordService 객체를 생성하지 않는다.)

3. PlatformTransactionManagement를 사용하여 트랜잭션을 시작한다

  • PlatformTransactionManagement 클래스의 getTransaction() 메소드 : 트랜잭션 시작

4. chaangePassword() 메서드를 트랜잭션 내에서 실행한다.

  • changdPassword() 내의 memberDao.selectByEmail(), member.changePassword(), memberDao.update() 를 한 트랜잭션으로 실행한다.

5. 성공적으로 실행되면 트랜잭션을 commit() 한다.

 

:MainForCPS  :프록시(changePwdSvc빈)  :PlatformTransactionManagement  :ChangePasswordService

         - changePassword()             - getTransaction()

                                                      - changePassword() 

                                                      - commit()

 

7.3 @Transactional 적용 메소드의 롤백 처리

  • RuntimeException 일때 트랜잭션의 rollback이 발생한다.
  • WrongIdPasswordException의 경우 : RuntimeException을 상속하므로 rollback()이 실행된다.
  • DataAccessException의 경우 : DB연동 과정에 문제가 생겼을 때 발생한다.
    • DataAccessException도 RuntimeException을 상속하므로 rollback 이 발생한다.
  • SQLException의 경우 : RuntimeException을 상속하지 않으므로 rollback이 발생하지 않는다
    • 트랜잭션을 롤백하고 싶다면 @Transactional의 rollback 속성을 사용해야한다.
    • @Transactional(rollbackFor = SQLException.class)
@Transactional(rollbackFor = SQLException.class) //SQLException 발생시 rollback 발생하도록한다.
public void someMethod() {

	...
}
  • 여러개의 Exception 적용시 : @Transactional(rollbackFor = {SQLException.class, IOException.class})

 

7.4 @Transactional의 주요 속성

  • @Transactional 애노테이션의 주요 속성
속성 타입 설명
value String 트랜잭션을 관리할때 사용할 PlatformTransactionManager 빈의 이름을 지정한다. 기본값은 " "이다.
propagation Propagation 트랜잭션 전파 타입을 지정한다.
기본값은 Propagation, REQUIRED이다.
isolation isolation 트랜잭션과 격리 레벨을 지정한다.
기본값은 isolation, DEFAULT 이다.
timeout int 트랜잭션 제한 시간을 지정한다. 기본값은 -1로 이경우 데이터 베이스의 타임아웃 시간을 사용한다. 초 단위로 지정한다.
  • @Transactional의 value 속성값이 없으면 타입이 PlatformTransactionManager인 빈을 사용한다.
  • Propagration 열거 타입의 주요 값
설명
REQIRED 메서드를 수행하는데 트랜잭션이 필요하다는 것을 의미한다.
현재 진행중인 트랜잭션이 존재하면 해당 트랜잭션을 사용한다. 존재하지 않으면 새로운 트랜잭션을 생성한다.
MANDATORY 메서드를 수행하는 데 트랜잭션이 필요하다는 것을 의미한다. 하지만 REQUIRED와 달리 진행중인 트랜잭션의 존재하지 않을 경우 익셉션이 발생한다.
REQUIRES_NEW 항상 새로운 트랜잭션을 시작한다. 진행 중인 트랜잭션이 존재하면 기존 트랜잭션을 일시 중지하고 새로운 트랜잭션을 시작한다. 새로 시작된 트랜잭션이 종료된 뒤에 기존 트랜잭션이 계속된다.
SUPPORTS 메서드가 트랜잭션을 필요로 하지 않지만, 진행 중인 트랜잭션이 존재하면 트랜잭션을 사용한다는 것을 의미한다. 진행 중인 트랜잭션의 존재하지 않더라도 메서드는 정상적으로 작동한다.
NOT_SUPPORTED 메서드가 트랜잭션을 필욜 하지 않음을 의미한다. SUPPORTS와 달리 진행 중인 트랜잭션이 존재할 경우 메소드가 실행되는 동안 트랜잭션은 일시 중지되고 메서드 실행이 종료된 후에 트랜잭션을 계속 진행한다.
NEVER 메서드가 트랜잭션을 필요로 하지 않는다. 만약 진행 중인 트랜잭션이 존재하면 인셉션이 발생한다.
NESTED 진행중인 트랜잭션이 존재하면 기존 트랜잭션에 중첩된 트랜잭션에서 메서드를 실행한다. 진행 중인 트랜잭션이 존재하지 않으면 REQIRED와 동일하게 작동한다. 이 기능은 JDBC 3.0 드라이버를 사용할 때에만 적용된다.(JTA Provider가 이 기능을 지원할 경우에도 사용가능하다)
  • Isolation 열거 타입에 정의된 값
설명
DEFAULT 기본 설정을 사용한다.
READ_UNCOMMITTED 다른 트랜잭션이 커밋하지 않은 데이터를 읽을 수 있다.
READ_COMMITTED 다른 트랜잭션이 커밋한 데이터를 읽을 수 있다.
REPEATABLE_READ 처음에 읽어 온 데이터와 두 번째 읽어 온 데이터가 동일한 값을 갖는다.
SERIVALZABLE 동일한 데이터에 대해서 동시에 두 개 이상의 트랜잭션을 수정할 수 없다.

7.5 @EnableTransactionManagement 애노테이션의 주요 속성

속성 설명
proxyTargetClass 클래스를 이용하여 프록시를 생성할지 여부를 지정한다. 기본값은 false로서 인터페이스를 이용해서 프록시를 생성한다.
order AOP 적용 순서를 지정한다. 기본값은 가장 낮은 우선순위에 해당하는 int의 최댓값이다.

 

7.6 트랜잭션 전파

  • Propagation 열거 타입 값 목록에서 REQUIRED 값의 설명의 예시이다.
    • 메서드를 수항하는 데 트랜잭션이 필요하는 것을 의미한다.
    • 현재 진행중인 트랜잭션이 존재하면 해당 트랜잭션을 사용한다.
    • 존재하지 않으면 새로운 트랜잭션을 생성한다.
//SomeService 클래스
public class SomeService {

	private AnyService anyService;

	@Transactional
	public void some() {
		anyService.any();
	}
	
	...생략
}


//AnyService 클래스
public class AnyService {

	@Transactional
	public void any() {...}
}
@Configuration
@EnableTransactionManagement
public class Config {
	@Bean
	public SomeService some(){
		SomeService some = new SomeService();
		...생략
	}

	@Bean
	public AnyService any(){
		return new AnyService();
	}
	
	...생략
}

 

트랜잭션의 전파 과정

  • SomeService와 AnyService 모두 @Transactional 애노테이션을 적용하고있다.
    • SomeService의 some() 메소드 실행 → 트랜잭션 시작
    • AnyService의 any() 메소드 실행 → 트랜잭션 시작
    • 문제 발생 : some() 메소드 내부에 any() 메소드를 호출한다.
  • propagation속성의 기본값 : Propagation, REQUIRED

1. some() 메소드 호출 : 트랜잭션 새로 시작

2. some() 메소드 내부의 any() 메소드 호출 : 이미 트랜잭션 존재 → 호출 시점에서 트랜잭션 생성하지 않는다.

  • some()과 any()를 한 트랜잭션으로 묶어서 실행한다.

  • propagation의 속성값 : REQUIRED_NEW

1. some() 메소드 호출 : 트랜잭션 새로 시작

2. some() 메소드 내부의 any() 메소드 호출 : 새로운 트랜잭션 생성

  • some() 트랜잭션과  any() 트랜잭션이 따로 생성되어 실행된다.

  • MemberDao.update()에 @Transactional이 적용되지 않은 경우?
import.org.springframework.transaction.annotation.Transactional;

//ChangePasswordService 클래스
public class ChangePasswordService {

    @Transcational
    public void changePassword(String email, String oldPwd, String newPwd){
        Member member = memberDao.selectByEmail(email);
        if(member != null)
            throw new MemberNotFoundException();
        member.changePassword(oldPwd, newPwd);

        memberDao.update(member);
	}
}

//MemeberDao 클래스

@Transactional 없음
public class MemberDao {

	private JdbcTemplate jdbcTemplate;

	public void update(Memeber member){
		jdbcTemplate.update(
			"update Memeber set NAME = ?, PASSWORD = ?, where EMAIL = ?",
			member.getName(), member.getPassword(), member.getEmail());
	}
}
  • JdbcTemplate 클래스 덕분에 트랜잭션 범위에서 쿼리를 실행할 수 있도록한다.
  • JdbcTemplate은 진행중인 트랜잭션이 존재하면 해당 트랜잭션 범위에서 쿼리를 실행한다.

1. 트랜잭션 시작

  • ChangePasswordService의 @Transactioal이 붙은 changePassword()메소드를 실행하므로 
    프록시가 생성되어 트랜잭션을 시작한다.

2. changePassword() 메소드 실행

2.1. selectByEmail() 메소드 실행

2.1.1. JdbcTemplate 실행

2.2. update() 메소드 실행

2.2.1. JdbcTemplate 실행

3. commit

  • JdbcTemplate은 이미 실행중인 트랜잭션 범위에서 쿼리를 실행한다.
  • changePassword() 메서드에서 실행하는 모든 쿼리는 하나의 트랜잭션 범위에서 실행된다.
  • 과정 2와 과정 2.3이 하나의 트랜잭션안에서 실행된다는 의미이다.
  • 과정 2와 과정 2.3 사이에 익셉션이 발생하면 롤백된다.

 

Q&A - 빈칸채우기

1. 연동 기술에 상관 없이 동일하게 익셉션을 처리할 수 있도록 하는 Exception을 (DataAccessException)이라고 한다.

2. 두 개 이상의 쿼리를 한 작업으로 실행해야 할 때 사용하는 것을 (트랜잭션)이라고 한다.

3. 트랜잭션 범위에서 실행하고 싶은 메서드에 (@Transactional)애노테이션을 붙인다.

4. 스프링이 제공하는 트랜잭션 매니저 인터페이스이면서, 트랜잭션을 시작하도록 만드는 인터페이스는(PlatformTransactionManager)이라고 한다.

5. (@EnableTransactionManagement) 애노테이션은 @Transactional 애노테이션이 붙은 메서드를 트랜잭션 범위에서 실행하는 기능을 활성화한다.

6. ChangePasswordService 클래스의 changePwdSvc() 메소드에 @Transactional 애노테이션이 적용되어 getBean("changePwdSvc", ChangePasswordService.class)를 통해 스프링은 트랜잭션 기능을 적용한 changePwdSvc빈의 (프록시 객체)를 생성한다.

7. Propagation 열거 타입 값 중의 (REQIRED) 값은 현재 진행중인 트랜잭션이 존재하면 해당 트랜잭션을 사용한다.

 

Q&A - 주관식 문제

1. 조건에 맞게 빈칸을 채우세요

  • changePassword() 메서드를 동일한 트랜잭션 범위에서 실행하려고한다.
  • @Transactional 을 실행시키기 위해서 스프링 설정(AppCtx)을 작성하려고 한다.
  • 힌트) 트랜잭션 범위에서 실행하는 기능을 활성화 할 것이다. / 트랜잭션 연동에 사용할 DataSource를 지정한다.
@_______________
public void changePassword(String email, String oldPwd, String newPwd){
	Member member = memberDao.selectByEmail(email);
	if(member != null)
		throw new MemberNotFoundException();
	member.changePassword(oldPwd, newPwd);
	
	memberDao.update(member);
}
@Configuration
@________________________
public class AppCtx{
	@Bean(destoryMethod = "close")
	public DataSource dataSource(){
		DataSource ds = new DataSource();
		ds.setDriverClassName("com.mysql.jdbc.Driver");
		ds.setUrl("jdbc:mysql://localhost/spring5fs?characterEncoding=utf8");
		...생략
		reuturn ds;
	}

	@Bean
	public _____________________ transactionManager(){
		DataSourceTransactionManager tm = new DataSourceTransactionManager();
		tm.setDataSource(dataSource()); //dataSource 프로퍼티를 이용하여 트랜잭션 연동에 사용할 DataSource 지정
		return tm;
	}
}

정답 : @EnableTransactionManagement, PlatformTransactionManager

 

2. 다음 조건에 맞게 빈칸을 채우세요

  • SQLException과 IOException이 발생하는 경우 트랜잭션을 롤백하고 싶다. 이 경우에 어떻게 작성해야 할까?
@Transactional(______________________________)
public void someMethod() {

	...
}

정답 : rollForback = {SQLException.class, IOException.class}

 


Corner Spring #2

Editor : 스머프냠

728x90

관련글 더보기