발단
* 코드는 샘플입니다. 실제 개선사항과는 조금의 차이가 있습니다.
이런 프로세스를 생성하는데 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 에서 말하는 정적 클래스 남용의 예와 일치합니다.
"클래스가 하나 이상의 자원에 의존, 그 자원이 클래스 동작에 영향을 준다면 싱글턴 및 정적 유틸리티 클래스에 어울리지 않는다"
이처럼 더 개선할 여지가 많지만 업무량 등 팀 리소스상 트레이드 오프로 우선 여기까지 진행한 상태입니다.
추가적으로 변경될때 글을 업데이트 하도록 하겠습니다.
+ 좋은 의견 주시면 감사하겠습니다.
'Spring' 카테고리의 다른 글
SpringBoot + Redis 를 이용한 글로벌 캐시 (0) | 2022.03.14 |
---|---|
TestContainer 로 테스트환경 셋업하기 (0) | 2022.03.14 |
Enum 을 BeanProvider로 사용해보기 (0) | 2022.03.14 |
Spring Bean LifeCycle 스프링 빈 생명주기 (0) | 2022.01.24 |