2017년 4월 16일 일요일

effective java 규칙 2. 생성자 인자가 많을 때는 Builder 패턴 적용을 고려하라

객체를 생성할때 객체에서 사용할 멤버변수(상태 멤버변수는 아님 - 즉 사용 시점에 따라 값이 달라지는 멤버변수가 아닌)를 외부로 부터 받아야 하는 객체일 경우 생성자를 통하거나 객체 생성 후 setter 메서드를 통해 인자를 전달받는다.
이 멤버변수가 모두 필수 인자일 경우는 별 문제가 되지 않는다. 하나의 생성자에 인자를 모두 포함시키면 된다.
다만 인자의 갯수가 많아지면 가독성에 문제가 생기고 클라이언트 코드에서 객체 생성 코드를 작성하기가 어려워진다.
생각해보라 그 많은 인자를 생성자 인자 순서에 맞게 작성하는 것이 쉽지도 않고 인자타입이 같은 인자들의 순서를 잘 못 기입해도
컴파일러는 알지 못하며 런타임 시점에 문제가 생기게 되어 문제를 발견하기 어려워진다.
위 문제말고도 또 하나의 문제가 발생할 수 있다.

또한 객체의 멤버변수가 모두 필수 인자가 아니라 필수 인자와 선택적 인자가 혼재 되어있을때의 객체 생성시에도 문제가 발생한다.
필수필드와 선택적 필드가 같이 있는 클래스의 객체를 생성하는 방법은 보통 다음 두가지 방법을 사용한다.

1. 하나는 점층적 생성자 패턴 (telescoping constructor pattern) 을 사용하는 것이고
2. 다른 하나는 자바빈 패턴을 사용하는 것이다.

점층적 생성자 패턴은 
필수 인자값은 클래스에 필수 인자를 모두 받는 생성자를 하나 만들고
선택적 인자를 하나 받는 생성자를 추가하며 추가된 생성자 안에 두개의 선택적 인자를 받는 생성자를 추가하는 방식으로 생성자를 쌓아 올리듯 추가하는 방식이다.
이 방식은 선택적 인자의 갯수 만큼 생성자가 증가하며 설정할 필요가 없는 필드에도 인자가 전달된다.
그리고 이방식은 인자가 많을 경우의 문제점인 

가독성에 문제가 생기고 
클라이언트 코드에서 객체생성 코드를 작성하기가 어려워지며 
인자의 순서를 혼동할 가능성이 많고 
타입이 같은 인자의 순서를 혼동할 경우 컴파일러는 알지 못하며 런타임 시점에 오류가 발생할 수 있어 오류를 찾아내기가 어려워지는 문제
” 
이 전혀 해결되지 않는다.
다만 생성자를 통하여 객체를 생성하기 때문에 안정성은 보장이 된다. (멤버변수의 final 과 변경 불가능 객체로 생성 = 객체 일관성 향상)

* 점층적 생성자 패턴을 적용한 클래스
public class Telescoping {

// 필수
private final int year;
private final int month;
private final int day;

// 선택
private final int hour;
private final int minute;
private final int second;


public Telescoping(int year, int month, int day) {
this(year, month, day, 0);
}

public Telescoping(int year, int month, int day, int hour) {
this(year, month, day, hour, 0, 0);
}

public Telescoping(int year, int month, int day, int hour, int minute) {
this(year, month, day, hour, minute, 0);
}

/**
* 인자가 많아 질수록 인자의 순서를 혼동하기 쉽고 사용하기 어려워짐
* @param year
* @param month
* @param day
* @param hour
* @param minute
* @param second
*/
public Telescoping(int year, int month, int day, int hour, int minute, int second) {
this.year = year;
this.month = month;
this.day = day;
this.hour = hour;
this.minute = minute;
this.second = second;
}

}

두번째 대안인 자바빈(JavaBeans) 패턴은
인자가 없는 (또는 필수 인자만 받는) 생성자를 호출하여 객체를 생성한 다음 설정 메서드(setter method)를 통해 멤버변수 필드의 값을 채우는 것이다.
얼핏 보기에도 이 방식은 점층적 생성자 패턴에서 발생할수 있는 인자의 순서를 혼동하는 문제점도 해결되고 객체를 생성 하기도 쉬워 대안으로 선택하기 쉽다. 
이 패턴의 또 하나의 특징은 멤버변수를 setter 메서드로 설정하기 때문에 멤버변수의 final 지정이 불가능하다.
생성자로 인자를 받지 않기때문에 (필수 인자를 받는 생성자는 논외) 멤버변수는 setter 를 통해 값을 받을 수 밖에 없고 그렇기 때문에 멤버변수에 final 지정이 불가능하다.
이점이 첫번째 객체 생성 방식인 점층적 생성자 패턴과의 또다른 차이점이다.
이 차이점으로 인해 이 자바빈 패턴에는 심각한 단점이 발생한다.
객체 일관성(consistency) 훼손이 가능해지며 항상 변경 가능한 객체가 만들어지기 때문에 변경 불가능(immutable) 클래스를 만들 수 없다 (스레드 안정성을 제공하려면 더 많은 작업이 필요함). 
추가적인 작업을 통하여 일관성을 향상 시킬수는 있지만 매우 까다로우며 거의 쓰이지도 않는다.
그리고 또다른 단점은
1회의 함수(생성자) 호출로 객체 생성을 끝낼 수가 없다는 점이다. 이 단점 역시 객체 생성후 추가 객체 값 설정 작업으로 인해 일관성 문제를 야기 시킨다.

