플라이웨이트 패턴이란?

플라이웨이트 패턴은 많은 수의 유사한 객체를 효율적으로 관리하기 위한 구조적 디자인 패턴입니다. 객체의 내부 상태를 공유함으로써 메모리 사용을 최적화합니다. 코드를 보면서 이해해봅시다.

 

 

재사용 가능 여부

플라이웨이트 패턴은 자원의 재사용 여부를 먼저 따져봐야합니다. 같은 자원을 공유해도 문제가 없는 데이터를 재사용함으로써 메모리를 절약할 수 있으니까요.

예제로 전쟁시뮬레이션 게임을 만든다고 가정해보겠습니다. 하나의 객체를 만들때 텍스쳐, 사운드, 폴리곤, 객체의 좌표가 사용된다고 해보겠습니다.

 

 

 

플라이웨이트 패턴 적용 전

먼저 메모리 사용량을 표시하기 위해 메모리 클래스를 하나 만들겠습니다.

public class Memory {

    public static int size = 0;

    public static void print() {
        System.out.println("메모리 사용량 : " + size + "MB");
    }
}

 

 

public class Soldier {

    private final int size = 10;
    private final String texture;
    private final String sound;
    private final int polygons;

    private double positionX;
    private double positionY;

    public Soldier(String texture, String sound, int polygons, double positionX, double positionY) {
        this.texture = texture;
        this.sound = sound;
        this.polygons = polygons;
        this.positionX = positionX;
        this.positionY = positionY;

        // 메모리 사용량 증가
        Memory.size += size;
    }

}

 

public class Field {

    // 지형 타일 크기
    public static final int CANVAS_SIZE = 1000;

    public void render(String texture, String sound, int polygons, double positionX, double positionY) {
        Soldier soldier = new Soldier(texture, sound, polygons, positionX, positionY);
        System.out.println("병사 생성 : x : " + positionX + ", " + " y : " + positionY);
    }
}

 

 

    public static void main(String[] args) {
        Field field = new Field();

        for (int i = 0; i < 1000; i++) {
            field.render(
                "robe.png",
                "solider.wav",
                200,
                Field.CANVAS_SIZE * Math.random(),
                Field.CANVAS_SIZE * Math.random()
            );
        }

        Memory.print(); // 메모리 사용량 : 10000MB
    }

 

Solider 1000명을 Field에 rendering하는 코드를 간단하게 작성해보았습니다. 이때 무려 메모리를 10000mb나 사용되었네요. 만약 병사의 수가 증가한다면 기하급수적으로 메모리를 사용할 것입니다. 이 코드를 플라이웨이트 패턴을 이용해 메모리 사용량을 줄여보겠습니다.

 

 

플라이웨이트 패턴 적용 후

public class Soldier {

    private final int size = 2;

    private SoliderModel model;
    private double positionX;
    private double positionY;

    public Soldier(SoliderModel model, double positionX, double positionY) {
        this.model = model;
        this.positionX = positionX;
        this.positionY = positionY;

        // 메모리 사용량 증가
        Memory.size += size;
    }

}
public class SoliderModel {

    private final int size = 8;
    private final String texture;
    private final String sound;
    private final int polygons;

    public SoliderModel(String texture, String sound, int polygons) {
        this.texture = texture;
        this.sound = sound;
        this.polygons = polygons;

        Memory.size += size;
    }

}

 

Solider에서 재사용이 가능한 텍스쳐, 사운드, 폴리곤의 수를 SoliderModel 클래스로 새로 만들었습니다.

메모리 사용량은 SoliderModel = 8, Solider = 2 로 분배되었습니다.

 

public class SoliderModelFactory {

    private static final Map<String, SoliderModel> cache = new HashMap<>();

    public static SoliderModel getModel(String texture, String sound, int polygons) {
        String key = texture + sound + polygons;
        return cache.computeIfAbsent(key, k -> new SoliderModel(texture, sound, polygons));
    }
    
    public static void clear() {
        cache.clear();
    }
}

 

Factory를 만들고 한번 만들었던 모델데이터는 이곳에서 재사용됩니다.

 

public class Field {

    // 지형 타일 크기
    public static final int CANVAS_SIZE = 1000;

    public void render(String texture, String sound, int polygons, double positionX, double positionY) {
        SoliderModel model = SoliderModelFactory.getModel(texture, sound, polygons);
        Soldier soldier = new Soldier(model, positionX, positionY);
        System.out.println("병사 생성 : x : " + positionX + ", " + " y : " + positionY);
    }
}

