Effective Java를 읽고 정리한 정리본입니다.

✨ 자바 기본

자바가 지원하는 타입은 다음과 같다.

  1. 인터페이스
  2. 클래스
  3. 배열
  4. 기본 타입

여기서 애노테이션은 인터페이스의 일종이고, enum, record는 클래스의 일종이다.

4번을 제외한 나머지는 참조 타입이며, 클래스의 인스턴스와 배열은 객체이다.

클래스 멤버

  1. 필드
  2. 메서드
  3. 멤버 클래스
  4. 멤버 인터페이스

이 책에서 말하는 API는 흔히 쓰는 인터페이스로, 프로그래머가 클래스, 인터페이스, 패키지를 통해 접근할 수 있는 모든 클래스, 인터페이스, 생성자, 멤버, 직렬화된 형태를 말한다.

📌 아이템 1 : 생성자 대신 정적 팩터리 메서드를 고려하라

이번 챕터의 아이템 1에서는 클래스의 인스턴스를 얻는 가장 기본적인 수단인 public 생성자 대신 (혹은 생성자와 함께) 정적 팩터리 메서드를 사용하는 방안을 제시하고 있다.

🫧 정적 팩터리 메서드

✨ 정의

정적 팩터리 메서드는 객체의 생성을 담당하는 클래스 메서드이다.

가장 큰 특징으로는 static 키워드를 사용하며, 흔히 볼 수 있는 new 키워드를 통해 객체를 직접적으로 생성하는 것이 아닌, 메서드 호출 방식으로 객체를 생성한다.

💡 정적 팩터리 메서드 클래스 내에 선언되어 있는 메서드를 통해 new를 간접적으로 사용한다는 사실을 알고 넘어가자.

✨ 예시

public static Boolean valueOf(boolean b) {
  return b ? Boolean.TRUE : Boolean.FALSE;
}

위 팩터리 메서드는 boolean 기본 타입의 값을 받아 박싱 클래스인 Boolean 객체 참조로 변환하는 예이다.

✨ 장점

1. 이름을 가질 수 있다.

  • 객체의 특성을 한 눈에 파악할 수 있다.

2. 호출될 때마다 인스턴스를 새로 생성하지 않아도 된다.

  • 불필요한 객체 생성 방지
  • 플라이웨이트 패턴도 이와 비슷한 기법이다.

또한, 정적 팩터리 방식의 클래스는 인스턴스 통제 클래스가 될 수도 있다.

💡 인스턴스 통제 클래스 : 언제, 어느 인스턴스를 살아있게 할지를 통제할 수 있는 클래스

ex) 반복되는 요청에 같은 객체를 반환하는 클래스 -> enum 타입

3. 반환 타입의 하위 객체를 반환할 수 있다.

  • API를 작게 유지

예시의 출처는 다음과 같다.

서비스 제공자 프레임워크 (웨지의 개발 블로그)

아래 코드는 이름을 받아 객체를 반환하는 예시이다.

public class Hamburger {
  
  private final Map<String, Ingredient> ingredients = new HashMap<>();

  public Hamburger() {
  }

  public static Hamburger of(String name) {
    switch (name) {
      case "hamburger" -> {
        return new Hamburger();
      }
      default -> throw new IllegalStateException("Unexpected value: " + name);
    } 
  }
}

확장하지 않은 상태에서, 코드로 호출하게 되면 다음과 같은 일이 일어난다.

Hamburger hamburger2 = Hamburger.of("hamburger");

이 코드는 Hamburger이라는 객체를 반환하게 된다.

그러나 이를 확장시켜 “치즈버거”를 추가하게 된다면 코드는 다음과 같이 변할 것이다.

public class Hamburger {
  
  private final Map<String, Ingredient> ingredients = new HashMap<>();

	private Humburger(){}

  public static Hamburger of(String name) {
    switch (name) {
      case "hamburger" -> {
        return new Hamburger();
      }
			case "cheese" -> {
				return new ChesseBurger();
			}
      default -> throw new IllegalStateException("Unexpected value: " + name);
    } 
  }
}

