[이펙티브 자바] 아이템24 | 멤버 클래스는 되도록 static으로 만들어라

제롬·2022년 3월 16일
0

이펙티브자바

목록 보기
24/25

중첩 클래스(nested class) 정의

다른 클래스 안에 정의된 클래스를 말한다.

  • 중첩 클래스는 자신을 감싼 바깥 클래스에서만 사용되어야 한다.
  • 중첩 클래스가 바깥 클래스 이외의 쓰임새가 있다면 톱레벨 클래스로 만들어야 한다.
  • 중첩 클래스를 사용함으로써 불필요한 노출을 줄여 캡슐화를 할 수 있고 가독성 좋고 유지보수하기 좋은 코드를 작성할 수 있다.

중첩 클래스 종류

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

이 중 첫번째를 제외한 나머지 클래스들은 내부 클래스에 해당한다.

정적 멤버 클래스

정적 멤버 클래스와 비정적 멤버 클래스를 구분하는 기준은 static 키워드가 함께 작성되었는지 여부로 판단할 수 있다.

[정적 멤버 클래스 - 외부 클래스 private 멤버 접근]

class OuterClass {
    private int a = 100;

    static class InnerClass {
        private int b;
        
        void accessOuterClass(){
            OuterClass outerClass = new OuterClass();
            outerClass.a = 1;
        }
    }
}
  • 이처럼 static 키워드와 함께 작성된 InnerClass는 정적 내부 클래스이다.
  • 이런 정적 내부 클래스는 외부 클래스의 private 멤버에도 접근할 수 있다.
    • 위 코드에서 InnerClassOuterClassa 필드에 접근.
  • 외부 클래스의 private 멤버에 접근할 수 있다는점만 빼면 일반 클래스와 똑같다.
  • 다른 정적 멤버와 똑같은 접근 규칙을 적용받는다.
    • 예컨데, private으로 선언하면 바깥 클래스에서만 접근할 수 있다.
  • 개념상 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들어야한다.
  • static이 붙은 정적 멤버 클래스와 비정적 멤버 클래스의 차이는 외부 인스턴스없이 내부 인스턴스를 바로 생성할 수 있다는 점이다.

[정적 멤버 클래스 인스턴스 생성]

public void createClass(){
    OuterClass.InnerClass innerClass = new InnerClass();
}

비정적 멤버 클래스

static 키워드가 없이 작성된 클래스는 비정적 내부 클래스이다.

[비정적 멤버 클래스 선언]

class OuterClass {
    private int a = 100;

    class InnerClass {
        private int b;
    }
}
  • static 키워드에 따라 정적 그리고 비정적 멤버 클래스의 생성 방법이 달라진다.
  • 비정적 멤버 클래스의 인스턴스는 바깥 클래스의 인스턴스와 암묵적으로 연결된다.
  • 비정적 멤버 클래스는 this 키워드로 바깥 인스턴스의 메서드를 호출하거나 참조를 가져올 수 있다.
    • 이때 정규화된 this를 사용하는데 정규화된 this클래스명.this 형태로 바깥 클래스의 이름을 명시하는 용법을 말한다.
  • 비정적 멤버 클래스는 바깥 클래스의 존재 없이 존재할 수 없다. 따라서 비정적 멤버 클래스를 독립적으로 사용하려면 정적 멤버 클래스로 만들어야 한다.

[비정적 멤버 클래스 바깥 클래스 메서드 호출 - 암묵적 연결]

class OuterClass {
    private int a = 100;

    public void createClass(){
        ...
    }

    class InnerClass {
        private int b;

        void accessOuterClass(){
            OuterClass.this.createClass();
        }
    }
}
  • 비정적 멤버 클래스의 인스턴스와 바깥 인스턴스 사이의 관계는 멤버 클래스가 인스턴스화 될 때 확립되며, 변경할 수 없다.
  • 두 인스턴스 사이의 관계는 바깥 클래스의 인스턴스 메서드에서 비정적 멤버 클래스의 생성자를 호출할 때 자동으로 만들어진다.
  • 바깥 인스턴스의 클래스.new 클래스명(args)를 호출해 수동으로 만들기도 한다.

[비정적 멤버 클래스 인스턴스화 - 바깥, 비정적 멤버 클래스간 관계 확립]

public class Main {
    public static void main(String[] args) {
        final OuterClass outerClass = new OuterClass();
        outerClass.new InnerClass();
    }
}

비정적 멤버 클래스 활용

비정적 멤버 클래스는 어댑터를 정의할 때 자주 사용된다.

어댑터 패턴이란?
어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게하는 뷰로 사용하는 것.

[어댑터 정의 - 비정적 멤버 클래스 사용(반복자 구현)]

public class MySet<E> extends AbstractSet {
    @Override
    public Iterator iterator() {
        return new MyIterator();
    }

    private class MyIterator implements Iterator<E>{
		...
    }
}

