반응형

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

 

1. 개요

 필자는 클래스를 정의할 때 매개변수가 많으면 Builder를, 매개변수가 많이 없으면 생성자 메서드를 사용한다. Setter 메서드는 존재 자체로 객체의 상태 변경 가능성을 열어두기 때문에 사용을 지양하고 있으며, 상태 변경이 필요할 경우 Setter 대신 다른 Update와 같이 다른 네이밍을 사용하여 '비지니스적으로 필요한 상태변경이 있을 수 있다' 라는 것을 암시해주었다.

 이번 아이템에서는 필자가 알고있는 내용을 다시한번 정리했고, 빌더를 사용하는 이유와, Setter보다 어떤면에서 더 효율적인지를 이해하게 되었다. 

 


2. 자바 빈즈 패턴 

 

2.1. 자바 빈즈 패턴이란?

 자바 빈즈 패턴이란 기본 생성자로 객체를 만든 후, Setter 메서드를 호출해 원하는 매개변수의 값을 결정하는 방식이다. 인스턴스를 만들기 쉽고, 사용법도 매우 간단하지만 이 방식은 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태에 놓인다는 매우 심각한 단점을 갖고 있다.

public class NutritionFact {
    private int servingSize;
    private int servings;
    private int calories;
    private int fat;
    private int sodium;
    private int carbohydrate;

    public void setServingSize(int servingSize) {this.servingSize = servingSize;}

    public void setServings(int servings) {this.servings = servings;}

    public void setCalories(int calories) {this.calories = calories;}

    public void setFat(int fat) {this.fat = fat;}

    public void setSodium(int sodium) {this.sodium = sodium;}

    public void setCarbohydrate(int carbohydrate) {this.carbohydrate = carbohydrate;}
}

 


2.2. 일관성이 뭔가요?

 일관성(Consistency)이란 객체의 상태가 프로그램 실행 도중 항상 유효한 상태를 유지해야 한다는 원칙을 말한다. 즉, 객체 내부 상태가 항상 유효한 상태로 일관되게 유지되어야 한다는 뜻이다.

 

 즉, 객체의 상태 값이 할당이 되지 않거나, 할당이 되더라도 유효한 상태가 아닌 것이다. 후자의 경우 은행 계좌 잔고를 예로 들 수 있는데, 잔고는 항상 0 이상의 값으로 일관되게 유지되어야 한다. 이를 위해 아래와 같이 유효성 검사 역할을 하는 비지니스 로직을 넣기도 한다. 이처럼 객체의 상태를 유효한 상태로 일관성 있게 유지하는 것이다.

public class BankAccount{

    private double balance;
    
    public BankAccount(double initialBalance) {
        this.balance = initialBalance;
    } 
    
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
        }
    }
    
    // 출금 시 balance에 대한 일관성 유지를 위해 유효성 검사하는 로직이 추가됨
    public boolean withdraw(double amount) {
        if (amount > 0 && amount <= balance) {
            balance -= amount;
            return true;
        }
        return false;
    }
    
    ...
}

 


2.3. 자바 빈즈 패턴은 왜 일관성이 무너지나요?

 그럼 자바 빈즈 패턴을 사용할 경우 객체가 완전히 생성되기 전까지는 일관성이 무너진 상태. 즉, 유효하지 않은 상태가 되는 이유는 뭘까? 필수적으로 설정되어야할 객체의 상태 값이 설정되지 않을 수 있기 때문이다. 이를 책에서는 '객체가 완전히 생성되기 전' 이라고 표현하고 있다.

 

 예를들어 객체를 생성한 후 실수로 setServings()을 호출하지 않는다면 servings를 사용하는 어떤 비지니스 로직이 있을 때 예상치 못한 버그가 발생할 수 있다. 컴파일 단계에서 에러가 나는것도 아니기에 이를 사전에 인지하지 못한다면 버그를 찾기 힘들 수 있다. 참고로 아래 예제에서는 예외 또한 발생하지 않는다. int의 경우 기본 값인 0이 설정되기 때문이다. 이 모든 건 단순 Setter 메서드 호출 누락으로 인해 발생했다.

    NutritionFact nutritionFact = new NutritionFact();
    //nutritionFact.setServings(20); 실수로 누락
    nutritionFact.setServingSize(1);
    nutritionFact.setCalories(1);
    nutritionFact.setFat(10);
    nutritionFact.setCarbohydrate(10);
    nutritionFact.setSodium(10);
    
    ...
    
    int servings = nutritionFact.getServings();
    if(servings >= 10){
        // 1회 제공량이 10개 이상일 경우에 대한 비지니스 로직
    }

 

 그렇다면 위 단점을 극복하는 방법은 뭘까? 바로 일관성을 보장하는 빌더 패턴을 사용하는 것이다.

 


