Search

의존성은 어떻게 드러내는가

Tags

개요

방학 때 함께 프로젝트를 진행했던 팀원들과 더 좋은 코드를 짜기 위해 종종 의논하는데요, “의존성을 드러내라” 라는 주제로 리팩토링을 진행하게 되어 이에 대해 조금 더 알아보도록 하겠습니다.

1. 의존성이 숨겨진 코드

class User { private Long lastLoginTimestamp; public void login() { this.lastLoginTimestamp = Clock.systemUTC().millis(); } }
Java
복사
위 코드를 보고 ‘의존성이 숨겨져 있다’ 라고 합니다.
 어떤 면에서 ‘의존성이 숨겨져 있다’고 하는 걸까요?
내부 로직을 보면 login()은 Clock에 의존적입니다.
하지만 외부 로직을 보면 login()이 Clock에 의존하고 있는지 알 수 없습니다.
의존성이 숨겨져 있을 때 가장 큰 문제점은 원인 모를 버그가 발생하는 것입니다!
물론 코드를 짠 당사자는 ‘login() 은 시간과 연관이 있어!’ 라고 단언할 수 있겠지만, 다른 개발자가 login()을 사용하여 구현할 때는 이 사실을 인지하지 못할 가능성이 많습니다.
 그렇다면 Service 단에서도 Clock에 의존한다는 것을 알게하면 어떨까?
public class UserService { public void login(User user) { user.login(Clock.systemUMC()); ... } }
Java
복사
문제는 Service 단에서는 해결됐지만 Controller 단에서 의존성이 숨겨진다는 것입니다. 그렇다고 계속해서 매개변수로 의존성을 나타내는 것이 맞을까요?
한 개발자는 이를 “폭탄 돌리기”에 비유하였습니다.

2. 어떻게 해야할까?

사실 위와 같은 문제가 발생하는 이유는 시간과 랜덤 같은 값들이 변하는 값 이기 때문입니다.
변하는 값에 대한 대처 방법은 런타임 의존성과 컴파일 의존성을 다르게 하는 것입니다.
 그게.. 뭔데?
컴파일 의존성 (Compile-time Dependency)은 코드가 컴파일될 때 필요한 의존성입니다.
예를 들어, 특정 클래스나 메서드를 사용하는 코드가 있을 경우, 해당 코드를 컴파일하기 위해서는 그 클래스나 메서드의 정의가 포함된 라이브러리나 모듈이 필요합니다.
런타임 의존성 (Runtime Dependency) 코드가 실행될 때 필요한 의존성입니다.
컴파일할 때는 필요하지 않지만, 실행 시점에서 해당 리소스나 기능이 필요할 때 참조되는 의존성입니다.
수정된 코드
class User { private long lastLoginTimestamp; public void login(ClockHolder clockHolder) { this.lastLoginTimestamp = clockHolder.getMillis(); } } class UserService { private final ClockHolder clockHolder; public void login(User user) { user.login(clockHolder); } }
Java
복사

3. 내 코드에 적용해보자

@Transactional public MissedInputResponseDto getMissedInputs(CustomUserDetails userDetails) { // 응답 변수 추가 MissedInputResponseDto missedInputResponseDto = new MissedInputResponseDto(); // 현재의 날짜 GET LocalDate current = LocalDate.now(); // 1주 전의 날짜 GET LocalDate oneWeekAgo = current.minusWeeks(1); List<LocalDate> dates = new ArrayList<>(); LocalDate dateIterator = oneWeekAgo.plusDays(1); while (!dateIterator.isAfter(current)) { dates.add(dateIterator); dateIterator = dateIterator.plusDays(1); } List<Weather> weathers = weatherRepository.findWeatherByDateBetweenAndUser(oneWeekAgo, current, userDetails.getUser()); for (Weather weather : weathers) { LocalDate localDate = weather.getDate(); dates.remove(localDate); } missedInputResponseDto.setLocalDates(dates); return missedInputResponseDto; }
Java
복사
이를 다음과 같이 변경하였습니다.
@Transactional public MissedInputResponseDto getMissedInputs(CustomUserDetails userDetails) { List<LocalDate> lastWeekDates = generateLastWeekDates(); List<Weather> lastWeekWeathers = weatherRepository.findWeatherByDateBetweenAndUser( dateProvider.getOneWeekAgo(), dateProvider.getToday(), userDetails.getUser()); List<LocalDate> missedDates = generateLastWeekMissedDates(lastWeekDates, lastWeekWeathers); return new MissedInputResponseDto(missedDates); } List<LocalDate> generateLastWeekDates() { LocalDate startDate = dateProvider.getOneWeekAgo(); LocalDate endDate = dateProvider.getToday(); List<LocalDate> dates = new ArrayList<>(); for (; !startDate.isAfter(endDate); startDate = startDate.plusDays(1)) { dates.add(startDate); } return dates; } List<LocalDate> generateLastWeekMissedDates(List<LocalDate> lastWeekDates, List<Weather> lastWeekWeather) { for (Weather weather : lastWeekWeather) { LocalDate localDate = weather.getDate(); lastWeekDates.remove(localDate); } return lastWeekDates; }
Java
복사