Skip to content

5장 서비스 추상화 #5

@nsun9505

Description

@nsun9505

사용자 레벨 관리 기능 추가

비스니스 로직

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

필드 추가

Level Enum

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("Unkown value:" + value);
		}
	}
}

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());
	}
}
  • JDBC가 사용하는 SQL은 컴파일 과정에서는 자동으로 검증이 되지 않는 단순 문자열에 불과
  • SQL 문장이 완성돼서 DB에 전달되기 전까지는 문법 오류나 오타조차 발견하기 힘들다.
  • JPA를 사용한다면 @query를 사용해서 컴파일시점에 문법 오류를 잡을 수도 있다.

UserService.upgradeLevels()

  • 사용자 관리 로직을 담기 위한 UserService 클래스를 추가하자.
  • DAO는 데이터를 어떻게 가져오고 조작할지를 다루는 곳이라서 비즈니스 로직을 두는 곳이 아니다.

upgradeLevels() 메서드

public void upgradeLevels(){
	List<User> users = userDao.getAll();
  for (User user : users) {
		Boolean changed = false
		if(user.getLevel() == Level.BASIC && user.getLogin() >= 50){
			user.setLevel(Level.SILVER);
			changed = true;
		}
		else if(user.getLevel() == Level.SILVER && user.getRecommend() >= 30){
			user.setLevel(Level.GOLD);
			changed = true;
		}

		if(changed){
			userDao.update(user);
		}
	}
}
  • 모든 사용자 정보를 DAO에서 가져온 후에 한 명씩 레벨 변경 작업을 수행

코드 개선

작성된 코드를 살펴볼 때는 다음과 같은 질문을 해보자.

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

upgradeLevels() 메서드 리팩토링

public void upgradeLevels(){
	List<User> users = userDao.getAll();
	for(User user : users){
		if(canUpgradeLevel(user)){
			upgradeLevel(user);
		}
	}
}

private boolean canUpgradeLevel(User user){
	Level level = user.getLevel();
	switch(level){
		case BASIC: return user.getLogin >= 50;
		case SILVER: return user.getRecommend() >= 30;
		case GOLD: return false;
		default: throw new IllegalArgumentException(...);
	}
}

private void upgradeLevel(User user){
	if(user.getLevel() == Level.BASIC) user.setLevel(Level.SILVER);
	else if(user.getLevel() == Level.SILVER) user.setLevel(Level.GOLD);
	userDao.update(user);
}
  • canUpgradeLevel() 로직에서 처리할 수 없는 레벨인 경우에 예외를 던져준다.

  • 새로운 레벨이 추가됐지만 업그레이드 로직을 추가하지 않았다면 예외가 발생할 테니 쉽게 확인 가능

  • 아니면 이런 테스트 로직이 있어도 괜찮을 것이다.

    @Test
    public void numOfLevel(){
    	assertEquals(Level.values().size(), 3);
    }
    • Level Enum에 항목이 추가되면 이 테스트 케이스에서 통과하지 못하고 추가된 항목에 따라 변경할 부분을 수정하면 된다.
  • upgradeLevel()는 레벨이 늘어나면 if 문이 점점 길어질 것이고, 레벨 변경 시 사용자 오브젝트에서 level 필드 외의 값도 같이 변경해야 한다면 if 조건 뒤에 붙는 내용도 점점 길어질 것이다.

  • 레벨의 순서와 다음 단계 레벨이 무엇인지를 결정하는 일은 Level에게 맡기자.

    public enum Level {
    	BASIC(1, SILVER), SILVER(2, GOLD), GOLD(3, null);
    
    	private final int value;
    	private final Level next;
    
    	Level(int value, Level next) {
    		this.value = value;
    		this.next = next; 
    	}
    
    	public int intValue(){
    		return value;
    	}
    
    	public Level nextLevel(){
    		return this.next;
    	}
    
    	public static Level valueOf(int value){
    		switch(value){
    			case 1: return BASIC;
    			case 2: return SILVER;
    			case 3: return GOLD;
    			default: throw new AssertionError("Unkown value:" + value);
    		}
    	}
    }
  • User의 내부 정보가 변경되는 것은 UserService 보다는 User가 스스로 다루는 게 적절

    public class User {
    	...
    	
    	public void upgradeLevel(){
    		Level nextLevel = this.level.nextLevel();
    		if(nextLevel == null){
    			throw new IllegralStateException(...);
    		} 
    		this.level = nextLevel;
    	}
    }
    • User에 업그레이드 작업을 담당하는 독립적인 메서드를 두고 사용하면 업그레이드 시 기타 정보도 변경이 필요해지면 그 장점이 무엇인지 알 수 있을 것이다.

