스터디/[토비의 스프링 3.1 Vol 1] (2025.03)

[토비의 스프링 3.1 Vol 1] 5장. 서비스 추상화 (5.2.3 ~)

ttoance 2025. 4. 13. 09:10
반응형


5.2.3 트랜잭션 동기화 

Connection 파라미터 제거 

  • 먼저 Connection을 파라미터로 직접 전달하는 문제를 해결한다. 
  • upgradeLevels() 메소드가 트랜잭션 경계설정을 해야 한다는 사실은 피할 수 없다. 
    • 따라서 그 안에서 Connection을 생성하고 트랜잭션 시작과 종료를 관리하게 된다. 
    • Connection 오브젝트를 계속 파라미터로 전달하다가 DAO를 호출할 때 사용하게 하는 대신 스프링이 제안하는 방법은 트랜잭션 동기화 transaction synchronization 방식이다. 
  • 트랜잭션 동기화란 UserService에서 트랜잭션을 시작하기 위해 만든 Connection 오브젝트를 특별한 저장소에 보관해두고, 이후에 호출되는 DAO의 메소드에서는 저장된 Connection을 가져다가 사용하게 하는 것이다. 

 

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

(1) UserSercvice는 Connection을 생성 

(2) 트랜잭션 동기화에 저장해두고 Connection의 setAutoCommit(false) 를 호출해 트랜잭션 시작 후에 DAO기능 이용 시작 

(3) 첫번째 update() 메소드 호출되고 update() 메소드 내부에서 이용하는 JdbcTemplate 메소드에서 가장 먼저 

(4) 트랜잭션 동기화에 현재 시작된 트랜잭션을 가진 Connection 오브젝트가 존재하는지 확인 

(5) Connection 이용해 PreparedStatement을 만들어 수정 SQL 실행. 여전히 Connection은 열려있고 

(6) 두 번째 update가 호출되면 

(7) 트랜잭션 동기화 저장소에서 Connection을 가져와 

(8) 사용한다. 

(9) 마지막 updae도 

(10) 같은 트랜잭션 가져와 

(11) 사용한다. 

(12) 모든 작업이 정상적으로 끝나면 Connection의 commit() 호출해서 트랜잭션 완료 시킨다. 

(13) 마지막으로 더이상 Connection 오브젝트를 저장해두지 않도록 이를 제거한다. 

(*) 어느 작업 중에라도 예외가 발생하면 즉시 rollback() 호출하고 트랜잭션을 종료할 수 있다. 물론 이때도 트랜잭션 저장소에 저장된 동기화 Connection 오브젝트는 제거해줘야 한다. 

 

 

트랜잭션 동기화 저장소 이용한 동기화 기법 

  • 트랜잭션 동기화 저장소는 작업 스레드마다 독립적으로 Connection 오브젝트 저장하고 관리하기 때문에 다중 사용자를 처리하는 서버의 멀티스레드 환경에서도 충돌이 날 염려가 없다. 
  • 스프링은 JdbcTemplate과 더불어 이런 트랜잭션 동기화 기능 지원하는 유틸리티 메소드 제공한다. 
  • JDBC의 트랜잭션 경계설정 메소드 사용해 트랜잭션 이용하는 전형적인 코드에 간단한 트랜잭션 동기화 작업만 붙여줌으로써, 지저분한 Connection 파라미터 문제를 해결했다. 

Vol1-30/Ch5/5.2.3/src/springbook/user/service/UserService.java

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

 

 

5.2.4 트랜잭션 서비스 추상화 

새로운 여러 가지 문제가 발생한다. 

  • 하나의 트랜잭션 안에 여러 개의 DB에 데이터를 넣는 작업을 해야 할 수 있다. 
    • 로컬 트랜잭션으로는 불가능하고, 글로벌 트랜잭션 방식을 사용해야 한다. 
    • 자바는 JDBC 외에 이런 글로벌 트랜잭션 지원하는 트랜잭션 매니저 지원하기 위한 API인 JTA (Java Transaction API)를 제공하고 있다. 
  • 하이버네이트 이용한 트랜잭션 관리 코드는 JDBC나 JTA의 코드와는 다르다. 
    • 하이버네이트는 Connection을 직접 사용하지 않고 Session이라는 것을 사용하고, 독자적인 트랜잭션 API를 사용한다. 

 

JTA (Java Transaction API)

  • 애플리케이션은 기존의 방법대로 DB는 JDBC, 메시징 서버라면 JMS(Java Messaging System) 같은 API를 사용해서 필요한 작업 수행한다. 
  • 단, 트랜잭션은 JDBC나 JMS API 사용해서 직접 제어하지 않고 JTA를 통해 트랜잭션 매니저가 관리하도록 위임한다. 
  • 트랜잭션 매니저는 DB와 메시징 서버를 제어하고 관리하는 각각의 리소스 매니저와 XA 프로토콜을 통해 연결된다. 

 

 

