코드를 작성하다가 이러한 질문을 받았다.
나의 답변은 간단했다.
"자주 사용하는 상수를 모아놓은 상수 클래스 즉, 정적 변수만 사용하는 클래스이기 때문에 인스턴스가 필요하지 않기 때문에 private 으로 선언했습니다."
그런데 뭔가 이 이상 답변하지 못한다는게 아쉬웠다.
적어도 파생되는 개념들을 설명할 수 있어야 한다고 생각이 들었다.
그래서 다시 공부해보기로 했다.
내가 유틸이나 상수클래스등에는 `private` 생성자를 해놓아야 한다는 말을 처음 들은 곳은 Effective Java Item 4 "인스턴스화를 막으려거든 `private` 생성자를 사용하라"였다.
해당 내용을 이전에 발표했던 내용을 사용해서 정리하자면 아래와 같다.
🚀 자바에서 인스턴스가 필요하지 않는 경우
자바에서 인스턴스가 필요하지 않는 경우는 적정 메서드만 사용할 때, 정적 변수만 사용할 때, 유틸리티 클래스를 사용할 때 다.
일반적으로 유틸리티 클래스나 정적 메서드 or 변수만 사용한다면 인스턴스 생성과 상속을 방지하는 게 좋다.
❓ 생성자를 만들지 않으면?
자바에서 객체는 생성자를 만들지 않으면 자동으로 public으로 된 default 생성자를 만듭니다.
🤔 인스턴스 생성 방지는 왜 해줘야할까?
유틸리트 클래스는 인스턴스를 생성할 필요 없이 정적(static)메서드와 필드만을 제공하기 위해 만들어진다. 만약 인스턴스를 생성할 수 있게 하면 다음과 같은 문제가 발생할 수 있다.
1. 인스턴스를 생성할 필요가 없는데도 인스턴스를 만들어서 사용하면, 클래스의 목적과 어긋나는 사용이 발생할 수 있다.
2. 코드 리뷰나 협업 시, "왜 이 클래스는 인스턴스를 생성해야 하는가?"라는 혼란을 줄 수 있다.
3. 실수로 인스턴스를 생성하려고 했을 때 컴파일 에러가 발생하지 않아 실수를 미연에 방지할 수 없다.
3. 인스턴스가 불필요하게 생성되면 메모리 사용이 늘어나고, 나중에 유지보수에 문제가 될 수 있다.
따라서, 인스턴스 생성을 방지함으로써 클래스의 의도를 명확히 하고, 잘못된 사용이나 오해를 미연에 방지할 수 있기 때문에 유틸리티 클래스와 같은 형태로 만든다면 인스턴스 생성을 방지해주는게 좋다.
실제로 자바 유틸을 보면 아래와 같이 `private` 생성자를 사용해서 인스턴스화를 방지해주고 있다.
물론 내가 사용했던 정적 변수(static variables)만 사용하는 클래스도 당연히 인스턴스화 방지를 해줘야한다.
아래는 실제 스프링에서 정적 변수만 사용하는 클래스이다.
🤔 그렇다면 final은 왜 추가적으로 해줄까?
위에서 예제를 보았던 `Arrays` or `Math`를 보면 전부 final로 선언되어있다.
`private` 생성자만 있어도 "상속"자체는 불가능하다.
상속을 위해서는 자식 클래스가 부모 생성자를 호출해야 상속이 가능한데, `private` 생성자는 자식 클래스가 접근할 수 없기 때문이다.
이로인해 당연히 메서드를 추가하거나 오버라이드(재정의)도 할 수 없다.
그럼에도 불구하고 왜 `final`까지 쓰는 걸까?
📌 의도 명확화
클래스에 final을 붙이면 “이 클래스는 절대 확장될 의도가 없어요”라는 메시지를 전달할 수 있다. 다른 개발자들도 이 클래스는 오직 정적 기능만을 제공하기 위한 것임을 쉽게 이해할 수 있다.
쉽게 설명하자면 아래와 같다.
`private` 생성자는 집 문을 잠그는 것과 가다. -> 밖에서 절대 들어올 수 없음
`final` 키워드는 아예 "이 집은 출입 금지!!!!!" 라는 표지판을 붙이는 것과 같다.
이와 같은 이유로 자바 공식문서나 유명 라이브러리에서도 유틸리티 클래스에는 `final`키워드와 `private`생성자를 함께 사용하는 것이 권장 관습이다.
😎 한 걸음 더
🤔 private 생성자를 만들어 놓으면 인스턴스화를 완전히 막을 수 있나?
사실 `private` 생성자를 붙인다고해서 완벽하게 인스턴스화를 방지할 수 있는 것은 아니다.
리플렉션을 사용한다면 `private`생성자로 인스턴스화를 막아놓았다고 해도 접근이 가능하다.
아래처럼 유틸리티 클래스를 작성하면, 클래스 내부에서만 생성자가 호출되므로 외부에서는 인스턴스를 만들 수 없다.
public final class UtilityClass {
// private 생성자: 외부에서 인스턴스 생성 불가
private UtilityClass() {
System.out.println("UtilityClass 인스턴스 생성됨");
}
public static void doSomething() {
System.out.println("무언가를 수행함");
}
}
자바의 리플렉션 기능을 사용하면 private 생성자에도 접근할 수 있다.
아래 예제를 보면 어떻게 리플렉션으로 인스턴스를 만드는지 확인할 수 있다.
import java.lang.reflect.Constructor;
public class ReflectionTest {
public static void main(String[] args) {
try {
// UtilityClass의 private 생성자 정보를 가져옴
Constructor<UtilityClass> constructor = UtilityClass.class.getDeclaredConstructor();
// private 접근 제어자를 무시하도록 설정
constructor.setAccessible(true);
// 리플렉션을 통해 인스턴스 생성
UtilityClass instance = constructor.newInstance();
instance.doSomething();
} catch (Exception e) {
e.printStackTrace();
}
}
}
위 코드에서 `constructor.setAccessible(true)`를 호출하면, private 접근 제어자가 무시되어 인스턴스 생성이 가능해진다.
public final class UtilityClass {
// private 생성자로 외부 인스턴스 생성을 막음
private UtilityClass() {
throw new AssertionError("인스턴스 생성은 허용되지 않습니다.");
}
public static void doSomething() {
System.out.println("무언가를 수행함");
}
}
꼭 AssertionError를 던질 필요는 없지만, 클래스 안에서 실수로라도 생성자를 호출하지 않도록 도와줍니다.
❓ AssertionError
AssertionError는 자바 개발 언어에서 발생하는 오류입니다. 이 오류는 자바 프로그램이 예상하는 결과가 나타나지 않을 때 발생합니다. 이 오류는 런타임 오류로 인식되며, 코드에서 의도한 것과 다른 결과가 발생하는 문제를 식별하는 데 도움이 됩니다. 간단히 말해서, AssertionError는 프로그램이 "이렇게 동작해야 해"라고 우리가 가정한 규칙을 따르지 않을 때, "잠깐, 이건 맞을 수 없어!"라고 알려주는 오류라고 생각하시면 됩니다. 이 오류는 예외를 처리하기위한 에러가 아닙니다. try-catch에서 사용하고자 하는 에러가 아니고 정말 만나면 안되는 상황의 경우에 배치하고 혹시라도 발생하면 이건 무조건 잘못된 거다라는 곳에서 사용합니다.
🤔 왜 자바 유틸리티 클래스에서는 AssertionError를 던져주지 않고 private 생성자만 만들고 끝낼까?
일반적인 상황에서는 `private`생성자 때문에 해당 클래스의 인스턴스가 생서될 일이 없으므로, 추가적인 예외를 던질 필요가 없다.
즉, 컴파일 타임과 정상적인 런타임 환경에서는 인스턴스 생성 자체가 불가능하므로, 더 이상의 방어 코드가 필요하지 않다.
그럼에도 불구하고 누군가 리플렉션을 사용해 private 생성자에 접근하려고 한다면, 이는 이미 일반적인 사용이 아닌 비정상적인 상황인 것이다.
일부 개발자는 이런 경우를 방어하기 위해 위에서 언급한대로 `throw new AssertionError()` 같은 코드를 추가하기도 한다.
하지만 자바 유틸이나 다른 많은 라이브러리에서도 굳이 이런 예외 처리를 추가하지 않고 private 생성자만사용하고 있습니다.
(리플렉션을 통한 인스턴스 생성은 개발자가 의도적으로 규칙을 무시하는 행동이므로, 굳이 코드로 방어할 필요가 없다는 것)
또한 불필요한 예외 던지기를 추가하면 코드가 복잡해질 수 잇고, "내부 오류"처럼 보일 수 있기에 단순히 private 생성자만 선언함으로써 "이 클래스는 절대 인스턴스화되면 안 된다."는 의도를 깨끗하게 전달할 수 있다.
📌 Reference
- Effective Java item 4
- https://www.baeldung.com/java-private-constructors
- https://www.baeldung.com/java-private-constructor-access
'JAVA > 개념' 카테고리의 다른 글
yyyy-MM-dd vs YYYY-MM-dd (feat. 자바 날짜 포맷팅) (0) | 2025.02.21 |
---|---|
Java 방어적 복사(Defensive Copy) 가이드 (feat. ArrayList, unmodifiableList, List.copyOf()) (0) | 2025.02.16 |
언제 equals를 재정의 해야할까? (0) | 2024.11.22 |
Collection.forEach vs Stream.forEach (1) | 2024.11.20 |
stream.forEach (feat. 종단 연산) (1) | 2024.11.18 |