참고

  • 객체지향적인 코드는 다른 오브젝트의 데이터를 가져와서 작업하는 대신 데이터를 갖고 있는 다른 오브젝트에게 작업을 해달라고 요청한다.
  • 오브젝트에게 데이터를 요구하지 말고 작업을 요청하라는 것이 객체지향 프로그래밍의 가장 기본이 되는 원리
  • UserService는 User에게 레벨 업그레이드 작업을 해달라고 요청하고, User는 Level에게 다음 레벨이 무엇인지 알려달라고 요청하는 방식으로 동작하게 하는 것이 바람직하다.

상수의 도입

public static final int MIN_LOGCOUNT_FOR_SILVER = 50;
public static int MIN_RECOMMEND_FOR_GOLD = 30;

private boolean canUpgradeLevel(User user){
	Level level = user.getLevel();
	switch(level){
		case BASIC: return user.getLogin >= MIN_LOGCOUNT_FOR_SILVER;
		case SILVER: return user.getRecommend() >= MIN_RECOMMEND_FOR_GOLD;
		case GOLD: return false;
		default: throw new IllegalArgumentException(...);
	}
}

트랜잭션 서비스 추상화

  • 정기 사용자 레벨 관리 작업을 수행하는 도중에 네트워크가 끊기거나 서버에 장애가 생겨서 작업을 완료할 수 없다면, 그때까지 변경된 사용자의 레벨은 어떻게 할 것인가?
    • 책에서는 작업 중간에 문제가 발생하면 그때까지 변경 작업도 모두 취소시키도록 결정함.
    • 즉 하나의 트랜잭션에 묶어서 간다는 말이다.

모 아니면 도

테스트용 UserService 대역

  • 간단히 UserService를 상속해서 테스트에 필요한 기능을 추가하도록 일부 메서드를 오버라이딩하는 방법을 사용하자.
  • 테스트용 UserService의 서브클래스는 UserService 기능의 일부를 오버라이딩해서 특정 시점에서 강제로 예외가 발생(롤백되도록)만들 것이다.
  • 테스트용으로 작업 중간에 예외가 발생하도록 하면 이전 변경 작업은 취소되지 않고 그대로 남는 것을 확인할 수 있을 것이다.

문제는 트랜잭션

  • 모든 사용자의 레벨을 업그레이드하는 작업인 upgradeLevels()가 하나의 트랜잭션 안에서 동작하지 않았기 때문에 발생한 문제이다.
  • 트랜잭션이란 더 이상 나눌 수 없는 단위 작업을 말한다.
    • 작업을 쪼개서 작은 단위로 만들 수 없다는 것은 트랜잭션의 원자성을 기억하자.
  • 중간에 예외가 발생해서 작업을 완료할 수 없다면 아예 작업이 시작되지 않은 것처럼 초기 상태로 돌려놔야 한다

트랜잭션 경계설정

  • 하나의 SQL 명령을 처리하는 경우는 DB가 트랜잭션을 보장해준다.
  • 여러 개의 SQL이 사용되는 작업을 하나의 트랜잭션을 취급해야 하는 경우도 있다.
    • 이체를 할 때는 출금계좌의 잔고는 이체금액만큼 줄어들고, 입금계좌에는 이체금액만큼 증가
    • DB에 두 번 요청을 보냄(빼고, 더하고)

