< effective java 3편과 백기선님의 youtube 강의를 토대로 쓴 글입니다. >

지난 시간에 했던 내용

  • 점층적 생성자 패턴 : 매개변수가 0~N개까지 모두 선언해서 사용하는 것

  • 자바빈즈 패턴 : 매개변수 없는 생성자로 객체 만들고, setter를 사용하는 것

  • 빌더 패턴 : 클라이언트 코드에서 필요한 객체를 직접 생성하는 대신, 그 전에 필수 인자들을 전달하여 빌더 객체를 만든 뒤, 빌더 객체에 정의된 설정 메서드들을 호출하여 인스턴스를 생성하는 것



오늘의 주제 - 생성자 대신 정적 팩터리 메서드를 고려하라.   (static factory method)



정적 팩터리 메서드란 무엇인지 코드를 보면서 알아보자.

<코드 1.>

public class Person {

   private String name;

   private int age;

 

   public String getName() { return name; }

   public void setName(String name) { this.name = name; }

 

   public int getAge() { return age; }

   public void setAge(int age) { this.age = age; }

 

   private Person(){ }

   public static Person newPerson(){

       return new Person();

   }

 

   public static void main(String[] args) {

       Person conatuseus=Person.newPerson();

   }

}

위와 같이 생성자가 있고, public static인 newPerson 메서드가 있다.

newPerson 메서드는 Person 객체를 생성해서 반환한다.

main에서 Person 객체를 “Person.newPerson()” 이렇게 생성할 수 있다.

이렇게 정적 팩터리 메서드는 public static 팩토리 메서드를 사용해서 해당 클래스의 인스턴스를 만드는 방법이다.

쉽게 말해서 객체를 생성하는 메서드를 만들고, static으로 선언하는 기법이다.



장점

1. 이름을 가질 수 있다.

생성자에 넘기는 매개변수와 생성자 자체만으로는 반환될 객체의 특성을 제대로 설명하지 못한다.

 

반면 정적 팩터리는 이름만 잘 지으면 반환될 객체의 특성을 쉽게 묘사할 수 있다.

<코드 1>에서 Person클래스에 다음과 같은 메서드를 추가했다고 생각해보자.

 

public static Person withName(String name){

       Person p=new Person();

       p.setName(name);

       return p;

   }

 

이 메서드를 사용하면 main에 Person 객체를 생성할 때 다음과 같이 생성할 수 있다.

 

Person firstPerson=Person.withName(“conatuseus”);

 

객체를 생성하는 위의 코드만 보더라도 이름이 conatuseus인 Person이라는 것을 알 수 있다. 따라서 사용하기 쉽고, 읽기 편하다.

 

또, 생성자는 시그니처에 제약이 있다.

똑같은 타입을 파라미터로 받는 생성자를 두 개 만들 수 없다. 이런 경우에도 public static 팩토리 메서드를 사용하는 것 유용하다.

 

public Person(String name){

         this.name=name;

}

public Person(String address){

         this.address=address;

}

위와 같이 똑같은 타입을 파라미터로 받는 생성자 같이 만들 수 없다.

따라서 이것을 public static 팩토리 메서드로 만들어보면 다음과 같다.

 

public Person withName(String name){

         Person p=new Person();

         p.setName(name);

         return p;

}

 public Person withAddress(String address){

         Person p=new Person();

         p.setAddress(address);

         return p;

}



2. 반드시 새로운 객체를 만들 필요가 없다.

불변(immutable) 클래스인 경우나 매번 새로운 객체를 만들 필요가 없는 경우에 미리 만들어둔 인스턴스 또는 캐시해둔 인스턴스를 반환할 수 있다.

 

방법은 다음과 같다.

 

   public static final Person person=new Person();

   public static Person getPerson(){

       return person;

   }

 

public static final로 person 인스턴스를 생성해놓고 public static 메서드인 getPerson으로 가져다가 쓰는 것이다.




3. 반환 타입의 하위 타입 인스턴스를 만들 수 있다.

 

public class Person {

   private String name;

   private int age;

