Reading

1.디자인 패턴(Design Pattern)

박은유 2024. 3. 17. 17:07
반응형

디자인 패턴이란?

: 프로그램을 설계할 때 나타나는 문제들을 해결하기 위해 일반적인 '규약'의 형태로 만들어 놓은 것을 의미합니다. 어떤 문제가 발생했을 때 이런식으로 해결하자! 라고 정해진 약속이라 라이브러리와는 다르게 추상적인 느낌이 있습니다. 디자인패턴을 이용해 라이브러리, API, 프레임워크 등을 만든다고 생각하면 되겠습니다. 예시를 통해 이해해보겠습니다.

 

1) 싱글톤 패턴(Singleton Pattern)

싱글톤 패턴이란 "생성자가 여러 차례 호출되더라도 실제로 생성되는 객체는 하나이고 최초 생성 이후에 호출된 생성자는 최초의 생성자가 생성한 객체를 리턴한다." (위키백과 발췌)라고 하는데 무슨말일까요? Java를 사용하셨거나 공부해보셨다면 한번쯤 들어봤을 패턴입니다. 먼저 코드를 보시죠.

 

class Main {
	
    public void A() {
    	Animal A = new Animal();
        ...
    }
    
    
    public void B() {
    	Animal B = new Animal();
        ...
    }
}

 

위의 코드에서는 Animal이라는 객체를 A()와 B()에서 각각 선언해 사용하고 있습니다. 위의 방식을 따라서 Main클래스에 더 많은 코드가 생길 수록 객체를 그만큼 새로 생성해서 사용해야 되겠죠. 그렇게 되면 메모리 사용량이 많아지게 됩니다. 따라서 이럴땐 싱글톤 패턴, 즉 하나의 객체(싱글)를 만들어 놓고 해당 객체를 다른 모듈들이 공유하여 사용하는 것입니다.

 

class Animal {
    public int age;
    public String name;

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

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

class Main {

    Animal animal = new Animal();

    public void A() {
        Animal A = animal;
        A.setAge(10);
        A.setName("A");
        System.out.println(A.age + " " + A.name);
    }

    public void B() {
        Animal B = animal;
        B.setAge(20);
        B.setName("B");
        System.out.println(B.age + " " + B.name);
    }

    public static void main(String[] args) {
        Main main = new Main();
        main.A();
        main.B();
    }
}

/*
10 A
20 B
*/

 

위의 코드를 보면 Main클래스에 Animal 클래스의 인스턴트를 하나 생성하고, A()와 B()에서 각각 가져와 사용하고 있습니다. 이렇게 하나의 인스턴트를 만들어 놓고 해당 인스턴트를 공유해 사용해 메모리를 효율적으로 사용하는것이 싱글톤 패턴의 목적이라고 할 수 있습니다. 하지만 문제점이 하나 있는데 Main 클래스 상단에 Animal객체를 생성해 두었지만, 다른 모듈에서 Animal객체를 따로 생성해 사용할 수 있기때문에 추가로 객체가 생성 될 가능성이 열려있습니다. 다음코드를 보겠습니다.

 

class Animal {
    private int age;
    private String name;

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

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

    public int getAge() {
        return age;
    }

    public String getName() {
        return name;
    }

    private static class makeSingleInstance {
        private static final Animal INSTANCE = new Animal();
    }

    public static Animal getInstance() {
        return makeSingleInstance.INSTANCE;
    }
}

class Main {


    public void A() {
        Animal A = Animal.getInstance();
        A.setAge(10);
        A.setName("A");
        System.out.println(A.getAge() + " " + A.getName());
    }

    public void B() {
        Animal B = Animal.getInstance();
        B.setAge(20);
        B.setName("B");
        System.out.println(B.getAge() + " " + B.getName());
    }

    public static void main(String[] args) {
        Main main = new Main();
        main.A();
        main.B();
    }
}

 

