[토비의 스프링] Week11(6.8~7.1)

8 분 소요

토비의 스프링 3.1 Chapter 6.8 ~ 7.1

Chapater 6.8 - 트랜잭션 지원 테스트

선언적 트랜잭션과 트랜잭션 전파속성

  • 선언적 트랜잭션과(declarative transaction)
    AOP를 이용해 코드 외부에서 트랜잭션의 기능을 부여해주고 속성을 지정할 수 있게 하는 방법
  • 프로그램에 의한 트랜잭션(programmatic transaction)
    TransactionTemplate이나 개별 데이터 기술의 트랜잭션 API를 사용해 적접 코드 안에서 사용하는 방법

트랜잭션 동기화와 테스트

트랜잭션의 전파와 그로인한 유연한 개발의 배경은 AOP가 있었다.
하지만, AOP를 통한 선언적 트랜잭션이나 트랜잭션 전파를 가능하게 한 것은 트랜잭션 추상화이다.

트랜잭션 추상화 핵심은 트랜잭션 매니저와 트랜잭션 동기화이다.

  • 트랜잭션 메니저를 통해 구체적인 트랜잭션 기술의 종류에 상관없이 일관된 트랜잭션 제어가 가능
  • 트랜잭션 동기화를 통해 트랜잭션 정보를 저장소에 보관해뒀다가 DAO에서 공유 or 트랜잭션 전파 속성 참여 가능

트랜잭션 매니저를 이용한 테스트용 트랜잭션 제어

| 여러 개의 트랜잭션을 하나로 통합할 수 없을까?
여러 개의 메소드 모두 트랜잭션 전파 속성이 REQUIRED이면, 이 메소드들이 호출되기 전에 트랜잭션이 시작되게만 한다면 가능

롤백 테스트

| 테스트 내의 모든 DB 작업을 하나의 트랜잭션 안에서 동작하게 하고 테스트가 끝나면 무조건 롤백해버리는 테스트

롤백 테스트는 DB 작업이 포함된 테스트가 수행돼도 DB에 영향을 주지 않기 때문에 장점이 많다.
롤백 테스트는 테스트를 진행하는 동안에 조작한 데이터를 모두 롤백하고 테스트를 시작하기 전 상태로 만들어주기 때문에 유용

  • 여러 개발자가 하나의 공용 테스트 DB도 사용 가능하게 한다.
  • DB에 따라 트랜잭션을 롤백하면 커밋할 때 보다 성능이 더 향상된다.

테스트를 위한 트랜잭션 애노테이션

  • @Transactional
    테스트용 @Transactional은 기본적으로 트랜잭션을 강제 롤백시키도록 설정되어 있다.

  • @Rollback
    트랜잭션을 커밋시켜서 테스트에서 진행한 작업을 그대로 DB에 반영하고 싶을 때 @Rollback(false) 사용
    메소드 레벨에만 적용 가능

  • @TransactionConfiguraiton
    클래스 레벨에 사용 가능
    @TransactionConfiguraiton을 사용하면 롤백에 대한 공통 속성을 지정할 수 있다.

NotTransactional과 Propagation.NEVER

@NotTransactional을 테스트 메소드에 부여하면 클래스 레벨의 @Transactional을 무시하고 트랜잭션을 시작하지 않은 채로 테스트를 진행
@NotTransactional 대신 전파 속성을 Propagation.NEVER로 설정 가능

효과적인 DB 테스트

단위 테스트와 통합 테스트는 클래스를 구분하는 것이 좋음
DB가 사용되는 통합 테스트를 별도의 클래스로 만들어둔다면 기본적으로 클래스 레벨에 @Transactional을 부여

Chapater 7.1 SQL과 DAO의 분리

SQL을 Dao에서 분리하는 이유?
-> 운영 중에 DB의 테이블 or 필드이름 or SQL문이 변경될 수 있는데, 그 때마다 Dao를 수정해서 다시 컴파일하기에는 무리가 있기 때문

XML 설정을 이용한 분리

SQL을 xml설정파일의 프로퍼티 값으로 정의해서 DAO에 주입