두 가지 작업을 하나의 트랜잭션으로 묶기

  • 두 번째 SQL이 성공적으로 DB에서 수행되기 전에 문제가 발생할 경우에는 앞에서 처리한 SQL도 취소해야 함

    → 트랜잭션 롤백

  • 여러 개의 SQL을 하나의 트랜잭션으로 처리하는 경우에 모든 SQL 수행작업이 다 성공적으로 마무리됐다고 DB에 알려줘서 작업을 확정시켜야 한다.

    → 트랜잭션 커밋

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

  • 모든 트랜잭션은 시작하는 시점과(begin)과 끝나는 시점(rollback, commit)이 있다.
  • 애플리케이션 내에서 트랜잭션이 시작되고 끝나는 위치를 트랜잭션의 경계라고 부른다.
    • 복잡한 로직의 흐름 사이에서 정확하게 트랜잭션 경계를 설정하는 일은 매우 중요한 작업
  • JDBC의 트랜잭션은 하나의 Connection을 가져와 사용하다가 닫는 사이에서 일어난다.
    • 트랜잭션의 시작과 종료는 Connection을 통해 이뤄지기 때문이다.
    • JDBC에서 트랜잭션을 시작하려면 자동커밋 옵션을 false로 만들어주자
      Connection.setAutoCommit(false)
    • JDBC의 기본 설정은 DB 작업을 수행한 직후에 자동으로 커밋이 되도록 되어 있다.
    • 작업마다 커밋해서 트랜잭션을 끝내버리므로 여러 개의 DB 작업을 모아서 트랜잭션을 만드는 기능이 꺼져있는 것이다.
    • 트랜잭션이 한 번 시작되면 commit() 또는 rollback()이 호출될 때까지의 작업이 하나의 트랜잭션으로 묶인다.
  • setAutoCommit(false)로 트랜잭션 시작을 선언하고, commit() 또는 rollback()으로 트랜잭션을 종료하는 작업을 트랜잭션의 경계설정이라고 한다.
    • 트랜잭션의 경계는 하나의 Connection이 만들어지고 닫히는 범위 안에 존재한다는 점을 꼭 기억하자.
    • 이렇게 하나의 DB 커넥션 안에서 만들어지는 트랜잭션을 로컬 트랜잭션이라고도 한다.

UserService와 UserDao의 트랜잭션 문제

  • UserService.upgradeLevels()에 트랜잭션이 적용되지 않았던 이유는 지금까지 만든 코드 어디에도 트랜잭션을 시작하고, 커밋하고, 롤백하는 트랜잭션 경계설정 코드가 존재하지 않기 때문이다.
  • 여러 번 DB에 업데이트를 해야 하는 작업을 하나의 트랜잭션으로 만들기 위해서는 그 작업이 진행되는 동안 DB 커넥션도 하나만 사용해야 한다.

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

  • UserService와 UserDao를 그대로 둔 채로 트랜잭션을 적용하려면 결국 트랜잭션의 경계설정 작업을 UserService 쪽으로 가져와야 한다.
    • 흐름을 볼 때 upgradeLevels()의 시작과 함께 트랜잭션이 시작하고 메서드를 빠져나올 때 트랜잭션이 종료돼야 하기 때문이다.

upgradeLevels의 트랜잭션 경계설정 구조

