@Builder로 객체를 생성하면서 빌더 패턴을 왜 쓰는지 궁금증이 들었고, 디자인 패턴 중 하나하는 것을 알게됬다.
필자는 오늘 디자인 패턴이 무엇이고, 어떤 종류가 있는지 알아보고자 한다.
코드 재사용을 쉽게 하고, 유지보수 비용도 줄이고, 개발자들끼리 소통도 더 잘하게 만들기 위함이다.
1970-80년대로 거슬러 올라가야 한다.
그때는 소프트웨어가 점점 복잡해지는데 개발자들이 각자 다른 방식으로 코딩하다 보니까 같은 문제를 해결하는데도 시간이랑 노력이 너무 많이 들었었다.
크리스토퍼 알렉산더라는 건축가가 1977년에 쓴 'A Pattern Language'라는 책에서 건물 지을 때 자주 마주치는 문제들의 해결책을 패턴화했는데, 이 아이디어를 프로그래밍에도 적용할 수 있겠다고 생각이 들었다고 한다.
1994년, GoF(Gang of Four)라고 불리는 네 명의 개발자들이 'Design Patterns'라는 책을 냈는데, 여기서 23가지 디자인 패턴을 정리했고 지금 우리가 수업시간에 배우는 그 패턴들이다.
[ 빌더, 팩토리 메소드, 추상 팩토리, 싱글톤, 프로토타입 ]
복잡한 객체를 한 단계씩 만든다. 마치 레고 조립하듯이!
서브웨이에서 샌드위치 주문할 때처럼 빵 → 치즈 → 야채 → 소스 이렇게 단계별로 선택하는 것과 비슷하다.
// 피자 주문 예시
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());
}
객체를 생성하는 작업을 서브 클래스에게 맡기는 패턴이다.
공장에서 물건 만들듯이 객체를 찍어낸다.
// 동물 만드는 공장 예시
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();
}
}
팩토리 메서드보다 좀 더 복잡하다.
연관된 여러 객체를 한번에 생성할 때 사용하는 패턴이다.
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 파서 생성
// 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();
하나의 객체만 만들어서 모든 곳에서 그걸 공유해서 사용하는 패턴이다.
프린터 관리자나 데이터베이스 연결 같이 딱 하나만 있어도 될 때 사용한다.
// 데이터베이스 연결 관리자 예시
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"); // 팩토리 메소드
기존 객체를 복사해서 새로운 객체를 만드는 패턴이다.
포토샵에서 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)
겉으로 보이는 것만 복사한다.
실생활 예시: 구글 드라이브에서 파일 '바로가기' 만들기
바로가기를 만들어도 실제 파일은 하나뿐이다.
원본 파일이 변경되면 바로가기로 볼 때도 변경된 내용이 보인다.
참조하는 객체까지 모두 새로 복사한다.
실생활 예시: 문서를 완전히 복사해서 '새 파일 만들기'
복사본은 원본과 완전히 독립적이다.
원본이 변경되어도 복사본은 영향 받지 않는다.
독립적인 객체가 필요하거나 데이터 안전성이 중요할 때는 깊은 복사를 사용한다.
싱글톤: 메모리 절약, 상태 공유가 필요한 객체 관리
팩토리: 객체 생성 로직 중앙화, 의존성 주입 관리
빌더: 복잡한 객체의 생성을 편리하게, 불변 객체 생성 용이
@Service // 스프링이 자동으로 싱글톤으로 관리
public class UserService {
@Autowired
private UserRepository userRepository; // 싱글톤으로 주입
public User findUser(Long id) {
return userRepository.findById(id);
}
}
// 스프링의 BeanFactory 사용
@Configuration
public class AppConfig {
@Bean
public UserService userService() {
return new UserService();
}
}
// 사용할 때
@Autowired
private UserService userService; // 스프링이 팩토리를 통해 객체 생성
@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();