public class UserDaoJdbc implements UserDao{
private JdbcTemplate jdbcTemplate;	// 스프링이 제공하는 JDBC 코드용 기본 템플릿
	private String sqlAdd;
	
public void setSqlAdd(String sqlAdd) {
  this.sqlAdd = sqlAdd;
}

public void setDataSource(DataSource dataSource) {
  jdbcTemplate = new JdbcTemplate(dataSource);
}

public void add(final User user) throws DuplicateUserIdException {
  try { 
    // sqlAdd로 변경
    jdbcTemplate.update(this.sqlAdd, 
        user.getId(), user.getName(), user.getPassword(), user.getLevel().intValue(), user.getLogin(), user.getRecommend(), user.getEmail());
  } catch (DuplicateKeyException  e) {
    throw new DuplicateUserIdException(e);
  }
}
<bean id="userDao" class="com.taxol.dao.UserDaoJdbc">
  <property name="dataSource" ref="dataSource"/>
  <property name="sqlAdd" value="insert into users(id, name, password, level, login, recommend, email) values(?,?,?,?,?,?,?)"/>
</bean>

스프링의 DI를 사용하여, String값을 외부에서 SQL을 분리하였다.

매번 프로퍼티 추가하는건 굉장히 번거롭다 -> SQL을 하나의 컬렉션으로 묶는다.

<bean id="userDao" class="com.taxol.dao.UserDaoJdbc">
  <property name="dataSource" ref="dataSource"/>
  <property name="sqlAdd" value="insert into users(id, name, password, level, login, recommend, email) values(?,?,?,?,?,?,?)"/>
  <property name="sqlMap">
    <map>
      <entry key="add" value="insert into users(id, name, password, level, login, recommend, email) values(?,?,?,?,?,?,?)" />
      <entry key="get" value="select * from users where id = ?" />
      ...
    </map>
  </property>
</bean>
public User get(String id) {
  // xml의 Map을 통해 sql을 가져왔다.
  return jdbcTemplate.queryForObject(this.sqlMap.get("get"), new Object[] {id} , userMapper);
}

SQL 제공 서비스

스프링 설정파일 안에 SQL과 DI 설정 정보가 섞여있는건 문제가 있다.
Why?
-> 운영 중인 애플리케이션에 빈번하게 참조되는 맵 내용을 수정할 경우 동시성 문제를 일으킬 수도 있다.

따라서, 독립적인 SQL 제공 서비스를 게공해야한다.
-> SQL을 분리하자.

applicationContext.xml

<bean id="userDao" class="com.taxol.dao.UserDaoJdbc">
  <property name="dataSource" ref="dataSource"/>
  <property name="sqlService" ref="sqlService"/>
</bean>

<bean id="sqlService" class="com.taxol.domain.sqlservice.SimpleSqlService">
  <property name="sqlMap">
    <map>
      <entry key="userAdd" value="insert into users(id, name, password, level, login, recommend, email) values(?,?,?,?,?,?,?)" />
      <entry key="userGet" value="select * from users where id = ?" />
      <entry key="userGetAll" value="select * from users order by id" />
      <entry key="userDeleteAll" value="delete from users" />
      <entry key="userGetCount" value="select count(*) from users" />
      <entry key="userUpdate" value="update users set name = ?, password = ?, level = ?, login = ?, recommend = ? where id = ?" />
    </map>
  </property>
</bean>

SQL 서비스 인터페이스

//SQL에 대한 키 값을 전달하면 그에 해당하는 SQL을 돌려주면 된다.
public interface SqlService {
  String getSql(String key) throws SqlRetrievalFailureException;
}
public class SimpleSqlService implements SqlService {
	private Map<String, String> sqlMap; // sql정보는 이 프로퍼티에 <map>을 이용해 등록.
	
	public void setSqlMap(Map<String, String> sqlMap) {
		this.sqlMap = sqlMap;
	}

	@Override
	public String getSql(String key) throws SqlRetrievalFailureException {
		String sql = sqlMap.get(key);
		if(sql == null)
			throw new SqlRetrievalFailureException(key + "에 대한 SQL을 찾을 수 없습니다.");
		else
			return sql;
	}

}

SQL에 대한 키 값을 전달하면, 그에 해당하는 SQL을 돌려주게 한다.

UserDaoJdbc

public class UserDaoJdbc implements UserDao{
	// SqlService 프로퍼티 사용
	private SqlService sqlService;
	
	public void setSqlService(SqlService sqlService) {
		this.sqlService = sqlService;
	}

public void add(final User user) throws DuplicateUserIdException {
		try { 
			jdbcTemplate.update(this.sqlService.getSql("userAdd"), 
					user.getId(), user.getName(), user.getPassword(), user.getLevel().intValue(), user.getLogin(), user.getRecommend(), user.getEmail());
		} catch (DuplicateKeyException  e) {
			throw new DuplicateUserIdException(e);
		}
	}
	
	public User get(String id) {
		return jdbcTemplate.queryForObject(this.sqlService.getSql("userGet"), new Object[] {id} , userMapper);
	}

	public void deleteAll(){
		this.jdbcTemplate.update(sqlService.getSql("userDeleteAll"));
	}

	public int getCount(){
		return this.jdbcTemplate.queryForInt(sqlService.getSql("userGetCount"));
	}
	
	public List<User> getAll() {
		return this.jdbcTemplate.query(sqlService.getSql("userGetAll"), userMapper);
	}

	@Override
	public void update(User user) {
		this.jdbcTemplate.update(this.sqlService.getSql("userUpdate") , 
				user.getName(), user.getPassword(),	user.getLevel().intValue(), user.getLogin(), user.getRecommend(),
				user.getId());
	}

}

댓글남기기