5. 서비스 추상화

이유석·2023년 1월 2일
1

Book - Toby's Spring

목록 보기
20/20
post-thumbnail

5장에서는 지금까지 만든 DAO에 트랜잭션을 적용해보면서 스프링이 어떻게 성격이 비슷한 여러 종류의 기술을 추상화하고 이를 일관된 방법으로 사용할 수 있도록 지원하는지를 살펴볼 것이다.

5.1 사용자 레벨 관리 기능 추가

이전에 만들어 두었던 UserDao 를 다수의 회원이 가입할 수 있는 인터넷 서비스의 사용자 관리 모듈에 적용한다고 생각해보자.
사용자의 활동내역을 참고해서 레벨을 조정해주는 기능을 추가해보도록 하겠다.

5.1.3 UserService.upgradeLevels()

사용자 관리 로직(사용자의 활동 내역을 참고해서 레벨을 조정해주는 기능)은 어디다 두는 것이 좋을까?

  • UserDaoJdbc는 적당하지 않다.
    DAO는 데이터를 어떻게 가져오고 조작할지를 다루는 곳이지 비즈니스 로직을 두는 곳이 아니다.

  • 사용자 관리 비즈니스 로직을 담을 클래스를 하나 추가하자.
    비즈니스 로직 서비스를 제공한다는 의미에서 클래스 이름은 UserService 로 한다.

5.1.4 UserService.add()

해당 서비스에 처음 가입하는 사용자에 대하여 기본 레벨 (BASIC)을 부여하는 코드는 어디에 작성하는게 좋을까?

  • 이 또한 UserDaoJdbc 는 적당하지 않다.
    UserDaoJdbc는 User오브젝트를 DB에 정보를 넣고 읽는 방법에만 관심을 가져야지, 비즈니스적인 의미를 지니 정보를 설정하는 책임을 지는 것은 바람직하지 않다.

  • User클래스에 level 필드를 기본 레벨 (BASIC)으로 초기화 하는 방법은 어떨까?
    처음 가입할 때를 제외하면 무의미한 정보이기 때문에, 해당 로직을 위해 클래스에서 직접 초기화 하는것은 문제가 있어 보인다.

  • UserService 에 add() 를 만들어두고 사용자가 등록될 때 적용할 만한 비즈니스 로직을 담당하게 하면 될 것이다.

즉, 비즈니스 로직을 담은 코드(UserService)는 데이터 액세스 로직을 담은 코드(UserDao)와 깔끔하게 분리되는 것이 바람직하다.

5.1.5 코드 개선

작성된 코드가 스프링 사용자답게 깔끔하게 만들어진 코드인지 검토하기 위해 아래의 질문을 해볼 필요가 있다.

  • 코드에 중복된 부분은 없는가?
  • 코드가 무엇을 하는 것인지 이해하기 불편하지 않은가?
  • 코드가 자신이 있어야 할 자리에 있는가?
  • 앞으로 변경이 일어난다면 어떤 것이 있을 수 있고, 그 변화에 쉽게 대응할 수 있게 작성되어 있는가?

즉, 하나의 메서드에는 성격이 다른 여러가지 로직이 섞여있으면 안된다.
비즈니스 로직 코드 또한 내부적으로 책임과 역할에 따라서 깔끔하게 메서드로 정리되어야 한다.

  • 각 오브젝트와 메서드가 각각 자기 몫의 책임을 맡아 일을 하는 구조로 만들어야 한다.
  • 이렇게 코드를 작성하면,
    • 코드를 이해하기 쉽다.
    • 변경이 필요할 때 어디를 수정해야 할지도 쉽게 알 수 있다.

5.2 트랜잭션 서비스 추상화

5.2.1 모 아니면 도

다수의 사용자 레벨을 변경 하는 비즈니스 로직 중 중간에 실패했을 때, 모든 변경사항을 폐지하고자 한다.
즉, 실패하기 이전의 변경 사항 또한 이전으로 롤백 시켜주어야 한다.

이와 같이 DAO 를 사용하는 비즈니스 로직에는 작업의 단위를 지정하여,
해당 단위 작업을 보장해주는 트랜잭션이 필요하다.

트랜잭션

  • 더 이상 나눌 수 없는 단위 작업을 말한다.
  • 작업을 쪼개서 작은 단위로 만들 수 없다는 것은 트랜잭션의 핵심 속성인 원자성을 의미한다.

5.2.2 트랜잭션 경계 설정

DB 는 하나의 SQL 명령을 처리하는 경우는 DB 가 트랜잭션을 보장해준다.
하지만 2개 이상의 SQL 명령들을 하나의 트랜잭션으로 취급하여 처리해야 하는 경우를 생각해보자.

