본문 바로가기

스프링

[Spring] 트랜잭션_기본1

1.     Com.springsource.junit : junit

2.     Commons-logging : Spring-context가 사용

3.     Mysql-connector : Mysql JDBC

4.     Spring-aop : 스프링 기능 자체의 aop

5.     Spring-bean : 스프링 빈을 활용하는 경우 필요. 스프링의 XML 설정파일과 자바 애노테이션을 파싱 하는데 필요한 클래스 포함

6.     Spring-context : 스프링 코어를 확장한 많은 클래스가 들어 있는데 모든 클래스는 EJB, JNDI(Java Naming Directory Interface), JMX용 클래스와 연동하는데 applicationcontext기능을 사용해야 하며 스프링 리모팅 클래스, 동적 스크립팅 언어(제이루비, 그루비등)와 연동하는 클래스, 빈 유효성검증(JSR-303) API, 스케줄링을 하는 클래스도 포함되어 있다.

7.     Spring-core : 모든 스프링 모듈에서 필요한 모듈. 다른 스프링 모듈에서 사용하는 공통 클래스가 포함됨.

8.     Spring-dao : EmpltyResultDataAccessException 등 사용을 위한 jar

9.     Spring-expression : 스프링 표현언어(SpEL) 지원 클래스 포함.

10.   Spring-jdbc : 스프링이 지원하는 jdbc.

11.   Spring-test.jar

-       @RunWith : Junit 프레임워크의 테스트 실행방법을 확장시 사용.

-       SpringJUnit4ClassRunner : 어플리케이션컨텍스트를 만들고 관리하는 확장 클래스

-       @ContextConfiguration(경로) : 자동으로 만들어줄 어플리케이션 컨텍스트 설정파일

12.   Spring-tx : DuplicateKeyException.class 파일 존재 및 스프링 트랜잭션

 

* 트랜잭션 : 레벨관리 도중 네트워크가 끊기거나 서버의 장애가 생겨 작업을 완료할 수 없다면, 그때까지 변경된 레벨은 그냥 둘까? 초기상태로 내버려둬야할까? 일부사용자만 변경되었다면 반발을 생각하여 모두 취소시키도록 결정했다.

테스트시나리오

일단 5명의 사용자정보를 DB에 넣는데 중간에 예외가 발생하도록 개발하여 취소가 되는지 확인한다.

1초도 안걸리는 짧은 업그레이드 상황에서 DB서버를 다운, 네트워크를 끊는 등의 강제적인 장애상황 연출은 불가능하기 때문에 장애가 발생했을 때 일어나는 현상 중의 하나인 예외를 던져 상황을 의도적으로 만드는게 나을 것이다.

테스트 방법

이럴 경우 테스트를 위해 코드(UserService)를 함부로 건드리는 것은 좋은 생각이 아니다.

UserService를 대신하여 테스트 목적에 맞는 테스트 클래스를 만들어 사용하는 것이 좋다.

>  UserService의 코드를 복사하여 새로운 클래스를 만들고 일부를 수정하는 것도 방법이지만, 코드 중복도 발생하고 사용하기도 번거로워보인다. 간단히 UserService를 상속해서 테스트에 필요한 기능을 추가하도록 일부 메소드를 오버라이딩 하는 방법이 나을 것 같다.

 

레벨을 다음 레벨로 변경할 수 있는 일은 레벨에게 위임한다. 서비스 코드의 복잡성을 줄이기 위해

 

User의 레벨셋팅은 User에게 위임한다. 서비스코드의 복잡성을 줄이기 위해

 

UserDao 인터페이스 Jdbc뿐만 아닌 Hibernate, JPA, JDO 등의 데이터엑세스 API로 변경될 수 도 있으므로 확장성을 위해

 

 

 

 

 

 

 

11,12. 공통적으로 쓰이는 상수는 static final

39. 테스트를 위해 어플리케이션 코드를 직접 수정하는 일은 가능한한 피하는 것이 좋지만 이번은 예외로 수정함(private > protected) : 상속해서 오버라이딩 하기 위함

 

 

 

 

 

