반응형

조슈아 블로크의 'Effective Java' 책을 읽고 제멋대로 정리한 내용입니다. :)

 

1. 개요

 인스턴스를 생성할 때 생성자를 많이 사용하는데, '정적 팩터리 메서드'를 사용하기도 한다. 책에서는 정적 팩터리 메서드 사용을 권장하고 있는데, 과연 어떤 장점을 갖고 있길래 이를 권장하는 것일까?

 


2. 정적 팩터리 메서드가 뭔가요?

자바에서의 '팩터리'는 '인스턴스를 만드는 공장' 을 의미한다. 즉, 정적 팩터리 메서드란 인스턴스를 만드는 역할을 하는 Static 메서드이다. 아래와 같이 말이다.

 

public class User {

    private String name;

    // 접근 제한자를 private 로 하여 외부 호출을 막음.
    private User(String name){
        this.name = name;
    }
    
    // 유저 인스턴스를 생성하는 정적 팩터리 메서드
    public static User from(String name){
        return new User(name);
    }
}

 

 정적 팩터리 메서드 안에서 생성자를 호출하고 있고, 생성자는 private로 하여 외부 호출을 막고있다. public 생성자를 사용했을때보다 코드가 늘어나고 복잡해진것 같은데, 과연 어떤 장점을 갖고 있길래 생성자보다 정적 팩터리 메서드 방식을 권장하는 걸까?

 


3. 장점

 

3.1. 이름을 지정함으로써 가독성을 증가시킨다.

 생성자는 이름이 없다. 굳이 따지자면 클래스 명이다. 이에 반해 정적 팩터리 메서드는 이름을 지정할수 있다. 이름과 대학 입학년도를 갖는 Student 클래스로 예를들어 설명하겠다. 멤버필드인 name은 학생 이름, admissionYear는 입학년도이다.

 

1) public 생성자 사용

public class Student {

    private String name;
    private int admissionYear;

    public Student(String name){
        LocalDate now = LocalDate.now();
        this.name = name;
        this.admissionYear = now.getYear();
    }

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

class Main{
    public static void main(String[] args) {
        Student student1 = new Student("김철수");
        Student student2 = new Student("곽영희", 2020);
    }
}

 

2) 정적 팩터리 메서드 사용

public class Student {

    private String name;
    private int admissionYear;

    private Student(String name, int admissionYear) {
        this.name = name;
        this.admissionYear = admissionYear;
    }

    public static Student createFreshman(String name) {
        LocalDate now = LocalDate.now();
        return new Student(name, now.getYear());
    }

    public static Student createOfAdmissionYear(String name, int year) {
        return new Student(name, year);
    }
}

class Main{
    public static void main(String[] args) {
        Student student1 = Student.createFreshman("김철수");
        Student student2 = Student.createOfAdmissionYear("곽영희", 2020);
    }
}

 

 먼저 생성자의 신입생과 재학생에 대한 학생 인스턴스를 생성할 때 시그니처가 다른 public 생성자를 호출하고 있다. 

 여기서 중요한 점은 메인 메서드에서 이를 사용할 때 public 생성자의 시그니처만 봐서는 어떤 특성을 갖는 인스턴스를 생성하는지 알 수 없다. 직접 생성자 코드를 봐야 알 수 있다.

 

 반면 정적 팩터리 메서드를 사용한 경우 메서드 명을 통해 생성할 인스턴스의 특성을 묘사할 수 있다. createFreshman은 올해 입학한 학생, createOfAdmissionYear는 특정 년도에 입학한 학생 인스턴스를 생성하고 있다. Student라는 생성자 명만 덩그러니 있는것보다 특성을 묘사할 수 있는 메서드 이름을 사용함으로써  가독성을 증가시킨 것이다.

 


3.2. 인스턴스 재활용을 통한 메모리 효율 증대

 인스턴스를 미리 만들어 놓거나, 새로 생성한 인스턴스를 캐싱하여 불필요한 객체 생성을 피할 수 있다. 대표적으로 Boolean.valueOf(boolean) 메서드는 미리 만들어 놓은 인스턴스를 리턴하는 방식으로 사용된다.

 만약 생성 비용이 큰 객체가 자주 생성될 경우에 이 방식을 활용한다면 객체 생성 시 사용하게 되는 힙 메모리의 사용율을 줄일 수 있어 메모리 효율적이라고 할 수 있다.

public final class Boolean{

    public static final Boolean TRUE = new Boolean(true);
    public static final Boolean FALSE = new Boolean(false);
    
    ...
    
    public static Boolean valueOf(boolean b) {
        return (b ? TRUE : FALSE);
    }
    
}

 


3.3. 하위 타입 객체 반환을 통한 유연성 증대

 Java 8버전부터 인터페이스 내에 정적 팩터리 메서드를 구현할 수 있다. 이를 통해 인터페이스의 구현체 클래스를 메서드마다 자유롭게 지정할 수 있어 인스턴스 생성에 대한 유연성을 제공한다.

 아래는 Weapon 인터페이스에 정의한 정적 팩터리 메서드를 통해 Weapon의 구현체 클래스인 Sword, Gun에 대한 인스턴스를 생성하고 있다.

public interface Weapon {
    static Weapon createSword(){
        return new Sword();
    }
    
    static Weapon createGun(){
        return new Gun();
    }
}

...

public class Main {

