Strategy vs Factory Design Patterns in Java

DongHee Lee
26 min readJun 7, 2020

--

해당 문서는 아래의 블로그에서 번역하여 작성한 내용입니다. PDF 변환 라이브러리를 설계하면서 전략패턴과 팩토리패턴에 대해 잘 이해하지 못했던 부분에 대해 파악하고자 작성하였습니다.

References

https://dzone.com/articles/strategy-vs-factory-design-pattern-in-java

Contents

  1. Strategy Design Pattern
  2. Factory Design Pattern
  3. Account-Management Example With the Strategy Pattern
  4. Account-Management Example With the Factory pattern
  5. Conclusion

개발자들의 대부분은 Strategy 디자인 패턴뿐만 아니라 Factory 디자인 패턴을 사용합니다. 그러나 개발자들은 두 디자인 패턴의 차이점에 대해 설명하기 어려워하거나, 프로젝트를 시작할 때 어떤 디자인 패턴을 선택해야할지 어려움을 겪습니다. 이번 포스팅은 각각의 키포인트와 예제를 살펴보면서 차이점을 알아보도록 하겠습니다.

Strategy Design Pattern

  • Strategy 디자인 패턴 (또한 Policy 디자인 패턴으로 알려진)은 런타임시에 우리에게 알고리즘을 선택할수 있도록 허용해주는 행위 패턴입니다.
  • Strategy 다자인 패턴은 문맥, Client 그리고 이것을 사용하는 코드들로부터 알고리즘을 독립적으로 만들어줍니다.
  • 예를 들어, 수신된 데이터에 대해 유효성 검사를 수행하는 유효성 검사 객체는 전략 패턴을 사용하여 형식, 데이터 소스 또는 다른 사용자 매개 변수에 따라 유효성 검사 알고리즘을 선택할 수 있습니다.
  • 이러한 유효성 알고리즘들은 유효성 검사를 수행하는 객체로 부터 분리되어 작성되며, 어떠한 코드 중복없이 쉽게 사용할 수 있습니다.
  • Strategy 디자인 패턴은 일부 알고리즘 또는 코드에 대한 Reference를 저장하고 필요할 때마다 제공합니다.
  • 즉, Strategy 디자인 패턴은 디자인 패턴 제품군에서 많이 정의된 알고리즘 중 하나이며 데이터에 적용되거나 사용될 수 있습니다.
  • 런타임에 사용할 알고리즘을 결정하는 기능을 통해 호출 또는 Client 코드를보다 유연하고 재사용 할 수 있으며 코드 중복을 피할 수 있습니다.
  • Client 또는 Context는 어떤 전략(Algorithm)을 사용할지는 모릅니다.

Factory Design Pattern

  • Factory 디자인 패턴은 생성 디자인 패턴으로, 객체를 만드는 가장 좋은 방법 중 하나입니다. 해당 패턴은 Factory 메서드를 사용하여 객체의 정확한 클래스를 정의하지 않고 객체 생성 문제를 처리합니다.
  • Factory 패턴에서 개발자들은 생성자를 호출하는 대신 Factory 메소드를 호출함으로써 객체를 생성합니다.
  • Factory 패턴은 자바에서 가장 많이 사용되는 디자인 패턴들 중 하나입니다.
  • 인터페이스로 구현되거나, 자식 클래스로 구현되어 집니다.
  • Factory 디자인 패턴에서, 개발자들은 Client에게 생성 로직을 노출없이 객체를 생성합니다.
  • 즉, Factory 디자인 패턴은 우리가 사용하는 클래스 계열로부터 적용되는 객체를 제공합니다. 이 객체는 다양한 기능뿐만 아니라 알고리즘을 나타냅니다.

아래의 예제에서 두 디자인 패턴의 차이점에 좀 더 강조하여 알려드리도록 하겠습니다. 예제는 회계관리 프로그램을 사용하여 설명드리겠습니다.

Account-Management Example With the Strategy Pattern