75. new TestUserService(“예외를 발생시킬 네번째 사용자 id를 넣어서 테스트 오브젝트 생성”)

76. 조상의 setUserDao를 통해 userDao 수동 DI

> 수동DI : TestUserServiceupgradeAllOrNothing 메소드에만 특별한 목적으로 사용하는 것이니, 번거롭게 빈으로 등록할 필요 없이 동작하는데 필요한 스프링이 만들어준 UserDao 오브젝트를 DI해주면 된다. 컨테이너에 종속적이지 않은 평범한 자바코드로 만들어지는 스프링의 DI 스타일의 장점. 이러면 UserService 빈과 동일하게 UserDao를 사용해 데이터 엑세스 기능을 할 수 있다.

82. upgradLevels : TestUserService를 통해 오버라이딩한 메서드를 호출해 4번째 사용자에서 익셉션.

83. fail : 정상종료라면 문제가 있으니 테스트 실패

84. catch : 예외를 잡아 계속 진행. 그 외의 예외라면 테스트 실패

86. checkLevelUpgraded : 예외가 발생하기 전에의 레벨로 바뀌었나 확인

테스트에서만 사용할 클래스라면 번거롭게 파일을 따로 만들지 말고 테스트 클래스 내부에 스태틱 클래스로 만드는 것이 간편하다.

로컬내부클래스는 외부의 변수를 공유할 수 있지만, 스태틱 내부 클래스는 변수 공유 없이 일관된 클래스의 그룹핑을 위해 사용할 뿐이다. 소스의 가독성과 테스트 클래스에서만 사용할 클래스이기 때문에 static 클래스로 생성함.

> 결과 : 테스트 실패. 네번째 사용자 처리 중 예외가 발생했지만, 두 번째 사용자 레벨이 BASIC에서 SILVER로 바뀐 것이 유지되고 있다는 뜻. , 82라인 upgradeLevels 메소드가 하나의 트랜잭션 안에서 동작하지 않았기 때문이다. 지금까지 만든 코드 어디에서도 트랜잭션을 시작하고 커밋/롤백하는 JDBC 트랜잭션 경계설정 코드(아래스샷)은 구경도 못함.

 

 

JDBC 트랜잭션은 하나의 Connection을 가져와 사용한다.

트랜잭션을 시작하려면 자동커밋옵션은 false로 만들어 준 뒤 커밋하면된다.

트랜잭션 경계설정 : setAutoCommit(false)로 트랜잭션 시작을 선언하고 commit() 또는 rollback()으로 트랜잭션을 종료하는 작업

>  로컬 트랜잭션 : 하나의 DB커넥션 안에서 만들어지는 트랜잭션

 

 

 

 

 

스프링의 JdbcTemplate는 직접 만들어봤던 JdbcContext와 작업 흐름이 거의 동일하다.

14.   하나의 템플릿메소드 안에서 connection 오브젝트를 가져오고, 작업을 마치면 Connection을 확실하게 닫아주고 템플릿 메소드를 빠져나온다.

결국 템플릿 메소드 호출 한번에 한 개의 DB커넥션이 만들어지고 닫히는 일까지 일어나는 것이다.

>  결국 JdbcTemplate의 메소드를 사용한는 UserDao는 각 메소드마다 하나씩 독립적인 트랜잭션으로 실행될 수 밖에 없다.

 

 

 

따라서 현재 UserService 내에서 진행되는 여러가지 작업을 하나의 트랜잭션으로 묶는 일이 불가능해진다.

 

 

결국 위와같이 UserService 쪽으로 트랜잭션 경계설정 작업을 가져와야하는데, 여기서 생성한 ConnectionUserDao에도 전달해 줘야하므로 아래와 같이 변경되어야 할 것이다.

 

 

 

즉 이런식으로 커넥션을 전달 시켜야 한다.

이렇게 되면 DB커넥션을 비롯한 리소스의 깔끔한 처리를 가능하게 했던 JdbcTemplate을 더 이상 활용할 수 없다.

즉 서비스에 try/catch/finally 블록이 서비스내에 존재하게 될 것이다.