name이 cheese이면 Hamburger이 아닌 chesseBurgger을 반환하게 된다.

Hamburger cheeseBurger2 = Hamburger.of("cheese");

이 코드는 이름이 cheese인 CheeseBugger을 반환하게 된다. 이처럼, CheeseBugger이라는 객체를 따로 써 주지 않고도 반환 타입의 하위 타입 객체를 반환하며 API를 작게 유지할 수 있다.

API가 작아짐에 따라 원하는 기능들만 골라 쓸 수 있고, 프로그래머가 API를 사용하기 위해 익혀야 하는 개념의 수와 난이도를 낮출 수 있다.

4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.

  • 클라이언트가 클래스의 존재를 몰라도, 설계에 따라 각기 다른 클래스의 객체를 반환할 수 있다.

예시, 원소가 64개 이하면 원소들을 long 변수 하나로 관리하는 RegularEnumSet의 인스턴스를, 65개 이상이면 long 배열로 관리하는 JumboEnumSet 인스턴스 반환.

크기에 따라 다른 클래스를 반환함으로써 API를 작게 유지할 수 있다.

이를 통해 알 필요 없는 api를 숨길 수도 있고, 재사용도 할 수 있다.

5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.

이러한 특징은 서비스 제공자 프레임워크의 근간이 되고 있다.

서비스 제공자 프레임워크

서비스 제공자 프레임워크의 핵심 컴포넌트는 다음과 같다.

  1. 서비스 인터페이스 : 구현체 동작 정의
  2. 제공자 등록 API : 제공자가 구현체를 등록할 때 사용
  3. 서비스 접근 API : 클라이언트가 서비스 인스턴스를 얻을 때 사용

여기서의 서비스 접근 API가 바로 서비스 제공자 프레임워크의 근간이라고 하는 “유연한 정적 팩터리”의 실체이다.

추가로, 서비스 제공자 인터페이스 컴포넌트가 쓰이기도 한다. 이는 서비스의 인터페이스를 생성하는 팩터리 객체를 설명해준다.

책에서는 예시로 대표적인 서비스 제공자 프레임워크인 “JDBC”에 대해 설명하고 있다. JDBC를 예로 들어 서비스 제공자 프레임워크 & 5번 장점을 다시 한 번 설명해 보고자 한다.

우선 JDBC가 무엇인지 짚고 넘어가자.

JDBC는 Java를 사용해 데이터베이스에 접속할 수 있는 API를 제공하는 프레임워크로, “서비스 제공자 프레임워크”의 관점에서 보면 JDBC의 서비스는 “Java”를 통해 DB와 소통할 수 있도록 커넥션을 만드는 것이라고 할 수 있겠다.

JDBC는 Java를 통해 DB와 소통할 수 있도록 커넥션을 만들어야 하므로, 여러 종류의 DB를 커넥션할 필요가 있을 것이다.

이 여러 종류의 DB는 새로 생겼다 없어지는 등의 변경이 잦다. 따라서, 계속해서 객체 생성을 위한 코드를 추가하는 것이 아닌, 서비스를 제공하는 자 (ex, DB 회사)가 직접 서비스를 구현하여 등록하면, 사용자가 쓸 수 있도록 하는 것이다.

이러한 프레임워크가 바로 “서비스 제공자 프레임워크”이다.

이를 JDBC 입장에서 재설명한다면, 핵심 컴포넌트는 다음과 같이 설명될 것이다.

  1. 서비스 인터페이스
    • 커넥션 객체 생성 인터페이스 정의 (서비스 접근 API 역할)
  2. 제공자 등록 api : 제공자가 구현체를 등록할 때 사용
    • 각 DB 회사가 구현체를 구현하면, 일일이 등록 해야 함.
  3. 서비스 접근 API : 사용자가 서비스 제공자의 구현체를 얻을 수 있게 해 주는 API
    • DB 회사에서 DBDriver 인터페이스를 구현하여 배포하고, 사용자는 해당 Driver을 다운 받아 간단한 설정을 해 주기만 하면 사용이 가능하다.
    • DB가 추가될 때마다 반환할 객체의 클래스를 추가하는 것이 아닌, 작성하는 시점에서 미리 만들어두면 정적 팩터리 메서드의 특징에 따라 나중에 클래스를 추가하여 오류 없이 이용할 수 있다.