UserService의 코드가 특정 트랜잭션 방법에 의존적이지 않고 독립적이려면 ?

  • UserService의 메소드 안에서 트랜잭션 경계설정 코드는 제거할 수 없다. 
  • 하지만 특정 기술에 의존적인 Connection, UserTransaction, Session/Transaction API 등에 종속되지 않게 할 수 있는 방법은 있다. 
    • 트랜잭션 처리 코드에도 추상화를 도입하여 JDBC, JTA, 하이버네이트..의 공통적인 특징 모아서 추상화된 트랜잭션 관리 계층을 만들 수 있다. 

 

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

  • 스프링이 제공하는 트랜잭션 경계설정을 위한 추상 인터페이스는 PlatformTransactionManager이다. 
    • 로컬 트랜잭션 이용한다면 PlatformTransactionManager 구현한 DataSourceTransactionManager 사용하면 된다. 
    • JDBC 이용한 경우에는 먼저 Connection을 생성하고 나서 트랜잭션을 시작했다. 
      • 하지만 PlatformTransactionManager에서는 트랜잭션 가져오는 요청인 getTransaction() 메소드를 호출하기만 하면 된다. 

 

Vol1-30/Ch5/5.final/src/springbook/user/service/UserService.java

	public void upgradeLevels() {
		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 (RuntimeException e) {
			this.transactionManager.rollback(status);
			throw e;
		}
	}

 

 

 

 

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

 

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

  • 기술과 서비스에 대한 추상화 기법 이용하면 특정 기술환경에 종속되지 않는 포터블한 코드를 만들 수 있다. 
    • UserDao와 UserService는 각각 담당하는 코드의 기능적인 관심에 따라 분리되고, 서로 불필요한 영향을 주지 않으면서 독자적으로 확장이 가능하도록 만든다. 
      • 같은 계층에서 수평적인 분리라고 할 수 있다. 
      • UserService는 순수하게 사용자 관리의 업무의 비즈니스 로직을 담고 있다. 
      • UserDao와 UserService는 인터페이스와 DI를 통해 연결됨으로써 결합도가 낮아졌다. 
        • 결합도가 낮다는 건 데이터 액세스 로직이 바뀌거나, 심지어 데이터 액세스 기술이 바뀐다고 할지라도 UserService의 코드에는 영향을 주지 않는다. 서로 독립적으로 확장할 수 있다. 
      • 또, UserDao는 DB 연결을 생성하는 방법에 대해 독립적이다. 
        • DataSource 인터페이스와 DI를 통해 추상화된 방식으로 로우레벨의 DB 연결 기술을 사용하기 때문이다. 
        • 애플리케이션 안에서 동작하는 DB 폴링 라이브러리를 사용하든, JDBC의 원시적인 DriverManager를 사용하든, WAS가 JNDI를 통해 제공하는 데이터 소스 서비스를 이용하든 상관없이 UserDao의 코드는 조금도 변하지 않는다. 
    • 트랜잭션 추상화는 이와는 다르다. 애플리케이션의 비즈니스 로직과 그 하위에서 동작하는 로우레벨의 트랜잭션 기술이라는 아예 다른 계층의 특성 갖는 코드를 분리한다. 
      • UserService와 트랜잭션 기술과도 스프링이 제공하는 PlatformTransactionManager 인터페이스 통한 추상화 계층을 사이에 두고 사용했기 때문에, 구체적인 트랜잭션 기술에 독립적인 코드가 됐다. 

 

단일 책임 원칙 

  • 객체지향 설계의 원칙 중의 하나인 단일 책임 원칙으로 설명할 수 있다. 
    • 단일 책임 원칙은 하나의 모듈은 한 가지 책임을 가져야 한다는 의미다. 
    • 하나의 모듈이 바뀌는 이유는 한 가지여야 한다. 

ex, UserService에 JDBC Connection의 메소드 직접 사용하는 트랜잭션 코드가 있을때는 (1) 어떻게 사용자 레벨을 관리할 것인가와 (2) 어떻게 트랜잭션 관리할 것이라는 두 가지 책임을 갖고 있다. 

 

 

