본문 바로가기

Spring Framework

[Spring] PSA(Portable Service Abstraction)

반응형

Spring 3대 핵심 기술

이번 글에서는 스프링의 핵심 기술 IoC/DI, AOP와 더불어 PSA에 대해 정리해보려고 합니다. (본 글은 토비의 스프링 3.1의 내용을 제가 이해한 방식으로 정리한 글입니다.)

PSA란 Portable Service Abstraction의 약자로 서비스 추상화라고 부릅니다. 추상화라는 단어에 대해 좀 더 깊이 들어가 보겠습니다. 자바에서는 표준 스펙, 사용 제품, 오픈 소스를 통틀어서 사용 방법과 형식은 다르지만 기능과 목적이 유사한 기술이 존재합니다. 예를 들어 DB에 접근하기 위해 Jdbc, Hibernate, Mybatis, JPA 등 DB Connection을 생성하고 SQL문을 실행하고 결괏값을 받는 등 DB와 관련된 로직을 처리한다는 점에서 기능과 목적은 동일하지만 사용하기 위한 방법은 다릅니다.

환경과 상황에 따라서 기술이 바뀌고 그에 따라 다른 API를 사용하고 다른 스타일의 접근 방법을 따라야 하는 상황은 매우 귀찮고 피곤한 일일 수 있습니다. 추상화는 이런 상황을 해결하기 위해 존재합니다. low 레벨에서 기술이 변경되더라도 (예를 들면 DB 접근 방법이 Jdbc에서 hibernate로 변경되는 상황) 비즈니스 로직을 처리하는 Service 클래스는 영향을 받지 않도록 하는 것이 PSA에 핵심입니다.

비즈니스 로직을 처리하는 경우에서 추상화를 설명하기 위해 다수의 회원을 관리하는 서비스가 있다고 가정하겠습니다. 이 서비스는 정해진 조건에 따라 사용자의 레벨을 주기적으로 변경(Basic -> Silver -> Gold) 한다고 합니다. 클라이언트(서비스 이용자)가 이 서비스에서 Basic과 Silver 사이에 Bronze 단계를 추가한다거나 레벨의 변경 조건을 수정 하는 요청이 빈번할때 수정이 용이하게 코드를 설계하는 것은 중요한 문제입니다

public void upgradeLevels() {
	List<UIser> users = userDao.getAll();
    
    for(User user : users) {
    	if (Basic -> Silver로 레벨 변경 조건) {
        	// 사용자 레벨 변경
        }
        elsee if(Silver -> Gold로 레벨 변경 조건) {
        	// 사용자 레벨 변경
        }
        userDao.update(user);
    }
}


만약 위의 코드 처럼 작성되어있다면 코드가 길어지고 요구사항이 빈번하게 변경될 경우 어떤 로직을 수정해야 하는지 찾는 작업에 시간이 오래 걸릴 것입니다. (지금은 코드가 간단해 보이지만 레벨의 수가 늘어났다거나 레벨이 삭제된다거나 하는 경우를 상상해 보세요)

이제 해당 코드를 자주 변경될 가능성이 있는 부분(사용자 레벨 수정)을 고려해 리팩터링 해보겠습니다.

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


리팩터링 후 코드는 "레벨이 변경 가능한 상태라면 레벨을 변경해"라는 내용을 충분히 추측할 수 있도록 변경된 것을 확인할 수 있습니다. 핵심은 레벨을 업그레이드 하는 작업의 기본 흐름만 만들고 구체적인 구현에서 외부에 노출할 인터페이스(canUpgradeLevel(), upgradeLevel())를 분리하는 것입니다.

PSA는 오브젝트에서 데이터를 요구하지 않고 작업을 요청하는 객체 지향 프로그래밍의 가장 기본이 되는 원리를 실현할 스프링의 핵심 기술이라고 할 수 있습니다.


이번에는 비즈니스 로직을 수행하는 모듈에서 low 레벨의 기술이 변경되어도(DB의 접근 API가 JDBC에서 Hiberate로 변경) 영향을 미치지 않기 위해 스프링이 제공하는 추상화에 대해 정리해보려고 합니다. (설명을 위해 이전의 가정 예시를 유지합니다)

Q. 사용자 레벨 관리 작업 수행 중 시스템 오류로 작업을 완료하지 못한 경우 변경된 사용자 레벨을 그대로 둘지, 아니면 초기 상태로 되돌릴까요?

A. 보통 이런 경우에는 후자의 방식을 사용하는 것이 일반 적입니다. 이렇게 시스템 오류로 인해 로직의 수행을 없었던 일로 되돌리는 것을 트랜잭션의 롤백(rollback)을 수행한다고 합니다.

