나를 기록하다
article thumbnail
반응형

private 생성자 사용 이유

sonarlint의 코드리뷰

프리코스를 진행하면서 내 코드를 자동으로 리뷰해주는 IDE 확장 플러그인인 sonarlint의 도움을 많이 받았다.

sonarlint는 코드에 문제가 있다고 판단되면 해당 코드에 물결표 밑줄 표시를 하고 마우스를 가져가면 해당 코드의 문제점에 대해서 설명해준다. 아래에 출력을 담당하는 GameOutput 클래스의 클래스명에 밑줄로 경고를 하는데 이유가 뭘까?

숫자 야구 게임의 GameOutput 클래스

위 사진처럼 출력을 담당하는 기능을 하는 GameOutput에 따로 생성자를 만들지 않고 메서드들을 static으로 선언하여 외부 클래스에서도 GameOutput.{메서드명}과 같이 사용할 수 있도록 하였다.


이유

그런데 여기서 왜 이슈가 발생했을까?

이유는 아래에 sonarlint가 자세하게 설명해준다.

java:S1118 이슈 발생 이유


설명

설명은 다음과 같다.

유틸리티 클래스는 정적 멤버(메서드 또는 필드)의 모음으로, 인스턴스화되어서는 안 되는 클래스이다. 추상 유틸리티 클래스라도(public constructor 대신) public 생성자를 가지고서는 안 됩니다. 자바는 적어도 하나의 명시적 생성자를 정의하지 않은 모든 클래스에 암시적(public) 생성자를 추가한다. 따라서 최소한 하나의 private 생성자를 정의해야 한다.

이것은 유틸리티 클래스를 설계할 때 중요한 원칙 중 하나인데, 유틸리티 클래스는 주로 정적 메서드나 필드로만 이루어져 있으며 인스턴스화되지 않는 것이 일반적이다. 그래서 public 생성자를 가지지 않도록 하고, 명시적으로 생성자를 정의하지 않으면 자바는 암시적으로 public 생성자를 추가해버리기 때문에 반드시 하나 이상의 private 생성자를 클래스에 정의해야 한다. 이렇게 하면 다른 클래스에서 무분별한 인스턴스 생성을 방지할 수 있기 때문이다.


private 생성자를 사용하는 이유는 무엇일까?

생성자를 private로 정의하면 앞서 말했듯이 외부에서 메서드 호출을 할 수 없기 때문에 객체를 생성할 수 없다.

객체를 생성하는 방법은 클래스 안에서 생성자를 호출하는 방법 뿐이다.

외부에서 객체를 생성할 수 없고, 내부에서만 객체를 생성할 수 있다는 특징을 이용하기 위해 private 생성자를 사용한다.

그럼 우리는 언제, 어떤 이유로 private 생성자를 사용할까?


불필요한 객체 생성을 제한

다음 예시는 Arrays 클래스이다. 이 클래스의 모든 메서드는 static으로 구성되어 있고 그렇기 때문에 객체 생성 없이 메서드를 직접 호출할 수 있다. 불필요하게 객체를 생성할 필요가 없기에 이 코드를 잘 알고 있는 개발자는 객체를 생성하지 않겠지만 익숙하지 않은 개발자가 실수로 객체를 생성할 수 있다. 라이브러리를 설계하는 사람은 private 생성자를 사용하여 불필요한 객체 생성을 구조적으로 막을 수 있다.

public class Arrays {

    // Suppresses default constructor, ensuring non-instantiability.
    private Arrays() {}

    public static void sort(int[] a) {
        DualPivotQuicksort.sort(a, 0, a.length - 1, null, 0, 0);
    }

    public static <T> void sort(T[] a, Comparator<? super T> c) {
        if (c == null) {
            sort(a);
        } else {
            if (LegacyMergeSort.userRequested)
                legacyMergeSort(a, c);
            else
                TimSort.sort(a, 0, a.length, c, null, 0, 0);
        }
    }

    /** To be removed in a future release. */
    private static <T> void legacyMergeSort(T[] a, Comparator<? super T> c) {
        T[] aux = a.clone();
        if (c==null)
            mergeSort(aux, a, 0, a.length, 0);
        else
            mergeSort(aux, a, 0, a.length, 0, c);
    }
}

Singleton 패턴

Singletone 패턴은 프로세스 내에서 객체를 하나만 생성하고 모든 곳에서 이 객체를 사용하는 패턴이다. 만약 외부에서 생성자를 호출하여 객체를 쉽게 생성할 수 있다면, 프로세스 내에 1개의 객체만 생성되도록 관리하기는 어려울 것이다. 그래서 생성자를 private으로 선언하여 외부에서는 객체를 생성하지 못하도록 막고, getInstance()처럼 객체를 제공하는 스태틱 메서드를 만들고 이 메서드에서 1개의 객체만 생성되도록 관리할 수 있다.

public final class SingletonClass {

    private static SingletonClass INSTANCE;
    private String info = "Initial info class";

    private SingletonClass() {
    }

    public static SingletonClass getInstance() {
        if (INSTANCE == null) {
            INSTANCE = new SingletonClass();
        }

        return INSTANCE;
    }

    // getters and setters
}

외부에서는 static 메서드인 getInstance()를 호출하여 객체를 얻는다.

Singleton object = Singleton.getInstance();

인자 제한, 생성 작업 위임