public void upgradeLevels() throws Exception {
	(1) DB Connection 생성
	(2) 트랜잭션 시작
	try {
		(3) DAO 메서드 호출
		(4) 트랜잭션 커밋
	} catch (Exception e) {
		(5) 트랜잭션 롤백
		throw e;
	} finally {
		(6) DB Connection 종료
	}
}
  • 트랜잭션을 사용하는 전형적인 JDBC 코드의 구조
  • 생성된 Connection을 가지고 데이터 액세스 작업을 진행하는 UserDao의 update() 안에 있어야 한다.
  • 트랜잭션 때문에 DB 커넥션과 트랜잭션 관련 코드는 어쩔 수 없이 UserService로 가져옴.
    • 순수한 데이터 액세스 로직은 UserDao에 둔다.
  • 기존의 JdbcTemplate처럼 매번 새로운 Connection을 만들어버리면, upgradeLevels() 안에서 시작한 트랜잭션과 무관한 별개의 트랜잭션이 만들어지므로 주의해야 한다.
  • UserService에서 만든 Connection을 UserDao에서 사용하려면 DAO 메서드를 호출할 때마다 Connection 오브젝트를 파라미터로 전달해줘야 한다.
  • upgradeLevels() 안에서 트랜잭션의 경계설정 작업이 일어나야 하고, 그 트랜잭션을 갖고 있는 DB 커넥션을 이용하도록 해야만 별도의 클래스에 만들어둔 DAO 내의 코드도 트랜잭션이 적용될 테니 결국 이 방법을 사용할 수밖에 없다.

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

  • DB 커넥션을 비롯한 리소스의 깔끔한 처리를 가능하게 했던 JdbcTemplate을 더이상 사용 불가
    • try/catch/finally 블록은 이제 UserService에 존재
    • UserService의 코드는 JDBC 작업 코드의 전형적인 문제점을 갖게 된다.
  • DAO의 메서드와 비즈니스 로직을 담고 있는 UserService의 메서드에 Connection 파라미터가 추가되어야 한다는 점
    • 트랜잭션이 필요한 작업에 참여하는 UserService의 메서드는 Connection 파라미터로 지저분해질 것임
  • Connection 파라미터가 UserDao 인터페이스 메서드에 추가되면 UserDao는 더이상 데이터 액세스 기술에 독립적일 수 없다.
    • JPA나 하이버네이트로 UserDao의 구현 방식을 변경하려고 하면 Connection 대신 EntityManager나 Session 오브젝트를 UserDao 메서드가 전달받도록 해야 한다.
  • DAO 메서드에 Connection 파라미터를 받게 하면 테스트 코드에도 영향을 미친다.
    • 테스트 코드에서 직접 Connection 오브젝트를 일일이 만들어서 DAO 메서드를 호출하도록 모두 변경해야 한다.

트랜잭션 동기화

Connection 파라미터 제거

  • 스프링이 제안하는 방법은 독립적인 트랜잭션 동기화(Transaction Synchronization) 방식이다.
  • 트랜잭션 동기화란 UserService에서 트랜잭션을 시작하기 위해 만든 Connection을 특별한 저장소에 보관하고, 이후에 호출되는 DAO의 메서드에서는 저장된 Connection을 가져다가 사용하게 하는 것이다.
  • 정확히는 DAO가 사용하는 JdbcTemplate이 트랜잭션 동기화 방식을 이용하도록 하는 것이다.
  • 트랜잭션이 모두 종료되면, 그때는 동기화를 마치면 된다.

트랜잭션 동기화를 사용한 경우의 작업 흐름

  1. UserService에서 Connection 생성
  2. 생성한 Connection을 트랜잭션 동기화 저장소에 저장하고 Connection.setAutoCommit(false) 호출해서 트랜잭션을 시작시킨 후에 DAO 메서드 호출
  3. update() 호출되고, update() 내부의 JdbcTemplate는 트랜잭션 동기화 저장소에 현재 시작된 트랜잭션을 가진 Connection이 존재하는지 확인
  4. upgradeLevels() 시작 부분에서 저장해둔 Connection을 발견하고 이를 가져와서 Connection을 이용해서 SQL을 실행
  5. 트랜잭션 동기화 저장소에서 DB 커넥션을 가져왔을 때는 JdbcTemplate은 Connection을 닫지 않은 채로 작업을 마친다.
    • 여전히 Connection은 열려 있고 트랜잭션은 진행 중인 채로 트랜잭션 동기화 저장소에 저장되어 있음.
  6. 두 번째 update()가 호출되면 마찬가지로 트랜잭션 동기화 저장소에서 Connection을 가져와서 사용
  7. 마지막 update()도 같은 트랜잭션을 가진 Connection을 가져와서 사용
  8. 작업을 정상적으로 끝났다면 UserService는 이제 Connection.commit() 호출
    1. 예외상황이 발생하면 UserService는 Connection.rollback() 호출하고 트랜잭션을 종료
    2. 이때도 9번과 같이 트랜잭션 저장소에 저장된 동기화된 Connection을 제거해줘야 함.
  9. 마지막으로 트랜잭션 저장소가 더 이상 Connection을 저장하지 않도록 제거

