[토비의 스프링1] 서비스 추상화

2022. 5. 12. 21:10Spring/개념

객체지향적인 코드는 다른 오브젝트의 데이터를 가져와서 작업하는 대신 

데이터를 갖고있는 다른 오브젝트에게 작업을 해달라고 요청한다.

오브젝트에게 데이터를 요구하지 말고 작업을 요청하라는 것이

객체지향 프로그래밍의 가장 기본이 되는 원리이다.

 

JdbcTemplate과 트랜잭션 동기화

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

JdbcTemplate은 미리 생성되서 트랜잭션 동기화 저장소에 등록된 DB 커넥션이나

트랜잭션이 없는 경우에는 직접 DB커넥션을 만들고 트랜잭션을 시작해서 JDBC작업을 진행한다.
반면에 위 메서드처럼 트랜잭션 동기화를 시작해놓았다면 그때부터 실행되는 JdbcTemplate 의 

메소드에서는 직접 DB 커넥션을 만드는 대신 트랜잭션 동기화 저장소에

들어있는 DB 커넥션을 가져와 사용한다. 
이를 통해 이미 시작된 트랜잭션에 참여하는 것이다.

스프링 트랜잭션의 추상화

public void upgradeLevels() throws Exception {
		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를 이용한다면

JDBC의 로컬 트랜잭션인 DataSourceTransactionManager을 구현한다.

사용할 DB의 DataSource를 생성자로 넣으면서 DataSourceTransactionManager오브젝트를 만든다.

getTransaction메소드 호출을 통해 트랜잭션을 가져온다(시작한다).

JdbcTemplate에서 사용될 수 있는 방식으로 트랜잭션을 관리해준다.

(PlatformTransactionManager를 통해 시작한 트랜잭션은 UserDao의 JdbcTemplate안에서 사용된다.)

이 코드도 커밋을 하다 오류가 나면 롤백을 한다.

 

트랜잭션 기술 설정의 분리

추상화 API를 적용한 UserService 코드를.. JTA를 이용하는 글로벌 트랜잭션으로 변경하려면 어떻게 해야 할까?

PlatformTransactionManager 구현 클래스를 DataSourceTransactionManager에서 JTATranctionManager로

바꾸기만 하면 된다고 한다. JTATranctionManager는 주요 자바 서버에서 제공하는 JTA 정보를 통해 자동으로

인식하는 기능을 갖고있다고 한다. 그래서 별 다른 설정 없이 서버의 트랜잭션 매니저/서비스와 연동해서

동작을 한다고 한다. 바꾸려면 PlatformTransactionManager txManager = new JTATranctionManager();로

바꾸면 된다. 만약 하이버네이트의 경우 HibernateTransactionManager로, JPA는 JAPTranctionManager로

바꾸면 된다. (바꾸고싶은 JDBC API매니저를 불러오면 되는듯)

하지만! 어떤 매니저를 구현에 쓸지 UserService가 알고있는건 DI원칙에 위배된다고 한다.

토비는 이런걸 알려주는게 참 좋은거같다. 암튼 그래서 의존성 주입을 통해 바꾸라고 한다.

어떤 클래스든 스프링 빈으로 등록할때 먼저 검토해야 할 것은

싱글톤으로 만들어져 여러 스레드에서 동시에 사용해도 괜찮은가 하는 점이다. 

(상태를 갖고있고, 멀티스레드 환경에서 안전하지 않은 클래스를

빈으로 무작정 등록하면 심각한 문제가 발생하기 때문)

근데 스프링이 제공하는 모든 PlatformTransactionManager의 구현 클래스는 싱글톤으로 사용이 가능하단다. 

그래서 그냥 스프링의 싱글톤 빈으로 등록해도 좋다고 한다.

평소 하는거처럼 수정자 메소드로 의존성을 주입하면 되는데 원래는
인터페이스 이름 = 변수이름 = 수정자 메소드 이름 으로 통일시키지만

PlatformTransactionManager는 관례적으로 transactionManager라고 이름을 사용한단다.

이름이 길어서 (JAT의 트랜잭션 매니저랑 구분할라고) 지은거라 별 의미가 있는건 아니라서 사용한다.

DataSource는 이제 없어도 된다고.. 지워도 된다. (PlatformTransactionManager의 빈에 의존성 주입만 하면 된다.)

빈을 등록하고나면 이런 코드가 된다.

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

JTA로 고치려면 고치면 된다고 한다. 이때부턴 UserService의 코드를 수정 할 필요가 없다.

 

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

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

앞서 해본 것 처럼 애플리케이션 코드는 추상화를 통해 수평적인 분리를 시킬 수 있다.

하지만 트랜잭션의 추상화는 이와는 좀 다르다. 애플리케이션의 비즈니스 로직과 그 하위에서 동작하는

로우레벨의 트랜잭션 기술이라는 아예 다른 계층의 특성을 갖는코드를 분리한 것이다.

UserDao와 UserService의 결합도가 낮다는 건 데이터 액세스 로직이 바뀌거나, 심지어

데이터 액세스 기술이 바뀐다고 할지라도 UserService의 코드에는 영향을 주지 않는다는 뜻이다.

서로 독립적으로 확장될 수 있다. 

애플리케이션 로직의 종류에 따른 수평적인 구분이든, 로직과 기술이라는 수직적인 구분이든

모두 결합도가 낮으며, 서로 영향을 주지 않고 자유롭게 확장될 수 있는 구조를 만들 수 있는 데는

스프링의 DI가 중요한 역할을 하고 있다. DI의 가치는 이렇게 관심, 책임, 성격이 다른 코드를

깔끔하게 분리하는 데 있다.

단일 책임 원칙

이런 적절한 분리가 가져오는 특징은 객체지향 설계의 원칙 중의 하나인 단일 책임 원칙이 있다.

단일 책임 원칙은 하나의 모듈은 한 가지 책임을 가져야 한다는 의미다.

하나의 모듈이 바뀌는 이유는 한 가지여야 한다고 설명 할 수도 있다.

단일 책임 원칙의 장점

단일 책임 원칙을 잘 지키고 있다면 어떤 변경이 필요할 때 수정 대상이 명확해진다.

기술이 바뀌면 기술 계층과의 연동을 담당하는 기술 추상화 계층의 설정만 바꿔주면 된다.

데이터를 가져오는 테이블의 이름이 바뀌었다면 데이터 액세스 로직을 담고있는 UserDao만

변경하면 된다. 비즈니스 로직도 마찬가지다. 서비스가 많아질 경우 굉장한 장점이다.

적절하게 책임과 관심이 다른 코드를 분리하고, 서로 영향을 주지 않도록 다양한 추상화 기법을

도입하고, 애플리케이션 로직과 기술/환경을 분리하는 등의 작업은 갈수록 복잡해지는 엔터프라이즈

애플리케이션에는 반드시 필요하다. 이를 위한 핵심적인 도구가 바로 스프링이 제공하는 DI다.

스프링의 의존관계 주입 기술인 DI는 모든 스프링 기술의 기반이 되는 핵심이자 원리이며,

스프링이 지지하고 지원하는, 좋은 설계와 코드를 만드는 모든 과정에서 사용되는 가장 중요한 도구다.

5.4 메일 서비스 추상화

자바 메일을 서비스를 사용할때는 javax.activation, javax.mail, Spring의 context.support가 필요하다.

메일 같은 경우는 메일 서버가 없기도 하고, 메일 서비스를 dataSource처럼 인터페이스가 있어 구현을 바꿀

수는 없다. 그래서 테스트를 위해 서비스를 추상화 시켜야한다. (안되면 인터페이스로 추상화 시켜서

만들어야 하는듯..) 

public interface MailSender {
	void send(SimpleMailMessage simpleMessage) throws MailException;
	void send(SimpleMailMessage[] simpleMessages) throws MailException;
}
private void sendUpgradeEmail(User user) {		
		SimpleMailMessage mailMessage = new SimpleMailMessage();
		mailMessage.setTo(user.getEmail());
		mailMessage.setFrom("useradmin@ksug.org");
		mailMessage.setSubject("Upgrade 안내");
		mailMessage.setText("사용자님의 등급이 " + user.getLevel().name());
		
		this.mailSender.send(mailMessage);
	}
public class DummyMailSender implements MailSender {

	@Override
	public void send(SimpleMailMessage simpleMessage) throws MailException {
		// TODO Auto-generated method stub
	}

	@Override
	public void send(SimpleMailMessage[] simpleMessages) throws MailException {
		// TODO Auto-generated method stub
		
	}
	
}

요렇게 JavaMail과 같은 일을 하는 인터페이스를 만들어 추상화를 시키고,

실험용 메소드를 만든다. 그리고 JavaMail과 같은일을 하는 인터페이스를 만든것을

빈 오브젝트로 구체화 시킨다. 그럼 아무기능이 없이 그냥 있는척 하는 실험용 오브젝트가 생긴것이다.

이렇게 추상화를 시킴으로써 없는것도 직접 테스트를 할 수 있도록 만들 수 있다.

같은 기능을 하지만 실제로 메일을 보내지 않아도, 보낸 척 할수 있는것이다.

나중에 실제 발송용 코드를 직접 구현해 실제로 발송할 수도 있을것이다.

이렇게 어떤 경우에도 UserService와 같은 애플리케이션 계층의 코드는 아래 계층에서는

어떤 일이 일어나는지 상관없이 메일 발송을 요청한다는 기본 기능에 충실하게 작성하면 된다.

메일 서버가 바뀌고 메일 발송 방식이 바뀌어도 메일 발송을 한다는 비즈니스 로직이 바뀌지 않는 한

UserService는 수정할 필요가 없다.

지금 이 코드에서도 트랜잭션이 빠져있다고 한다..(실행되는 단위에서 예기치 못한 상황에 의해

취소가 되야할 때는 필요한거 같다.)

일단 스프링의 기초 개념을 설명하기 위한것이니 넘어간다. 다행.

서비스 추상화란 이렇게 원활한 테스트만을 위해서도 충분히 가치가 있다.

기술이나 환경이 바뀔 가능성이 있음에도, JavaMAil처럼 확장이 불가능하게

설계해놓은 API를 사용해야 하는 경우라면 추상화 계층의 도입을 적극 고려해볼 필요가 있다!

특별히 외부의 리소스와 연동하는 대부분 작업은 추상화의 대상이 될 수 있다!

테스트 대역의 종류와 특징

테스트용으로 사용되는 특별한 오브젝트들이 있다. 대부분 테스트 대상인 오브젝트의

의본 오브젝트가 되는 것들이다. UserDao의 DataSource 이거나,

UserService의 MailSender인터페이스를 구현한 것들이다. 이렇게 테스트 환경을 만들어주기 위해,

테스트 대상이 되는 오브젝트의 기능에만 충실하게 수행하면서 빠르게, 자주 테스트를 실행할 수 있도록

사용하는 이런 오브젝트를 통틀어서 테스트 대역이라고 부른다.

대표적인 테스트 대역은 테스트 스텁이다. DumyMailSender는 가장 단순하고 심플한 테스트 스텁의 예다.

 

정리

  • 비즈니스 로직을 담은 코드는 데이터 액세스 로직을 담은 코드와 깔끔하게 분리되는것이 바람직하다! 비즈니스 로직 코드 또한 내부적으로 책임과 역할에 따라서 깔끔하게 메소드로 정리돼야 한다.
  • 이를 위해서는 DAO의 기술 변화에 서비스 계층의 코드가 영향을 받지 않도록 인터페이스와 DI를 잘 활용해서 결합도를 낮춰줘야 한다.
  • DAO를 사용하는 비즈니스 로직에는 단위 작업을 보장해주는 트랜잭션이 필요하다.
  • 트랜잭션의 시작과 종료를 지정하는 일을 트랜잭션 경계설정이라고 한다. 트랜잭션 경계설정은 주로 비즈니스 로직 안에서 일어나는 경우가 많다.
  • 시작된 트랜잭션 정보를 담은 오브젝트를 파라미터로 DAO에 전달하는 방법은 매우 비효율적이기 때문에 스프링이 제공하는 트랜잭션 동기화 기법을 활용하는 것이 편리하다.
  • 자바에서 사용되는 트랜잭션 API의 종류와 방법은 다양하다. 환경과 서버에 따라서 트랜잭션 방법이 변경되면 경계설정 코드도 함께 변경돼야 한다.
  • 트랜잭션 방법에 따라 비즈니스 로직을 담은 코드가 함께 변경되면 단일 책인ㅁ 원칙에 위배되며, DAO가 사용하는 특정 기술에 대한 강한 결합을 만들어낸다.
  • 트랜잭션 경계설정 코드가 비즈니스 로직 코드에 영향을 주지 않게 하려면 스프링이 제공하는 트랜잭션 서비스 추상화를 이용하면 된다.
  • 서비스 추상화는 로우레벨의 트랜잭션 기술과 API의 변화에

'Spring > 개념' 카테고리의 다른 글

Maven scope  (0) 2022.06.20
[토비의 스프링] AOP 용어  (0) 2022.06.02
[토비의 스프링2] 1 IoC 컨테이너와 DI  (0) 2022.05.11
디자인 패턴 정리  (0) 2022.04.27
[Spring] MVC 패턴에서의 5가지 계층에 대한 정보  (0) 2021.08.21