6. AOP

6장 AOP

6.1. 트랜잭션 코드의 분리

6.1.1. 메서드 분리

기존 upgradeLevels() 메서드는 다음과 같은 구조였습니다.

  • 트랜잭션 시작

  • 비즈니스 로직 실행

  • 커밋 또는 롤백

즉, 트랜잭션 경계설정 코드 + 비즈니스 로직이 하나의 메서드 안에 공존하고 있습니다.

fun upgradeLevels() {
    val status = transactionManager.getTransaction(DefaultTransactionDefinition())

    try {
        val users = userDao.getAll()
        for (user in users) {
            if (canUpgradeLevel(user)) {
                upgradeLevel(user)
            }
        }
        transactionManager.commit(status)
    } catch (e: Exception) {
        transactionManager.rollback(status)
        throw e
    }
}

여기서 눈에 띄는 문제는 다음입니다.

  • 트랜잭션 코드가 로직의 앞뒤를 감싸고 있음

  • 비즈니스 로직이 기술 코드에 의해 오염됨

  • 수정 시 실수 위험 증가

1차 개선 – 내부 메서드로 분리

비즈니스 로직을 별도 메서드로 분리합니다.

이렇게 하면 다음과 같은 효과가 있습니다.

  • 트랜잭션 경계설정 코드가 위로 정리됨

  • 비즈니스 로직이 한눈에 들어옴

  • 가독성 향상

하지만 여전히 같은 클래스 안에 트랜잭션 코드가 존재합니다.

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

UserService는 비즈니스 로직만 담당해야 하는데, 트랜잭션이라는 기술 코드가 포함되어 있다.

구조 변경 목표

  • UserServiceImpl → 순수 비즈니스 로직만 담당

  • UserServiceTx → 트랜잭션 경계설정만 담당

이를 위해 먼저 인터페이스를 도입합니다.


UserServiceImpl – 순수 비즈니스 로직

이제 이 클래스에는 다음이 없습니다.

  • transactionManager

  • commit

  • rollback

  • DefaultTransactionDefinition

완전히 기술 코드가 제거되었습니다.


UserServiceTx – 트랜잭션 전담 클래스

여기서 중요한 점은 다음입니다.

  • UserServiceTx는 비즈니스 로직을 모름

  • 단지 위임(delegate)만 수행

  • upgradeLevels() 호출 전후에 트랜잭션 처리

이 구조는 데코레이터 패턴과 매우 유사합니다.


DI 설정의 의미

이제 클라이언트는 다음 구조로 동작합니다.

즉,

  • 외부에서 보기에는 UserService 하나

  • 실제로는 트랜잭션 기능이 덧붙여진 구조

스프링 설정은 다음과 같은 개념입니다.

  • userService 빈 → UserServiceTx

  • userServiceImpl 빈 → 실제 비즈니스 로직

  • transactionManager → UserServiceTx에 주입


트랜잭션 분리 후 테스트 수정

구조가 바뀌면 테스트도 바뀝니다.

이전에는 UserServiceImpl을 직접 테스트했습니다. 이제는 트랜잭션 기능이 포함된 UserServiceTx를 테스트해야 합니다.

테스트에서 수동 DI를 구성하면 다음과 같습니다.

그리고 트랜잭션이 롤백되는지 검증합니다.

여기서 중요한 포인트는 다음입니다.

  • 트랜잭션 테스트는 비즈니스 테스트와 다름

  • 트랜잭션 경계설정이 정상 동작하는지 검증

  • 구조 변경에 따라 테스트도 변경되어야 함


트랜잭션 경계설정 코드 분리의 장점

이 분리를 통해 얻는 가장 큰 장점은 다음입니다.

1. 비즈니스 로직의 순수성 확보

UserServiceImpl은 이제 다음에만 집중합니다.

  • 사용자 레벨 업그레이드 조건

  • 상태 변경

  • 메일 발송

기술 코드와 완전히 분리되었습니다.


2. 기술 교체 가능성 확보

  • JTA

  • JDBC 트랜잭션

  • 다른 트랜잭션 전략

비즈니스 로직을 건드리지 않고 교체 가능해집니다.


3. 트랜잭션 적용 대상 확장 가능

같은 방식으로 다른 서비스에도 트랜잭션 기능을 붙일 수 있습니다. 트랜잭션은 횡단 관심사(Cross-Cutting Concern)입니다.

6.2. 고립된 단위 테스트

6.2.1. 복잡한 의존관계 속의 테스트