    public static void main(String[] args) {
        Weapon sword = Weapon.createSword();
        Weapon gun = Weapon.createGun();
    }
}

3.4. 조건에 따른 하위 타입 객체 반환을 통한 유연성 증대

 앞에서는 구현체 클래스에 대한 인스턴스 생성을 위해 외부에서 직접 해당 메서드를 호출하고 있다. 만약 요구사항이 바뀌어 직업이 검사일때는 Sword, 스나이퍼일때는 Gun 인스턴스를 생성해야한다면 어떻게 할까? main 메서드에서 직업에 대해 분기 후 검사일때는 createSword() 메서드를, 스나이퍼일때는 createGun() 메서드를 호출할 수도 있지만, 정적 팩터리 메서드 내에서 직업을 파라미터로 받고, 내부 조건문에 따라 무기 인스턴스를 반환할 수도 있다.

public interface Weapon {
    static Weapon createFromJob(Job job){
        if(job == Job.SNIPER){
            return new Gun();
        }else{
            return new Sword();
        }
    }
}

...

public class Main {

    public static void main(String[] args) {
        Weapon sword = Weapon.createFromJob(Job.SWORDS_MAN);
        Weapon gun = Weapon.createFromJob(Job.SNIPER);
    }
}

 


3.5. 캡슐화를 통한 코드중복 및 파편화 방지

 위 예제를 보면 Weapon 인터페이스에서 무기 생성에 대한 구현부를 캡슐화시키고, 메인 메서드에서는 캡슐화된 메서드를 호출하고 있다. 그렇다면 캡슐화란 무엇이고 목적은 뭘까?

 


※ 캡슐화

캡슐화란 객체의 속성과 행위를 하나로 묶고, 실제 구현부는 외부로부터 은닉하는 것을 말한다.

 

환자가 약사에게 독감 진단서를 제출하면 약사는 정해진 재료와 제조 과정을 거쳐 약을 조제한다. 알약을 받는 환자는 여기에 사용된 재료나 제조 과정을 이해할 필요도, 알 필요도 없다. 다만 진단서를 제출할 뿐이다.

 

 자바 코드로 이해하면 약사 객체인 Chemist 클래스를 만들고 약을 제조하는 전 과정을 makePill 이라는 약을 조제하는 메서드로 묶는 것이다. 외부 클래스에서 약이 필요하다면 Chemist 클래스에서 해당 메서드를 호출하기만 하면 된다. 호출하는 클래스는 어떤 과정을 거쳐 약이 만들어지는지는 알 필요가 없게 된다. 

 이처럼 약을 조제하는데 필요한 여러 속성들과 행위를 makePill 이라는 하나의 메서드로 묶고, 이에 대한 구현부는 외부로부터 은닉하여 알 필요가 없게하는 것이 바로 캡슐화이다.

 

public class Chemist {
    public Pill makePill(String 진단서){

        // 약을 만드는 방법에 대한 로직

        return pill; // 생성된 약
    }
}

 

 만약 약사라는 클래스를 통해 해당 로직을 캡슐화를 하지 않는다면 어떤 일이 벌어질까? 약이 필요한 모든 곳에서 약을 조제하는 로직을 구현해야 하며, 아래와 같이 중복코드와 메서드 파편화가 발생하게 된다.

public void hospitalCare(){

    // 1. 병원 진료

    // 2. 약은 만드는 방법에 대한 로직 >> 코드중복 ! 메서드 파편화!

}

// 병원 진료 내역을 조회한 후 해당 내역에 대한 처방전을 받는 메서드
public void getHospitalCareHistory(){

    // 1. 병원 진료 내역 조회

    // 2. 약은 만드는 방법에 대한 로직 >> 코드중복 ! 메서드 파편화!

}

 

 이 상태에서 약을 만드는 방법에 대한 로직이 변경되면 어떻게될까? 모든 메서드의 로직을 전부 바꿔야한다. 이때 작은 실수가 발생한다면 심각한 버그가 발생하게 된다. 만약 캡슐화가 되어있다면? 앞서 약사 클래스의 makePill 메서드만 변경해주면 된다.

 정리하면 캡슐화는 구현부를 외부로부터 은닉함으로써 책임을 분리하고, 코드의 중복 및 파편화를 예방하고, 유지보수하기 용이한 코드로 만들어주는 유용한 프로그래밍 기법인것이다.

 


4. 정적 팩터리 메서드 명명 방식

 메서드 명은 개발자 마음이지만, 정적 팩터리 메서드 같은 특별한 메서드의 경우 권장하는 명명 방식이 있다.

 


4.1. from

 매개변수를 하나 받아서 해당 타입의 인스턴스를 반환하는 메서드이다.

Date d = Date.from(instant);

4.2. of

 여러 매개변수를 받아 적합한 타입의 인스턴스를 반환하는 메서드이다.

Set<Rank> faceCards = EnumSet.of(JACK, QUEEN, KING);

4.3. instance, getInstance

 매개변수로 명시한 인스턴스를 반환하지만, 같은 인스턴스임을 보장하지 않는 메서드이다. 즉 내부에 캐싱된 인스턴스를 리턴할 수도 있다는 말이다.

StackWalker luke = StackWalker.getInstance(options);

4.4. create, newInstance

 매번 새로운 인스턴스를 반환함을 보장한다.

Object newArray = Array.newInstance(classObject, arrayLen);

 

 

반응형

+ Recent posts