[Spring] AOP-트랜잭션 코드의 분리

Zoe·2022년 2월 24일
0

Spring

목록 보기
8/9
post-thumbnail

AOP-트랜잭션 코드의 분리


🌟 AOP를 이해하려면 필연적인 등장배경과 스프링이 그것을 도입한 이유, 그 적용을 통해 얻을 수 있는 장점이 무엇인지에 대한 충분한 이해가 필요
🌟 AOP의 적용 대상 : 선언적 트랜잭션 기능
🌟 서비스 추상화를 통해 많은 근본적인 문제를 해결했던 트랜잭션 경계설정 기능을 AOP를 이용해 더욱 세련되고 깔끔한 방식으로 바꿔보자

🚨 UserService를 깔끔하게 다듬어왔지만 찜찜함. 트랜잭션 경계설정을 위해 넣은 코드 때문. 스프링이 제공하는 깔끔한 트랜잭션 인터페이스를 썼음에도 비즈니스 로직이 주인이어야 할 메소드 안에 이름도 긴 트랜잭션 코드가 너무 많은 자리를 차지하고 있음. 그래도 트랜잭션의 경계를 분명히 비즈니스 로직의 전후에 설정돼야 함...

✅ 메소드 분리

1️⃣ 트랜잭션 경계설정과 비즈니스 로직이 공존하는 메소드

public void upgradeLevels() throws Exception {
	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 (Exception e) {
    	this.transactionManager.rollback(status);
        throw e;
    }
}
  • 비즈니스 로직 코드를 사이에 두고 트랜잭션 시작과 종료를 담당하는 코드가 구분되어 있음
  • 트랜잭션 경계설정의 코드와 비즈니스 로직 코드 간 서로 주고받는 정보가 없음 ➡️ 독립적인 코드

2️⃣ 비즈니스 로직과 트랜잭션 경계설정의 분리

public void upgradeLevels() throws Exception {
	TransactionStatus status = this.transactionManager
    	.getTransaction(new DefaultTransactionDefinition());
    try {
    	upgradeLevelsInternal(); //바뀐 코드
  
        this.transactionManager.commit(status);
    } catch (Exception e) {
    	this.transactionManager.rollback(status);
        throw e;
    }
}

private void upgradeLevelsInternal() {
	List<User> users = userDao.getAll();
    for (User user : users) {
    	if(canUpgradeLevel(user)) {
            upgradeLevel(user);
        }
    }
}
  • 코드를 분리하고 나니 보기가 깔끔

✅ DI를 이용한 클래스의 분리

🌟 트랜잭션 코드를 클래스 밖으로 뽑아내기(서로 직접적인 정보를 주고 받는 것이 없으므로 UserService에서 빼내기)

1️⃣ DI 적용을 이용한 트랜잭션 분리

  • UserService클래스와 클라이언트는 강한 결합도 : 만약 여기서 트랜잭션 코드를 클래스에서 빼버리면 클라이언트는 트랜잭션 기능이 빠진 UserService를 사용하게 됨

  • UserService를 인터페이스로 만들기
  • 기존 코드는 인터페이스의 구현 클래스로
  • 직접 구현 클래스에 의존하고 있지 않기 때문에 유연한 확장 가능
  • 이렇게 DI를 통해 적용하는 방법을 쓰는 이유 : 구현 클래스를 바꿔가면서 사용하기 위함

  • UserService를 구현한 또 다른 클래스 만듦
  • UserServiceTx : 트랜잭션의 경계설정이라는 책임을 맡음

2️⃣ UserService 인터페이스 도입

public interface UserService {
	void add(User user);
    void upgradeLevels();
}
  • 사용자 관리 로직 메소드 : add(), upgradeLevels()
//트랜잭션 코드를 제거한 UserService 구현 클래스
package springbook.user.service;
...
public class UserServiceImpl implements UserService {
	UserDao userDao;
	MailSender mailSender;

