2017년 4월 13일 목요일

effective java 규칙 1. 생성자 대신 정적 팩토리 메서드

일반 생성자로 객체를 생성할때 발생할 수 있는 문제점


1. class 의 생성자는 동일한 signature 별로 하나의 생성자만 가질수 있다.
(signature : 매개변수의 구성을 뜻하며 자료형, 갯수, 순서에 따라 달라진다.)


2.다른 초기화 기능이나 목적을 가진 객체를 생성하기 위해 생성자 매개변수의 순서를 바꾸는 방법으로 생성자를 overloading 한다.


3. 이렇게 만들어진 생성자를 사용하는 코드를 작성하려는 사람은 코드나 api 설명을 참조하지 않고서는 생성자가 만드려는 객체가 어떤 초기화 기능을 하는지 파악하지 못한다.

아래와 같은 class 가 있을때

public class Person {

private int age;
private int year;
private String name;

// 일반 생일자
public Person(int age, int year, String name) {
this.age = age;
this.year = year;
this.name = name;
}

// 빠른 생일자
public Person(String name, int age, int year) {
this.name = name;
this.age = age + 1;
this.year = year;
}
}

빠른 생일자 객체와 일반 생일자 객체를 생성하려고 할때 아래와 같은 코드로 생성할 수 있다. 


class PersonMain {

public static void main(String[] args) {

Person man =
new Person(21, 1990, "홍길동");
Person earlyBirthMan = new Person(빠른 홍길동", 20, 1991);
}
}

하지만 Person class 코드를 보지 않고서는 빠른생일자와 일반생일자를 구별하여 객체를 생성할 수 없다.


(장점 1) 반면 정적 팩터리 메서드는 생성자의 class 명이 아닌 메소드 명을 명명할수 있다.

팩터리 메서드의 이름으로 생성하는 객체의 특성을 나타낼수도 있다.

또 같은 signaure 를 가진 생성자는 오로지 하나 이지만 팩터리 메서드로는 메서드명만 달리하면 그 제한이 없다.

따라서 같은 시그너처를 갖는 생성자를 여러개 정의할 필요가 있을때와

생성자마다 생성할 객체의 특성을 나타낼 필요가 있을때 

정적 팩터리 메서드로 생성자를 대신하여 객체를 생성하는 것이 좋다.

아래는 정적 팩토리 메서드를 사용한 수정한 코드이다.

class ImprovedPerson {

private int age;
private int year;
private String name;

private ImprovedPerson(int age, int year, String name){
this.age = age;
this.year = year;
this.name = name;
}
// 일반생일자
public static ImprovedPerson getPerson(int age, int year, String name) {
return new ImprovedPerson(age, year, name);
}

// 빠른생일자
public static ImprovedPerson getEarlyBirthPerson(String name, int age, int year) {
return new ImprovedPerson(age + 1, year, name);

}
}

class PersonMain {

public static void main(String[] args) {

ImprovedPerson man = ImprovedPerson.getPerson(21, 1990, "홍길동");
ImprovedPerson earlyBirthMan = ImprovedPerson.getEarlyBirthPerson(20, 1991, "빠른 홍길동");
}
}
(장점 2) 또한 생성자와는 달리 호출할 때마다 새로운 객체를 생성할 필요가 없다. 
- java.lang.Boolean.class 참고

이러한 장점을 활용한 패턴이 싱글턴 패턴이다.

class Sun {

private static Sun sun = new Sun();

// 생성자로 객체 생성 불가능하도록
private Sun(){}

// 항상 같은 객체 반환
public static Sun getInstance() {
return sun;
}
}
싱글턴 뿐만 아니라 몇몇의 만들어진 객체를 pool 형태로 캐시 해놓고 재사용도 가능하다.
동일한 객체가 요청되는 일이 잦고 객체를 만드는 비용이 클 때 적용하면 성능이 크게 개선된다.
이렇게 어떤 시점에 어떤 객체가 얼마나 존재할지를 정밀하게 제어가 가능하다.
(이런 기능을 갖춘 클래스는 개체 통제 클래스-instance-controlled class- 라고 부른다.)

그리고 생성자로 생성하는 객체는 자신의 자료형 객체만 반환하지만 
(장점 3) 정적 팩터리 메서드로 생성하는 객체는 반환값 자료형의 하위 자료형 객체를 반환 할 수 있다.
(정적 팩터리 메서드가 반환하는 객체의 클래스가 public 일 필요도 없다.)


class Star {

// Sun class 에서 extends 하기 때문에 private 선언 할 수 없음
Star(){}

public static Star getSun() {
return Sun.getInstance();
}

}


class Sun extends Star {

private static Sun sun = new Sun();

// 생성자로 객체 생성 불가능하도록
private Sun(){
}

// 항상 같은 객체 반환
public static Sun getInstance() {
return sun;
}
}


위 코드를 보면 Star의 getSun 팩터리 메서드를 사용해 Star class 에서 하위 자료형인 Sun 객체를 생성한다.
이러한 유연성을 활용하면 public 으로 선언되지 않은 클래스의 객체를 반환하는 api를 만들수 있고
공개되지 않은 클래스에서 구현을 하기 때문에 구현 세부사항을 감출 수 있어 캡슐화가 가능하다.

(
다만 위 Star class 코드 자체는 생성자로도 객체를 생성할 수 있는 문제가 있다. 차라리 Star 를 interface 로 만들고
Sun 에서 implement 한 후 StarBuilder (api class) 라는 클래스에서 Sun 객체를 반환하는 팩터리 메서드를 구현하는 방법이 좋다.

* 샘플코드는 추후에 작성 

interface Star {
void hello();
}

public class StarBuilder {

private StarBuilder(){}

public static Star getSun() {
return Sun.getInstance();
}
public static Star getMoon() {
return Moon.getInstance();
}
}


class Sun implements Star {

private static Sun sun = new Sun();

// 생성자로 객체 생성 불가능하도록
private Sun() {
}

// 항상 같은 객체 반환
public static Sun getInstance() {
return sun;
}
@Override
public void hello() {
System.out.println("hi sun");
}
}


class Moon implements Star {

private static Moon moon = new Moon();

// 생성자로 객체 생성 불가능하도록
private Moon() {
}

// 항상 같은 객체 반환
public static Moon getInstance() {
return moon;
}

@Override
public void hello() {
System.out.println("hi moon");
}
}

public class Client {
public static void main(String[] args) {
Star s = StarBuilder.getSun();
Star m = StarBuilder.getMoon();

s.hello();
m.hello();
}
}
)

이해를 돕기 위해 컬렉션 프레임워크 (java.util.Collections) 참조하겠다.

public static <E> Set<E> newSetFromMap(Map<E, Boolean> var0) {
return new Collections.SetFromMap(var0);
}


private static class SetFromMap<E> extends AbstractSet<E> implements Set<E>, Serializable {
private final Map<E, Boolean> m;
private transient Set<E> s;
private static final long serialVersionUID = 2454657854757543876L;

SetFromMap(Map<E, Boolean> var1) {
if(!var1.isEmpty()) {
throw new IllegalArgumentException("Map is non-empty");
} else {
this.m = var1;
this.s = var1.keySet();
}
}
………………
}

이렇게 newSetFromMap 메서드에 의해 반환된 객체 (SetFromMap) 은 Set interface 를 구현한 객체이고 
Set 인터페이스만 보고 클라이언트 코드를 작성한다. 

또한 팩터리 메서드에 전달되는 인자에 의해 반환되는 객체를 달리 할 수 도 있다. java.util.EnumSet 참조


public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> var0) {
Enum[] var1 = getUniverse(var0);
if(var1 == null) {
throw new ClassCastException(var0 + " not an enum");
} else {
return (EnumSet)(var1.length <= 64?new RegularEnumSet(var0, var1):new JumboEnumSet(var0, var1));
}
}