스레드마다 독립적인 Connection

  • 트랜잭션 동기화 저장소는 작업 스레드마다 독립적으로 Connection 오브젝트를 저장하고 관리
  • 다중 사용자를 처리하는 서버의 멀티스레드 환경에서도 충돌이 날 염려는 없다.
  • 트랜잭션 도기화 기법을 사용하면 파라미터에 일일이 Connection을 전달할 필요가 없어진다.

트랜잭션 동기화 적용

  • 스프링은 JdbcTemplate과 더불이 트랜잭션 동기화 기능을 지원하는 간단한 유틸리테 메서드 제공
  • 트랜잭션 동기화 방식을 적용한 UserService
private DateSource dataSource;

public void setDateSource(DataSource dataSource){
	this.dataSource = dataSource;
}

public void upgradeLevels() throws Exception {
//(1) DB Connection 생성
	TransactionSynchronizationManager.initSynchronization();

//(2) 트랜잭션 시작
	Connection c = DataSourceUtils.getConnnection(dataSource);
	c.setAutoCommit(false);

	try {
//	(3) DAO 메서드 호출
	  List<User> users = userDao.getAll();
    for (User user : users) {
	    if(canUpgradeLevel(user)){
	      upgradeLevel(user);
			}
		}

// (4) 트랜잭션 커밋
		c.commit();
	} catch (Exception e){
// 	(5) 트랜잭션 롤백
		c.rollback();
    throw e;
	} finally {
// 	(6) DB Connection 종료
		DataSourceUtils.releaseConnection(c, dataSource);
		TranscationSynchronizationManager.unbindResource(this.dataSource);
		TranscationSynchronizationManager.clearSynchronization();
	}
}
  • 스프링이 제공하는 트랜잭션 동기화 관리 클래스는 TransactionSychronizationManager
    • 이 클래스를 이용해 먼저 트랜잭션 동기화 작업을 초기화하도록 요청
  • DataSourceUtils에서 제공하는 getConnection()을 통해 DB 커넥션 생성
    • DataSource에서 직접 가져오지 않고, 스프링이 제공하는 유틸리티를 쓰는 이뉴는 DataSourceUtils의 getConnection()은 Connection을 생성해줄 뿐만 아니라 트랜잭션 동기화에 사용하도록 저장소에 바인딩해주기 때문이다.
  • 트랜잭션 동기화가 되어 있는 채로 JdbcTemplate을 사용하면 JdbcTemplate의 작업에서 동기화시킨 DB 커넥션을 사용
    • UserDao에서는 upgradeLevels()에서 만든 Connection을 사용하고 같은 트랜잭션에 참여
  • JDBC의 트랜잭션 경계설정 메서드를 사용해 트랜잭션을 이용하는 전형적인 코드에 간단한 트랜잭션 동기화 작업만 붙여줌으로써, 지저분한 Connection 파라미터의 문제를 깔끔히 해결