UserService는 스프링 빈으로 선언해서 싱글톤으로 되어 있으니, UserService의 인스턴스 변수에 이 Connection을 저장해뒀다가 다른 메소드에서 사용하게 할 수도 없다. (멀티스레드 환경에서 공유하는 인스턴스 변수에 스레드별로 생성하는 정보를 저장하다가는 서로 덮어쓰는 일이 발생하기 때문이다. 트랜잭션을 사용하는 커넥션과 필요없는 커넥션이 있기 때문)

>  Connection을 인자값으로 전달하면 EntityManagerSession 오브젝트를 사용하는 다른 데이터 액세스 기술에 독립적인 Dao, Service가 될 수 없다.

 

 

 

(1)  UserServiceConnection을 생성하고

(2)   이를 트랜잭션 동기화 저장소에 저장해두고, ConnectionsetAutoCommit(false)를 호출해 트랜잭션을 시작시킨 후에 본격 적으로 dao의 기능을 이용한다.

(3)   첫 번째 update() 메소드가 호출되고, update() 메소드 내부에서 이용하는 JdbcTemplate 메소드에서는 가장먼저

(4)   트랜잭션 동기화 저장소에 현재 시작된 트랜잭션을 가진 Connection 오브젝트가 존재하는지 확인한다. (2) upgradeLevels() 메소드 시작 부분에 저장해둔 Connection을 발견하고 이를 가져온다.

(5)   가져온 Connection을 이용해 PreparedStatement를 만들어 수정 SQL을 실행한다. 트랜잭션 동기화 저장소에서 DB커넥션을 가져왔을 때는 JdbcTemplateConnection을 닫지 않은 채로 작업을 마친다.

(6)   두 번째 update()가 호출되면 이때도 마찬가지로 (7) 트랜잭션 동기화 저장소에서 Connection을 가져와 (8) 사용한다.

(9) 마지막 update()(10)같은 트랜잭션을 가진 Connection을 가져와 사용한다.

(12) Connectioncommit()을 호출해서 트랜잭션을 완료시킨다.

(13) 트랜잭션 저장소가 더 이상 Connection 오브젝트를 저장해두지 않도록 이를 제거한다. 어느 작업 중에라도 예외상황이 발생하면 즉시 Connectionrollback()을 호출하고 트랜잭션을 종료할 수 있다. 물론 이때도 트랜잭션 저장소에 저장된 동기화된 Connection 오브젝트를 제거해줘야 한다.

* 트랜잭션 동기화 저장소는 작업 스레드마다 독립적으로 Connection 오브젝트를 저장하고 관리하기 때문에 멀티스레드 환경에서도 충돌이 날 염려는 없다.

* 트랜잭션 동기화 기법을 사용하면 파라미터를 통해 일일이 Connection 오브젝트를 전달할 필요가 없어지고, 트랜잭션 경계설정이 필요한 upgradeLevels() 에서만 Connection을 다루게 하고, 여기서 생성된 Connection과 트랜잭션을 DAOJdbcTemplate이 사용할 수 있도록 별도의 저장소에 동기화하는 방법을 적용하기만 하면 된다.

* 더 이상 로직을 담은 메소드에 Connection 타입의 파라미터가 전달될 필요도 없고, UserDao의 인터페이스에도 일일이 JDBC 인터페이스인 Connection을 사용한다고 노출할 필요가 없다.

 

 

 

 

 

 

 

 

 

 

 

JdbcTemplate

만약 미리 생성되서 트랜잭션 동기화 저장소에 등록된 DB커넥션이나 트랜잭션이 없는 경우에는 JdbcTemplate이 직접 DB커넥션을 만들고 JDBC 작업을 진행한다.

반면 test 클래스의 upgradeLevels() 메소드처럼 트랜잭션 동기화를 시작해놓았다면 그때부터 실행되는 JdbcTemplate의 메소드에서는 직접 DB커넥션을 만드는 대신 트랜잭션 동기화 저장소에 들어 있는 DB커넥션을 가져와서 사용한다.

