본문 바로가기

Spring

도메인 객체에서 Bean 객체의 기능을 필요로 해 수행한 코드 개선기

발단

* 코드는 샘플입니다. 실제 개선사항과는 조금의 차이가 있습니다.

 

이런 프로세스를 생성하는데 dday 가 특정일 (공휴일이나 주말 같은) 날 일 경우 그런 특별한 날이 아닌 날까지 밀거나 당기는 기능이 필요했습니다. '특별한 날' 은 어떤 API 를 통해 매년 DataBase 에 적재시켜놓고 있었고, 밀고 당기는 기능도 존재하는 상황이었습니다.

 

 

 

기존의 코드

사실상 유틸성 클래스지만, 빈으로 제공되고 메서드들이 static 으로 제공되지 않고 있어 도메인 내부에서 참조할 수 없었습니다.

또 isSpecialDay 는 불필요하게 매번 모든 데이터를 가져오고 있습니다.

 

이 클래스를 사용하여 Service Layer 에서 Process 를 생성하는 매 메서드 마다 데이터를 조회하고, pull 을 호출하는 코드를 매번 작성하고 있었습니다.

 

개선 

개선을 위해 테스트 코드를 작성해봅니다.

@ExtendWith(MockitoExtension::class)
internal class SpecialDateServiceTest(
    @Mock
    val specialDateRepository: SpecialDateRepository
) {

    lateinit var specialDateService: SpecialDateService

    @BeforeEach
    fun setUp() {
        val specialDays = listOf(SpecialDate("1", "2022현충일", LocalDate.of(2022, 6, 6)),
            SpecialDate("2", "2022창립기념일", LocalDate.of(2022, 6,7)))

        Mockito.`when`(specialDateRepository.findAll()).thenReturn(specialDays)

        specialDateService = SpecialDateService(specialDateRepository)
    }

    @Test
    @DisplayName("입력한 날짜가 특별한 날인지 아닌지 여부를 반환")
    fun isSpecialDay() {
        assertAll(
            { assertFalse(specialDateService.isSpecialDay(LocalDate.of(2022, 6, 5))) },
            { assertTrue(specialDateService.isSpecialDay(LocalDate.of(2022, 6, 6))) }
        )
    }
    

    @Test
    @DisplayName("입력한 날짜가 특별한 날 일 경우 특별한 날이 아닌 날까지 민다")
    fun push() {
        val date = LocalDate.of(2022, 6, 6)
        assertThat(specialDateService.push(date).isEqual(LocalDate.of(2022, 6, 8))).isTrue
    }

    @Test
    @DisplayName("입력한 날짜가 특별한 날 일 경우 특별한 날이 아닌 날까지 당긴다")
    fun pull() {
        val date = LocalDate.of(2022, 6, 7)
        assertThat(specialDateService.pull(date).isEqual(LocalDate.of(2022, 6, 5))).isTrue
    }
}

결과

 

이제 마음껏 코드를 바꿀 준비가 되었습니다.

 

다시 코드를 살펴보면 isSpecialDay() 와 push() / pull() 메서드를 서로 다른 책임으로 분리할 수 있어보입니다.

우리가 도메인에서 활용해야 하는 기능은 push() / pull() 이고 isSpecialDate() 는 결국 데이터 조회가 필요한 작업을 포함합니다.

클래스를 분리하면서, 모든 메서드를 유틸성 기능에 맞게 static 으로 변경하도록 하겠습니다.

 

 

 

SpecialDateChecker() 라는 클래스에 특별한 날을 판단하는 책임을 분리하여 부여합니다.

SpecialDateRepository 를 생성자에서 주입받아 specialDay 들을 메모리에 올려둡니다.

그리하여 Repository 와 직접적인 의존성은 끊어내고, isSpecialDay() 를 static 메서드로 만들 수 있었습니다.

 

이제 정상적으로 동작하는지 테스트 해봅니다.