비정적 멤버 클래스로 어댑터 구현시

  • 바깥 인스턴스로부터 숨은 외부 참조를 갖게 된다.
  • 이 참조를 저장하려면 시간과 공간이 소비된다.
  • GC(가비지 컬렉션)이 바깥 클래스의 인스턴스를 수거하지 못하는 메모리 누수가 생길 수 있다.
  • 참조가 눈에 보이지 않아 문제의 원인을 찾기 어렵다.

따라서, 멤버 클래스에서 바깥 클래스에 접근할 일이 없다면 무조건 static을 붙여서 정적 멤버 클래스로 만들자.

[어댑터 정의 - 정적 멤버 클래스 사용(반복자 구현)]

public class MySet<E> extends AbstractSet {
    @Override
    public Iterator iterator() {
        return new MyIterator();
    }

    private static class MyIterator implements Iterator<E>{
		...
    }
}

이와 비슷하게 SetList 같은 다른 컬렉션 인터페이스 구현자들도 자신의 반복자를 구현할 때 비정적 멤버 클래스를 주로 사용한다.

정적 멤버 클래스 활용

private 정적 멤버 클래스는 흔히 바깥 클래스가 표현하는 객체의 한 부분을 나타낼 때 사용한다.

MapEntry의 관계에서 생각해보면
Mapkey-value를 표현하는 Entry객체들을 가지고 있다. 이 Entry객체들의 메서드들(getKey, getValue 등)은 맵을 직접 사용하지 않는다.

따라서, 이럴 때 Entry클래스를 비정적 멤버 클래스로 만든다면 Entry는 매번 Map과의 참조 관계를 유지하고 있을 것이다.

이럴 경우 시간과 공간의 낭비가 발생하기 때문에 static 키워드를 사용해 정적 클래스로 만들어 관리하는 것이 좋다.

익명 클래스

  • 이름이 없다.
  • 바깥 클래스의 멤버가 아니다.
  • 멤버와 달리 선언하는 시점에 인스턴스가 만들어진다.
  • 코드의 어디서든 만들 수 있다.
  • 비정적인 문맥에서 사용될때만 바깥 클래스의 인스턴스를 참조할 수 있다.

[익명 클래스]

public class Main {
    Inner innerClass(){
        return new Inner() {
            @Override
            public void hello(final String s) {
                System.out.println("hello");
            }
        };
    }
    
    interface Inner{
        void hello(String s);
    }
}

위 익명 클래스 코드는 람다로도 표현이 가능하다.

[람다로 표현한 익명 클래스]

public class Main {
    Inner innerClass() {
        return (String s) -> {
            System.out.println("hello " + s);
        };
    }

    interface Inner {
        void hello(String s);
    }
}

이런 익명클래스는 몇가지 단점이 있다.

  • 편리하지만 재사용이 안된다.
  • 멀티 구현이 불가능하다.
  • instanceof 검사나 클래스 이름이 필요한 작업은 수행이 불가능하다.
  • 여러 인터페이스를 구현할 수 없고 인터페이스를 구현하는 동시에 클래스를 상속할 수도 없다.
  • 익명 클래스가 상위 타입에서 상속한 멤버 외에는 호출할 수 없다.
  • 초기화된 final 기본 타입과 문자열 필드만 가질 수 있다.

익명 클래스는 정적 팩터리 메서드를 구현할때 자주 쓰이곤 한다.

지역 클래스

[지역 클래스 사용]
static 멤버는 갖지 못하며, 클래스 내부에서 필요한 기능을 정의할 때 사용한다.

class OuterClass {
    public void test() {
        class LocalClass {
            void printHello() {
                System.out.println("Local Class Hello!");
            }
        }

        final LocalClass localClass = new LocalClass();
        localClass.printHello();
    }
}

public class Main {
    public static void main(String[] args) {
        final OuterClass outerClass = new OuterClass();
        outerClass.test();
    }
}
  • 네가지 중첩 클래스중 가장 드물게 사용된다.
  • 지역 변수를 선언할 수 있는 곳이면 어디서든 선언이 가능하다.
  • 지역 클래스가 작성된 메서드 내부에서 생성해서 사용이 가능하다.
  • 유효범위도 지역변수와 같다.
  • 멤버 클래스처럼 이름이 있고 반복해서 사용이 가능하다.
  • 익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있다.
  • 정적 멤버는 가질 수 없으며 가독성을 위해 짧게 작성해야 한다.

정리

중첩 클래스는 4가지 종류가 있고 각각의 성격에 따라 쓰임이 다르다.

메서드 밖에서도 사용해야 하거나 메서드 안에 정의하기에 너무 길다면 멤버 클래스로 만들자.

멤버 클래스의 인스턴스가 바깥 인스턴스를 참조한다면 비정적으로 만들고 그렇지 않다면 정적으로 만들자.

중첩 클래스가 한 메서드 안에서 쓰이면서 그 인스턴스를 생성하는 지점이 한곳이고 해당 타입으로 쓰기에 적합한 클래스나 인터페이스가 이미 있다면 익명 클래스로 그렇지 않다면 지역 클래스로 만들자.


[Reference]
이펙티브 자바 아이템24
멤버 클래스는 되도록 static으로 만들자

0개의 댓글