>  따라서, 트랜잭션이 굳이 필요없다면 DAO를 바로 호출해서 사용하면 되고, 트랜잭션을 만들고 이를 관리할 필요가 있다면 미리 DB커넥션을 생성한 다음 트랜잭션 동기화를 해주고 사용하면 된다.

 

 

테스트 클래스의 43. upgradeLevels()는 스프링 컨테이너가 초기화한 userService를 사용하기 때문에 주입

>  테스트 클래스의 77. upgradeAllOrNothing() TestUserService를 직접 구성해 줘야하기 때문에 수동 DI

 

 

* 멀티스레드 환경에서도 안전한 트랜잭션 동기화 방법을 구현하는 일이 기술적으로 간단하지 않지만, 스프링은 JdbcTemplate과 더불어 이런 트랜잭션 동기화 기능을 지원하는 간단한 유틸리티 메소드를 제공한다.

20. Connection을 생성할 때 사용할 DataSourceDI 받도록 한다.

26. 여러 데이터엑세스 기술을 사용하므로 일단 Exception 사용

27. 트랜잭션 동기화 관리자를 이용해 동기화 작업을 초기화

28.. DataSourceUtils.getConnection() : DB커넥션을 생성과 동기화 저장을 함께 해주는 유틸리티 메소드. 트랜잭션을 시작한다. 여기서 DAO 작업은 모두 트랜잭션 안에서 진행된다.

42. DataSourceUtils.releaseConnection() : 스프링 유틸리티 메소드를 이용해 DB커넥션을 안전하게 닫음.

* 이렇게 함으로써 트랜잭션이 적용되어도 UserDAO는 수정할 필요가 없고, JDBC코드의 try/catch/finally 작업 흐름 지원, SQLException의 예외 변환과 함께 JdbcTemplate이 제공해주는 세가지 유용한 기능 중 하나이다.

> 비즈니스 로직 레벨의 트랜잭션을 적용했지만, JdbcTempate을 포기할 필요도 없고, 지저분한 Connection 파라미터를 계속 물고 다니지 않아도 된다.

> UserDao는 여전히 데이터 엑세스 기술에 종속되지 않는 깔끔한 인터페이스 메소드를 유지하고 있다.

 

 

 

 

 

80. UserServicetestService를 만들어도 동일한 dataSource를 수동DI 스프링의 장점

87. fail : 테스트가 catch에 안 걸리고 성공된다면 테스트를 실패처리함

88. 예외를 잡았지만 아무 처리도 하지 않았으므로 걍 지나감

 

이정도면 JDBC API를 사용하고 트랜잭션을 적용했으면, 책임과 성격에 따라 데이터 액세스 부분과 비즈니스 로직을 잘 분리, 유지할 수 있게 만든 뛰어난 코드라고 볼 수 있다.

 

하지만, 여러 개의 DB에 넣는 작업을 해야할 때는 문제가 발생한다. JDBC의 로컬 트랜잭션은 하나의 DB Connection에 종속되기 때문이다. 따라서, 글로벌 트랜잭션 방식을 사용해야한다.

 

글로벌 트랜잭션

-       여러 개의 DB가 참여하는 작업을 하나의 트랜잭션으로 만들 수 있다.

-       JMS와 같은 트랜잭션 기능을 지원하는 서비스도 트랜잭션에 참여시킬 수 있다.

JTA(Java Transaction API)

-       자바는 JDBC 외에 이런 글로벌 트랜잭션을 지원하는 트랜잭션 매니저를 지원하는 API를 제공하고 있다.

-       사용방법

기존 방법대로 DB JDBC, 메시징 서버라면 JMS 같은 API를 사용해서 필요한 작업을 수행한다. , 트랜잭션은 JDBCJMS API를 사용해서 직접 제어하지 않고, JTA를 통해 트랜잭션 매니저가 관리하도록 위임한다.

>  트랜잭션 매니저는 DB와 메시징 서버를 제어하고 관리하는 각각의 리소스 매너저와 XA 프로토콜을 통해 연결된다. 이를 통해 트랜잭션 매니저가 실제 DB와 메시징 서버의 트랜잭션을 종합적으로 제어할 수 있게 되는 것이다.