빌더 (Builder)

빌더 패턴은 복잡한 객체의 생성과 표현을 분리하여 인스턴스를 만드는 생성패턴입니다. 생성자에 들어갈 매개 변수를 메서드로 하나하나 받아들이고 마지막에 통합 빌드해서 객체를 생성하는 방식입니다.

 

 

빌더패턴을 사용하는 이유

우리는 데이터를 주입할 때 생성자를 이용하거나 기본생성자로 객체를 생성하고 이후에 Setter를 이용해서 값을 넣었습니다.

 

 

 

생성자를 이용한 주입

public class User {

    private String name;
    private int age;

    private String sex;
    private String phone;
    private int height;

    public User(String name, int age, String sex, String phone, int height) {
        this.name = name;
        this.age = age;
        this.sex = sex;
        this.phone = phone;
        this.height = height;
    }
}

public class Main {

    public static void main(String[] args) {

        User user = new User("김OO", 20, "남자", "01012345678", 70);

    }
}

 

Setter를 이용한 주입

public class User {

    private String name;
    private int age;

    private String sex;
    private String phone;
    private int height;
    

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

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

    public void setSex(String sex) {
        this.sex = sex;
    }

    public void setPhone(String phone) {
        this.phone = phone;
    }

    public void setHeight(int height) {
        this.height = height;
    }
}

public class Main {

    public static void main(String[] args) {

        User user = new User();
        user.setName("김OO");
        user.setAge(20);
        user.setSex("남자");
        user.setPhone("01012345678");
        user.setHeight(70);
    }
}

 

이러한 방식은 생성자의 인자가 많아질 수록 데이터를 구별하기 어렵고, 필드와 생성자의 위치를 직관적으로 파악하기 어렵다는 것입니다. 그리고 타입이 많아질 수록 메서드 수가 기하급수적으로 늘어나 가독성과 유지보수 측면에서 안좋을 수 밖에 없습니다.

Setter는 객체를 생성하는 시점에 값을 주입하지않아 일관성(Consistency) 불변성(Immutable) 문제가 생길 수 있습니다.

 

생성자와 Setter의 문제점

 

 

일관성 문제

필수 매개변수란 객체가 초기화 될 때 반드시 설정되어야 하는 값입니다. 만약 개발자가 setAge() 메서드를 호출 하지 않고 User 객체를 생성했다면 이 객체는 일관성이 없는 상태가 됩니다. 만약 이 객체를 사용하게 된다면 예외가 발생할 수 있습니다.

 

불변성 문제

Setter의 문제는 객체를 생성한 이후에 값을 주입하는데요 이는 객체를 생성한 이후로도 누구나 외부에서 Setter를 사용해서 값을 주입할 수 있는 문제가 생깁니다. 다시 말해 누구든지 Setter를 호출해 객체를 조작할 수 있다는 뜻입니다. 따라서 Setter는 불변성이 보장되지않는 객체라고 할 수 있습니다.

 

 

 

Builder패턴의 사용

 

빌더 패턴은 이러한 문제들을 해결하기위해 Buillder 클래스를 만들어 생성되는 시점에 모든 값들을 넣어주도록 하는 패턴입니다.

 

        // 생성자 주입
        User user1 = new User("김OO", 20, "남자", "01012345678", 70);

        // Setter 주입
        User user2 = new User();
        user.setName("김OO");
        user.setAge(20);
        user.setSex("남자");
        user.setPhone("01012345678");
        user.setHeight(70);

        // Builder 주입
        User user3 = User.builder()
            .name("김OO")
            .age(20)
            .sex("남자")
            .phone("01012345678")
            .height(70)
            .build();

 

빌더 패턴을 이용하면 생성자를 오버로딩을 하지 않아도되며, 데이터의 순서에 상관없이 데이터를 주입할 수 있게되었습니다.

 

 

 

