[Onboarding] : Design Pattern

문승현·2022년 7월 29일
0

BeDev_2

목록 보기
5/8
post-thumbnail

소프트웨어 엔지니어링에서 자주 등장하는 문제들이 있다.
그런 문제 해결을 위한 설계와 기술적 구현 방침을 디자인 패턴이라 한다.
디자인 패턴은 말 그대로 설계 패턴을 의미한다.
따라서 구체적인 구현은 결정되어 있지 않으며, 개발자의 재량에 달려있다.

디자인 패턴은 그 종류가 다양하나, 일부 유명한 패턴들의 경우 카테고리화 할 수 있다.
디자인 패턴 카테고리 중 가장 유명한 3가지는 아래와 같다.

  1. 생성 패턴(Creational Patterns)
    특정 클래스의 객체를 생성하는데 사용되는 패턴

  2. 구조 패턴(Structural Patterns)
    여러 클래스 혹은 객체들이 어떻게 결합하여 하나의 큰 구조를 만드는지에 대한 패턴

  3. 행위 패턴(Behaviroal Patterns)
    여러 객체들 간에 어떻게 상호작용하고 통신하는지에 포커스를 맞춘 패턴

아래는 여러 디자인 패턴 중 온보딩 과정에서 내가 접한 것을 정리한 내용이다.

Singleton 패턴

Singleton 패턴은 상기 카테고리 중 생성 패턴에 속한다.
클래스가 하나의 인스턴스만을 갖도록 하고,
그 인스턴스를 시스템 전역에서 액세스 할 수 있도록 하는 패턴이다.

일반적으로 클래스가 하나의 인스턴스를 갖도록 하기 위해서 생성자를 private로 만든다.
그리고 클래스 안에서 해당 생성자를 호출해 하나의 인스턴스만 생성하는 방법을 사용한다.

public sealed class Singleton
{
	public static readonly Singleton Instance = new Singleton();
    
    // 다른 외부 클래스가 사용할 수 없도록 pirvate화
    private Singleton()
    {
    }
    
    public void Method()
    {
    	Console.WriteLine("Singleton Pattern");
    }
}

위와 같이 코드를 작성할 경우, 처음으로 클래스를 사용할 때 하나의 인스턴스만 생성된다.
이후, 인스턴스를 만드는 것은 생성자가 private으로 되어 있기 때문에 불가하다.

Builder 패턴

Builder 패턴은 생성 패턴에 속하며, 복잡한 객체를 생성할 때 사용하는 디자인 패턴이다.
하나의 클래스 내에서 직접 부품(기능)들을 조합해 객체를 만들어 내는 방식은 유연하지 못하다.
해당 클래스를 이용해 복잡한 객체를 표현할 수 있지만,
그 클래스를 변경하지 않으면서 다른 조합의 복잡한 객체를 만드는 것은 어렵기 때문이다.

이와 달리 Builder 패턴은 객체를 구성하는 부품(기능)들을 Builder에 정의하고,
외부 클래스에서 객체를 생성할 때 이들 부품들을 조합해 만들도록 하는 방식이다.
이러한 방식을 사용하면 Builder에 정의된 동일한 부품 생성 과정을 사용하면서
그 조합을 어떻게 하는가에 따라 다양한 객체들을 표현할 수 있다.

Builder 패턴을 구현하기 위해서는 일반적으로,
Builder, ConcreteBuilder, Product와 Director 클래스를 정의하여 사용한다.
Builder, ConcreteBuilder 클래스는 부품(기능)을 생성하는 기능을 제공하고,
Director 클래스는 이들을 선택적으로 사용하여 객체를 만들어내는 일을 수행한다.

아래는 Builder 패턴의 예로서, 침대를 만드는 IBedBuilder 인터페이스를 정의하고,
해당 인터페이스의 구현체로 AceBed라는 클래스를 작성하였다.
Director 클래스는 IBedBuilder 메소드 중 세개만 선택하여 침대를 만들도록 지시하고 있다.
AceBed가 아닌 다른 클래스를 만들고 싶다면 IBedBuilder의 메소드를 조합해 만들 수 있다.

// Builder 인터페이스
public interface IBedBuilder
{
	void MakeFrame();
    void MakeMattress();
    void MakeSheet(string sheet);
    void MakePillow(int size);
    Bed Build();
}


// Concrete Builder 클래스
public class AceBed : IBedBuilder
{
	private Bed _bed = new Bed();
    private int pillowSize = 0;
    private string sheetName;
    
    public void MakeFrame()
    {
    	_bed.Frame = (Date.Time.Now.Month > 5 && DateTime.Now.Month < 9)
        	? "AceBed No1 Frame"
            : "AceBed No2 Frame"
    }
    
    public void MakeMattress()
    {
    	_bed.Mattress = "AceBed Mattress";
    }
    
    public void MakePillow(int size)
    {
    	pillowSize = size;
    }
    
    public void MakeSheet(string sheet)
    {
    	sheetName = sheet;
    }
    
    public Bed Build()
    {
    	_bed.Pillow = "Pillow Size " + pillowSize;
        _bed.Sheet = "Sheet " + sheetName;
        return _bed;
    }
}


// Product 클래스
pubilc class Bed
{
	public string Frame { get; set; }
    public string Mattress { get; set; }
    public string Pillow { get; set; }
    public string Sheet { get; set; }
    public override string ToString()
    {
    	return string.Format($"{Frame} {Mattress} {Pillow} {Sheet}");
    }
}


// Director 클래스
class Director
{
	public Bed Construct(IBedBuilder builder)
    {
    	builder.MakeFrame();
        builder.MakeMattress();
        builder.MakeSheet("White");
        
        Bed bed = builder.Build();
        return bed;
    }
}


// Client 클래스
class Client
{
	void HowToTest()
    {
    	IBedBuilder builder = new AceBed();
        var director = new Director();
        var bed = director.Construct(builder);
        Console.WriteLine(bed.ToString());
    }
}

Builder 패턴을 변형하여 Fluent Builder라는 것을 만들 수 있다.
아래와 같이 Builder 클래스의 메소드가 IBedBuilder를 반환하도록 하여,
Builder 객체가 메소드들을 계속 연달아 호출할 수 있도록 한 것이다.

// Builder 인터페이스
public interface IBedBuilder
{
	IBedBuilder MakeFrame();
    IBedBuilder MakeMattress();
    IBedBuilder MakeSheet(string sheet);
    IBedBuilder MakePillow(int size);
    Bed Build();
}

// Concrete Builder 클래스
public class AceBed : IBedBuilder
{
	private Bed _bed = new Bed();
    private int pillowSize = 0;
    private string sheetName;
    
    public IBedBuilder MakeFrame()
    {
    	_bed.Frame = (Date.Time.Now.Month > 5 && DateTime.Now.Month < 9)
        	? "AceBed No1 Frame"
            : "AceBed No2 Frame"
		return this;
    }
    
    public IBedBuilder MakeMattress()
    {
    	_bed.Mattress = "AceBed Mattress";
        return this;
    }
    
    public IBedBuilder MakePillow(int size)
    {
    	pillowSize = size;
        return this;
    }
    
    public IBedBuilder MakeSheet(string sheet)
    {
    	sheetName = sheet;
        return this;
    }
    
    public Bed Build()
    {
    	_bed.Pillow = "Pillow Size " + pillowSize;
        _bed.Sheet = "Sheet " + sheetName;
        return _bed;
    }
}

// Client 클래스
class Client
{
	void HowToTest()
    {
    	IBedBuilder builder = new AceBed();
        var bed = builder.MakeFrame()
					.MakeMattress()
                   	.MakePillow(10)
                    .Build();
        Console.WriteLine(bed.ToString());
    }
}

.NET Framework에서도 이러한 Fluent Builder 패턴을 사용하고 있다.
ASP.NET Core의 WebHostBuilder를 보면 아래와 같이 여러 옵션들을 선택한 후,
마지막에 Build() 메소드를 호출하여 최종적으로 필요한 옵션을 가진 Host 객체를 생성한다.
CreateDefaultBuilder(), UseContentRoot(), UseIISIntegration(), UseStartUp() 등은
Host 객체를 생성하기 위한 옵션들을 선택하는 메소드로 이들 모두 IWebHostBuilder를 리턴한다.

