JAVA/Effective Java

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

꾸준함. 2024. 2. 8. 09:47

중첩 클래스(nested class)란 다른 클래스 안에 정의된 클래스를 지칭하며 다음과 같이 네 가지 종류가 있습니다.

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

 

1. 정적 멤버 클래스

  • 정적 멤버 클래스는 바깥 클래스의 private 멤버에도 직접 접근이 가능하며 이외에는 일반 클래스와 같음
  • 정의되어 있는 scope 범위가 클래스
  • 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스
  • 바깥 클래스와 독립적이기 때문에 바깥 클래스 인스턴스 필요하지 않음

 

이전에 언급한 대로 OuterClass를 통해 함께 사용될 때 유용하며, 아래의 코드처럼 계산기 내 연산을 담당하는 enum 클래스를 선언하는 것이 대표적인 예시입니다.

  • 계산기의 연산 작업을 수행하는 Operation 클래스는 계산기와 연관이 있지만 계산기에서 제공하는 필드 혹은 메서드를 직접 사용하지 않음
  • 바깥 클래스인 Calculator와 독립적이기 때문에 바깥 클래스 인스턴스가 필요하지 않음 

 

 

public class Calculator {
public enum Operation {
PLUS(Integer::sum),
MINUS((x, y) -> x - y),
MULTIPLY((x, y) -> x * y),
DIVIDE((x, y) -> x / y);
private BiFunction<Integer, Integer, Integer> calculate;
Operation(BiFunction<Integer, Integer, Integer> calculate) {
this.calculate = calculate;
}
public BiFunction<Integer, Integer, Integer> getFunction() {
return calculate;
}
}
public int add(int x, int y) {
return Operation.PLUS.getFunction().apply(x, y);
}
public int subtract(int x, int y) {
return Operation.MINUS.getFunction().apply(x, y);
}
public int multiply(int x, int y) {
return Operation.MULTIPLY.getFunction().apply(x, y);
}
public int divide(int x, int y) {
return Operation.DIVIDE.getFunction().apply(x, y);
}
public static void main(String[] args) {
Calculator calculator = new Calculator();
int x = 4, y = 2;
System.out.println(String.format("덧셈 결과: %s", calculator.add(x, y)));
System.out.println(String.format("뺄셈 결과: %s", calculator.subtract(x, y)));
System.out.println(String.format("곱셈 결과: %s", calculator.multiply(x, y)));
System.out.println(String.format("나눗셈 결과: %s", calculator.divide(x, y)));
}
}
view raw .java hosted with ❤ by GitHub

 

 

2. 비정적 멤버 클래스

  • 비정적 멤버 클래스의 인스턴스는 바깥 클래스 인스턴스와 암묵적으로 연결되어 있음
  • 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this(클래스명.this 형태로 바깥 클래스의 이름을 명시하는 용법)를 사용해 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있음
  • 멤버 클래스에서 바깥 인스턴스를 참조할 필요가 없다면 정적 멤버 클래스로 만드는 것을 권장
    • 바깥 인스턴스로의 숨은 외부 참조가 생겨 참조를 저장할 때 시간과 메모리가 소비
    • GC가 바깥 클래스의 인스턴스를 수거하지 못해 메모리 누수 발생 가능
    • 참조가 가시적으로 보이지 않기 때문에 문제의 원인을 파악하는 것이 쉽지 않고 때로는 심각한 상황 초래 가능

 

  • 비정적 멤버 클래스와 바깥 인스턴스 사이의 관계는 멤버 클래스가 인스턴스화될 때 확립되며 추후 변경 불가능
  • 어댑터를 정의할 때 자주 쓰이는 중첩 클래스
    • 어떤 클래스의 인스턴스를 감싸 마치 다른 클래스의 인스턴스처럼 보이게 하는 view로 사용
    • Map 인터페이스의 구현체는 자신의 컬렉션 뷰를 구현할 때 비정적 멤버 클래스 사용
    • 비슷하게 Set과 List 같은 다른 컬렉션 인터페이스 구현체들도 자신의 iterator를 구현할 때 비정적 멤버 클래스 주로 사용

 