  위의 코드를 보면 Animal 클래스 안에 makeSingleInstance클래스를 생성하고 getInstance()를 통해 객체를 생성합니다. 여기서 살펴보아야 할 부분은 makeSingleInstance클래스의 접근제어자 private와 static입니다. private로 선언하면 해당 클래스 내에서만 접근이 가능하고 static으로 생성시 빌드와 동시에 static영역에 최초생성되고 프로그램이 꺼질때까지 유지가 되기 때문에 싱글턴 패턴의 목적인 최초의 인스턴스를 단 하나 생성해 공유하는 목적에 맞게 사용할 수 있습니다. 그렇게 사용하자고 다른 동료들과 "약속"하는 것이지요. 

 

싱글톤 패턴의 장단점

싱글톤패턴의 장점은 다음과 같습니다.

- 메모리/속도에서 이점을 챙길 수 있습니다.

- 클래스가 하나의 인스턴트만 갖는다는 것을 확신할 수 있습니다.

 

단점은 다음과 같습니다.

- 테스트코드 작성이 어렵습니다.

- 멀티스레드에서 사용이 어렵습니다.

 

 

 2) 팩토리 패턴(Factory Pattern)

 팩토리 패턴은 말그대로 공장을 떠올리게 하는 패턴입니다. 객체를 생성하는 부분을 상위클래스에 맡겨두고 하위클래스에서는 그 객체를 어떻게 생성하는지에 대해 정해주는 방식의 패턴입니다. 예를 들어 자동차 공장(상위 클래스)에 어떤 차를 만들지 각각 설계도 역할을 하는 하위클래스를 만들어 사용하는 패턴입니다. 코드를 보고 설명하겠습니다.

 

abstract class Car{

    public abstract String getName();
    public abstract int getPrice();

    @Override
    public String toString() {
        return "Car name : " + getName() + " price : " + getPrice();
    }
}

class Benz extends Car{

    private String name;
    private int price;
    public Benz(String name, int price){
        this.name = name;
        this.price = price;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public int getPrice() {
        return this.price;
    }
}

class BMW extends Car{
    private String name;
    private int price;
    public BMW(String name, int price){
        this.name = name;
        this.price = price;
    }

    @Override
    public String getName() {
        return this.name;
    }

    @Override
    public int getPrice() {
        return this.price;
    }
}

class CarFactory{
    public static Car createCar(String name, int price) {
        if(name.equals("Benz")){
            return new Benz(name, price);
        } else if(name.equals("BMW")){
            return new BMW(name, price);
        } else {
            return null;
        }
    }
}

public class Main {
    public static void main(String[] args) {
        Car car = CarFactory.createCar("BMW", 100);
        Car car2 = CarFactory.createCar("Benz", 200);
//        Car car3 = CarFactory.createCar("Audi", 300);
        System.out.println("Car name : " + car.getName() + " price : " + car.getPrice());
        System.out.println("Car name : " + car2.getName() + " price : " + car2.getPrice());
//        System.out.println("Car name : " + car3.getName() + " price : " + car3.getPrice());
    }
}

 

위의 코드는 Car 추상클래스를 생성하고 상속받는 하위클래스 Benz와 BMW를 생성했습니다. 그 후 각각의 클래스의 객체를 생성해주는 CarFactory클래스를 생성합니다. 메인메소드에서는 CarFactory의 createCar메소드를 사용해 객체를 생성합니다. 만약 CarFactory의 createCar에 이름과 맞지않은 객체를 생성하려고 하면 null을 반환합니다. 공장에 설계도가 존재하지 않는 차를 만들어달라고 요구하는 것과 같은 뜻입니다.

 

팩토리 패턴의 장점

 : 객체를 생성하는 코드 부분을 분리 시켰기 때문에 객체를 추가/수정이 일어 나더라도 객체를 생성하는 코드만 건들면 됩니다.

 

팩토리 패턴의 단점

 : 패턴을 사용하기 위해 추가로 작성되야 하는 코드가 많아집니다.

 

3) 전략 패턴(Strategy Pattern)

 전략 패턴은 객체들의 행위가 변경 될 가능성이 존재할 때 해당 행위의 전략을 생성해두고 교체를 해 사용하게 하는 패턴입니다. 예를 들어 새와 개클래스가 있고, Movable 인터페이스를 구현한다고 할 때, 다음과 같이 구현할 수 있습니다.

 

