기록

[Spring Boot] 1. Design Pattern 본문

Bootcamp/Spring Boot

[Spring Boot] 1. Design Pattern

서기A 2023. 9. 4. 15:32

엔코아 플레이데이터(Encore Playdata) Backend 2기 백엔드 개발 부트캠프 (playdata.io)

 

백엔드 개발 부트캠프

백엔드 기초부터 배포까지! 매력있는 백엔드 개발자 포트폴리오를 완성하여 취업하세요.

playdata.io


1. 생성 (Creational) 패턴

객체 생성에 사용되는 패턴, 객체를 수정해도 호출부가 영향받지 않게 한다.

 

1. 추상 팩토리 패턴 (Abstract Factory Pattern)
인터페이스를 통해 서로 관련있는 객체들을 생성할 수 있게 해준다.

public class BenzPartsFactory implements CarPartsFactory {
    @Override
    public Engine createEngine() {
        return new BenzEngine();
    }

    @Override
    public Tire createTire() {
        return new BenzTire();
    }
}

2. 빌더 패턴 (Builder Pattern)
객체의 생성과 표현을 분리해, 복잡한 객체들을 단계별로 생성할 수 있게 만들어 복잡도를 줄여준다.

public class DefaultHouseBuilder implements HouseBuilder {
    private String houseName;
    private String address;
    private float size;
    private int rooms;
    private int floor;

    @Override
    public HouseBuilder name(String houseName) {
        this.houseName = houseName;
        return this;
    }

    @Override
    public HouseBuilder address(String address) {
        this.address = address;
        return this;
    }

    @Override
    public HouseBuilder houseSize(float size, int rooms) {
        this.size = size;
        this.rooms = rooms;
        return this;
    }

    @Override
    public HouseBuilder floor(int floor) {
        this.floor = floor;
        return this;
    }

    @Override
    public House build() {
        return new House(
                houseName
                , address
                , size
                , rooms
                , floor);
    } 
}

3. 팩토리 메서드 패턴 (Factory Method Pattern)
인터페이스나 추상 클래스를 사용해 객체 생성을 별도의 클래스에 넘기는 패턴이다.

public class MyFactory implements CarFactory {

    @Override
    public Car produceCar(CarType type) {

        if(type.equals(CarType.SEDAN)) {
            return new Car(type, 부품1, 부품2);
        }

        if(type.equals(CarType.TRUCK)) {
            return new Car(type, 부품1, 부품2);
        }

        throw new IllegalArgumentException("생성할 수 없는 종류의 자동차입니다.");
    }
    
}

4. 프로토 타입 패턴 (Prototype Pattern)

기존에 존재하는 객체와 비슷한 객체를 새로 만들고 싶을 때 용이하다. 복제되는 객체에 복제 프로세스를 위임할 수 있다.

Object.clone()

5. 싱글톤 패턴 (Singleton Pattern)

하나의 객체를 생성하여, 애플리케이션 전역에서 이용할 수 있도록 만들어준다. 메모리를 효율적으로 사용할 수 있고,

보안상의 문제로 단 하나만 존재해야 하는 객체를 생성할 때의 문제를 해결할 수 있다.

public class SingletonClass {
    private static instance;

    private SingletonClass() {
        // 생성자를 private 으로 만들면, 외부에서 생성자 호출이 불가능하다.
    }

    public static SingletonClass getInstance() {
        // 1번이라도 호출되어야 인스턴스를 생성한다. 처음부터 싱글톤 인스턴스가 만들어져있는 것이 아니다.
        if (instance == null) {
            instance = new SingletonClass();
        }

        // static 메서드에서 static 필드를 반환하는식으로 구현한다.
        return instance;
    }
}

2. 구조 (Structural) 패턴

객체를 조합해서 더 큰 구조를 만드는 패턴이다.

1. 어댑터 패턴 (Adapter Pattern)

호환되지 않는 인터페이스를 가진 객체들이 협업할 수 있도록 해주는 패턴이다.

