Code/Test

[단위 테스트] 7-1. 가치 있는 단위 테스트를 위한 리팩터링: 코드 유형

noahkim_ 2025. 2. 27. 09:26

블라디미르 코리코프 님의 "단위 테스트" 책을 정리한 포스팅입니다.


1. 리팩터링할 코드 식별하기

코드 유형

항목 설명
복잡도 코드 내 의사 결정 지점 수로 정의
도메인 유의성 코드가 프로젝트의 도메인에 대해 얼마나 의미 있는가
협력자 수 코드가 의존하는 객체의 수
- 가변 의존성 외부 의존성이 얼마나 자주 바뀌는지
- 외부 의존성 외부 시스템에 대한 의존성

 

험블 객체 패턴을 사용해 지나치게 복잡한 코드 분할하기

지나치게 복잡한 코드
  • 복잡도 및 도메인 유의성도 높고 협력자 수도 많은 코드
  • 테스트가 어렵고, 변경하기도 어려우며, 버그가 생기기 쉬움

 

예시) 프레임워크 의존성 (비동기)

더보기
@Service
public class NotificationService {

    @Async
    public void sendEmail(String email, String message) {
        // 실제 이메일 전송 로직
        System.out.println("Sending email to " + email + ": " + message);
    }
}
  • 순수 Java 환경이나 단위 테스트에서는 작동하지 않음
      • @Async는 Spring에 의존
      • Spring 프레임워크 없이는 동작하지 않음
  • 테스트하면 비동기로 실행되지 않고 동작이 꼬일 수 있음

 

예시) UI

더보기
JButton button = new JButton("Order Now");
button.addActionListener(e -> {
    Order order = new Order("user1", List.of("item1", "item2"));
    order.calculateTotalPrice(); // UI에서 도메인 로직 직접 호출 ❌
    JOptionPane.showMessageDialog(null, "Order placed!");
});
  • UI 이벤트 안에서 도메인 객체 직접 생성, 로직 실행
  • 테스트 어려움

 

예시) 외부 의존성 통신

더보기
public class WeatherService {
    public Weather getCurrentWeather(String city) {
        RestTemplate restTemplate = new RestTemplate(); // 외부 통신
        String url = "https://api.weather.com/v1/" + city;
        return restTemplate.getForObject(url, Weather.class); // 직접 호출 ❌
    }
}
  • 장애 시 실패 전파가 직접 일어남

 

험블 객체 패턴
  • 복잡한 코드를 쪼개기 위한 패턴
    • 테스트가 가능한 부분을 추출
    • 결과적으로는 코드는 테스트 가능한 부분을 둘러싼 얇은 험블 래퍼가 됨
  • 육각형 아키텍처와 매우 흡사함
    • 비즈니스 로직과 오케스트레이션 코드를 분리

 

예시) 프레임워크 의존성 (비동기)

더보기
public class EmailSender {
    public void send(String email, String message) {
        // 이메일 발송 (순수 로직. 비동기 X)
    }
}

 

@Service
public class NotificationService {

    private final EmailSender emailSender;

    public NotificationService(EmailSender emailSender) {
        this.emailSender = emailSender;
    }

    @Async
    public void sendEmailAsync(String email, String message) {
        emailSender.send(email, message); // 테스트 대상은 이게 아님
    }
}
  • EmailSender는 단위 테스트 가능 (단순 기능)
  • NotificationService는 통합 테스트로 따로 다루기 (언제 어떻게 실행되는지 결정)

 

예시) UI

더보기
public class OrderService {
    public Order createOrder(String userId, List<String> items) {
        Order order = new Order(userId, items);
        order.calculateTotalPrice();
        return order;
    }
}
JButton button = new JButton("Order Now");
OrderService orderService = new OrderService();

button.addActionListener(e -> {
    Order order = orderService.createOrder("user1", List.of("item1", "item2"));
    JOptionPane.showMessageDialog(null, "Order for " + order.getTotalPrice() + " placed!");
});
  • OrderService는 로직 테스트
  • UI는 입력/출력만

 

예시) 외부 의존성 통신

더보기
public class WeatherService {
    private final WeatherClient weatherClient;

    public WeatherService(WeatherClient weatherClient) {
        this.weatherClient = weatherClient;
    }

    public boolean shouldBringUmbrella(String city) {
        Weather weather = weatherClient.getCurrentWeather(city);
        return weather.isRainy();
    }
}
public interface WeatherClient {
    Weather getCurrentWeather(String city);
}

public class WeatherClientImpl implements WeatherClient {
    private final RestTemplate restTemplate = new RestTemplate();

    @Override
    public Weather getCurrentWeather(String city) {
        String url = "https://api.weather.com/v1/" + city;
        return restTemplate.getForObject(url, Weather.class);
    }
}
  • 테스트에서는 WeatherClient를 Mock으로 대체 가능