Table of Contents

개요#

5.1. 사용자 레벨 관리 기능 추가#

  • 사용자의 레벨은 BASIC, SILVER, GOLD 세가지 중 하나다.
  • 사용자가 처음 가입하면 BASIC레벨이 되며, 이후 활동에 따라서 한단계씩 업그레이드 될 수 있다.
  • 가입후 50회이상 로그인을 하면 BASIC에서 SILVER레벨이 된다.
  • SILVER 레벨이면서 30번 이상 추천을 받으면 GOLD레벨이 된다.
  • 사용자 레벨의 변경작업은 일정한 주기를 가지고 일괄적으로 진행된다. 변경 작업 전에는 조건을 충족하더라도 레벨의 변경이 일어나지 않는다.

5.1.1. 필드추가#

Level 이늄#

  • 사용자 레벨용 이늄
public enum Level {
	BASIC(1), SILVER(2), GOLD(3);

	private final int value;
		
	Level(int value) {
		this.value = value;
	}

	public int intValue() {
		return value;
	}
	
	public static Level valueOf(int value) {
		switch(value) {
		case 1: return BASIC;
		case 2: return SILVER;
		case 3: return GOLD;
		default: throw new AssertionError("Unknown value: " + value);
		}
	}
}

User 필드 추가#

public class User {
	...
	Level level;
	...
	
	public Level getLevel() {
		return level;
	}

	public void setLevel(Level level) {
		this.level = level;
	}
...
}

UserDaoTest 테스트 수정#

UserDaoJdbc 수정#

public class UserDaoJdbc implements UserDao {
	...	
	private RowMapper<User> userMapper = 
		new RowMapper<User>() {
				public User mapRow(ResultSet rs, int rowNum) throws SQLException {
				User user = new User();
				...
				user.setLevel(Level.valueOf(rs.getInt("level")));
				...
				return user;
			}
		};

	public void add(User user) {
		this.jdbcTemplate.update(
				"insert into users(id, name, password, level, login, recommend) " +
				"values(?,?,?,?,?,?)", user.getId(), user.getName(), user.getPassword(), 
					user.getLevel().intValue(), user.getLogin(), user.getRecommend());
	}

...

5.1.2. 사용자 수정 기능 추가 #

수정기능 테스트 추가#

UserDao와 UserDaoJdbc 수정#

수정 테스트 보완#

5.1.3. UserService.upgradeLevels()#

그림5-1

UserService 클래스와 빈 등록#

UserServiceTest 테스트 클래스#

upgradeLevels() 메소드#

upgradeLevels() 테스트#

5.1.4. UserService.add()#

5.1.5. 코드개선#

  • 코드에 중복된 부분은 없는가?
  • 코드가 무엇을 하는 것인지 이해하기 불편하지 않은가?
  • 코드가 자신이 있어야 할 자리에 있는가?
  • 앞으로 변경이 일어난다면 어떤 것이 있을수 있고 그 변화에 쉽게 대응할 수 있게 작성되어 있는가?

upgradeLevels() 메소드 코드의 문제점#

upgradeLevels() 리팩토링#

리스트 5-23, 5-24

User 테스트#

UserServiceTest 개선#

5.2. 트랜잭션 서비스 추상화#

모 아니면 도#

테스트용 UserService 대역#

강제 예외 발생을 통한 테스트#

테스트 실패의 원인#

5.2.2. 트랜잭션 경계설정#

JDBC 트랜잭션의 트랜잭션 경계설정#

UserService와 UserDao의 트랜잭션 문제#

비즈니스 로직 내의 트랜잭션 경계설정#

  • 트랜잭션을 사용하는 전형적인 JDBC코드의 구조*
public void upgradeLevels() throws Exception {
	1. DB connection 생성
	2. 트랜잭션 시작
	try {
		3. DAO 메소드 호출
		4. 트랜잭션 커밋
	} catch( Exception e ) {
		5. 트랜잭션 롤백
		throw e;
	} finally {
		6. DB connection 종료
	}
} 
위 구조대로 처리하기 위해서는 Dao 메소드가 모두 Connection 을 파라미터로 받도록 수정해야 한다.
  • Connection 오브젝트를 파라미터로 전달받는 UserDao 메소드*
public interface UserDao {
	public void add(Connection c, User user);
	public User get(Connection c, String id);
	...
	public void update(Connection c, User user);
} 

UserService 트랜잭션 경계설정의 문제점#

