디자인패턴에는 생성패턴
, 구조패턴
, 행위패턴
총 3가지로 나눌 수 있습니다. 그 중 첫번째로 생성패턴
에 대해서 알아보겠습니다.
말 그대로 객체를 생성하는 것에 중점을 두는 패턴입니다.
따라서 객체 생성에 대한 로직은 캡슐화
하고 이에 따라 재사용성
을 증가시킬 수 있다는 장점이 있습니다. 객체를 독립적으로 생성하고 교체할 수 있게 되기 때문입니다.
생성패턴에는 위의 이미지에서처럼 싱글톤, 팩토리, 추상, 빌터, 프로토타입 패턴으로 꽤 많습니다!
주니어 입장이기 때문에 싱글톤패턴
, 팩토리패턴
만 자세히 알아보았습니다. (가장 일반적으로 쓰이는 생성패턴이라고 합니다.)
클래스의 인스턴스를 '하나만' 생성되도록 보장하는 패턴이다. 즉, 인스턴스가 필요할 때, 똑같은 인스턴스를 만들지 않고 기존의 인스턴스를 활용하는 것이다.
용도
커넥션 풀
, 스레드풀
, 캐시
, 로그 기록 객체
등사용 방법 : JAVA에서는 생성자를 private
하게 선언하여 다른 곳에서 생성하지 못하게 만들고, getInstance()
메소드를 통해 사용할 수 있도록 구현한다.
효과 및 장점
단점
개방-폐쇄 원칙
에 위반하게 된다. ❗❗❗ 이처럼 단점이 치명적이므로 꼭 필요한 상황이 아니라면 큰 프로젝트에서는 지양하는 것이 좋을 것 같다.
하지만 자바처럼 멀티스레드 환경에서도 안전하게 싱글톤 패턴을 구현할 수 있는 방식이 있습니다.
private static
으로 인스턴스 변수를 만든다.private
으로 생성자를 만들어 외부에서의 생성을 막는다.synchronized
동기화를 활용하여 스레드를 안전하게 만든다. (단, synchronized
는 성능 저하를 초래하므로 사용을 지양한다.)public class ThreadSafe_Lazy_Initialization{
private static ThreadSafe_Lazy_Initialization instance;
private ThreadSafe_Lazy_Initialization(){}
public static synchronized ThreadSafe_Lazy_Initialization getInstance(){
if(instance == null){
instance = new ThreadSafe_Lazy_Initialization();
}
return instance;
}
synchronized
를 걸지 않고, 먼저 인스턴스의 존재여부를 확인한다. 따라서 두 번째 조건문에 있는 synchronized
로 동기화를 시켜 인스턴스를 생성한다. synchronized
를 실행하지 않아 1번만큼의 성능저하는 일어나지 않는다. (하지만 여전히 synchronized
로 인한 성능저하는 해결되지 않음)public class ThreadSafe_Lazy_Initialization{
private volatile static ThreadSafe_Lazy_Initialization instance;
private ThreadSafe_Lazy_Initialization(){}
public static ThreadSafe_Lazy_Initialization getInstance(){
if(instance == null) {
synchronized (ThreadSafe_Lazy_Initialization.class){
if(instance == null){
instance = new ThreadSafe_Lazy_Initialization();
}
}
}
return instance;
}
}
package SingleTonExample;
public class InitializationOnDemandHolderIdiom {
private InitializationOnDemandHolderIdiom(){}
private static class SingleTonHolder{
// static이므로 클래스 로딩 시점에 한번만 호출됨
// final값으로 다시 값이 할당받지 않도록 함
private static final InitializationOnDemandHolderIdiom instance = new InitializationOnDemandHolderIdiom();
}
public static InitializationOnDemandHolderIdiom getInstance(){
return SingleTonHolder.instance;
}
}
싱글톤의 특성을 이용하여 데이터베이스
, 캐시
, 로깅
등으로 용도가 자주 쓰인다고 위에서 언급했었습니다! 모두 상태를 공유해야 하고 리소스 사용의 최적화가 필요한 예시들입니다.
그 중 로깅
에 쓰이는 Logger 클래스를 만든다고 생각해 봅시다.
public class Logger {
private static Logger instance;
private String logData = "";
private Logger(){
// private Constructor
}
public static synchronized Logger getInstance(){
if (instance == null){
instance = new Logger();
}
return instance;
}
}
public void log(String message){
logData += message + "\n";
}
public void printLog(){
System.out.println(logData);
}
위와 같이 private
으로 생성되는 인스턴스를 클라이언트 입장에서 어떻게 이용할 수 있을까요?
public class Client{
public static void main(String[] args){
Logger logger = Logger.getinstance();
logger.log("message1");
logger.log("message2...");
logger.printLog();
}
}
위와 같이 클라이언트는 getInstance()를 한 번만 호출하여 log를 공유하여 사용할 수 있게 됩니다. 🎉
팩토리 Class가 있으며, 해당 Class에서 객체생성 로직을 캡슐화한 패턴을 말합니다. 클라이언트가 직접 객체를 생성하지 않아, 코드간의 결합도가 높습니다.
용도
사용 방법 : Creator라는 최상위 클래스가 있어, 팩토리 메서드를 추상화하여 서브 클래스인 ConcreteCreator에서 구현하게끔 만듭니다.
위의 이미지 예시를 들면, 바리스타가 Creator로서 최상위 클래스로 있습니다. recipe()라는 팩토리 메서드를 만들어, 라떼와 아메리카노 레시피를 재정의하게 됩니다.
효과 및 장점
단일 책임 원칙
준수 : 객체 생성을 한 클래스로 정리합니다.개발/폐쇄 원칙
준수 : 새로운 유형을 생성할 때도 기존 코드는 수정하지 않고 도입이 가능합니다. 단점
class Animal{
String name, color;
@Override
public String toString(){
return String.format("my friend is called {name : '%s'}, and she is {color : '%s'} one", name, color);
}
}
public static Cat adoptCat(String name, String color){
if (name == null){
throw new IllegalArgumentException("반려동물의 이름을 지어주세요");
}
// 동물 객체 생성
Animal animal = new Animal();
// 객체 생성 후 처리
animal.name = name;
if name.equalsIgnoreCase("고양이"){
animal.color = "white";
}else if (animal.equalsIgnoreCase("강아지"){
animal.color = "black";
}
System.out.println(animal.name + " 이를 입양하였습니다. ");
return animal;
}
public static void main(String[] args){
Animal 소금 = adoptAnimal("고양이");
Animal 흑임자 = adoptAnimal("강아지");
}
💥 위의 방식은 강아지/말/새 등 객체의 유형이 늘어난다면 코드도 복잡해지고 유지보수가 힘들어질 것입니다. 이를 개선하기 위해 아래와 같이 리팩토링이 가능합니다.
// 동물 객체
Class Animal{
String name, color;
@Override
public String toString{
return String.format("my friend is called {name : '%s'}, and she is {color : '%s'} one", name, color);
}
}
class Cat extends Animal{
Cat(String name, String color){
this.name = name;
this.color = color;
}
}
class Dog extends Animal{
Dog(String name, String color){
this.name = name;
this.color = color;
}
}
// 공장 객체 : 입양보호소
class AdoptFactory{
final animal adopt(String email){
validate(email);
Animal animal = adoptProcess(); // 입양 절차
sendEmail(email, animal);
return animal;
}
// 팩토리 메서드
abstract protected Animal adoptProcess();
class CatAdoptFactory extends AdoptFactory{
@Override
protected Animal adoptProcess(){
return new Cat("고양이", "yellow")
}
}
class DogAdoptFactory extends AdoptFactory{
@Override
protected Animal adoptProcess(){
return new Dog("강아지", "black")
}
}
private void validate( String email){
if (email == null) {
throw new IllegalArgumentException("이메일을 남겨주세요");
}
}
private void sendEmail(String email, Animal animal){
System.out.println(animal.name + " 이를 입양정보를 "+ email + "을 통해 보내드렸습니다." );
}
}
class Client {
public static void main(String[] args){
Animal cat = new CatAdoptFactory().adopt("aaa@naver.com);
Animal dog = new DogAdoptFactory().adopt("bbb@naver.com);
이처럼, 고양이 입양 절차를 통해 입양팩토리 객체를 생성함으로서 수정에는 닫혀있고 확장에는 열려있는 구조로 리팩토링이 가능해진다.
👻 참고 | Spring Framework의 BeanFactory도 팩토리 패턴을 이용했다고 한다.
Creator
: Bean FactoryConcreteCreator
: ClassPathXmlApplicationContext, AnnotationConfigApplicationContext
위의ConcreteCreator
에서 넘기는Object
타입을 넘겨받는 인스턴스가ConcreteProduct
가 된다고 한다.
ConcreteProduct
: 컴포넌트 스캔, bean설정 어노테이션...