본문 바로가기
Java

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

by 성건희 2021. 1. 23.
반응형

시작하기 전에

이펙티브 자바의 저자는 '조슈아 블로크'이다.

처음에는 누군지 몰랐는데 검색해보니 자바의 어머니라고 불리는 어마어마한 분이셨다.

(물론 여자는 아니심... 자바의 아버지가 제임스 고슬링이라 그렇게 지은듯?)

 

(우리가 흔히 사용하는 Override 어노테이션의 author에서도 조슈아 블로크를 볼 수 있다.)

여튼,, 자바 개발자라면 꼭 읽어봐야 할 필독서로 책의 난이도가 있다는 말을 주변에서 들었는데 천천히 꾸준하게 읽어보려한다.

책을 보고도 잘 와 닿지 않는 부분이 있어, 직접 코드를 작성하면서 이해하려고 함.

 

인스턴스를 생성하는 방법

클라이언트가 클래스의 인스턴스를 얻는 전통적인 수단은 2가지가 있다.

 

  • public 생성자
  • static factory method (정적 팩토리 메서드)

여기서 우리는 정적 팩토리 메서드에 대해 알아볼 것이다.

 

정적 팩토리 메서드

정적 팩토리 메서드란 메서드를 static(정적)으로 만들어 사용하는 것을 말한다.

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

정적 팩토리 메서드는 장단점을 모두 가지고 있다.

 

정적 팩토리 메서드의 장점

1. 이름을 가질 수 있다

아래 코드를 보자.

class User {
    private String name;

    private User(String name) {
      this.name = name;
    }

    public static User createUser(String name) {
      return new User(name);
    }

    public String getName() {
      return name;
    }
}

name을 가지고 있는 간단한 User 클래스이다.

하지만 다른점은 생성자를 private으로 막고, 정적 팩토리 메서드로 User를 생성하고 있다는 점이다.

따라서 User를 생성하기 위해서는 아래와 같이 사용해야한다.

User user = createUser("성건희");

 

정적 팩토리 메서드를 사용하지 않았던 기존에는 아래와 같이 생성했을 것이다.

User user = new User("성건희");

 

이처럼 정적 팩토리 메서드로 객체를 생성하는 방법은 메서드의 이름으로 동작을 유추할 수 있기 때문에
개발자가 이해하기 좀 더 편한 것 같다.


한 클래스에 시그니처가 같은 생성자가 여러 개 필요할 것 같다면,
생성자를 정적 팩터리 메서드로 바꾸고 적절한 이름을 지어주는 것이 좋다.

 

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

public class Phone {
    private static Phone phone;

    private Phone() {}

    public static Phone createPhone() {
        if (phone == null) {
            phone = new Phone();
        }
        return phone;
    }
}

Phone 클래스는 기존 생성자를 private으로 막고, 정적 팩토리 메서드로 객체를 생성하는 방식으로 구현되어있다.

정적 팩토리 메서드에서 값이 있으면 새로 만들지 않고 기존 객체를 재사용하기 때문에

불필요한 객체 생성을 피할 수 있고, 자원을 효율적으로 사용할 수 있다는 장점이 있다.

 

@Test
@DisplayName("호출할 때마다 인스턴스를 새로 생성하지 않는다")
void notCreateInstanceIsCalled() {
    Phone phone1 = Phone.createPhone();
    Phone phone2 = Phone.createPhone();

    assertThat(phone1).isSameAs(phone2); // true
}

동일한 객체를 재사용하기 때문에 테스트 코드가 통과한다.

 

값이 있으면 새로 만들지 않고 기존 객체를 재사용하는 이러한 클래스를 인스턴스 통제(instance-controlled) 클래스 라고 한다.

인스턴스를 통제하면 클래스를 싱글턴으로 만들 수도 있고,

인스턴스 불가(인스턴스를 생성하지 못하도록 막는)로 만들 수도 있다.

 

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

처음에는 이게 무슨말인가 싶었는데..

쉽게 말해 생성자는 반환 값을 지정할 수 없지만, 정적 팩토리 메서드는 반환 값을 지정할 수 있다는 말이다.

아래 코드를 보면 좀 더 이해하기 쉬울것이다.

public class Fruit {
    public static Banana createBanana() {
        return new Banana();
    }

    public static Apple createApple() {
        return new Apple();
    }
}

public class Apple extends Fruit {}
public class Banana extends Fruit {}

Fruit 이라는 부모 클래스가 있고, 이를 상속받고 있는 Apple과 Banana 클래스를 부모클래스에서 생성해줄 수 있다.