기존의 isSpecialDayTest 를 새 기능으로 고쳐봅시다. 

 

    @Test
    @DisplayName("입력한 날짜가 특별한 날인지 아닌지 여부를 반환")
    fun isSpecialDay() {
        // 실제론 컨테이거나 뜨는 과정에서 초기화 될 것이지만, 테스트에서는 강제로 수행
        val specialDateChecker = SpecialDateChecker(specialDateRepository)

        assertAll(
            { assertFalse(SpecialDateChecker.isSpecialDay(LocalDate.of(2022, 6, 5))) },
            { assertTrue(SpecialDateChecker.isSpecialDay(LocalDate.of(2022, 6, 6))) }
        )
    }

잘 통과합니다. 유틸성 클래스이나 생성자를 제공하여 인스턴스화가 가능한 점은 조금 아쉽습니다.

접근제어자를 이용해서 접근을 최소화 해야겠습니다.

일단 다음은 밀고 당기기를 새로운 곳으로 옮겨 봅시다.

 

 

SpecialDay 의 여부를 static 메소드로 제공받을 수 있게 되어 손쉽게 변경되었습니다.

역시 기존의 테스트 코드를 수정해 테스트 해봅시다.

 

    @BeforeEach
    fun setUp() {
        val specialDays = listOf(SpecialDate("1", "2022현충일", LocalDate.of(2022, 6, 6)),
            SpecialDate("2", "2022창립기념일", LocalDate.of(2022, 6,7)))

        Mockito.`when`(specialDateRepository.findAll()).thenReturn(specialDays)

        // 실제론 컨테이거나 뜨는 과정에서 초기화 될 것이지만, 테스트에서는 강제로 수행
        val specialDateChecker = SpecialDateChecker(specialDateRepository)
//        specialDateService = SpecialDateService(specialDateRepository)
    }

    @Test
    @DisplayName("입력한 날짜가 특별한 날 일 경우 특별한 날이 아닌 날까지 민다")
    fun push2() {
        val date = LocalDate.of(2022, 6, 6)
        assertThat(NormalDayGenerator.push(date).isEqual(LocalDate.of(2022, 6, 8))).isTrue
    }

    @Test
    @DisplayName("입력한 날짜가 특별한 날 일 경우 특별한 날이 아닌 날까지 당긴다")
    fun pull2() {
        val date = LocalDate.of(2022, 6, 7)
        assertThat(NormalDayGenerator.pull(date).isEqual(LocalDate.of(2022, 6, 5))).isTrue
    }

테스트가 다 변경되었으니 setUp 도 아예 바꿔줍니다. 

(공통 setUp 을 사용하지 않는 편이 테스트 코드 관리에 용이하지만, 샘플 코드이므로 가독성을 위해 이렇게 작성하도록 합니다.)

 

 

역시 모두 통과합니다.

이제 밀고 당기기 기능을 도메인에서 참조할 수 있습니다.

사실 완성된 코드도 그리 좋은 패턴은 아닙니다. 기존보다는 훨씬 의존성을 줄이고 리소스 낭비도 줄였지만

결국은 도메인이 Bean 을 (물론 2단계에 걸쳐서지만) 간접참조 하고 있어 DIP 를 위배하고,

SpecialDateChecker 는 아래에서 설명한 Effective Java 에서 말하는 정적 클래스 남용의 예와 일치합니다.

 

"클래스가 하나 이상의 자원에 의존, 그 자원이 클래스 동작에 영향을 준다면 싱글턴 및 정적 유틸리티 클래스에 어울리지 않는다"

 

이처럼 더 개선할 여지가 많지만 업무량 등 팀 리소스상 트레이드 오프로 우선 여기까지 진행한 상태입니다.

추가적으로 변경될때 글을 업데이트 하도록 하겠습니다.

 

+ 좋은 의견 주시면 감사하겠습니다.