본문 바로가기
OOP

[OOP] 객체지향의 원칙 SOLID - 개방 폐쇄 원칙 (OCP)

by mizuiro 2024. 6. 21.

개방-폐쇄 원칙 (Open/Closed Principle, OCP)


소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다.
새로운 기능을 추가할 때 기존 코드를 수정하지 않고도 확장할 수 있어야 한다.

 

개방 폐쇄 원칙의 정의는 즉 새로운 기능을 추가하거나 기존 성능을 확장 할 때 기존의 안정적인 코드를 변경하지 않고 새로운 코드를 추가해야 한다는 것이다

 

이러한 개방 폐쇄 원칙은 객체 지향의 네 가지 특성 중 (캡슐화, 다형성, 추상화)과 연관이 있다

 

캡슐화 :  다른 객체가 객체의 데이터에 대한 접근을 제어하고 데이터의 변경을 최소화 하는 것

그렇기에 개방 패쇄 원칙은 공용 인터페이스(public) 을 통해 다를 객체와 협력하고 다른 메서드들과 상태들은 들어나지 않게 되면서 캡슐화가 된다

 

다형성 :  하나의 기능에 대해 여러 객체나 메서드들이 대체 가능할 수 있다는 것

이러한 다형성은 상속과 관련한 overriding과, 인터페이스를 통한 overloading을 통해 구현 될 수 있다

개방 폐쇄 원칙에서는 새로운 클래스를 추가하여 코드의 기능을 변경하고자 하는 것이므로 다형성이 적용되고 있다

 

추상화 : 객체들의 공통점을 취하여 일반화를 시킨 것으로 추상화를 통해 인터페이스를 정의하고 구현을 분리하여 기능을 확장 시킬 수 있다는 것

개방 폐쇄 원칙에서는 하나의 공통점을 가진 인터페이스나 추상화 클래스를 만들어서 적용함으로써 기능을 추가하여 추상화가 적용되고 있다

 

 

개방 폐쇄 원칙 장점

1. 메서드의 동작을 변경 하거나 확장 할 때 기존 코드를 수정하지 않아 코드의 유연성이 증가하고 유지 보수성이 향상

2. 메서드의 구조를 변경하지 않고 다양한 방식으로 동작이 수행할 수 있게 되면서 코드의 재사용성이 증가

 

개방 폐쇄 원칙을 적용하기 위해 디자인 패턴( 팩토리 패턴, 템플릿 메서드 패턴, 전략 패턴) 과 함수형 프로그래밍을 사용해 수행한다

 

다른 원칙과의 관계

  • SRP: SRP를 지키면 클래스가 단일 책임만 가지므로, 기존 클래스의 수정 없이 확장이 가능해져 OCP를 만족할 수 있습니다.
  • LSP: OCP를 지키기 위해서는 서브 클래스가 기반 클래스를 대체할 수 있어야 하므로 LSP를 준수해야 합니다.
  • ISP: 인터페이스가 잘 분리되어 있으면 새로운 기능 추가 시 특정 인터페이스를 확장하기 쉬워 OCP를 만족할 수 있습니다.
  • DIP: DIP를 지키면 구체적인 구현체 대신 인터페이스나 추상 클래스에 의존하게 되어, 기존 코드를 수정하지 않고도 확장이 가능합니다.

팩토리 패턴

구조

  • 인터페이스: 객체 생성을 위한 팩토리 메서드를 정의한다.
  • 구체적인 팩토리 클래스들: 각각의 구체적인 클래스에 대해 팩토리 메서드를 구현한다
// Shape 인터페이스
public interface Shape {
    void draw();
}

// 구체적인 도형 클래스들
public class Circle implements Shape {
    @Override
    public void draw() {
        System.out.println("원을 그립니다.");
    }
}

public class Rectangle implements Shape {
    @Override
    public void draw() {
        System.out.println("사각형을 그립니다.");
    }
}

public class Triangle implements Shape {
    @Override
    public void draw() {
        System.out.println("삼각형을 그립니다.");
    }
}

// 팩토리 인터페이스
public interface ShapeFactory {
    Shape createShape();
}

// 구체적인 팩토리 클래스들
public class CircleFactory implements ShapeFactory {
    @Override
    public Shape createShape() {
        return new Circle();
    }
}