마지막으로 팩터리 메서드를 사용하면 얻게되는 장점은
(장점 4) 형인자 자료형(parameterized type) 객체를 좀더 편리하게 생성할 수 있다.

java.util.Map 의 interface 를 보면 아래와 같이 선언되어있다.

public interface Map<K,V> {

따라서 맵객체를 uncheck warning 없이 사용하려면 아래와 같이 생성해야 한다.
Map<String, Integer> m = new HashMap<String, Integer>();
이렇게 일반적인 생성시 Map 의 형인자 자료형을 HashMap 생성시에도 작성해줘야 한다.

그렇지만 Guava 의 Maps 유틸처럼(com.google.common.collect) 

public static <K, V> HashMap<K, V> newHashMap() {
return new HashMap();
}

위와 같이 hashmap 의 팩터리 메서드가 정의 되어 있으면 형인자가 어떤식으로 복잡하게 되어있던 변수 선언부에만 작성하면 된다.
Map<String, List<List<List<String>>>> crazyVariable = Maps.newHashMap();
또한 java 1.7 이후 부터는 형식 추론이 가능해 아래와 같은 방법도 가능하다.
Map<String, List<List<List<String>>>> crazyVariable = new HashMap<>();

물론 단점도 있다.

(단점 1. ) 정적 팩터리 메서드만 있는 클래스는 public 이나 protected 로 선언된 생성자가 없으면 하위 클래스를 만들 수 없다.
(단점 2. ) 다른 static 메소드와 쉽게 구별할 수 없다.
그러나 명명 convension 에 의해 구별에 도움을 얻을순 있다.

- valueOf : 인자로 주어진 값(매개변수)과 같은 값을 갖는 객체를 반환, 형변환 메서드
- of : valueOf 축약형, EnumSet 참고
- getInstance : 싱글턴 패턴을 따를 경우, 인자 없이 항상 같은 객체를 반환
- newInstance : 항상 새로운 객체 반환
- getType : getInstance 와 같지만 팩터리 메서드가 다른 클래스 (util class) 에 있을때 getType 의 Type 은 반환되는 객체 타입
- newType : newInstance 와 같고 getType 처럼 팩터리 메서드가 다른 클래스에 있을때

요약
- 정적 팩터리 메서드와 public 생성자는 용도가 서로 다르며 그 차이와 장단점을 이해하는 것이 중요

- 정적 팩터리 메서드가 효과적인 경우가 많으니 무조건 public 생성자를 만들기 보다는 팩터리 메서드를 고려

댓글 없음:

댓글 쓰기

Intelij 설정 및 plugin

1. preferences... (settings...) Appearance & Behavior > Appearance - Window Options        ✓   Show memory indicator Editor &...