JdbcTemplate과 트랜잭션 동기화

  • 트랜잭션 동기화를 시작해놓았다면 그때부터 실행되는 JdbcTemplate의 메서드에서는 직접 DB 커넥션을 만드는 대신 트랜잭션 동기화 저장소에 들어 있는 DB 커넥션을 가져와서 사용한다.
    • 이를 통해 이미 시작된 트랜잭션에 참여하는 것이다.
  • DAO를 사용할 때 트랜잭션이 굳이 필요없다면 바로 호출해서 사용해도 되고, DAO 외부에서 트랜잭션을 만들고 이를 관리할 필요가 있다면 미리 DB 커넥션을 생성한 다음 트랜잭션 동기화를 해주고 사용하면 된다.
    • 트랜잭션 동기화를 하면 DAO에서 사용하는 JdbcTemplate은 자동으로 트랜잭션 안에서 동작할 것이다.
    • 트랜잭션 적용 여부에 맞춰 UserDao를 수정할 필요는 없어진다.

트랜잭션 서비스 추상화

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

  • 한 개 이상의 DB로의 작업을 하나의 트랜잭션으로 만드는 건 JDBC의 Connection을 이용한 트랜잭션 방식인 로컬 트랜잭션으로는 불가능
    • 로컬 트랜잭션은 하나의 DB Connection에 종속되기 때문이다.
  • 각 DB와 독립적으로 만들어지는 Connection을 통해서가 아니라 별도의 트랜잭션 관리자를 통해 트랜잭션을 관리하는 글로벌 트랜잭션 방식을 사용해야 한다.
    • 글로벌 트랜잭션을 이용하면 트랜잭션 매니저를 통해 여러 개의 DB가 참여하는 작업을 하나의 트랜잭션으로 만들 수 있다.

분산 트랜잭션 관리(글로벌 트랜잭션)

  • 자바는 글로벌 트랜잭션을 지원하는 트랜잭션 매니저를 지원하기 위한 API인 JTA(Java Transaction API)를 제공하고 있다.
  • 트랜잭션은 JDBC나 JMS API를 사용해서 직접 제어하지 않고 JTA를 통해 트랜잭션 매니저가 관리
  • 트랜잭션 매니저는 DB와 메시징 서버를 제어하고 관리하는 각각의 리소스 매니저와 XA 프로토콜을 통해 연결
    • 트랜잭션 매니저가 실제 DB와 메시징 서버의 트랜잭션을 종합적으로 제어 가능
  • JTA를 이용해 트랜잭션 매니저를 활용하면 여러 개의 DB나 메시징 서버에 대한 작업을 하나의 트랜잭션으로 통합하는 분산 트랜잭션 또는 글러벌 트랜잭션이 가능해진다.
  • 11장에서 자세히 다룸.

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

  • 현재 UserService에서 트랜잭션 경계 설정을 해야할 필요가 생기면 특정 데이터 액세스 기술에 종속되는 구조
    • JDBC 트랜잭션 API와 JdbcTemplate과 동기화하는 API로 인해 JDBC DAO에 의존하게 됨.
  • DAO 클래스의 구현 기술이 JDBC에서 하이버네이트와 같은 다른 기술로 바뀌면 UserService는 영향을 받지 않는다.
  • 하지만, JDBC에 종속적인 Connection을 이용한 트랜잭션 코드가 UserService에 등장하면서부터 UserService는 UserDaoJdbc에 간접적으로 의존하는 코드가 돼버렸다.
  • UserService가 특정 트랜잭션 방법에 의존적이지 않고 독립적일 수 있게 만들기 위해서는 여러 기술의 사용 방법에 공통점을 찾고 이를 추상화해볼 수 있다.
    • 트랜잭션의 경계설정을 담당하는 코드는 일정한 패턴을 갖는 유사한 구조이기 때문이다.
  • DB에서 제공하는 DB 클라이언트 라이브러리와 API는 서로 전혀 호환되지 않지만 모두 SQL을 사용하는 방식이라는 공통점이 있다.
    • 이 공통점을 뽑아내 추상화한 것이 JDBC
  • 애플리케이션 코드에서는 트랜잭션 추상화 계층이 제공하는 API를 이용해 트랜잭션을 이용하게 만들어준다면 특정 기술에 종속되지 않는 트랜잭션 경계설정 코드를 만들 수 있을 것이다.

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

  • 스프링은 트랜잭션 기술의 공통점을 담은 트랜잭션 추상화 기술을 제공하고 있다.
  • 이를 이용하면 애플리케이션에서 직접 각 기술의 트랜잭션 API를 이용하지 않고도, 일관된 방식으로 트랜잭션을 제어하는 트랜잭션 경계설정 작업이 가능해진다.

