Java/서적

[Effective Java]아이템1. 생성자 대신 정적 팩터리 메서드를 고려하라.

feel2 2024. 4. 2. 19:36
반응형

 

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

 

클래스의 인스턴스를 생성하는 방법은 일반적으로 (1) public 생성자를 사용하거나 (2) 정적 팩터리 메서드를 사용하는 방법이 있다.

  1. public 생성자를 통해 인스턴스 생성
public class Person {
    public Person() {

    }
}
@DisplayName("생성자를 통해서 인스턴스를 생성한다.")
    @Test
    void createInstanceUsingConstructor()  {
        // given
        String className = "effective_java.item1.Person";

        // when
        Person person = new Person();

        // then
        assertEquals(className,person.getClass().getName());
    }
  1. 정적 팩토리 메서드를 통해서 인스턴스 생성
public class Man {
    private static final Man MAN = new Man();

    private Man() {

    }

    public static Man getInstance() {
        return MAN;
    }
}
        @DisplayName("정적 팩터리 메서드를 통해서 인스턴스를 생성한다.")
    @Test
    void createInstanceUsingStaticFactoryMethod() {
        // given
        String className = "effective_java.item1.Man";

        // when
        Man man = Man.getInstance();

        // then
        assertEquals(className,man.getClass().getName());
    }
@DisplayName("정적 팩터리 메서드로 싱글톤을 보장해줄 수 있다.")
    @Test
    void guaranteedSingleton() {
        // given
        Man man1 = Man.getInstance();
        Man man2 = Man.getInstance();

        // when // then
        assertEquals(man1,man2);
        assertSame(man1,man2);
    }

정적 팩터리 메서드란?

class 내에 static으로 선언된 메서드이며, 객체를 생성할 때, 생성자를 쓰지 않고 정적 메서드를 사용하는 것이다.

정적 팩터리 메서드가 생성자 보다 좋은 이유

1. 이름을 가질 수 있다.

한 클래스에 다른 특성을 가진 생성자가 여러개 필요할 것 같으면, 생성자를 정적 팩터리 메서드로 바꾸고 
그 차이를 잘 드러내는 이름을 주자. 
public class Info {

    private String name;
    private String address;

    private Info() {}

    public static Info withName(String name) {
        Info info = new Info();

        info.name = name;
        return info;
    }
    public static Info withAddress(String address) {
        Info info = new Info();

        info.address = address;
        return info;
    }

    public String getName() {
        return this.name;
    }

    public String getAddress() {
        return this.address;
    }
}
@DisplayName("정적 팩터리 매서드 패턴을 사용하면 이름을 가질 수 있다.")
    @Test
    void getNameUsingStaticFactoryMethod() {
        // given
        String name = "홍길동";
        String address = "마포구";

        //when
        Info info1 = Info.withName(name);
        Info info2 = Info.withAddress(address);

        // then
        assertEquals(name,info1.getName());
        assertNull(info1.getAddress());
        assertEquals(address,info2.getAddress());
        assertNull(info2.getName());

    }

2. 호출할 때 마다 인스턴스를 새로 만들지 않아도 된다.

덕분에 불변 클래스(immutable class)를 만들 수 있다.

불변 클래스?(아이템17 참조)

변경이 불가능한 클래스이며, 가변적이지 않은 클래스 
= 래퍼런스 타입의 객체(heap 영역에서 생성)
= 값 재할당은 가능 (String a = "aa"; a = "b")
ex) String, Boolean, Integer, Float, Long 등등..
package effective_java.item1;

public class Man {
    private static final Man MAN = new Man();

    private Man() {

    }

    public static Man getInstance() {
        return MAN;
    }
}
@DisplayName("호출할 떄 마다 새로운 인스턴스를 만들지 않아도 된다.")
    @Test
    void getSameInstanceWhenCallStaticFactoryMethod() {
        // given
        Man man1 = Man.getInstance();
        Man man2 = Man.getInstance();

        //when // then
        assertSame(man1,man2);

    }

이와 비슷한 기법

플라이 웨이트 패턴
- 인스턴스를 **공유**하여 쓸데없는 new 연산자를 통해서 생기는 **메모리 낭비를 줄이는** 패턴
- 객체 내부에 참조하는 객체가 있다면 공유하고 없다면 새로운 객체를 생성함
원하는 색을 입력해주세요 :)
초록
새로운 객체 생성
x:47 y:55 위치에 초록색 나무를 설치했습니다!
연두
새로운 객체 생성
x:86 y:33 위치에 연두색 나무를 설치했습니다!
초록
x:62 y:30 위치에 초록색 나무를 설치했습니다!
초록
x:83 y:21 위치에 초록색 나무를 설치했습니다!
초록
x:47 y:46 위치에 초록색 나무를 설치했습니다!
연두
x:49 y:59 위치에 연두색 나무를 설치했습니다!
연두
x:10 y:38 위치에 연두색 나무를 설치했습니다!
카키
새로운 객체 생성
x:78 y:39 위치에 카키색 나무를 설치했습니다!
카키
x:21 y:41 위치에 카키색 나무를 설치했습니다!
카키
x:58 y:26 위치에 카키색 나무를 설치했습니다!
  싱글톤 패턴 플라이 웨이트 패턴