빌더패턴의 장점

  • 필요한 데이터만 설정할 수 있습니다.
  • 가독성이 좋습니다.
  • 변경 가능성을 최소화 할 수 있습니다.

 

 

빌더패턴의 구조

구현자체는 어렵지 않습니다. 위의 User 객체에 Builder 패턴을 구현해보겠습니다.

 


1. builder()

User 클래스 안에 Builder 클래스를 구현해줍니다.

public class User {

    private String name;
    private int age;

    private String sex;
    private String phone;
    private int height;

    public static UserBuilder builder() {
        return new UserBuilder();
    }

    public static class UserBuilder {

    }

}

 

 

User.builder()로 Builder 패턴을 시작해야하기 때문에 static 을 사용해 클래스를 만들어줍니다.

 

 

 

2. Chaning

    public static class UserBuilder {

        private final User user;

        public UserBuilder() {
            this.user = new User();
        }

        public UserBuilder name(String name) {
            user.name = name;
            return this;
        }

        public UserBuilder age(int age) {
            user.age = age;
            return this;
        }

        public UserBuilder sex(String sex) {
            user.sex = sex;
            return this;
        }

        public UserBuilder phone(String phone) {
            user.phone = phone;
            return this;
        }

        public UserBuilder height(int height) {
            user.height = height;
            return this;
        }

        public User build() {
            return user;
        }
    }

 

User.builder() 를 입력하면 UserBuilder 기본생성자를 통해 User 기본생성자를 주입시켜줍니다. 여기서 주의할 사항은 User의 기본생성자가 존재해야합니다. 인자를 받는 생성자가 존재한다면 기본생성자를 하나 더 만들어주어야합니다.

 

이후에 반환타입은 UserBuilder, 메서드명은 User의 변수명으로 만들어 메서드를 체이닝(Chaining) 형태로 호출하고 마지막에 build()를 통해 최종적으로 객체를 반환하도록 만들어주면 Builder 패턴을 쉽게 구현할 수 있습니다.

추상 팩토리 패턴

서로 관련이 있는 객체들을 통째로 묶어서 팩토리 클래스로 만들고,

이들 팩토리를 조건에 따라 생성하도록 다시 팩토리를 만들어서 객체를 생성하는 패턴입니다.

 

쉽게말해 추상적인 공장에서는 추상적인 부품을 조합하여 추상적인 제품을 만든다고 생각하시면됩니다.

무슨말이야? 라고 하신다면 '추상화' 라는 말은 '구체적으로 어떻게 구현되어 있는지 생각하지 않고 인터페이스에만 주목하는 상태'를 뜻합니다.

 

추상화에 대표적인 예로 부품이 많은 자동차, 컴퓨터 등이 있으나 저는 이번에 컴퓨터로 예를 들어보겠습니다.

컴퓨터의 부품으로는 키보드, 마우스, 모니터, 스피커 등등 많습니다.

각 부품마다 제조사도 다르고 일부 지원하는 기능도 다양합니다. 우리가 원래 사용하던 부품이 아니고 다른 제조사의 부품을 갈아낀다고해서 컴퓨터가 작동이 안될까요? 당연히 아닙니다. 왜냐하면 각 부품마다 인터페이스화 되어있기때문입니다. 일종의 가이드라인 또는 설계도면인것인 셈이죠.

 

이를 코드로 작성한다면 키보드, 마우스, 모니터, 스피커는 interface 입니다. 그리고 제조사별로 구현체를 갈아끼울 수 있겠죠.

 

이런식으로 말입니다. 여기에서 Factory는 어떤 구현체를 적용할 지 조합하는 공장입니다.

 

지금부터 코드를 작성하며 같이 보겠습니다. 예시를 위해 키보드와 마우스에 대해서만 작성해보도록 하겠습니다.

 

 

추상팩토리패턴의 예

 

1. 키보드

public interface Keyboard {

    void init();

}

