이전에 TDD를 공부하면서 작성했던 코드를 다시 보면 현재 시간을 기록해야 하는 기능을 테스트해야 하는 부분이 있었다.
하지만 해당 메소드가 실행되어서 기록되던 시간과 테스트할 때의 시간은 명백히 다르기 때문에 테스트가 거의 불가능했었다.
코드를 다시 한번 보자.
fun insert(
id: Long,
amount: Long,
transactionType: TransactionType,
updateMillis: Long,
): PointHistory {
Thread.sleep(Math.random().toLong() * 300L)
val history = PointHistory(
id = cursor++,
userId = id,
amount = amount,
type = transactionType,
timeMillis = updateMillis,
)
table.add(history)
return history
}
이게 맨처음의 문제되었던 코드이다.
updateMills가 분명히 여기 추가되어있다. 이 함수에서 현재 시간을 바로 추가하지 말고, 외부에서 주입받은 시간을 사용해야 했다.
이 함수를 사용하는 서비스 클래스를 보자.
fun usePoint(userID: Long, amount: Long, updateMills: Long): PointHistory {
return pointHistoryTable.insert(userID, amount, TransactionType.USE, updateMills)
}
이 함수에서도 파라미터로 updateMills를 받는다!! 왜 이렇게 만들었을까? 그냥 여기서 바로 System.currentTimeMillis() 를 써버리면 되는 거 아닌가? 싶은데... 이 usePoint를 테스트하는 테스트 코드를 보자.
val mockUpdateMills : Long = System.currentTimeMillis()
@Test
fun PointHistory_사용내역_저장() {
val amount = 3000L
val willReturningHistory = makePointHistory(mockUserId, amount, TransactionType.USE, mockUpdateMills)
val historyId = 1L
given(pointHistoryTable.insert(mockUserId, amount, TransactionType.USE, mockUpdateMills))
.willReturn(willReturningHistory)
val history : PointHistory = pointHistoryFactory.usePoint(mockUserId, amount, mockUpdateMills)
assertThat(history.userId).isEqualTo(mockUserId)
assertThat(history.amount).isEqualTo(amount)
assertThat(history.type).isEqualTo(TransactionType.USE)
assertThat(history.id).isEqualTo(historyId)
assertThat(history.timeMillis).isEqualTo(mockUpdateMills)
then(pointHistoryTable).should(times(1)).insert(mockUserId, amount, TransactionType.USE, mockUpdateMills)
}
나도 맨 처음에는 usePoint 함수에서 System.currentTimeMillis()를 사용했다. 근데 이렇게 사용해버리니까 usePoint에서 System.currentTimeMillis()를 호출해서 얻은 시간을 알수가 없는 것이다.
그러면 어떻게 그 시간이 정확한지 테스트할수 있겠는가??
나는 그 당시에는 이 부분을 어떻게 해야할지 몰라 이렇게 따로 시간을 인자로 받을 수 밖에 없었다... 이건 좋은 코드가 아니다!!
이 usePoint를 사용하는 컨트롤러까지 올라가서 System.currentTimeMillis() 를 사용해야 한다 !!
간단한 함수라서 망정이지 만약 8단계를 거치는 함수였다면 어떻게 되었을까... 그럼 그 함수들에 전부 필요하지도 않은 updateMills를 추가해야하는 상황이 펼쳐진다.
이것을 의존성 역전으로 한번 잘 고쳐보자 !!
의존성 역전으로 테스트를 쉽게 하기
먼저 시간을 제공해주는 인터페이스를 하나 만들어주자.
interface TimeProvider {
fun getCurrentTime(): Long
}
간단하다. getCurrentTime을 호출하면 Long 형태로 현재 시간을 갖다주기만 하면 된다.
이제 usePoint를 사용하는 클래스에서 TimeProvider를 의존하도록 해주자.
@Component
class PointHistoryFactory(
val pointHistoryTable: PointHistoryTable,
val timeProvider: TimeProvider
) {
fun usePoint(userID: Long, amount: Long): PointHistory {
System.currentTimeMillis()
return pointHistoryTable.insert(
userID,
amount,
TransactionType.USE,
timeProvider.getCurrentTime()) // 이부분 수정
}
}
이렇게 되면 usePoint에서는 더이상 updateMills를 파라미터로 가질 필요도 없고, 이 함수를 사용하는 다른 곳에서도 시간을 넘겨줄 필요가 없다.
참고로 코프링에서는 @RequiredArgsContructor 같은거 안써도 알아서 주입해줌~~ 개편함~~
그리고 TimeProvider를 구현하는 구현체를 만들어주자.
@Component
class RealTimeProvider : TimeProvider {
override fun getCurrentTime(): Long {
return System.currentTimeMillis()
}
}
그냥 이렇게 만들어주면 끝!!! 그러면 실제 서비스 시에는 이 구현체가 주입되어서 알아서 현재 시간을 넘겨줄 것이다.
그럼 테스트 시에는 어떻게 하는지 보자. 이때는 테스트 시에만 사용할 구현체를 주입해주면 된다 !!
@ExtendWith(MockitoExtension::class)
class PointHistoryFactoryUnitTest {
private lateinit var pointHistoryFactory: PointHistoryFactory
@Mock
private lateinit var pointHistoryTable: PointHistoryTable
val mockUpdateMills: Long = System.currentTimeMillis()
class TestTimeProvider(
val testTime: Long
) : TimeProvider {
override fun getCurrentTime(): Long {
return testTime
}
}
@BeforeEach
fun setUp() {
pointHistoryFactory = PointHistoryFactory(
pointHistoryTable,
TestTimeProvider(mockUpdateMills)
)
}
...
테스트 클래스 내에서 TimeProvider 인터페이스를 구현하는 TestTimeProvider를 만들었고, 생성자로 테스트 시간을 받아 이를 그대로 리턴하도록 구현했다. 이제 원하는 시간을 그대로 반환하는지 테스트가 가능하다 !!
이후에 다시 이 usePoint 테스트를 돌려보면...

이렇게 모두 통과한다.
요새 토비의 스프링, 그리고 테스트에 관련된 강의를 보고 있는데 볼때마다 객체지향에 대한 새로운 관점에 눈이 뜨이는 것 같다.
'코딩' 카테고리의 다른 글
| Redis 자료구조와 사용방법 (0) | 2025.11.19 |
|---|---|
| 객체지향은 신이고 테스트는 무적이다 (2) | 2025.11.17 |
| PG 결제 (0) | 2025.10.21 |
| PNG 변환기 만들기 (0) | 2025.10.21 |
| 이분 탐색 (0) | 2025.10.10 |