[백엔드 로드맵 - 개발방법론] Design Pattern Part1

Sierra·2022년 7월 30일
0

Backend-Roadmap

목록 보기
18/43
post-thumbnail

Intro

디자인 패턴에 대한 시리즈는 세 파트로 나누기로 하겠다. 생각보다 양도 많고 코드도 많이 들어갈테니까. 글이 길면 쓰는 사람도, 보는 사람도 힘드니까.

학부 시절에 알아 뒀으면 얼마나 좋을까 싶었던 지식들이라 생각한다. 마냥 내가 JAVA로 코딩을 할 줄 아는것과 이걸로 어떻게 문제를 해결 하느냐의 문제는 완전히 다른 문제니까.

학부 생활을 하면서 MVC에 대해서 만큼은 정말 진득하게, 많이도 사용하고 알아 보았지만 그게 다가 아니다. 실무에서 Spring Framework든 Node.js든 서버 프로그래밍을 할 때 왜 이런식으로 코딩 해 두었을까? 라는 고민들이 드는 코드들을 이해하기 시작 할 때 쯤, 디자인 패턴을 잘 알아두는 게 좋다는 생각이 들었다.

물론 어느 책에서든 나오는 얘기지만, 패턴이 다가 아니다. 더 좋은 방법이 있으면 그걸 쓰는 게 맞다. 하지만 복싱도 기본 동작부터 배워야 숄더롤을 하든 말든 하는거다. 잽과 훅 부터 제대로 써야 콤비네이션을 연계 할 수 있는 법.

Part 1에서는 SingleTon, Factory, Factory Method, Strategy Pattern 에 대해서 다룰 생각이다. Part 2에서는 Observer, Proxy, Iterator, revealing module 을, Part 3에서는 MVC, MVP, MVVM 에 대해 다룰 생각이다.

Design Pattern 이란?

프로그램을 설계 할 때 생길 수 있는 문제점들을 객체 간의 상호 관계 등을 통해 해결 할 수 있도록 규약 형태로 만들어 둔 것을 말한다.

이해하기 쉽게 생각 해 보자. 어떤 물건을 개발 할 때, 비슷한 역할을 하는 물건들의 설계 패러다임이 다 비슷비슷 하다는 것을 알 수 있다.

자동차 엔진의 경우 4행정 기관을 채택하고 있다. 이러한 4행정 기관 메커니즘을 기반으로 디젤 엔진, 가솔린 엔진 등의 엔진들이 개발 되어왔다. 사용하는 연료와 짜잘한 차이들이 있겠지만(필자는 자동차 전공이 아니다...자세히 알 지는 못 한다.) 자동차를 앞으로 굴린다는 문제를 해결하기 위해 이러한 패턴을 채택하여 엔진을 설계 했다고 볼 수 있다.

프로그램 설계도 마찬가지다. 우리는 다양한 서비스를 개발 하지만, 그러한 서비스 내에서도 겹치는, 비슷한 기능들이 상당히 많을 것이다. 예를 들면 팔로우 한 인플루언서의 새로운 포스팅에 대한 푸쉬 알림 같은 기능들은 인스타그램 뿐 만 아니라 트위터, 페이스북 등 SNS 에서 매우 활발히 쓰이고 있는 기능이다. 이러한 기능을 개발할 때 각 각의 회사가 모두 다른 메커니즘을 썼을까?

실제 코드를 봐야 알겠지만 대부분은 Observer Pattern을 썼을 가능성이 높다. 어떠한 기능을 개발 해 내는데 유리한 코딩 방식은 분명 정해져 있을 것이다. 마치 자동차 엔진을 개발하는 데 4행정 기관이 가장 널리 쓰이듯이.

물론 전기자동차 처럼 모터를 통해 동력을 전달하는 경우는 얘기가 다르다. 애초에 이런 경우엔 에너지원 자체가 다르기 때문에 메커니즘이 달라 질 수 밖에 없다.

백엔드 개발자로써 Design Pattern은 왜 중요할까? 개인적인 생각엔 화면 뒤의 수 많은 로직들을 처리하는 건 결국 백엔드 개발자라고 생각한다. 예시를 든 SNS의 푸쉬알림 기능과 같은 건 백엔드에서 모든 처리가 완료되었을 때 가능 한 일이라고 생각한다. 즉 더 좋은 서비스를 개발하기 위해, 다양한 기능들을 개발 할 수 있는 서비스 개발자가 되기 위해 반드시 필요한 지식이기 때문이지 않을까 라고 짧은 소견을 얘기 해 본다.

