본문 바로가기
Dev/Effective Java

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

by VIPeveloper 2021. 1. 9.
반응형

1. 서론

이팩티브 자바 스터디를 혼자 시작했습니다. 자바-봄 이라는 블로그를 보고 감명받았기 때문입니다. 단지 그 이유 하나입니다. 스터디 후기를 읽어보니 나도 참 기본이 부족하다는 회고를 했습니다. 기본기를 다져서 나쁠 것이 전혀! 없기 때문에 책은 없지만 최고의 선생님인 구글과 함께 시작해보았습니다.

스터디의 모든 내용은 코드를 통해 구현한 것만 이해된다고 가정하고 시작했습니다. 해당 내용은 여기에서 확인할 수 있습니다.

2. 본론

전통적으로 public 생성자를 사용하고 더 나아가 정적 팩터리 메서드를 제공할 수 있다.

이 말이 무엇인지도 이해를 잘 하지 못하는 것이 충격이었다. 답은 역시 구글링.

 

1. 클래스가 기본적으로 인스턴스를 얻기 위해서는 public 생성자를 이용해야 합니다.
2. 하지만, 또 알아두어야 할 기법이 있으니 정적 팩토리 메서드(static factory method)를 이용해서도 제공할 수 있습니다.

 

Q. 정적 팩토리 메서드가 뭐지?

A. 아래의 코드처럼 static이 들어간 메서드입니다.

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

 

정적 팩토리 메서드가 생성자보다 좋은 장점

1. 이름을 가질 수 있습니다.

Q. 그럼 정적 팩토리 메서드는 왜쓰는거지? 생성자로 써도 되잖아!

A. 왜 그런지 답을 해줄게. 먼저, 정적 팩토리 메서드(이하 '정팩메')를 쓰면 이름을 가질 수 있게 됩니다.

Q. 무슨소리야!

A. 코드를 보면, 정팩메를 쓰면 함수 이름만 보고도 어떤 쓰임을 가질지 유추할 수 있게 됩니다.

class Student {
    private String name;
    private int age;

    public Student(){}

    public Student(int age, String name){
        this.name = name;
        this.age = age;
    }


    // 어떤 역할인지 명시적 표현.
    public static Student studentWithNameAndAge(int age, String name) {
        Student student = new Student();
        student.name = name;
        student.age = age;
        return student;
    }
}

정팩매와 생성자 메서드 두개를 구현해보았습니다. 테스트 코드를 통해 확인해보겠습니다.

package com.study;

import org.junit.Test;
import org.junit.jupiter.api.Assertions;

public class StudentTest {

    @Test
    @DisplayName("정팩매 메서드는 이름이 존재한다.")
    public void study_test(){
        Student student1 = new Student(20,"홍길동");
        Student student2 = Student.studentWithNameAndAge(20, "홍길동");

        assertNotEquals(student2,student1);
    }
}

 

 

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

불변 클래스는 인스턴스를 캐싱해서 재활용하기 때문에, 메모리 활용에 더 유리한 장점이 될 수 있습니다. 즉, 반복되는 요청이라면 같은 객체를 반환하기 때문에 인스턴스의 과도한 생성을 막을 수 있습니다.

class Activity {

    private double price;
    private int activityCount;
    
    // public 생성자
    public Activity(){}
    public Activity(double price, int activityCount){
        this.price = price;
        this.activityCount = activityCount;
    }

    // 정팩매
    public static final Activity DISCOUNT_THREE_ACTIVITY = new Activity(10000.0, 3);
    public static Activity setPriceWithActivity(double price, int activityCount) {
        // 생성되어 있는 객체를 할당.
        if (activityCount == 3) {
            return DISCOUNT_THREE_ACTIVITY;
        }
        Activity activity = new Activity();
        activity.price = 20000.0;
        activity.activityCount = activityCount;
        return activity;
    }
}

역시 정팩매와 생성자 메서드 두개를 구현해보았습니다. 테스트 코드를 통해 확인해볼 수 있듯 계속 new로 생성하는 요청이 일어날 경우 5개의 새로운 인스턴스가 생성됩니다. 하지만, 정팩매를 사용하게 되면 하나의 인스턴스만을 반환하게 됩니다.

public class ActivityTest {
    @Test
    @DisplayName("인스턴스 생성 통제")
    public void activity_test(){
        Activity activity1 = new Activity(10000.0, 3);
        Activity activity2 = new Activity(10000.0, 3);

        assertNotEquals(activity1,activity2);

        Activity activity6 = Activity.setPriceWithActivity(10000.0,3);
        Activity activity7 = Activity.setPriceWithActivity(10023.0,3);

        assertEquals(activity6,activity7);
    }
}

이를 인스턴스 통제 클래스라고도 하는데, 통제 클래스는 싱글톤으로 만들 수 있습니다.

 

 

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

Q. 말이 너무 어렵잖아!

A. 생성자는 리턴값이 없죠?

Q. 응

