회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
AWS 이용 중이라면 최대 700만 원 지원받으세요
오늘은 자바 디자인 패턴 시리즈 세 번째 글로 이번 편에서는 생성 패턴(Creational Patterns)의 종류 및 프로젝트 적용 방법에 대해 알아보고자 합니다. 전편에서 살펴본 구조 패턴처럼 생성 패턴 역시 소프트웨어 설계에 있어 다양하게 활용되고 있습니다. 자바 생성 패턴 중 싱글턴(Singleton), 팩토리(Factory), 빌더(Builder), 프로토타입(Prototype) 패턴을 살펴보고, 실제 프로젝트에서 어떻게 적용하는지 참고해 보시길 바랍니다.
회원가입을 하면 원하는 문장을
저장할 수 있어요!
다음
회원가입을 하면
성장에 도움이 되는 콘텐츠를
스크랩할 수 있어요!
확인
오늘은 자바 디자인 패턴 시리즈 세 번째 글로 이번 편에서는 생성 패턴(Creational Patterns)의 종류 및 프로젝트 적용 방법에 대해 알아보고자 합니다. 전편에서 살펴본 구조 패턴처럼 생성 패턴 역시 소프트웨어 설계에 있어 다양하게 활용되고 있습니다. 자바 생성 패턴 중 싱글턴(Singleton), 팩토리(Factory), 빌더(Builder), 프로토타입(Prototype) 패턴을 살펴보고, 실제 프로젝트에서 어떻게 적용하는지 참고해 보시길 바랍니다.
디자인 패턴은 크게 생성 패턴, 구조 패턴, 행동 패턴으로 나뉩니다. 이 중에서 생성 패턴은 객체를 생성하는 방법에 중점을 두는 디자인 패턴을 말하는데요. 생성 패턴에 속한 디자인 패턴들은 객체 생성에 관련된 로직을 캡슐화하여 코드의 재사용성을 향상시켜 줍니다.
생성 패턴은 시스템의 다른 부분과 객체를 독립적으로 생성하고 교체할 수 있도록 하여, 효율적인 프로그램 개발이 가능합니다. 이를 통해 유지 보수가 좀 더 용이해질 수 있죠. 앞서 언급했듯이 생성 패턴의 종류에는 싱글턴, 팩토리 메서드, 추상 팩토리, 빌더, 프로토타입 패턴 등이 있는데요. 각 패턴들의 특징과 사용 예제들을 살펴보겠습니다.
싱글턴 패턴은 클래스의 인스턴스가 한 개만 생성되도록 보장하는 디자인 패턴입니다. 디자인 패턴 중 가장 기본적인 형태의 패턴이라고 할 수 있는데요. 싱글턴 패턴을 적용하면 하나의 인스턴스만 유지하기 때문에 리소스를 절약하고, 상태를 전역적으로 공유할 수 있다는 장점이 있습니다. 다만 전역 변수와 같은 문제로 인해 예측하지 못한 동작을 일으키거나 테스트가 어렵다는 단점도 있습니다.
싱글턴 패턴은 로깅(Logging), 데이터베이스(Database, 캐시(Cache)와 같이 한 번 생성된 인스턴스를 계속 유지하면서 그 상태를 공유해야 하는 경우에 주로 사용됩니다. 이런 부분에 싱글턴 패턴을 적용하면 리소스 사용을 최적화할 수 있는 것이죠. 이번 예시에서는 아래와 같이 로깅 작업에 사용되는 Logger 클래스를 예시로 들어보겠습니다.
이 예제에서 Logger 클래스는 자신의 유일한 인스턴스(instance)를 가지며, getInstance() 메서드를 통해 이 인스턴스에 접근할 수 있습니다. 유의해서 볼 점은 Logger의 생성자를 private으로 선언했다는 점입니다.
이를 통해 외부에서 직접 인스턴스를 생성할 수 없게 했습니다. 즉, Logger의 getInstance() 메서드를 통해서만 인스턴스를 얻을 수 있도록 한 것이 싱글턴 패턴의 핵심이라고 할 수 있죠. 그리고 인스턴스가 아직 생성되지 않았을 때만 new Logger()를 호출하여 인스턴스를 생성하기 때문에, Logger 클래스의 인스턴스가 단 하나만 생성되도록 합니다.
getInstance() 메서드를 더 살펴보면, 해당 메서드는 synchronized로 선언되기 때문에 멀티스레드 환경에서도 동기화를 보장합니다. 물론 동기화 처리 방법에는 synchronized 키워드 외에 다른 방법들도 있지만 여기서는 간단하게 구현했습니다.
위 코드는 클라이언트 클래스에서 Logger 클래스의 getInstance() 메서드를 사용하는 예시 코드입니다. 이 코드에서 Client 클래스는 Logger의 getInstance() 메서드를 호출하여 Logger 인스턴스를 가져옵니다. 그리고 이 인스턴스를 사용하여 메시지를 로깅하고 출력하죠. 이와 같이 싱글턴 패턴으로 Logger 인스턴스를 공유하면서, 여러 클라이언트 클래스에서 동일한 로그 데이터에 접근하고 변경할 수 있습니다.
다음은 팩토리 패턴인데요. 팩토리 클래스를 통해 객체 생성 로직을 캡슐화하는 디자인 패턴을 말합니다. 이를 통해 클라이언트 코드가 직접 인스턴스 객체를 생성하지 않기 때문에, 시스템 전체 코드의 결합도를 줄이고 확장성을 높일 수 있습니다. 팩토리 패턴은 특히 다양한 타입의 객체를 생성해야 할 때 유용한데요. 예를 들어, GUI 툴킷, 데이터베이스 연결, 파일 포맷 변환과 같이 클라이언트가 요청에 따라 다양한 타입의 인스턴스를 생성해야 하는 경우에 주로 팩토리 패턴을 사용합니다.
팩토리 패턴을 적용한 간단한 자바 코드를 살펴보겠습니다. 아래 예시 코드는 Animal 인터페이스를 구현한 Dog 클래스와 Cat 클래스인데요. Dog와 Cat 클래스에서는 각각 Animal 인터페이스의 speak 메서드를 구현했습니다.
아래 코드는 Animal의 구체적인 객체를 생성하는 AnimalFactory 클래스입니다. AnimalFactory의 팩토리 메서드인 createAnimal()는 받은 인자에 맞게 적절한 타입의 동물 객체를 생성합니다.
클라이언트 코드는 이제 AnimalFactory를 사용하여 원하는 Animal 객체를 생성할 수 있습니다. 즉, 클라이언트 코드는 Dog나 Cat 객체 세부 사항을 알 필요 없이 AnimalFactory를 통해 Animal 객체를 생성하고 사용할 수 있는 것이죠. 이처럼 팩토리 클래스를 적용하면 클라이언트 코드와 Dog, Cat 같은 구체 클래스의 결합도를 줄일 수 있는 것입니다.
빌더 패턴은 객체 생성 과정을 단계별로 분리한 디자인 패턴을 말합니다. 빌더 패턴은 특히 선택적 매개변수를 가지고 있는 객체를 생성할 때 유용한데요. 예를 들어, 사용자 계정 생성, SQL 쿼리 작성, 제품의 특성 선택 등에 주로 사용됩니다. 빌더 패턴을 사용하면 각 단계별 과정을 가독성 있게 구현할 수 있으며, 유지 보수 시에도 보다 용이하게 코드를 수정할 수 있습니다.
아래의 예시 코드는 빌더 패턴을 이용하여 Pizza 객체를 생성하는 코드입니다. Pizza 클래스는 private로 설정된 dough와 topping을 속성으로 가지고 있고, 외부에서 직접 객체를 생성하지 못하도록 하기 위해서 constructor 역시 private으로 설정했습니다.
이제 Pizza 클래스의 객체를 생성하기 위해 빌더 패턴을 적용해 보겠습니다. 빌더 패턴을 적용하기 위해 아래와 같이 Pizza 클래스 내에 PizzaBuilder라는 중첩 빌더 클래스(Nested Builder Class)를 구현했는데요. 이 빌더 클래스는 Pizza 내부에 구현된 중첩 클래스이기 때문에 프라이빗한 Pizza 속성에 접근할 수 있으며, 외부에서 접근 가능한 public 메서드를 가지고 있습니다.
참고로 빌더 패턴은 중첩 클래스를 사용하지 않고도 구현할 수 있습니다. 다만, 중첩 클래스로 빌더 패턴을 적용하면 몇 가지 장점이 있는데요. 대상 클래스와 빌더 클래스가 같이 있기 때문에 가독성이 좋다는 점, 빌더 클래스를 대상 클래스의 내부에 중첩시킴으로써 일종의 캡슐화 효과를 얻을 수 있다는 점 등이 있습니다.
위 코드는 클라이언트 코드에서 빌더 클래스를 활용하여 Pizza 객체를 생성하는 예시 코드입니다. 예시 코드에서는 Pizza 객체를 생성하는 과정에서 선택해야 하는 속성(dough, topping)을 지정한 후, build() 메서드를 호출한 것을 볼 수 있는데요. 이처럼 빌더 패턴을 활용하면 복잡한 객체 생성 과정을 단계적으로 구분하여 관리할 수 있습니다.
마지막으로 살펴볼 프로토타입 패턴은 기존에 생성된 객체(프로토타입)를 복제하여 새로운 객체를 생성하는 패턴을 말합니다. 프로토타입 패턴은 특히 객체 생성 과정이 복잡하거나, 새로운 객체 생성 시 시스템 성능에 영향을 미치는 경우 주로 사용하게 되는데요. 복잡한 객체를 처음부터 새로 생성하는 대신 기존 객체를 간단히 복제함으로써 리소스를 절약하고 시스템 성능을 개선할 수 있습니다.
간단한 프로토타입 패턴의 자바 예시 코드를 보겠습니다. 아래와 같은 Sheep 클래스의 객체를 복제해야 하는 경우를 생각해 봤습니다. 이 예제에서 Sheep 클래스는 clone() 메서드를 오버라이드하여 객체를 복제할 수 있도록 만들었습니다.
여기서 Cloneable은 자바에서 제공하는 인터페이스를 말합니다. Cloneable 인터페이스를 구현한 클래스는 기본적으로 복제 가능하다는 것을 나타내는데요. 만약 Cloneable 인터페이스를 구현하지 않고, clone() 메서드를 호출하면 CloneNotSupportedException이 발생할 수 있습니다.
참고로, clone() 메서드는 Object 클래스에 정의되어 있어서 기본적으로 얕은 복사(Shallow copy)를 수행합니다. Shallow copy는 복제될 필드가 기본 데이터 타입인 경우 그대로 복사되어 괜찮지만, 필드가 참조 타입인 경우에는 참조 값(메모리 주솟값)만 복사되기 때문에 이 부분을 주의해야 합니다.
만약 단순히 메모리 주솟값을 복사하는 것이 아니라 완전히 새로운 메모리 공간을 확보해서 복사하는 깊은 복사(Deep copy)를 한다면, clone() 메서드를 오버라이드 하여 적절한 복제 로직을 구현해야 합니다.
위 클라이언트 클래스에서는 원본 객체인 "Holy"를 복제하여 새로운 객체인 "Moly"를 만들고 있습니다. 여기서 한 가지 체크할 점은 원본 객체와 복제된 객체가 서로 독립적인 객체인지 확인하는 것입니다. 앞서 살펴본 Shallow copy로 객체 복사가 적용된 경우에는 한 객체의 참조 값이 변하면 다른 객체도 같이 변하기 때문입니다.
각 객체가 서로 독립적인지 테스트하려면 복제된 객체의 속성을 변경하고, 원본 객체 속성이 변경되었는지를 체크하면 됩니다.
지금까지 자바 생성 패턴의 종류와 각각의 예시 코드를 살펴봤습니다. 그렇다면 실제 프로젝트에서 각각의 디자인 패턴을 언제, 어떻게 적용해야 할까요? 디자인 패턴 적용은 프로젝트 상황에 따라 다를 수 있지만, 일반적으로 각 디자인 패턴 적용을 검토하는 과정은 다음과 같습니다.
싱글턴 패턴은 특정 객체의 상태를 시스템 전역에 걸쳐 동일하게 유지해야 하거나, 고비용의 리소스를 효율적으로 관리해야 하는 경우에 적용을 검토해 볼 수 있습니다. 예를 들어, 데이터베이스 연결, 로거(Logger), 파일 시스템, 설정 관리 등과 같은 경우죠.
다만 싱글턴 패턴이 과도하게 사용될 경우 시스템의 유연성을 저해하고, 코드 테스트를 어렵게 만들 수 있습니다. 또한 다중 스레드 환경에서 동기화 문제가 발생할 수 있기 때문에 신중하게 적용을 검토해야 합니다.
다음으로 팩토리 패턴은 객체 생성 로직을 클라이언트 코드로부터 분리할 때 적용을 검토할 수 있습니다. 특히 생성해야 하는 객체의 타입이 실행 시점에 결정되거나, 다양한 종류의 객체를 유연하게 생성해야 하는 경우 팩토리 패턴이 유용한데요.
그러나 팩토리 패턴도 과도한 적용은 지양해야 합니다. 팩토리 패턴을 적용하면 팩토리 클래스를 따로 만들어야 하기 때문에, 클래스 수가 늘어나게 되고 코드가 복잡해질 수 있기 때문이죠. 더불어 팩토리 클래스가 구체적인 클래스에 과도하게 의존하게 되면, 팩토리 패턴의 주된 이점 중 하나인 객체 생성과 클라이언트 코드의 결합도 감소가 무색해질 수 있으니 주의해야 합니다.
빌더 패턴은 객체 생성 과정이 복잡하거나, 생성자에 전달해야 할 매개변수가 많을 때 적용을 검토할 수 있습니다. 다만 빌더 패턴 역시 빌더 클래스를 별도로 만들어야 하기 때문에, 무분별하게 적용하면 코드만 복잡해질 수 있습니다. 또한 빌더 패턴은 추가적인 객체 생성과 메모리 사용을 수반하므로, 메모리 사용에 민감한 시스템인 경우에는 적용을 주의해야 합니다.
프로토타입 패턴은 기존 인스턴스를 복제함으로써 비용과 시간을 절약하며, 런타임에 동적으로 객체의 상태를 조정할 수 있기 때문에 새로운 객체 생성 비용이 높은 상황에 적용하면 좋습니다. 하지만 프로토타입을 제대로 적용하기 위해서는 얕은 복사와 깊은 복사에 대한 이해가 필요하며, 자칫 잘못 적용하면 예상치 못한 부작용을 유발할 수 있습니다.
또한 프로토타입 패턴을 적용한 클래스를 만들려면 Cloneable 인터페이스를 반드시 구현해야 합니다. 따라서 클래스 설계에 제약이 발생할 수 있어, 프로그램 설계 시 이 부분도 미리 검토해야 합니다.
지금까지 자바 디자인 패턴 중 생성 패턴의 종류와 특징을 알아보고, 실제 프로젝트에서의 적용 방법을 정리해 봤습니다. 싱글턴, 팩토리, 빌더, 프로토타입 패턴과 같은 생성 패턴들은 객체의 생성 과정을 캡슐화하거나, 리소스를 효율적으로 사용하도록 도움을 줍니다. 하지만 앞서 살펴본 바와 같이 각 패턴들은 장단점이 명확하기 때문에 시스템 적용 시 올바른 이해가 필요합니다.
다음 편에서는 구조 패턴, 생성 패턴에 이어 자바 디자인 패턴 시리즈의 마지막 ‘행동 패턴(Behavioral Patterns)’에 대해 살펴보겠습니다.
요즘IT의 모든 콘텐츠는 저작권법의 보호를 받는 바, 무단 전재와 복사, 배포 등을 금합니다.