필드에서 SoliderModelFactory.getModel 로 모델데이터를 가져와 Solider를 만들어 렌더링합니다.

 

    public static void main(String[] args) {
        Field field = new Field();

        for (int i = 0; i < 1000; i++) {
            field.render(
                "robe.png",
                "solider.wav",
                200,
                Field.CANVAS_SIZE * Math.random(),
                Field.CANVAS_SIZE * Math.random()
            );
        }

        Memory.print(); // 메모리 사용량 : 2008MB
        SoliderModelFactory.clear();
    }

메모리 사용량을 출력해본 결과 2008MB 가 나왔습니다. 플라이웨이트 적용전보다 무려 80% 감소되었습니다. 반복이 많아질 수록 경량화될겁니다.

 

 

주의점

렌더링이 끝났다면 factory를 초기화해줄 필요가 있습니다. 관리되고있는 인스턴스는 GC에서 제거하지 않기 때문에 메모리 누수가 발생할 수 있습니다.  참고로 이런 캐싱방식은 StringConstantPool과 같은 개념이라고 할 수 있습니다.

 

 

[JAVA] String의 불변성(immutability)

들어가며JAVA를 처음 접할 때, String은 불변객체라고 배우고 지나갔습니다. 하지만 실제 String을 다루다보면 어째서 불변객체인지 의문이 들곤합니다.  String str = "apple"; str = "banana";왜냐하면, 재할

tmd8633.tistory.com

String Constant Pool과 String 불변성에 대해서 더 알고싶다면 해당 게시물을 읽어보시는걸 추천드립니다.

 

 

마치며

플라이웨이트 패턴은 메모리 사용량을 크게 줄일 수 있는 강력한 도구입니다. 하지만 모든 상황에서 적합한 것은 아니며, 다음과 같은 상황에서 고려해볼 만합니다:

  1. 애플리케이션에서 많은 수의 유사한 객체를 사용할 때
  2. 객체의 상태를 내부와 외부로 명확히 구분할 수 있을 때
  3. 메모리 사용량이 중요한 제약 조건일 때

 

 

 

 

 

복합 패턴 개요

복합 패턴은 객체들을 트리 구조로 구성하여 부분-전체 계층구조를 구현하는 패턴입니다. 이 패턴을 사용하면 클라이언트에서 개별 객체와 복합 객체를 동일하게 다룰 수 있습니다. 오늘은 이 패턴에 대해서 공부해보겠습니다.

출처 : https://ko.wikipedia.org/wiki/%EC%BB%B4%ED%8F%AC%EC%A7%80%ED%8A%B8_%ED%8C%A8%ED%84%B4

 

 

 

뭐하는 패턴이지?

위의 구조를 보면 당최 뭐하는 패턴인지 감이 안잡힐 수 있습니다. 간단하게 말하면 복합객체와 단일객체를 동일한 타입을 취급해서 단순화시키는 것인데요. 예를들어 폴더 - 파일의 구조를 가진다고 했을때, 폴더와 파일을 하나의 객체로 동일하게 취급해서 구조를 단순화 시키는 것입니다. 폴더안에는 파일이 들어갈 수 있고, 폴더안에는 폴더가 들어갈 수 있는 구조를 생각했을 때 이걸 효과적으로 단일화 시킬 수 있다는 뜻이죠. 코드를 보면서 이해해보도록 하겠습니다.

 

 

 

복합 패턴의 구조

위의 사진에서 Leaf는 단일객체, Composite는 복합객체를 뜻합니다. 우리는 Leaf -> 파일, Composite -> 폴더로 치환해서 코드를 구현해보겠습니다.

 

public interface FileComponent {

    int getSize();
    String getTitle();

}

 

public class File implements FileComponent {

    private final String title;
    private final int size;

    public File(String title, int size) {
        this.title = title;
        this.size = size;
    }

    @Override
    public int getSize() {
        return size;
    }

    @Override
    public String getTitle() {
        return title;
    }
}

 

public class Folder implements FileComponent {

    private final String title;
    private final List<FileComponent> files = new ArrayList<>();

    public Folder(String title) {
        this.title = title;
    }

    @Override
    public int getSize() {
        int size = 0;
        for (FileComponent file : files) {
            size += file.getSize();
        }
        return size;
    }

    @Override
    public String getTitle() {
        return title;
    }

    public void add(FileComponent file) {
        files.add(file);
    }

