Backend/Java

[Effective Java : Item 4] 인스턴스화를 막으려거든 private 생성자를 사용하라

김세진 2024. 1. 30. 11:18
반응형

 

 

 

 

 

개요

 

프로젝트 내에 정적 메서드와 정적 필드만을 담은, 예를 들면 java.lang.Math java.util.Arrays 같은 클래스를  생성하고 싶을 수 있다. 이러한 유틸리티 클래스는 인스턴스로 만들어 사용하고자 하는 것이 아니지만, 생성자를 명시하지 않으면 컴파일러가 자동으로 매개변수가 없는 기본 생성자를 만들어주기 때문에 인스턴스화될 여지가 있다. 따라서 생성자를 private으로 선언해줌으로써 인스턴스화를 방지할 수 있다. 아이템 1에서 소개되었던 내용이기도 하다.

 

만약 클래스를 추상 클래스로 만든다면 인스턴스화되지 않기는 하지만, 이는 완벽하게 인스턴스화를 막는 방법이 아니다. 클래스를 상속해서 인스턴스화 하면 되기 때문이다. 그리고 이런 방법은 명시적인 설명이 없다면 오히려 상속해서 사용하라는 뜻으로 오해할 수 있으니 적절한 방법이 아니라고 할 수 있다.

 


private 생성자

 

위는 java.lang.Math 클래스에서 생성자가 private로 선언된 코드이다. 주석으로도 이 클래스를 인스턴스화하지 말라고 표기되어 있다. 경고를 무시하고 억지로 인스턴스화 해보려고 시도해 보자.

 

당연하게도 접근 권한 관련 메시지가 뜨며 생성자를 사용할 수 없다는 오류가 발생한다. 클래스 내에서 인스턴스를 생성하는 정적 팩토리를 따로 제공하지도 않기 때문에 Math 클래스를 인스턴스화 할 방법은 없다.

 

또한 상속을 하기 위해서는 생성자가 public 또는 protected이어야 하므로 상속한 뒤 인스턴스화 하는 것도 자연스럽게 불가능해진다.

 


리플렉션 공격

 

아이템 3에서 소개되었던 내용이다. 생성자를 private로 명시하였더라도, 권한이 있는 다른 개발자가 리플렉션 API를 사용하여 private 메서드에 접근 권한을 얻어 강제로 인스턴스화할 수 있다.

 

java.lang.Math 와 같은 자바의 핵심 라이브러리는 안전성 및 보안을 위해 java.base 모듈이 패키지를 열어주지 않게 강제함으로써 공격 자체가 불가능하지만, 개발자가 임의로 생성한 클래스들은 충분히 가능한 내용이다.

 

public final class MyUtility {
	private MyUtility() {
		System.out.println("MyUtility instantiated");
	}

	public static void test() {
		System.out.println("test");
	}
}


public class MyUtilityReflectionAttack {
	public static void main(String[] args) throws Exception {
		Constructor<?> constructor = MyUtility.class.getDeclaredConstructors()[0];
		constructor.setAccessible(true);
		MyUtility myUtility = (MyUtility) constructor.newInstance();
		myUtility.test();
	}
}

 

MyUtility 클래스는 private 생성자를 가지고 있고 인스턴스를 가지지 않는 유틸리티 클래스이다. 이를 외부 클래스에서 리플렉션 API를 통해 강제로 인스턴스화를 시도해 보면 아래와 같은 결과를 볼 수 있다.

 

 

정상적으로 인스턴스화 된 것을 확인할 수 있다. 이를 방지하기 아래와 같은 조치를 취할 수 있다.

private MyUtility() {
    throw new AssertionError();
}

 

그냥 AssertionError를 던져 인스턴스화를 아예 불가능 하도록 만들어주자. 위처럼 코드를 수정한 뒤 main 코드를 다시 실행해보자.

 

 

기대했던 것과 같이 AssertionError 를 던지며 인스턴스 생성이 막히는 모습을 볼 수 있다. 뿐만 아니라, 이는 혹여라도 다른 개발자가 클래스 내에서 생성자를 호출할 여지를 막아주기도 한다.

 

 

 


정리

 

1. 인스턴스화를 막기 위해 추상 클래스로 만든다면 상속한 뒤 인스턴스화할 여지가 있을 뿐더러 오히려 그렇게 쓰라는 뜻으로 다른 개발자가 오해할 수 있다. 

2. 생성자를 private으로 만든 뒤 혹여나 호출될 시 AssertionError를 throw하도록 하여 인스턴스화를 방지하자.

 

 

 

 

 

반응형