어댑터 패턴

 

  • 기존에 정의한 코드가 클라이언트에서 사용하는 인터페이스와 호환이 되지 않을 때 해당 인터페이스와 호환이 될 수 있도록 적합하게 변형해 주는 디자인 패턴
    • 클라이언트가 사용하는 인터페이스를 따르지 않는 기존 코드를 재사용할 수 있게 해줌
  • 호환성이 없는 인터페이스를 갖는 두 클래스를 함께 동작할 수 있게끔 하는 데 사용
  • 주로 라이브러리나 프레임워크의 재사용성을 높이기 위해 쓰임

 

다음은 어댑터 패턴의 간단한 예시입니다.

 

 

// 기존 시스템의 클래스
class Square {
public void drawSquare() {
System.out.println("정사각형을 그린다.");
}
}
// 새로운 시스템에서 사용하려는 인터페이스
interface Rectangle {
void drawRectangle();
}
// Rectangle 인터페이스에 맞게 동작하도록 Square 클래스를 어댑트하는 어댑터 클래스
class SquareAdapter implements Rectangle {
private Square square;
public SquareAdapter(Square square) {
this.square = square;
}
@Override
public void drawRectangle() {
// Square 클래스의 drawSquare 메서드를 Rectangle 인터페이스의 drawRectangle에 맞게 호출
square.drawSquare();
}
public static void main(String[] args) {
// 기존의 Square 클래스 인스턴스 생성
Square square = new Square();
// 어댑터를 통해 Square를 Rectangle로 사용
Rectangle squareAdapter = new SquareAdapter(square);
// 클라이언트 코드에서는 Rectangle 인터페이스의 메서드를 호출
squareAdapter.drawRectangle();
}
}
view raw .java hosted with ❤ by GitHub

 

 

 

비정적 멤버 클래스를 사용한 어댑터 패턴은 아래와 같습니다.

 

 

class Person {
private String name;
public Person(String name) {
this.name = name;
}
public String getName() {
return name;
}
}
class PersonList implements Iterable<Person> {
private List<Person> people;
public PersonList() {
this.people = new ArrayList<>();
}
public void addPerson(Person person) {
people.add(person);
}
// 비정적 멤버 클래스
private class PersonIterator implements Iterator<Person> {
private int index = 0;
@Override
public boolean hasNext() {
return index < people.size();
}
@Override
public Person next() {
if (hasNext()) {
return people.get(index++);
} else {
throw new IndexOutOfBoundsException("리스트에 더 이상 사람이 없습니다.");
}
}
}
@Override
public Iterator<Person> iterator() {
return new PersonIterator();
}
public static void main(String[] args) {
PersonList personList = new PersonList();
personList.addPerson(new Person("봉준호"));
personList.addPerson(new Person("손흥민"));
personList.addPerson(new Person("J-Park"));
Iterator<Person> iterator = personList.iterator();
while (iterator.hasNext()) {
Person person = iterator.next();
System.out.println(String.format("이름: %s", person.getName()));
}
}
}
view raw .java hosted with ❤ by GitHub

 

 

3. 익명 클래스

  • 익명 클래스는 바깥 클래스의 멤버가 아님
  • 멤버와 달리 쓰이는 시점과 동시에 인스턴스가 생성됨
  • 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스를 참조 가능
  • 정적 팩토리 메서드를 구현할 때 익명 클래스 사용
  • 자바에서 람다를 사용하기 전에 주로 사용되었으며 람다가 지원되는 자바 8+ 버전부터는 거의 사용되지 않음
    • 람다 표현식은 단일 추상 메서드(SAM, Single Abstract Method)를 가진 인터페이스의 구현에 사용
    • 메서드가 많은 경우에는 해당 인터페이스가 람다로 적합하지 않을 수 있음

 