즉, 클래스의 인터페이스를 내가 원하는 인터페이스로 변환할 수 있다. (Handler Adapter)

public interface MyHandlerAdapter {
    boolean supports(Object handler);
    ModelView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws ServletException, IOException;
}
private final List<MyHandlerAdapter> handlerAdapters = new ArrayList<>();

public FrontControllerServlet() {
    initHandlerAdapters();
}

private void initHandlerAdapters() {
    handlerAdapters.add(new Controller4HandlerAdapter());
    handlerAdapters.add(new Controller3HandlerAdapter());
}

2. 브릿지 패턴 (Bridge Pattern)
추상화와 구현을 분리, 각각 독립적으로 개발, 즉 인터페이스 사용하는 것을 의미한다.

public interface Color {
    String getColorCode();
}
public class Red implements Color {
    @Override
    public String getColorCode() {
        return "FF0000";
    }
}

 

3. 컴포지트 패턴 (Composite Pattern)

그룹 전체와 개별 객체를 동일하게 처리할 수 있는 패턴으로, 트리 형태로 데이터를 표현할 수 있을 때 유용하다.

즉, 상위 인터페이스나 추상 클래스를 상속해 여러 구현체를 뒀을 때, 클라이언트 입장에선 최상위 인터페이스를 통해 어디든 접근할 수 있게 되는 것이다.

public interface Item {
    int getPrice();
    String getName();
}
public class DefaultItem implements Item {
    private final int price;
    private final String name;

    public DefaultItem(int price, String name) {
        this.price = price;
        this.name = name;
    }

    @Override
    public int getPrice() {
        return price;
    }

    @Override
    public String getName() {
        return name;
    }
}
public class ItemBag extends DefaultItemStorage implements Item {
    private final String name;
    private final int price;

    public ItemBag(int price, String name) {
        this.name = name;
        this.price = price;
    }

    @Override
    public int getPrice() {
        return this.getAllPrice() + this.price;
    }

    @Override
    public String getName() {
        return this.name;
    }
}

 

4. 데코레이터 패턴 (Decorator Pattern)
기존 기능에 영향을 주지 않고, 래퍼 객체를 이용해 기존 객체에 기능을 추가할 수 있다.

public interface PizzaService {
    String pizzaName();
}
public abstract class PizzaDecorator implements PizzaService {
    PizzaService pizzaService;

    public PizzaDecorator(PizzaService pizzaService) {
        this.pizzaService = pizzaService;
    }

    @Override
    public String pizzaName() {
        return pizzaService.pizzaName();
    }
}
public class CheeseDecorator extends PizzaDecorator{
    public CheeseDecorator(PizzaService pizzaService) {
        super(pizzaService);
    }

    @Override
    public String pizzaName() {
        return "치즈 " + super.pizzaName();
    }
}

5. 파사드 패턴 (Facade Pattern)
복잡한 라이브러리에 사용하기 편한 인터페이스를 구성하는 패턴이다.

public class Facade {

    private DBMS dbms = new DBMS();
    private Cache cache = new Cache();

    public void insert() {
        dbms.put("기본키", new Row("이름", "나이", "이메일"));
    }

    public void run(String name) {
        Row row = cache.get(name);
    }
    
}

6. 플라이웨이트 패턴 (Flyweight Pattern)
변화하지 않고, 재사용 가능한 커다란 객체를 캐시에 저장해두고 재활용하는 기법이다.
즉, 무작정 새롭게 만드는게 아니라 같은 값이라면 기존에 저장한 데이터를 활용할 수 있다.

public void stringAddressTest() {
    String myName1 = "Jake Seo";
    String myName2 = "Jake Seo";

    /* 다른 변수지만 같은 주소를 가리키고 있다. */
    System.out.println("myName1 = " + System.identityHashCode(myName1)); // 793331940
    System.out.println("myName2 = " + System.identityHashCode(myName2)); // 793331940
}

7. 프록시 패턴 (Proxy Pattern)
객체를 직접 참조하지 않고 대리 객체를 통해 접근하여, 원본 객체를 대신 처리하게 함으로써 로직의 흐름을 제어한다.