이때 첫번째 SQL 을 성공적으로 실행했지만, 두번째 SQL 이 장애가 생겨 작업이 중단되었다면
첫번째 SQL 의 적용사항을 되돌려놓아야 한다. 이를 트랜잭션 롤백 이라고 한다.

이와 반대로 모든 SQL 명령들이 성공적으로 작업을 처리했다면, DB에게 성공적으로 마무리 되었다고 알려주며 작업을 확정시켜야 한다. 이를 트랜잭션 커밋 이라고 한다.

즉, 트랜잭션을 시작하는 방법은 한가지 이지만, 끝나는 방법은 두가지(트랜잭션 롤백, 트랜잭션 커밋) 이다.

트랜잭션 경계 설정

  • 트랜잭션의 시작과 종료를 지정하는 일을 트랜잭션 경계 설정이라고 한다.
    이는 주로 비즈니스 로직 안에서 일어나는 경우가 많다.

  • 즉, 비즈니스 로직 (서비스 코드) 내에서 트랜잭션의 시작과 종료를 지정해주어야 한다.
    하지만 이를 위해서 DB Connection 을 서비스 코드에서 생성하고 이를 DAO 코드에 파라미터로 전달해줘야만 한다.

  • 이런 식으로 수정하면 트랜잭션 문제는 해결 할 수 있지만, 여러 가지 새로운 문제가 발생한다.
    • DB Connection 을 비롯한 리소스의 깔끔한 처리를 가능하게 했던 JdbcTemplate 을 더 이상 활용할 수 없다.
    • DAO 의 메서드와 비즈니스 로직을 담고 있는 코드에 Connection 파라미터가 추가되어야 한다.
    • Connection 파라미터가 DAO 의 메서드의 파라미터로 추가되면 DAO 는 더 이상 데이터 액세스 기술에 독립적일 수 가 없다.
    • 또한, 테스트 코드에도 직접 Connection 오브젝트를 만들어 DAO 메서드가 이를 호출하도록 변경해주어야 한다.

5.2.3 트랜잭션 동기화

위 문제를 해결할 수 있도록 스프링은 독립적인 트랜잭션 동기화란 기술을 제공한다.

  • 비즈니스 로직이 담긴 코드가 트랜잭션을 시작하기 위해 만든 Connection 오브젝트를 특별한 저장소에 보관하고,
  • 이후에 호출되는 DAO 메서드에서 사용하는 JdbcTemplate 은 저장된 Connection을 가져다가 사용하면 된다.
  • 이후 트랜잭션이 모두 종료되면 (커밋 또는 롤백), 그때는 동기화를 마치면 된다.

5.2.4 트랜잭션 서비스 추상화

이때, 한 개 이상의 DB 로 작업을 수행한다면 새로운 문제가 발생한다.

  • 트랜잭션 동기화 방법은 로컬 트랜잭션 방법으로 하나의 DB Connection 에 종속되어 있기 때문이다.

따라서 각 DB 을 통합적으로 관리하는 트랜잭션 관리자를 통해 트랜잭션을 관리하는 글로벌 트랜잭션방식을 사용하여야 한다.

Java 는 글로벌 트랜잭션을 지원하는 트랜잭션 매니저를 지원하기 위한 API 인 JTA (Java Trasaction API) 를 제공한다.

또한, Jdbc 가 아닌 Hibernate 를 이용하여 UserDao 를 직접 구현한 사용자를 위한 트랜잭션 관리를 위한 코드가 필요한 상황이 발생한다면 이를 어떻게 해결할 수 있을까?

  • Hibernate는 Connection 을 직접 사용하지 않고, Session 이라는 것을 사용하고, 독자적인 트랜잭션 관리 API를 사용한다.

이와 같이 Java 에서 사용되는 트랜잭션 API의 종류와 방법은 다양하다. 환경과 서버에 따라서 트랜잭션 방법이 변경되면 경계 설정 코드도 함께 변경되어야 한다.

이때, 트랜잭션 API 의 종류와 방법에 따라 비즈니스 로직을 담은 코드가 함께 변경되면 단일 책임 원칙에 위배되며, DAO 가 사용하는 특정 기술에 대해 강한 결합을 만들어낸다.

트랜잭션 경계 설정 코드가 비즈니스 로직 코드에 영향을 주지 않게 하려면 스프링이 제공하는 트랜잭션 서비스 추상화를 이용하면 된다.

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

추상화 : 하위 시스템의 공통점을 뽑아내서 분리시키는 것

  • 트랜잭션의 경계 설정을 담당하는 코드는 일정한 패턴을 갖는 유사한 구조이다.

  • 서비스 추상화는 로우레벨의 트랜잭션 기술과 API의 변화에 상관 없이 일관된 API를 가진 추상화 계층을 도입한다.

  • 서비스 추상화는 테스트 하기 어려운 Java Mail 같은 기술에도 적용할 수 있다. 테스트를 편리하게 작성하도록 도와주는 것만으로도 서비스 추상화는 가치가 있다.

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

