[Java] utility class는 무엇으로 구현하는 것이 좋을까?
Utility class?
utility class(유틸리티 클래스)는 애플리케이션 전체에서 활용할 수 있는 클래스로 정적 메소드(static method)를 통해 구현한다.
Interface의 static method 사용?
Java 8 이후로, interface에 static method를 사용할 수 있게 되었다.
만약 static method를 사용하여 유틸리티 클래스를 구현한다면 다음과 같이 작성할 수 있다.
public interface CalculatorUtils {
static int getSumResult(int a, int b) {
return a + b;
}
}
인터페이스로 구현하니 간결하고 실용적이게 보인다.
하지만 인터페이스로 유틸리티 클래스를 구현하는 데에는 몇 가지 단점이 있다.
1. 상속 및 객체 생성 차단 불가
인터페이스는 특성 상 상속을 막을 수 없다. 이 말은 즉, 다음과 같이 할 수 있다는 얘기다.
public class Client {
public static void main(String[] args) {
CalUtils calUtils = new CalUtils() {};
}
}
익명 클래스를 구현하여 인스턴스를 생성할 수 있게 된다. 이렇게 되면 유틸리티 클래스의 의도와 멀어지게 될 수 있다.
유틸리티 클래스는 기본적으로 정적 메서드만 구현하여 제공하기 때문에 인스턴스화할 필요가 없다.
인스턴스화한다면 메모리만 낭비할 뿐이며 다른 개발자들이 잘못 보고 인스턴스화해서 사용할 가능성도 생기게 된다.
https://www.baeldung.com/java-helper-vs-utility-classes
2. 인터페이스 의도와 멀어진 설계
Java의 인터페이스는 클래스들이 구현해야 하는 동작을 정의하는데 사용된다.
인터페이스는 클래스의 메서드 구현을 위한 기본 틀이 되기도 하며, 다른 클래스가 동작을 호출하기 위한 추상 자료형이 되기도 한다.
인터페이스를 활용하여 같은 틀에 여러 방식으로 구현 할 수 있으며, 필요한 구현이 사용되도록 할 수 있다. 이 특징을 다형성이라고 한다.
Java 8 이후에는 인터페이스에 default, static method를 구현할 수 있게 되었는데, 이는 하위 호환성을 위한 업데이트였다.
예를 들어, 다음 인터페이스가 있다고 가정할 때
public interface Calculator {
int getSumResult(int a, int b);
}
다음과 같이 곱셈을 위한 메서드가 추가되었다고 해보자.
public interface Calculator {
int getSumResult(int a, int b);
int getMultipleResult(int a, int b);
}
그러면 이 인터페이스를 구현한 클래스는 모두 getMultipleResult를 구현해야 될 것이다. 만약 이 인터페이스를 구현한 라이브러리가 수백 개라면 큰 참사가 일어날 수 있다.
하지만, default 메서드를 활용하여 구현하면 이를 방지할 수 있다.
public interface Calculator {
int getSumResult(int a, int b);
default int getMultipleResult(int a, int b) {
return a * b;
}
}
미리 기본 설계를 제공해주어 강제로 구현해야 되는 것을 방지할 수 있다.
static method는 default 메서드를 보조하는 helper method로 사용될 수 있다.
다음은 getZoneId 정적 메서드가 getZonedDateTime default 메서드의 helper method로 사용된 예시이다.
public interface TimeClient {
// ...
static ZoneId getZoneId (String zoneString) {
try {
return ZoneId.of(zoneString);
} catch (DateTimeException e) {
System.err.println("Invalid time zone: " + zoneString +
"; using default time zone instead.");
return ZoneId.systemDefault();
}
}
default ZonedDateTime getZonedDateTime(String zoneString) {
return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
}
}
이렇듯 인터페이스는 클래스의 설계, 구현, 호환성을 위한 자료형임을 알 수 있다.
하지만 유틸리티 클래스로 사용하게 되면 인터페이스가 클래스의 기능을 제공하는 것이 아니라 정적 메서드의 모음을 제공한다는 혼란을 가져다줄 수 있다.
또 추상 자료형이 아닌 여러 구현이 포함된다는 점에서 인터페이스의 기능이 이미 상실된 것 처럼 느껴진다.
그럼 어떻게 구현해야 할까?
먼저 유틸리티 클래스는 위 링크에서도 나와있듯이 상속 및 인스턴스화 하지 못하게 구현하는 것이 좋다.
public final class CalculatorUtils {
private CalculatorUtils() {}
public static int getSumResult(int a, int b) {
return a + b;
}
}
final class로 구현한 후 생성자를 private으로 선언하면 상속되거나 인스턴스화 되는 것을 막을 수 있다.
또한 유틸리티 클래스는 인스턴스화 하지 못하게 구현하므로 상태를 가지지 않도록하며, 메소드는 상태를 처리하는 것이 아닌 Input과 Output에 대한 연산을 처리하도록 구성한다.
Utility Class가 최선일까?
사실 유틸리티 클래스를 구성하는 것 자체가 좋지 않은 구현이 될 가능성이 높다.
유틸리티 클래스는 상태를 가지지 않으므로 객체의 자율성이 없다. 즉, 객체지향적인 설계를 하기 어렵게된다.
또한 유틸리티 클래스를 사용하는 순간 다른 클래스와의 결합도가 높아질 수 밖에 없다.
public class Client {
public void printSumResult() {
System.out.println(CalculatorUtils.getSumResult()); // CalculatorUtils와 강결합이 된다.
}
}
강결합 되어 있기 때문에 테스트도 까다로워 진다.
만약 적은 곳에서 사용된다면 클래스들의 책임이 적절하게 부여되어 있는지 고민해보자.
여러 곳에서 사용된다면 자율적인 객체로 만들어 의존성을 주입하여 해결할 수 있는지 고민해보자.
추가
interface를 통한 유틸리티 클래스 구현을 아예 안 좋다고 생각하지는 않는다.
만약 조직 내에서 실용적인 방식이라 판단되고 이를 구분할 수 있는 여러 규칙을 규정하여 사용한다면 인터페이스를 활용할 수 있을 것이라 생각한다.
Java API 중에서도 org.springframework.data.util.StreamUtils는 정적 메서드만 포함된 유틸리티 클래스가 인터페이스를 통해 구현이 되어있으며, java.util.Comparator에도 구현된 정적 메서드가 외부 클래스에서 다수 활용되고 있는 것을 볼 수 있다.
또한 oracle에서는 interface의 static method는 interface에 관련한 helper method로 명시하고 있다.
StreamUtils, Comparator와 같이 인터페이스와 관련한 기능을 별도의 클래스가 아닌 한 인터페이스에 구현할 수 있다는 장점을 드러내고 있다.
https://docs.oracle.com/javase/tutorial/java/IandI/defaultmethods.html
하지만 interface를 유틸리티 클래스로 사용하는 것에는 많은 주의가 필요해 보인다.
참고 자료
https://www.baeldung.com/java-interface-default-method-vs-abstract-class
https://www.baeldung.com/java-helper-vs-utility-classes
https://www.baeldung.com/java-static-default-methods
https://stackoverflow.com/questions/30905236/are-interfaces-a-valid-substitute-for-utility-classes-in-java-8
https://en.m.wikipedia.org/wiki/Constant_interface
'Language > Java' 카테고리의 다른 글
[Java] System.out.println의 사용을 지양하자 (0) | 2024.06.21 |
---|---|
[Java] 위도/경도 값에 BigDecimal or double? (2) | 2024.04.30 |
[Java] 제네릭(Generic) (0) | 2024.03.12 |
[Java] Mockito (0) | 2023.07.31 |
[Java] hashCode() (1) | 2023.07.02 |
댓글
이 글 공유하기
다른 글
-
[Java] System.out.println의 사용을 지양하자
[Java] System.out.println의 사용을 지양하자
2024.06.21 -
[Java] 위도/경도 값에 BigDecimal or double?
[Java] 위도/경도 값에 BigDecimal or double?
2024.04.30 -
[Java] 제네릭(Generic)
[Java] 제네릭(Generic)
2024.03.12 -
[Java] Mockito
[Java] Mockito
2023.07.31