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