  • DB커넥션을 비롯한 리소스의 깔끔한 처리를 가능하게 했던 JdbcTemplate 을 더 이상 활용할 수 없다.
    • try / catch / finally 형태의 코드 형태를 사용해야 한다.
  • DAO의 메소드와 비즈니스 로직을 담고 있는 UserService 의 메소드에 Connection 파라미터를 추가해야 한다.
  • Connection 파라미터가 UserDao 인터페이스 메소드에 추가되면 UserDao 는 더 이상 데이터 엑세스 기술에 독립적일수가 없다.
    • JPA나 하이버네이트를 사용한다면 EntityManager 나 Session 객체를 전달해야 한다.
  • 테스트 코드에도 Connection 파라미터를 만들어서 처리해야 한다.

5.2.3. 트랜잭션 동기화#

Connection 파라미터 제거#

  • 트랜잭션 동기화 : UserService 에서 트랜잭션을 시작하기 위해 만든 Connection 오브젝트를 특별한 저장소에 보관해두고 이후에 호출되는 DAO 의 메소드에서 저장된 Connection을 가져다가 사용하게 하는 것
    • DAO가 사용하는 JdbcTemplate이 트랜잭션 동기화 방식을 이용하도록 한다.
그림 5-3
  • 트랜잭션 동기화 저장소는 작업 스레드마다 독립적으로 Connection 오브젝트를 저장하고 관리하기 때문에 다중 사용자를 처리하는 서버의 멀티스레드 환경에서도 충돌날 염려는 없다.

트랜잭션 동기화 적용#

  • 문제는 멀티스레드 환경에서도 안전한 트랜잭션 동기화 방법을 구현하는 일이 기술적으로 간단하지 않다.
public class UserService {
	.....

	private DataSource dataSource;  			

	public void setDataSource(DataSource dataSource) {
		this.dataSource = dataSource;
	}
	
	public void upgradeLevels() throws Exception {
		TransactionSynchronizationManager.initSynchronization();  
		Connection c = DataSourceUtils.getConnection(dataSource); 
		c.setAutoCommit(false);
		
		try {									   
			List<User> users = userDao.getAll();
			for (User user : users) {
				if (canUpgradeLevel(user)) {
					upgradeLevel(user);
				}
			}
			c.commit();  
		} catch (Exception e) {    
			c.rollback();
			throw e;
		} finally {
			DataSourceUtils.releaseConnection(c, dataSource);	
			TransactionSynchronizationManager.unbindResource(this.dataSource);  
			TransactionSynchronizationManager.clearSynchronization();  
		}
	}
	...
}

  • TransactionSynchronizationManager 는 스프링이 제공하는 트랜잭션 동기화 관리 클래스이다. 트랜잭션 동기화 작업을 초기화한다.
  • DataSourceUtils는 getConnection() 메소드를 제공한다. getConnection() 메소드는 Connection 오브젝트를 생성하고 트랜잭션 동기화에 사용하도록 저장소에 바인딩한다.
  • 작업을 정상적으로 마치면 트랜잭션을 커밋
  • 예외가 발생하면 트랜잭션을 롤백한다. 커밋과 롤백 모두 스프링 유틸리티 메소드를 사용한다.

JdbcTemplate 과 트랜잭션 동기화#

  • 만약 미리 생성돼서 트랜잭션 동기화 저장소에 등록된 DB커넥션이나 트랜잭션이 없는 경우에는 JdbcTemplate 이 직접 DB커넥션을 만들고 트랜잭션을 시작해서 JDBC작업을 진행한다.
  • 미리 만들어둔 DB커넥션이 동기화 저장소에 있다면 가져다가 사용한다.
  • 비즈니스 로직 레벨에서 트랜잭션을 적용했지만 코드는 바꾸지 않아도 된다.
    • Connection 파라미터를 가지고 다니지 않아도 된다.
    • 데이터 엑세스 기술에 종속적이지 않다.

5.2.4. 트랜잭션 서비스 추상화#

기술과 환경에 종속되는 트랜잭션 경계설정 코드#

