| 일 | 월 | 화 | 수 | 목 | 금 | 토 |
|---|---|---|---|---|---|---|
| 1 | 2 | 3 | 4 | 5 | 6 | |
| 7 | 8 | 9 | 10 | 11 | 12 | 13 |
| 14 | 15 | 16 | 17 | 18 | 19 | 20 |
| 21 | 22 | 23 | 24 | 25 | 26 | 27 |
| 28 | 29 | 30 |
- 로드밸런서
- spring boot
- 스프링
- 성능 개선
- 코드카타
- 프로그래머스
- 자바
- GoF 23
- 백엔드
- 계산기
- 디자인 패턴
- 김영한
- lv1
- java
- 트러블슈팅
- Spring Batch
- Til
- 추상클래스
- Effective Java
- 빌더 패턴
- 프록시 패턴
- 이펙티브 자바
- DB
- 토스
- 스케줄러
- 배치
- redis
- 템플릿 메서드 패턴
- Spring
- 스프링 배치
- Today
- Total
김코딩
[ Effective Java ] 1. 생성자 대신 정적 팩터리 메서드를 고려하라 본문
정적 팩터리 메서드란?
객체 생성을 담당하는 정적(static) 메서드이다. 생성자 대신 또는 생성자와 함께 객체를 생성하는 방법을 제공한다.
public class Person {
private String name;
private int age;
// private 생성자 (직접 생성 방지)
private Person(String name, int age) {
this.name = name;
this.age = age;
}
// 정적 팩터리 메서드들
public static Person newBaby(String name) {
return new Person(name, 0);
}
public static Person newAdult(String name, int age) {
if (age < 18) {
throw new IllegalArgumentException("성인은 18세 이상이어야 합니다.");
}
return new Person(name, age);
}
public static Person newSenior(String name) {
return new Person(name, 65);
}
}
사용 방법
// 생성자 방식 (만약 public이라면)
Person person1 = new Person("김철수", 25); // 의미가 불분명
// 정적 팩터리 메서드 방식
Person baby = Person.newBaby("김아기"); // 명확한 의미
Person adult = Person.newAdult("김어른", 30); // 명확한 의미
Person senior = Person.newSenior("김할머니"); // 명확한 의미
장점
1. 이름을 가질 수 있다.
생성자
- 생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다.
- 하나의 시그니처로는 생성자를 하나만 만들 수 있다.
정적 팩터리 메서드
- 정적 팩터리는 이름만 잘지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다.
- 한 클래스에 시그니처가 같은 생성자가 여러 개 필요할 것 같으면, 생성자를 정적 팩터리 메서드로 바꾸고 각각의 차이를 잘 드러내는 이름으로 지을 수 있다.
ex) "값이 소수인 BigInteger를 반환한다."
생성자 : BigInteger(int, int, Random)
정적 팩터리 메서드 : BigInteger.probablePrime()
2. 호출될 때마다 인스턴스를 새로 생성하지는 않아도 된다.
- 불변 클래스는 인스턴스를 미리 만들어 놓거나 새로 생성한 인스턴스를 캐싱하여 재활용하는 식으로 불필요한 객체 생성을 피할 수 있다. ex) Boolean.valueOf(boolean)
- 플라이웨이트(Flyweight pattern) 패턴도 이와 비슷한 기법이다.
플라이웨이트 패턴 : 구조적 디자인 패턴 중 하나로, 메모리 사용량을 최소화하기 위해 객체들 간에 상태를 효율적으로 공유하는 패턴
- 정적 팩터리 방식의 클래스는 언제 어느 인스턴스를 살아 있게 할지를 철저히 통제할 수 있다. (인스턴스 통제 클래스 라고 한다.)
- 인스턴스를 통제하면 클래스를 싱글턴(Singleton)으로 만들 수도, 인스턴스화 불가로 만들수도 있다.
3. 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
- 대표적인 예시로 자바의 Collections가 있다.
- 자바 8부터는 인터페이스가 정적 메서드를 가질 수 없다는 제한이 풀렸다.
Java 8 이전 방식
// 인터페이스
public interface List<E> extends Collection<E> {
// 정적 메서드 선언 불가능
}
// 별도의 유틸리티 클래스 필요
public class Collections {
public static <T> List<T> emptyList() {
return (List<T>) EMPTY_LIST;
}
public static <T> List<T> singletonList(T o) {
return new SingletonList<>(o);
}
}
// 사용
List<String> empty = Collections.emptyList();
Java 8 이후 방식
// 인터페이스 내부에 정적 팩터리 메서드 직접 선언 가능
public interface List<E> extends Collection<E> {
// Java 9부터 실제로 추가된 팩터리 메서드들
static <E> List<E> of() {
return ImmutableCollections.emptyList();
}
static <E> List<E> of(E e1) {
return new ImmutableCollections.List12<>(e1);
}
static <E> List<E> of(E e1, E e2) {
return new ImmutableCollections.List12<>(e1, e2);
}
@SafeVarargs
static <E> List<E> of(E... elements) {
// 구현체는 숨기고 List 타입으로 반환
return ImmutableCollections.listFromTrustedArray(elements);
}
}
// 사용 - 더 직관적!
List<String> list = List.of("a", "b", "c");
4. 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.
- 반환 타입의 하위 타입이기만 하면 어떤 클래스의 객체를 반환하든 상관 없다.
대표적인 예시 : EnumSet
public abstract class EnumSet<E extends Enum<E>> {
public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
Enum<?>[] universe = getUniverse(elementType);
if (universe == null)
throw new ClassCastException(elementType + " not an enum");
// 원소 개수에 따라 다른 구현체 반환!
if (universe.length <= 64)
return new RegularEnumSet<>(elementType, universe); // 비트 벡터 사용
else
return new JumboEnumSet<>(elementType, universe); // long 배열 사용
}
}
// 사용 - 클라이언트는 구현체를 신경 쓰지 않음
EnumSet<Color> colors = EnumSet.noneOf(Color.class); // RegularEnumSet 반환
EnumSet<BigEnum> bigSet = EnumSet.noneOf(BigEnum.class); // JumboEnumSet 반환 (원소가 64개 초과)
5. 정적 팩터리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다.
- 정적 팩터리 메서드를 미리 정의해놓고, 나중에 구현체가 추가되어도 기존 코드 변경 없이 동작 할 수 있다.
대표적인 예시 : JDBC
처음에는 인터페이스와 팩터리 메서드만 존재
// java.sql 패키지에 미리 정의됨
public class DriverManager {
// 이 메서드가 작성될 때는 MySQL, Oracle 등의 구체적인 드라이버가 없었음!
public static Connection getConnection(String url) {
// 등록된 드라이버들 중에서 url에 맞는 것을 찾아 반환
for (DriverInfo aDriver : registeredDrivers) {
if (aDriver.driver.acceptsURL(url)) {
Connection con = aDriver.driver.connect(url, info);
if (con != null) {
return con; // 구체적인 Connection 구현체 반환
}
}
}
throw new SQLException("No suitable driver found");
}
}
나중에 구현체들이 추가됨
// MySQL 드라이버 (나중에 개발됨)
public class MySQLDriver implements Driver {
static {
// 드라이버 자동 등록
DriverManager.registerDriver(new MySQLDriver());
}
public Connection connect(String url, Properties info) {
return new MySQLConnection(url, info); // MySQL 전용 Connection
}
}
// Oracle 드라이버 (나중에 개발됨)
public class OracleDriver implements Driver {
static {
DriverManager.registerDriver(new OracleDriver());
}
public Connection connect(String url, Properties info) {
return new OracleConnection(url, info); // Oracle 전용 Connection
}
}
클라이언트 코드는 변경 없이 동작
// DriverManager.getConnection()이 작성될 때는
// MySQLConnection, OracleConnection 클래스가 존재하지 않았지만
// 나중에 추가되어도 기존 코드는 그대로 동작!
Connection mysqlConn = DriverManager.getConnection("jdbc:mysql://localhost/test");
// -> MySQLConnection 인스턴스 반환
Connection oracleConn = DriverManager.getConnection("jdbc:oracle:thin:@localhost:1521:xe");
// -> OracleConnection 인스턴스 반환
단점
1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.
- 정적 팩터리 메서드만 제공하려면 보통 생성자를 private으로 만드는데, 이렇게 되면 상속이 불가능해진다.
- 해결 방법 :
- 생성자를 protected로 만들기
- public 생성자와 정적 팩터리 메서드를 함께 제공(이러면 정적 팩터리 메서드를 쓰는 이유가 있을까..?)
- 컴포지션 사용
상속의 기본 원리
// 부모 클래스
public class Parent {
private String name;
public Parent(String name) { // public 생성자
this.name = name;
}
}
// 자식 클래스
public class Child extends Parent {
private int age;
public Child(String name, int age) {
super(name); // 부모의 생성자를 반드시 호출해야 함!
this.age = age;
}
}
문제 상황 : private 생성자
// 정적 팩터리 메서드만 제공하는 클래스
public class Person {
private String name;
// 생성자를 private으로 숨김
private Person(String name) {
this.name = name;
}
// 정적 팩터리 메서드만 제공
public static Person create(String name) {
return new Person(name);
}
}
// 상속 시도 - 컴파일 에러!
public class Student extends Person {
private String school;
public Student(String name, String school) {
super(name); //컴파일 에러 private 생성자에 접근 불가
this.school = school;
}
}
2. 정적 팩터리 메서드는 프로그래머가 찾기 어렵다.
- 생성자처럼 API 설명에 명확히 드러나지 않으니 사용자는 정적 팩터리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 한다.
정적 팩터리 메서드에서 흔히 사용하는 명명 방식
- from : 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
의미: "~으로부터" 변환하여 생성
// Date에서 Instant로 변환
Date date = new Date();
Instant instant = Instant.from(date.toInstant());
// String에서 BigInteger로 변환
BigInteger bigInt = BigInteger.from("12345");
// 다른 타입에서 변환하는 경우
LocalDate date = LocalDate.from(localDateTime); // LocalDateTime → LocalDate
- of : 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
의미: "~의", 여러 값들을 조합하여 생성
// 여러 원소로 Set 생성
Set<String> colors = Set.of("red", "green", "blue");
// 년, 월, 일로 LocalDate 생성
LocalDate christmas = LocalDate.of(2023, 12, 25);
// 여러 원소로 List 생성
List<Integer> numbers = List.of(1, 2, 3, 4, 5);
// EnumSet 생성
enum Day { MON, TUE, WED, THU, FRI }
EnumSet<Day> weekdays = EnumSet.of(Day.MON, Day.TUE, Day.WED, Day.THU, Day.FRI);
- valueOf : from과 of의 더 자세한 버전
의미: "~의 값으로", 주로 문자열이나 기본 타입을 객체로 변환
// String을 Boolean으로 변환
Boolean bool = Boolean.valueOf("true"); // "true" → Boolean.TRUE
Boolean bool2 = Boolean.valueOf(false); // boolean → Boolean
// String을 Integer로 변환
Integer num = Integer.valueOf("123"); // "123" → Integer(123)
Integer num2 = Integer.valueOf(42); // int → Integer
// enum 변환
enum Color { RED, GREEN, BLUE }
Color color = Color.valueOf("RED"); // "RED" → Color.RED
- instance 혹은 getInstance : 매개변수를 받아 매개변수로 명시한 인스턴스 반환
의미: 싱글턴이거나 매개변수에 맞는 기존 인스턴스 반환 (새로 만들지 않을 수 있음)
// 캘린더 인스턴스 (로케일별로 다른 구현체)
Calendar cal = Calendar.getInstance(); // 기본 로케일
Calendar usCal = Calendar.getInstance(Locale.US); // 미국 로케일
// 시간대별 인스턴스
TimeZone seoulTz = TimeZone.getInstance("Asia/Seoul");
TimeZone nyTz = TimeZone.getInstance("America/New_York");
// 커스텀 예시
public class DatabaseConnection {
private static final Map<String, DatabaseConnection> instances = new HashMap<>();
public static DatabaseConnection getInstance(String database) {
return instances.computeIfAbsent(database, DatabaseConnection::new);
}
}
- create 또는 newInstance : 매번 새로운 인스턴스를 생성해서 반환
의미: 항상 새로운 객체 생성을 보장
// Array 생성 (항상 새로운 배열)
Object newArray = Array.newInstance(String.class, 10); // String[10] 생성
// Thread 생성
Thread thread = Thread.newThread(() -> System.out.println("Hello"));
// 커스텀 예시
public class Report {
public static Report create(String title) {
return new Report(title); // 항상 새 인스턴스
}
public static Report newInstance(String title, String content) {
return new Report(title, content); // 항상 새 인스턴스
}
}
// 사용
Report report1 = Report.create("2023 분기 보고서");
Report report2 = Report.newInstance("연간 보고서", "상세 내용...");
- getType : 다른 타입의 인스턴스를 생성, Type은 반환할 객체의 타입
의미: "~타입을 가져와", 보통 팩터리 클래스에서 다른 타입 생성
// Files 클래스에서 다른 타입들 생성
Path path = Paths.get("/tmp/file.txt");
FileStore fileStore = Files.getFileStore(path); // FileStore 타입 반환
FileSystem fileSystem = FileSystems.getFileSystem(uri); // FileSystem 타입 반환
// 커스텀 예시
public class ConnectionFactory {
public static DatabaseConnection getConnection(String url) {
return new DatabaseConnection(url);
}
public static HttpClient getHttpClient() {
return HttpClient.newHttpClient();
}
public static FtpClient getFtpClient(String server) {
return new FtpClient(server);
}
}
- newType : 다른 타입의 새로운 인스턴스를 생성
의미: "새로운 ~타입", 매번 새로운 다른 타입의 객체 생성
// Files에서 다양한 타입의 새 객체 생성
Path path = Paths.get("/tmp/input.txt");
BufferedReader reader = Files.newBufferedReader(path); // BufferedReader 생성
BufferedWriter writer = Files.newBufferedWriter(path); // BufferedWriter 생성
InputStream inputStream = Files.newInputStream(path); // InputStream 생성
// 커스텀 예시
public class IOFactory {
public static BufferedReader newReader(String filename) throws IOException {
return Files.newBufferedReader(Paths.get(filename));
}
public static PrintWriter newWriter(String filename) throws IOException {
return new PrintWriter(Files.newBufferedWriter(Paths.get(filename)));
}
}
- type : getType과 newType의 간결한 버전
의미: 타입 이름만으로 간단히 표현
// Collections에서 다양한 컬렉션 타입 반환
List<String> list = Collections.list(enumeration); // List 타입 반환
Set<String> set = Collections.set(array); // Set 타입 반환
// 커스텀 예시
public class CollectionUtils {
public static <T> List<T> list(T... elements) {
return Arrays.asList(elements);
}
public static <T> Set<T> set(T... elements) {
return new HashSet<>(Arrays.asList(elements));
}
public static <K, V> Map<K, V> map(K key, V value) {
Map<K, V> map = new HashMap<>();
map.put(key, value);
return map;
}
}
// 사용
List<String> colors = CollectionUtils.list("red", "green", "blue");
Set<Integer> numbers = CollectionUtils.set(1, 2, 3, 4, 5);
Map<String, Integer> ages = CollectionUtils.map("Alice", 25);
명명 규칙 선택 가이드
- 변환: from, valueOf
- 조합/생성: of
- 싱글턴/캐싱: getInstance
- 항상 새로 생성: create, newInstance
- 다른 타입 반환: getType, newType, type
Effective Java 에서
정적 팩터리 메서드와 public 생성자는 각자의 쓰임새가 있으니 상대적인 장단점을 이해하고 사용하는 것이 좋다. 그렇다고 하더라도 정적 팩터리 메서드를 사용하는게 유리한 경우가 더 많으므로 무작정 public 생성자를 제공하던 습관이 있으면 고치자
'개발 서적 > Effective Java' 카테고리의 다른 글
| [ Effective Java ] 2. 생성자에 매개변수가 많다면 빌더를 고려하라 (0) | 2025.09.26 |
|---|