public class JavaBean {

// 필수 - final 선언 하지 못함
private int year;
private int month;
private int day;

// 선택 - final 선언 하지 못함
private int hour;
private int minute;
private int second;

public JavaBean() {
}

public int getYear() {
return year;
}

public void setYear(int year) {
this.year = year;
}

public int getMonth() {
return month;
}

public void setMonth(int month) {
this.month = month;
}

public int getDay() {
return day;
}

public void setDay(int day) {
this.day = day;
}

public int getHour() {
return hour;
}

public void setHour(int hour) {
this.hour = hour;
}

public int getMinute() {
return minute;
}

public void setMinute(int minute) {
this.minute = minute;
}

public int getSecond() {
return second;
}

public void setSecond(int second) {
this.second = second;
}

public static void main(String[] args) {

/*
가독성은 향상되나 객체 생성을 1회에 끝낼 수 없고
멤버변수를 setter 로 추후에 셋팅이 가능해 일관성이 훼손될 수 있다
*/
JavaBean j = new JavaBean();
j.setYear(2017);
j.setMonth(4);
// ...
j.setSecond(17);

// 일관성 훼손 가능
// j.setYear(2018);
}
}


위 두 패턴의 단점을 개선한 세번째 대안은 빌더(Builder) 패턴이다.
이 패턴은 점층적 생성자 패턴의 안정성과 자바빈 패턴의 가독성을 결합한 대안이다.
기본적인 생성 방법은 코드로 설명하겠다.

* 빌더 패턴을 적용한 클래스
public class SomePerson {

// 필수인자 final 선언가능
private final String name;
private final Date birthday;

// 선택적인자 final 선언가능
private final String address;
private final int height;
private final String socialStanding;
private final Boolean isCyber;

/**
* builder 를 통해서 만 객체 생성할 수 있도록 private 선언
* 빌더 객체에서 실제 객체로 인자 복사
* @param builder
*/
private SomePerson(Builder builder) {
this.name = builder.name;
this.birthday = builder.birthday;

this.address = builder.address;
this.height = builder.height;
this.socialStanding = builder.socialStanding;
this.isCyber = builder.isCyber;
}

public static class Builder {
private String name;
private Date birthday;
private String address;
private int height;
private String socialStanding;
private Boolean isCyber;

/**
* 필수 인자 값은 생성자로 설정
* @param name
* @param birthday
*/
public Builder(String name, Date birthday) {
this.name = name;
this.birthday = birthday;
}

public Builder address(String address) {
this.address = address;
return this; // builder 객체 자신을 return
}

public Builder height(int height) {
this.height = height;
return this; // builder 객체 자신을 return
}

public Builder socialStanding(String socialStanding) {
this.socialStanding = socialStanding;
return this;
}

public Builder nomalPeople(String address, int height) {
this.address = address;
this.height = height;
return this;
}

public Builder rankPeople(String socialStanding, String address, int height) {
this.socialStanding = socialStanding;
this.address = address;
this.height = height;
return this;
}

public Builder cyberPeople(String address, int height) {
this.address = address;
this.height = height;
this.isCyber = Boolean.TRUE;
return this;
}

public Builder realPeople(String address, int height) {
this.address = address;
this.height = height;
this.isCyber = Boolean.FALSE;
return this;
}

// 최종적으로 변경 불가능 객체 생성
public SomePerson build() {
return new SomePerson(this);
}
}
}