public class LGKeyboard implements Keyboard{

    @Override
    public void init() {
        System.out.println("LG Keyboard Call");
    }
}

public class SAMSUNGKeyboard implements Keyboard{

    @Override
    public void init() {
        System.out.println("SAMSUNG Keyboard Call");
    }
}

 

 

 

 

2. 마우스

public interface Mouse {

    void init();
    
    void sound();
   
}

public class LGMouse implements Mouse{

    @Override
    public void init() {
        System.out.println("LG Mouse Call");
    }

    @Override
    public void sound() {
        System.out.println("LG Sound");
    }
}

public class SANSUNGMouse implements Mouse{

    @Override
    public void init() {
        System.out.println("SAMSUNG Mouse Call");
    }

    @Override
    public void sound() {
        System.out.println("SAMSUNG Sound");
    }
}

 

 

인터페이스에 대한 구현체는 모두 만들었습니다. 다음은 Factory를 만들어보겠습니다.

 

 

 

 

3. 팩토리

public class KeyboardFactory {

    public Keyboard createKeyboard(String type) {
        if ("LG".equals(type)) {
            return new LGKeyboard();
        }
        if ("SAMSUNG".equals(type)) {
            return new SAMSUNGKeyboard();
        }
        return new Keyboard() {
            @Override
            public void init() {
                System.out.println("Default Keyboard Call");
            }
        };
    }
}

public class MouseFactory {

    public Mouse createMouse(String type) {
        if ("LG".equals(type)) {
            return new LGMouse();
        }
        if ("SAMSUNG".equals(type)) {
            return new SANSUNGMouse();
        }
        return new Mouse() {
            @Override
            public void init() {
                System.out.println("Default Mouse Call");
            }

            @Override
            public void sound() {
                System.out.println("Default Sound");
            }
        };
    }

}

 

Factory 클래스에서 만약 type이 LG라면 LG에 맞는 구현체를 넣고, Samsung이라면 Samsung에 맞는 구현체를 넣어주는 부분입니다. 만약 둘다 아니라면 익명클래스를 이용해서 기본 부품을 넣어주었습니다.

 

이제 컴퓨터를 조립해볼까요?

 

 

 

4. 완성

public class Computer {

    private Keyboard keyboard;
    private Mouse mouse;

    public Computer(String type) {
        keyboard = createKeyboard(type);
        mouse = createMouse(type);
    }

    public void call() {
        keyboard.init();
        mouse.init();
    }

    public void click() {
        mouse.sound();
    }

    private Keyboard createKeyboard(String type) {
        KeyboardFactory keyboardFactory = new KeyboardFactory();
        return keyboardFactory.createKeyboard(type);
    }

    private Mouse createMouse(String type) {
        MouseFactory mouseFactory = new MouseFactory();
        return mouseFactory.createMouse(type);
    }

}

 

Factory 클래스의 createKeyboard와 createMouse 메서드를 사용해서 구현체를 넣어주었습니다.

 

실제 컴퓨터를 사용해보겠습니다.

 

추상팩토리패턴의 결과

public class Main {

    public static void main(String[] args) {

        Computer computer = new Computer("LG");
        computer.call();

        computer.click();
        
        // LG Keyboard Call
        // LG Mouse Call
        // LG Sound
        
        Computer computer2 = new Computer("APPLE");
        computer2.call();

        computer2.click();

        // Default Keyboard Call
        // Default Mouse Call
        // Default Sound

    }
}

 

 

이 추상 팩토리 패턴의 장점은  공장에서 생성되는 제품들의 상호 호환을 보장할 수 있습니다.

그리고 구현체들은 한 곳에서 코드를 쉽게 유지보수할 수 있습니다.

 

단점으로는 패턴과 함께 새로운 인터페이스들과 클래스들이 많이 도입되기 때문에 코드가 필요 이상으로 복잡해질 수 있습니다.

+ Recent posts