A. 정팩매는 반환값을 유연하게 사용할 수 있습니다. 덧붙여, 반환하는 타입을 상속한 객체까지도 반환할 수 있습니다. 코드를 볼게요.

class Student {
    private String name;
    private int age;

    public Student(){}

    public Student(int age, String name){
        this.name = name;
        this.age = age;
    }
    // 어떤 역할인지 명시적 표현.
    public static Student studentWithNameAndAge(int age, String name) {
        Student student = new Student();
        student.name = name;
        student.age = age;
        return student;
    }
    // 반환 타입의 하위 타입 객체를 반환할 수 있는 능력이 있다.
    public static MiddleSchool middleSchoolStudnent() {
        return new MiddleSchool();
    }
}
class MiddleSchool{
    public String print(){
        return "hello world!";
    }
}

테스트 코드를 보겠습니다. MiddleSchool 객체가 Student의 메서드를 사용하고 있습니다.

    @Test
    @DisplayName("반환 타입의 하위 타입 객체를 반환")
    public void study_test_2(){
        MiddleSchool middleSchool = Student.middleSchoolStudnent();
        assertEquals("hello world!",middleSchool.print());
    }

 

 

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

Q. 이건 다행히 이해가 좀 된다. 매개변수가 뭔지에 따라 클래스를 다르게 반환할 수 있다는거잖아

A. 맞습니다. 이것도 예제를 보시면 됩니다.

class Fruit {
    public static Fruit getFruit(String name) {
        if ("Apple".equals(name)) {
            return new Apple();
        } else if ("Banana".equals(name)) {
            return new Banana();
        } else {
            return new Strawberry();
        }
    }
}
class Apple extends Fruit {
    public String print(){
        return "Apple!!";
    }
}
class Banana extends Fruit {
    public String print(){
        return "Banana!!";
    }
}
class Strawberry extends Fruit {
    public String print(){
        return "Strawberry!!";
    }
}

Q. 어 나 이상한거 찾았어. extends 아까 반환타입 받을 때 없었던 거 같은데, 이번엔 왜 extends 해줬죠? 그 전에! extends가 뭐죠?

A. 음.. 나란놈은 컴공이 맞았을까. 하지만 이제라도 알 수 있으니까.. 다행이라고 생각하며 구글링했다.

 

+) 이상한점 발견.

정팩매 단점을 공부하다가 발견했는데, Fruit은 기본 생성자가 없는데도 잘 동작한다. 왜그럴까?

궁굼했던 나머지 직접 질문을 드려보았습니다.

질문하는 나.

 

 

답변

 

생각해보니 기본 생성자는 파라메터 생성자가 없다면 자동으로 생성해 주는 것인데, 왜 알았는데도 그때는 몰랐던 것일까..? 다시한번 되짚어보는 계기가 되었다. 링크는 여기에서 확인할 수 있다.


더보기

간단하게 정리하자면 추상 클래스와 인터페이스의 차이로 나눌 수 있는데, 먼저 가정을 해야합니다. 먼저 추상클래스부터 이야기해봅시다.

 

- 먼저 추상클래스부터 정의해봅시다

여기에 A, B 두 개발자가 있고, A는 B의 상사라고 생각해 봅시다. A가 B에게 말합니다.

A : 여기 내가 필요한 변수랑 메서드를 몇가지 만들었으니까, 나머지는 B님께서 받아서 처리해주세요!

B : 어떻게 처리하나요?

A : extends 받아서 확장 구현하시면 됩니다.

대략 이런 느낌입니다.

 

- 다음은 인터페이스입니다.

여기에 A, B 두 개발자가 있고, A는 B의 상사라고 생각해 봅시다. A가 B에게 말합니다.

A : 여기에 내가 필요한 함수들 정의했으니, B님께서 로직 짜서 구현해주세요!

B : 어떻게 처리하나요?

A : implements 받아서 구체적으로 구현하시면 됩니다.

 

추상클래스는 이어달리기의 느낌이고, 인터페이스는 철제 인형을 만들 때 뼈대만 만들어놓고 지점토 붙이는 작업을 시키는 것을 말하는 느낌으로 이해했습니다. (맞나?)

이제 정확한 정의를 보며 간단하게 마무리하겠습니다.

추상클래스는 슈퍼 클래스의 기능을 서브 클래스에서 이용하거나 확장하기 위함이고,
인터페이스는 구현한 객체들에 대해서 동일한 동작을 약속하기 위해 존재한다.
출처 : https://gongbu-ing.tistory.com/53

 

테스트 코드를 보겠습니다. 매개변수를 다르게 주었을 뿐인데 다른 객체가 반환되는 것을 볼 수 있습니다.

public class FruitTest {

