Language/Java

[이펙티브자바] 아이템 22, 23, 24 요약 정리

anxi 2024. 6. 6. 00:03

아이템 22. 인터페이스는 타입을 정의하는 용도로만 사용하라

인터페이스는 자신을 구현할 클래스의 인스턴스를 참조할 수 있는 "타입" 역할을 한다.
클래스가 어떤 인터페이스를 구현한다는 것은 자신의 인스턴스로 무엇을 할 수 있는지를 클라이언트에게 알려주는 것!

 

위 용도로 인터페이스를 사용해야 한다.

 

안티 패턴 - 상수 인터페이스

static final 필드로만 가득 찬, 메서드는 없는 인터페이스

public interface NumberConstants {
	
    static final double ONE_NUMBER = 1.111111;
    static final double TWO_NUMBER = 2.222222;
    static final double THREE_NUMBER = 3.333333;
}

 

  • 클래스 내부에서 사용하는 상수는 외부 인터페이스가 아닌 내부 구현에 해당한다.
    따라서 상수 인터페이스를 구현하는 것은 내부 구현을 클래스의 API로 노출하는 행위다.
  • 클라이언트 코드가 내부 구현에 해당하는 상수들에 종속되게 한다.
  • 상수를 더이상 쓰지 않더라도 바이너리 호환성을 위해 상수 인터페이스를 구현하고 있어야 한다.

상수 공개 방법

  • 특정 클래스나 인터페이스와 강하게 연관된 상수라면 그 클래스나 인터페이스 자체에 추가한다.
    • ex) Integer와 Double에 선언된 MIN_VALUE, MAX_VALUE 상수
  • 열거 타입으로 나타내기 적합한 상수라면 열거 타입으로 만들어 공개한다.
  • 인스턴스화 할 수 없는 유틸리티 클래스에 담아 공개한다.
public class NumberConstants {

    // 유틸 클래스이므로 인스턴스화 방지
    private NumberConstants() {}
	
    public static final double ONE_NUMBER = 1.111111;
    public static final double TWO_NUMBER = 2.222222;
    public static final double THREE_NUMBER = 3.333333;
}

 

  • 유틸 클래스에서 정의된 상수를 사용하려면 클래스 이름까지 함께 명시하여 사용한다.
    -> NumberConstants.ONE_NUMBER...
  • 유틸 클래스의 상수를 자주 사용하면 정적 임포트(static import)를 하여 클래스 이름을 생략한다.

아이템 23. 태그 달린 클래스보다는 클래스 계층 구조를 활용하라

public class Figure {

    enum Shape {RECTANGLE, CIRCLE};

    // 태그 필드
    final Shape shape;
    
    // 사각형 필드
    double length;
    double width;
    
    // 원용 필드
    double radius;

    // 사각형 생성자
    public Figure(double length, double width) {
        this.shape = Shape.RECTANGLE;
        this.length = length;
        this.width = width;
    }
    
    // 원 생성자
    public Figure(double radius) {
        this.shape = Shape.CIRCLE;
        this.radius = radius;
    }
    
    double area() {
        switch (shape) {
            case RECTANGLE:
                return length * width;
            case CIRCLE:
                return Math.PI * (radius * radius);
            default:
                throw new AssertionError(shape);
        }
    }
}

 

태그 달린 클래스

두 가지 이상 의미를 표현할 수 있으며, 그중 현재 표현하는 의미를 태그 값으로 알려주는 클래스

 

단점

  • 가독성이 나쁘다. -> 열거 타입 선언, 태그 필드, switch문 등 쓸데 없는 코드가 많다. (+ 메모리 많이 사용)
  • 필드들을 final로 선언하려면 해당 의미에 쓰이지 않는 필드들까지 생성자에서 초기화해야 한다.
  • 또 다른 의미를 추가하려면 코드를 수정해야 한다. (관여되는 모든 코드 수정해야 함)
  • 인스턴스의 타입(열거)만으로 현재 나타내는 의미를 알 수 없다.

개선

태그 달린 클래스를 클래스 계층구조로 바꾸자.

 

  • 계층 구조의 루트가 될 추상 클래스를 정의하고, 태그 값에 따라 동작이 달라지는 메서드들을 추상 메서드로 선언한다.
  • 태그 값에 상관 없이 동작이 일정한 메서드들을 루트 클래스에 일반 메서드로 추가한다.
    • 결과 : 루트 클래스에선 추상 메서드만 남게 된다.
  • 루트 클래스를 확장한 구체 클래스를 의미별로 정의한다.
  • 루트 클래스가 정의한 추상 메서드를 각자 의미에 맞게 구현한다.
// 추상클래스
// 루트 클래스
abstract class Figure {
    // 태그에 따라 달라지는 메서드
    abstract double area();
}

// 구체 클래스 : 원
class Circle extends Figure {
    final double radius;

    public Circle(double radius) {
        this.radius = radius;
    }

    @Override
    double area() {
        return Math.PI * (radius * radius);
    }
}

