과정을 즐기자

서비스 추상화 본문

Spring

서비스 추상화

320Hwany 2023. 4. 11. 18:18

이 글은 토비의 스프링을 읽고 정리한 내용입니다.

 

트랜잭션 경계설정

애플리케이션 내에서 트랜잭션이 시작되고 끝나는 위치를 트랜잭션의 경계라고 부릅니다. 

JDBC의 트랜잭션은 하나의 Connection을 가져와 사용하다가 닫는 사이에서 일어납니다. 

트랜잭션의 시작과 종료는 Connection 오브젝트를 통해 이뤄지기 때문입니다.  

지금까지 사용한 템플릿 메소드 호출 한 번에 한 개의 DB 커넥션이 만들어지고 닫힙니다.  

 

결국 JdbcTemplate의 메소드를 사용하는 UserDao는 각 메소드마다 하나씩의 독립적인 

트랜잭션으로 실행될 수밖에 없습니다. 

 

여러 개의 작업을 하나의 트랜잭션으로 묶어주려면 트랜잭션의 경계 설정 작업은 비지니스 로직을 

처리하는 쪽에서 가져와야 합니다. 하지만 이때 크게 4가지의 문제 상황이 발생합니다.

 

1. DB 커넥션을 비롯한 리소스의 깔끔한 처리를 가능하게 했던 JdbcTemplate을 사용할 수 없다. 

2. Service의 메소드에 Connection 파라미터가 추가된다.

3. Connection 파라미터가 UserDao 인터페이스 메소드에 추가되면 데이터 액세스 기술에 

독립적이지 않다.

4. Connection 파라미터를 받으면 기존 테스트 코드에 영향을 미친다.   

트랜잭션 동기화

위에서 발생한 문제를 해결해보겠습니다. 

 

1. 스프링이 제공하는 트랜잭션 동기화 관리 클래스는 TransactionSynchronizationManager이다.  

이 클래스를 이용해 먼저 트랜잭션 동기화 작업을 초기화하도록 요청한다.  

2. DataSourceUtils에서 제공하는 getConnection() 메소드를 통해 DB 커넥션을 생성한다.  

이것은 Connection 오브젝트를 생성해줄 뿐만 아니라 트랜잭션 동기화에 사용하도록  

저장소에 바인딩 해준다.  

3. 동기화가 준비됐으면 트랜잭션을 시작하고 트랜잭션 내의 작업을 진행한다. 작업을 정상적으로 

마치면 트랜잭션을 커밋해준다. 

4. 스프링 유틸리티 메소드의 도움을 받아 커넥션을 닫고 트랜잭션 동기화를 마치도록 요청한다.  

예외가 발생하면 롤백한다. 

 

이 과정에서 JdbcTemplate은 트랜잭션 동기화 저장소에 등록된 DB 커넥션이나 트랜잭션이 없으면  

직접 DB 커넥션을 만들고 트랜잭션을 시작합니다. 만약 트랜잭션 동기화를 시작해놓았다면  

그때부터 실행되는 메소드에서는 트랜잭션 동기화 저장소에 들어있는 DB 커넥션을 가져와서 사용합니다.  

 

이로써 비즈니스 로직 레벨에서 트랜잭션을 적용하면서도 JdbcTemplate을 사용하고 지저분한 

Connection을 파라미터로 전해주지 않아도 됩니다. 데이터 액세스 기술에 종속되지도 않고  

테스트 코드에서도 문제가 발생하지 않습니다!

 

트랜잭션 서비스 추상화

하지만 아직 문제가 남아있습니다. 바로 트랜잭션 관리 코드는 데이터 액세스 기술마다 다르다는 점입니다.  

비지니스 로직의 메소드 안에서 트랜잭션 경계설정 코드를 제거할 수는 없습니다. 하지만 특정 기술에 

의존적인 Connection, UserTransaction, Session/Transaction API 등에 종속되지 않게 

할 수는 있습니다.  

 

애플리케이션 코드에서는 트랜잭션 추상계층이 제공하는 API를 이용해 트랜잭션을 이용하게 만들어준다면

특정 기술에 종속되지 않는 트랜잭션 경계설정 코드를 만들 수 있습니다.

 

스프링이 제공하는 트랜잭션 경계설정을 위한 추상 인터페이스는 PlatformTransactionManager입니다.

JDBC를 이용하는 경우에는 먼저 Connectoin을 생성하고 나서 트랜잭션을 시작했습니다.  

하지만 PlatformTransactionManager에서는 트랜잭션을 가져오는 요청인 getTransaction() 메소드를 

호출하기만 하면 됩니다. 필요에 따라 트랜잭션 매니저가 DB 커넥션을 가져오는 작업도 같이 수행해줍니다.  

 

스프링의 트랜잭션 추상화 기술은 앞에서 적용해봤던 트랜잭션 동기화를 사용합니다. 

PlatformTransactionManager로 시작한 트랜잭션은 트랜잭션 동기화 저장소에 저장됩니다.  

 

메일 서비스 추상화

메일 서비스 추상화 부분은 마침 하고 있던 프로젝트에서 메일 전송 기능이 있어서 이 부분에 적용해보았습니다.  

 

원래는 하나의 EmailQuizSender라는 클래스를 사용해 메일을 작성하고 전송하는 기능이 있었습니다.  

하지만 메일을 전송한다는 것은 매번 테스트 하기가 어렵습니다.

이를 해결하기 위해 먼저 EmailQuizSender를 인터페이스로 변경하였습니다.  

 

