[Effective Java] Item1 - 생성자 대신 정적 팩토리 메소드를 고려하라

지구🌍·2023년 2월 11일
0

Effective Java 공부

목록 보기
1/12
post-thumbnail

생성자 대신 정적 팩토리 메소드를 고려하라

지금까지 인스턴스를 생성할 때 생성자만 사용했던 것 같다. 사실 이 책을 읽으면서 정적 팩토리 메소드를 처음 접해서 두 개의 차이를 먼저 구분해보고자 한다.

생성자와 정적 팩토리 메소드의 차이는 무엇일까?

생성자 : 인스턴스가 생성될 때 호출되는 '인스턴스 초기화 메소드', 인스턴스 변수의 초기화 작업에 사용되고, 인스턴스 생성 시에 실행, new 방식

코드로 확인 해보자

일반적인 생성자 사용법 예시

public class Test {
	private String name;

    public Test(String name) {
        this.name = name;
    }
}

정적 팩토리 메소드 : 객체 생성을 담당하는 클래스 메소드, 흔히 사용하는 생성자가 아니라 정적 메소드로 생성하는 방법

정적 팩토리 메소드 예시

public class Test {
    private String name;

    public Test(String name) {
        this.name = name;
    }

    public static Test withName(String name) {
        return new Test(name);
    }

    public static void main(String[] args) {
        Test test = withName("Gyunny");
    }
}

정적 팩토리 메소드의 필요성?

생성자를 통해 객체를 생성하는 것이 기본이지만 생성자는 명명을 할 수 없어 생성자 안에 별도의 전처리 로직이 있을 경우 해당 전처리에서 무엇을 하는지 "직접" 확인해야한다.

정적 팩토리 메소드의 장점

1. 이름을 가질 수 있다

  • 반환될 객체의 특성에 따라 메소드 네이밍으로 묘사할 수 있다 -> 코드의 가독성 증가
  • 생성자 방식에 비해 어떤 객체가 반환되는지 유추가 쉽다

예제코드 - 생성자 new 방식

public class Pizza {

    private final String name;
    private final String cheese;

    public Pizza(String name, String cheese) {
        this.name = name;
        this.cheese = cheese;
    }
    
    public Pizza(String name){
        this.name = name;
        this.cheese = "치즈 없음";
    }
}
public static void main(String[] args) {
        Pizza cheese = new Pizza("yesCheese", "모짜렐라");
        Pizza olive = new Pizza("noCheese");
    }

위 코드를 확인하면 치즈가 없는 피자를 만들 때 어떤 방식으로 Pizza 클래스에서 치즈를 할당하는지 확인이 불가 하다.

예제 코드 - 정적 팩토리 메소드 방식

public class Pizza {

    private final String name;
    private final String cheese;

    public static Pizza makePizza(String name, String cheese) {
        return new Pizza(name, cheese);
    }

    public static Pizza makeNoCheesePizza(String name) {
        return new Pizza(name, "치즈 없음");
    }

    private Pizza(String name, String cheese) {
        this.name = name;
        this.cheese = cheese;
    }
}
public static void main(String[] args) {
        Pizza cheesePizza = makePizza("치즈 피자", "모짜렐라");
        Pizza noCheesePizza = makeNoCheesePizza("치즈 없는 피자");
    }

생성자로 객체를 생성했을 때에 비해 메소드 명으로 어떤 객체일 지 쉽게 유추할 수 있다.

2. 호출할 때마다 새로운 객체를 생성할 필요가 없다.

  • 인스턴스를 미리 생성하고 새로 생성한 인스턴스를 캐싱하여 재활용하는 방식으로 불필요한 객체 생성을 피할 수 있다.
  • 객체 생성할 때마다, 중복되는 과정을 줄일 수 있다.

플라이 웨이트 패턴(Flyweight pattern) : 데이터를 공유하여 메모리를 절약하는 패턴, 공통으로 사용되는 객체는 한 번만 사용되고 Pool에 의해서 관리, 사용되는 방식 즉, 인스턴스를 가능한 만큼 공유하게 하여 쓸데 없이 new 연산자를 통한 메모리 낭비를 줄이는 방식

예제 코드

public class Day {

    private static final Map<String, Day> days = new HashMap<>();

    static {
        days.put("mon", new Day("Monday"));
        days.put("tue", new Day("Tuesday"));
        days.put("wen", new Day("Wednesday"));
        days.put("thu", new Day("Thursday"));
        days.put("fri", new Day("Friday"));
        days.put("sat", new Day("Saturday"));
        days.put("sun", new Day("Sunday"));
    }

    public static Day from(String day) {
        return days.get(day);
    }

    private final String day;

    private Day(String day) {
        this.day = day;
    }

    public String getDay() {
        return day;
    }
}

public static void main(String[] args) {
        Day day = Day.from("mon");
        System.out.println(day.getDay());
}

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

코드의 유연성을 제공해준다. 반환할 객체의 자유도를 높일 수 있고 원하는 객체를 리턴 할 수 있다.