// 구체 클래스 : 사각형
class Rectangle extends Figure {
    final double length;
    final double width;

    public Rectangle(double length, double width) {
        this.length = length;
        this.width = width;
    }

    @Override
    double area() {
        return length * width;
    }
}

장점

  • 간결하고 명확하다. (쓸데없는 코드 모두 사라짐)
  • 살아 남은 필드는 모두 final 이다.
  • 각 클래스의 생성자가 모든 필드를 남김 없이 초기화하고 추상 메서드를 모두 구현했는지 컴파일러가 확인해준다.
  • 루트 클래스의 코드를 건들지 않고 독립적으로 계층 구조를 확장하고 함께 사용할 수 있다.
  • 타입이 의미별로 따로 존재하기 때문에 변수의 의미를 명시하거나 제한할 수 있고, 특정 의미만 매개변수로 받을 수 있다.

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

중첩 클래스 (nested class)

  • 다른 클래스 안에 정의된 클래스
  • 자신을 감싼 바깥 클래스에서만 쓰여야 하며, 그 외의 쓰임새가 있다면 톱레벨 클래스로 만들어야 한다.
  • 종류
    • 정적 멤버 클래스, <내부 클래스 3가지> { (비정적) 멤버 클래스, 익명 클래스, 지역 클래스 }

정적 멤버 클래스

  • 바깥 클래스와 함께 쓰일 때만 유용한 public 도우미 클래스
  • ex) 계산기와 연산 종류(정적 멤버 클래스)
    Calculator(클래스)의 정적 멤버 클래스인 Operation(열거 타입) 

    Calculator.Operation.PLUS, Calculator.Operation.MINUS와 같이 연산 참조 가능
class Outer {

  private final String house;
  private final String door;

  public static class Builder {
    private final String house;

    private String door = "";

    public Builder(String house) {
      this.house = house;
    }

    public Builder door(String d) {
      this.door = d;
      return this;
    }

    Outer build() {
      return new Outer(this);
    }
  }

  private Outer(Builder builder) {
    house = builder.house;
    door = builder.door;
  }
}

Outer outer = new Outer.Builder("아파트").door("문 4개").build();

비정적 멤버 클래스

  • 정적 멤버 클래스와 차이 : static 유무
  • 바깥 클래스의 인스턴스 없이 존재할 수 없다.
  • 비정적 멤버 클래스의 인스턴스 메서드에서 정규화된 this를 사용해 바깥 인스턴스의 메서드를 호출하거나 바깥 인스턴스의 참조를 가져올 수 있다.
    • 정규화된 this : 클래스명.this 형태로 바깥 클래스의 이름을 명시하는 용법
public class Outer {
    String house = "아파트";

    public void build() {
        Inner inner = new Inner();
        inner.build();
    }

    public class Inner {
        public void build() {
            System.out.println(Outer.this.house);
        }
    }
}

 

결론

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

 

static을 생략하면 바깥 인스턴스로의 숨은 외부 참조를 갖게 된다. => 참조 저장시 시간과 공간 소비, GC가 바깥 클래스의 인스턴스를 수거하지 못하는 경우 발생 (메모리 누수)

 

익명 클래스

  • 이름이 없다.
  • 바깥 클래스의 멤버도 아니다. 쓰이는 시점에 선언과 동시에 인스턴스가 만들어진다. (한번만 사용 가능)
  • 코드의 어디서든 만들 수 있다.
  • 비정적인 문맥에서 사용될 때만 바깥 클래스의 인스턴스를 참조할 수 있다.
  • 정적 문맥에서라도 상수 변수 이외의 정적 멤버는 가질 수 없다.
  • 익명 클래스에 새롭게 정의된 필드와 메서드는 익명 클래스 내부에서만 사용 가능하다.
  • 람다가 나오면서 익명 클래스를 대체했다.
public class Anonymous {
    public void log() {
    }
}

public class Main {
    public static void main(String[] args) {
        Anonymous anonymous = new Anonymous() {
            // 내부 클래스에서만 사용 가능
            public void removeLog() {
                log.info("---------");
            }

            @Override
            public void log() {
                log.info("log");
                removeLog();
            }
        };
        
        //anonymous.removeLog(); 호출 불가
        anonymous.log();
    }
}

지역 클래스

  • 가장 드물게 사용된다.
  • 지역변수를 선언할 수 있는 곳에서 어디서든 선언할 수 있다. (유효 범위도 지역 변수와 같다.)
  • 멤버 클래스처럼 이름이 있고 반복해서 사용할 수 있다.
  • 익명 클래스처럼 비정적 문맥에서 사용될 때만 바깥 인스턴스를 참조할 수 있다.
  • 정적 멤버는 가질 수 없으며 가독성을 위해 짧게 작성해야 한다.
public class Outer {
	private String outer = "outer";
    
    public void do() {
    	// 지역변수 자리
    	class Local {
        	public void localDo() {
            	String local = "local";
                System.out.println(outer + local);
            }
        }
        
        Local local = new Local();
        local.localDo();
    }
}

최종

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