6장-AOP

3 분 소요

6장 AOP

AOP는 IoC/DI, 서비스 추상화와 더불어 스프링의 3대 기반 기술의 하나다.

AOP는 스프링의 기술 중에서 가장 이해하기 힘든 난해한 용어와 개념을 가진 기술로 악명이 높다.

AOP를 바르게 이용하려면 AOP의 필연적인 등장배경스프링이 그것을 도입한 이유, 그 적용을 통해 얻을 수 있는 장점이 무엇인지에 대한 충분한 이해가 필요하다.

6.1 트랜잭션 코드의 분리

트랜잭션의 경계는 비즈니스 로직의 전후에 설정돼야 하는 것이 분명하니 UserService의 메소드에 두는 것을 거부할 명분이 없다.

6.1.1 메소드 분리

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

public void upgradeLevels() throws Exception {
	Transaction 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;
	}
}

비즈니스 로직 코드를 사이에 두고 트랜잭션 시작과 종료를 담당하는 코드가 앞뒤에 위치하고 있다.

이 두가지 코드는 성격이 다를 뿐 아니라 서로 주고 받는 것도 없는, 완벽하게 독립적인 코드다.

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

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);
		}
	}
}

코드를 분리하고 나니 보기가 깔끔해졌다. 비즈니스 로직 코드만 독립적인 메소드에 담겨 있으니 이해하기도 편하고, 수정하기에도 부담이 없다.

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

트랜잭션 코드가 존재하지 않는 것처럼 사라지게 할 수는 없을까? 적어도 UserService에서는 보이지 않게 할 수 있지 않을까? –> 간단하게 트랜잭션 코드를 클래스 밖으로 뽑아내면 된다.

DI를 이용한 트랜잭션 분리

트랜잭션 코드를 어떻게서든 해서 UserService 밖으로 빼버리면 UserService 클래스를 직접 사용하는 클라이언트 코드에서는 트랜잭션 기능이 빠진 UserService를 사용하게 될 것이다. 구체적인 구현 클래스를 직접 참조하는 경우의 전형적인 단점이다.

DI의 기본 아이디어는 실제 사용할 오브젝트의 클래스 정체는 감춘 채 인터페이스를 통해 간접으로 접근하는 것이다.

6-1

그림 6-1 UserService 클래스와 클라이언트의 직접 연결을 통한 강한 결합

현재 구조는 그림 6-1처럼 UserService 클래스와 그 사용 클라이언트 간의 관계가 강한 결합도로 고정되어 있다. 이 사이를 비집고 다른 무엇인가를 추가하기는 힘들다.

6-2

그림 6-2 UserService 인터페이스 도입을 통해 약한 결합을 갖는 유연한 구조

보통 이렇게 인터페이스를 이용해 구현 클래스를 클라이언트에 노출하지 않고 런타임 시에 DI를 통해 적용하는 방법을 쓰는 이유는, 일반적으로 구현 클래스를 바꿔가면서 사용하기 위해서다.

테스트 때는 필요에 따라 테스트 구현 클래스를, 정식 운영 중에는 정규 구현 클래스를 DI 해주는 방법처럼 한 번에 한 가지 클래스를 선택해서 적용하도록 되어있다. 하지만 꼭 그래야 한다는 제약은 없다.

지금 해결하려고 하는 문제는 UserService에는 순수하게 비즈니스 로직을 담고 있는 코드만 놔두고 트랜잭션 경계설정을 담당하는 코드를 외부로 빼내려는 것이다. 하지만 클라이언트가 UserService의 기능을 제대로 이용하려면 트랜잭션이 적용돼야한다. 그래서 그림 6-3과 같은 구조를 생각해볼 수 있다.

UserService를 구현한 또다른 구현 클래스를 만든다. 이 클래스는 단지 트랜잭션의 경계설정이라는 책임을 맡고 있을 뿐이다. 그리고 스스로는 비즈니스 로직을 담고 있지 않기 때문에 또다른 비즈니스 로직을 담고 있는 UserService의 구현 클래스에 실제적인 로직 처리 작업은 위임하는 것이다. 그 위임을 위한 호출 작업 이전과 이후에 적절한 트랜잭션 경계를 설정해주면, 클라이언트 입장에서 볼 때는 결국 트랜잭션이 적용된 비즈니스 로직의 구현이라는 기대하는 동작이 일어날 것이다.

6-3

그림 6-3 트랜잭션 경계설정을 위한 UserServiceTx의 도입

UserService 인터페이스 도입

먼저 기존의 UserService 클래스를 UserServiceImpl로 이름을 변경한다. 그리고 클라이언트가 사용할 로직을 담은 핵심 메소드만 UserService 인터페이스로 만든 후 UserServiceImpl이 구현하도록 만든다.

클라이언트에 노출할 메소드를 담은 인터페이스는 리스트 6-3과 같다. 현재 구현한 사용자 관리 로직의 메소드는 add()와 upgradeLevels() 두 개뿐이다.

public interface UserService {
    void add(User user);
    void upgradeLevels();
}

리스트 6-3 UserService 인터페이스

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);
            }
        }
    }
}

리스트 6-4 트랜잭션 코드를 제거한 UserService 구현 클래스

분리된 트랜잭션 기능

비즈니스 트랜잭션 처리를 담은 UserServiceTx를 만들어본다. UserServiceTx는 기본적으로 UserService를 구현하게 만든다. 같은 인터페이스를 구현한 다른 오브젝트에게 고스란히 작업을 위임하게 만들면 된다.

UserServiceTx는 사용자라는 비즈니스 로직을 전혀 갖지 않고 고스란히 다른 UserService 구현 오브젝트에 기능을 위임한다. 이를 위해 UserService 오브젝트를 DI 받을 수 있도록 만든다.

public class UserServiceTx implements UserService {
	UserService userService;

	public void setUserService(UserService userService) { //UserService를 구현한 다른 오브젝트를 DI 받는다.
		this.userService = userService;
	}

	public void add(User user) { // DI 받은 UserService 오브젝트에 모든 기능을 위임한다.
		userService.add(user);
	}

	public void upgradeLevels() { // DI 받은 UserService 오브젝트에 모든 기능을 위임한다.
		userService.upgradeLevels();
	}
}

UserServiceTx에 트랜잭션의 경계설정이라는 부가적인 작업을 부여해본다.



카테고리:

업데이트:

댓글남기기