Spring/객체지향

코드에 SRP 원칙 적용 후 Mock 테스트 작성하기

mint* 2024. 4. 6. 17:56
728x90

SRP(Single Responsibility Principle) : 단일 책임 원칙

어떤 클래스를 변경해야 하는 이유는 오직 하나뿐이어야 한다. - 로버트 C.마틴

 

하나의 클래스에 역할과 책임이 너무 많은 경우 클래스를 각각의 역할과 책임에 따라 분리하자는 원칙이다.

 

SRP 원칙을 통해 클래스 하나가 과도한 책임을 가지는 것을 방지하고, 각 클래스의 책임이 명확해진다.

 

아래 글에 SOLID에 대해 정리했으니 시간나면 읽어보는 것도 좋다.

https://shout-to-my-mae.tistory.com/417

 

객체 지향 설계 5원칙: SOLID

객체 지향 언어를 이용해 객체 지향 프로그램을 올바르게 설계하는 방법과 원칙이 존재한다. SOLID 객체 지향 설계(OOD)의 정수라고 할 수 있는 5원칙이다. SRP(Single Responsibility Principle) : 단일 책임

shout-to-my-mae.tistory.com

 

SRP 원칙에 따라 리팩토링하기

기존 코드

import java.util.Random;

public class Car {

    private final String name;
    private long position;

    private final Random random = new Random();

    public Car(String name) {
        this.name = name;
        this.position = 0;
    }

    public long moveMain(){
        int randomValue = makeRandomValue();
        return move(randomValue);
    }

    public int makeRandomValue(){
        return random.nextInt(9);
    }

    public long move(int randomValue) {
        if (randomValue >= 4) {
            return ++position;
        }

        return position;
    }

    public void reset() {
        this.position = 0;
    }
}

 

자동차는 상태와 기본적인 행위를 가질 뿐만 아니라, 랜덤값을 생성하고 랜덤값에 따라 이동여부를 결정하는 역할까지 한다.

 

문제점

  • 랜덤값 생성 로직이 변경되거나 자동차가 움직이는 방식이 달라질 경우 Car 클래스 코드를 변경해야한다.
  • 랜덤 값을 생성하는 코드와 Car 객체를 움직이는 코드가 하나의 클래스에 있어 코드가 길어질 경우 가독성이 떨어진다.

 

SRP 원칙을 적용하여 책임 단위로 분리하기

  • Car : 자동차 클래스는 자동차의 속성과 기본적인 동작만 담당한다.
  • RandomValueGenerator : 랜덤 값을 생성한다.
  • CarMover : 생성된 랜덤 값에 따라 자동차를 움직인다.

 

Car

public class Car {

    private final String name;
    private long position;

    public long getPosition() {
        return position;
    }

    public Car(String name) {
        this.name = name;
        this.position = 0;
    }

    public long move() {
        return ++position;
    }

    public void reset() {
        this.position = 0;
    }
}
 name은 당장 사용하지 않으므로 필요없어서 name에 대한 getter는 만들지 않았다.
(position 속성은 테스트에서 검증시 사용한다.)

 

RandomValueGenerator

public class RandomValueGenerator {
    private final Random random = new Random();

    public int generateRandomValue() {
        return random.nextInt(9);
    }
}

 

CarMover

public class CarMover {
    private final RandomValueGenerator randomValueGenerator;

    public CarMover(RandomValueGenerator randomValueGenerator) {
        this.randomValueGenerator = randomValueGenerator;
    }

    public void move(Car car) {
        int randomValue = randomValueGenerator.generateRandomValue();
        if (randomValue >= 4) {
            car.move();
        }
    }
}

CarMover에서 RandomValueGenerator를 생성자를 통해 의존성을 주입받도록 하였다.

 

  • RandomValueGenerator 속성을 필드에서 바로 생성하여 사용해도 되지만, CarMover와 RandomValueGenerator간의 결합도가 높아지므로 의존성을 주입받았다.
    또한 나중에 랜덤 값 생성 방식이 여러개로 확장될 경우 적절한 랜덤값 생성 방식을 선택하여 사용할 수 있도록 확장성을 고려했다.
  • private로 필드를 둔 이유는 private 접근 제어자를 통해 클래스 내부에서만 사용하여 캡슐화하기 위해서이다.
  • 또한 필드에 final 키워드를 사용하면 오직 한번 할당되므로 값에 대한 예측이 쉽고 스레드 안정성이 높다.

 

SRP 원칙 적용으로 인한 파일의 개수 증가

책임에 따라 클래스를 분리하다보면 파일의 개수가 증가하여 관리가 어려울 수 있다.

그래서 적절한 기준을 가지고 클래스를 분리하는 것이 중요하다.

 

하지만 trade-off로 각 클래스의 책임이 명확해져 코드 유지보수가 쉽다.

그리고 책임 별로 클래스가 쪼개지므로 각각의 테스트 코드를 좀 더 쉽게 작성할 수 있어 좋다.

 

테스트 코드 리팩토링

기존 코드에서는 move 메서드에 랜덤값을 파라미터로 넣어줘서 자동차가 움직이는지 테스트할 수 있었다.

하지만 현재 코드에서는 랜덤 값 생성 객체(RandomValueGenerator)를 CarMover에 주입받아 움직이므로, 랜덤 값을 외부에서 컨트롤 할 수 없다.

 

해결 방법 : Stubbing

1. 랜덤 값 생성 객체(RandomValueGenerator)의 Mock 객체를 생성(@Mock)하고 CarMover에 주입(@InjectMocks)한다.

2. BDDMockito의 given()을 이용하여 CarMover에서 RandomValueGenerator의 랜덤 값 생성 메서드 호출 시 Mocking을 통해 가로채어 원하는 값로 반환하도록 변경한다.

 

코드

@DisplayName("움직이는 자동차 테스트")
class CarMoverTest {

    private final Car car = new Car("Test car");

    @Mock
    private RandomValueGenerator randomValueGenerator;

    @InjectMocks
    private CarMover carMover;

    private final Random random = new Random();

    @BeforeEach
    void setUp() {
        MockitoAnnotations.initMocks(this);
    }

    @AfterEach
    void tearDown() {
        car.reset();
    }

    @Test
    @DisplayName("random 값이 4 이상 9 이하인 경우 1만큼 전진한다")
    void moveCarWhenRandomValueIsGreaterThanOrEqual4() {
        // given
        int randomValue = 4 + random.nextInt(5);
        given(randomValueGenerator.generateRandomValue()).willReturn(randomValue);

        // when
        carMover.move(car);

        // then
        assertThat(car.getPosition()).isEqualTo(1L);
    }

    @Test
    @DisplayName("random 값이 3 이하인 경우 멈춘다")
    void stopCarWhenRandomValueIsLessThan4() {
        // given
        int randomValue = random.nextInt(3);
        given(randomValueGenerator.generateRandomValue()).willReturn(randomValue);

        // when
        carMover.move(car);

        // then
        assertThat(car.getPosition()).isEqualTo(0);
    }

}

 

@Mock : Mock 객체(가짜 객체) 생성
@InjectMocks : Mock 객체를 해당 필드에 주입, 해당 필드의 의존성을 분석하여 해당 의존성에 대한 Mock 객체를 자동으로 주입한다.

 

✅ @Mock, @InjectMocks 사용시 해당 어노테이션 초기화하는 코드 @BeforeEach에 작성하기! 

MockitoAnnotations.initMocks(this)

 

728x90