  • 한개의 DB 트랜잭션만 처리
  • 두개 이상의 DB 트랜잭션을 처리, 글로벌 트랜잭션 방식을 사용
    • 자바는 글로벌 트랜잭션 처리를 위해 JTAjava Transaction API 를 제공
  • JMS와 같은 트랜잭션 기능을 지원하는 서비스도 트랜잭션에 참여시킬수 있다.
그림 5-4
  • JTA를 이용한 트랜잭션 코드 구조

InitialContext ctx = new InitialContext();
UserTransaction tx = (UserTransaction)ctx.lookup(USER_TX_JNDI_NAME);
tx.begin();
Connection c = dataSource.getConnection();
try {
	// 데이터 엑세스 코드
	tx.commit();
} catch (Exception e) {
	tx.rollback();
	throw e;
} finally {
	c.close();
}
  • Connection 의 메소드 대신에 UserTransaction의 메소드를 사용한다는 점을 제외하면 트랜잭션 처리 방법은 별로 차이가 없다.
  • 코드의 구조는 JDBC코드와 거의 비슷하다.
  • 하이버네이트를 사용한다면 Connection을 직접 사용하지 않고 Session이라는 건 사용해서 코드 형태가 많이 바뀐다.

트랜잭션 API의 의존관계 문제와 해결책#

그림 5-5
  • 추상화란 하위 시스템의 공통점을 뽑아내서 분리시키는 것을 말한다.
  • JDBC라는 추상화 기술이 있기 때문에 자바의 DB프로그램 개발자는 DB의 종류에 상관없이 일관된 방법으로 데이터 엑세스 코드를 작성할 수 있다.
  • 트랜잭션 코드도 추상화가 가능하지 않을까.?

스프링의 트랜잭션 서비스 추상화#

  • 스프링이 제공하는 추상화 계층
그림 5-6
public void upgradeLevels() {
	PlatformTransactionManager transactionManager = 
		new DataSourceTransactionManager(dataSource);
	TransactionStatus status =
		transactionManager.getTransaction(new DefaultTransactionDefinition());
	try{	  
		List<User> users = userDao.getAll();
		for (User user : users) {
			if (canUpgradeLevel(user)) {
				upgradeLevel(user);
			}
		}
		transactionManager.commit(status);  
	} catch (Exception e) {    
		transactionManager.rollback(status);
		throw e;
	} 
}
  • 스프링이 제공하는 트랜잭션 경계설정을 위한 추상 인터페이스는 PlatformTransactionManager 이다.
  • JDBC를 이용한다면 PlatformTransactionManager 를 구현한 DataSourceTransactionManager 를 사용한다.
  • transactionManager의 getTransaction() 메소드는 Connection을 가져옴과 동시에 트랜잭션을 시작한다.

트랜잭션 기술 설정의 분리#

  • JTA를 사용한다면 JTATransactionManager 로 바꾼다.
  • 하이버네이트로 구현한다면 HibernateTransactionManager 로 바꾼다.
  • JPA로 구현한다면 JPATransactionManager 로 바꾼다.
  • 모두 PlatformTransactionManager 인터페이스를 구현한 구현체라서 getTransaction(), commit(), rollback() 메소드를 모두 가지고 있다.
public class UserService {
	...
	private PlatformTransactionManager transactionManager;
	...	
	public void setTransactionManager(PlatformTransactionManager transactionManager) {
		this.transactionManager = transactionManager;
	}

	public void upgradeLevels() {
		TransactionStatus status = 
			this.transactionManager.getTransaction(new DefaultTransactionDefinition());
		try {
			List<User> users = userDao.getAll();
			for (User user : users) {
				if (canUpgradeLevel(user)) {
					upgradeLevel(user);
				}
			}
			this.transactionManager.commit(status);
		} catch (RuntimeException e) {
			this.transactionManager.rollback(status);
			throw e;
		}
	}
...

<bean id="userService" class="springbook.user.service.UserService">
	<property name="userDao" ref="userDao" />
	<property name="transactionManager" ref="transactionManager" />
	<property name="mailSender" ref="mailSender" />
</bean>
<bean id="transactionManager" 
					class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
	<property name="dataSource" ref="dataSource" />  
</bean>

5.3. 서비스 추상화와 단일 책임 원칙#

수직, 수평 계층구조의 의존관계#

그림 5-7

단일 책임 원칙#

단일 책임 원칙의 장점#

5.4. 메일 서비스 추상화#

5.4.1. JavaMail 을 이용한 메일 발송 기능#

JavaMail 메일 발송#

protected void upgradeLevel(User user) {
	user.upgradeLevel();
	userDao.updae(user);
	sendUpgradeEMail(user);
}

Add new attachment

Only authorized users are allowed to upload new attachments.
« This page (revision-11) was last changed on 16-Nov-2014 23:46 by DongGukLee