객체 생성을 위한 디자인 패턴 가이드에 대해 알아봄

알쓸코딩·2024년 12월 3일
0

트러블 슈팅

목록 보기
13/13
post-thumbnail

@Builder로 객체를 생성하면서 빌더 패턴을 왜 쓰는지 궁금증이 들었고, 디자인 패턴 중 하나하는 것을 알게됬다.
필자는 오늘 디자인 패턴이 무엇이고, 어떤 종류가 있는지 알아보고자 한다.

✍️ 디자인 패턴이 생긴 이유?

코드 재사용을 쉽게 하고, 유지보수 비용도 줄이고, 개발자들끼리 소통도 더 잘하게 만들기 위함이다.

1970-80년대로 거슬러 올라가야 한다.
그때는 소프트웨어가 점점 복잡해지는데 개발자들이 각자 다른 방식으로 코딩하다 보니까 같은 문제를 해결하는데도 시간이랑 노력이 너무 많이 들었었다.

크리스토퍼 알렉산더라는 건축가가 1977년에 쓴 'A Pattern Language'라는 책에서 건물 지을 때 자주 마주치는 문제들의 해결책을 패턴화했는데, 이 아이디어를 프로그래밍에도 적용할 수 있겠다고 생각이 들었다고 한다.

1994, GoF(Gang of Four)라고 불리는 네 명의 개발자들이 'Design Patterns'라는 책을 냈는데, 여기서 23가지 디자인 패턴을 정리했고 지금 우리가 수업시간에 배우는 그 패턴들이다.

객체 생성과 관련된 주요 디자인 패턴

[ 빌더, 팩토리 메소드, 추상 팩토리, 싱글톤, 프로토타입 ]

2.1. 빌더 패턴

복잡한 객체를 한 단계씩 만든다. 마치 레고 조립하듯이!
서브웨이에서 샌드위치 주문할 때처럼 빵 → 치즈 → 야채 → 소스 이렇게 단계별로 선택하는 것과 비슷하다.

// 피자 주문 예시
Pizza pizza = new Pizza.Builder()
    .addDough("씬크러스트")    // 도우 선택
    .addSauce("토마토")       // 소스 선택
    .addTopping("치즈")      // 토핑 선택
    .addTopping("페퍼로니")   // 토핑 추가
    .build();               // 피자 완성

// 실제 자바에서는 StringBuilder로 많이 쓴다.
StringBuilder sb = new StringBuilder()
    .append("Hello")
    .append(" ")
    .append("World");

2.2.1.빌더 패턴을 고려해야할 경우

  • 불변성을 보장하고 싶을때
    - setter 사용 시 언제든 값이 변경될 수 있는데 이를 방지

  • 값을 검증하면서 객체를 생성하고 싶을 때
    - 회원가입할 때 이메일, 나이, 비밀번호를 양식에 맞을 경우 객체를 생성할 수 있게 해줌

public class User {
    private String email;
    private int age;
    private String password;

    public static class Builder {
        private String email;
        private int age;
        private String password;

        public Builder email(String email) {
            if (!email.contains("@")) {
                throw new IllegalArgumentException("이메일 형식이 올바르지 않습니다");
            }
            this.email = email;
            return this;
        }

        public Builder age(int age) {
            if (age < 0) {
                throw new IllegalArgumentException("나이는 0보다 작을 수 없습니다");
            }
            this.age = age;
            return this;
        }

        public Builder password(String password) {
            if (password.length() < 8) {
                throw new IllegalArgumentException("비밀번호는 8자 이상이어야 합니다");
            }
            this.password = password;
            return this;
        }

        public User build() {
            return new User(this);
        }
    }
}

// 이제 이렇게 잘못된 값을 넣으면 에러가 발생해
try {
    User user = new User.Builder()
        .email("잘못된이메일")  // 에러: 이메일 형식이 올바르지 않음
        .age(-20)             // 에러: 나이는 0보다 작을 수 없음
        .password("123")      // 에러: 비밀번호는 8자 이상
        .build();
} catch (IllegalArgumentException e) {
    System.out.println("유효하지 않은 값: " + e.getMessage());
}

2.2. 팩토리 메서드 패턴

객체를 생성하는 작업을 서브 클래스에게 맡기는 패턴이다.
공장에서 물건 만들듯이 객체를 찍어낸다.

// 동물 만드는 공장 예시
public interface Animal {
    void makeSound();
}

public class Dog implements Animal {
    @Override
    public void makeSound() {
        System.out.println("멍멍!");
    }
}

public class Cat implements Animal {
    @Override
    public void makeSound() {
        System.out.println("야옹!");
    }
}

public class AnimalFactory {
    public Animal createAnimal(String type) {
        if ("dog".equals(type)) {
            return new Dog();
        } else if ("cat".equals(type)) {
            return new Cat();
        }
        return null;
    }
}

// 사용예시
AnimalFactory factory = new AnimalFactory();
Animal dog = factory.createAnimal("dog");  // Dog 객체 생성
Animal cat = factory.createAnimal("cat");  // Cat 객체 생성