   private String address;

 

   private Person(){}

 

   static class Student extends Person{

   }

   static class Teacher extends Person{

   }

 

   public static Person getStudent(){

       return new Student();

   }

   public static Person getTeacher(){

       return new Teacher();

   }

   

   public static void main(String[] args) {

       Person conatuseus=Person.getStudent();

       Person dynamiseus=Person.getTeacher();

   }

}

이 코드에서 Student, Teacher 클래스는 Person의 하위 타입이다.

main에서 conatuseus는 Student 타입 인스턴스이고, dynamiseus는 Teacher 타입 인스턴스이다.

이 능력은 반환할 객체의 클래스를 자유롭게 선택할 수 있게 하는 ‘엄청난 유연성’을 가진다.




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

 3번 장점과 같은 이유로 객체의 타입이 다를 수 있다. 매개변수에 따라 선택적인 Sub class 객체를 반환할 수 있다.

이 예는 Enum을 자세히 공부하고 다시 적어보자.




단점.

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

'Java' 카테고리의 다른 글

점층적 생성자 패턴, 자바빈즈 패턴, 빌더 패턴  (0) 2019.03.13
Annotation (어노테이션)  (0) 2018.11.13

생성자에 선택적 매개변수가 많다면 어떻게 해결할까?


1) 점층적 생성자 패턴(telescoping constructor pattern)


피자를 만든다고 생각해보자. (= Pizza Class가 있다.)

피자를 만드는데 사이즈와 조각 수는 필수이다. (=size와 piece는 필수 매개변수이다. )

피자를 만드는데 햄, 치즈, 고기, 소스는 선택사항이다. (=ham, cheese, beef, sauce 는 선택적 매개변수다.)


이 클래스에서 우리는 생성자를 다음과 같이 구현 할 수 있다.


‘필수 매개변수만 받는 생성자’                  - (size, piece )

‘필수 매개변수와 선택 매개변수 1개를 받는 생성자’     -  (size, piece, ham)

‘필수 매개변수와 선택 매개변수 2개를 받는 생성자’     - (size, piece, ham, cheese)

…….

‘필수 매개변수와 선택 매개변수 전체를 받는 생성자’    -(size, piece, ham, cheese, beef, sauce)


이런식으로 선택 매개변수가 없는 생성자부터 선택 매개변수를 전부 받는 생성자까지 늘려나가는 방식을  ‘점층적 생성자 패턴’ 이라고 한다.




만약 ( Size : 10, Piece : 4, Beef : 20 )인 Pizza 객체를 만들어야 한다면 다음과 같이 객체를 생성할 수 있다.

<Ex 1.1>




< 장점 >

( 10, 5 , 햄 없음, 치즈 없음, 고기 없음, 소스 없음 ) 과 같은 Pizza 객체를 생성할 때

new Pizza(10,5) 으로 쉽게 생성할 수 있다.


< 단점 >

<Ex1.1> 객체를 만들기위해 ham과 cheese에 0을 넣어줘야하는 불편함이 생긴다.

현재는 beef 앞에 사용하지 않는 선택 매개변수가 햄과 치즈 2개 밖에 없지만, 프로그램을 짜다보면 그 수는 훨 씬 늘어난다.

따라서 점층적 생성자 패턴을 쓸 수는 있지만, 매개변수 개수가 많아지면 클라이언트 코드를 작성하거나 읽기 어렵다. 코드를 읽을 때 각 값의 의미가 무엇인지 헷갈릴 수 있고, 매개변수가 몇 개 인지도 주의해서 세어 보아야 할 것이다.







2) 자바빈즈 패턴 (JavaBeans pattern)

자바빈즈 패턴은 매개변수가 없는 생성자로 객체를 만든 후, 세터 메서드를 호출해 원하는 매개변수의 값을 설정하는 방식이다.


점층적 생성자 패턴 구현했던 피자 클래스를 자바빈즈 패턴으로 변환한 아래의 코드를 보자.

점층적 생성자 패턴에서 봤던 피자 클래스가 위와 같이 바뀌었다.

