1. 싱글톤 패턴 Singleton Pattern
싱글톤이란?
- 자바에서는 객체는 항상 new를 통해 생성한다.
- 매번 new를 하여 새로운 인스턴스를 생성하는 것이 아니라, 시스템 내에 하나의 인스턴스를 갖는 것
-> 단 하나임을 보장 !
-> C, C++에서는 전역 변수로 사용하는데 자바에서는 그 개념이 아니라 (global 변수가 없음) 하나의 인스턴스를 갖고 공유하도록 하는 의미임
의도와 동기
- 왜 하나여야 할까? 여러개가 있으면 문제가 되는 것을 하나로 보장하는 것
- 여러 번 생성되어서 각각의 값을 가질 필요가 없는 것이다. > 예를 들어 JDBC Connection Pool -> 단 하나만 필요하다.
- 클래스의 인스턴스가 하나만 생성되고, 이후에는 해당 인스턴스를 공유하여 사용할 수 있도록 설계한다.
-> 이렇게 구현된 싱글톤 패턴은 여러 스레드에서 동시에 접근해도 항상 동일한 인스턴스를 반환하므로, 동기화에 대한 처리가 필요하지 않다.
-> 자원의 공유와 관련하여 많이 사용되며, 특히 리소스 풀, 캐시, 로그 객체 등에 적용되어 자원의 효율적인 관리를 가능하게 한다.
클래스 다이어그램
- 가장 일반적인 방법은 private 생성자를 갖고 있는 클래스 내부에 자기 자신의 인스턴스를 private static 변수로 선언하고, getInstance() 메서드를 통해 해당 인스턴스를 반환하는 방식이다.
객체 협력
- 클라이언트는 Singleton 클래스에 정의된 getInstance() 메서드를 통해서 유일하게 생성되는 Singleton 인스턴스에 접근할 수 있다.
- 이때 해당 메서드가 public이어야 한다.
예시
public class ConnectionPool {
private static ConnectionPool instance = new ConnectionPool();
private ConnectionPool(){}
public static ConnectionPool getInstance(){
if (instance==null){
instance = new ConnectionPool();
}
return instance;
}
}
import java.util.Calendar;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class SingletonMain {
@Test
public static void test(){
ConnectionPool instance1 = ConnectionPool.getInstance();
ConnectionPool instance2 = ConnectionPool.getInstance();
Assertions.assertEquals(instance1, instance2);
// JDK에서 이미 만들어놓은 싱글톤 예시
Calendar calendar = Calendar.getInstance();
}
public static void main(String[] args) {
test();
}
}
결론
- 유일하게 존재하는 인스턴스로의 접근을 통제할 수 있다.
- 전역 변수를 사용함으로써 발생할 수 있는 오류를 (C++ 의 경우) 줄일수 있다.
2. 프로토타입 패턴 Prototype Pattern
프로토타입이란?
- 하나 샘플을 만들어 놓고 복제해서 사용한다.
의도와 동기
- 자바에서는 생성자를 통해 객체를 생성하는데, 어떤 경우에는 생성 과정이 복잡할 수 있다.
- 예를 들어 여러 개의 모임으로 된 객체 -> 단계가 여러 개 일 수 있음 -> 매번 DB에서 가져와서 관련된 객체를 List로 만들어서 해당 List를 멤버 변수로 가져야 하는 경우 -> 번거롭고 비효율적
-> 견본을 만들어 놓고 쌍둥이를 만들어보자.
클래스 다이어그램
객체 협력
- 자바에서는 클론이라는 메서드가 존재한다. clone()을 통해 객체 복제를 할 수 있다. 복제하는데 필요한 인터페이스를 정의하고 해당 인터페이스의 구현체에서 clone() 메서드를 재정의하여 객체를 복제하는 로직을 구현한다.
예시
책이라는 객체를 생각해보자. 이 클래스를 가지고 있는 책장이 있다. 이때 ArrayList로 책을 갖고 있는 책장을 또 하나 만든다고 할 때, 책의 정보들을 전부 다시 가져와야 하는 비용이 발생한다. 메모리에 있는 객체라고 하면, 이를 클론해서 사용하면 된다.
인터페이스 cloneable은 강제 구현 메서드는 없기 때문에 mark 메서드라고 한다. 단순히 표시해주는 인터페이스.
Book 클래스 정의
import lombok.Data;
@Data
public class Book {
private String author;
private String title;
public Book(String author, String title) {
this.author = author;
this.title = title;
}
public String toString() {
return title + "," + author;
}
}
BookShelf 클래스 정의
import java.util.ArrayList;
import lombok.Data;
@Data
class BookShelf implements Cloneable {
private ArrayList<Book> shelf;
public BookShelf() {
shelf = new ArrayList<Book>();
}
public void addBook(Book book) {
shelf.add(book);
}
@Override
protected Object clone() throws CloneNotSupportedException {
// super.clone(); // <- 얕은 복사
BookShelf another = new BookShelf();
for (Book book : this.shelf) {
another.addBook(new Book(book.getAuthor(), book.getTitle()));
}
return another;
}
}
이때 clone() 메서드를 super.clone() 하는 것과 재정의하는 것이 차이가 있다.
상위 메서드의 clone을 해버리면 얕은복사로 복사된다. 즉 같은 객체의 주소값만이 복사되므로 동일한 객체를 참조한다.
다른 객체로 복사하고 싶다면 내부의 객체들을 새로운 객체를 만들어서 복사해주는 것이다.
위와 같이 원하는 방식으로 재정의를 해주면 된다.
import org.junit.jupiter.api.Assertions;
import lombok.Data;
@Data
public class PrototypeMain {
public static void test() throws CloneNotSupportedException {
BookShelf bookShelf = new BookShelf();
bookShelf.addBook(new Book("박완서", "나목"));
bookShelf.addBook(new Book("박경리", "토지"));
bookShelf.addBook(new Book("조정래", "태백산맥"));
BookShelf another = (BookShelf)bookShelf.clone();
System.out.println(bookShelf);
System.out.println(another);
Assertions.assertEquals(bookShelf.getShelf().get(0), another.getShelf().get(0));
bookShelf.getShelf().get(0).setAuthor("한강");
bookShelf.getShelf().get(0).setTitle("눈");
System.out.println(bookShelf);
System.out.println(another);
Assertions.assertNotEquals(bookShelf.getShelf().get(0), another.getShelf().get(0));
}
public static void main(String[] args) throws CloneNotSupportedException {
test();
}
}
결론
- 프로토타입 속성값을 활용하여 다양한 객체를 생성할 수 있음
- 서브클래스의 수를 줄일 수 있다.
- 자바에서는 clone() 메서드를 재정의하여 구현한다.
3. 추상 팩토리 패턴 Abstract Factory Pattern
추상 팩토리 패턴이란?
- 하나의 틀이 있는데 어떤 일에 대한 정의를 갖는다.
- 인스턴스를 만들어주는 공장이 있고, 그 인스턴스를 만들어낸다.
의도와 동기
- 일종의 Product들을 계속해서 만들어야 하는데, 구체적인 클래스를 생성하지않고도 서로 관련성이 있거나 독립적인 여러 객체의 군을 생성하기 위한 인터페이스
- 예를 들어 DB 관련된 여러 DAO가 있다고 할 때, DB 종류에 따른 DAO 인스턴스를 한꺼번에 생성되어야 한다.
- 그 set들이 동시에 replace 된다.
- 즉 Oracle Dao Factory를 생성하면 Oracle Dao가 생성되는 것
-> Factory만 바꿔주면 하위 객체들이 생성디도록 한다.
클래스 다이어그램
AbstractFactory를 의존하고 구체 Product는 추상 팩토리 구현체 팩토리가 생성한다.
클라이언트는 Oracle인지 Mysql인지 알 필요 없다.
객체 협력
- AbstractFactory
-> 개념적 제품에 대한 객체 생성하는 인터페이스 선언
- ConcreteFactory
-> 추상 팩토리의 구현체로서 구체적인 인스턴스 객체 생성
- AbstractProduct
-> 개념적 인스턴스 객체에 대한 인터페이스 선언
- ConcreteProduct
-> 구체적으로 팩토리가 생서알 객체 정의, AbstractProduct가 정의하는 인터페이스 구현
- Client
-> AbstractFactory와 AbstractProduct 클래스에 선언된 인터페이스 사용
예시
Client가 있고, 추상 Facotry와 추상 Product들이 존재한다. 또한 이에 대한 구현체들이 있다.
팩토리만 선택하면 유저가 의존하고 있는 Set이 바뀌므로 if문을 분기할 필요가 사라진다.
DB가 늘어날 때마다 그걸 일일히 관리하는 것이 아니라 Factory만 선택하면 알아서 돌아가도록 한다.
이런 기능을 수행해야 한다는 Dao를 다음과 같이 정의한다.
public interface UserInfoDao {
void insertProduct(UserInfo userInfo);
void updateProduct(UserInfo userInfo);
void deleteProduct(UserInfo userInfo);
}
public interface ProductDao {
void insertProduct(Product product);
void updateProduct(Product product);
void deleteProduct(Product product);
}
이에대한 구현체들은 다음과 같이 작성할 수 있다.
public class UserInfoMySqlDao implements UserInfoDao{
@Override
public void insertUserInfo(UserInfo userInfo) {
System.out.println("insert into MYSQL DB userId =" + userInfo.getUserId() );
}
@Override
public void updateUserInfo(UserInfo userInfo) {
System.out.println("update into MYSQL DB userId = " + userInfo.getUserId());
}
@Override
public void deleteUserInfo(UserInfo userInfo) {
System.out.println("delete from MYSQL DB userId = " + userInfo.getUserId());
}
}
public class ProductOracleDao implements ProductDao{
@Override
public void insertProduct(Product product) {
System.out.println("Oracle Dao Insert Operation");
}
@Override
public void updateProduct(Product product) {
System.out.println("Oracle Dao update Operation");
}
@Override
public void deleteProduct(Product product) {
System.out.println("Oracle Dao delete Operation");
}
}
팩토리라는 것을 만들어서 팩토리가 해당 DAO 세트를 갖고 있도록 하자.
public interface DaoFactory {
UserInfoDao createUserInfoDao();
ProductDao createProductDao();
}
이에 대한 구현체
public class OracleDaoFactory implements DaoFactory{
@Override
public UserInfoDao createUserInfoDao() {
return new UserInfoOracleDao();
}
@Override
public ProductDao createProductDao() {
return new ProductOracleDao();
}
}
그렇다면 이제 클라이언트가 이를 사용하는 상황을 보자. 즉 팩토리의 셋을 어떻게 선택해서 구체적으로 사용할 것인가?
설정 파일에서 읽어오는 케이스라고 해보면 다음과 같다.
if(dbType.equals("ORACLE")){
daoFactory = new OracleDaoFactory();
}
else if(dbType.endsWith("MYSQL")){
daoFactory = new MySqlDaoFactory();
userInfoDao = daoFactory.createUserInfoDao();
productDao = daoFactory.createProductDao();
팩토리 라는 인터페이스가 각각의 DAO를 생성하도록 정의하고, 구체 구현체들이 각각 자기 DB에 맞는 DAO를 반환한다.
결론
- 일반적으로 ConcreteFactory 클래스의 인스턴스는 실행 할 때 만들어진다. -> 실행 상황에 따라 필요한 Set를 생성.
- 구체적 팩토리는 어떤 특정 구현을 갖는 제품 객체를 생성한다. 서로 다른 제품 객체를 생성하기 위해서 사용자는 서로 다른 ConcretetFactory 를 사용한다.
- 서브클래스에 필요한 제품 객체를 생성하는 책임을 위임한다.
4. 빌더 패턴 Builder Pattern
빌더패턴이란?
- Gof에서의 빌더 패턴이 있고 이펙티브 자바의 빌더 패턴이 있다.
-> Gof에서는 여러가지 오퍼레인시 결합해서 하나의 product를 내는
-> 이펙티브자바에서는 생성자를 대체할 수 있는 방법
-> 스프링에서 처음 객체를 생성할 때 builder를 통해서 클래스를 생성하도록 하는 방법이 이펙티브 자바의 제안과 유사
- 생성에 대한 과정과 각 결과물을 표현하는 방법을 분리하여 동일한 생성과정에서 여러가지 셋업에 따른 셋업을 가진 생성을 하기 한다.
- 단계별 생성에 중점
의도와 동기
- 생성 과정과 구현 방법을 분리
-> 동일한 생성에서 여러 다른 표현이 나올 수 있다.
클래스 다이어그램
가운데 Director를 두고, builder를 갖고 있다. 실제적인 concrete builder가 어떤 구현을 할 것인지를 결정한다.
객체 협력
- Builder: Product의 각 요소들을 생성하는데 필요한 추상 메서드가 선언된 클래스나 인터페이스
- ConcreteBuilder: Builder에 선언된 메서드를 구현한 클래스
- Director: 여러 구체 클래스를 직접 쓰는 거이 아니라 builder를 이용해서 핸들링 하도록 Builder 인터페이스를 사용하여 Product를 생성
- Product: 결과물
예시
어떤 리포트를 만든다고 해보자. 결과물 자체는 Report일 텐데, 타입에 있어서 text로 할 수도 있고 html로 할 수 도 있다.
public interface MakeReport {
public void MakeHeader();
public void MakeBody();
public void MakeFooter();
public String getReport();
}
이펙티브 자바에서 제안하는 생성자 대체하는 Builder Pattern
어떤 객체의 매개 변수가 많을 경우 여러 오버로딩하는 생성자를 만들기보다는 Builder를 제공하여 유연하게 수정할 수 있는 구조를 제공한다.
Pizza를 만든다고 해보자. 다양한 토핑들이 있을 수 있다. 이를 내부적으로 Builder 클래스를 사용한다. -> 정적 이너 클래스로서 피자가 생성되지 않아도 호출될 수 있다.
다음과 같이 Pizza Abstract 클래스를 만든다.
public abstract class Pizza {
public enum Topping { HAM, MUSHROOM, ONION, PEPPER, SAUSAGE};
final Set<Topping> toppings;
abstract static class Builder {
EnumSet<Topping> toppings = EnumSet.noneOf(Topping.class);
public Builder addTopping(Topping topping) {
toppings.add(Objects.requireNonNull(topping));
return self();
}
public Builder sauceInside() {
return self();
}
abstract Pizza build();
protected abstract Builder self();
}
Pizza(Builder builder){
toppings = builder.toppings.clone();
}
public String toString() {
return toppings.toString();
}
}
이 피자의 구현체들을 다음과 같이 정의한다.
public class NyPizza extends Pizza{
public enum Size { SMALL, MEDIUM, LARGE};
private final Size size;
public static class Builder extends Pizza.Builder {
private final Size size;
public Builder(Size size) {
this.size = Objects.requireNonNull(size);
}
public NyPizza build() {
return new NyPizza(this);
}
protected Builder self() {return this;}
}
private NyPizza(Builder builder) {
super(builder);
size = builder.size;
}
}
- Pizza.Builder: 피자를 생성하기 위한 빌더 클래스. toppings 필드는 Topping enum 집합으로 초기화되며, addTopping() 메서드를 통해 토핑을 추가할 수 있다.
- sauceInside() 메서드는 체인 형태로 빌더 객체를 반환한다. build() 메서드는 Pizza 객체를 생성하기 위한 추상 메서드이며, self() 추상 메서드를 호출하여 자기 자신을 리턴한다.
- NyPizza 클래스: 뉴욕 스타일의 피자를 나타내는 클래스. Size 열거형과 size 필드를 갖는다. 여기서 포인트는 build() 메서드를 통해 NyPizza 객체를 생성한다는 것이다. NyPizza 생성자는 Builder 객체를 매개변수로 받아서 초기한다.
그렇다면 실제 사용 예시를 살펴보자.
import static org.designpattern.builder.NyPizza.Size.*;
import static org.designpattern.builder.Pizza.Topping.*;
public class BuilderMain {
public static void test(){
Pizza nyPizza = new NyPizza.Builder(SMALL).addTopping(SAUSAGE)
.addTopping(ONION).build();
Pizza calzone = new Calzone.Builder().addTopping(HAM).addTopping(PEPPER)
.sauceInside().build();
System.out.println(nyPizza);
System.out.println(calzone);
}
public static void main(String[] args) {
test();
}
}
결론
- 생성 과정과 구현 분리
- 제품의 다양한 구현이 가능
- 생산 과정 세분화
- 클라이언트는 세부 구체적인 사항을 알 필요 없음
참고자료
- 패스트 캠퍼스 (박은종의 객체지향 설계를 위한 디자인패턴 with 자바)
'Lecture' 카테고리의 다른 글
자바 디자인 패턴: 데코레이터 패턴, 콤포지트 패턴, 어댑터 패턴 (0) | 2023.07.05 |
---|---|
자바 디자인 패턴: 템플릿 메서드, 팩토리 메서드, 전략 패턴, 브릿지 패턴 (0) | 2023.07.05 |
디자인 패턴을 배워야 하는 이유, 객체지향 설계와 SOLID, 클래스 다이어그램 (0) | 2023.07.05 |
대규모 서비스에서 발생하는 데이터 처리, 백엔드 엔지니어링의 역할과 범위 (0) | 2023.07.04 |
데이터 모델과 트랙잭션 디자인 / 개별 프로세스 운영 관리 및 배포 시스템 디자인 (0) | 2023.07.03 |