UserService는 단순해 보이지만 실제로는 다음 의존성을 가집니다.

  • UserDao → DB, DataSource, JDBC Driver

  • PlatformTransactionManager → 트랜잭션 인프라

  • MailSender → JavaMail, 메일 서버

즉, UserService 테스트는 다음을 함께 테스트하는 것과 같습니다.

  • DB 서버

  • 트랜잭션 매니저

  • 메일 서버

  • 네트워크 환경

이건 더 이상 단위 테스트가 아닙니다.

문제점은 다음과 같습니다.

  • 테스트 속도가 느림

  • 환경 의존성 발생

  • 실패 원인 추적 어려움

  • DB 상태에 따라 결과가 달라질 수 있음

6.2.2. 6.2 고립된 단위 테스트

이번 절에서는 테스트를 가능한 한 작은 단위로 분리하는 방법과, 그 과정에서 등장하는 목(Mock) 객체와 Mockito의 활용까지 정리합니다. 핵심은 다음 한 문장으로 요약할 수 있습니다.

테스트는 작게, 의존성은 제거하고, 행위만 검증한다.


요약

  • 작은 단위 테스트는 실패 원인 파악이 쉽고 빠르다.

  • UserService는 겉보기보다 많은 외부 의존성을 가진다.

  • 의존 객체(DB, 트랜잭션, 메일 서버 등)를 제거하지 않으면 진짜 단위 테스트가 아니다.

  • 이를 해결하기 위해 스텁(Stub)과 목(Mock) 객체를 사용한다.

  • Mockito는 목 객체를 매우 간단하게 만들어준다.


6.2.1 복잡한 의존관계 속의 테스트

UserService는 단순해 보이지만 실제로는 다음 의존성을 가집니다.

  • UserDao → DB, DataSource, JDBC Driver

  • PlatformTransactionManager → 트랜잭션 인프라

  • MailSender → JavaMail, 메일 서버

즉, UserService 테스트는 다음을 함께 테스트하는 것과 같습니다.

  • DB 서버

  • 트랜잭션 매니저

  • 메일 서버

  • 네트워크 환경

이건 더 이상 단위 테스트가 아닙니다.

문제점은 다음과 같습니다.

  • 테스트 속도가 느림

  • 환경 의존성 발생

  • 실패 원인 추적 어려움

  • DB 상태에 따라 결과가 달라질 수 있음

6.2.2 테스트 대상 오브젝트 고립시키기

진짜 단위 테스트를 만들기 위해서는 테스트 대상 외의 모든 것을 가짜 객체로 바꿔야합니다.

  • DB 대신 MockUserDao

  • 메일 서버 대신 MockMailSender

  • 트랜잭션은 이미 분리되었으므로 제거 가능

기존 방식 (DB 의존 테스트)

이 테스트는 DB에 의존합니다.

고립된 테스트 구조

이제 UserServiceImpl만 직접 생성합니다.

이제 DB가 필요 없습니다.


MockUserDao 구현

핵심은 다음입니다.

  • getAll() → 테스트용 사용자 반환

  • update() → 호출된 사용자 기록

  • 나머지 메서드 → 사용되면 예외 발생


검증 코드

이제 DB를 확인할 필요가 없습니다.

테스트 성능 비교

  • DB 사용하는 테스트: 0.25초,

  • 고립 테스트: 0.001초 이하로 차이가 큽니다.

테스트가 커질수록 실행 속도 차이가 큽니다.

6.2.3. 단위 테스트와 통합 테스트

정리하면 다음과 같습니다.

단위 테스트

  • 외부 의존성 제거

  • 빠름

  • 안정적

  • 행위 검증 중심

통합 테스트

  • 여러 객체가 함께 동작

  • DB, 네트워크 포함 가능

  • 느림

  • 환경 의존적

가이드라인은 다음입니다.

  • 항상 단위 테스트를 먼저 고려한다.

  • DAO는 통합 테스트로 검증해도 된다.

  • 외부 리소스를 사용하는 테스트는 최소화한다.

6.2.4 목 프레임워크 – Mockito

목 객체를 직접 구현하면 번거롭습니다.

Mockito를 사용하면 매우 간단해집니다.

Mockito 기본 사용법

MailSender 검증

ArgumentCaptor 사용 예시

Mockito가 해주는 일

  • 인터페이스 구현 자동 생성

  • 호출 횟수 검증

  • 파라미터 검증

  • 반환값 설정

  • 예외 강제 발생 가능

직접 Mock 클래스를 작성할 필요가 거의 없습니다.

Last updated