본문 바로가기

Back-End/토비의 스프링3

#5. 서비스 추상화

요약



 비즈니르 로직을 담은 UserService 클래스를 만들고, 트랜잭션을 적용하면서 스프링의 서비스 추상화에 대해 알아보았다. 


  1.  비즈니스 로직을 담은 코드는 데이터 엑세스 코드와 깔끔하게 분리되는 것이 좋다.

  2.  DAO 기술 변화에 서비스 코드가 영향을 받지 않도록 인터페이스와 DI를 잘 활용해서 결합도를 낮춰야 한다.

먼저 프레임워크에 대해 생각해보면, MVC나 DB 등 특정 기술을 도와주는 역할로 알고 있다. 하지만 스프링은 JavaEE 전반을 도와주는 것이기 때문에 에플리케이션 프레임워크라고 불린다.


1. 비즈니로직과 데이터 엑세스 코드의 분리 



 비즈니르 로직을 담은 UserService 클래스를 만들고, 트랜잭션을 적용하면서 스프링의 서비스 추상화에 대해 알아보았다. 


 5.1.2 까지 작업을 통해 UserDao가 준비되었으니 데이터 액세스 기능은 문제 없다. 이제부터 사용자 관리 비즈니스 로직을 작성하면 된다.


 사용자 관리 로직은 어디에 두면 좋을까?

 UserDaoJdbc ? 


 아니다. 여긴 데이터를 어떻게 가져오고 조작할지를 다루는 곳이지 비즈니스 로직을 두는 곳이 아니다.


 즉, 사용자 관리 비즈니스 로직을 담을 클래스 ( UserService ) 를 하나 추가하자.    


  • 데이터 엑세스 로직의 변화가 비즈니스 로직에 영향이 가지 않도록 DI를 적용하자 ( 5.1.3 커밋 참고 )

 ( 내가 궁금해 했던 class DI 하는 방법 나옴 )

 

5.1.4. UserService.add()


 처음 가입하는 사람은 레벨이 BASIC으로 설정되어야 한다. 그렇다면 이 로직을 어디에 놓는게 좋을까?


  •  UserDaoJDBC의 add() ? - User 오브젝트를 DB에 정보를 넣고 읽는 방법에만 관심을 가져야 한다!

  • User의 생성자? - 나쁘지 않지만, 처음 가입할 때를 제외하면 무의미한 로직을 클래스에서 직접 초기화 하는 것은 문제가 있다.


5.1.5. 코드 개선

 비즈니스 로직을 모두 구현했으니 넘어갈 수도 있지만, 깔끔한 코드를 위해 다시 한번 검토해보자.

 다음과 같은 질문을 항상 하자.

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

(1)  upgradeLevels()





 5.1.5을 마무리하고 나면 각자의 책임이 분리가 된 것을 확인할 수 있다.

 객체지향적인 코드는 다른 오브젝트의 데이터를 작업하는 대신 데이터를 가지고 있는 다른 오브젝트에 작업을 

해달라고 요청하는 것이다.

 즉, UserService는 User에게 " 레벨 업그레이드 해줘" 라고 요청하고, User는 Level에게 "다음 레벨이 무엇인지 알

려달라"고 요청하는 것이다.


2. 트랜잭션 서비스 추상화 




 레벨 조정 작업중에 문제가 생긴다면 어떻게 해야 되지?

 모 아니면 도로 가야하는데, 테스트는 어떻게 발생시키지? - 어플리케이션을 건드리는 것은 매우 위험하고 좋지 않은 일이다. 결국 기존 Test를 확장해야 한다.

5.2.1. 모 아니면 도

( 코드는 소스트리 참고 )

 테스트가 실패한 이유는 뭘까?

 트랜잭션 문제다. upgradeLevel()는 하나의 작업 단위인 트랜잭션이 적용되지 않아 다음과 같은 에러가 발생하는 것이다.

5.2.2. 트랜잭션 경계설정

DB는 그 자체로 완벽한 트랜잭션을 지원한다. 하지만 여러 개의 SQL을 하나의 트랜잭션으로 취급해야 하는 경우도 있다. ( 항상 예로 드는 계좌이체와 같은 경우 )


우리는 upgradeLevel() 도중 실패했을 경우 트랜잭션 롤백(rollback)과 성공한 경우 트랜잭션 커밋(commit)을 해주어야 한다. 결국, 트랜잭션의 트랜잭션 경계설정을 해줘야 한다.

트랜잭션을 사용한 JDBC 코드


 UserService와 UserDAO의 트랜잭션 문제


 우리 코드를 보면 어디에도 트랜잭션 경계설정 코드가 있지 않다. 그 이유는 update()를 할때 jdbcTemplate 안에서 datasource의 getConnection()을 이용하여 사용하기 때문이다. 즉 update() 할때마다 독립적인 트랜잭션으로 실행 된 것이다. 즉 ,DAO 메소드 내에서 DB 커낵션을 매번 만들기 때문이다.

 * 그렇다면 upgradeLevels()와 같이 여러번 DB에 업데이트를 해야하는 경우 어떻게 하나의 트랜잭션으로 만들 수 있을까?

 1) upgradeLevels()의 내용을 DAO로 옮기는 방법?

 - 비즈니스 로직과 데이터 로직을 한데 묶어버리는 문제


 2) upgradeLevels()에 트랜잭션 경계 설정 작업을 하는 방법

 - DB Connection 생성 부분 이동 ( 위의 JDBC 트랜잭션 경계 설정과 같은 구조로 )

 - 문제는 Connection을 공유해야 되기 때문에 DAO로 connection 파라미터 전달

   => DB 커넥션을 비롯한 리소스의 깔끔한 처리를 가능하게 했던 jdbcTemplate을 활용 못하게 됨
      ( 결국 JDBC API를 직접 사용하게 됨 )  

   => 파라미터에 connection이 생겨서, 다른 곳에서 DAO를 필요로 하는 경우 계속 connection 을 파라미터로 전       달 해야함.

    => JDBC 대신 다른 JPA나 하이버네이트로 구현하는 경우 interface를 다 수정해줘야 하고, DI 적용했던 것이 물        거품이 됨.


5.2.3. 트랜잭션 동기화

깔끔한 코드 or 트랜잭션 기능 포기? ㄴㄴ 스프링이 도와줌!

먼저 Connection을 파라미터로 전달하는 문제를 해결해보자. upgradeLevels()에서 경계설정을 해야하는 것은 피할 수 없는 문제다. 그렇다면 파라미터로 Connection을 전달하는 것이 아니라, Connection을 특별한 저장소에 보관해두고 이를 공유하면 된다. 이것을 트랜잭션 동기화라 부른다.


 트랜잭션 동기화 저장소는 작업 스레드마다 독립적으로 Connection 오브젝트를 저장하고 관리하기 때문에 다중 사용자를 처리하는 서버의 멀티스테르 환경에서도 충돌날 염려는 없다.


 동기화 적용 코드는 소스 트리 참고 ( 책 p.364 )

 jdbcTemplate과 트랜잭션 동기화

jdbcTemplate은 Connection을 어떻게 알지? 미리 생성되어 있으면 그걸 사용하고, 아니면 자체적으로 생성하도록 설계되어 있음 ( 자세한 코드는 JdbcTemplate 참고 )


이제 비즈니스 로직과 데이트 액세스 분리를 유지한 채로 트랜잭션을 적용하였다. 하지만 이게 끝일까?

5.2.4. 트랜잭션 서비스 추상화

JDBC API를 사용하고 트랜잭션을 적용하면서, 책임과 성격에 따라 코드를 잘 분리하였다.


기술과 환경에 종속되는 트랜잭션 경계설정 코드

  •  문제 1. 다른 DB를 사용하고 싶은경우

 userDAO는 DI를 통해 연결되어 있기 때문에 UserService는 변경되지 않아도 된다. 하지만 트랜잭션 관리코드가 JDBC 에 종속되어 있어 코드를 변경해야 되고, 이는 특정 데이터 엑세스 기술에 종속되어 있다는 것이다.


  •  문제 2. 한 개 이상의 DB로의 작업을 하나로 트랜잭션으로 만들고 싶은 경우

 로컬 트랜잭션이 아니라 글로벌 트랜잭션 방식을 사용해야 함 ( JTA )


  •  문제 1의 해결책. 

 트랜잭션 경계 설정을 담당하는 코드를 보면 구조는 모두 유사하다. 이렇게 사용 방법에 공통점이 있다면 추상화를 해줘야 한다.  그렇게 하면, 하위 시스템이 어떤 것인지 알지 못해도, 일관된 방법으로 접근할 수 있다.

스프링의 트랜잭션 서비스 추상화 ( 소스트리 참고 )

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




 이제 스프링의 트랜잭션 서비스 계층화를 통해 다양한 트랜잭션 기술을 일관된 방식으로 제어 할 수 있다. 즉, Service의 핵심코드는 숨겨둔채로 외부에서 일관된 방법으로 바꿔서 사용할 수 있는 것이다. 


 단일 책임 원칙

하나의 모듈은 하나의 책임만을 가져야 한다. 즉, 수정해야 할 경우가 한가지여야 한다는 것이다.

리팩토링 전의 UserService를 보면, 레벨 업그레이드 방식이 변경된 경우와 트랜잭션 기술이 변경된 경우 즉 2가지 경우 수정을 필요로 한다.


하지만 리팩토링 후에는 트랜잭션 기술이 변경되어도 UserService 코드는 수정할 필요가 없고, 단지 레벨 업그레이드 방식이 변경된 경우만 코드를 수정해야 한다. 즉, 단일 책임 원칙을 충실하게 지키고 있는 것이다.


 장점

 만약 단일 책임 원칙을 지키기 않은 상태에서 DAO가 수정된다면? 수백개의 Service의 수정을 불러오게 됨 -> 테스트도 수정