public class MyCarMakeProxy implements CarMake {
    CarMake car; // 원본 객체

    @Override
    public void makeCar() {
        if (this.car == null) {
            this.car = new MyCarMake(); // 원본 객체를 통해 프록시 생성
        }
        car.makeCar();
    }
}

3. 행위 (Behavioral) 패턴

객체 간 알고리즘이나 책임 분배에 관한 패턴으로, 객체 하나로는 수행할 수 없는 작업을 여러 객체를 이용해 작업을 분배한다. 이 때, 결합도의 최소화를 고려해야한다.

1. 책임 연쇄 패턴 (Chain of Responsibility Pattern)
요청 처리 객체 간 연결을 통해 작업을 나눠 처리, 체인 방식이기 때문에 작업을 처리하는 모든 객체가 서로를 알필요가 없다. 즉, 결합도가 낮아진다.

@Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
    System.out.println("First 필터 시작");
    chain.doFilter(request, response);
    System.out.println("First 필터 종료");
}

2. 커맨드 패턴 (Command Pattern)
요청을 호출하는 쪽과 요청을 수행하는 쪽을 분리하는 패턴이다. 즉, 클라이언트의 요청과, 비즈니스 로직 수행이라 볼 수 있다.

이로인해 기능이 캡슐화 되어, 여러 기능을 실행하도록 설계할 수 있다.

public void Command() {
    Light light = new Light();
    Game game = new Game();

    Executor executor = new Executor();
    executor.submit(light::on);
    executor.submit(game::start);
    executor.submit(game::end);
    executor.submit(light::off);
    executor.shutdown();
}

3. 인터프리터 패턴 (Interpreter Pattern)
문법에 등장하는 규칙을 클래스로 정의하고, 일련의 규칙을 통해 언어 및 문법을 해석하는 패턴이다.

대표적인 예로 정규식이 있다.

@Override
public boolean isValid(String value, ConstraintValidatorContext context) {
    return value != null ? value.matches("01(?:0|1|[6-9])[.-]?(\\d{3}|\\d{4})[.-]?(\\d{4})$") : false;
}

4. 이터레이터 패턴 (Iterator Pattern)
컬렉션을 순회하며 탐색하는 패턴이다.

List<Post> posts = new ArrayList<>();

for (Post p : posts) {
	System.out.println(post.getTitle());
}

5. 중재자 패턴 (Mediator Pattern)
객체들이 서로의 필드에 있는 메서드를 마구잡이로 호출하면, 코드가 이해하기 힘들어진다.
때문에 객체 간 직접적인 접근을 제한하고, Mediator 객체를 통해서만 협업하도록 하는 패턴이다.

public void MediatorBankAccount() {
    Person 1번사람 = new Person("1번", 100_000);
    BankAccount 1번사람계좌 = new BankAccount(1번사람);
    1번사람계좌.saveMoney(3000);

    Person 2번사람 = new Person("2번", 5000);
    BankAccount 2번사람계좌 = new BankAccount(jack);
    2번사람계좌.saveMoney(3000);

    Company 회사 = new Company("회사", 100_000);
    BankAccount 회사계좌 = new BankAccount(apple);

    // 돈 보내기
    1번사람계좌.sendMoney(jackBankAccount, 1000);

    // 회사에 투자하기
    회사계좌.sendMoney(appleBankAccount, 1000);
}

6. 메멘토 패턴 (Memento Pattern)
객체의 상태 정보를 저장, 필요에 따라 상태를 복원한다.
즉, 객체의 생성시간, 저장시간 등을 관리할 수 있는 기능을 inner class 등의 형태로 추가하는 것이다.

public static class Memento {
    private final String time;

    public Memento(String timeToSave) {
        this.time = timeToSave;
    }

    public String getSavedTime() {
        return time;
    }
}

7. 옵저버 패턴 (Observer Pattern)
어떤 객체의 상태 변화를 관찰하는 관찰자 객체를 생성하여 사용하는 패턴이다.
대표적인 예시로, 객체에 변화가 감지되면 정의한 update 메서드를 실행한다. 즉, 이벤트 처리 등에 사용하게 된다.

