저번주부터 이펙티브 자바스터디를 시작했다. 정리해오고 발표는 그때 그때 랜덤으로 돌리는 형식이며, 6만원 예치금에서 빠지면 2만원씩 차감이닼ㅋㅋㅋㅋㅋ
그래서 그런지 쫄리지만 도파민
도는 느낌 ~
열심히 해볼 예정임
이 책은 성능에 집중하는 부분은 많지 않다. 대신 프로그램을 명확하고, 정확하고, 유용하고, 견고하고, 유연하고, 관리하기 쉽게 짜는데 집중한다.
기술 용어는 대부분 자바8용 언어 명세를 따르며, 자바가 지원하는 타입은 인터페이스(interface), 클래스(class), 배열(array), 기본 타입(primitive) 총 네 가지다.
인터페이스의 일종
클래스의 일종
인터페이스, 클래스, 배열
즉, 클래스의 인스턴스와 배열은
객체(object)
인 반면, 기본 타입 값은 그렇지 않다.
메서드 시그니처
는 메서드 이름과 입력 매개변수(parameter)의 타입들로 이뤄진다.(반환값의 타입은 시그니처에 포함되지 않는다)
단, 일부 용어는 자바 언어 명세와 다르게 사용한다. 자바 언어 명세와는 달리 이 책은 상속(inheritance)을 서브클래싱(subclassing)과 동의로 쓴다.
인터페이스 상속 대신
✔️ 클래스가 인터페이스를 구현한다(implemetent);
✔️ 인터페이스가 다른 인터페이스를 확장한다(extend)
프로그래머가 클래스, 인터페이스, 패키지를 통해 접근할 수 있는 모든 클래스, 인터페이스, 생성자, 멤버, 직렬화된 형태(serialiozed form)을 말한다.
API를 사용하는 프로그램 작성자(사람)을 그 API의 사용자라고 함
API를 사용하는 클래스(코드)는 그 API의 클라이언트
객체의 생성과 파괴를 다룬 2장으로, 객체를 만들어야 할 때와 만들지 말아야 할 때를 구분하는 방법 등을 알아봄
클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 public 생성자
다.;
하지만 1가지 더 클래스는 생성자와 별도로 정적 팩터리 메서드(static factory method)를 제공할 수 있다.;
그 클래스의 인스턴스를 반환하는 단순한 정적 메서드 말이다.
이 메서드는 기본 타입인 boolean 값을 받아 Boolean 객체 참조로 변환해 준다.
public static Boolean valusOf(boolean b){
return b ? Boolean.TRUE : Boolean.FALSE;
}
✅ 여기서 얘기하는 정적 팩터리 메서드인 패턴에서의 팩터리 메서드(Factory Method)
와 다른다. 디자인 패턴 중에는 이와 일치하는 패턴은 없다.
public static Boolean valueOf(boolean b) {
return new Boolean(b); // 직접 객체를 생성
}
생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명 못하지만, 정적 팩토리의 경우, 이름만 잘 지으면 쉽게 묘사 가능하다.
뿐만 아니라, 하나의 시그니처로는 생성자를 하나만 만들 수 있다. 생성자가 어떤 역할을 하는 지 정확히 기억하기 어려워 엉뚱한 것을 호출하는 실수를 할 수 있다.
그렇기 떄문에 이름을 가질 수 있는 정적 팩터리 메서드를 쓰는 것이 좋다. 이런 제약이 없기 때문이다.;
한 클래스 에 시그니처가 같은 생성자가 여러 개 필요할 것 같으면, 생성자를 정적 팩터리 메서드로 바꾸고 각각의 차이를 잘 드러내는 이름을 지어주자
덕분에 불변 클래스(immutable class; 아이템 17는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용
하는 식으로 불필요한 객체 생성을 피할 수 있다.
따라서(특히 생성 비용이 큰) 같은 객체가 자주 요청되는 상황 이라면 성능을 끌어올려 준다. 플라이웨이트 패턴(Flyweight pattern)[^1]도 이와 비슷한 기법이라 할 수 있다.
인스턴스 통제(instance-controlled) 클래스
반복되는 요청에 같은 객체를 반환하는 식으로 정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 통제할 수 있다.
인스턴스를 통제하는 이유는?;
싱글턴 패턴(Singleton Pattern)
싱글턴 패턴은 클래스의 인스턴스가 오직 하나만 생성되도록 보장하는 디자인 패턴입니다. 특정 클래스의 객체가 하나만 존재해야 하거나, 동일한 자원에 대해 여러 객체가 동시에 접근할 필요가 없을 때 사용됩니다.
싱글턴 패턴을 사용하는 클래스는 private 생성자를 사용하여 외부에서 새로운 객체를 생성할 수 없도록 하고, 내부에서 하나의 객체만 생성하여 공유하는 방식으로 동작합니다. 이를 통해 메모리 낭비를 줄이고, 모든 호출자들이 동일한 인스턴스를 사용하게 됩니다.
public class Singleton {
// 유일한 인스턴스
private static final Singleton INSTANCE = new Singleton();
// private 생성자: 외부에서 인스턴스화 금지
private Singleton() {}
// 유일한 인스턴스를 반환하는 메서드
public static Singleton getInstance() {
return INSTANCE;
}
}
위 코드에서 getInstance()
메서드를 통해 객체가 오직 한 번만 생성되며, 동일한 객체가 모든 호출자에게 반환됩니다. 이 패턴은 메모리를 절약하고, 불필요한 객체 생성을 방지하여 성능을 최적화할 수 있습니다.
인스턴스화 불가
어떤 클래스를 인스턴스화 불가능하게 만들려면, 생성자를 private으로 선언하여 외부에서 그 클래스를 생성할 수 없도록 할 수 있습니다. 이는 특정 클래스가 유틸리티 클래스이거나, 그 자체로는 인스턴스화되지 않고 오직 static 메서드나 필드만 사용하도록 하기 위해 사용됩니다.
public class UtilityClass {
// private 생성자: 인스턴스화를 금지함
private UtilityClass() {
throw new AssertionError("Cannot instantiate this class.");
}
// static 메서드
public static void utilityMethod() {
// 유틸리티 기능을 제공
}
}
위와 같이 생성자를 private
으로 선언하여, UtilityClass
는 인스턴스화될 수 없습니다. 이 클래스는 오직 utilityMethod()
같은 정적 메서드를 제공하는 데 사용됩니다.
불변 클래스(Immutable Class)는 객체가 생성된 이후 그 상태가 절대 변경되지 않는 클래스입니다. 불변 클래스를 설계할 때, 동치인 인스턴스가 단 하나만 존재하도록 보장하는 것이 가능합니다. 이를 인스턴스 통제(instance control)라고 하며, 같은 값을 가진 인스턴스는 항상 동일한 인스턴스를 반환하는 방식으로 구현할 수 있습니다.
이를 통해 a == b
일 때만 a.equals(b)
가 성립하도록 보장할 수 있습니다. 즉, 값이 동일한 객체는 동일한 인스턴스를 사용하게 만들어서 메모리 낭비를 줄이고 성능을 향상시킵니다.
public final class Boolean {
public static final Boolean TRUE = new Boolean(true);
public static final Boolean FALSE = new Boolean(false);
private final boolean value;
private Boolean(boolean value) {
this.value = value;
}
public static Boolean valueOf(boolean value) {
return value ? TRUE : FALSE; // 이미 생성된 객체를 반환
}
}
위 코드에서 Boolean
클래스는 불변 클래스이며, valueOf()
메서드를 통해 true
또는 false
값에 따라 미리 정의된 TRUE
또는 FALSE
객체를 반환합니다. 이 방식은 값이 같으면 항상 같은 객체를 사용하므로, a == b
가 a.equals(b)
와 동일한 결과를 보장합니다.
플라이웨이트 패턴과 인스턴스 통제
플라이웨이트 패턴(Flyweight Pattern)은 동일한 객체를 여러 번 생성하지 않고, 동일한 상태를 공유함으로써 메모리와 성능을 최적화하는 디자인 패턴입니다. 플라이웨이트 패턴에서 인스턴스 통제가 중요한 이유는, 객체 생성이 비용이 많이 드는 경우에 동일한 값의 객체가 여러 개 생성되는 것을 방지하여 메모리 낭비를 줄이는 것이 목표이기 때문입니다.
예를 들어, 많은 문자를 출력하는 텍스트 편집기에서 각 문자를 객체로 표현한다면, 동일한 문자에 대해 매번 객체를 새로 생성하지 않고 하나의 객체를 재사용하도록 하여 메모리를 절약할 수 있습니다. 이때, 동일한 값을 가진 객체는 항상 같은 인스턴스를 재사용하게 됩니다.
플라이웨이트 패턴의 핵심은 객체를 가능한 한 많이 공유하여 메모리 낭비를 줄이는 것입니다. 인스턴스 통제는 이 패턴의 근간을 이루며, 이를 통해 객체 생성과 메모리 사용을 최적화합니다.
열거형(enum)과 인스턴스 하나만 생성 보장
자바에서 열거형(enum)은 자동으로 인스턴스 하나만 생성되도록 보장하는 구조를 가지고 있습니다. 열거형은 자바에서 싱글턴 패턴을 구현하는 가장 안전하고 간단한 방법 중 하나입니다.
열거형은 JVM에 의해 인스턴스가 한 번만 생성되며, 프로그램 내에서 해당 인스턴스를 재사용합니다. 따라서 각 열거형 상수는 하나의 인스턴스만 유지하며, 동일한 열거형 값에 대해서는 항상 같은 인스턴스가 반환됩니다.
public enum SingletonEnum {
INSTANCE;
public void someMethod() {
// 동작 정의
}
}
위의 SingletonEnum
클래스는 자바에서 열거형으로 구현된 싱글턴입니다. INSTANCE
는 프로그램 실행 중에 오직 하나의 인스턴스만 생성되어 사용됩니다. 따라서 열거형을 사용하면 인스턴스가 하나만 생성됨이 보장됩니다.
==
비교가 가능하게 만드는 방법.이 안에 있는 내용들은 뒤에 자세히 나옴
이 능력은 반환할 객체의 클래스를 자유롭게 선택할 수 있는 엄청난 유연성
을 선물한다. API를 만들 때 이 유연성을 응용하면 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있어 API를 작게 유지할 수 있다.
자바 8 이전에는 인터페이스에 정적 메서드 사용 불가였음 자바 8부터 제한이 풀렸기 때문에 인스턴스화 불가 동반 클래스를 둘 이유가 없다. 동반 클래스에 두었던 public 정적 멤버들 상당수를 그냥 인터페이스 자체에 두면 됨
public static
메서드를 정의할 수 있어서 동반 클래스가 더 이상 필요하지 않게 되었다.private static
메서드를 정의할 수 있게 되어, 인터페이스 내부에서만 사용되는 유틸리티 메서드를 구현할 수 있다.자바 8 이전 코드 예시:
// 인터페이스 정의
public interface MyInterface {
void doSomething();
}
// 동반 클래스(Companion Class) - 정적 메서드 제공
public class MyInterfaceUtils {
public static void printInfo() {
System.out.println("MyInterface Utils method");
}
}
MyInterface
에는 doSomething()
이라는 추상 메서드가 정의되어 있지만, 정적 메서드는 정의할 수 없다.MyInterfaceUtils
)를 만들어 정적 메서드를 정의해야 했다.사용 예시:
public class Main {
public static void main(String[] args) {
// 동반 클래스의 정적 메서드 호출
MyInterfaceUtils.printInfo();
}
}
자바 8 이후 코드 예시:
// 자바 8 이후 인터페이스에 정적 메서드 추가 가능
public interface MyInterface {
void doSomething();
// 정적 메서드 정의
static void printInfo() {
System.out.println("MyInterface static method");
}
}
MyInterface
안에 정적 메서드 printInfo()
를 정의할 수 있다. 더 이상 동반 클래스를 만들어 정적 메서드를 관리할 필요가 없다.사용 예시:
public class Main {
public static void main(String[] args) {
// 인터페이스의 정적 메서드 호출
MyInterface.printInfo();
}
}
반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관없다. 팩토리 메서드 패턴과 캡슐화를 활용하여 라이브러리의 내부 구현을 클라이언트(사용자)로부터 숨기고, 유연하게 클래스 설계를 변경할 수 있는 방법에 대한 예시로,
EnumSet
클래스가 어떻게 작동하는지 설명하자면 아래와 같다. 참고로EnumSet
클래스는 열거형(enum) 상수들을 집합으로 관리할 때 사용하는 고성능, 경량화된 Set 구현체
public enum Day {
MONDAY, TUESDAY, WEDNESDAY, THURSDAY, FRIDAY, SATURDAY, SUNDAY
}
// 주중만 포함하는 EnumSet
EnumSet<Day> weekdays = EnumSet.of(Day.MONDAY, Day.TUESDAY, Day.WEDNESDAY, Day.THURSDAY, Day.FRIDAY);
EnumSet
클래스는 public 생성자 없이 정적 팩토리 메서드로만 인스턴스를 생성한다.EnumSet
은 원소의 개수에 따라 두 가지 하위 클래스를 사용하여 성능을 최적화한다.RegularEnumSet
을 사용하여 long 변수를 하나만 사용해 원소를 관리한다.JumboEnumSet
을 사용해 long 배열로 원소를 관리한다.RegularEnumSet
과 JumboEnumSet
)의 존재를 모른다. 단지 EnumSet
인스턴스만 받는다.RegularEnumSet
이 필요 없어진다면, 이를 삭제해도 클라이언트 코드에는 영향이 없다.💬 풀어서 설명하자면,
정적 팩토리 메서드:
EnumSet
클래스는 public 생성자를 제공하지 않고, 대신 정적 팩토리 메서드만을 통해 객체를 생성합니다. 정적 팩토리 메서드는 객체를 생성하는 유연한 방법으로, 생성자와 달리 여러 가지 반환 타입을 제공하거나 캐싱을 통해 객체 생성을 제어할 수 있습니다.예를 들어, EnumSet.of()
같은 메서드를 사용해 EnumSet
의 인스턴스를 생성할 수 있습니다. 여기서 중요한 점은 클라이언트가 객체를 생성할 때 어떤 특정 하위 클래스의 생성자를 호출하지 않는다는 것입니다. 그저 팩토리 메서드(of()
)만 호출하여 EnumSet
을 받을 뿐입니다.
내부 구현의 캡슐화:
EnumSet
클래스는 내부적으로 두 가지 하위 클래스를 사용합니다. 이는 성능을 최적화하기 위해 사용되는 방법입니다.RegularEnumSet
: 원소의 개수가 64개 이하일 때 사용됩니다. 이 경우, 단일 long
변수를 사용해 64비트 내에서 원소들을 관리할 수 있으므로 메모리와 성능이 매우 효율적입니다.JumboEnumSet
: 원소의 개수가 65개 이상일 때 사용됩니다. 이 경우, 단일 long
변수로는 모든 원소를 관리할 수 없으므로 long 배열을 사용하여 원소들을 관리합니다.이 구조는 성능을 극대화하려는 목적에서 만들어졌지만, 클라이언트는 이 두 가지 하위 클래스의 존재를 전혀 알 필요가 없습니다. 클라이언트는 EnumSet
팩토리 메서드로 객체를 생성하지만, 실제로 어떤 구체적인 클래스(RegularEnumSet
또는 JumboEnumSet
)가 반환되는지는 몰라도 괜찮습니다. 이것이 캡슐화의 핵심입니다.
미래의 유연성:
RegularEnumSet
을 더 이상 사용하지 않는다고 결정하면, 이를 삭제하더라도 클라이언트의 코드에는 영향을 주지 않습니다. 클라이언트는 EnumSet
팩토리 메서드를 사용해 객체를 생성하고, 반환된 객체가 구체적으로 어떤 하위 클래스인지는 중요하지 않기 때문입니다. 이처럼 내부 구현을 자유롭게 변경할 수 있는 유연성을 확보할 수 있습니다.다형성의 활용:
EnumSet
의 팩토리 메서드는 두 하위 클래스 중 하나를 반환하는데, 클라이언트는 그저 상위 타입인 EnumSet
만 사용하게 됩니다. 이로 인해, 구체적인 하위 클래스의 존재를 몰라도 프로그램은 정상적으로 작동합니다. 다형성을 사용함으로써, 구체적인 구현에 대한 의존성을 제거하고, 내부 구현을 자유롭게 변경할 수 있게 됩니다.자바 8 이전 스타일: 동반 클래스가 없는 경우 (이 설명과 연관 없음)
public enum Color {
RED, GREEN, BLUE;
}
EnumSet<Color> set = EnumSet.of(Color.RED, Color.GREEN);
자바 8 이후 스타일: 내부적으로 두 하위 클래스를 사용하지만 클라이언트는 모름
// 정적 팩토리 메서드 사용
EnumSet<Color> set = EnumSet.of(Color.RED, Color.GREEN);
// 내부에서의 구현 예시 (OpenJDK 내부적 처리)
if (enumConstants.length <= 64) {
return new RegularEnumSet<>(...); // long으로 처리
} else {
return new JumboEnumSet<>(...); // long 배열로 처리
}
RegularEnumSet
또는 JumboEnumSet
중 어떤 것을 사용하는지 알지 못합니다. 팩토리 메서드는 내부에서 가장 효율적인 구현체를 선택해 제공한다.이런 유연함이 서비스 제공자 프레임워크를 만드는 근간이 되며, 서비스 제공자 프레임워크는 여러 종류의 서비스(또는 기능)를 다양한 방식으로 제공할 수 있도록 설계된 시스템이다. 이 프레임워크는 동적인 유연성을 제공하여, 특정한 구현체에 고정되지 않고 다양한 구현체를 선택하거나 교체할 수 있다. 자바에서 이 패턴은
JDBC
와 같은 시스템에서 사용
패턴의 세 가지 핵심 컴포넌트
데이터베이스 연결
을 처리한다.DriverManager.registerDriver()
가 제공자 등록 API 역할을 한다. 각 데이터베이스 드라이버는 자신을 이 API에 등록하여 클라이언트가 사용할 수 있게 만든다.DriverManager.getConnection()
이 서비스 접근 API로, 이 메서드를 통해 클라이언트는 특정 조건에 맞는 데이터베이스 연결 객체를 얻을 수 있다.이 패턴에는 서비스 제공자 인터페이스(Service Provider Interface)
라는 네 번째 컴포넌트가 추가될 수 있다. 이 인터페이스는 각 서비스의 구현체를 생성하는 팩토리 객체로 사용된다. 이 컴포넌트가 없으면 구현체를 인스턴스화할 때 리플렉션(Reflection)을 사용해야 한다. 예를 들어, JDBC에서는 Driver
가 서비스 제공자 인터페이스 역할을 한다. 이는 데이터베이스 연결 객체(Connection
)을 생성하는 팩토리 역할을 수행한다.
💬 자바의 예시 (JDBC)
Connection
DriverManager.registerDriver()
DriverManager.getConnection()
Driver
JDBC는 자바에서 데이터베이스와 상호작용할 수 있는 표준 API입니다. 데이터베이스마다 다른 드라이버가 필요하지만, 이들은 모두 같은 Connection
인터페이스를 구현한다. 각 데이터베이스 드라이버는 DriverManager
에 자신을 등록하여 클라이언트가 사용할 수 있게 된다.
💬 변형 및 확장
ServiceLoader
(자바 6부터 제공): 자바 6부터는 java.util.ServiceLoader
라는 범용 서비스 제공자 프레임워크가 제공된다. 이를 통해 직접 서비스 제공자 프레임워크를 만들 필요 없이 간편하게 구현체를 로드하고 사용할 수 있다.ServiceLoader
는 자바 애플리케이션에서 구현체를 찾고 로드하는 범용 메커니즘을 제공한다. 이를 통해 다양한 서비스 제공자가 존재하더라도 손쉽게 서비스를 사용하고 관리할 수 있다.서비스 제공자 인터페이스(Service Provider Interface)는 종종 추가되는 네 번째 컴포넌트로, 서비스 인터페이스의 인스턴스를 생성하는 팩토리 역할을 한다. 자바에서는
java.util.ServiceLoader
라는 범용 서비스 제공자 프레임워크가 제공됨
앞서 이야기한 컬렉션 프레임워크의 유틸리티 구현 클래스들은 상속할 수 없다는 이야기다
어찌 보면 이 제약은 상속보다 컴포지션
을 사용(아이템 18하도록 유도하고 불변 타입(아이템 17으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점으로 받아들일 수도 있다.
API 문서를 잘 써놓고 메서드 이름도 널리 알려진 규약을 따라 짓는 식으로 문제를 완화해줘야 한다.
이정적 메서드로 미리 정의되어 있는 것들
메서드 | 설명 | 예시 |
---|---|---|
from | 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형 변환 메서드. | Date d = Date.from(instant); |
of | 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드. | Set faceCards = EnumSet.of(JACK, QUEEN, KING); |
valueOf | from과 of의 더 자세한 버전. | BigInteger prime = BigInteger.valueOf(Integer.MAX_VALUE); |
instance / getInstance | 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지 않음. | StackWalker luke = StackWalker.getInstance(options); |
create / newInstance | instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스를 생성해 반환함을 보장함. | Object newArray = Array.newInstance(classObject, arrayLen); |
getType | getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용. 'Type'은 반환할 객체의 타입. | FileStore fs = Files.getFileStore(path); |
newType | newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용. 'Type'은 반환할 객체의 타입. | BufferedReader br = Files.newBufferedReader(path); |
type | getType과 newType의 간결한 버전. | List litany = Collections.list(legacyLitany); |
❗ 핵심 정리
정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다. 그렇다고 하더라도 정적 팩터리를 사용하는 게 유리한 경우 가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있다면 고치자.