// ASP.Net Core(Program.cs)
public static void Main(string[] args)
{
	IWebHostBuilder builder = 
    	WebHost.CreateDefaultBuilder(args)
        	.UseContentRoot(Directory.GetCurrentDirectory())
            .UseIISIntegration()
            .UseStartUp<StartUp>();
	
    IWebHost host = builder.Build();
    
    host.runt();

Factory 패턴

Factory 패턴 역시 생성 패턴에 속하며 객체를 직접 생성하는 대신
객체를 생성하는 기능을 가진 Factory 클래스를 만들어 그로부터 객체를 생성하는 패턴이다.

일반적으로 객체지향 프로그래밍에서 객체는 클래스의 생성자를 호출하여 생성된다.
예를 들어, C#에서는 new 키워드를 이용하여 클래스로부터 객체를 생성한다.
그런데 Factory 패턴은 이렇게 new 키워드를 이용하여 객체를 직접 생성하지 않고,
별도로 Factory 클래스를 만들어 그 안의 멤버를 호출하여 객체를 생성한다.

Factory Method 패턴에는 Static Factory Method, Simple Factory, Factory Method,
Abstract Factory 등이 있는데 뒤의 두 가지가 전통적인 패턴으로 알려져있다.

Static Factory Method

Static Factory Method는 Factory 클래스의 정적 메소드를 이용해 객체를 리턴하는 패턴이다.
인터페이스는 정적 메소드를 가질 수 없기 때문에,
Static Factory Method는 통상 일반 클래스 혹은 추상 클래스 안에 존재한다.

interface ILogger { }
class DbLogger : ILogger { }
class XmlLogger : ILogger { }
class JsonLogger : ILogger { }

class LoggerFactory
{
	public static ILogger Create(string loggerType)
    {
    	ILogger logger = null;
        
        switch (loggerType)
        {
        	case "DB":
            	logger = new DbLogger();
                break;
           	case "XML":
            	logger = new XmlLogger();
                break;
           	case "JSON":
            	logger = new JsonLogger();
                break;
			default:
            	throw new InvalidOperationException();
        }
        return logger;
    }
}

class Client
{
	void HowToTest()
    {
    	ILogger logger = LoggerFactory.Create("DB);
    }
}

Simple Factory

Simple Factory는 하나의 메소드에서 특정 객체를 생성하는 패턴이다.
객체 생성 과정이 복잡한 경우 전용 메소드를 사용하여 이를 전담하도록 할 때 유용하다.
예를 들어, 객체를 생성하는 작업이 new를 사용하여 단순히 객체만을 얻어오는 것이 아니라
약간의 복잡한 절차를 필요로 하는 경우, 그리고 이런 작업이 빈번할 경우 사용하면 좋다.

Simple Factory는 Static Factory Method를 조금 더 일반화한 것으로,
객체 생성을 위한 메소드가 정적 메소드일 필요가 없는 형태라고 할 수 있다.

class DbLogger
{
}

class LoggerFactory
{
	public DbLogger CreateDbLogger
    {
    	string connStr = Configuration.Settings["DbConn"].ToString();
        var Db = new DbLogger();
        
        // Db 관련 작업들
        Db.Timeout = 60;
        Db.Error += () => { }
        
        return Db;
    }
}

class Client
{
	void HowToTest()
    {
    	var factory = new LoggerFactory();
        var DbLogger = factory.CreateDbLogger();
    }
}

Factory Method

Static Factory Method 혹은 Simple Factory는 하나의 구체적인 Factory 구현 클래스 안에
객체를 생성하는 메소드 하나가 정의되어 있는 방식이다.

반면, Factory Method는 하나의 Factory 인터페이스(추상 클래스)안에 객체 생성 메서드가 있고,
이를 구현하는 하나 이상의 Factory 서브 클래스들이 존재하는 패턴이다.

아래 예제에서는 LogFactory 추상 클래스안에 GetLog()라는 객체 생성 메소드가 있다.
그리고 이 LogFactory를 XmlLogFactory, DbLogFactory가 상속하고 있다.
XmlLogFactory, DbLogFactory에서 GetLog() 메소드를 재정의하여 특정 객체를 생성한다.

어떤 로거를 사용할 지 미리 결정할 수 없거나 차후 여러 다른 로거들을 추가할 수 있는 경우,
Factory Mehtod 패턴이 유용하게 사용될 수 있다.

interface ILog
{
	void Write(string s);
}

class XmlLog : ILog
{
	private string xmlFile;
    public XmlLog(string xmlFile)
    {
    	this.xmlFile = xmlFile;
    }
    
    public void Write(string s)
    {
    	// ...
    }
}

class DbLog : ILog
{
	private string connString;
    public DbLog(string connString)
    {
    	this.connString = connString;
    }
    public void Write(string s)
    {
    	// ...
    }
}

abstract class LogFactory
{
	protected abstract ILog GetLog();
    
    public void Log(string message)
    {
    	var logger = GetLog();
        logger.Write(message);
    } 
}

class XmlLogFactory : LogFactory
{
	protected override ILog GetLog()
    {
    	string logFile = ConfigurationManager.AppSettings["XmlFile"];
        var xmlLog = new XmlLog(logFile);
        return xmlLog;
    }
}

class DbLogFactory : LogFactory
{
	protected override ILog GetLog()
    {
    	string connString = ConfigurationManager.AppSettings["DBConn];
        var DbLog = new DbLog(connString);
        return DbLog;
    }
}

class Client 
{
	public void HowToTest
    {
    	LogFactory logger = new XmlLogFactory();
        logger.Log("Log Something");
    }
}

Abstract Factory

public abstract class AbstractFactory
{
	public abstract ProductA CreateProductA();
    public abstract ProductB CreateProductB();
}

public class ConcreteFactoryA : AbstractFactory
{
	public override ProductA CreateProductA()
    {
    	return new ConcreteProductA1();
    }
    
    public override ProductB CreateProductB()
    {
    	return new ConcreteProductB1();
	}
}

public class ConcreteFactoryB : AbstractFactory
{
	public override ProductA CreateProductA()
    {
    	return new ConcreteProductA2();
    }
    
    public override ProdcutB CreateProductB()
    {
    	return new ConcreteProductB2();
    }
}

public class ProductA
{
}

public class ConcreteProductA1 : ProductA
{
}

public class ConcreteProductA2 : ProductA
{
}

public class ProductB
{
}

public class ConcreteProductB1 : ProductB
{
}

public class ConcreteProductB2 : Product B
{
}

class Cliet
{
	public void HowToTest()
    {
    	AbstractFactory factory = new ConcreteFactoryA();
        ProductA productA = factory.CreateProductA();
        ProductB productB = factory.CreateProductB();        
    }
}

참고 자료 1) - refactoring.guru
참고 자료 2) - C#으로 이해하는 디자인 패턴

0개의 댓글