객체 수 하나의 객체를 재사용 특성이 같은 여러개의 객체를 재사용

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

반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 ‘엄청난 유연성’ 부여한다.

정적 메서드로부터 인터페이스 자체를 반환하여, 구현 클래스를 공개하지 않고도 그 객체를 반환할 수 있다.

구현 클래스의 상세를 숨길 수 있고, 사용자는 객체를 인터페이스만으로 다룰 수 있다.

인터페이스 기반 프레임워크의 핵심 기술!

public Collections(){
      ///...

    public static final List EMPTY_LIST = new EmptyList<>();

    public static final <T> List<T> emptyList() {
        return (List<T>) EMPTY_LIST;
    }

      //...

        public static <T> ArrayList<T> list(Enumeration<T> e) {
                ArrayList<T> l = new ArrayList<>();
                while (e.hasMoreElements())
                    l.add(e.nextElement());
                return l;
            }
}

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

대표적으로 EnumSet 이고, public 생성자 없이 오직 정적 팩토리 메서드 패턴으로만 인스턴스를 제공한다.

public abstract class EnumSet<E extends Enum<E>> extends AbstractSet<E>
    implements Cloneable, java.io.Serializable
{
//...
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);
    }

}

원소의 개수에 따라 RegularEnumSet or JumboEnumSet 객체를 반환한다.

public interface Color {

    static Color of(int v) {
        Color instance;

        if (v > 50) {
            instance = new Blue(v);
        } else {
            instance = new Red(v);
        }

        return instance;
    }

    String getColor();

    class Red implements Color {

        private final int value;

        public Red(int value) {
            this.value = value;
        }

        @Override
        public String getColor() {
            return "red color value is " + this.value;
        }
    }

    class Blue implements Color {

        private final int value;

        public Blue(int value) {
            this.value = value;
        }

        @Override
        public String getColor() {
            return "blue color value is " + this.value;
        }
    }
}
@DisplayName("입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있다.")
    @Test
    void ReturnDifferentTypeChildClass() {
        Color color1 = Color.of(51);
        Color color2 = Color.of(50);

        System.out.println(color1.getClass()+" " + color1.getColor());
        System.out.println(color2.getClass()+" " + color2.getColor());

    }
class effective_java.item1.Color$Blue blue color value is 51
class effective_java.item1.Color$Red red color value is 50

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

이런 유연한 점을 이용하여 서비스 제공자 프레임워크(Service Provider Framework)를 만드는 근간이 된다.

서비스 제공자 프레임워크는 3가지의 핵심 컴포넌트로 이뤄진다.

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

3개의 핵심 컴포넌트와 더불어 종종 네번째 컴포넌트가 사용되기도 한다.

  1. 서비스 제공자 인터페이스(Service Provider Interface): 서비스 인터페이스의 인스턴스를 생성하는 팩토리 객체를 설명

대표적인 서비스 제공자 프레임워크로는 JDBC가 있다.

MySql, OracleDB, MariaDB등 다양한 Database를 JDBC라는 프레임워크로 관리할 수 있다.

ex) JDBC의 경우, **Connection**서비스 인터페이스 역할을, DriverManager.registerDriver()제공자 등록 API, DriverManager.getConnection()서비스 접근 API, 그리고 Driver서비스 제공자 인터페이스 역할을 한다. getConnection을 썼을 때 실제 return 되어서 나오는 객체는 DB Driver마다 다르다.

출처:김영한 스프링 db2편

  • MySql 드라이버 사용

출처:김영한 스프링 db2편

  • Oracle 드라이버 사용

출처:김영한 스프링 db2편

public interface Connection  extends Wrapper, AutoCloseable {

...

    @CallerSensitive
  public static Connection getConnection(String url,
      String user, String password) throws SQLException {
      java.util.Properties info = new java.util.Properties();

      if (user != null) {
          info.put("user", user);
      }
      if (password != null) {
          info.put("password", password);
      }

      return (getConnection(url, info, Reflection.getCallerClass()));
  }

...

}
@Slf4j
public class DBConnectionUtil {