	public void upgradeLevels() { 
    	List<User> users = userDao.getAll();
		for (User user : users) { 
        	if (canUpgradeLevel(user)) { 
            	upgradeLevel(user);
			} 
    	} 
	}
...
  • 비즈니스 로직에만 충실한 코드

3️⃣ 분리된 트랜잭션 기능

//위임 기능을 가진 UserServiceTx 클래스
package springbook.user.service;
...
public class UserServiceTx implements UserService { 
	//UserService를 구현한 다른 오브젝트를 DI받는다.
	UserService userService;
	
    public void setUserService(UserService userService) { 
    	this.userService = userService;
	}
    //DI받은 UserService 오브젝트에 모든 기능을 위임
	public void add(User user) { 
    	userService.add(user);
	}
	public void upgradeLevels() { 
    	userService.upgradeLevels();
	} 
}
  • 비즈니스 로직에 대해서는 아무런 관여하지 않음
  • 같은 인터페이스를 구현한 다른 오브젝트에게 작업을 위임
//트랜잭션이 적용된 UserServiceTx
public class UserServiceTx implements UserService { 
	UserService userService;
	PlatformTransactionManager transactionManager;

	public void setTransactionManager( 
    		PlatformTransactionManager transactionManager) { 
		this.transactionManager = transactionManager;
	}

	public void setUserService(UserService userService) { 
		this.userService = userService;
	}

	public void add(User user) { 
    	this.userService.add(user);
	}

	public void upgradeLevels() {
		TransactionStatus status = this.transactionManager
        	.getTransaction(new DefaultTransactionDefinition());
		try {
			userService.upgradeLevels();
			this.transactionManager.commit(status);
		} catch (RuntimeException e) { 
        	this.transactionManager.rollback(status);
			throw e;
		} 
    } 
}
  • 추상화된 트랜잭션 구현 오브젝트를 DI 받을 수 있도록 PlatformTransactionManager 타입의 프로퍼티도 추가됨

4️⃣ 트랜잭션 적용을 위한 DI 설정

  • 이제 남은 것은 설정파일을 수정하는 부분

  • 클라이언트가 UserService라는 인터페이스를 통해 사용자 관리 로직을 이용하려고 할 때 먼저 트랜잭션을 담당하는 오브젝트가 사용돼서 트랜잭션에 관련된 작업을 하고, 실제 사용자 관리 로직을 담은 오브젝트가 이후에 호출돼서 비즈니스 로직에 관련된 작업을 수행하도록 만듦

5️⃣ 트랜잭션 분리에 따른 테스트 수정

  • 분리 작업이 끝났으니 테스트 돌리기 -> 수정 필요
@Test public void upgradeLevels() throws Exception { 
	...
	MockMailSender mockMailSender = new MockMailSender();
	userServiceImpl.setMailSender(mockMailSender);
//분리된 테스트 기능이 포함되도록 수정한 upgradeAllOrNothing()
@Test public void upgradeAllOrNothing() throws Exception {
	TestUserService testUserService = new TestUserService(users.get(3).getId());
	testUserService.setUserDao(userDao);
	testUserService.setMailSender(mailSender);

	UserServiceTx txUserService = new UserServiceTx();
	txUserService.setTransactionManager(transactionManager);
	txUserService.setUserService(testUserService);

	userDao.deleteAll();
	for(User user : users) userDao.add(user);

	try {
		txUserService.upgradeLevels();
		//트랜잭션 기능을 분리한 오브젝트를 통해 예외 발생용 TestUserService가 호출되게 해야 한다.
		fail("TestUserServiceException expected");
	} 
    ...

6️⃣ 트랜잭션 경계설정 코드 분리의 장점

  • 트랜잭션 경계설정 코드의 분리와 DI를 통한 연결의 장점
    : 비즈니스 로직을 담당하고 있는 UserServiceImpl의 코드를 작성할 때는 트랜잭션과 같은 기술적인 내용에는 전혀 신경쓰지 않아도 됨.
    비즈니스 로직에 대한 테스트를 손쉽게 만들어낼 수 있음.
profile
iOS 개발자😺

0개의 댓글