싱글톤 패턴

싱글톤 패턴은 단 하나의 유일한 객체를 만들기 위한 코드 패턴입니다. 싱글톤 패턴을 따르는 클래스는 생성자가 여러번 호출되더라도 실제 생성되는 객체는 단 하나이고, 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴하게됩니다. 주로 공통된 객체를 여러개 생성해서 사용하는 DBCP(DataBase Connection Pool) 과 같은 상황에서 많이 사용합니다. 

 

사용하는 주된 이유는 리소스를 많이 차지하는 무거운 클래스를 한 개의 객체로 관리하면서 메모리 절약을 할 수 있기 때문입니다.

 

 

싱글톤 패턴 구현

public class Singleton {

    private static Singleton instance;

    private Singleton() { }

    public static Singleton getInstance() {

        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

public static void main(String[] args) {

    Singleton instance1 = Singleton.getInstance();
    Singleton instance2 = Singleton.getInstance();
    Singleton instance3 = Singleton.getInstance();

    System.out.println(instance1); // Singleton@27d6c5e0
    System.out.println(instance2); // Singleton@27d6c5e0
    System.out.println(instance3); // Singleton@27d6c5e0

    System.out.println(instance1 == instance2 && instance2 == instance3); // true
}

 

싱글톤 패턴 구현은 어렵지 않습니다.

생성자의 접근제한자를 private로 선언해서 new 생성자를 통한 객체생성을 제한하고, static 을 붙혀주면됩니다.

세번의 getInstance()로 불러와도 같은 주소를 참조하고있다는걸 확인할 수 있습니다.

다만 이 방식은 멀티쓰레드 환경에서 안전하지 않습니다. if 문에 다수의 사람이 동시에 들어간다면 객체라 여러번 생성될것입니다.

 

이러한 싱글톤 패턴의 종류는 여러가지가 있습니다. 검증된 싱글톤 패턴을 확인하시려면 6번7번으로 넘어가주세요.

 

 

 

1. Eager Initialization

public class Singleton {

    private static final Singleton instance = new Singleton();

    private Singleton() { }

    public static Singleton getInstance() {
        return instance;
    }
}

 

미리 만들어주는 방법입니다. static 영역에 재할당이 불가능한 final 이기때문에 멀티쓰레드 환경에서 안전합니다. 다만 예외처리를 할 수 없고, 리소스가 크다면 static 영억에서 자원을 사용하지 않더라도 메모리에 적재하기 때문에 자원낭비가 될 수 있습니다.

 

 

 

 

2. Static block Initialization

public class Singleton {

    private static Singleton instance;

    private Singleton() { }
    
    static {
        try {
            instance = new Singleton();
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    public static Singleton getInstance() {
        return instance;
    }
}

 

static block ( 클래스가 로딩되고 클래스 변수가 준비된 후 자동으로 실행된느 블록)

 

static block을 통해 예외를 처리할 수 있습니다.

 

 

 

 

3. Lazy Initialization

public class Singleton {

    private static Singleton instance;

    private Singleton() { }

    public static Singleton getInstance() {

        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

 

처음 싱글톤 패턴을 구현했던 방식입니다. 

 

 

 

 

 

4. Thread Safe Initialization

public class Singleton {

    private static Singleton instance;

    private Singleton() { }

    public static synchronized Singleton getInstance() {

        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

 

synchronized 키워드를 사용하여 쓰레드의 접근을 한개로 제한시켜 동기화시킬 수 있습니다. 하지만 매번 사용하는것은 성능하락이 될 수 있습니다.

 

 

 

 

5. Double Checked Locking

public class Singleton {

    private static volatile Singleton instance;

    private Singleton() { }

    public static Singleton getInstance() {

        if (instance == null) {

            synchronized (Singleton.class) {
                if (instance == null) {
                    instance = new Singleton();
                }
            }

        }
        return instance;
    }
}

 

volatile 키워드를 사용해서 캐시가 아닌 메인메모리에서 읽도록 지정해줍니다. 

synchronized를 클래스에 동기화를 걸어 최초 초기화만 동기화 작업을 진행하여 리소스 낭비를 최소화 할 수 있습니다.

하지만 volatile 은 JVM 1.5이상이 되어야하고 JVM에 따라서 쓰레드 세이프하지 않는 문제가 발생할 수 있기때문에 사용하지 않는다고 합니다.

 

 

 

 

6. Bill Pugh Solution (LazyHolder)

public class Singleton {

    private Singleton() { }

    private static class SingleInstanceHolder {
        public static final Singleton INSTANCE = new Singleton();
        
    }
    public static Singleton getInstance() {
        return SingleInstanceHolder.INSTANCE;
    }
}

 

클래스 내부에 static 클래스를 만들어 사용합니다. 클래스가 메모리에 로드되지 않고 getInstance 메서드가 호출되어야 로드됩니다. 이 방식은 JVM의 클래스 로더 매커니즘과 클래스가 로드되는 시점을 이용한 방법이라고 합니다.

하지만 Reflection API, 직렬화 / 역직렬화를 통해 클라이언트가 임의로 싱글톤을 파괴할 수 있다는 단점이 존재합니다.

 

 

Reflection API 우회

    public static void main(String[] args) {

        Singleton singleton = Singleton.getInstance();
        System.out.println(singleton); // Singleton@4f3f5b24
        
        try {
            Class<?> singletonClass = Class.forName("blog.design.싱글톤패턴.Singleton");
            Constructor<?> constructor = singletonClass.getDeclaredConstructor();
            constructor.setAccessible(true);
            Singleton newSingleton = (Singleton) constructor.newInstance();
            
            System.out.println(newSingleton); // Singleton@15aeb7ab
            System.out.println(singleton == newSingleton); // false
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        
    }

 

private 생성자를 우회하고 private 필드에 접근할 수 있습니다.

 

 

직렬화 / 역직렬화 우회

public class Singleton implements Serializable { ... }

public static void main(String[] args) {

    Singleton instance = Singleton.getInstance();
    System.out.println(instance); // Singleton@4f3f5b24
    
    // 직렬화
    try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("blog.design.싱글톤패턴.Singleton"))) {
        out.writeObject(instance);
    } catch (IOException e) {
        e.printStackTrace();
    }

    // 역직렬화
    try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("blog.design.싱글톤패턴.Singleton"))) {
        Singleton newInstance = (Singleton) in.readObject();
        
        System.out.println(newInstance); // Singleton@4c98385c
        System.out.println(instance == newInstance); // false
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }

}

 

직렬화 / 역직렬화를 사용하는 경우 readResolve 메서드를 사용하여 싱글톤을 유지할 수 있습니다.

 

 

public class Singleton implements Serializable {

    private Singleton() { }


    private static class SingleInstanceHolder {
        public static final Singleton INSTANCE = new Singleton();

    }
    public static Singleton getInstance() {
        return SingleInstanceHolder.INSTANCE;
    }
    
    private Object readResolve() {
        return SingleInstanceHolder.INSTANCE;
    }
    
}

 

public static void main(String[] args) {

    Singleton instance = Singleton.getInstance();
    System.out.println(instance); // Singleton@4f3f5b24
    
    // 직렬화
    try (ObjectOutputStream out = new ObjectOutputStream(new FileOutputStream("blog.design.싱글톤패턴.Singleton"))) {
        out.writeObject(instance);
        
    } catch (IOException e) {
        e.printStackTrace();
    }

    // 역직렬화
    try (ObjectInputStream in = new ObjectInputStream(new FileInputStream("blog.design.싱글톤패턴.Singleton"))) {
        Singleton newInstance = (Singleton) in.readObject();

        System.out.println(newInstance); // Singleton@4f3f5b24
        System.out.println(instance == newInstance); // true
    } catch (IOException | ClassNotFoundException e) {
        e.printStackTrace();
    }

}

 

readResolve 메서드는 Serializable 인터페이스를 구현한 클래스에서 제공할 수 있는 메서드로 역직렬화시에 호출됩니다.

이 메서드를 사용하여 역직렬화된 객체 대신에 기존의 인스턴스를 반환하도록 지정하면 싱글톤 패턴을 사용할 수 있습니다.

 

 

 

 

 

7. Enum

enum SingletonEnum {
    INSTANCE;

    private final Client client;
	
    SingletonEnum() {
        dbClient = Database.getClient();
    }

    public static SingletonEnum getInstance() {
        return INSTANCE;
    }

    public Client getClient() {
        return client;
    }
}

public class Main {
    public static void main(String[] args) {
        SingletonEnum singleton = SingletonEnum.getInstance();
        singleton.getClient();
    }
}

 

enum은 thread safe하고 상수뿐만 아니라 변수나 메서드를 싱글톤클래스처럼 사용이 가능합니다.

위의 Bill Pugh Solution과 달리 Reflection API 우회에도 안전합니다. 또한 위에 직렬화 / 역직렬화에서 readResolve() 메서드를 구현한것과 달리 enum은 그 과전이 불필요하고 JVM에서 Serialization을 보장해준다고합니다.

 

 

 

싱글톤의 문제

  1. 싱글톤의 전역상태로 인해 코드의 복잡성이 증가할 수 있고 디버깅이 어려워진다.
  2. 테스트 시 mock 객체나 다른 객체로 대체하기 어려워 테스트 작성을 어렵게한다.
  3. 싱글톤의 의존성때문에 클래스간의 결합도를 높힐 수 잇고 유연성을 감소시킬 수 있다.
  4. SOLID 원칙에 위배될 소지가 있다.

프로토타입 패턴

프로토타입(Prototype) 패턴은 객체를 생성하는 비용이 높은 경우, 기존 객체를 복제하여 새로운 객체를 생성하는 패턴입니다.

다시 말해 기존의 객체를 새로운 객체로 복사하여 우리의 필요에 따라 수정하는 메커니즘을 제공합니다.

 

이 프로토타입 패턴은 객체를 생성하는 데 비용이 많이 들고, 이미 유사한 객체가 존재하는 경우에 사용됩니다. DB로부터 가져온 데이터를 수차례 수정해야하는 경우 new 라는 키워드를 통해 객체를 생성하는 것은 비용이 너무 많이 들기 때문에 한 번 DB에 접근하여 객체를 가져오고 필요에 따라 새로운 객체에 복사하여 데이터를 수정하는 것이 더 좋은 방법입니다.

 

 

 

 

 

 

 

 

프로토타입 패턴 구현

Java에서는 이 패턴을 구현하기 위해 Cloneable 인터페이스를 사용합니다. Cloneable 인터페이스는 마커(marker) 인터페이스로, 별도의 메소드가 정의되어 있지 않습니다. 이 인터페이스를 구현하는 클래스는 얕은 복사 (shallow copy) 를 지원한다는 표시입니다.

 

Cloneable 인터페이스를 구현하는 이유는 clone() 메소드를 호출하여 객체를 복제하기 위함입니다. clone() 메소드는 Object 클래스에서 상속받은 메소드 중 하나로, 복제 가능한 객체에서 호출할 수 있습니다. 그러나 Cloneable을 구현하지 않은 객체가 clone()을 호출하게 되면, CloneNotSupportedException이 발생합니다.

 

public class Cookie{

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}

public class Main {

    public static void main(String[] args) throws CloneNotSupportedException {

        Cookie cookie = new Cookie();
        cookie.clone(); // CloneNotSupportedException 발생!

    }
}

 

따라서 Cloneable 인터페이스를 구현함으로써 해당 클래스의 인스턴스가 복제 가능하다는 것을 나타내고, 이를 통해 프로토타입 패턴을 적용할 수 있게 됩니다. 하지만 주의할 점은 clone() 메소드는 얕은 복사를 수행하므로, 필요에 따라서 깊은 복사(Deep Copy)를 구현해야 할 수 있습니다.

 

 

 

얕은 복사

얕은 복사 (Shallow Copy)는 객체의 필드들을 복제할 때, 참조 타입의 필드는 원본 객체와 복제된 객체가 같은 인스턴스를 참조합니다.

 

public class ShallowCopyExample implements Cloneable {
    private int value;
    private int[] array;

    public ShallowCopyExample(int value, int[] array) {
        this.value = value;
        this.array = array;
    }

    @Override
    public ShallowCopyExample clone() throws CloneNotSupportedException {
        return (ShallowCopyExample) super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        int[] originalArray = {1, 2, 3};
        ShallowCopyExample original = new ShallowCopyExample(42, originalArray);

        ShallowCopyExample cloned = original.clone();

        // 값과 배열은 복제되지만 배열은 같은 배열을 참조하고 있음
        System.out.println(original == cloned); // false
        System.out.println(original.array == cloned.array); // true (얕은 복사)
    }
}

 

 

깊은 복사

깊은 복사 (Deep Copy) 는 참조 타입의 필드도 새로운 인스턴스를 생성하여 복사하게 됩니다. 즉 모든 필드 변수들이 새로운 인스턴스를 가집니다.

 

public class DeepCopyExample implements Cloneable {
    private int value;
    private int[] array;

    public DeepCopyExample(int value, int[] array) {
        this.value = value;
        this.array = array.clone(); // 깊은 복사: 배열의 복사본을 생성하여 참조
    }

    @Override
    public DeepCopyExample clone() throws CloneNotSupportedException {
        return (DeepCopyExample) super.clone();
    }

    public static void main(String[] args) throws CloneNotSupportedException {
        int[] originalArray = {1, 2, 3};
        DeepCopyExample original = new DeepCopyExample(42, originalArray);

        DeepCopyExample cloned = original.clone();

        // 값은 복제되고 배열은 복제본을 참조하고 있음
        System.out.println(original == cloned); // false
        System.out.println(original.array == cloned.array); // false (깊은 복사)
    }
}

 

 

 

프로토타입 패턴 구현 예제

public class Employee implements Cloneable {

    private String name;
    private int age;

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

    @Override
    protected Employee clone() {
        try {
            return (Employee) super.clone();
        } catch (CloneNotSupportedException e) {
            throw new RuntimeException(e);
        }
    }
}

public class Employees {

    private final List<Employee> employeeList;

    public Employees(List<Employee> employeeList) {
        this.employeeList = employeeList;
    }

    public List<Employee> getCloneList() {
        List<Employee> clone = new ArrayList<>();

        for (Employee employee : employeeList) {
            Employee cloneEmployee = employee.clone();
            clone.add(cloneEmployee);
        }

        return clone;
    }
    
    public List<Employee> getOriginalList() {
        return employeeList;
    }
}

 

Cloneable를 사용하는 Employee 에 clone() 메소드를 오버라이드 했고, 그 리스트를 담은 Employees 에 비교를 위해 cloneList와 originalList 메소드를 구현했습니다.

 

public class Main {

    public static void main(String[] args) throws CloneNotSupportedException {

        Employee emp1 = new Employee("김OO", 20);
        Employee emp2 = new Employee("이OO", 22);
        Employee emp3 = new Employee("박OO", 25);
        Employee emp4 = new Employee("오OO", 29);
        Employee emp5 = new Employee("나OO", 23);

        List<Employee> employeeList = List.of(emp1, emp2, emp3, emp4, emp5);

        Employees employees = new Employees(employeeList);

        List<Employee> originalList = employees.getOriginalList(); // 같은 인스턴스
        List<Employee> cloneList = employees.getCloneList(); // clone 인스턴스

        System.out.println(employeeList == originalList); // true
        System.out.println(employeeList == cloneList); // false

        Employee originalEmp = employeeList.get(0); // 같은 인스턴스의 첫번째 Employee
        Employee cloneEmp = cloneList.get(0); // clone 인스턴스의 첫번째 Employee

        System.out.println(originalEmp == cloneEmp); // false

    }
}

 

 

프로토타입 패턴의 장/단점

프로토타입 패턴은 객체를 복제하여 새로운 객체를 생성하는 패턴으로, 이를 통해 객체 생성의 비용을 줄일 수 있습니다. 하지만 프로토타입 패턴은 특정 상황에서 유용하게 사용될 수 있지만, 모든 상황에서 적합하지는 않습니다. 

 

 

장점

  1. 객체 생성 비용 감소 : 객체를 복제함으로써 새로운 객체를 생성할 때의 비용을 줄일 수 있습니다. 특히 복잡한 객체 구조이거나 생성에 많은 리소스가 필요한 경우에 유용합니다.
  2. 동적 객체 생성 : 런타임에서 동적으로 객체를 생성할 수 있습니다. 사용자가 필요에 따라 객체를 생성하고 조작할 수 있습니다.
  3. 객체 생성 시간 단축 : 객체를 복제하여 생성하기 때문에 클래스의 초기화나 설정 작업을 생략할 수 있어 객체 생성 시간을 단축할 수 있습니다.

 

 

단점

  1. Cloneable 인터페이스의 한계 : 프로토타입 패턴을 구현하기 위해서는 Cloneable 인터페이스를 필연적으로 사용하게 되는데 이 인터페이스는 마커(marker) 인터페이스이고, clone() 메소드는 얕은 복사만을 수행하기 때문에 깊은 복사를 수행하려면 추가적인 작업이 필요합니다.
  2. 복제과정의 복잡성 : 객체가 복잡한 구조를 가지거나 참조하는 객체들이 많다면, 이를 적절하게 복제하려면 복잡한 복제 과정이 필요할 수 있습니다. 특히 객체 그래프가 순환이 발생하는 경우에 복제 과정이 더욱 복잡해질 수 있습니다.
  3. 메모리 사용량 증가 : 객체를 복제하여 생성하면 메모리 사용량이 증가할 수 있습니다. 특히 객체가 큰 데이터를 가지고 있거나 복제 해야 할 객체 수가 많을 수록 사용량이 증가합니다.

 

 

프로토타입 패턴의 사용여부는 특정 상황과 요구사항에 따라 주의 깊게 고려하여 사용해야합니다.

팩토리 메소드 패턴

팩토리 메소드 패턴은 객체 생성을 Factory 클래스에서 대신 생성하게 하는 디자인 패턴입니다. 즉 객체를 생성할 때 어떤 클래스의 인스턴스를 만들지 서브클래스에서 결정하게 하는겁니다. 이 패턴은 기존의 코드의 변경 없이 확장하기 위한 패턴입니다.

 

MemberFactory는 Member 인터페이스에만 의존하고 실제로 어떤 구현체를 호출할지는 MemberFactory에서 결정하게 됩니다. 이렇게 하면 구현체가 추가되어도 기존 코드를 수정하기 않고 확장 시킬 수 있습니다.

 

 

 

팩토리 메소드 구현

 

1. Member Interface

public interface Member {

    void signup();
}


public class NaverMember implements Member{

    @Override
    public void signup() {
        System.out.println("네이버 가입");
    }
    
}

public class KakaoMember implements Member {

    @Override
    public void signup() {
        System.out.println("카카오 가입");
    }
    
}

 

 

2. MemberFactory

public abstract class MemberFactory {

    public Member newMember() {
        Member member = createMember();
        member.signup();
        return member;
    }
    
    protected abstract Member createMember();
    
}

// Java 8 이후부터 interface에 default 메서드를 통해 구현할 수 있습니다. 
// 따라서 abstract를 사용하지않고 interface를 사용해도 됩니다.
// 아래 Factory 구현체는 interface를 사용했습니다.

public interface MemberFactory {

    default Member newMember() {
        Member member = createMember();
        member.signup();
        return member;
    }
    
    Member createMember();
    
}

public class NaverMemberFactory implements MemberFactory{

    @Override
    public Member createMember() {
        return new NaverMember();
    }
    
}

public class KakaoMemberFactory implements MemberFactory{

    @Override
    public Member createMember() {
        return new KakaoMember();
    }
    
}

 

 

3. Client

public class Main {

    public static void main(String[] args) {

        MemberFactory factory = new NaverMemberFactory();
        Member member = factory.newMember();
        
        // 네이버 가입
        
    }
}

 

 

팩토리 메소드 패턴의 흐름

  1. MemberFactory 구현체인 NaverMemberFactory 객체를 생성
  2. newMember() 메소드내부에서 createMember() 메소드를 실행 -> NaverMemberFactory 클래스에서 오버라이드 한 NaverMember 객체생성
  3. signup() 메소드 실행 -> NaverMember 클래스에서 오버라이드한 signup() 메소드 호출
  4. "네이버 가입" 출력

 

구현체 확장

public class GoogleMember implements Member{

    @Override
    public void signup() {
        System.out.println("구글 가입");
    }
    
}

public class GoogleMemberFactory implements MemberFactory{

    @Override
    public Member createMember() {
        return new GoogleMember();
    }
    
}

Member 인터페이스의 구현체 GoogleMember 클래스를 추가하고 MemberFactory 인터페이스의 구현체 GoogleMemberFactory 클래스를 추가해주는 것으로 쉽게 확장할 수 있습니다.

 

 

 

패턴의 장/단점

장점

  • OCP 원칙 : 기존 코드의 수정 및 변경 없이 확장할 수 있습니다.
  • SRP 원칙 : 하나의 책임을 가지는 클래스로써 코드를 유지보수하기 쉽습니다.

단점

  • 구현체마다 각각의 Factory 클래스를 모두 구현해주어야 하기 때문에 구현체가 늘어날때마다 Factory 클래스도 함께 생성해야합니다. 따라서 관리해야하는 클래스가 증가합니다.
  • 코드의 복잡성이 증가합니다.

 

빌더 (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