3. 빌더 패턴

 빌더 패턴은 객체를 생성하는 과정을 보다 유연하게 하기 위한 디자인 패턴 중 하나로, Builder라는 내부 클래스를 통해 객체를 생성하는 방법이다.

 NutritionFacts 의 생성자 메서드를 보면 알 수 있듯이 Builder를 통해서만 객체를 생성하고 있고, Builder의 생성자 메서드를 통해 servings나 servingSize와 같은 필수 값을 받도록 설정한다. 나머지 값은 체이닝 메서드 설정하도록 하고 있다. 

 

public class NutritionFacts {
    private final int servingSize;
    private final int servings;
    private final int calories;
    private final int fat;
    private final int sodium;
    private final int carbohydrate;

    public static class Builder{

        // 필수 매개변수는 final
        private final int servings;

        // 선택 매개변수는 기본값으로 초기화
        private int servingSize;
        private int calories = 0;
        private int fat = 0;
        private int sodium = 0;
        private int carbohydrate = 0;

        public Builder(int servings){
            this.servings = servings;
        }

        public Builder servingSize(int val){
            servingSize = val;
            return this;
        }

        public Builder calories(int val){
            calories = val;
            return this;
        }

        public Builder fat(int val){
            fat = val;
            return this;
        }

        public Builder sodium(int val){
            sodium = val;
            return this;
        }

        public Builder carbohydrate(int val){
            carbohydrate = val;
            return this;
        }

        public NutritionFacts build(){
            return new NutritionFacts(this);
        }
    }

    private NutritionFacts(Builder builder){
        servingSize = builder.servingSize;
        servings = builder.servings;
        calories = builder.calories;
        fat = builder.fat;
        sodium = builder.sodium;
        carbohydrate = builder.carbohydrate;
    }
}

 

 여기서 생성된 NutritionFacts 객체는 필수 값인 servings 값이 없을 수 없다. 누락시킬 경우 컴파일 단계에서 에러가 발생하기 때문이다. 즉, Builder를 통해 생성한 인스턴스는 일관성이 보장되는 것이다.

필수 값인 servings를 설정하지 않았을때 에러 발생

 


4. 자바 빈즈 패턴 + 명시적 생성자

 그럼 자비 빈즈 패턴에서 명시적 생성자를 통해 필수 값을 설정하면 되지 않을까? 그래도 된다. 이 경우 필수 값에 대한 일관성을 보장할 수 있다.

public class NutritionFact {
    private int servingSize;
    private int servings;
    private int calories = 0;
    private int fat = 0;
    private int sodium = 0;
    private int carbohydrate = 0;

    public NutritionFact(int servingSize, int servings, int calories, int fat, int sodium, int carbohydrate){
        this.servingSize = servingSize;
        this.servings = servings;
        this.calories = calories;
        this.fat = fat;
        this.sodium = sodium;
        this.carbohydrate = carbohydrate;
    }
    
    ...
}

 

 그리고 사용 시 아래처럼 생성자 메서드의 파라미터의 순서에 맞게 값을 입력하기만 하면 된다. 그런데 문제가 있다. 이것만 봐서는 어떤 파라미터가 객체의 멤버필드에 매핑되는지 바로 알 수 없다. 직접 생성자 메서드를 확인해야하고, 순서를 일일이 세어야 하는 수고가 필요하다.

NutritionFact nutritionFact = new NutritionFact(10, 10,1,1,1,1);

 

 이에 반해 빌더 패턴은 체인 메서드를 통해 설정하기 때문에 어떤 멤버필드에 값을 설정하는지 바로 알 수 있어 가독성을 향상시킨다. 또한 Setter 메서드가 없으니 중간에 객체의 상태가 변경되지 않음을 보장한다. 즉, 안전한 객체가 되는것이다.

new NutritionFacts.Builder(10)
        .calories(100)
        .sodium(10)
        .carbohydrate(10)
        .servingSize(10)
        .fat(1)
        .build();

 


5. 정리

 생성자나 정적 팩터리가 처리해야 할 매개변수가 많다면 빌더 패턴을 선택하는 게 더 낫다. 매개변수 중 다수가 필수가 아니거나 같은 타입이면 특히 더 그렇다. 빌더는 클라이언트 코드를 읽고 쓰기가 훨씬 간결하고, 자바빈즈보다 훨씬 안전하다.

반응형
반응형

조슈아 블로크의 '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