    @Test
    public void fruit_test(){
        // 입력 매개변수에 따라 매번 다른 클래스의 객체를 반환할 수 있습니다.
        Fruit fruit1 = Fruit.getFruit("Apple");
        assertEquals(fruit1.getClass().getSimpleName(),"Apple");

        Fruit fruit2 = Fruit.getFruit("Banana");
        assertEquals(fruit2.getClass().getSimpleName(),"Banana");

        Fruit fruit3 = Fruit.getFruit("asdfad");
        assertEquals(fruit3.getClass().getSimpleName(),"Strawberry");

        Apple apple = (Apple) Fruit.getFruit("Apple");
        assertEquals(apple.print(),"Apple!!");

        Banana Banana = (Banana) Fruit.getFruit("Banana");
        assertEquals(Banana.print(),"Banana!!");

        Strawberry Strawberry = (Strawberry) Fruit.getFruit("ewfsd");
        assertEquals(Strawberry.print(),"Strawberry!!");
    }
}

 

다음과 같은 4가지의 장점을 알아보았습니다. 이름을 줄 수 있다는 점, 호출할 때마다 새로운 인스턴스를 주지 않아도 된다는 점, 자식 클래스타입을 반환할 수 있다는 점, 매개변수에 따라 다른 클래스를 반환할 수 있다는 점입니다. 

 

 

정적 팩토리 메서드의 단점

1. 상속을 하려면 public, protected의 생성자가 필요합니다.

정적 팩터리 메서드만 제공하면 하위 클래스를 만들 수 없습니다.

또한, 상속을 하기 위해서는 생성자가 필요없더라도 필수적으로 필요합니다.

 

Q. 이건 무슨소리야? 하위 클래스 못만들어? 진짜?

A. 네. 이번껀 컴파일 에러를 보이기 위해 사진으로 보여드릴게요.

[그림01] 생성자를 만들지 않아 생기는 컴파일 에러

퀵 픽스로 고쳐보도록 하겠습니다.

[그림02] 생성자를 만들어 해결

 

이 제약으로 인해 상속보다 컴포지션(합성) 사용을 유도할 수 있고 불변 클래스(선택 인스턴스화)로 만들기 위해 해당 제약을 지켜야 한다는 점에서는 장점으로 받아들일 수 있습니다.

 

Q. 컴포지션은 뭔데! 불변 클래스는 아까 2번에서 new 안하는 장점인 것 같아.

A. 아니,, 이제 첫 포스팅인데 왤케 모르는게 많은걸까.. [아이템_18] 에 나온다고 하니 그때 가서 다시 알아보자.

 

 

2. 정팩매는 프로그래머가 찾기 어렵다.

생성자 처럼 API 설명에 명확히 드러나있지 않으므로 프로그래머는 정팩매를 활용하여 클래스를 인스턴스화 할 방법을 알아내야 합니다.

 

 

3. 결론

1. 처음으로 EnumSet 함수에 들어가서 함수를 읽어보았다. EnumSet에는 RegularEnumSet과 JumboEnumSet이 하위 클래스도 있고 65개 이하, 이상으로 반환하지만 어떻게 동작하는지 EnumSet을 사용하면서 알 필요가 없다.

    public static <E extends Enum<E>> EnumSet<E> noneOf(Class<E> elementType) {
        Enum<?>[] universe = getUniverse(elementType);
        if (universe == null)
            throw new ClassCastException(elementType + " not an enum");

        if (universe.length <= 64)
            return new RegularEnumSet<>(elementType, universe);
        else
            return new JumboEnumSet<>(elementType, universe);
    }

정팩매 4번 장점의 예제로 나온 것인데, 매개변수(Class<E> elementType)에 따라 다른 클래스를 반환하는 것을 직접 본게 너무 신기했다.

 

 

 

2. 참고 자료

 

yhmane.tistory.com/134?category=905498

 

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

생성자 대신 정적 팩터리 메서드를 고려하라 클래스의 인스터스를 얻는 전통적인 수단은 public 생성자를 이용하는 것입니다. 하지만, 또 알아두어야 할 기법이 있으니 정적 팩토리 메서드(static f

yhmane.tistory.com

 

otrodevym.tistory.com/entry/%EC%9D%B4%ED%8E%99%ED%8B%B0%EB%B8%8C-%EC%9E%90%EB%B0%94-2%EC%9E%A5-%EA%B0%9D%EC%B2%B4-%EC%83%9D%EC%84%B1%EA%B3%BC-%ED%8C%8C%EA%B4%B4?category=820394

 

이펙티브 자바 - 2장 : 객체 생성과 파괴

2장 객체 생성과 파괴 아이템 1 : 생성자 대신 정적 팩터리 메서드를 고려하라 전통적으로 public 생성자를 사용하고 더 나아가 정적 팩터리 메서드를 제공할 수 있다. public class item1 { public static voi

otrodevym.tistory.com

it-mesung.tistory.com/184

 

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

지난 포스팅 때, 책을 그저 적기만 한 것 같아 다시 한번 포스팅을 시작했다.. 생성자와 정적 팩터리 메서드 보통 클래스의 인스턴스는 public 생성자를 활용하여 생성한다. 그런데 클래스 자체는

it-mesung.tistory.com

 

반응형