우리는 계좌에 저장된 원금에 대한 이자를 계산하기위한 다양한 전략을 가지고 있습니다.

첫 번째로, 우리는 전략(Algorithm)의 레이아웃을 정의하기 위한 인터페이스를 정의해야합니다. 이를위해 InterestCalculationStratey 인터페이스를 생성하도록 하겠습니다.

public interface InterestCalculationStrategy {     double calculateInterest(double principal, double rate, int term);
}

이제, 단순 이자 계산 알고리즘인 SimpleInterestCalculator에 비율과 특정 기간에 대한 단순이자를 계산하기 위해 두 가지 특징을 정의합니다.

public class SimpleInterestCalculator implements InterestCalculationStrategy {     @Override
public double calculateInterest(final double principal, final double rate, final int term) {
return ((principal * term * rate) / 100);
}
@Override
public String toString() {
return "Simple Interest";
}
}

그리고 주어진 비율과 기간에 대한 복리이자를 계산하기 위해 CompoundInterestCalculator를 사용합니다.

public class CompundInterestCalculator implements InterestCalculationStrategy {     @Override
public double calculateInterest(final double principal, final double rate, final int term) {
return (principal * Math.pow(1.0 + rate/100.0, term) - principal);
}
@Override
public String toString() {
return "Compound Interest";
}
}

우리는 Saving 또는 Current인 2개의 타입인 계좌를 가지고 있습니다. 이자율은 계좌타입에 따라 정의되어 집니다. 아래의 코드에 계좌 타입에따라 이자율을 고정시켜두었습니다.

public enum AccountType {     SAVING (2.0d),
CURRENT (1.0d);
private double rate;

AccountType (final double rate) {
this.rate = rate;
}
public double getRate() {
return rate;
}
}

그리고 우리는 Account의 상세내용을 저장하는 Model 객체를 생성합니다.

public class Account {
private long accountNo;
private String accountHolderName;
private AccountType accountType;
private InterestCalculationStrategy interestStrategy;
private double amount;
public Account() {
super();
}
public Account(long accountNo, String accountHolderName, AccountType accountType) {
this();
this.accountNo = accountNo;
this.accountHolderName = accountHolderName;
this.accountType = accountType;
}
public Account(long accountNo, String accountHolderName, AccountType accountType,InterestCalculationStrategy interestStrategy) {

this(accountNo, accountHolderName, accountType);
this.interestStrategy = interestStrategy;
}
public long getAccountNo() {
return accountNo;
}
public void setAccountNo(long accountNo) {
this.accountNo = accountNo;
}
public String getAccountHolderName() {
return accountHolderName;
}
public void setAccountHolderName(String accountHolderName) {
this.accountHolderName = accountHolderName;
}
public AccountType getAccountType() {
return accountType;
}
public void setAccountType(AccountType accountType) {
this.accountType = accountType;
}
public InterestCalculationStrategy getInterestStrategy() {
return interestStrategy;
}
public void setInterestStrategy(InterestCalculationStrategy interestStrategy) {
this.interestStrategy = interestStrategy;
}
public double getAmount() {
return amount;
}
public void setAmount(double amount) {
this.amount = amount;
}
public void deposit(double amount) {
// check for only positive/valid amount
if (amount > 0.0d) {
this.amount += amount;
}
}
public void withdraw(double amount) {
// check for only positive/valid amount and also for below than the available amount in account
if (amount > 0.0d && amount < this.amount) {
this.amount -= amount;
}
}
public double getInterest(int term) {
if (getInterestStrategy() != null && getAccountType() != null) {
return getInterestStrategy().calculateInterest(getAmount(), getAccountType().getRate(), term);
}
return 0.0d;
}
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Account [accountNo=").append(getAccountNo())
.append(", accountHolderName=").append(getAccountHolderName())
.append(", accountType=").append(getAccountType())
.append(", rate=").append((getAccountType() != null) ? getAccountType().getRate() : 0.0d)
.append(", interestStrategy=").append(getInterestStrategy())
.append(", amount=").append(getAmount()).append("]");
return builder.toString();
}
}

