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

Loopy·2022년 7월 23일
0

이펙티브 자바

목록 보기
23/76
post-thumbnail

중첩 클래스

중첩 클래스(nested class)란, 다른 클래스 안에 정의되어 자신을 감싼 바깥 클래스에서만 쓰여야 하는 클래스를 의미한다.

📚 중첩 클래스 종류
정적 멤버 클래스, (비정적)멤버 클래스, 익명 클래스, 지역 클래스

이번 아이템에서, 각각의 중첩 클래스를 언제 그리고 왜 사용해야 하는지에 대해 알아보자.

☁️ 정적 멤버 클래스

정적 멤버 클래스는 다른 클래스 안에 선언되고, 바깥 클래스의 private 멤버에도 접근할 수 있다는 점만 제외하면 일반 클래스와 동일하다. 또한, 다른 정적 멤버와 똑같은 접근 규칙을 받는다.

더불어 흔히 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스로도 쓰인다. 예를 들어, 계산기가 지원하는 연산 종류를 정의하는 열거 타입을 들때, Operation 열거 타입이 Calculator의 public 정적 멤버 클래스가 되면은 아래와 같은 형태로 원하는 연산을 참조할 수 있다.

public class Calculator {

    public enum Operator {
        PLUS("+", (x, y) -> x + y),
        MINUS("-", (x, y) -> x - y);

        private final String token;
        private final Strategy strategy;

        Operator(String token, Strategy strategy) {
            this.token = token;
            this.strategy = strategy;
        }

        public double operate(double x, double y) {
            return this.strategy.operate(x, y);
        }

        private interface Strategy {
            double operate(double x, double y);
        }
    }

}

주의사항

정적 멤버 클래스의 static 이 붙어 있고 없고는, 일반 내부 클래스와 의미상으로 큰 차이를 가진다.

  1. 비정적 멤버 클래스의 인스턴스 메서드에서 this 를 사용하여 바깥 인스턴스의 참조를 가져올 수 있다. 이말은 즉 비정적 멤버 클래스는 숨은 바깥 클래스의 인스턴스 참조를 가지고 있게 되고, 이는 가비지 컬렉션이 바깥 클래스의 인스턴스를 수거하지 못해 메모리 누수가 생길 수 있다.
  2. 바깥 클래스는 로드 없이 내부 클래스만 독립적으로 로드된다.

따라서 중첩 클래스의 인스턴스가 바깥 인스턴스와 독립적으로 존재할 수 있다면 정적 멤버 클래스로 만들어야 한다.

📚 private 정적 멤버 클래스
바깥 클래스가 표현하는 객체의 한 부분(구성요소)를 나타낼 때 사용한다.

☁️ 비정적 멤버 클래스

비정적 멤버 클래스와 바깥 인스턴스 사이의 관계는 멤버 클래스가 인스턴스화될 때 확립되며, 더이상 변경 불가능하다. 아래 예제 코드에서 정규화된 this 를 사용하여 바깥 클래스의 참조를 가져온 것을 볼 수 있다.

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();
        }
    }
}

그렇다면 비정적 멤버 클래스는 주로 어디에 사용하면 좋을까?

비정적 멤버 클래스는 주로 멤버 클래스에서 바깥 인스턴스에 접근해야 하는 어댑터를 정의할 때 자주 쓰인다. 어댑터란, 특정 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 뷰를 의미한다.

예컨대, Map 인터페이스의 KeySet 과 같은 컬렉션 뷰를 구현할 때 비정적 멤버 클래스를 사용한다.

Set이나 List같은 컬렉션 인터페이스도 반복자를 구현할 때 비정적 멤버 클래스를 주로 사용한다.

public class MySet<E> extends AbstractSet<E> {
	@Override public Iterator<E> iterator(){
    	return new MyIterator();
    }
    
    private class MyIterator implements Iterator<E>{  //비정적 멤버 클래스
    	...
   }
}

