이전에도 말했듯이 서비스계층은 비즈니스 로직만을 담는 계층이다. 즉, DB와 접근하는 기술, 트랜잭션 처리 기술 과 같이 다른 곳에서 처리해야하는 기술 로직이 포함되어있으면 안된다.
이를 해결하고자 스프링에서는 프록시와 AOP를 도입하게되었다.
프록시는 간단히 말해서 대리인이다. 이 프록시 객체를 사용하게되면 이제 앞으로 서비스 계층에서는 트랜잭션에 관여를 하는 코드를 일절 존재 하지 않게된다.
대신 그 모든걸 프록시 객체가 도맡아 하게된다. 간단한 코드를 통해 구조를 파악해보자.
프록시 객체에는 트랜잭션을 처리하는 코드가 포함되어있으며 또한 트랜잭션 프록시는 본인이 처리 해야할 타겟 서비스 객체(비즈니스 로직)를 필드 멤버로 주입받는다.
이제 클라이언트는 비즈니스로직을 실행하려할 때 서비스 객체가 아닌 프록시 객체를 호출하게된다. (물론 클라이언트는 그 사실을 인지하지못한다.)
호출된 프록시 객체는
- 트랜잭션 매니저를 통해 트랜잭션 시작, 커넥션을 생성
- 서비스 로직 실행 (서비스객체 호출)
- 트랜잭션 매니저를 통해 트랜잭션 종료, 커넥션 반납.
을 수행하게된다.
놀랍게도 이 프록시 객체는 스프링이 알아서 만들어준다. 즉, 개발자는 서비스 계층에서 비즈니스 로직 개발에만 풀집중 을 하면 되는것이다.
트랜잭션의 경우 공통기능이기 때문에 하나로 묶어서 관리하는 것이 굉장히 효율적이다. 여태 스프링을 공부하면서 느꼈겠지만 무언가 공통 기능을 수행하는 코드들을 분리하고 모듈화시키는 것을 스프링에서는 굉장히 잘 지원해준다.
이를 AOP
라고 부른다.
Spring AOP(Aspect-Oriented Programming)은 스프링 프레임워크에서 제공하는 기능 중 하나로, 애플리케이션의 여러 부분에서 로깅, 트랜잭션 관리, 보안 등과 같은 공통 기능을 분리하고 모듈화 하는 데 사용됩니다.
이를 통해 애플리케이션의 핵심 비즈니스 로직과 관심 사항을 분리하여 코드의 재사용성, 유지 보수성, 확장성을 향상할 수 있습니다.
스프링에서는 트랜잭션 AOP 기능을 굉장히 간단하게 사용할 수 있다. 바로 @Transactional
이다.
package hello.jdbc.service;
import hello.jdbc.domain.Member;
import hello.jdbc.repository.MemberRepositoryV3;
import lombok.extern.slf4j.Slf4j;
import org.springframework.transaction.annotation.Transactional;
import java.sql.SQLException;
/**
* 트랜잭션 - @Transactional AOP
*/
@Slf4j
public class MemberServiceV3_3 {
private final MemberRepositoryV3 memberRepository;
public MemberServiceV3_3(MemberRepositoryV3 memberRepository) {
this.memberRepository = memberRepository;
}
@Transactional
public void accountTransfer(String fromId, String toId, int money) throws SQLException {
bizLogic(fromId, toId, money);
}
private void bizLogic(String fromId, String toId, int money) throws SQLException {
Member fromMember = memberRepository.findById(fromId);
Member toMember = memberRepository.findById(toId);
memberRepository.update(fromId, fromMember.getMoney() - money);
validation(toMember);
memberRepository.update(toId, toMember.getMoney() + money);
}
private void validation(Member toMember) {
if (toMember.getMemberId().equals("ex")) {
throw new IllegalStateException("이체중 예외 발생");
}
}
}
이제 전혀 트랜잭션 로직을 찾아볼수가 없다.
DataSource
와 TransactionManager
의 경우 각각 구현체들이 다양하다. (ex, JDBC, JPA, MyBatis,,,)
그래서 과거 개발자들은 본인이 어떤 구현체를 사용할지
직접 @Bean
으로 등록해주었다. 하지만 지금은 스프링부트의 시대이다.
다음과 같이 간단한 설정 등록만으로 모든것이 해결된다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.username=sa
spring.datasource.password=
DataSource
: 스프링부트는 데이터소스의 기본값으로 HikariDataSource 를 사용하며 설정을 통해 다른 데이터소스를 사용할 수 있다.
TransactionManager
: 스프링 부트는 spring.datasource.url 을 확인하여 해당하는 기술에 알맞는 트랜잭션매니저를 주입해준다. 만일 url을 등록해놓지 않았다면 스프링부트는 그냥 메모리를 DB로 사용하게된다.
class MemberServiceV3_3Test {
@Autowired
private MemberRepositoryV3 memberRepository;
@Autowired
private MemberServiceV3_3 memberService;
@Test
void AopCheck() {
log.info("memberService class={}", memberService.getClass());
log.info("memberRepository class={}", memberRepository.getClass());
Assertions.assertThat(AopUtils.isAopProxy(memberService)).isTrue();
Assertions.assertThat(AopUtils.isAopProxy(memberRepository)).isFalse();
}
}
MemberService
의 클래스타입을 로그로 찍어서 출력해보면 다음과 같이 뒤에 CGLIB
라고 추가가 되는데 이게 바로 현재 MemberService
가 프록시객체임을 의미한다.
MemberRepository
는 AOP 를 적용 안했으므로 그런게 붙어있지 않다.
본 포스트는
김영한의 스프링 DB 1편 - 데이터 접근 핵심 원리 를 보고 정리했습니다.