따라서, 정적 팩터리 메서드를 작성하는 시점 (JDBC 개발) 에는 반환할 객체 (나중에 새로 만들 DB Driver)의 클래스가 존재하지 않아도 추후에 추가해 사용이 가능하므로 확장에 용이해지는 것이다.

💡 ServiceLoader

  • 자바 6부터 제공되는 범용 서비스 제공자 프레임워크
  • 서비스 제공자가 구현체를 구현만 해 두면 자동으로 등록할 수 있는 메커니즘.

참고로, JDBC는 자바 6 이전에 등장했기 때문에 ServiceLoader을 사용하지 않는다.

하지만 이러한 정적 팩터리 메서드에도 단점이 있다.

✨ 단점

  1. 정적 팩터리 메서드만으로 하위 클래스를 만들 수 없다.
    • 상속을 하려면 public or protected 생성자가 필요하다.
    • 그러나 상속 대신 컴포지션 사용으로 유도한다는 점에서 장점이 될 수 있다.

💡 컴포지션 : 다른 객체의 인스턴스를 자신의 인스턴스 변수로 포함해서 메서드를 호출하는 기법

예시

public class House {
	private Bedroom bedroom;
    private LivingRoom livingRoom;
  // ... etc
}

컴포지션에 대한 자세한 이야기는 아이템 18에서 다룰 예정이다.

  1. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.

정적 팩터리 메서드와 public 생성자의 장단점에 대해 올바르게 알고 사용하는 것이 무엇보다 중요할 것이다. 책에서는 정적 팩터리를 사용하는 게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있으면 고치는 것이 좋다고 말한다.

📌 추가 공부 및 질문

🫧 디자인 패턴에서의 팩터리 메서드 vs 정적 팩터리 메서드

🫧 정적 팩터리 메서드 위에도 빌더 애노테이션을 붙일 수 있을까?

🫧 불변 클래스 내부는 모두 불변 객체만 존재하는가?

🫧 플라이웨이트 패턴

🫧 Record 클래스도 인스턴스 통제 클래스라 할 수 있는가?

🫧 API vs 라이브러리

🫧 인스턴스화 불가 클래스

🫧 private에 애노테이션을 붙일 수 없는 까닭은?

  • 실제로 프로젝트를 진행하며, 스프링에서 private에 @Transactional을 붙일 수 없다는 것을 깨달았다. 애노테이션이 인터페이스이기 때문에 이러한 오류가 난 것일까?

🫧 반환하는 정적 메서드가 필요한 경우 인스턴스화가 가능한 동반 클래스는 두지 못하는가?

🫧 브리지 패턴

🫧 의존 객체 주입 (의존성 주입) 프레임워크

  • 의존 객체 주입 (의존성 주입) 프레임워크는 서비스 제공자 프레임워크 중 하나이다. 세 가지 핵심 컴포넌트의 입장에서 바라보자.

🫧 상속을 하려면 왜 public or protected 생성자가 필요한가?

기본적으로 java에서는 private이 그 클래스 내부에서만 쓰일 수 있다.

자바에서 상속이란 기존의 클래스를 재활용하여 새로운 클래스를 작성하는 자바의 문법 요소이다.

상속은 상위 클래스와 하위 클래스로 나누어 상위 클래스의 멤버(필드, 메서드, 이너 클래스)를 하위 클래스와 공유하기 때문에 그 클래스 내부에서만 쓸 수 있는 private 필드, 메서드, 이너 클래스는 당연하게도 상속이 불가능하다.

🫧 스프링에서의 “엔티티” 또한 컴포지션 기법을 사용한 것일까?

🫧 왜 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 알아 내야 하는가?

🫧 자바의 생성자

📌 참고 자료