본문 바로가기
Effective Java

[Effective Java] 아이템24 멤버 클래스는 되도록 static으로 만들라

by byeongoo 2021. 3. 1.

■ 중첩 클래스

중첩 클래스란 다른 클래스안에 정의된 클래스를 말한다. 중첩 클래스에는 4가지 종류가 있다. 이중 정적 멤버 클래스를 제외한 클래스를 내부 클래스(inner class)라고 부른다.

  • 정적 멤버 클래스
  • 멤버 클래스
  • 익명 클래스
  • 지역 클래스

■ 중첩 클래스 사용 이유

  • 내부 클래스에서 외부 클래스의 멤버에 손쉽게 접근할 수 있다.
  • 서로 관련 있는 클래스들을 논리적으로 묶어, 코드의 캡슐화를 증가시킬 수 있다.
  • 외부에서 내부 클래스에 접근할 수 없으므로 코드의 복잡성을 줄일 수 있다.
  • 외부 클래스의 복잡한 코드를 내부 클래스로 옮겨 코드 복잡성을 줄일 수 있다.

■ 정적 멤버 클래스

정적 멤버 클래스는 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다는 점을 제외하고 일반 클래스와 동일하다. 외부 클래스를 간편하게 사용하기 위한 목적으로 쓰이는 대표적인 예시로 builder 패턴이 있다.

public class NutritionFacts {
  private final int servingSize;
  private final int servings;
  private final int calories;
  private final int fat;

  public static class Builder {
    // 필수 매개변수
    private final int servingSize;
    private final int servings;

    // 선택 매개변수 - 기본값으로 초기화한다.
    private int calories      = 0;
    private int fat           = 0;

    public Builder(int servingSize, int servings) {
      this.servingSize = servingSize;
      this.servings    = servings;
    }

    public Builder calories(int val) {
      calories = val;      
      return this; 
    }
    public Builder fat(int val) {
      fat = val;           
      return this; 
    }

    public NutritionFacts build() {
      return new NutritionFacts(this);
    }
  }

  public NutritionFacts(Builder builder) {
    servingSize  = builder.servingSize;
    servings     = builder.servings;
    calories     = builder.calories;
    fat          = builder.fat;
  }

  public static void main(String[] args) {
    NutritionFacts cocaCola = new NutritionFacts.Builder(240, 8)
      .calories(100).build();
  }
}

 

 

■ 멤버 클래스

정적 멤버 클래스와 비정적 멤버 클래스의 구문상 차이는 단지 static이 붙어있고 없고 뿐이지만, 의미상 차이는 의외로 꽤 크다. 비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다. 그래서 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this를 사용해 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다.

package ch4.hoon.item24;

public class NestedNonStaticExample {

    private final String name;

    public NestedNonStaticExample(String name) {
        this.name = name;
    }

    public String getName() {
        // 비정적 멤버 클래스와 바깥 클래스의 관계가 확립되는 부분
        NonStaticClass nonStaticClass = new NonStaticClass("nonStatic : ");
        return nonStaticClass.getNameWithOuter();
    }

    private class NonStaticClass {
        private final String nonStaticName;

        public NonStaticClass(String nonStaticName) {
            this.nonStaticName = nonStaticName;
        }

        public String getNameWithOuter() {
            // 정규화된 this 를 이용해서 바깥 클래스의 인스턴스 메서드를 사용할 수 있다.
            return nonStaticName + NestedNonStaticExample.this.getName();
        }
    }
}

개념상 중첩 클래스의 인스턴스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들어야한다. 비정적 멤버 클래스는 바깥 인스턴스 없이는 생성할 수 없기 때문이다.

 

대개 비정적 멤버 클래스는 바깥 클래스의 메소드에서 인스턴스화 됨 그 관계가 확립이 된다. 드물게 "바깥 클래스의 인스턴스.new 비정적클래스"를 통해 생성하기도 하나 이는 비용과 생성시간 측면에서 좋지 못하다. 아래 경우는 되도록 피하도록하자.

public class NestedNonStaticExample {

