사용자 레벨 관리 기능 추가
- 기능 구현 및 리팩토링을 쉽게 설명하는 내용이다. 왜 이렇게 구현되었는지 왜 이렇게 리팩토링을 하였는지 한 번 더 자세히 설명해준다.
- 필드 추가
- Enum을 잘 활용하자
- 사용자 수정 기능 추가
- 쿼리를 사용하는 테스트 같은 경우, 쿼리가 잘못될 수도 있다는 점을 확인할 수 있도록 테스트를 작성하자.
- 예를 들어, update 기능을 테스트를 하는 경우, where절을 실수로 빼먹었어도, 데이터를 하나만 테스트할 경우 정상적으로 처리된 것처럼 보인다.
- update문이 리턴하는 값을 확인 또는 바뀌어서는 안되는 데이터도 같이 테스트를 하여보완한다.
- UserService.updateLevels()
- 비즈니스 로직을 분리하기 위해 Service라는 계층을 만들어서 구현하자.
-
UserService.add()
- 코드 개선
- 조건을 확인하는 로직은 메소드로 추출하자.
- 조건에 따라 동작하는 방식은 조건이 추가될 경우 점점 if/else문이 늘어난다. 다른쪽에서 해결해줄 수 있는지 확인해보고 처리한다. (ex : enum)
트랜잭션 서비스 추상화
- 스프링이 트랜잭션 서비스를 추상화하게 된 이유와 방법을 설명하는 내용이다.
- 트랜잭션 : 더 이상 나눌 수 없는 단위 작업. 원자성을 의미한다.
- 모 아니면 도
- 코드 중간에 예외가 발생할 경우, 이전에 작업하였던 DB 작업은 어떻게 처리할까?
- 예외가 처리되기전 까지 수행한 DB 작업을 그대로 유지할지, 아니면 모두 되돌리지는 기능 요구사항에 따라 결정
- 결정한 요구사항을 처리하기 위해서 트랜잭션의 경계를 설정해야한다.
- 트랜잭션 경계 설정
- 롤백(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 오브젝트를 비즈니스 로직에서 가지고 있어야함. 이런 식으로 수정하면 트랜잭션 문제는 해결할 수 있지만, 코드가 깔끔하지 않게 됨.
- 트랜잭션 동기화
- 스프링은 위와 같은 문제를 해결할 수 있는 트랜잭션 동기화 방식을 제공
-
트랜잭션 동기화 : 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(); }
- 트랜잭션 서비스 추상화
- 위와 같이 해결은 하였지만 문제점이 있다.
- 만약 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는 비즈니스 로직만 책임져야하고, 트랜잭션 추상화를 통해 트랜잭션은 책임에서 분리한다.
메일 서비스 추상화
- 스프링에서 제공하는 메일 서비스 추상화와 테스트의 목 오브젝트를 설명하는 챕터이다.
- JavaMail을 이용한 메일 발송 기능
- JavaMail : 자바에서 메일 기능을 제공하는 API
- JavaMail이 포함된 코드의 테스트
- 테스트 코드를 작성할 경우, 테스트를 수행할 때마다 메일이 전송됨
- 또는 테스트중이라 메일서버가 없을 수도 있음.
-
따라서 테스트용 메일 서버를 사용하도록 구현하면 됨
- 테스트를 위한 서비스 추상화
- 하지만, 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 {} }
- 테스트 대역
- 테스트 대역 : 테스트 대상이 되는 오브젝트의 기능에만 충실하게 수행하면서 빠르게, 자주 테스트를 싱핼할 수 있도록 사용하는 오브젝트들을 테스트 대역(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)를 리스트에 저장한다. 테스트를 수행할 때 리스트를 제공하여 검증하는데 사용할 수 있다.