코드로 확인해보자~!

반환 타입 변환 예제

public abstract class StaticFactoryMethod {

    abstract void getName();

    static public StaticFactoryMethod getNewInstance(String code) {
        StaticFactoryMethod staticFactoryMethod = null;

        if (code.indexOf("2") == 1) {
            staticFactoryMethod = new Point();
        } else {
            staticFactoryMethod = new Coupon();
        }

        return staticFactoryMethod;
    }
}

class Coupon extends StaticFactoryMethod {
    public void getName() {
        System.out.println("쿠폰을 발행합니다!");
    }
}

class Point extends StaticFactoryMethod {
    public void getName() {
        System.out.println("포인트를 적립합니다!");
    }
}

main

public class Main {
    public static void main(String[] args) {
        StaticFactoryMethod staticFactoryMethod = StaticFactoryMethod.getNewInstance("223123");
        StaticFactoryMethod staticFactoryMethod1 = StaticFactoryMethod.getNewInstance("123123");
        staticFactoryMethod.getName();
        staticFactoryMethod1.getName();
    }
}

위에 예시와 같이 코드에 따라 반환 객체를 자신의 하위 타입 중에 선택이 가능하다.

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

장점 3과 비슷한 의미이다.
예를 들면 같은 이름의 메소드지만 매개변수에 따라 다른 클래스를 리턴할 수 있다. -> 코드의 유연성이 높아진다.
특정 인터페이스들을 상속받은 구현체들이 있을 때, 객체 생성 시 상황에 따라 유동적으로 해당 구현체 타입으로 반환 가능!!

매개변수에 따라 다른 클래스 리턴 예시

public abstract class StaticFactoryMethod {

    abstract void getName();

    static public StaticFactoryMethod getNewInstance(String code, String coupon) {
        return new Coupon();
    }

    static public StaticFactoryMethod getNewInstance(String code) {
        return new Point();
    }
}

class Coupon extends StaticFactoryMethod {
    public void getName() {
        System.out.println("쿠폰을 발행합니다!");
    }
}

class Point extends StaticFactoryMethod {
    public void getName() {
        System.out.println("포인트를 적립합니다!");
    }
}
public class Main {
    public static void main(String[] args) {
        StaticFactoryMethod coupon = StaticFactoryMethod.getNewInstance("223123", "coupon");
        StaticFactoryMethod point = StaticFactoryMethod.getNewInstance("123123");
        coupon.getName();
        point.getName();
    }
}

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

이러한 유연함은 서비스 제공자 프레임워크를 만드는 근간이 된다. -> JDBC 대표적

이게 무슨 말일까...?
당장 클래스나 인터페이스가 존재하지 않아도 특정 텍스트 파일에 인터페이스 구현체의 위치를 알려주는 곳의 정보를 가지고 해당 객체를 읽어 생성할 수 있다.
하위 타입의 클래스가 존재하지 않아도 나중에 만든 클래스가 기존의 인터페이스나 클래스를 상속 받으면 언제든지 의존성을 주입 받을 수 있다.

코드로 확인해보자...!

예시

public abstract class StaticFactoryMethod5 {

    public abstract void getName();

    public static StaticFactoryMethod5 getNewInstance() {
        StaticFactoryMethod5 temp = null;

        try {
            Class<?> childClass = Class.forName("StaticFactoryMethod5Child");
            temp = (StaticFactoryMethod5) childClass.newInstance();
        } catch (ClassNotFoundException e) {
            System.out.println("클래스가 없습니다.");
        } catch (InstantiationException  e) {
            System.out.println("메모리에 올릴수 없습니다.");
        } catch (IllegalAccessException  e) {
            System.out.println("클래스파일 접근 오류입니다.");
        }

        return temp;
    }
}
public class StaticFactoryMethod5Child extends StaticFactoryMethod5{

    @Override
    public void getName() {
        System.out.println("정상 로드 되었습니다~!~");
    }
}
public class Main {
    public static void main(String[] args) {
        
        StaticFactoryMethod5 staticFactoryMethod5 = StaticFactoryMethod5.getNewInstance();
        staticFactoryMethod5.getName();
    }
}

정적 팩토리 메소드의 단점

1. 상속을 하려면 public이나 protected 생성자가 필요하다.

정적 팩토리 메소드만 사용하게 하려면 기존 생성자는 private 으로 해야하고 상속할 수 없다.

2. 정적 팩토리 메소드는 프로그래머 찾기 어렵다.

new 를 통한 인스턴스화는 모든 개발자가 알고 있지만, 메소드를 통해 제공한다면 찾아야한다.
개발자가 임의로 만든 정적 팩토리 메소드 특성상, 다른 개발자들이 사용 시 정적 팩토리 메소드를 찾기 어려울 수 있다.


🎈귀중한 참고자료🎈
참고자료1
참고자료2
참고자료3

profile
일취월장 하며 성장! 중! 공부한 것을 기록하자(^∀^●)ノシ

0개의 댓글