interface Movable {
    public void move();
}

class Bird implements Movable{

    @Override
    public void move() {
        System.out.println("날아서 이동했습니다.");
    }
}

class Dog implements Movable{
    
    @Override
    public void move() {
        System.out.println("뛰어서 이동했습니다.");
    }
}


public class Main {

    public static void main(String[] args) {
        Movable bird = new Bird();
        Movable dog = new Dog();
        
        bird.move();
        dog.move();
    }
}

 

새는 날아서 이동하고 개는 뛰어서 이동합니다. 하지만, 새는 걸어서도 이동 할 수 있습니다. 이렇게 행위에 변경이 일어날 확률이 있을 때 다음과 같이 전략패턴을 사용해 구현합니다.

 

interface MovableStrategy {
    public void move();
}

class FlyingMove implements MovableStrategy {

    @Override
    public void move() {
        System.out.println("날아서 이동했습니다.");
    }
}

class WalkingMove implements MovableStrategy{

    @Override
    public void move() {
        System.out.println("걸어서 이동했습니다.");
    }
}

class Moving {
    private MovableStrategy movableStrategy;
    
    public void move() {
        movableStrategy.move();
    }
    
    public void setMovableStrategy(MovableStrategy movableStrategy) {
        this.movableStrategy = movableStrategy;
    }
}

class Bird extends Moving{

}

class Dog extends Moving{
    
}


public class Main {

    public static void main(String[] args) {
        Moving bird = new Bird();
        Moving dog = new Dog();
        
        bird.setMovableStrategy(new FlyingMove());
        bird.setMovableStrategy(new WalkingMove());
        dog.setMovableStrategy(new WalkingMove());
        
        bird.move();
        dog.move();
    }
}

 

위의 코드는 MoveStrategy라는 움직임에 대한 전략인터페이스를 생성하고 move()메서드를 넣습니다. 그리고 FlyingMove클래스와 WalkingMove클래스에서 각각 move()메소드를 구현합니다. 그다음 새와 개 클래스에 바로 move()를 구현하지않고 중간에 상황에 맞게 전략을 넣어 줄 수 있는 Moving클래스를 생성해 setMovableStrategy메서드를 만듭니다. 이제 새와 개클래스는 Moving클래스를 상속받아 사용합니다. Main클래스에서 bird객체는 날아서 이동도 가능하고 원하면 걸어서도 이동이 가능해졌습니다.

 

 이처럼 전략패턴은 클래스내에 메서드를 정의하면 추후 변경이 어렵기때문에 전략적으로 변경해 사용할 때 채택하면 좋은 패턴입니다.

 

그 외의 패턴들

 이처럼 디자인패턴은 종류도 방식도 다양합니다. 주체가 어떤 객체의 상태 변화를 관찰하다가 상태 변화가 있을 때마다 메서드 등을 통해 옵저버 목록에 있는 옵저버들에게 변화를 알려주는 "옵저버 패턴", 대상 객체에 접근하기 전 그 접근에 대한 흐름을 가로채 대상 객체 앞단의 인터페이스 역할(비서 역할을 떠올리면 됩니다.)을 하는 "프록시 패턴", 이터레이터(iterator)를 사용하여 컬렉션의 요소들에 접근해 여러 가지 자료형의 구조에 상관없이 순회하는 "이터레이터 패턴", 그리고 Spring을 사용해보셨다면 많이 접했을 "MVC 패턴" 등 정말 많은 패턴이 존재합니다.

 

 이러한 패턴들은 글 초반에서 말했듯이 라이브러리처럼 고정되어 있는것이 아니라 라이브러리나 모듈내에 녹아있는 패턴들을 정의한 규약들입니다. 프로그래밍을 할 때 상황에 알맞은 패턴들을 잘 사용하는것이 중요하겠습니다.

반응형