디자인 패턴(1)

Jeong Gyejin·2023년 2월 22일
0

JAVA

목록 보기
7/18

싱글턴 패턴

오직 하나의 인스턴스만 가지는 패턴으로 이 객체를 싱글턴이라고 이야기 합니다.

사용 이유

  • 싱글턴 패턴을 사용하는 이유는 정보를 보관하고 공유하고자 하는 클래스를 한 번의 메모리만 할당하고 그 할당한 메모리에 대해서 객체로 관리하기 위해서 주로 사용합니다.
  • 여러 클래스에서 싱글턴 클래스의 생성자를 호출하더라도 처음 한번 생성된 인스턴스를 반환해주기 때문에 정보 공유차원에서도 변수 관리, 즉 동기화에 용이하기 때문에 사용합니다.

사용 방법

  • 싱글턴을 만들기 위해서는 클래스 외부에서 new 연산자로 생성자를 호출할 수 없도록 막아야 합니다. 생성자를 호출한 만큼 객체가 생성되기 때문입니다.
  • 생성자를 외부에서 호출할 수 없도록 하려면 생성자 앞에 private이라는 접근제한자를 붙여줘야합니다.
  • 그리고 자신의 타입인 정적 필드를 하나 선언하고 자신의 객체를 생성해 초기화합니다.
public class Singleton{

private static Singleton singleton = new Singleton();  // 정적 필드

private Singleton(){}			// 생성자

static Singleton getInstance(){			// 정적메소드

return singleton;
	}
}

싱글턴 패턴의 장점

  • 중복된 인스턴스 생성을 할 경우 비용이 줄어든다는 장점이 있습니다.
  • 또한 사용하기 쉽고 실용적입니다.

싱글턴 패턴의 단점

  • 반복해서 사용할 경우 의존성이 높아지는 경향이 있습니다.
  • 의존성이 높아질 경우 모듈간의 결합이 너무 강해지는 문제가 있습니다.
  • 미리 생성된 하나의 인스턴스를 기반으로 하기 때문에 테스트마다 독립적인 인스턴스를 만들기 어렵다는 단접이 있습니다.

의존성 주입

  • 종속성이라고도 하며 A가 B에 의존성이 있다는 것은 B의 변경사항에 대해 A또한 변해야 한다는 것을 의미합니다.
  • 의존성 주입 원칙: 상위 모듈을 하위 모듈에서 어떠한 것도 가져오지 않아야 하며, 둘 다 추상화에 의존해야 하며, 이때 추상화는 세부사항에 의존하지 말아야 합니다.
  • 장점: 모듈들을 쉽게 교체할 수 있는 구조가 되어 테스팅하기 쉽고 구현할 때 추상화 레이어를 넣고 이를 기반으로 구현체를 넣어주기 때문에 의존성 방향이 일관되고, 쉽게 추론할 수 있으며, 관계들이 명확해집니다.
  • 단점: 모듈들이 더 많이 분리됨으로 복잡성이 증가되어 런타임 패널티가 증가하는 단점이 있습니다.

팩토리 패턴

객체의 생성 부분을 떼어내 추상화한 패턴이자 상송관계에 있는 두 클래스에서 상위 클래스가 중요 뼈대를 결정한 후 하위클래스에서 객체 생성에 관한 구체적인 내용을 결정하는 패턴입니다.

사용 이유

  • 생성할 객체 타입을 예측할 수 없을 때 사용합니다.
  • 생성할 객체의 기술하는 책임을 서브 클래스에게 맡길때 주로 사용합니다.
  • 객체 생성의 책임을 서브 클래스에 위임시키고, 서브 클래스에 대한 정보를 은닉하기 위해 사용합니다.

사용 방법

개별적으로 이름(name)을 반환하는 메소드를 작성했습니다.

public abstract class Fruit {
	public abstract String getName();
}
public class Apple extends Fruit {
	@Override
	public String getName() {
		return "Apple";
	}
}
public class Banana extends Robot {
	@Override
	public String getName() {
		return "Banana";
	}
}

FruitFactory

팩토리 메소드 패턴의 꽃은 역시 팩토리 클래스입니다. 차례대로 봅시다. 첫번째 보이는 코드는 기본 팩토리 클래스입니다.

public abstract class FruitFactory {
	abstract Fruit createFruit(String name);
}

아래 클래스는 기본 팩토리 클래스를 상속 받아 실제 로직을 구현한 팩토리입니다.

public class SuperFruitFactory extends FruitFactory {
	@Override
	Fruit createFruit(String name) {
		switch( name ){
			case "Apple": return new Apple();
			case "Banana": return new Banana();
		}
		return null;
	}
}

