Programming-[Backend]/Java

[TIL][TDD] TDD 3편. 인터페이스 분리로 테스트는 분리, 서비스는 트랜잭션으로 묶기

컴퓨터 탐험가 찰리 2021. 11. 16. 21:37
728x90
반응형

 

TDD 2편에서는 서비스 메서드를 기능(의미)적으로 분리해가며 작성하는 것이 TDD 방식의 기본이라고 기록했다. 이번 글에서는 이것을 각 인터페이스로 분리해서 설계하는 방법에 대해 정리한다. 정확한 정보는 아닐 수 있으니 이 글을 발견하시는 분이 있다면 참조만 하시길.

 

TDD 2편

[TIL][TDD] 테스트 코드 기반으로 짜되, service 코드에서 의미적으로(기능적으로) 분리해가며 작성하기

 


 

1. 문제점

 

1-1. 메서드를 분리하면 트랜잭션 하나로 묶이지 않는다. 다른 사용자가 실행 순서를 기억해야한다.

2편에서 다룬 것처럼 서비스 코드를 메서드별로 분리하면서 테스트하면 좋다. 그런데, 분리만 하다보면 모든 서비스 코드가 개별 트랜잭션으로 분리되고, Controller에서 각 service 메서드들을 불필요하게 참조하게 된다. 예를 들어 다음과 같이 TV를 통해 재미를 얻는(?) 서비스가 있다고 생각해보자.

(class 위에 @Transactional 어노테이션 하나만 써도 되지만, 강조를 위해 메서드마다 표현해주었다.)

 

TvServiceImpl.class

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
@RequiredArgsConstructor
public class TvServiceImpl implements TvService {
 
  //1. TV 켜기
  @Override
  @Transactional
  public void turnOn() {
   결과 A
  }
  
  //2. TV 채널 바꾸기
  @Override
  @Transactional
  public void changeChannel(결과 A) {
    결과 B
  }
 
  //3. TV 끄기
  @Override
  @Transactional
  public void turnOff(결과 A, 결과 B) {
 
  }
  
}
cs

 

개별 트랜잭션이라 위험

기능적으로 잘 분리됬지만 문제가 발생할 위험을 내포하고 있다. 각 메서드가 각자의 트랜잭션을 실행하기 때문에, 메서드가 실행될 때마다 결과값으로 나오는 데이터와 그 다음 메서드로 전달되는 파라미터 및 실행코드에서 데이터가 틀어지거나 스프링 등에서 에러를 발생할 확률이 커진다. (배운 바에 의하면, 예를 들어 JPA의 영속성 정보는 트랜잭션 단위로 이루어진다.) 첫번째 turnOn 메서드에서 DB에 어떤 조작을 가하고, 두번째 changeChannel 메서드에서 같은 DB에 어떤 조작을 가했을 때, 1개의 트랜잭션으로 처리하는 것과 2개의 개별 트랜잭션으로 처리하는 것은 다른 결과를 얻게될 수 있다.

 

다른 곳에서 메서드 사용불가

또한 인터페이스(TvService)의 목적 중에는 해당 메서드를 참조하여 다른 곳에서 사용하는 기능도 있는데, 이렇게 순서가 정해진 방식으로 코딩을 하면 다른 사람이 사용할 때 어떤 순서로 작동해야하는지 잘 모를 수 있다. 예제처럼 상식적인 내용이 담긴 코드라서 1-2-3 메서드를 순서대로 사용할 수 있는 것이 아니라 복잡한 로직이 1-2-3 순서로 실행되야하는데, 누군가는 1-3-2의 순서대로 해당 인터페이스의 메서드를 참조하고 같은 결과를 기대할 수도 있다.

 

캡슐화 불가

끝으로 캡슐화도 불가하다. 아래 TvService 인터페이스에서처럼 사용자는 TV에서 자동으로 켜기-채널바꾸기-끄기가 작동해서 편하게 사용하고 싶은데, 굳이 이 메서드 3가지를 모두 알아야하는 불필요한 상황이 발생한다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
public interface TvService {
 
  /**
   * TV 켜기
   */
  void turnOn();
 
  /**
   * 채널 바꾸기
   */
  void changeChannel();
 
  /**
   * TV 끄기
   */
  void turnOff();
}
cs

 

1-2. 메서드를 합치자니, 2편에서 본것과 같이 메서드가 방대해져서 TDD가 어렵다.

 

그럼 메서드를 1개로 합치면 해결이 될까? 1개의 트랜잭션으로 묶이고 캡슐화도 되겠지만 기능별로 서비스를 분리할 수도 없고 메서드가 방대해져서 SRP의 원칙을 위반하며 테스트하기가 어려워진다.

 

각 turnOn, changeChannel, turnOff 등의 메서드를 public으로 바꾸면 테스트코드는 작성이 가능하지만, 캡슐화의 원리를 위배하게 된다. 굳이 사용자에게 공개되지 않아도 될 기능이 공개되어 하나의 온전한 완성된 기능을 표현할 수 없는 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
@Service
@RequiredArgsConstructor
public class TvServiceImpl implements TvService {
 
  @Transactional
  public void getFun() {
 
    //1. TV 켜기
    turnOn();
 
    //2. TV 채널 바꾸기
    changeChannel();
 
    //3. TV 끄기
    turnOff();
 
  }
 
 
  //1. TV 켜기
  private void turnOn() {
 
  }
 
  //2. TV 채널 바꾸기
  private void changeChannel() {
 
  }
  //3. TV 끄기
  private void turnOff() {
 
  }
 
}
cs

 


2. 해결책

 

인터페이스를 하나 더 분리하자! TvService 인터페이스의 메서드가 다른 인터페이스의 메서드를 참조하도록 하면 된다. 이것이 그야말로 객체지향설계가 아닌가 하는 생각도 든다.

 

Tv를 on, off 하는 메서드가 실은 다른 가전제품을 on, off하는 메서드로 쓰일 수 있다고 가정하면, TvService 코드는 다음과 같이 변경될 것이다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
@Service
@RequiredArgsConstructor
public class TvServiceImpl implements TvService {
 
  private final TurnOnOffService turnOnOffService;
 
  @Transactional
  public void getFun() {
 
    //TV 켜기
    turnOnOffService.turnOn();
 
    //TV 채널 바꾸기
    changeChannel();
 
    //TV 끄기
    turnOnOffService.turnOff();
 
  }
 
  //TV 채널 바꾸기
  private void changeChannel() {
 
  }
 
}
cs

 

 

1개의 트랜잭션으로 묶이고 TvService가 TurnOnOffService를 주입받게 되었다.

 

만약 TvService를 테스트하고 싶은 경우, TurnOnOffService는 @MockBean으로 받고, 각 turnOn, turnOff 메서드의 결과값을 Mockitto의 given으로 처리하면 TvService의 changeChannel 메서드만 온전히 테스트할 수 있게 된다.

 

마지막으로 TunOnOffService와 같이, 인터페이스의 이름도 다른 사용자가 해당 인터페이스의 기능을 쉽게 유추할 수 있도록 직관적으로 짜주는 것이 좋다.


참조

 

1. 회사 선배 hsr 님, 파트장 lde님

728x90
반응형