매개변수가 없는 생성자 1개와 Setter 6개로 구현된 클래스이다.


사용 :



점층적 생성자 패턴의 단점은 사라지고, 인스턴스를 만들기 쉽고, 더 읽기 쉬운 코드가 되었다.

하지만 위의 자바빈즈 패턴은 심각한 단점이 있다. 뭘까?


 자바빈즈 패턴의 단점.

(1). 객체 하나를 만드려면 메서드를 여러개 호출해야한다.

  • 위의 ‘사용’ 예에서 Setter 메서드가 6개나 호출된 것을 볼 수 있다.

  • 따라서 1회의 호출로 객체 생성이 끝나지 않으므로 객체의 일관성(Consistency)이 깨진다.


(2) 일관성이 무너지는 문제 때문에 클래스를 불변으로 만들 수 없으며, 스레드 안전성을 얻으려면 프로그래머가 추가 작업을 해줘야만 한다.









3) 빌더 패턴

  • 점층적 생성자 패턴의 안전성과 자바빈즈 패턴의 가독성을 겸비한 빌더 패턴

  • 클라이언트는 필요한 객체를 직접 만드는 대신, 필수 매개변수만으로 생성자를 호출해 빌더 객체를 얻는다.

그런 다음 빌더 객체가 제공하는 일종의 세터 메서드들로 원하는 선택 매개변수들을 설정한다.

마지막으로 매개변수가 없는 build 메서드를 호출해 드디어 우리에게 필요한 객체를 얻는다.

→ 빌더는 생성할 클래스 안에 정적 멤버 클래스로 만들어두는 게 보통이다.



앞의 예제를 빌더 패턴으로 구현한 코드를 예로 보도록 하자.


Pizza 클래스 내에 정적 멤버 클래스로 Builder 클래스가 있는 구조이다.

Builder 클래스는 필수 매개변수를 가진 생성자가 있다.

그리고 Builder의 세터 메서드들은 빌더 자신을 반환하기 때문에 연쇄적으로 호출할 수 있다.


사용 예를 보자.

 클라이언트는 위와 같이 두 가지 방법으로 사용할 수 있다.

이 클라이언트 코드는 쓰기 쉽고, 읽기 쉽다. 빌더 패턴은 명명된 선택적 매개변수를 흉내낸 것이다.(파이썬과 스칼라에 존재하는)






☞ 빌더 패턴은 계층적으로 설계된 클래스와 함께 쓰기에 좋다.


  • 각 계층의 클래스에 관련 빌더를 멤버로 정의한다.

  • 추상 클래스는 추상 빌더를, 구체 클래스(concrete class)는 구체 빌더를 갖게 한다.

나는 클래스를 계층적으로 설계해본 경험이 부족해서 책에 있는 예제를 보여주고 설명하겠다.


<abstract class Pizza>


< 뉴욕피자 >

Pizza 클래스의 하위 클래스인 뉴욕피자 클래스다.

뉴욕피자는 크기를 필수 매개변수로 받는다.



<칼초네 피자>

Pizza 클래스의 하위 클래스인 칼초네(Calzone) 클래스다.

Pizza 클래스는 소스를 넣을지 않넣을지 선택하는 매개변수를 필수로 받는다.



각 하위 클래스의 빌더가 정의한 build 메서드는 해당하는 구체 하위 클래스를 반환하도록 한다. ( NyPizza.Builder는 NyPizza,  Calzone,Builder는 Calzone를 반환)

이렇게 하위 클래스의 메서드가 상위 클래스의 메서드가 정의한 반환타입이 아닌, 그 하위 타입을 반환하는 기능을 ‘공변환 반환 타이핑’이라 한다. 이 기능을 사용하면 클라이언트가 형변환에 신경쓰지 않고도 빌더를 사용할 수 있다.



사용 예)



빌더 패턴의 단점

  • 객체를 만드려면 빌더부터 만들어야한다.

(빌더 생성 비용이 크지는 않지만 민감한 상황에서는 문제가 될 수 있음)

  • 점층적 생성자 패턴보다는 코드가 장황해서 매개변수가 4개 이상은 되어야 값어치를 한다.