    private final String name;

    public NestedNonStaticExample(String name) {
        this.name = name;
    }

    ...

    public class NonStaticPublicClass{

    }
}
NestedNonStaticExample nestedNonStaticExample = new NestedNonStaticExample("name");
nestedNonStaticExample.new NonStaticPublicClass();

 

■ 정적 내부 클래스와 비정적 내부 클래스 차이 (메모리 누수)

다음과 같은 정적 내부 클래스와 비정적 내부 클래스가 있다고해보자.

class A{
    private int a;
  
    static class B{
    	    private int b;  
    }
}

class A{
    private int a;
  
    class B{
  	      private int b;  
    }
}

정적 내부 클래스의 경우 다음과 같이 객체를 생성할 수 있다. static 예약어가 있음으로 독립적으로 생성할 수 있다.

void foo(){
    A.B b = new B();
}

비정적 내부 클래스인 경우에는 다음과 같이 생성해야한다. 비정적 내부 클래스는 A객체를 생성 후 A객체를 이용하여 B 객체를 생성해야한다. 즉, 비정적 내부 클래스는 바깥 클래스에대한 참조가 필요한 것이다.

void foo(){
    A a = new A();
    A.B b = a.new B();
}
//or
void foo(){
    A.B b = new A().new B();
}

 

비정적 내부 클래스의 경우 바깥 클래스에 대한 참조를 가지고 있기 때문에 메모리 누수가 발생할 여지가 있다. 바깥 클래스는 더 이상 사용되지 않지만 내부 클래스의 참조로 인해 GC가 수거하지 못해서 바깥 클래스의 메모리 해제를 하지 못하는 경우가 발생할 수 있다.

 

정적 내부 클래스의 경우 바깥 클래스에 대한 참조 값을 가지고 있지 않기 때문에 메모리 누수가 발생하지 않는다. 메모리 누수가 발생할 수 있는 문제점이 있기 때문에 만약 내부 클래스가 독립적으로 사용된다면 정적 클래스로 선언하여 사용하는 것이 좋다. 바깥 클래스에 대한 참조를 가지지 않아 메모리 누수가 발생하지 않기 때문이다.

 

비정적 클래스를 어댑터 패턴을 이용하여 바깥 클래스를 다른 클래스로 제공할 때 사용하면 좋다. 이러한 케이스로 HashMap keySet() 이 있다. keySet() 을 사용하면 Map의 key에 해당하는 값들을 Set으로 반환해 주는 데 어댑터 패턴을 이용해서 Map Set으로 제공한다.

어댑터 패턴을 이용하는 경우 비정적 내부 클래스는 내부 클래스가 바깥 클래스 밖에서 사용되지 않는다. 내부에서는 KeySet이라는 객체로 생성되었지만 반환될 때는 Set으로 반환되기 때문에 KeySet이 직접적으로 노출이 되지 않는다. 따라서 일반 클래스 만들어서 사용하게 된다면 이는 논리적으로 군집화를 하지 않는 것이고 캡슐화를 해치게 된다고 볼 수 있다. 그리고 keySet()으로 반환된 Set은 Map에 새로운 Entry가 추가될 때 동기화된다. 하지만 일반 클래스로 이를 만든다면 그러한 의도를 가지고 구현을 해야 하고 사용하는 사람은 동기화가 된다고 파악하기도 힘들어진다.

 

■ 익명 클래스

익명 클래스는 이름이 없는 클래스이고 외부 클래스의 멤버 클래스도 아니다. 사용되는 시점에서 선언과 동시에 인스턴스가 만들어진다. 또한 익명 클래스가 상위 타입(자기 자신 혹은 부모)에 상속한 멤버 외에는 호출할 수가 없다. 자바 1.8부터는 익명 클래스보다는 람다를 활용한다.

 

■ 지역 클래스

지역 클래스는 가장 드물게 사용하는 클래스이다. 지역 변수를 선언할 수 있는 곳이라면 어디든 선언이 가능하고 유효 범위도 지역 변수와 동일하다.

 

REFERENCE