Jdbc 에서 트랜잭션은 하나의 Connection을 가져와서 사용하고 닫는 사이에서 수행될 수 있습니다. (DB 변경 작업은 commit(), 시스템 오류 시 초기화는 rollback() 메서드를 호출하면서 수행될 수 있습니다.) 이렇게 하나의 DB 커넥션 안에서 만들어지는 트랜잭션을 로컬 트랜잭션이라고 합니다.

Q. 하나의 트랜잭션 안에서 여러 개의 DB에 데이터를 넣는 작업으로 수정될 경우는 트랜잭션을 어떻게 구현할 수 있을까요?

A. 로컬 트랜잭션은 하나의 DB Connection에 종속되기 때문에 이 문제를 해결할 수 없습니다. 때문에 각 DB와 독립적으로 만들어지는 Connection을 통해서가 아니라, 별도의 트랜잭션 관리자를 통해 트랜잭션을 관리하는 글로벌 트랜잭션 방식을 사용해야 합니다.

트랜 잭션은 Jdbc나 JMS API를 사용해서 직접 제어하지 않고 JTA를 통해 트랜잭션 매니저가 관리하도록 위임합니다.

JTA에 관한 구체적인 내용은 아래의 링크를 참고해 주세요.

[Spring 3 - Transaction] JTA를 이용한 글로벌/분산 트랜잭션

한 개 이상의 DB나 JMS의 작업을 하나의 트랜잭션 안에서 동작하게 하려면 서버가 제공하는 트랜잭션 매니저를 JTA를 통해 사용해야 한다. 스프링에서는 서버에 설정해둔 XA DataSource와 트랜잭션 매

springsource.tistory.com


하지만 글로벌 트랜잭션의 경우 두 가지 문제가 있습니다.

P1. 비즈니스 로직을 담당하는 클래스는 비즈니스 로직과 관계없이 기술 환경에 따라 (로컬 / 글로벌 트랜잭션) 코드가 변경되어야 한다.
P2. Hibernate를 이용한 트랜잭션 관리 코드는 Connection이 아니라 Session을 사용하고 독자적인 트랜잭션 관리 API를 사용하기에 비즈니스 로직을 담당하는 클래스의 코드 수정이 필요해진다.


정리하자면 트랜잭션을 관리한다는 공통의 목적을 가진 기능을 구현할 때 그 구현 방식이 변경되면 비즈니스 로직을 담당하는 코드 부분이 변경되어야 한다는 문제가 있습니다.

이러한 트랜잭션 API의 의존 관계 문제에 대한 해결 방법은 트랜잭션의 경계설정을 당당하는 코드는 일정한 패턴을 갖는, 즉 공통점을 갖는 유사한 구조이기 때문에 추상화 기법을 적용할 수 있습니다.

스프링의 트랜잭션 추상화 계층



위의 그림의 핵심은 애플리케이션의 비즈니스 로직을 담당하는 UserService라는 클래스에서 트랜잭션을 사용하기 위해서는 PlatformTranscationManager 인터페이스를 사용하도록 하는 것입니다. 이렇게 하면 PlatformTranscationManager 인터페이스를 구현한 클래스가 변경되더라도 (JDBC나 JTA, Hibernate로 트랜잭션을 구현하는 방법이 변경되더라도) UserService는 코드 수정에 영향을 받지 않게 됩니다. PSA는 이렇게 low 레벨의 기술적 구현 방식의 변경으로부터 비즈니스 로직을 처리하는 부분 의과 분리되어 있습니다. PSA를 가능하게 하는 근본적인 이유는 역시 DI(의존성 주입)이 있습니다.

public class UserService {
    private PlatformTransactionManager transactionManager;
    
    // ePlatformTransactionManager setter 코드 부분
    
    public void upgradeLevels() {
        TransactionStatus status 
            = transactionManager.getTransaction(new DefaultTransactionDefinition());
        
        try {
            List<User> users = userDao.getAll();
            for(User user : users) {
            	if(canUpgradeLevel(user)) {
                	upgradeLevel(user)
                }
            }
        } catch(RuntimeException e) {
        	this.transactionManager.roolaback(status);
            throw e;
        } 
    }
}


아래 코드는 비즈니스 로직을 쉽게 파악할 수 있고 트랜잭션의 구현 방법을 변화로부터 분리되어있는 PSA를 구현한 최종적인 코드입니다.

이번 글은 PSA, 서비스 추상화에 대해 정리했습니다. PSA를 변화에 열려있는 코드를 만드는 방법에 대해 알 수 있었던 것 같습니다. 잘못된 내용이 있다면 댓글로 남겨주세요.

반응형

'Spring Framework' 카테고리의 다른 글

QueryDSL 바로 알기  (0) 2024.03.31
[Spring] spring에 mybatis 적용하기  (0) 2022.11.26
[Spring] 예외를 처리하는 방법  (0) 2022.11.01
[Spring] JdbcTemplate  (0) 2022.10.22
[Spring] TDD란?  (0) 2022.10.14