팩토리 메소드 패턴을 고려해야할 경우

  • 비슷한 객체를 자주 생성해야 할 때
// 1. DB 연결을 위한 인터페이스
public interface DBConnection {
    void connect();
    void executeQuery(String query);
    void close();
}

// 2. 각 데이터베이스별 구체적인 구현
public class MySQLConnection implements DBConnection {
    @Override
    public void connect() {
        System.out.println("MySQL DB 연결");
    }

    @Override
    public void executeQuery(String query) {
        System.out.println("MySQL에서 쿼리 실행: " + query);
    }

    @Override
    public void close() {
        System.out.println("MySQL 연결 종료");
    }
}

public class OracleConnection implements DBConnection {
    @Override
    public void connect() {
        System.out.println("Oracle DB 연결");
    }

    @Override
    public void executeQuery(String query) {
        System.out.println("Oracle에서 쿼리 실행: " + query);
    }

    @Override
    public void close() {
        System.out.println("Oracle 연결 종료");
    }
}

// 3. 데이터베이스 연결을 생성하는 팩토리
public class DatabaseFactory {
    public DBConnection createConnection(String type) {
        // DB 종류에 따라 다른 연결 객체 반환
        if ("mysql".equals(type)) {
            return new MySQLConnection();
        } else if ("oracle".equals(type)) {
            return new OracleConnection();
        }
        throw new IllegalArgumentException("지원하지 않는 DB 타입");
    }
}

// 4. 실제 사용 예시
public class Main {
    public static void main(String[] args) {
        DatabaseFactory factory = new DatabaseFactory();
        
        // MySQL 사용
        DBConnection mysql = factory.createConnection("mysql");
        mysql.connect();
        mysql.executeQuery("SELECT * FROM users");
        mysql.close();
        
        // Oracle 사용
        DBConnection oracle = factory.createConnection("oracle");
        oracle.connect();
        oracle.executeQuery("SELECT * FROM employees");
        oracle.close();
    }
}

2.3. 추상 팩토리 패턴

팩토리 메서드보다 좀 더 복잡하다.
연관된 여러 객체를 한번에 생성할 때 사용하는 패턴이다.
UI 테마(다크/라이트)에 따라 모든 버튼, 체크박스 등이 한번에 바뀌어야 할 때 쓰기 좋다.

// GUI 테마 예시
public interface GUIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

// 다크모드 팩토리
public class DarkThemeFactory implements GUIFactory {
    public Button createButton() { return new DarkButton(); }
    public Checkbox createCheckbox() { return new DarkCheckbox(); }
}

// 실제 자바애서
DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance();
DocumentBuilder builder = factory.newDocumentBuilder();  // XML 파서 생성

추상 팩토리 패턴을 고려해야 할 경우

  • UI 테마처럼 여러 컴포넌트가 같은 스타일을 가져야 할 때
  • 새로운 테마는 추가될 수 있지만, UI 컴포넌트의 종류(버튼, 체크박스)는 고정적일 때
// 1. 먼저 만들 제품들의 인터페이스를 정의
public interface Button {
    void render();
}

public interface Checkbox {
    void render();
}

// 2. 각 테마별로 구체적인 제품 클래스를 만듦
public class LightButton implements Button {
    @Override
    public void render() {
        System.out.println("밝은 버튼을 그립니다");
    }
}

public class DarkButton implements Button {
    @Override
    public void render() {
        System.out.println("어두운 버튼을 그립니다");
    }
}

public class LightCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("밝은 체크박스를 그립니다");
    }
}

public class DarkCheckbox implements Checkbox {
    @Override
    public void render() {
        System.out.println("어두운 체크박스를 그립니다");
    }
}

// 3. 추상 팩토리 인터페이스 정의
public interface UIFactory {
    Button createButton();
    Checkbox createCheckbox();
}

// 4. 구체적인 팩토리 클래스 구현
public class LightThemeFactory implements UIFactory {
    @Override
    public Button createButton() {
        return new LightButton();
    }
    
    @Override
    public Checkbox createCheckbox() {
        return new LightCheckbox();
    }
}

public class DarkThemeFactory implements UIFactory {
    @Override
    public Button createButton() {
        return new DarkButton();
    }
    
    @Override
    public Checkbox createCheckbox() {
        return new DarkCheckbox();
    }
}

// 5. 사용 예시
public class Application {
    private UIFactory factory;
    private Button button;
    private Checkbox checkbox;
    
    // 테마에 따라 팩토리를 선택
    public Application(UIFactory factory) {
        this.factory = factory;
    }
    
    // UI 컴포넌트 생성
    public void createUI() {
        button = factory.createButton();
        checkbox = factory.createCheckbox();
    }
    
    // UI 그리기
    public void render() {
        button.render();
        checkbox.render();
    }
}

// 실제 사용
Application app = new Application(new DarkThemeFactory());
app.createUI();
app.render();

2.4. 싱글톤 패턴

