프로그래밍Java 개발자

유틸리티 클래스와 정적 메서드를 사용하는 것 (utility class)과 비즈니스 로직을 수행하기 위해 개별 클래스 인스턴스를 생성하는 것의 차이점은 무엇인가요? 어떤 접근 방식을 언제 사용해야 하나요?

Hintsage AI 어시스턴트로 면접 통과

답변.

질문 배경

자바는 처음부터 객체 지향 언어로 개발되었으며, 기본적으로 클래스의 인스턴스를 생성하여 문제를 해결하는 데 중점을 두었습니다. 그러나 자주 사용되는 메서드(예: 정렬, 데이터 변환)의 경우, 정적 메서드 세트를 가진 유틸리티 클래스가 등장하기 시작했습니다(예: java.util.Collections).

문제

정적 메서드는 도움 기능을 호출하는 것을 간편하게 하지만 상태를 저장하거나, 의존성을 주입하거나, 독립적으로 테스트하는 데는 적합하지 않습니다. 반면 인스턴스 클래스는 더 유연하지만 초기화에 더 많은 코드를 요구하며, 신중한 생명 주기를 필요로 합니다.

해결책

  • 유틸리티 클래스 (utility classes) — 상태가 없는 정적 메서드의 집합으로 객체 생성을 요구하지 않습니다. 컬렉션 조작, 변환, 수학 연산에 적합합니다.

  • 인스턴스 클래스 — 상태를 저장하고, 의존성을 사용하며, 확장성과 테스트 가능성을 보장합니다. 비즈니스 로직, 서비스, 컨트롤러 등에서 사용됩니다.

코드 예시:

// 유틸리티 클래스 예시 public class MathUtils { public static int add(int a, int b) { return a + b; } } // 사용법: int sum = MathUtils.add(1, 2); // 비즈니스 로직을 위한 클래스 인스턴스 예시 public class OrderService { private final OrderRepository repo; public OrderService(OrderRepository repo) { this.repo = repo; } public void placeOrder(Order o) { repo.save(o); } }

주요 특징들:

  • Stateless이며 독립적으로 테스트할 수 없는 유틸리티 클래스.
  • 상태가 있는 클래스는 의존성 주입(DI)을 통해 쉽게 교체할 수 있습니다.
  • 유틸리티 클래스는 종종 final로 선언되며, 생성자는 private으로 선언됩니다.

의도가 있는 질문들.

유틸리티 클래스를 상속하거나 정적 메서드를 확장할 수 있나요?

아니요, 일반적으로 유틸리티 클래스는 final로 선언되고, private 생성자를 가지고 있습니다. 정적 메서드의 상속은 가능하지만 의미가 없으며, 인스턴스 호출 수준에서 상속되지 않습니다.

코드 예시:

public final class MyUtils { private MyUtils() {} // 인스턴스 생성을 방지함 }

유틸리티 클래스가 상태를 가질 수 있나요?

아니요. 유틸리티 클래스가 상태(instance 또는 static 필드)를 가지고 있다면, 이는 유틸리티의 원칙을 위반하며, 멀티스레딩 문제와 가독성을 저하시킵니다.

테스트시에 유틸리티 클래스의 정적 메서드를 모킹할 수 있나요?

특별한 도구인 PowerMock을 사용하여 가능합니다. 그러나 이는 테스트를 더 복잡하고 때때로 불안정하게 만들 수 있습니다. 일반적으로 DI 친화적인 접근이 테스트에 더 적합합니다.

일반적인 실수 및 안티 패턴

  • 단일 책임 원칙 위반: 유틸리티가 상태를 저장하기 시작함.
  • 유틸리티 클래스의 상속 또는 확장.
  • 상태를 고려해야 할 경우에 정적 메서드 사용 (예: 비즈니스 서비스).

실제 사례

부정적인 사례

프로젝트에서 모든 서비스가 유틸리티 정적 메서드로 구현됨. 의존성 주입이 불가능하고, 단위 테스트는 격리되지 않으며, 각 테스트가 환경 상태에 의존함.

장점:

  • 빠른 시작.
  • 호출의 간단함.

단점:

  • 테스트 가능성 부족.
  • 책임 분산 문제.

긍정적인 사례

서비스는 인터페이스를 통해 의존성을 주입 받은 별도 클래스를 활용. 변환과 간단한 작업에는 상태가 없는 별도 유틸리티 클래스를 사용.

장점:

  • 아키텍처의 유연성, 좋은 확장성.
  • 우수한 테스트 가능성.

단점:

  • 클래스 설명 및 의존성 주입을 위한 코드량 증가.