Singleton Pattern

싱글톤 패턴은 객체의 인스턴스가 오직 1개만 생성되는 패턴을 의미한다.
DB Connection Pool, thread pool, Log 객체 등 하나의 인스턴스를 생성해서 메모리에 등록 후 다른 클래스의 인스턴스들이 데이터를 공유하여 사용 할 수 있도록 개발 하고자 할 때 사용된다.

단점이 있다면 개방 폐쇄 원칙을 위배 할 수 있다는 것이다. (SOLID에 대한 글 또한 이거 마치는 대로 쓸 예정)
이러한 객체 하나가 너무 많은 일을 하거나, 너무 많은 데이터를 공유시킨다면 다른 클래스들과 결합도가 높아진다.

이런 문제가 생긴다면 유지보수도 힘들고 Unit test 시 상당히 불리해진다.
TDD를 하게 될 때 생기는 문제를 한번 서술 해 보자면, Unit Test 시 각 테스트가 모두 독립적이어야 하는데, 싱글톤 객체의 경우엔 각 테스트마다 독립적인 인스턴스를 만드는 게 어려워진다.

DI(Dependency Injection) 을 통해 모듈간의 결합도를 조금 줄일 수는 있다. 이 경우에 모듈을 쉽게 교체 할 수 있으므로 테스팅이 쉬워진다. 또한 애플리케이션 Dependecy 방향이 일관적으로 변하기에 모듈간의 관계가 조금 더 명확해진다.
그렇지만 이런 경우엔 모듈들이 더욱 분리되므로 클래스 수가 늘어나 복잡성이 증가할 수 있다.

Singleton Pattern 예제 코드

class Singleton{
    private static clsss singleInstanceHolder{
        private static final Singleton INSTANCE = new Singleton();
    }
    public static synchronized Singleton getInstance(){
        return singleInstanceHolder.INSTANCE;
    }
}

public class Main{
    public static void main(String[] args){
        Singleton a = Singleton.getInstance();
        Singleton b = Singleton.getInstance();
        /*
        이렇게 생성 된 두 객체는 같은 인스턴스를 공유하기 때문에
        같은 객체 마냥 인식된다. 
        */
       	System.out.println(a.hashcode());
        System.out.println(b.hashcode());
        //둘 다 같은 hashcode가 출력
        if(a == b){
        	System.out.println("true");
            //true 출력
        }
    }
}

Factory Pattern

세 가지 종류가 있다. 팩토리 패턴, 추상 팩토리 패턴, 그리고 팩토리 메소드 패턴.

모두 객체를 생성하는 방식에 대한 패턴이라는 공통점을 가지고 있다.

객체를 생성하는 로직을 따로 빼서 상위 클래스와 하위 클래스 간의 결합도를 줄이는 데 목적이 있다.

만약 팩토리가 생성하고자 하는 다양한 객체 중 한 가지 객체에 대한 코드를 수정하고자 할 때, 팩토리를 사용하고 있는 상위 클래스에 대한 코드는 전혀 수정 할 필요가 없게 된다. 즉 유지보수성이 뛰어난 코드를 개발 해 낼 수 있다.

차이점?

팩토리 패턴은 가장 단순한 형태의 팩토리 패턴. Factory 클래스를 통해 객체를 생성하는 데 목적을 가지고 있는 패턴이다. 이 큰 틀 아래에 두 가지 패턴이 존재한다고 생각하면 된다.

팩토리 메소드 패턴은 객체를 생성하는 인터페이스는 미리 정의하되, 객체 생성은 서브 클래스(팩토리)로 위임하는 패턴이다. 즉 팩토리 클래스에서의 메소드의 입력값이 따라 그 결과 값으로 해당 입력값에 대한 객체가 생성되도록 하는 것이다.

추상 팩토리 패턴은 서로 관련이 있거나 의존적인 객체들의 조합을 만드는 인터페이스를 제공하는 패턴이다.