객체 생성 시 ValueTypeClass(String value, String type)처럼 두 개의 인자를 받는데, 외부에서 첫 번째 인자로 intboolean만 받고 싶고, 두 번째 인자 type을 입력받지 않도록 제한하고 싶다. 임의로 null을 넣거나 잘못된 값을 넣었을 때 오동작을 방지하기 위해서이다. JavaDoc으로 잘 설명하더라도 사용법이 단순하지 않으면 오용할 수 있다. 이럴 때 생성자를 private으로 만들고 intboolean을 받는 public 생성자를 만들 수도 있다. public 생성자는 내부적으로 private 생성자를 호출하여 생성 작업을 위임하기도 한다.

public class ValueTypeClass {

    private final String value;
    private final String type;

    public ValueTypeClass(int x) {
        this(Integer.toString(x), "int");
    }

    public ValueTypeClass(boolean x) {
        this(Boolean.toString(x), "boolean");
    }

    private ValueTypeClass(String value, String type) {
        this.value = value;
        this.type = type;
    }

    // getters and setters
}

인스턴스화할 수 없는 클래스 생성

아래 예시는 단순한 정적 메서드들을 포함하는 클래스를 새엇ㅇ한다.

public class StringUtils {

    private StringUtils() {
        // this class cannot be instantiated
    }

    public static String toUpperCase(String s) {
        return s.toUpperCase();
    }

    public static String toLowerCase(String s) {
        return s.toLowerCase();
    }
}

StringUtils 클래스에는 정적 유틸리티 메서드가 포함되어 있으나 private 생성자가 있기에 이 클래스를 인스턴스화할 수 없다.

실제로 정적 메서드는 객체 인스턴스를 사용하지 않고도 사용할 수 있으므로 객체 인스턴스를 허용할 필요가 없다.


Builder 패턴

Singleton 패턴과 비슷하게 Builder 패턴에서도 private 생성자를 사용한다. Builder 클래스를 통해서만 Employee 객체를 생성할 수 있도록, Employee 생성자는 private으로 선언하여 외부에서 객체를 생성할 수 없도록 제한한다.

public class Employee {

    private final String name;
    private final int age;

    private Employee(String name, int age) {
        this.name = name;
        this.age = age;
    }

    public static class Builder {
        private String name;
        private int age;

        public Builder setName(String name) {
            this.name = name;
            return this;
        }

        public Builder setAge(int age) {
            this.age = age;
            return this;
        }

        public Employee build() {
            return new Employee(name, age);
        }
    }
}

클래스 외부에서는 아래와 같이 Builder를 통해서 Employee 객체를 생성할 수 있다.

Employee.Builder builder = new Employee.Builder();
builder.setName("John Doe");
builder.setAge(30);

Employee employee = builder.build();

final은 무엇일까?

정의

  • 값을 한 번 할당한 이후에는 변경이 불가능(immutable)함을 뜻한다.

종류

  • final 클래스: 상속할 수 없는 클래스
  • final 메서드: 오버라이딩 불가능한 메서드
  • final 변수: 수정 불가능한 변수. static final 조합으로 상수로 많이 사용된다.

익명 클래스 또는 람다식 내부에서 참조하는 외부 지역변수는?

  • 익명 클래스 또는 람다식 내부에서 참조하는 외부 지역변수는 final 변수 또는 Effectively final 변수만 가능하다.
    • effectively final이란 Java 8에 등장한 기능으로 final이 없어도 따로 변경이 없다면 final 변수로 간주해주는 기능
    • 객체의 경우에는 참조값만 바뀌지 않으면 된다.
  • 익명 클래스나 람다식의 경우 멀티 스레드 환경에서 동작할 수 있기에, 메서드 종료 시점보다 더 늦게 실행될 가능성이 존재한다. 이 경우 스택 영역에 저장되어 있던 지역변수들이 메서드가 모두 종료되면서 사라져 버리기에 람다식이 참조할 값이 없어져 버린다.
  • 인스턴스 변수는 힙 영역에, 클래스 변수(static)는 메서드 영역에 저장되므로 final이 아니라도 접근이 가능하다.
  • 아래 코드와 같은 상황을 방지하기 위해서이다.
public void method() {
    int val = 0;
    new Thread(() -> {
        try {
            Thread.sleep(90000);
            System.out.println(val);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }).start();
}

자바의 메모리 구조

Method( = class)

  • 클래스, 인터페이스, 메서드, 클래스 변수(static) 정보가 저장되는 영역
  • 모든 스레드가 공유하여 사용

Heap

  • new를 통해 생성된 인스턴스 객체(인스턴스 변수(전역변수) 포함)들이 저장되는 영역
  • GC에 의해 회수되기 전까지는 사용되지 않아도 남아 있다.
  • 모든 스레드가 공유하여 사용

Stack

  • 메서드 내에서 사용되는 지연변수, 매개변수, 리턴값이 저장되는 영역
  • 메서드가 호출될 때 생성되고, 메서드가 종료되면 바로 사라진다.
  • 각 스레드마다 별개로 존재

PC Register

  • 현재 수행 중인 JVM 명령어 주소 저장

Native Method Stack

  • 자바 외 다른 언어의 동작을 위해 할당된 영역

Runtime Data Area

참고: 이것이 자바다(신용권)
Class Loader와 JVM Memory


참고자료

https://www.baeldung.com/java-private-constructors

 

Private Constructors in Java | Baeldung

Learn about private constructors in Java.

www.baeldung.com

  • 이것이 자바다 - 한빛미디어, 신용권
반응형
profile

나를 기록하다

@prao

포스팅이 좋았다면 "좋아요❤️" 또는 "구독👍🏻" 해주세요!

profile on loading

Loading...