객체지향 설계와 프로그래밍의 원칙은 서로 긴밀하게 관련이 있다. 단일 책임 원칙을 잘 지키는 코드를 만들려면 인터페이스를 도입하고 이를 DI로 연결해야 하며, 그 결과로 단일 책임 원칙뿐만 아니라 개방 폐쇄 원칙도 잘 지키고, 모듈 간에 결합도가 낮아서 서로의 변경이 영향을 주지 않고, 같은 이유로 변경이 단일 책임에 집중되는 응집도 높은 코드가 나오니까 말이다. 그뿐인가 ? 이런 과정에서 전략 패턴, 어댑터 패턴, 브리지 패턴, 미디에이터 패턴 등 많은 디자인 패턴이 자연스럽게 적용되기도 한다. 객체지향 설계 원칙을 잘 지켜서 만든 코드는 테스트하기도 편하다. 스프링이 지원하는 DI와 싱글톤 레스트리 덕분에 더욱 편리하게 자동화된 테스트를 만들 수 있다. 

 

 

5.4 메일 서비스 추상화 

테스트를 하면서 매번 메일이 발송되는 것이 바람직한가 ?

  • 바람직하지 않다. 
    • 메일 발송이란 매우 부하가 큰 작업이다. 
      • 테스트 실행할 때마다 메일 보내면 메일 서버에 부담을 줄 수 있다. 
    • 메일이 실제로 발송돼버린다는 문제도 있다.
  • 운영  시에는 JavaMail을 직접 이용해서 동작하도록 하고, 개발 중이거나 테스트 수행할 때는 JavaMail 대신할 수 있는, 그러나 JavaMail을 사용할 때와 동일한 인터페이스 갖는 코드가 동작하도록 만든다. 

 

테스트 위한 서비스 추상화 

  • 이 방식에는 한 가지 문제가 있다. 
    • 바로 트랜잭션 개념이 뺘져 있다는 사실이다. 
      • 한 가지 방법은 메일 업그레이드 할 사용자를 발견했을 때마다 발송하지 않고 발송 대상을 별도의 목록에 저장해두는 것이다. 업그레이드 작업 성공적으로 끝나면 한 번에 메일을 전송하면 된다. 
      • 다른 방법은 MailSender를 확장해서 메일 전송에 트랜잭션 개념을 적용하면 된다. 이 오브젝트에 업그레이드 작업 이전에 새로운 메일 전송 작업 시작을 알려주고, 그때부터는 메일 발송하지 않고 저장해두고 업그레이드 작업 끝나면 메일 모두 발송하고 예외 발송하면 모두 취소하게 한다. 
    • 전자가 사용자 관리 비즈니스 로직과 메일 발송에 트랜잭션 기능 적용하는 기술적인 방법이 한데 섞이는 거고, MailSender 구현 클래스 이용하는 방법은 서로 다른 종류의 작업을 분리해 처리한다는 점에서 장점이 있다. 

 

테스트 대역 

  • 테스트 대상이 되는 오브젝트가 또 다른 오브젝트에 의존하는 일은 매우 흔하다. 
  • 그래서 테스트용으로만 사용되는 특별한 오브젝트들이 있다. 
    • 테스트 대역 test double : 테스트 환경을 만들어주기 위해, 테스트 대상이 되는 오브젝트의 기능에만 충실하게 수행하면서 빠르게, 자주 테스트 실행할 수 있도록 사용하는 오브젝트 
      • 대표적인 테스트 대역은 테스트 스텁 test stub이다. 
        • 원래 오브젝트의 동작을 흉내 내지만, 복잡한 로직은 생략하고 테스트에 필요한 최소한의 결과만 제공
        • 진짜 동작 대신 고정된 값이나 예상되는 값을 리턴

 

목 오브젝트 이용한 테스트 

 

✅ Stub (결과만 필요할 때)

 

  • 목적: 결제가 성공했다고 가정하고 테스트만 하고 싶을 때
  • 반환 값이 고정되어 있음

 

class PaymentServiceStub:
    def pay(self, amount):
        return True  # 무조건 결제 성공하게 만듦

 

 

✅ Mock (행동 검증용)

 

  • 목적: pay()가 정말 호출됐는지 확인하고 싶을 때
  • 테스트에서 "호출 여부나 횟수" 등을 검증
class PaymentServiceMock:
    def __init__(self):
        self.was_called = False

    def pay(self, amount):
        self.was_called = True
        return True

# 테스트에서: assert mock.was_called == True

 

✅ Dummy (그냥 자리를 채움)

class PaymentServiceDummy:
    def pay(self, amount):
        pass  # 아무것도 안 함
  • 목적: 아직 구현 안 했거나, 전혀 중요하지 않은 의존성 채우기용

 

 

5.5 정리 

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

 

토비 스프링 소스코드 ▶ https://github.com/AcornPublishing/toby-spring3-1/tree/main

토비 스프링 ▶ https://product.kyobobook.co.kr/detail/S000000935358

반응형