@Test
@DisplayName("정적 팩토리 메서드는 하위 타입 객체를 반환할 수 있다")
void returnSubtypeObjectWhenUseStaticFactoryMethod() {
    assertThat(Fruit.createBanana()).isInstanceOf(Banana.class); // true
    assertThat(Fruit.createApple()).isInstanceOf(Apple.class); // true
}

 

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

대표적으로 EnumSet 클래스를 예로 들 수 있다.

image

원소가 64개 이하이면 RegularEnumSet, 65개 이상이면 JumboEnumSet 의 인스턴스를 반환한다.

클라이언트는 이 두 클래스의 존재를 모른다.

의존도가 낮기때문에 noneOf 메서드의 요구사항이 변경되어 코드가 수정되어도 문제가 없다. (유지보수 좋음)

 

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

이 부분은 잘 이해가 안갔는데, 다른 블로그를 기웃거리다가 코드보고 약간 이해됨.

public abstract class Company {
    public static Company getInstance(String path) {
        Company company = null;
        try {
            Class<?> childCompany = Class.forName(path);
            company = (Company) childCompany.newInstance();

        } catch (ClassNotFoundException e) {
            System.out.println("클래스가 없습니다.");
        } catch (IllegalAccessException e) {
            e.printStackTrace();
        } catch (InstantiationException e) {
            e.printStackTrace();
        }
        return company;
    }

    public abstract String getCompanyName();
}

Company 클래스는 리플랙션 API를 통해서 자식 클래스를 가져오고, 인스턴스로 생성해주며

실제 클래스가 존재하지 않더라도 예외처리를 통해서 처리가 가능하다는 말로 이해했다.

 

Company 를 상속하는 자식 클래스를 만들어주자.

public class Nhn extends Company {
    @Override
    public String getCompanyName() {
        return "NHN";
    }
}

 

그 후 아래와 같이 테스트 하였다.

@Test
@DisplayName("정적 팩토리 메서드를 작성하는 시점에는 반환할 객체의 클래스가 존재하지 않아도 된다")
void canNotReturnObjectClassWhenUseStaticFactoryMethod() {
    Company nhn = Company.getInstance("item1.Nhn"); // 실제 경로
    assertThat(nhn.getCompanyName()).isEqualTo("NHN"); // true

    Company kakao = Company.getInstance("item1s.kakao"); // 존재하지 않는 path
    assertThat(kakao.getCompanyName()).isEqualTo("KAKAO"); // 에러
}

 

정적 팩토리 메서드 단점

정적 팩토리 메서드를 사용하면 단점도 존재한다.

1. 상속을 하려면 public이나 protected 생성자가 필요하니 정적 팩토리 메서드만 제공하면 하위 클래스를 만들 수 없다.

Fruit 예제를 보자.

public class Fruit {
    private Fruit() {}

    public static Banana createBanana() {
        return new Banana();
    }
}

 

생성자를 private 으로 막고 자식 클래스인 Banana 클래스를 만들었다.

public class Banana extends Fruit {
    public Banana() {
        super(); // 컴파일 에러 발생
    }
}

자식 클래스를 생성할 때는 부모클래스를 먼저 super() 메서드로 생성하는데, 부모의 생성자가 private으로 지정되어있어 컴파일 에러가 발생한다.

따라서 이것을 해결하려면 부모 생성자를 public 또는 protected 로 지정해주어야 한다.

단점이라고 이야기했지만 사실 상속은 의존도를 높이므로 좋지 않다.

컴포지션(조합) 사용을 유도하여 오히려 장점으로 받아들일 수도 있다.

 

참고 ) 컴포지션이란..

public class Banana {
    private Fruit;
}

위 처럼 상속이 아닌 인스턴스 변수로 가져오는 방법이다.

 

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

생성자처럼 API 설명에 명확히 드러나지 않으니 사용자는 정적 팩토리 메서드 방식 클래스를 인스턴스화할 방법을 알아내야 한다.

솔직히 잘 와닿지는 않는다;; 생성자가 API 설명에 드러난다는게 무슨말인지 모르겠다. 자바독을 많이 안써봐서 그런가 = _ =

느낌상으로는 자바독에 생성자 설명란이 있는데,

정적 팩토리 메서드는 생성자가 없으므로 각각의 메서드를 일일히 확인해주어야 한다는 말인듯

 

정리

정적 팩터리의 장단점을 살펴보았는데,

정적 팩터리를 사용하는데 주는 이점이 더 많으므로 무작정 public 생성자를 만들던 습관을 가지고 있다면 개선해보는게 어떨까.

 

참고

반응형

댓글