    public void remove(FileComponent file) {
        files.remove(file);
    }

}

 

FileComponent interface를 만들고 파일과 폴더에서 구현했습니다.

 

 

    public static void main(String[] args) {
        Folder folder1 = new Folder("폴더1");
        folder1.add(new File("파일1", 300));

        Folder folder2 = new Folder("폴더2");
        folder2.add(new File("파일2", 1000));
        folder2.add(new File("파일3", 500));


        Folder folder3 = new Folder("폴더2");


        folder1.add(folder2);
        folder1.add(folder3);

        System.out.println("folder1.getTitle() = " + folder1.getTitle()); // 폴더1
        System.out.println("folder2.getSize() = " + folder1.getSize()); // 1800
    }

 

폴더1을 만들고 파일1을 넣었습니다.

폴더2를 만들고 파일2와 파일3을 넣었습니다.

그리고 아무것도 들어있지않은 폴더3을 만들었습니다.

 

마지막으로 폴더1에 폴더2와 폴더3을 넣었습니다.

 

이제 folder1의 title과 size를 출력해보면 내부에 들어있는 모든 파일의 크기가 출력됩니다.

 

 

 

복합 패턴의 장단점

장점

  1. 클라이언트 코드를 단순화
  2. 새로운 종류의 구성요소를 쉽게 추가
  3. 전체-부분 관계를 표현하기 용이

단점

  1. 설계가 지나치게 일반화될 수 있음
  2. 제약사항을 추가하기 어려움

 

 

 

복합패턴을 이용해 메뉴시스템 구현

복합패턴에 대해서 배웠으니 이 패턴을 활용해서 메뉴시스템을 구현해보겠습니다.

 

public abstract class MenuComponent {

    protected String name;
    protected String description;

    public MenuComponent(String name, String description) {
        this.name = name;
        this.description = description;
    }

    public abstract void print();
}
public class MenuItem extends MenuComponent {

    private int price;

    public MenuItem(String name, String description, int price) {
        super(name, description);
        this.price = price;
    }

    @Override
    public void print() {
        System.out.println(name + ": " + description + " - " + price + "원");
    }
}
public class Menu extends MenuComponent {
    
    private List<MenuComponent> menuComponents = new ArrayList<>();

    public Menu(String name, String description) {
        super(name, description);
    }

    public void add(MenuComponent menuComponent) {
        menuComponents.add(menuComponent);
    }

    @Override
    public void print() {
        System.out.println("\n" + name + ": " + description);
        System.out.println("--------------------");

        for (MenuComponent menuComponent : menuComponents) {
            menuComponent.print();
        }
    }
}

 

폴더 - 파일 구조와 동일하게 구현했습니다. 조금 다른게 있다면 interface -> abstract가 변경된게 있습니다만 무시해도됩니다.

 

    public static void main(String[] args) {
        // 메인 메뉴
        Menu mainMenu = new Menu("메인 메뉴", "메뉴를 선택해주세요");

        // 커피 메뉴
        Menu coffeeMenu = new Menu("커피", "커피를 선택해주세요");
        coffeeMenu.add(new MenuItem("아메리카노", "진한 에스프레소의 맛과 향을 부드럽게 즐길 수 있는 아메리칸 스타일의 커피입니다.", 4500));
        coffeeMenu.add(new MenuItem("카메라떼", "진한 에스프레소에 우유를 넣어 풍부한 커피향을 부드럽게 즐실 수 있습니다.", 5200));

        // 음식 메뉴
        Menu foodMenu = new Menu("음식", "커피와 함께 즐길 음식을 선택해주세요");
        foodMenu.add(new MenuItem("크로크무슈", "햄과 치즈를 차곡차곡 쌓아 올려 만든 프랑스식 샌드위치로 사르르 녹아내린 모짜렐라 치즈가 입안 가득 풍성한 맛", 4500));
        foodMenu.add(new MenuItem("페스토 햄 파니니", "터키햄과 모짜렐라치즈를 두둑하게 채운 대중적인 파니니", 6500));

        // 메인메뉴에 커피, 음식메뉴를 추가
        mainMenu.add(coffeeMenu);
        mainMenu.add(foodMenu);

        // 전체 메뉴 출력
        mainMenu.print();
    }
메인 메뉴: 메뉴를 선택해주세요
--------------------

