소프트웨어 개발에서 객체 생성은 매우 중요한 부분입니다. 객체 생성 로직을 분리하고 유연하게 관리하기 위해 다양한 디자인 패턴이 사용됩니다. 그중 하나가 팩토리 메서드 패턴입니다.
이번 글에서는 팩토리 메서드 패턴의 개념과 필요성, 그리고 이를 자바로 구현하는 방법을 살펴보겠습니다.
팩토리 메서드 패턴이란 무엇인가
팩토리 메서드 패턴 (Factory Method Pattern)은 객체 생성 코드를 캡슐화하여, 객체 생성의 책임을 서브클래스에 위임하는 패턴입니다. 이를 통해 객체 생성 로직을 클라이언트 코드와 분리할 수 있으며, 객체 생성 방법을 변경하더라도 클라이언트 코드를 수정하지 않아도 됩니다.
팩토리 메서드 패턴의 구현 방법
팩토리 메서드 패턴은 커피를 주문하는 시스템을 구현하면서 진행하겠습니다. 먼저 팩토리 메서드를 만들기 전에 예제를 만들어 볼게요. 개발 환경은 다음과 같아요.
개발환경
- Spring Boot 3.3.1
- Java 21
- IntelliJ
🧑🏻💻 Github 링크
https://github.com/koonsland/design-patterns.git
팩토리 메서드 패턴 적용 전
가장 먼저 커피 클래스를 만들어 볼게요. 커피는 어떤 종류인지 type과 크기인 size를 가지고 있습니다.
package com.koonsland.designpatterns.creational.factorymethod.example;
public class Americano {
private final String type = "아메리카노";
private String size;
public String getType() {
return type;
}
public String getSize() {
return size;
}
public void setSize(String size) {
this.size = size;
}
@Override
public String toString() {
return "Coffee{" + "type='" + type + '\'' + ", size='" + size + '\'' + '}';
}
}
그리고 각각은 Getter와 Setter를 만들고 toString() 메서드를 만들어서 결과를 확인해 볼 수 있도록 했어요.
다음은 이 클래스를 이용해서 서비스 로직을 만들어 볼게요.
package com.koonsland.designpatterns.creational.factorymethod.example;
public class CoffeeService {
public static Americano orderCoffee(String size, String nickname) {
// validate
if (size == null || size.isBlank()) {
throw new IllegalArgumentException("사이즈를 선택해주세요");
}
if (nickname == null || nickname.isBlank()) {
throw new IllegalArgumentException("주문자 닉네임을 입력해주세요");
}
// notify before
notifyBefore(nickname);
// make
Americano americano = new Americano();
americano.setSize(size);
// notify after
notifyAfter(nickname);
return americano;
}
private static void notifyBefore(String nickname) {
System.out.println(nickname + " 님, 주문하신 커피가 제조중입니다");
}
private static void notifyAfter(String nickname) {
System.out.println(nickname + " 님, 주문하신 커피가 완성되었습니다");
}
}
CoffeeSerivce 클래스에서는 orderCoffee라는 static 메서드를 가지고 있습니다. 이 메서드는 커피의 종류, 크기, 그리고 주문자의 닉네임을 받아서 커피를 제조하고 알림(notify)을 주도록 했어요.
그럼 정상적으로 동작하는지 클라이언트 프로그램으로 실행해 볼게요.
package com.koonsland.designpatterns.creational.factorymethod.example;
public class Client {
public static void main(String[] args) {
Americano americano = CoffeeService.orderCoffee("GRANDE", "koonsland");
System.out.println(americano);
}
}
CoffeeService의 orderCoffee는 static 메서드이기 때문에 바로 호출이 가능합니다. 이때 parameter로 커피의 종류와 사이즈, 그리고 주문자의 닉네임을 넣어주었어요. 실행해 보겠습니다.
koonsland 님, 주문하신 커피가 제조중입니다
koonsland 님, 주문하신 커피가 완성되었습니다
Coffee{type='아메리카노', size='GRANDE'}
CoffeeService에서 커피를 만들기위해서 사이즈와 nickname을 넣어주었어요. 그래서 리턴 받은 Americano 인스턴스를 출력해 보았습니다. 정상적으로 아메리카노가 주문되었고 사이즈도 주문한 그대로 나온 것을 확인할 수 있습니다.
OCP (Open-Closed Principle)
객체지향 프로그래밍을 하면 반드시 보게되는 단어가 있습니다. 바로 OCP입니다. 보통은 "개방-폐쇄 원칙"이라 합니다. 풀어서 다시 이야기하면 "확장에서는 열려있고 변경에는 닫혀있있다"라는 의미입니다. 대시(-)로 연결되어 있기 때문에 확장이 가능하면서도 변경이 불가능하도록 만드는 게 좋다는 자바의 SOLID 5원칙 중 하나입니다.
🧑🏻💻 S.O.L.I.D 5원칙
자바의 객체 지향 프로그래밍은 보다 효과적으로 개발하고 수행하기 위해 만드러진 원칙입니다.
Single Responsibility Principle: 단일 책임 원칙
Open-Closed Principle: 개방-폐쇄 원칙
Liskov Sibstitution Principle: 리스코프 치환 원칙
Interface Segregation Principle: 인터페이스 분리 원칙
Dependency Inversion Principle: 의존성 역전 원칙
위 경우 Lette를 만들기 위해서는 Americano 클래스와 동일하게 만들면서 Latte인 클래스를 추가해 주어야 합니다. 이 경우 CoffeeService 클래스 내의 orderCoffee 메서드를 고쳐야 합니다. 또한 필요하다면 외부에서 넣어주는 값들도 수정이 필요할 수 있습니다. 이 경우 OCP 원칙에 어긋나기 때문에 이를 개선해서 개발하는 방법을 알아야 합니다.
팩토리 메서드 패턴 적용 후
이번에는 위 소스를 팩토리 메서드 패턴을 적용해서 다시 만들어 보도록 할게요.
Americano 클래스는 약간의 추상화를 위해서 Coffee 인터페이스를 만들고 상속받아서 사용할게요.
package com.koonsland.designpatterns.creational.factorymethod.after;
public class Coffee {
private String type;
private String size;
protected Coffee() {}
protected Coffee(String type, String size) {
this.type = type;
this.size = size;
}
public String getType() {
return type;
}
public void setType(String type) {
this.type = type;
}
public String getSize() {
return size;
}
public void setSize(String size) {
this.size = size;
}
@Override
public String toString() {
return "Coffee{" + "type='" + type + '\'' + ", size='" + size + '\'' + '}';
}
}
이렇게 Coffee 클래스를 만들고 이를 상속받아서 Americano, Lette 클래스를 만들어 보도록 하겠습니다.
package com.koonsland.designpatterns.creational.factorymethod.after;
public class Americano extends Coffee{
public Americano(String size) {
super("아메리카노", size);
}
}
package com.koonsland.designpatterns.creational.factorymethod.after;
public class Latte extends Coffee{
public Latte(String size) {
super("카페라떼", size);
}
}
다음은 기존 CoffeeService 클래스를 팩토리 메서드 패턴을 적용한다는 의미로 CoffeeFactory로 수정하고 interface로 변경할게요.
package com.koonsland.designpatterns.creational.factorymethod.after;
public interface CoffeeFactory {
default Coffee orderCoffee(String type, String size, String nickname) {
// validate
validate(type, size, nickname);
// notify before
notifyBefore(nickname);
// make
Coffee coffee = createCoffee(type, size);
// notify after
notifyAfter(nickname);
return coffee;
}
Coffee createCoffee(String type, String size);
private static void validate(String type, String size, String nickname) {
if (type == null || type.isBlank()) {
throw new IllegalArgumentException("커피 종류를 선택해주세요");
}
if (size == null || size.isBlank()) {
throw new IllegalArgumentException("사이즈를 선택해주세요");
}
if (nickname == null || nickname.isBlank()) {
throw new IllegalArgumentException("주문자 닉네임을 입력해주세요");
}
}
private static void notifyBefore(String nickname) {
System.out.println(nickname + " 님, 주문하신 커피가 제조중입니다");
}
private static void notifyAfter(String nickname) {
System.out.println(nickname + " 님, 주문하신 커피가 완성되었습니다");
}
}
Java8 버전 이상에서는 default 메서드가 가능합니다. 인터페이스 내에서 구현체가 가능하며 Java9 버전 이상에서는 private static 메서드를 사용할 수 있습니다. 이를 이용해서 validation 체크와 기본 구현체를 준비했습니다.
그리고 Coffee 인스턴스는 createCoffee 메서드를 이용하되 이를 상속받아서 구현하도록 메서드만 준비하도록 했어요. 그럼 지금 만든 CoffeeFactory를 상속받아서 CoffeeService 클래스를 만들어서 구현체를 넣을게요.
package com.koonsland.designpatterns.creational.factorymethod.after;
public class CoffeeService implements CoffeeFactory{
@Override
public Coffee createCoffee(String type, String size) {
if (type.equalsIgnoreCase("아메리카노")) {
return new Americano(size);
}
if (type.equalsIgnoreCase("카페라떼")) {
return new Latte(size);
}
throw new IllegalArgumentException("판매할 수 없는 커피 종류입니다.");
}
}
구현체에서는 createCoffee 메서드를 Override합니다. 이때, type은 클라이언트로부터 받은 값이 어떤 값인지 확인하고, 확인이 끝나면 각각 알맞은 커피를 만들어서 return 하도록 합니다.
그럼 최종 클라이언트 코드를 볼게요.
package com.koonsland.designpatterns.creational.factorymethod.after;
public class Client {
public static void main(String[] args) {
CoffeeService coffeeService = new CoffeeService();
Coffee americano = coffeeService.orderCoffee("아메리카노", "REGULAR", "koonsland");
System.out.println(americano);
Coffee latte = coffeeService.orderCoffee("카페라떼", "LARGE", "koonsland");
System.out.println(latte);
}
}
CoffeeService 인스턴스를 만들어주고 orderCoffee를 이용해서 아메리카노와 카페라떼를 주문합니다. CoffeeService는 CoffeeFactory의 default 메서드는 orderCoffee를 그대로 사용하며 구현체인 createCoffee에 의해서 커피의 종류를 확인하고 그에 맞는 커피를 return 합니다.
이제는 추가로 새로운 커피를 만들기 위해 기존의 코드는 수정하지 않고 coffeeService의 factory를 추가하고 Coffee 클래스를 상속받아서 새로운 커피 클래스를 만들어 주면 됩니다.
팩토리 메서드 패턴의 장점
팩토리 메서드 패턴의 장점은 다음과 같습니다.
- 객체 생성 로직의 캡슐화: 객체 생성 로직을 팩토리 메서드로 캡슐화하여, 코드의 가독성과 유지보수성을 높입니다.
- 유연한 확장성: 새로운 커피 종류를 추가할 때 CoffeeFactory의 코드를 수정하지 않아도 됩니다.
- 단일 책임 원칙: 객체 생성의 책임을 팩토리 클래스에 위임하여, 클라이언트는 커피 주문 로직에만 집중할 수 있도록 합니다.
결론
팩토리 메서드 패턴은 객체 생성 로직을 캡슐화하여 코드의 유연성과 확장성을 높이는 데 유용한 디자인 패턴입니다. 커피 주문 시스템 예제를 통해 팩토리 메서드 패턴을 이해하고, 이를 이용해서 스프링 부트 코드에 적용할 수도 있습니다. 앞으로 다양한 패턴을 학습하면서 실제 애플리케이션에 어떻게 적용할 수 있을지 알아보겠습니다.
글을 보시고 궁금하신 부분은 댓글을 이용해서 남겨주시면 말씀드릴게요. 읽어주셔서 감사드립니다.
'쿤즈 Dev > Design pattern' 카테고리의 다른 글
[Design Pattern] 빌더 패턴: 복잡한 객체 생성의 효율적인 관리 (0) | 2024.07.26 |
---|---|
[Design Pattern] 추상 팩토리 패턴: 복잡한 객체 생성의 유연한 관리 (0) | 2024.07.19 |
[Design Pattern] 싱글톤 패턴: 자바에서의 효율적인 인스턴스 관리 (2) | 2024.07.05 |
[Design Pattern] 디자인 패턴: 왜 알아야 할까요? (0) | 2024.06.28 |
[Design Pattern] Singleton(1) : 싱글톤! 하나의 인스턴스로 관리 (0) | 2021.11.19 |
댓글