public interface Observer {
    public void update(int runs, int wickets, float overs); 
}

8. 스테이트 패턴 (State Pattern)
객체 내부의 상태에 따라 동작을 변경해야할때 사용하는 패턴이다.
즉, 한 객체 내에 여러 동작을 구현해야할 때, 해당 객체의 상태만 변경하면 현재 진행 중인 동작을 바꿀 수있다

public interface State {
    public void alert(AlertStateContext ctx); 
}
public class AlertStateContext {
    private MobileAlertState currentState;

    public AlertStateContext() {
        currentState = new Vibration();
    }

    public void setState(MobileAlertState state) {
        currentState = state;
    }

    public void alert() {
        currentState.alert(this);
    }  
}

 

9. 스트레티지 패턴 (Strategy Pattern)
행위 자체를 인터페이스로 선언, 각각의 알고리즘은 클래스로 선언 후 오버라이딩을 통해 구현하는 전략 패턴이다.

public interface RockScissorPaper {
    String submit();
}
public class Rock implements RockScissorPaper {
    @Override
    public String submit() {
        return "Rock";
    }
}

 

10. 템플릿 메서드 패턴 (Template Method Pattern)

상속을 통해 슈퍼 클래스의 기능을 확장할 때 사용하는 대표적인 방법으로, 변하지 않는 기능은 슈퍼 클래스에 만들어 두고 자주 변경되며 확장할 기능은 서브 클래스에 만든다.

public abstract class Ramen {
    public void makeRamen() {
        boilWater();
        putNoodles();
        putExtra();
        waitForMinutes();
    }

    public void boilWater() {
        System.out.println("물을 끓인다.");
    }

    public void putNoodles() {
        System.out.println("면을 넣는다.");
    }

    public abstract void putExtra();

    public abstract void waitForMinutes();
}
public class ShinRamen {
    public void boilWater() {
        System.out.println("물을 끓인다.");
    }

    public void putNoodles() {
        System.out.println("면을 넣는다.");
    }

    public void putEgg() {
        System.out.println("계란을 넣는다.");
    }

    public void waitForMinutes() {
        System.out.println("4분 기다린다.");
    }
}

 

11. 비지터 패턴 (Visitor Pattern)
비슷한 종류의 객체 그룹에서 작업을 수행할 때, 메서드 오버로딩으로 만든 메서드들을 한 클래스에 몰아서 구현하는 것이다.

public class ShoppingCartVisitorImpl implements ShoppingCartVisitor{
    @Override
    public int visit(Book book) {
        int cost = 0;

        if(book.getPrice() > 50) cost = book.getPrice()-5;
        else cost = book.getPrice();

        System.out.println("Book ISBN : " + book.getIsbnNumber() + " cost = " + cost);
        return cost;
    }

    @Override
    public int visit(Fruit fruit) {
        int cost = fruit.getPricePerKg() * fruit.getWeight();
        System.out.println(fruit.getName() + " cost = " + cost);
        return cost;
    }
}

4. 36일차 후기

디자인 패턴이란, 소프트웨어 개발 시 자주 발생하는 문제에 대한 일반적인 해결책을 의미한다.

 

스프링 같은 프레임워크들 또한 해당 패턴들을 많이 사용하여 구현 된 것이기 때문에, 전부는 아니더라도 중요한 몇 가지는 꼭 알고 넘어가면 좋을 것 같다는 생각이 들었다.

'Bootcamp > Spring Boot' 카테고리의 다른 글

[Spring Boot] 6. Spring Data JPA  (0) 2023.09.11
[Spring Boot] 5. 상속 관계 매핑  (0) 2023.09.11
[Spring Boot] 4. 연관 관계 매핑  (0) 2023.09.07
[Spring Boot] 3. 단위 테스트, TDD  (0) 2023.09.05
[Spring Boot] 2. JPA  (1) 2023.09.04