3.1 익명 클래스 제약 사항

 

  • 선언한 지점에서만 인스턴스를 생성할 수 있으며 instanceof 검사나 클래스명이 필요한 작업은 수행 불가능
  • 익명 클래스를 사용하는 클라이언트는 해당 익명 클래스의 상위 타입에서 상속한 멤버 외에는 호출 불가능
  • 여러 인터페이스를 구현할 수 없고 인터페이스를 구현하는 동시에 다른 클래스를 상속할 수도 없음
  • 익명 클래스는 표현식이 중간에 등장하기 때문에 짧지 않으며 가독성이 떨어짐

 

익명 클래스 예시

 

 

Collections.sort(list, new Comparator<Integer>() {
@Override
public int compare(Integer o1, Integer o2) {
return Integer.compare(o1, o2);
}
});
view raw .java hosted with ❤ by GitHub

 

 

Java 8+ 이후 람다 적용

 

Collections.sort(list, (o1, o2) -> Integer.compare(o1, o2));
Collections.sort(list, Comparator.comparingInt(o -> o));
view raw .java hosted with ❤ by GitHub

 

 

4. 지역 클래스

  • 가장 드물게 사용됨
  • 지역 변수를 선언하는 곳이면 어디든 지역 클래스를 정의해서 사용 가능하며 scope 범위도 지역 변수와 같음
  • 멤버 클래스처럼 이름이 존재하며 반복해서 사용 가능
  • 익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있으며 정적 멤버는 가질 수 없음
  • 가독성을 위해 짧게 작성 권장

 

지역 클래스 예시


public class LocalClassExample {
public static void main(String[] args) {
Thread thread = new Thread(() -> {
try {
for (int i = 0; i <10; i++){
Thread.sleep(1000);
System.out.println(i);
}
} catch (InterruptedException e) {
e.printStackTrace();
}
});
thread.start();
}
}
view raw .java hosted with ❤ by GitHub

 

정리

중첩 클래스에는 네 가지 종류가 있으며 각각의 용도가 다릅니다.

만약 중첩 클래스를 메서드 외부에서 사용해야 하거나 해당 메서드 내에 정의하기에는 길다면, 멤버 클래스로 생성하는 것이 좋습니다.

또한, 멤버 클래스의 각 인스턴스가 외부 인스턴스를 참조해야 한다면 비정적으로, 그렇지 않다면 정적으로 생성하는 것이 권장됩니다.

만약 중첩 클래스가 특정 메서드에서만 사용되며 해당 인스턴스를 생성하는 지점이 단 하나이고 이미 적합한 클래스나 인터페이스가 존재한다면 익명 클래스로 생성하는 것이 좋습니다.

그렇지 않은 경우, 지역 클래스로 생성하는 것을 권장합니다.

 

참고

이펙티브 자바
이펙티브 자바 완벽 공략 2부 - 백기선 강사님

https://github.com/Meet-Coder-Study/book-effective-java/blob/main/4%EC%9E%A5/24_%EB%A9%A4%EB%B2%84%20%ED%81%B4%EB%9E%98%EC%8A%A4%EB%8A%94%20%EB%90%98%EB%8F%84%EB%A1%9D%20static%EC%9C%BC%EB%A1%9C%20%EB%A7%8C%EB%93%A4%EB%9D%BC_jiae.md

https://tecoble.techcourse.co.kr/post/2020-11-05-nested-class/

 

정적, 비정적 내부 클래스 알고 사용하기

자바의 중첩 클래스(Nested Class)에는 여러 가지 종류가 있는데 그중 정적 내부 클래스와 비정적 내부 클래스에 대해 다뤄보고자 한다. 글에서 사용된 코드는 Github…

tecoble.techcourse.co.kr

 

반응형