둘 다 용도는 비슷하지만, 확실히 다르다. 팩토리 메소드 패턴은 말 그대로 객체 하나를 가져오기 위해 필요하지만, 추상 팩토리 패턴은 서로 연관 있는 객체들의 조합이 필요할 때 사용한다.

Head first Design Patterns 에 나오는 유명한 예시인 피자가게 예시를 통해 두 패턴의 차이를 한번 알아보자.

Factory Pattern 예시 코드

Factory Method Pattern

public abstract class Pizza {
    String name;
    String dough;
    String sauce;

    void prepare() {
        System.out.println("preparing~~ " + name);
    }

    void bake() {
        System.out.println("baking~~");
    }

    void box() {
        System.out.println("boxing~~");
    }

    public String getName() {
        return name;
    }
}

피자 추상 클래스가 하나 있다고 생각해보자.

public class ChicagoStyleCheesePizza extends Pizza {
    public ChicagoStyleCheesePizza() {
        name = "ChicagoStyleCheesePizza";
    }
}
public class ChicagoStyleClamPizza extends Pizza {
    public ChicagoStyleClamPizza() {
        name = "ChicagoStyleClamPizza";
    }
}
public class ChicagoStyleVeggiePizza extends Pizza {
    public ChicagoStyleVeggiePizza() {
        name = "ChicagoStyleVeggiePizza";
    }
}

이 추상클래스를 기반으로 다양한 종류의 피자 클래스가 만들어 질 수 있다. 피자 가게에서 다양한 피자를 팔아 줘야 경쟁력이 있겠지.

public abstract class PizzaStore {
    public Pizza orderPizza(String type) {
        Pizza pizza = createPizza(type);

        pizza.prepare();
        pizza.bake();
        pizza.box();

        return pizza;
    }

    abstract Pizza createPizza(String type);
}

다음으로 PizzaStore 클래스를 생성해보자. 피자 가게는 피자 주문 기능과 피자 생성 기능이 있어야 한다. 또한 추상클래스니까 이 클래스를 기반으로 다양한 종류의 피자 가게를 만들 수 있어야 한다.

public class ChicagoPizzaStore extends PizzaStore {
    @Override
    Pizza createPizza(String item) {
        if ("cheese".equals(item)) {
            return new ChicagoStyleCheesePizza();
        } else if ("veggie".equals(item)) {
            return new ChicagoStyleVeggiePizza();
        } else if ("clam".equals(item)) {
            return new ChicagoStyleClamPizza();
        } else {
            return null;
        }
    }
}

시카고 피자 전문점을 차려 보았다. createPizza 추상 메소드를 Override 해서 Factory Method를 만들어 보았다.
입력 값에 따라 그에 맞는 피자 객체를 생성 해 줄 수 있다.

예시에서는 뉴욕 피자도 나오지만 그 부분은 생략하도록 하겠다.

Abstract Factory Pattern

public interface PizzaIngredientFactory {
    public Dough createDough();
    public Sauce createSauce();
    public Cheese createCheese();	
    public Veggies[] createVeggies();	
    public Pepperoni createPepperoni();	
    public Clams createClams();
}

Factory Method Pattern 과는 다르게 정말 원초적인 부분부터 시작한다. 피자 객채만 땅 하고 생성하는 게 아니라 정말 피자 재료 공장부터 한번 만들어보자 이거다.

public class ChicagoPizzaingredientFactory implements PizzaIngredientFactory{	
    @Override	
    public Dough createDough() {		
        return new ThickCrustDough();	
    }	
    @Override	
    public Sauce createSauce() {		
        return new PlumTomatoSauce();	
    }	
    @Override	
    public Cheese createCheese() {		
        return new MozzarellaCheese();	
    }	
    @Override	
    public Veggies[] createVeggies() {		
        Veggies veggies[] = { 
            new BlackOlives(), 
            new Spinach(), 
            new EggPlant()
        };		
        return veggies;	
    }	
    @Override	
    public Pepperoni createPepperoni() {		
        return new Slicedpepperoni();	
    }	
    @Override	
    public Clams createClams() {		
        return new FrozenClam();	
    }
}
public class NYPizzaingredientFactory implements PizzaIngredientFactory{	
    @Override	
    public Dough createDough() {		
        return new ThinCrustdough();	
    }	
    @Override	
    public Sauce createSauce() {		
        return new MarinaraSauce();	
    }	
    @Override	
    public Cheese createCheese() {		
        return new ReggianoCheese();	
    }	
    @Override	
    public Veggies[] createVeggies() {		
        Veggies veggies[] = {
             new Farlic(), 
             new Onion(), 
             new Mushroom(), 
             new RedPepper() 
            };		
        return veggies;	
    }	
    @Override	
    public Pepperoni createPepperoni() {		
        return new SlicedPepperoni();	
    }	
    @Override	
    public Clams createClams() {		
        return new Freshclams();	
    }
}