EmailQuizSender

public interface EmailQuizSender {

    void sendQuizList(List<Quiz> randomQuizList, String toEmail);

    StringBuffer makeText(List<Quiz> randomQuizList);
}

EmailQuizSender의 구현 클래스로 실제 운영환경에서 사용할 클래스, 테스트를 위한 클래스 2개를 만들었습니다.  

 

EmailQuizSenderProd

@RequiredArgsConstructor
@Transactional(readOnly = true)
@Primary
@Service
public class EmailQuizSenderProd implements EmailQuizSender {

    private final JavaMailSender mailSender;

    public void sendQuizList(List<Quiz> randomQuizList, String toEmail) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(toEmail);
        message.setSubject(EMAIL_SUBJECT);
        StringBuffer sb = makeText(randomQuizList);
        message.setText(String.valueOf(sb));
        mailSender.send(message);
    }

    public StringBuffer makeText(List<Quiz> randomQuizList) {
        StringBuffer sb = new StringBuffer();
        for (Quiz quiz : randomQuizList) {
            sb.append("-------------------------------------------").append("\n");
            sb.append("분야 : ").append(quiz.getSubject()).append("\n");
            sb.append("문제 : ").append(quiz.getProblem()).append("\n");
            sb.append("답안").append("\n").append(quiz.getAnswer()).append("\n").append("\n");
        }
        return sb;
    }
}

이렇게 운영환경에서 사용할 클래스는 실제로 메일을 전송하는 기능을 가지고 있습니다. 

 

EmailQuizSenderTest

@Getter
@Service
public class EmailQuizSenderTest implements EmailQuizSender {

    private ThreadLocal<SimpleMailMessage> testMailSender = new ThreadLocal<>();

    public void sendQuizList(List<Quiz> randomQuizList, String toEmail) {
        SimpleMailMessage message = new SimpleMailMessage();
        message.setTo(toEmail);
        message.setSubject(EMAIL_SUBJECT_TEST);
        StringBuffer sb = makeText(randomQuizList);
        message.setText(String.valueOf(sb));
        testMailSender.set(message);
    }

    public StringBuffer makeText(List<Quiz> randomQuizList) {
        StringBuffer sb = new StringBuffer();
        for (Quiz quiz : randomQuizList) {
            sb.append(quiz.getProblem());
        }
        return sb;
    }
}

이메일 전송을 위한 테스트를 위해서 직접 메일을 전송하는 것이 아니라 testMailSender를 만들고 

여기에 잘 저장이 되는지 확인하면 됩니다. makeText() 메소드 또한 실제 운영환경보다 

단순화하여 작성하였습니다.  

또한 두 개의 구현 클래스 모두 스프링 빈으로 등록되지만 운영환경에서 사용할 클래스에 

@Primary를 붙여 우선적으로 사용할 빈을 지정해주었습니다. 

 

MyEmailQuizSenderTest

public class MyEmailQuizSenderTest {

    private EmailQuizSenderTest emailQuizSenderTest = new EmailQuizSenderTest();

    @Test
    @DisplayName("랜덤 퀴즈 리스트가 testEmailSender에 저장되는지를 확입합니다")
    void sendQuizList() {
        // given
        String toMail = "test email";
        List<Quiz> randomQuizList = IntStream.range(1, 3)
                .mapToObj(i -> Quiz.builder()
                        .problem("퀴즈 문제 " + i)
                        .build()).toList();

        // when
        emailQuizSenderTest.sendQuizList(randomQuizList, toMail);

        // then
        ThreadLocal<SimpleMailMessage> testMailSender =
        					emailQuizSenderTest.getTestMailSender();
        SimpleMailMessage message = testMailSender.get();
        assertThat(message.getSubject()).isEqualTo(EMAIL_SUBJECT_TEST);
    }

    @Test
    @DisplayName("랜덤 퀴즈 리스트로 이메일의 텍스트를 생성합니다")
    void makeText() {
        // given
        List<Quiz> randomQuizList = IntStream.range(1, 3)
                .mapToObj(i -> Quiz.builder()
                        .problem("퀴즈 문제 " + i)
                        .build()).toList();

        // when
        StringBuffer sb = emailQuizSenderTest.makeText(randomQuizList);

        // then
        assertThat(sb).isNotBlank();
    }
}​

테스트 코드에서는 테스트 구현 클래스를 직접 생성하여 사용하였습니다.

이메일 전송은 testMailSender에 message가 형성되었고 이 message의 제목이 입력한 제목과

같은지 확인하는 작업으로 대체하였습니다. 

이메일 내용 생성에 대한 테스트는 단지 내용이 들어갔는지만 확인하는 작업으로 대체하였습니다.  

 

테스트는 반드시 운영환경과 일치할 수 없으므로 어느정도 다른 점은 있지만 최대한 비슷하게

작업을 해야합니다. 그리고 무엇보다 중요한 점은 어떤 상황에서라도 반복 가능해야 합니다.  

 

 

출처 :  토비의 스프링 3.1 Vol.1 스프링의 이해와 원리

'Spring' 카테고리의 다른 글

로컬 캐시를 적용하여 이메일로 전송하고자 하는 퀴즈 캐싱하기  (0) 2023.05.03
[Spring Boot] 이메일로 사용자 인증하기  (0) 2023.04.22
예외  (0) 2023.04.03
템플릿  (0) 2023.04.02
테스트  (0) 2023.03.26