하나의 객체만 만들어서 모든 곳에서 그걸 공유해서 사용하는 패턴이다.
프린터 관리자나 데이터베이스 연결 같이 딱 하나만 있어도 될 때 사용한다.

// 데이터베이스 연결 관리자 예시
public class DatabaseConnection {
    private static DatabaseConnection instance;
    
    private DatabaseConnection() {}  // 생성자를 private으로
    
    public static DatabaseConnection getInstance() {
        if (instance == null) {
            instance = new DatabaseConnection();
        }
        return instance;
    }
}

// 실제 자바에서는
Runtime runtime = Runtime.getRuntime();  // JVM 런타임 객체
Logger logger = Logger.getLogger("MyLog");  // 로거 객체

싱글톤 패턴을 고려해야 할 경우

싱글톤 패턴은 남용하면 안된다.
정말 전체 애플리케이션에서 하나의 인스턴스만 필요한 경우에만 사용해야 함!

아까 전에 나온, 팩토리 메소드에서 DB 연결과 비교해보면,

팩토리 메소드 :"어떤 객체를 생성할지" 결정할 때 사용
싱글톤: "객체를 하나만 생성하고 공유"할 때 사용

실제로는 두 패턴을 같이 쓰기도 한다.

public class DatabaseManager {
    // 싱글톤으로 매니저 관리
    private static DatabaseManager instance;
    
    // 팩토리 메소드로 다양한 연결 생성
    public Connection createConnection(String type) {
        if ("mysql".equals(type)) {
            return new MySQLConnection();
        } else if ("oracle".equals(type)) {
            return new OracleConnection();
        }
        return null;
    }
    
    public static DatabaseManager getInstance() {
        if (instance == null) {
            instance = new DatabaseManager();
        }
        return instance;
    }
}

// 사용 예시
DatabaseManager manager = DatabaseManager.getInstance(); // 싱글톤
Connection mysql = manager.createConnection("mysql");   // 팩토리 메소드
Connection oracle = manager.createConnection("oracle"); // 팩토리 메소드

2.5. 프로토타입 패턴

기존 객체를 복사해서 새로운 객체를 만드는 패턴이다.
포토샵에서 Ctrl+C, Ctrl+V 하는 것과 비슷하다.

 // 1. 기본적인 프로토타입 패턴
public class Sheep implements Cloneable {
    private String name;
    private int age;
    
    public Sheep(String name, int age) {
        this.name = name;
        this.age = age;
    }
    
    @Override
    public Sheep clone() {
        try {
            return (Sheep) super.clone();
        } catch (CloneNotSupportedException e) {
            return null;
        }
    }
}

// 사용 예시
Sheep original = new Sheep("돌리", 1);
Sheep cloned = original.clone();

프로토타입 패턴을 고려해야 할 경우

  • 객체 생성 비용이 큰 경우
  • 비슷한 객체를 자주 생성해야 할 경우

여기서, 깊은 복사와 얕은 복사에 대한 개념이 나오는데, 살짝쿵만 알아보자.

얕은 복사(Shallow Copy)와 깊은 복사(Deep Copy)

얕은 복사

겉으로 보이는 것만 복사한다.
실생활 예시: 구글 드라이브에서 파일 '바로가기' 만들기

바로가기를 만들어도 실제 파일은 하나뿐이다.
원본 파일이 변경되면 바로가기로 볼 때도 변경된 내용이 보인다.

깊은 복사

참조하는 객체까지 모두 새로 복사한다.
실생활 예시: 문서를 완전히 복사해서 '새 파일 만들기'

복사본은 원본과 완전히 독립적이다.
원본이 변경되어도 복사본은 영향 받지 않는다.
  
독립적인 객체가 필요하거나 데이터 안전성이 중요할 때는 깊은 복사를 사용한다.  

✍️ 스프링 프레임워크에서 선호하는 객체 생성 패턴

싱글톤: 메모리 절약, 상태 공유가 필요한 객체 관리
팩토리: 객체 생성 로직 중앙화, 의존성 주입 관리
빌더: 복잡한 객체의 생성을 편리하게, 불변 객체 생성 용이

싱글톤 패턴 (@Component, @Service, @Controller)

@Service  // 스프링이 자동으로 싱글톤으로 관리
public class UserService {
    @Autowired
    private UserRepository userRepository;  // 싱글톤으로 주입
    
    public User findUser(Long id) {
        return userRepository.findById(id);
    }
}

팩토리 패턴 (BeanFactory 사용)

// 스프링의 BeanFactory 사용
@Configuration
public class AppConfig {
    @Bean
    public UserService userService() {
        return new UserService();
    }
}

// 사용할 때
@Autowired
private UserService userService;  // 스프링이 팩토리를 통해 객체 생성 

빌더 패턴 (@Builder)

@Entity
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class User {
    private Long id;
    private String name;
    private String email;
    private String password;
    private String address;
}

// 빌더 패턴으로 객체 생성
User user = User.builder()
    .name("김철수")
    .email("kim@email.com")
    .password("1234")
    .address("서울시")
    .build(); 
profile
알면 쓸데있는 코딩 모음!

0개의 댓글