Untitled

스프링의 트랜잭션 추상화 API를 적용한 upgradeLevels()

public void upgradeLevels(){
	PlatformTransactionManager transactionManager = 
																new DataSourceTransactionManger(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를 사용하면 된다.
  • 사용할 DB의 DataSource를 생성자 파라미터로 넣으면서 DataSourceTransactionManager의 오브젝트를 만든다.
  • PlatformTransactionManager에서는 트랜잭션을 가져오는 요청인 getConnection() 메서드만 호출하면 트랜잭션이 시작된다.
    • 필요에 따라 트랜잭션 매니저가 DB 커넥션을 가져오는 작업도 같이 수행해주기 때문이다.
    • 트랜잭션을 가져온다는 것은 일단 트랜잭션을 시작한다는 의미라고 생각하자.
  • 파라미터로 넘기는 DefaultTransactionDefinition 오브젝트는 트랜잭션에 대한 속성을 담고 있다.
  • 시작된 트랜잭션은 TransactionStatus 타입의 변수에 저장된다.
    • 트랜잭션에 대한 조작이 필요할 때 PlatformTransactionManager 메서드의 파라미터로 전달해주면 된다.
  • PlatformTransactionManager로 시작한 트랜잭션은 트랜잭션 동기화 저장소에 저장된다.
    • PlatformTransactionManager를 구현한 DataSourceTransactionManager는 JdbcTemplate에서 사용될 수 있는 방식으로 트랜잭션을 관리해준다.
    • PlatformTransactionManager를 통해 시작한 트랜잭션은 UserDao의 JdbcTemplate 안에서 사용됨.
  • 트랜잭션 작업을 모두 수행한 후에는 트랜잭션을 만들 때 돌려받은 TransactionStatus를 파라미터로 해서 PlatformTransactionManager의 commit()을 호출하면 된다.
    • 예외가 발생하면 rollback()을 호출하자.

트랜잭션 기술 설정의 분리

  • 트랜잭션 추상화 API를 적용한 UserService에 JTA를 이용한 글로벌 트랜잭션으로 적용하거나 Dao를 하이버네이트나 JPA를 적용했다면
    • 각 데이터 액세스 기술의 PlatformTransactionManager 인터페이스를 구현한 구현 클래스를 사용하면 됨
  • 하지만 어떤 트랜잭션 매니저 구현 클래스를 사용할지 UserService 코드가 알고 있는 것은 DI 위배
    • 사용할 구체적인 클래스를 스스로 결정하고 생성하지 말고, 컨테이너를 통해 외부에서 제공받는 스프링 DI를 사용하자.
  • 어떤 클래스든 스프링의 빈으로 등록할 때 먼저 검토해야 하는 것이 있다.
    • 바로 싱글톤으로 만들어져 여러 스레드에서 동시에 사용해도 괜찮은가 하는 점이다.
    • 상태를 갖고 있고, 멀티 스레드 환경에서 안전하지 않은 클래스를 빈으로 등록하면 심각한 문제 발생
    • 스프링이 제공하는 모든 PlatformTransactionManager의 구현 클래스는 싱글톤으로 사용 가능

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

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

  • 기술과 서비스에 대한 추상화 기법을 이용하면 특정 기술환경에 종속되지 않는 포터블한 코드를 만들 수 있다.

Untitled

  • UserService와 트랜잭션 기술과도 스프링이 제공하는 PlatformTransactionManager 인터페이스를 통한 추상화 계층을 사이에 두고 사용하게 했기 때문에, 구체적인 트랜잭션 기술에 독립적인 코드가 됐다.
  • 애플리케이션 로직의 종류에 따른 수평적인 구분이든, 로직과 기술이라는 수직적인 구부이든 모든 결합도가 낮으며, 서로 영향을 주지 않고 자유롭게 확장될 수 있는 구조를 만들 수 있는 데는 스프링의 DI가 중요한 역할
    • DI의 가치는 관심, 책임, 성격이 다른 코드를 깔끔하게 분리하는 데 있다.

단일 책임 원칙(Single Responsibility Principle)

  • 하나의 모듈은 한 가지 책임을 가져야 한다.
  • 하나의 모듈이 바뀌는 이유는 한 가지여야 한다.
  • 트랜잭션 서비스의 추상화 방식을 도입하고, DI를 통해 외부에서 제어하도록 만들고 나서는 UserService가 바뀔 이유는 한 가지 뿐이다.
    • 사용자 관리 로직이 바뀌거나 추가되지 않은 한 UserService는 변경되지 않는다.

단일 책임 원칙의 장점

  • 단일 책임 원칙을 잘 지키고 있다면, 어떤 변경이 필요할 때 수정 대상이 명확해진다.
  • 기술이 바뀌면 기술 계층과의 연동을 담당하는 기술 추상화 계층의 설정만 바꿔주면 된다.
  • 데이터를 가져오는 테이블의 이름이 바뀌었다면 데이터 액세스 로직을 담고 있는 UserDao를 변경하면 된다.

스프링 DI

  • 적절하게 책임과 관심이 다른 코드를 분리하고, 서로 영향을 주지 않도록 다양한 추상화 기법을 도입하고
    애플리케이션 로직과 기술/환경을 분리하는 등의 작업은 갈수록 복잡해지는 엔터프라이즈 애플리케이션에는
    반드시 필요

    → 이를 위한 핵심적인 도구가 스프링이 제공하는 DI

  • 스프링의 DI가 없다면 인터페이스를 도입해서 나름 추상화를 했더라도 적지 않은 코드 사이의 결합이 남아 있게 된다.

  • 스프링을 DI 프레임워크라고 부르는 이유는 외부 설정정보를 통한 런타임 오브젝트 DI라는 단순한 기능을 제공하기 때문이 아니다.

    • 오히려 스프링이 DI에 담긴 원칙과 이를 응용하는 프로그래밍 모델을 자바 엔터프라이즈 기술의 많은 문제를 해결하는 데 적극적으로 활용하고 있기 때문이다.
    • DI를 활용해서 깔끔하고 유연한 코드와 설계를 만들어낼 수 있도록 지우너하고 지지해준다.

정리

  • 비즈니스 로직을 담은 코드는 데이터 액세스 로직을 담은 코드와 깔끔하게 분리되는 것이 바람직
  • DAO를 사용하는 비즈니스 로직에는 단위 작업을 보장해주는 트랜잭션이 필요
  • 트랜잭션의 시작과 종료를 지정하는 일을 트랜잭션 경계설정이라고 한다.
    • 주로 비스니스 로직 안에서 일어나는 경우가 많다.
  • 자바에서 사용되는 트랜잭션 API의 종류와 방법은 다양하다.
  • 트랜잭션 경계설정 코드가 비즈니스 로직 코드에 영향을 주지 않게 하려면 스프링이 제공하는 트랜잭션 서비스 추상화를 이용하면 된다.
  • 서비스 추상화는 로우레벨의 트랜잭션 기술과 API의 변화에 상관없이 일관된 API를 가진 추상화 계층을 도입
  • 서비스 추상화는 테스트하기 어려운 JavaMail 같은 기술에도 적용할 수 있다.
    • 테스트 대역 중에서 테스트 대상으로부터 전달받은 정보를 검증할 수 있도록 설계된 것을 Mock 오브젝트라고 한다.

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions