사용자 레벨 관리 기능 추가

  • 기능 구현 및 리팩토링을 쉽게 설명하는 내용이다. 왜 이렇게 구현되었는지 왜 이렇게 리팩토링을 하였는지 한 번 더 자세히 설명해준다.
  1. 필드 추가
    • Enum을 잘 활용하자
  2. 사용자 수정 기능 추가
    • 쿼리를 사용하는 테스트 같은 경우, 쿼리가 잘못될 수도 있다는 점을 확인할 수 있도록 테스트를 작성하자.
    • 예를 들어, update 기능을 테스트를 하는 경우, where절을 실수로 빼먹었어도, 데이터를 하나만 테스트할 경우 정상적으로 처리된 것처럼 보인다.
    • update문이 리턴하는 값을 확인 또는 바뀌어서는 안되는 데이터도 같이 테스트를 하여보완한다.
  3. UserService.updateLevels()
    • 비즈니스 로직을 분리하기 위해 Service라는 계층을 만들어서 구현하자.
  4. UserService.add()

  5. 코드 개선
    • 조건을 확인하는 로직은 메소드로 추출하자.
    • 조건에 따라 동작하는 방식은 조건이 추가될 경우 점점 if/else문이 늘어난다. 다른쪽에서 해결해줄 수 있는지 확인해보고 처리한다. (ex : enum)

트랜잭션 서비스 추상화

  • 스프링이 트랜잭션 서비스를 추상화하게 된 이유와 방법을 설명하는 내용이다.
  • 트랜잭션 : 더 이상 나눌 수 없는 단위 작업. 원자성을 의미한다.
  1. 모 아니면 도
    • 코드 중간에 예외가 발생할 경우, 이전에 작업하였던 DB 작업은 어떻게 처리할까?
    • 예외가 처리되기전 까지 수행한 DB 작업을 그대로 유지할지, 아니면 모두 되돌리지는 기능 요구사항에 따라 결정
    • 결정한 요구사항을 처리하기 위해서 트랜잭션의 경계를 설정해야한다.
  2. 트랜잭션 경계 설정
    • 롤백(rollback) : 앞에서 처리한 SQL 작업을 취소
    • 커밋(commit) : SQL 작업을 확정
    • JDBC 트랜잭션 코드 예제

        Connection c = dataSource.getConnection();
      
        c.setAutoCommit(false); // 트랜잭션 시작
      
        try {
        	... // SQL 작업
        	c.commit();
        } catch(Exception e) {
        	c.rollback();
        }
      
        c. close() ;
      
    • Dao를 사용하는 비즈니스 로직(UserService)에서 트랜잭션을 다루기 위해서는 Connection 오브젝트를 비즈니스 로직에서 가지고 있어야함. 이런 식으로 수정하면 트랜잭션 문제는 해결할 수 있지만, 코드가 깔끔하지 않게 됨.
  3. 트랜잭션 동기화
    • 스프링은 위와 같은 문제를 해결할 수 있는 트랜잭션 동기화 방식을 제공
    • 트랜잭션 동기화 : Connection 오브젝트를 직접 다룰 필요 없이, 스프링이 특별한 저장소에 보관하고 이후에 호출되는 SQL문에서는 저장된 Connection 오브젝트를 가져다 사용하도록 하는 방식

    • 트랜잭션 동기화 코드 예제

        TransactionSynchronizationManager.initSynchronization(); // 동기화 초기화
      
        // DB 커넥션 생성 후, 트랜잭션 동기화에 사용하도록 저장소에 바인딩
        Connection c = DataSourceUtils.getConnection(dataSource);
      
        c.setAutoCommit(false);
      
        	try {
        		... // SQL 작업
        		c.commit();
        	} catch(Exception e) {
        		c.rollback();
        	} finally{
        		DataSourceUtils.doReleaseConnection(c, dataSource);
        		TransactionSynchronizationManager.unbindResource(dataSource);
        		TransactionSynchronizationManager.clearSynchronization();
        	}
      
  4. 트랜잭션 서비스 추상화
    • 위와 같이 해결은 하였지만 문제점이 있다.
    • 만약 JDBC 방식이 아닌 다른 방식의 DB (JTA방식, JPA 등)를 사용할 경우 UserService의 코드가 변경되어야 한다. 즉 UserService는 트랜잭션을 처리하기 위해 JDBC와 강한 결합이 되어있음.
    • 이 문제를 해결하기 위해 스프링은 트랜잭션의 특징을 모아서 추상화한 트랜잭션 추상화 기술을 제공한다.

    • 트랜잭션 추상화 코드 예제

        // JDBC 트랜잭션 추상화 오브젝트 생성
        PlatformTransactionManager tm = new DataSourceTransactionManager(dataSource);
        // 트랜잭션 시작
        TransactionStatus status = tm.getTransaction(new DefaultTransactionDefinition());
      
        try {
        	... // SQL 작업
        	tm.commit(status);
        } catch (Exception e) {
        	tm.rollback(status);
        }
      
    • 위 코드에서 다른 API를 사용하고 싶다면 PlatformTransactionManager의 구현체만 변경해주면 된다.

        // JTA 방식
        PlatformTransactionManager tm = new JTATransactionManager();
      
    • 따라서 PlatformTransactionManager을 DI하게 되면 UserService는 DB 방식에 따라 코드가 변경될 필요가 없다.
    • 관례적으로 PlatformTransactionManager의 빈이름은 transactionManager를 사용한다.

        <bean id="transactionManager"
        class="org .springframework.transaction . jta. JtaTransaction Manager" />
      

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

  • 추상화 기법을 이용해 트랜잭션 기술을 일관된 방식으로 사용할 수 있게되었다.
  • 이 챕터는 추상화란 정확히 무엇이며, 추상화를 통해 얻을 수 있는 객체지향 설계의 원칙 중의 하나인 단일 책임 원칙에 대해 짧게 설명해주는 내용이다.

  • 추상화 : 비즈니스 로직 하위에서 동작하는 로우레벨의 특성을 갖는 코드를 분리하는 것

      ![](https://knowjea.github.io/assets/images/2021-03-22/2.png)
    
  • UserService는 트랜잭션 기술이 변경되었다고 하더라도, 트랜잭션 추상화를 이용해 로우레벨의 기술 서비스와 환경에서 독립 됨.

  • 단일 책임 원칙 : 하나의 모듈은 한 가지의 책임만을 가져야 한다.
  • UserService는 비즈니스 로직만 책임져야하고, 트랜잭션 추상화를 통해 트랜잭션은 책임에서 분리한다.

메일 서비스 추상화

  • 스프링에서 제공하는 메일 서비스 추상화와 테스트의 목 오브젝트를 설명하는 챕터이다.
  1. JavaMail을 이용한 메일 발송 기능
    • JavaMail : 자바에서 메일 기능을 제공하는 API
  2. JavaMail이 포함된 코드의 테스트
    • 테스트 코드를 작성할 경우, 테스트를 수행할 때마다 메일이 전송됨
    • 또는 테스트중이라 메일서버가 없을 수도 있음.
    • 따라서 테스트용 메일 서버를 사용하도록 구현하면 됨

  3. 테스트를 위한 서비스 추상화
    • 하지만, JavaMail은 인터페이스를 제공하지 않는 악명 높은 API임
    • 스프링은 이를 해결하기 위해 MailSender 인터페이스를 제공해 메일 서비스를 추상화 함

    • JavaMail의 구현체는 SimpleMailMessage를 사용하면 된다.
    • 테스트를 위하여 MailSender 인터페이스를 구현한 빈 클래스를 만든다.

        public class DummyMailSender implements MailSender {
        		public void send(SimpleMailMessage mailMessage) throws MailException {}
      
        		public void send(SimpleMailMessage[] mailMessage) throws MailException {}
        	}
      
  4. 테스트 대역
    • 테스트 대역 : 테스트 대상이 되는 오브젝트의 기능에만 충실하게 수행하면서 빠르게, 자주 테스트를 싱핼할 수 있도록 사용하는 오브젝트들을 테스트 대역(test double)이라고 함.
    • 대표적인 테스트 대역은 테스트 스텁(test stub)이라고 한다. 스텁은 코드가 정상적으로 수행할 수 있도록 돕는 것. DummyMailSender가 스텁의 예이다.
    • 테스트 대역은 메소드가 호출될 때 값을 리턴해야 하는 경우가 있는데, 이때 그 값 자체를 검증해야 하는 경우가 존재한다. 이 때는 목 오브젝트(mock object)를 사용한다. 목 오브젝트는 스텁처럼 정상적인 실행을 도와주면서, 자신의 결과를 검증하는데 활용할 수 있게 해준다.
    • 목 오브젝트 예제

        public class MockMailSender implements MailSender {
      
        		// send 메시지가 호출되면 수신자(to)를 저장하는 리스트
        		private List<String> requests = new ArrayList<>();
      
        		... // getter
      
        		public void send(SimpleMailMessage mailMessage) throws MailException {
        			requests.add(mailMessage.getTo()[0]);
        		}
      
        		public void send(SimpleMailMessage[] mailMessage) throws MailException {}
        	}
      
    • MockMailSender는 간단한 목 오브젝트 예제이다. send가 호출되면 수신사(to)를 리스트에 저장한다. 테스트를 수행할 때 리스트를 제공하여 검증하는데 사용할 수 있다.