커피: 커피를 선택해주세요
--------------------
아메리카노: 진한 에스프레소의 맛과 향을 부드럽게 즐길 수 있는 아메리칸 스타일의 커피입니다. - 4500원
카메라떼: 진한 에스프레소에 우유를 넣어 풍부한 커피향을 부드럽게 즐실 수 있습니다. - 5200원

음식: 커피와 함께 즐길 음식을 선택해주세요
--------------------
크로크무슈: 햄과 치즈를 차곡차곡 쌓아 올려 만든 프랑스식 샌드위치로 사르르 녹아내린 모짜렐라 치즈가 입안 가득 풍성한 맛 - 4500원
페스토 햄 파니니: 터키햄과 모짜렐라치즈를 두둑하게 채운 대중적인 파니니 - 6500원

 

출력결과입니다.

 

 

마치며

복합 패턴은 트리 구조의 객체 구성을 다룰 때 매우 유용한 패턴입니다. 특히 전체-부분 관계를 표현해야 하는 상황에서 강력한 도구가 될 수 있습니다. 하지만 설계가 지나치게 일반화될 수 있다는 점을 주의해야 하며, 실제 요구사항에 맞게 적절히 제약을 추가하는 것이 중요합니다.

퍼사드 패턴

퍼사드 패턴은 복잡한 클래스들을 편하게 사용하기위해 인터페이스를 구성하는 구조 패턴입니다.

 

 

 

예제

IOT 를 조작하는 컨트롤러를 만든다고 가정해보겠습니다.

컨트롤러는 일어날때 커튼을 열고, 전등을 키고, 스피커에서 음악이 재생되도록 하고싶고,

집을 떠날때는 전등을 끄고, 스키퍼에서 음악이 멈추도록 하고싶습니다.

 

public class Remote {

    private final Lights lights;
    private final Curtains curtains;
    private final Speaker speaker;

    public Remote(Lights lights, Curtains curtains, Speaker speaker) {
        this.lights = lights;
        this.curtains = curtains;
        this.speaker = speaker;
    }

    public void wakeUp() {
        lights.on();
        curtains.open();
        speaker.musicStart();
    }

    public void leaveHome() {
        lights.off();
        speaker.musicStop();
    }
    
}
public class Lights {

    public void on() { ... }
    public void off() { ... }
    
}

public class Curtains {

    public void open() { ... }
    public void close() { ... }
    
}

public class Speaker {

    public void musicStart() { ... }
    public void musicStop() { ... }
    
}

 

이렇게 작성한다면 Remote를 사용하는 사용자는 내부 클래스의 구현에 대해서 알 필요가 없어지고, 내부 클래스의 수정이 일어나도 Remote 코드만 수정하면 되므로 유연성이 향상됩니다.

 

 

이 퍼사드 패턴은 디자인패턴에 대해서 알지 못하더라도 그냥 자연스럽게 만들어지는 패턴같습니다. 쉽고 유용한 패턴이니 잘 적용해보시기 바랍니다.

데코레이터 패턴

데코레이터 패턴은 객체를 동적으로 기능을 확장하거나 추가할 수 있는 패턴입니다. 특징으로는 객체를 감싸는 방식으로 기능을 추가하는건데요. 말로 설명하는 것보다 예제를 통해 배워보도록 하겠습니다.

 

 

 

예제

커피를 예로 들어보겠습니다.

커피의 종류는 아메리카노, 카페라떼, 등등이 있습니다.

이 커피들에 설탕이나 샷을 추가할 수 있다고 했을때, 어떻게 구현해야 할까요?

 

커피와 추가메뉴를 나누고 거기에서 커피객체에 메뉴를 추가하는 식으로 구현할 수 있을것입니다. 그러나 오늘은 데코레이터 패턴을 사용해상속을 통한 확장이 아닌 객체를 감싸는 방식으로 확장을 해보겠습니다.

 

전체적인 구조는 다음과 같습니다.

 

 

 

Coffee

public interface Coffee {

    String getDescription();
    int getCost();
    
}
public class Americano implements Coffee {

    @Override
    public String getDescription() {
        return "아메리카노";
    }

    @Override
    public int getCost() {
        return 5000;
    }
}

 

Coffee에는 메뉴의 이름을 반환하는 getDescription가 가격을 반환하는 getCost()가 있습니다.

Coffee를 상속받은 Americano를 구현했습니다.

 

 

 

CoffeeDecorator

public abstract class CoffeeDecorator implements Coffee {

    private final Coffee coffee;

    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public String getDescription() {
        return coffee.getDescription();
    }

    @Override
    public int getCost() {
        return coffee.getCost();
    }

}

 