PizzaIngredientFactory 클래스를 확장하여 두 가지 종류의 피자 재료 공장을 만들어 보았다. 엄연히 뉴욕 피자랑 시카고 피자는 다르니까. 각 피자 종류별로 필요한 재료들을 생성해 낼 수 있는 메소드 들이 Override 되어 구현되어 있다.

public abstract class Pizza{	
    String name;	
    Dough dough;	
    Sauce sauce;	
    Veggies veggies[];	
    Cheese cheese;	
    Pepperoni pepperoni;	
    Clams clams;		
    public abstract void prepare();		
    public void bake(){		
        System.out.println("Bake for 25 minutes at 350");
    }		
    public void cut(){		
        System.out.println("Cutting the pizza into diagonal slices");	
    }		
    public void box(){		
        System.out.println("Place pizza in official PizzaStore box");	
    }		
    public String getname(){		
        return this.name;	
    }
}

아까와는 더욱 피자를 구성하는 객체들이 많아졌다. 자 그러면 이 추상클래스를 기반으로 피자 클래스들을 만들어 보자.

public class CheesePizza extends Pizza{	
    PizzaIngredientFactory ingredientFactory;		
    public CheesePizza(PizzaIngredientFactory ingredientFactory) {		
        this.ingredientFactory = ingredientFactory;	
    }	
    @Override	
    public void prepare() {		
        this.dough = ingredientFactory.createDough();		
        this.sauce = ingredientFactory.createSauce();		
        this.cheese = ingredientFactory.createCheese();	
    }
}
public class ClamPizza extends Pizza{	
    PizzaIngredientFactory ingredientFactory;		
    public ClamPizza(PizzaIngredientFactory ingredientFactory) {		
        this.ingredientFactory = ingredientFactory;	
    }	
    @Override	
    public void prepare() {		
        this.dough = ingredientFactory.createDough();		
        this.sauce = ingredientFactory.createSauce();		
        this.cheese = ingredientFactory.createCheese();		
        this.clams = ingredientFactory.createClams();	
    }
}

여기서 PizzaIngredientFactory, 그리고 prepare 코드에 주목.

PizzaIngredientFactory의 종류에 따라 이 Pizza 객체에 반영 될 dough, sauce, cheese 가 달라 질 것이란 걸 알 수 있다.

그리고 위에서 언급했듯 PizzaIngredientFactory는 현재 두 가지가 있다. (갠적으론 시카고 피자는 맥주 먹을 때 아니면 별로 좋아하진 않는다.) 치즈 피자와 클램피자(클램차우더 할 때 클램. 조개피자) 두 가지 피자를 뉴욕식으로 만들거나 시카고 식으로 만들 수 있다는 것이다.

public class NYPizzaStore extends PizzaStore{	
    @Override	
    public Pizza createPizza(String type){		
        Pizza pizza = null;		
        PizzaIngredientFactory ingredientFactory = new NYPizzaingredientFactory();		
        if(type.equals("cheese")){			
            pizza = new CheesePizza(ingredientFactory);			
            pizza.setName(ingredientFactory.NY_STYLE+" Cheese Pizza");		
        }else if(type.equals("peper")){			
            pizza = new PepperoniPizza(ingredientFactory);			
            pizza.setName(ingredientFactory.NY_STYLE+" Pepperoni Pizza");		
        }else if(type.equals("clam")){			
            pizza = new ClamPizza(ingredientFactory);			
            pizza.setName(ingredientFactory.NY_STYLE+" Clam Pizza");		
        }else if(type.equals("veggie")){			
            pizza = new VeggiePizza(ingredientFactory);			
            pizza.setName(ingredientFactory.NY_STYLE+" Veggie Pizza");		
        }		
        return pizza;	
    }
}