이제 아래의 코드를 통해 전략 패턴을 사용해보도록 하겠습니다.

public class Main {
public static void main(String[] args) {
Account acct1 = new Account(12345678l, "Vijay Kumar", AccountType.SAVING);
acct1.setInterestStrategy(new CompundInterestCalculator());
acct1.deposit(10000.0d);

System.out.print(acct1);
System.out.println(" has interest : " + acct1.getInterest(5));
Account acct2 = new Account(12345680l, "Jay Kumar", AccountType.SAVING);
acct2.setInterestStrategy(new SimpleInterestCalculator());
acct2.deposit(10000.0d);
System.out.print(acct2);
System.out.println(" has interest : " + acct2.getInterest(5));
}
}

여기서 두 개의 계좌는 Saving Type인것을 알 수 있으며, Compound또는 Simple 알고리즘을 우리가 선택하여 알고리즘을 다른 방식으로 계산합니다. 기본적으로 알고리즘들은 느슨하게 결합된 Context(Account)와 함께 자유롭게 사용가능합니다. 이것이 Strategy 패턴을 사용하는 장점입니다.

여기서 또한 StrategyFactory를 사용하여 Factory를 생성할 수 있습니다.

public class StrategyFactory {
public InterestCalculationStrategy createStrategy(String strategyType) {
InterestCalculationStrategy strategy = null;

if (strategyType != null) {
if ("COMPOUND".equalsIgnoreCase(strategyType)) {
strategy = new CompundInterestCalculator();
} else if ("SIMPLE".equalsIgnoreCase(strategyType)) {
strategy = new SimpleInterestCalculator();
} else {
System.err.println("Unknown/unsupported strategy-type");
}
}
return strategy;
}
}

이러한 경우 우리의 코드는 아래와 같이 바뀔 수 있습니다.

public class Main {
public static void main(String[] args) {
StrategyFactory factory = new StrategyFactory();
Account acct1 = new Account(12345678l, "Vijay Kumar", AccountType.SAVING);
acct1.setInterestStrategy(factory.createStrategy("COMPOUND"));
acct1.deposit(10000.0d);
System.out.print(acct1);
System.out.println(" has interest : " + acct1.getInterest(5));
Account acct2 = new Account(12345680l, "Jay Kumar", AccountType.SAVING);acct2.setInterestStrategy(factory.createStrategy("SIMPLE"));
acct2.deposit(10000.0d);
System.out.print(acct2);
System.out.println(" has interest : " + acct2.getInterest(5));
}
}

Account-Management Example Using the Factory Pattern

Strategy 패턴과 동일하게 SavingType를 생성합니다.

public enum AccountType {     SAVING (2.0d),
CURRENT (1.0d);
private double rate;

AccountType (final double rate) {
this.rate = rate;
}
public double getRate() {
return rate;
}
}

Account에 대한 추상 클래스를 구현하고 sub-class를 만들어 다양한 Account 객체를 생성합니다.

아래는 Account에 대한 코드입니다. 해당 클래스가 sub-class로 사용되기 위해 Abstract로 정의되어진 것을 주목하세요.