public class ShotDecorator extends CoffeeDecorator {

    public ShotDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", 샷 추가";
    }

    @Override
    public int getCost() {
        return super.getCost() + 500;
    }

}

public class SugarDecorator extends CoffeeDecorator {
    
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    public String getDescription() {
        return super.getDescription() + ", 설탕 추가";
    }

    @Override
    public int getCost() {
        return super.getCost() + 300;
    }

}

 

CoffeeDecorator를 상속받아 커피에 추가할 수 있는 옵션인 설탕과 샷 객체를 구현했습니다.

CoffeeDecorator의 구현객체를 보면 멤버변수에 Coffee를 가지고있습니다. 추가옵션은 Coffee가 반드시 필요하기 때문입니다.

 

실행결과

    public static void main(String[] args) {
        Coffee coffee = new SugarDecorator(new ShotDecorator(new Americano()));

        System.out.println(coffee.getDescription());
        System.out.println(coffee.getCost());

        // 아메리카노, 샷 추가, 설탕 추가
        // 5800
    }

 

coffee 변수안에 Decorator를 사용해서 추가옵션을 넣어 줄 수 있었습니다.

이렇게 하면 기존코드를 수정하거나 변경없이도 추가 확장할 수 있습니다.

 

여기서 살짝 아쉬운부분은 CoffeeDecorator에서 Coffee를 이미 Override를 했기때문에 Decorator를 구현할때 Override를 강제하기 않는다는것이었는데요. 그래서 강제할 수 있도록 약간 더 수정해보았습니다.

 

public abstract class CoffeeDecorator implements Coffee {

    private final Coffee coffee;

    public CoffeeDecorator(Coffee coffee) {
        this.coffee = coffee;
    }

    @Override
    public String getDescription() {
        return coffee.getDescription() + getExtra();
    }

    @Override
    public int getCost() {
        return coffee.getCost() + getExtraCost();
    }

    abstract String getExtra();
    abstract int getExtraCost();
    
}

 

public class ShotDecorator extends CoffeeDecorator {

    public ShotDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    String getExtra() {
        return ", 샷 추가";
    }

    @Override
    int getExtraCost() {
        return 500;
    }
}

public class SugarDecorator extends CoffeeDecorator {
    
    public SugarDecorator(Coffee coffee) {
        super(coffee);
    }

    @Override
    String getExtra() {
        return ", 설탕 추가";
    }

    @Override
    int getExtraCost() {
        return 300;
    }
}

 

CoffeeDecorator에서 getExtra()와 getExtraCost()를 추상화하여 상속받은 클래스에게 구현을 강제하게 했습니다.

이렇게 하니까 더 코드가 깔끔해진것 같습니다.

 

이 데코레이터 패턴은 유연하게 확장이 가능하고, 책임을 분리하고, DIP를 준수하는 등 좋은 장점들이 많으나 코드 복잡성이 증가하고, 순서에 의존하는 단점도 존재합니다.

브릿지패턴

브릿지패턴은 객체의 확장성을 높히기 위한 디자인패턴입니다. 객체를 구현클래스와 추상클래스로 분리하고 추상클래스에서 확장해가는 패턴입니다. 오늘은 브릿지 패턴에 대해서 최대한 쉽게 알아보겠습니다.

 

 

 

 

 

예제

 

리모콘과 디바이스의 관계를 보겠습니다.

리모콘은 어떤 디바이스의 종류가 오든 상관없습니다. 그저 전원을 키거나 끄는 역활을 할 뿐입니다.

디바이스는 어떤 리모콘이 오든 상관없습니다. 신호가 오면 전원을 키거나 끄도록 하면 됩니다.

 

여기서 리모콘은 디바이스를 의존하고있어야 어떤 디바이스에 신호를 보낼 지 결정할 수 있습니다. 따라서 리모콘은 디바이스를 의존해야합니다.

리모콘이 디바이스를 의존하는 것을 bridge(다리) 로 표현한 것이 바로 브릿지패턴입니다.

 

 

 

Remote

public abstract class Remote {

    protected final Device device;

    public Remote(Device device) {
        this.device = device;
    }

    public abstract void power();

}

 

public class BasicRemote extends Remote {

    public BasicRemote(Device device) {
        super(device);
    }

    @Override
    public void power() {
        if (device.isOn()) {
            device.off();
        } else {
            device.on();
        }
    }
}

public class SmartRemote extends Remote {

    public SmartRemote(Device device) {
        super(device);
    }