이렇게 기술과 서비스에 대한 추강화 기법을 이용하면 특정 기술환경에 종속되지 않는 포터블한 코드를 만들 수 있다.

스프링은 이를 DI를 통해서 가능하게 해주며, 각 계층의 기술들이 서로 영향을 주지 않고 자유롭게 확장될 수 있는 구조를 만들 수 있게 해준다.

DI의 가치는 이렇게 관심, 책임, 성격이 다른 코드를 깔끔하게 분리하는 데 있다.

이런 적절한 분리가 가져오는 특징은 객체지향 설계의 원칙 중의 하나인 단일 책임 원칙 으로 설명할 수 있다.

단일 책임 원칙

  • 하나의 모듈은 한 가지 책임을 가져야 한다.
  • 즉, 하나의 모듈이 변경되는 이유는 한 가지여야 한다.

DI 를 통해 서비스 추상화 방식을 도입한 이후, 비즈니스 로직의 특정 기술의 종류가 바뀌더라도 설정 파일만 바꿔주면 된다.
이는 단일 책임 원칙을 충실하게 지키고 있다고 말할 수 있다.

단일 책임 원칙의 장점

  • 어떤 변경이 필요할 때 수정 대상이 명확해진다.

5.4 메일 서비스 추상화

사용자 레벨이 변경되었을 때, 해당 사용자에게 알림 메일을 전송해야 하는 요청사항이 들어왔다.

  • 사용자의 이메일 정보를 관리해야 한다. (User에 email 필드를 추가한다. 이에 따라 UserDao 및 테스트 코드를 수정한다.)
  • UserService 의 upgradeLevel() 메서드에 메일 발송 기능을 추가한다.

메일 발송 기능은 JavaMail 기술을 사용하여 추가한다.
JavaMail 기술을 사용하여 추가한 메일 발송 기능의 테스트는 어떻게 수행하여야 할까?

해당 테스트를 위해, 매번 실제 메일 서버를 사용하는 것은 큰 비용이다.
이를 방지하고자, 테스트용 메일 서버를 이용해 외부로 메일을 발송하지 않고 해당 메일 서버에 전송 요청이 잘 들어왔는지 까지만 테스트 한다.

이와 똑같은 원리로, JavaMail 기술을 실제 사용하지 않고, 테스트용 JavaMail 을 활용하여 JavaMail 의 작업인 외부 메일 서버 및 네트워크 연동 부분을 줄여 테스트 시, 부하가 발생하지 않도록 할 수 있다.

하지만 JavaMail 은 확장이나 지원이 불가능하도록 만들어져 있기 때문에, JavaMail 의 구현을 테스트용으로 바꾸는 것은 불가능 할거라고 생각할 것 이다.

이를 가능하게 하는 방법 역시, 추상화를 활용한 방법이다.

JavaMail 을 활용하는 비즈니스 로직에 JavaMail 을 인스턴스로 선언 후, DI를 통해 사용하게하면 된다.
이렇게 하면 설정 파일에서 간단히 JavaMail 의 class 경로만 변경해주면, 테스트용으로 구현한 클래스로 변경이 가능하다.

테스트를 편리하게 작성하도록 도와주는 것만으로도 서비스 추상화는 가치가 있다.

5.4.4 테스트 대역

테스트 대역

  • 테스트 대상이 사용하는 의존 오브젝트를 대체할 수 있도록 만든 테스트용 오브젝트
  • 즉, JavaMail 을 테스트용으로 구현한 클래스
  • 테스트 대역은 테스트 대상 오브젝트가 원활하게 동작할 수 있도록 도우면서 테스트를 위해 간접적인 정보를 제공해주기도 한다.

테스트 스텁

  • 대표적인 테스트 대역 중 하나이다.
  • 테스트 대상 오브젝트의 의존객체로서 존재하면서 테스트 동안에 코드가 정상적으로 수행할 수 있도록 돕는 것

목 오브젝트

  • 테스트 대상 오브젝트의 결과 뿐 아니라, 테스트 오브젝트가 간접적으로 의존 오브젝트에 넘기는 값과 그 행위 자체에 대해서도 검증하고 싶을 때 사용한다.
  • 테스트 오브젝트와 의존 오브젝트 사이에서 일어나는 커뮤니케이션 내용을 저장해뒀다가 테스트 결과를 검증하는 데 활용할 수 있게 해준다.

소스코드

profile
https://github.com/yuseogi0218

0개의 댓글