결론은 멤버 클래스에서 바깥 인스턴스에 접근할 일이 없다면, 무조건 static 을 붙여서 정적 멤버 클래스로 만들어야 한다는 것이다.

☁️ 익명 클래스

익명 클래스는 이름이 없는, 바깥 클래스의 멤버도 아닌 클래스이다. 멤버와 달리 쓰이는 시점에 선언과 동시에 인스턴스가 만들어지기 때문이다.

또한 익명 클래스는 오직 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스를 참조 할 수 있다.

public class AnonymousExample {
    private double x;
    private double y;


    public double operate() {
        Operator operator = new Operator() {  //익명 클래스
            @Override
            public double plus() {
                System.out.printf("%f + %f = %f\n", x, y, x + y);
                return x + y;
            }

            @Override
            public double minus() {
                System.out.printf("%f - %f = %f\n", x, y, x - y);
                return x - y;
            }
        };
        
        return operator.plus();
    }
}

interface Operator {
    double plus();

    double minus();
}

익명 클래스 제약

  • 선언한 지점에서만 인스턴스를 만들수 있다.
  • instanceof 검사, 클래스의 이름이 필요한 작업을 수행할 수 없다.
  • 여러 인터페이스를 구현할 수 없다.
  • 구현한 익명클래스는 다른 클래스를 상속할 수 없다.
  • 익명 클래스를 사용하는 클라이언트는 사용하는 익명 클래스가 상위 타입에서 상속한 멤버 외에는 호출할 수 없다.
  • 표현식 중간에 등장하기 때문에 10줄이 넘어가면 가독성이 나빠진다.

익명 클래스의 사용

자바가 람다를 지원하기 이전에는 즉석에서 작은 함수 객체나 처리 객체를 만드는데 익명 클래스를 사용했지만, 이제는 람다가 그 역할을 대신하고 있다.(아이템 42)

또한, 익명 클래스는 정적 팩터리 메서드를 구현할때도 주로 사용된다.

☁️ 지역 클래스

지역 클래스는 지역변수를 선언할 수 있는 곳이면 어디서든 선언 가능하며, 유효 범위도 지역번수와 같다.

다른 세 중첩 클래스의 공통점도 하나씩 가지고 있는데, 1)멤버 클래스처럼 이름이 있고 반복해서 사용 가능하며, 2)익명 클래스처럼 비정적 문맥에서 사용도리 때만 바깥 인스턴스를 참조 가능하고 3)정적 멤버는 가질 수 없으며, 가독성을 위해 짧게 작성해야 한다.

public class LocalExample {
    private int number;

    public LocalExample(int number) {
        this.number = number;
    }

    public void foo() {
        // 지역변수처럼 선언해서 사용할 수 있다.
        class LocalClass {
            private String name;

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

            public void print() {
                // 비정적 문맥에선 바깥 인스턴스를 참조 할 수 있다.
                System.out.println(number + name);
            }

        }

        LocalClass localClass = new LocalClass("local");

        localClass.print();
    }
}

📚 핵심 정리
중첩 클래스에는 네 가지가 있으며, 각각의 쓰임이 다르다. 메서드 밖에서도 사용해야 하거나 메서드 안에 정의하기엔 너무 길다면 멤버 클래스로 만든다. 멤버 클래스의 인스턴스 각각이 바깥 인스턴스를 참조한다면 비정적으로, 그렇지 않다면 정적으로 만들자. 중첩 클래스가 한 메서드 안에서만 쓰이면서 그 인스턴스를 생성하는 지점이 단 한 곳이고 해당 타입으로 쓰기에 적합한 인터페이스나 클래스가 이미 있다면 익명 클래스로 만들고, 그렇지 않으면 지역 클래스로 만들자.

참고 자료
https://javabom.tistory.com/46

profile
개인용으로 공부하는 공간입니다. 잘못된 부분은 피드백 부탁드립니다!

0개의 댓글