아래 클래스는 SuperFruitFactory 클래스와 비슷하지만 내부 구현이 조금 다릅니다. 과일 클래스의 이름을 String 인자로 받아서 직접 인스턴스를 만들어 냅니다.

public class ModifiedSuperFruitFactory extends FruitFactory {
	@Override
	Robot createFruit(String name) {
		try {
			Class<?> cls = Class.forName(name);
			Object obj = cls.newInstance();
			return (Fruit)obj;
		} catch (Exception e) {
			return null;
		}
	}
}

결과

준비가 다 되었다면 메인 프로그램을 작성해서 돌려봅시다.

public class FactoryMain {
	public static void main(String[] args) {

		FruitFactory rf = new SuperFruitFactory();
		Fruit r1 = rf.createFruit("Apple");
		Fruit r2 = rf.createFruit("Banana");

		System.out.println(r.getName());	// Apple
		System.out.println(r2.getName());	// Banana

		FruitFactory ff = new ModifiedSuperFruitFactory();
		Fruit r3 =  mrf.createFruit("pattern.factory.Apple");
		Fruit r4 =  mrf.createFruit("pattern.factory.Banana");

		System.out.println(r3.getName());	// Apple
		System.out.println(r4.getName());	// Banana
	}
}

팩토리 패턴의 장점

  • 상위 클래스와 하위 클래스가 분리되기 때문에 느슨한 결합을 가질 수 있음.
  • 상위 클래스는 인스턴스 생성 방식에 대해 전혀 알 필요가 없기 때문에 유연성이 증가하고 코드가 간결해집니다.
  • 병렬적 클래스 계층도를 연결하는 역할을 담당할 수 있습니다.

팩토리 패턴의 단점

  • 클래스가 많아지면 클래스가 바뀔때마다 새로운 서브 클래스를 생성해야합니다.
  • 의존성이 높아서 모듈간의 결합이 강해집니다.
  • 미리 생성된 하나의 인스턴스를 기반으로 하기에 테스트마다 독립적인 인스턴스를 만들기 어렵습니다.

전략 패턴

정책 패턴이라고도 하며, 객체들이 할 수 있는 행위 각각에 대해 전략 클래스를 생성하고, 유사한 행위들을 캡슐화하는 인터페이스를 정의하여, 객체의 행위를 동적으로 바꾸고싶은 경우 직접 행위를 수정하지 않고 전략을 바꿔주기만 함으로써 행위를 유연하게 확장하는 방법입니다.

사용 이유

객체가 할 수 있는 행위들을 각각의 전략으로 만들어 놓고, 동적으로 행위의 수정이 필요한 경우 전략을 바꾸는 것만으로도 행위의 수정이 가능하기 때문에 사용합니다.

사용 방법

전략 - 추상화된 알고리즘을 설정합니다.

interface Sports {
    void play();
}

class Soccer implements Sports {
    @Override
    public void play() {
        System.out.println("축구를 합니다");
    }
}

class Tennis implements Sports {
    @Override
    public void play() {
        System.out.println("테니스를 합니다");
    }
}

class Baseball implements Sports {
    @Override
    public void play() {
        System.out.println("야구를 합니다");
    }
}

Context에서 전략을 등록하고 실행합니다.

class PlaytheSports {
    Sports sports;

    void setSports(Sports sports) {
        this.sports = sports;
    }

    void play() {
        sports.play();
    }
}

Client에서 전략 제공/설정해서 보여줍니다.

class User {
    public static void main(String[] args) {
        // 플레이어가 스포츠를 하도록 전략 설정
        PlaytheSports sports = new PlaytheSports();

        // 플레이어가 축구를 하도록 전략 변경
        sports.setSports(new Soccer());
        sports.play(); // "축구를 합니다"

        // 플레이어가 테니스를 하도록 전략 변경
        sports.setSports(new Tennis());
        sports.play(); // "테니스를 합니다"
        
        // 플레이어가 야구를 하도록 전략 변경
        sports.setSports(new Baseball());
        sports.play(); // "야구를 합니다"
    }
}

전략 패턴의 장점

  • 공통 로직이 부모 클래스에 있지 않고 Context 라는 별도의 클래스에 존재하기 때문에 구현체들에 대한 영향도가 적습니다.
  • Context 가 Strategy 라는 인터페이스를 의존하고 있기 때문에 구현체를 갈아끼우기 쉽습니다.

전략 패턴의 단점

  • 로직이 늘어날 때마다 구현체 클래스가 늘어날 수 있습니다.
  • Context 와 Strategy 를 한번 조립하면 전략을 변경하기 힘듭니다.
profile
항상 더 나은 개발자가 되기 위해서 끊임없이 공부하고 학습하면서 성장하는 사람이 되겠습니다.

0개의 댓글