    @Override
    public void power() {
        if (device.isOn()) {
            device.off();
        } else {
            device.on();
        }
    }

    public void runApp(String app) {
        device.runApp(app);
    }
}

 

 

Device

public interface Device {

    void on();
    void off();
    boolean isOn();
    void runApp(String app);

}

 

public class TV implements Device {

    private boolean on = false;

    @Override
    public void on() {
        on = true;
        System.out.println("TV is on");
    }

    @Override
    public void off() {
        on = false;
        System.out.println("TV is off");
    }

    @Override
    public boolean isOn() {
        return on;
    }

    @Override
    public void runApp(String app) {
        if (isOn()) {
            System.out.println("TV Run App " + app);
        } else {
            System.out.println("TV is not On");
        }
    }
}

public class Radio implements Device {

    private boolean on = false;
    @Override
    public void on() {
        on = true;
        System.out.println("Radio is on");
    }

    @Override
    public void off() {
        on = false;
        System.out.println("Radio is off");
    }

    @Override
    public boolean isOn() {
        return on;
    }

    @Override
    public void runApp(String app) {

    }

}

public class Computer implements Device {

    private boolean on = false;

    @Override
    public void on() {
        on = true;
        System.out.println("Computer is on");
    }

    @Override
    public void off() {
        on = false;
        System.out.println("Computer is off");
    }

    @Override
    public boolean isOn() {
        return on;
    }

    @Override
    public void runApp(String app) {
        if (isOn()) {
            System.out.println("Computer run app : " + app);
        } else {
            System.out.println("Computer is not On");
        }
    }
}

 

 

실행결과

    public static void main(String[] args) {
        Device radio = new Radio();
        Remote basicRemote = new BasicRemote(radio);
        basicRemote.power();

        Device tv = new TV();
        SmartRemote smartRemote = new SmartRemote(tv);
        smartRemote.power();
        smartRemote.runApp("YouTube");

        smartRemote.power();
        smartRemote.runApp("Netflix");

    }
Radio is on
TV is on
TV Run App YouTube
TV is off
TV is not On

 

 

브릿지 패턴은 추상클래스와 구현클래스를 분리할 수 있어서 독립적인 확장이 가능합니다. 그리고 런타임시점에 구현체를 변경할 수 있다는 장점이 있습니다. 다만 이 브릿지패턴도 객체지향언어가 모두 그렇듯 분리하면 할수록 코드 복잡성이 증가하는 단점이 있습니다.

어댑터 패턴

어댑터 패턴은 호환되지 않은 인터페이스를 연결하는 디자인패턴입니다. 어댑터 패턴은 어려운거 없으니 바로 확인해보겠습니다.

 

 

 

 

 

public interface Target {

    void doSomething();
}
public class Adaptee {

    public void action() {
        System.out.println("action");
    }
}

 

Target과 Adaptee 는 서로 호환될 수 없는 관계입니다. 이 관계를 호환되게 만드는 것이 어댑터 패턴입니다.

 

 

우리는 기존코드를 수정하지 않으면서 Target 을 통해 Adaptee 클래스를 통제하고싶습니다. 그래서 두 클래스 사이에 새로운 클래스를 추가하겠습니다.

 

public class Adapter implements Target {

    private final Adaptee adaptee;

    public Adapter(Adaptee adaptee) {
        this.adaptee = adaptee;
    }

    @Override
    public void doSomething() {
        adaptee.action();
    }
}

 

어댑터 패턴 끝났습니다.

 

 

 

Target 인터페이스를 Adapter 클래스에서 상속받고 필드에 Adaptee 를 받아 doSomething 에서 action 메소드를 오버라이딩 했습니다.

 

 

 

이 어댑터 패턴은 기존 코드의 수정 없이 호환되지 않는 클래스를 이어줄 수 있다는 장점이 있습니다. 다만 호환되지 않는 클래스의 연결로 인해 코드 복잡성이 증가하면서 유지보수가 어려워 질 수 있다는 단점도 존재합니다.

싱글톤 패턴

싱글톤 패턴은 단 하나의 유일한 객체를 만들기 위한 코드 패턴입니다. 싱글톤 패턴을 따르는 클래스는 생성자가 여러번 호출되더라도 실제 생성되는 객체는 단 하나이고, 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴하게됩니다. 주로 공통된 객체를 여러개 생성해서 사용하는 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 패턴을 쉽게 구현할 수 있습니다.

+ Recent posts