* 객체를 생성하려는 클라이언트 클래스
public class MakePersonMain {

/**
* 점층적 생성자 패턴과 비교해봤을때
* 빌더패턴으로 객체를 생성시 인자의 이름이 있으므로 인자를 혼동하지 않는다. (자바빈 패턴 장점)
* 또한 빌더패턴으로 생성된 객체는 변경불가능한 객체로 생성이 가능하다. (점층적 생성자 패턴 장점)
* 그리고 빌더 패턴이 갖는 또 한가지의 장점은 선택적 인자를 필요한 만큼 포괄적으로 설정이 가능하며
* 여러 성격의 객체를 만들수 있다.
* 하나의 빌더로 여러 객체를 생성할 수 있다.
* @param args
*/
public static void main(String[] args) {

// 기본생성 (필수인자)
SomePerson person = new SomePerson.Builder("홍길동", new Date()).build();

// 선택적 인자 포함 생성
SomePerson personExt = new SomePerson.Builder("홍길동2", new Date())
.address("조선...").height(160).build();

// 일반인 - 선택적 인자 포괄 생성
SomePerson personExt2 = new SomePerson.Builder("홍길동", new Date())
.nomalPeople("한양", 170).build();
// 양반 - 객체 생성시 명시적으로 객체의 특성을 나타내도록 생성할 수 있다.
SomePerson nobility = new SomePerson.Builder("황희", new Date())
.rankPeople("영의정", "한양", 166).build();

// 하나의 빌더로 여러 객체 생성 가능, 어떤 필드의 값은 자동으로 채움(isCyber 필드와 같은)
SomePerson.Builder builder = new SomePerson.Builder("메트릭스-네오", new Date());
SomePerson cyberMan = builder.cyberPeople("뉴욕", 195).build();
SomePerson realMan = builder.realPeople("지하세계", 185).build();

}

}

요약

필수 인자와 선택적 인자가 있을때 점층적 생성자 패턴
    - 필수 인자들을 받는 생성자를 정의, 선택적 인자를 받는 추가적 생성자들을 쌓아 올리듯 추가
    - 단점 1. 인자 수가 늘어나면 클라이언트 코드를 작성하기가 어려워지고 가독성이 떨어짐
    - 단점 2. 설정할 필요가 없는 필드에도 인자를 전닳해야함
    - 단점 3. (많은 선택적 인자를 받는) 생성자 호출시 인자의 타입이 같다면 인자의 순서를 잘못 넣더라도
    컴파일시점에 오류를 검출할 수가 없고 런타임 시점에 문제가 발생하여 오류의 발견이 어려움

    - 장점 1. 생성된 객체의 일관성 = 생성자로 객체를 생성하기 때문에 누락된 값 없이 의도한 객체를 생성

생성자에 전달되는 인자 수가 많을 때
자바빈 패턴
    - 점층적 생성자 패턴의 단점 3. 을 보안
    - 인자 없는 생성자를 호출하여 객체부터 생성하고 설정 메서드(setter methods)들을 호출하여 값을 채움
    - 단점 1. 1회의 호출로 객체 생성을 끝낼 수 없음 = 객체 일관성(consistency)이 일시적으로 깨짐
    - 단점 2. 변경 불가능 클래스를 만들 수 없음 = 스레드 안정성 제공 하기 어려움
    - 단점 3. 누락된 필드값이 발생할 수 있음 = 런타임 시점에 오류 발생, 오류의 발견이 어려움

    - 장점 1. 쉬운 객체 생성, 가독성 향상

빌더 패턴
    - 점층적 생성자 패턴의 안정성 + 자바빈 패턴의 가독성
    - 장점 1. 불필요한 생성자를 만들지 않고 객체 생성 (또는 필수 인자만 받도록)
    - 장점 2. 인자의 순서 무관
    - 장점 3. 가독성 좋음
    - 장점 4. 안정성 (변경 불가능 객체로 생성가능)
    - 장점 5. 객체 생성시 객체의 특성을 나타낼 수 있음

    - 단점 1. 빌더 객체를 추가적으로 생성해야함
    - 단점 2. 코드량 증가

결론
    - 빌더 패턴을 상황에 맞게 적절히 수정해서 사용
        실무에서 멤버변수가 많은 객체를 생성시 항상 빌더 패턴을 사용하는 것은 아니다. 
        일단 DTO 나 VO 등 data entity 인 경우 자바빈 패턴을 사용하는 경우가 더 많고 편리할 때가 많다.
        그 이유는 orm 이나 sqlmap 의 조회 결과를 담을때 builder 패턴을 사용하기가 까다롭다.  mybatis resultmap 의 association 
        빌더패턴으로 객체를 구현하는 것은 인자(인자로 값을 전달 받는 클래스의 멤버변수)가 얼마나 많은지 
        그리고 클라이언트에서 해당 객체를 생성할때 가독성의 중요도등을
        파악해 상황에 맞게 사용하면 될 것 같다.
    - 인자가 많은 생성자를 가진 클래스를 설계할 때, 특히 대부분의 인자가 선택적일 때 유용함
    - 변경이 가능해야 하거나 상태값을 나타내는 변수는 setter method 제공
    - 그 이외의 상황에서는 final 설정 또는 변경 불가능하도록 만듬
    - 빌더 클래스의 설정 메서드는 만드려는 객체를 잘 설명할 수 있도록 만듬, 상황에 따라 여러 인자를 받는 설정 메서드를 만듬
    즉 각각의 비즈니스에 사용되는 인자를 묶어 각 비즈니스를 해결하는 객체를 만듬
    예) Sample s = new Sample.Builder(필수인자, 필수인자..).someBusiness(선택적인자, ...).build();

댓글 없음:

댓글 쓰기

Intelij 설정 및 plugin

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