public abstract class Account {

private long accountNo;
private String accountHolderName;
private AccountType accountType;
private String interestStrategy;
private double amount;
public Account() {
super();
}
public Account(long accountNo, String accountHolderName, AccountType accountType) {
this();
this.accountNo = accountNo;
this.accountHolderName = accountHolderName;
this.accountType = accountType;
}
public long getAccountNo() {
return accountNo;
}
public void setAccountNo(long accountNo) {
this.accountNo = accountNo;
}
public String getAccountHolderName() {
return accountHolderName;
}
public void setAccountHolderName(String accountHolderName) {
this.accountHolderName = accountHolderName;
}
public AccountType getAccountType() {
return accountType;
}
public void setAccountType(AccountType accountType) {
this.accountType = accountType;
}
public String getInterestStrategy() {
return interestStrategy;
}
public void setInterestStrategy(String interestStrategy) {
this.interestStrategy = interestStrategy;
}
public double getAmount() {
return amount;
}
public void setAmount(double amount) {
this.amount = amount;
}
public void deposit(double amount) {
// check for only positive/valid amount
if (amount > 0.0d) {
this.amount += amount;
}
}
public void withdraw(double amount) {
// check for only positive/valid amount and also for below than the available amount in account
if (amount > 0.0d && amount < this.amount) {
this.amount -= amount;
}
}
// this need to be defined by the sub-classes by applying right algorithm
public abstract double getInterest(int term);
@Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("Account [accountNo=").append(getAccountNo())
.append(", accountHolderName=").append(getAccountHolderName())
.append(", accountType=").append(getAccountType())
.append(", rate=").append((getAccountType() != null) ? getAccountType().getRate() : 0.0d)
.append(", interestStrategy=").append(getInterestStrategy())
.append(", amount=").append(getAmount()).append("]");
return builder.toString();
}
}

이제, 우리는 계좌유형을 생성합니다. 복리이자 계산을 수행하는 SavingAccount를 생성합니다.

public class SavingAccount extends Account {
public SavingAccount(long accountNo, String accountHolderName) {
super(accountNo, accountHolderName, AccountType.SAVING);
setInterestStrategy("Compound Interest");
}
@Override
public double getInterest(int term) {
if (this.getAccountType() != null) {
return this.getAmount() * Math.pow(1.0 + this.getAccountType().getRate()/100.0, term) - this.getAmount();
}
return 0.0d;
}
}

다음으로, 단순이자 알고리즘을 수행하는 CurrentAccount를 생성합니다.

public class CurrentAccount extends Account {
public CurrentAccount(long accountNo, String accountHolderName) {
super(accountNo, accountHolderName, AccountType.CURRENT);
setInterestStrategy("Simple Interest");
}
@Override
public double getInterest(int term) {
if (this.getAccountType() != null) {
return ((this.getAmount() * term * this.getAccountType().getRate()) / 100);
}
return 0.0d;
}
}

기본적으로, 계좌의 유형은 사전에 정의된 요율(Strategy와 같이)뿐만 아니라 사전에 정의된 이율계산 알고리즘이 있으며, 이것은 상당히 연관되어 있습니다. 그러므로 Account Instance는 기능으로 정의될 것입니다.

public class AccountFactory {

public Account createAccount(long accountNo, String accountHolderName, String accountType) {
Account account = null;
AccountType type = AccountType.valueOf(accountType);
if (type != null) {
switch (type) {
case SAVING:
account = new SavingAccount(accountNo, accountHolderName);
break;
case CURRENT:
account = new CurrentAccount(accountNo, accountHolderName);
break;
default:
// if we create any new account-type but failed to define the class for Account
System.err.println("Unknown/unsupported account-type.");
}
} else {
System.err.println("Undefined account-type.");
}
return account;
}
}

이제 아래의 Factory 샘플 코드를 보도록 하겠습니다.

public class Main {

public static void main(String[] args) {
AccountFactory factory = new AccountFactory();
Account acct1 = factory.createAccount(12345678l, "Vijay Kumar", "SAVING");
acct1.deposit(10000.0d); System.out.print(acct1);
System.out.println(" has interest : " + acct1.getInterest(5));
Account acct2 = factory.createAccount(12345680l, "Jay Kumar", "CURRENT"); acct2.deposit(10000.0d); System.out.print(acct2);
System.out.println(" has interest : " + acct2.getInterest(5));
}
}

Conclusion

이번 PDF 변환 라이브러리를 구현하면서 전략패턴과 팩토리 패턴의 구성의 차이점에 대해 의문이 많이 들었습니다.

단순한 UML을 비교해서 보면 각 알고리즘을 캡슐화를 시킨 후 해당 계열 내부에서 교체가 가능하도록 구현한 모습을 볼 수 있습니다.