public class RectangleFactory implements ShapeFactory {
    @Override
    public Shape createShape() {
        return new Rectangle();
    }
}

public class TriangleFactory implements ShapeFactory {
    @Override
    public Shape createShape() {
        return new Triangle();
    }
}

// 클라이언트 코드
public class Client {
    public static void main(String[] args) {
        ShapeFactory circleFactory = new CircleFactory();
        Shape circle = circleFactory.createShape();
        circle.draw(); // Output: 원을 그립니다.
        
        ShapeFactory rectangleFactory = new RectangleFactory();
        Shape rectangle = rectangleFactory.createShape();
        rectangle.draw(); // Output: 사각형을 그립니다.
        
        ShapeFactory triangleFactory = new TriangleFactory();
        Shape triangle = triangleFactory.createShape();
        triangle.draw(); // Output: 삼각형을 그립니다.
    }
}

 


템플릿 메서드 패턴

구조

  • 추상 클래스나 인터페이스에서 메서드의 골격을 정의하고, 세부 구현을 하위 클래스에서 담당하게 한다.
  • 이 방식은 메서드의 기본 구조를 변경하지 않고 새로운 기능을 추가하거나 기존 기능을 확장할 수 있게 한다
abstract class Notification {
    public void sendNotification() {
        prepare();
        send();
        log();
    }

    protected abstract void prepare();
    protected abstract void send();
    protected abstract void log();
}

class EmailNotification extends Notification {
    @Override
    protected void prepare() {
        // 이메일 준비 코드
    }

    @Override
    protected void send() {
        // 이메일 전송 코드
    }

    @Override
    protected void log() {
        // 이메일 전송 로그 코드
    }
}

 


 

전략 패턴

구조

  • 전략 인터페이스: 다양한 알고리즘을 정의하는 인터페이스
  • 구체적인 전략 클래스들: 각각의 구체적인 알고리즘을 구현한다
  • 컨텍스트: 전략 객체를 사용하여 작업을 수행한다
  • 기능 추가 : 메서드 동작 방식 변경시 새로운 전략 클래스 추가
// 정렬 전략 인터페이스
public interface SortingStrategy {
    void sort(int[] data);
}

// 버블 정렬 전략
public class BubbleSortStrategy implements SortingStrategy {
    @Override
    public void sort(int[] data) {
        System.out.println("버블 정렬을 수행합니다.");
        // 구현 생략
    }
}

// 퀵 정렬 전략
public class QuickSortStrategy implements SortingStrategy {
    @Override
    public void sort(int[] data) {
        System.out.println("퀵 정렬을 수행합니다.");
        // 구현 생략
    }
}

// 정렬을 수행하는 컨텍스트
public class SortContext {
    private SortingStrategy strategy;

    public SortContext(SortingStrategy strategy) {
        this.strategy = strategy;
    }

    public void setStrategy(SortingStrategy strategy) {
        this.strategy = strategy;
    }

    public void performSort(int[] data) {
        strategy.sort(data);
    }
}

// 클라이언트 코드
public class Client {
    public static void main(String[] args) {
        int[] data = {5, 1, 3, 7, 2};

        // 버블 정렬 전략을 사용하여 정렬
        SortingStrategy bubbleSort = new BubbleSortStrategy();
        SortContext context = new SortContext(bubbleSort);
        context.performSort(data); // Output: 버블 정렬을 수행합니다.

        // 퀵 정렬 전략을 사용하여 정렬
        SortingStrategy quickSort = new QuickSortStrategy();
        context.setStrategy(quickSort);
        context.performSort(data); // Output: 퀵 정렬을 수행합니다.
    }
}

 


 

함수형 프로그래밍

  • 람다 표현식이나 함수형 인터페이스를 이용하여 메서드의 기능을 동적으로 변경할 수 있다
  • 새로운 기능을 추가할 때 기존 메서드를 수정하지 않고 새로운 함수를 정의하여 사용할 수 있다
interface NotificationStrategy {
    void send();
}

public class NotificationService {
    public void sendNotification(NotificationStrategy strategy) {
        strategy.send();
    }
}

// 사용 예시
NotificationService service = new NotificationService();
service.sendNotification(() -> {
    // 이메일 전송 코드
});
service.sendNotification(() -> {
    // SMS 전송 코드
});

 

 

예시

앞의 SRP에서 가상적으로 만든 커피 주문 시스템을 사용한다
고객(커피를 주문하는 책임), 캐셔(주문을 받는 책임), 바리스타(커피를 제조하는 책임) 객체

메서드 추가
바리스타 객체의 클래스에서는 커피 제조 하는 행위인 메서드 makeCoffee 가 존재
기존의 makeCoffee 메서드는 단순히 커피를 제조하는 행위만 하는 것이었는데
새로운 기능을 추가하여서 캐셔가 커피 종류와 함께 커피 제조 요청을 하는 것으로 변환
그에 따라 바리스타 객체의 클래스는 makeCoffee 라는 메서드 안에 조건을 달아서
아메리카노나 라떼를 만들도록 수정

 

// origin
class barista {
	void makeCoffee()
}

// refactor
class barista {
	void giveCoffee(String coffee) {
		if(coffee = 'americano');
		if(coffee = 'latte');
}

 

문제점
이러한 코드는 기존 코드를 수정해서 기능을 확장하였기 때문에 ocp 가 지켜지지 않은 코드

OCP 적용
팩토리 패턴을 적용하여 코드를 수정
팩토리 인터페이스인 coffeeFactory 를 만들어서 makeCoffee라는 메서드를 정의
팩토리 인터페이스를 상속 받은 팩토리 클래스들인 EspressoFactory , LatteFactory 를 만들고 makeCoffee 메소드를 정의
바리스타 클래스에서는EspressoFactory ,LatteFactory 를 인스턴스로 만들어 맵으로 저장을 하고, 캐서의 요청을 수행할 giveCoffee 메서드를 생성
giveCoffee 메서드는 캐서가 커피의 종류를 String으로 받아 String에 해당하는 커피 객체를 찾아 객체의 makeCoffee 메서드를 실행시켜 커피를 생성

이 코드에서 만약 새로운 종류의 커피를 추가하려고 한다면,
새로운 팩토리 클래스를 구현하여 바리스타에게 커피 제조를 저장하게 하면 되므로 기존 코드를 수정하지 않고 새로운 코드를 추가하여 개방 폐쇄 원칙을 지킬 수 있다

 

 

팩토리 클래스

// coffeeFactory 인터페이스
public interface CoffeeFactory {
    String makeCoffee();
}

// 구체적인 커피 클래스들
public class EspressoFactory implements CoffeeFactory {
    @Override
    public String makeCoffee() {
        return "Espresso is ready!";
    }
}

public class LatteFactory implements CoffeeFactory {
    @Override
    public String makeCoffee() {
        return "Latte is ready!";
    }
}

public class CappuccinoFactory implements CoffeeFactory {
    @Override
    public String makeCoffee() {
        return "Cappuccino is ready!";
    }
}

 

 

바리스타 클래스

// 바리스타 클래스
import java.util.HashMap;
import java.util.Map;

public class Barista {
    private Map<String, CoffeeFactory> coffeeFactories;

    public Barista() {
        coffeeFactories = new HashMap<>();
        coffeeFactories.put("espresso", new EspressoFactory());
        coffeeFactories.put("latte", new LatteFactory());
        coffeeFactories.put("cappuccino", new CappuccinoFactory());
    }

    public String giveCoffee(String coffeeType) {
        CoffeeFactory factory = coffeeFactories.get(coffeeType.toLowerCase());
        if (factory == null) {
            throw new IllegalArgumentException("Unknown coffee type: " + coffeeType);
        }
        return factory.makeCoffee();
    }
}

 

 

캐셔 클래스

// 캐셔 클래스
public class Cashier {
    private Barista barista;

    public Cashier(Barista barista) {
        this.barista = barista;
    }

    public String orderCoffee(String coffeeType) {
        return barista.giveCoffee(coffeeType);
    }
}

 

 

메인 사용 클래스

// 사용 예시
public class Main {
    public static void main(String[] args) {
        Barista barista = new Barista();
        Cashier cashier = new Cashier(barista);

        System.out.println(cashier.orderCoffee("espresso"));   // Output: Espresso is ready!
        System.out.println(cashier.orderCoffee("latte"));      // Output: Latte is ready!
        System.out.println(cashier.orderCoffee("cappuccino")); // Output: Cappuccino is ready!
    }
}