→ 하지만 API는 시간이 지날수록 매개변수가 많아지는 경향이 있음.



정리.

생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다.

특히 매개변수 중 다수가 필수가 아니거나 같은 타입인 경우에.

빌더는 점층적 생성자보다 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.



'Java' 카테고리의 다른 글

정적 펙터리 메서드 (static factory method)  (0) 2019.03.27
Annotation (어노테이션)  (0) 2018.11.13
  1. Annotation 이란?
        - 본질적인 목적은 소스 코드에 메타데이터를 표현하는 것이다. 
            (메타데이터 : 데이터에 대한 데이터, 즉 어떤 목적을 가지고 만들어진 데이터)
        - 리플렉션(reflection)을 이용하면 어노테이션 지정만으로도 원하는 클래스를 주입하는 것 등이 가능하다.
 (리플렉션이란 객체를 통해 클래스의 정보를 분석해 내는 프로그램 기법을 말한다.)
        - 비지니스 로직에는 영향을 주지 않지만, 해당 타겟의 연결 방법이나 소스코드의 구조를 변경 할 수 있다.
        - Annotation의 종류에는 빌트인 어노테이션, 메타 어노테이션, 커스텀 어노테이션이 있다.

       



 2. 빌트인(Built-in) 어노테이션
         
    - 자바 SDK에서 지원하는 어노테이션으로 @Override, @Deprecated, @SuppressWarnings 등이 있다.

  • @Override : 어노테이션은 현재 메소드가 수퍼클래스의 메소드를 오버라이드한 메소드임을 명시한다.
                  만약 수퍼클래스 또는 구현해야할 인터페이스에 해당 메소드가 없으면 컴파일러가 에러를 발생시킨다.

  • @Deprecated : 해당 메소드가 더 이상 사용되지 않음을 표시한다. 
                      만약 사용할 경우 컴파일 경고를 발생시킨다.

  • @SuppressWarnings : 선언한 곳의 컴파일 경고를 무시하도록 한다.
                          Object형을 Element로 하는 컬렉션을 사용하면 컴파일러 경고가 발생하는데, 이 어노테이션을
                        사용하여 프로그래머의 의도적인 Object형 사용임을 알려 경고를 제거할 수 있다.

  • @SafeVarargs : 제너릭 같은 가변인자의 매개변수를 사용할 때의 경고를 무시한다.
  • @FunctionalInterface : 함수형 인터페이스를 지정하는 어노테이션.
                             만약 메소드가 존재하지 않거나, 1개 이상의 메소드가 존재할 경우 컴파일 오류를 발생시킨다.





      

 3. 메타(Meta) 어노테이션

  • @Retention : 자바 컴파일러가 어노테이션을 다루는 방법과 관련이 있다. 
                                        소스파일, 클래스파일, 런타임 중 어느 시점까지 어노테이션을 보유하고 있을 것인지를 결정한다.

  • @Target : 어노테이션이 적용할 위치를 선택한다.  @Target (ElementType.~)
    • ElementType.PACKAGE : 패키지 선언시
    • ElementType.TYPE : 타입 선언 시
    • ElementType.ANNOTATION_TYPE : 어노테이션 타입 선언시
    • ElementType.CONSTRUCTOR : 생성자 선언시
    • ElementType.FIELD : 멤버변수 선언시
    • ElementType.LOCAL_VARIABLE : 지역 변수 선언시
    • ElementType.METHOD : 메소드 선언시
    • ElementType.PARAMETER : 파라미터 선언시
    • ElementType.TYPE_PARAMETER : 파라미터 타입 선언시
    • ElementType.TYPE_USE : 타입선언시
  • @Documented : 해당 어노테이션을 Javadoc에 포함시킨다.
  • @Inherited : 어노테이션의 상속을 가능하게 한다.
  • @Repeatable : 연속적으로 어노테이션을 선언할 수 있게 해준다.


  (커스텀 어노테이션은 다음 글 : )




+ Recent posts