전략 패턴을 활용한 PDF변환 UML
팩토리 패턴을 활용한 PDF변환 UML

Dzone 블로그 내용을 정리하면서 다음의 문구가 가장 핵심적이지 않았나 생각이 듭니다.

Factory 디자인 패턴은 생성 디자인 패턴으로, 객체를 만드는 가장 좋은 방법 중 하나입니다. 해당 패턴은 Factory 메서드를 사용하여 객체의 정확한 클래스를 정의하지 않고 객체 생성 문제를 처리합니다.

전략패턴은 어떤 특정 알고리즘을 런타임시에 변경을 하고싶을 때 Context에 전략 사용을 위한 인터페이스 Reference를 저장하고 동작을 요청합니다.

중요한점은 Reference를 저장할 때 특정 전략에 대한 객체에 대해 new 를 통해 객체를 생성하고, context에 저장된 객체정보를 바탕으로 알고리즘을 수행합니다.

팩토리패턴은 객체를 생성하기 위한 인터페이스를 정의를 하는데, 어떤 클래스의 객체를 만들지는 Sub-Class에서 결정됩니다. 즉, DzPdfConvertStrategyFactory에서 클래스의 객체생성을 위임하기 때문에 부모 클래스에서 직접적으로 new 를 통해 객체를 생성하지 않아도 된다는 것입니다.

따라서 특정 행동을 수행하는 알고리즘이 추가가 되더라도 여러 클래스에서 행동 객체를 생성하는 불필요한 중복 코드를 줄일 수 있으며, Factory 클래스 하나만 사용하여 유연하게 코드 수정이 가능하게 됩니다.

그렇다면 PDF 변환 라이브러리를 설계를 하면서 과연 저는 어떤 패턴을 적용하여 구축했을까요? 정답은 전략패턴에 팩토리패턴을 적용하여 구현하였던것입니다.

DzConvertContext를 보면 getContext()가 구현되어있는 것을 볼 수 있습니다.

DzPdfConvertStrategy pdfConvertStrategy = null;if (fileType.equals("xls") || fileType.equals("xlsx")) {
pdfConvertStrategy = new DzExcelToPdfConverter(pdfConvertParameter);
} else if (fileType.equals("docx")) {
pdfConvertStrategy = new DzWordToPdfConverter(pdfConvertParameter);
} else if (fileType.equals("html") || fileType.equals("htm")){
pdfConvertStrategy = new DzHtmlToPdfConverter(pdfConvertParameter);
} else {
throw new RuntimeException(fileType + " can not be converted!");
}
return pdfConvertStrategy;

getContext 메서드 코드의 일부를 보면 확장자명에 따라 객체를 별도로 생성한 후 반환하는 모습을 볼 수 있습니다.

DzPdfConvertStrategy convertFactory = DzPdfConvertStrategyFactory.getStrategy(pdfConvertParameter);return convertFactory.execute();

부모 클래스에서 별도로 new를 통해 객체를 생성하지 않고 단순히 파라미터만 전송하여 전략을 찾은 뒤 객체를 반환하도록 되어있습니다. 반환받은 객체는 전략에 맞게 execute를 호출하여 알고리즘이 동작되었습니다.

즉, 저는 전략패턴인줄 알고 설계를하여 구현하였으나 나중에 확인하니 팩토리 패턴방식으로 구현되었다는 것을 알 수 있었습니다.

실제 프로젝트를 유연하게 구현하기 위해선 디자인 패턴은 정말 중요하다고 생각합니다. 새로운 로직이 추가가 되거나, 변화가 일어날 경우 유연하게 대처할 수 있어야 합니다.

추후에 유지보수를 진행하더라도 잘못된 방식으로 설계되어 있다면 문제가 발생되기 마련입니다. 프로젝트를 진행하기 전에 설계를 꼼꼼히 검토하고 이러한 문제점이 발생되지 않도록 다양한 디자인 패턴에 대해 숙지를 해야겠습니다.

--

--