  public static Connection getConnection() {
      try {
          Connection connection = DriverManager.getConnection(URL, USERNAME, PASSWORD);
          log.info("get connection={}, class={}", connection, connection.getClass());
          return connection;
      } catch (Exception e) {
          throw new IllegalArgumentException(e);
      }

  }
}
//application.properties

spring.datasource.url=jdbc:h2:tcp://localhost/~/test10
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=org.h2.Driver
//application.properties

spring.datasource.url=jdbc:mysql://127.0.0.1:3306/dbName
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
//build.gradle

plugins {
    id 'java'
    id 'org.springframework.boot' version '2.7.12'
    id 'io.spring.dependency-management' version '1.0.15.RELEASE'
}

...

dependencies {
    implementation 'org.springframework.boot:spring-boot-starter-jdbc'

  ...

    // mysql driver 사용하기 위해 
    **runtimeOnly 'com.mysql:mysql-connector-j'**
    // h2 driver 사용하기 위해 
    **runtimeOnly 'com.h2database:h2'**

  ...
}

tasks.named('test') {
    useJUnitPlatform()
}

정적 팩터리 매서드의 단점

1. 정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없다.

만약 부모타입의 생성자가 public or protected 아니라면 상속이 불가능하다.

보통 정적 팩토리 메서드만 제공하는 경우 생성자를 통한 인스턴스 생성을 막는 경우가 많다.

ex) Collections 는 생성자가 private 으로 구현되어 있기 때문에 상속 불가능함.

그러나, 이 제약은 상속보다 컴포지션 사용하도록 유도하고, 불변 타입으로 만들려면 이 제약을 지켜야 한다는 점에서 오히려 장점으로 작용한다.
(컴포지션 : 기존 클래스를 확장하는 대신에 새로운 클래스를 만들고 private 필드로 기존 클래스의 인스턴스를 참조하는 방식)

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

어떤 라이브러리를 사용하기 위해 API 문서를 보면, 정적 팩터리 메서드에 대한 정보를 확인하기가 쉽지 않다.

 

아래 Boolean 에 대한 API문서를 보면, 정적 팩터리 메서드와 일반 메서드가 같이 있어 구분이 쉽지 않다.

정적 팩터리 메서드 명명 방식

  • from: 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 형변환 메서드
  • Date d = Date.from(instant);
  • of: 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 집계 메서드
  • Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);
  • instance 혹은 getInstance: 매개변수로 명시한 인스턴스를 반환, 같은 인스턴스임을 보장하진 않음StackWalker luke = StackWalker.getInstance(options);
  • create 혹은 newInstance: instance 혹은 getInstance와 같지만, 매번 새로운 인스턴스 생성을 보장Object newArray = Array.newInstance(classObject, arrayLen);
  • getType: getInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용FileStore fs = Files.getFileStore(path); ("Type"은 팩터리 메서드가 반환할 객체의 타입)
  • newType: newInstance와 같으나, 생성할 클래스가 아닌 다른 클래스에 팩터리 메서드를 정의할 때 사용BufferedReader br = Files.newBufferedReader(path);
  • type: getTypenewType의 간결한 버전
  • List<Complaint> litany = Collections.list(legacyLitany);

결론

  • 정적 팩토리 메서드와 public 생성자는 각자의 쓰임새가 있으니, 상대적인 장단점을 이해하고 쓰는것이 좋다.
  • 그렇다고 해도 정적 팩터리를 사용하는 경우가 유리한 경우가 많으니, 무작정 public 생성자를 사용하던 습관이 있다면 고치자.

 

참조

 

[디자인 패턴] Flyweight 패턴

Flyweight(플라이웨이트) 패턴 ​ 인스턴스를 가능한 한 공유해서 사용함으로써 메모리를 절약하는 패턴 👿 문제상황 ​ 마인크래프트 게임에 나무를 설치하고 싶습니다. ​ 나무는 색(color)을 정

velog.io

 

ITEM 1: Static Factory Method(정적 메소드) | java | JAVA

Set faceCards = EnumSet.of(JACK, QUEEN, KING);

dahye-jeong.gitbook.io

 

[이펙티브 자바] 아이템 1. 생성자 대신 정적 팩터리 메서드를 고려하라

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

velog.io

 

 

스프링 DB 2편 - 데이터 접근 활용 기술 | 김영한 - 인프런

김영한 | 백엔드 개발에 필요한 DB 데이터 접근 기술을 활용하고, 완성할 수 있습니다. 스프링 DB 접근 기술의 원리와 구조를 이해하고, 더 깊이있는 백엔드 개발자로 성장할 수 있습니다., 백엔드

www.inflearn.com

 

반응형