마지막으로 뉴욕 피자 가게를 한번 차려보자. (PizzaStore 원형 코드는 생략한다.) 당연히 ingredientFactory는 NYPizzaingredientFactory일 것이다. 재료 조달 처 까지 생겼으니 피자를 팔아보자. 우린 createPizza 메소드를 통해 각 피자 특성을 살린 cheese, peper, clam, veggie 피자를 만들 수 있다.

이 처럼 Abstract Factory Pattern은 Factory Method와 비슷하지만 하나 확실한 객체의 조합을 만들어 내기 위해 사용된다.

Strategy Pattern

전략 패턴, 정책(Policy Pattern) 등등 다양한 이름이 붙어있는 이 패턴은 객체의 행위를 바꾸고싶은데, 직접 수정하지 않고 전략이라고 부르는 캡슐화 된 알고리즘을 컨텍스트 안에서 바꿔주면서 상호 교체가 가능하게 만드는 패턴이다.

예를 들어 물건을 살 때, 결제 방식에 다양한 방식들이 있겠지만, 네이버 페이든 카카오 페이든 어떤 방식으로든 '전략' 만 바꿔서 결제 처리를 할 때 사용된다.

Strategy Pattern 예제 코드

interface PaymentStrategy{
    public void pay(int amount);
}

이를테면(뭔가 수학의 정석 같지만 글 쓰다보면 참 자주 쓰게되는 접두사다.), PaymentStrategy라는 인터페이스가 하나 있다 치자.

class KAKAOPayStrategy implements PaymentStrategy{
    private String name;
    private String email;
    
    public KAKAOPayStrategy(Stirng name, String email){
        this.name = name;
        this.email = email;
    }
    @Override
    public void pay(int amount){
    	System.out.println(amount + " paid using KAKAOPay.");
    }

}
class KCPModuleStrategy implements PaymentStrategy{
	private String name;
    private String cardNum;
    private String cvv;
    private String dateOfExpiry;
    
    public KCPModuleStrategy(String name, String cardNum, String cvv, String dateOfExpiry){
    	this.name = name;
        this.cardNum = cardNum;
        this.cvv = cvv;
        this.dateOfExpiry = dateOfExpiry;
    }
    
    @Override
    public void pay(int amount){
        System.out.println(amount + " paid using KCP Module.");
    }
    
}

두 가지 전략을 한번 만들어봤다. 카카오페이와 KCP 모듈. 이제 물건을 결제할 때 이 두가지 방법 중 하나를 써 볼것이다. 결제창에 들어왔다고 생각 해 보자.

cart.addItem(A);
cart.addItem(B);

if(this.payStrategy.equals("KAKAO"){
	cart.pay(new KAKAOPayStrategy("SWJ", "email@email.com"));
} else if (this.payStrategy.equals("KCP"){
   cart.pay(new KCPModlueStrategy("SWJ","12341234","123","2025-05"));
}

선택 한 결제 방식에 따라 다른 전략을 취해 줌으로써 결제 트랜잭션을 진행시킬 수 있다. (에러 처리는 일단 되어있다고 생각하자...귀찮다.)

Outro

크게 Singleton, Factory(Abstract Factory, Factory Method), Strategy Pattern 에 대해 알아보았다.

이해하기 상당히 어려울 수 있지만, 이해 하고나면 왜 선임자가 코드를 저렇게 짰는 지 이해 할 수 있는...정말 중요한 지식들이라고 생각한다.

다음 포스팅에 계속 하도록 하겠다.

Reference

https://github.com/gyoogle/tech-interview-for-developer
https://book.interpark.com/product/BookDisplay.do?_method=Detail&sc.shopNo=0000400000&dispNo=&sc.prdNo=354684683&sc.saNo=002001023&bkid1=category&bkid2=ct028023&bkid3=c1&bkid4=001
https://ko.wikipedia.org/wiki/%EC%8B%B1%EA%B8%80%ED%84%B4_%ED%8C%A8%ED%84%B4
https://ko.wikipedia.org/wiki/%ED%8C%A9%ED%86%A0%EB%A6%AC_%EB%A9%94%EC%84%9C%EB%93%9C_%ED%8C%A8%ED%84%B4
https://www.aladin.co.kr/shop/wproduct.aspx?ItemId=582754

profile
블로그 이전합니다 : https://swj-techblog.vercel.app/

0개의 댓글