본문 바로가기
소프트웨어공학/OOP

[OOP] OCP(개방 폐쇄 원칙)과 디자인 패턴

by 클레어몬트 2024. 8. 12.

https://claremont.tistory.com/entry/SW-%EC%84%A4%EA%B3%84-%EC%9B%90%EC%B9%99-SOLID-%EC%9B%90%EC%B9%99%EA%B3%BC-%EA%B7%B8-%EC%A7%84%EC%A0%95%ED%95%9C-%EC%9D%98%EB%AF%B8

 

[SW 설계 원칙] SOLID 원칙과 그 진정한 의미

ㅁSOLID 원칙: 객체지향 설계의 핵심 원칙 소프트웨어 개발에서 유지보수성과 확장성을 높이기 위해서는 올바른 설계 원칙을 따르는 것이 중요하다. SOLID 원칙은 이러한 객체지향 설계를 효과적

claremont.tistory.com

 

 

 

ㅇ개방 폐쇄 원칙(OCP, Open-Closed Principle): 아주 좋은 객체 지향 설계 원칙 중 하나이다

 

- 확장에는 열려 있어야 한다(Open for extension)

  • 소프트웨어 엔티티(클래스, 모듈, 함수 등)는 새로운 기능을 추가하거나 확장할 수 있어야 한다
  • 이 확장은 기존 코드를 변경하지 않고, 새로운 코드 추가를 통해 이루어져야 한다

 

- 변경에는 닫혀 있어야 한다(Closed for modification)

  • 기존의 소프트웨어 엔티티는 변경되지 않아야 한다
  • 즉, 새로운 요구 사항이나 기능이 추가되더라도 기존의 코드를 수정하지 않고 기능을 확장할 수 있어야 한다

 

쉽게 이야기해서 "기존의 코드 수정 없이 새로운 기능을 추가"할 수 있다는 의미다. OCP는 소프트웨어를 보다 유연하게 만들어, 변경 요구에 대한 대응을 용이하게 하며, 코드의 안정성과 재사용성을 높이는 데 중요한 역할을 한다.

 

 

OCP 구현 방법

  • 추상화다형성 사용: OCP는 주로 추상 클래스나 인터페이스와 같은 추상화를 사용하여 구현된다. 새로운 기능을 추가할 때, 기존 클래스를 수정하는 대신 새로운 클래스를 작성하여 추상 클래스를 상속하거나 인터페이스를 구현하는 방식으로 확장한다.
  • 디자인 패턴: 전략 패턴, 데코레이터 패턴, 팩토리 패턴 등 다양한 디자인 패턴이 OCP를 실현하는 데 사용된다. 이러한 패턴은 새로운 기능을 기존 코드에 영향을 주지 않고 추가할 수 있도록 돕는다.

 

 

ㅁ디자인 패턴(Design Pattern)

개발하면서 발생하는 반복적인 문제들을 어떻게 해결할 것인지에 대한 해결 방안으로 실제 현업에서 비즈니스 요구 사항을 프로그래밍으로 처리하면서 만들어진 다양한 해결책 중에서 많은 사람들이 인정한 모범 사례

 

[OCP를 구현하는 디자인 패턴 종류 3가지]

1. 전략 패턴(Strategy Pattern): 행위를 캡슐화하여, 동적으로 행위를 선택할 수 있도록 하는 패턴

즉, 동일한 문제를 해결할 수 있는 여러 알고리즘이나 전략이 존재할 때, 이들을 각각 클래스화하고 필요에 따라 알고리즘을 선택하여 사용할 수 있도록 한다. (알고리즘을 클라이언트 코드의 변경없이 쉽게 교체할 수 있다) 

활용 케이스:

  • 여러 알고리즘 중 하나를 런타임 시점에서 선택해야 할 때
  • 조건문이나 분기문이 많이 사용되어 코드가 복잡해질 때

간단한 활용 예시:

  • 결제 방법(신용카드, PayPal 등)을 전략으로 구현하면, 결제 방식을 클라이언트 코드에서 변경하지 않고도 다른 결제 방법으로 교체할 수 있다

 

2. 데코레이터 패턴(Decorator Pattern): 객체에 추가적인 기능을 동적으로 추가할 수 있는 패턴

https://claremont.tistory.com/entry/%EB%94%94%EC%9E%90%EC%9D%B8-%ED%8C%A8%ED%84%B4-%EB%8D%B0%EC%BD%94%EB%A0%88%EC%9D%B4%ED%84%B0-%ED%8C%A8%ED%84%B4%EA%B3%BC-%ED%81%B4%EB%A1%9C%EC%A0%80-%EA%B8%B0%EB%8A%A5wpython%EA%B3%BC-java-%EA%B4%80%EC%A0%90

 

이 패턴은 상속을 사용하지 않고, 객체를 포장(wrapping)하여 기능을 확장한다. 데코레이터는 원래 객체와 동일한 인터페이스를 구현하고, 클라이언트는 데코레이터 객체를 원래 객체처럼 사용할 수 있다.

 

활용 케이스:

  • 객체의 기능을 동적으로 확장하고자 할 때
  • 상속을 통해 기능을 확장하는 것이 불가능하거나 비효율적일 때

간단한 활용 예시:

  • 커피에 우유나 설탕을 추가하는 데코레이터를 사용하면, 클라이언트 코드를 변경하지 않고도 다양한 조합의 커피를 만들 수 있다.

 

3. 팩토리 패턴(Factory Pattern): 객체 생성의 책임을 특정 팩토리 클래스에 위임하는 디자인 패턴

클라이언트 코드에서 직접 객체를 생성하지 않고, 팩토리 클래스가 객체를 생성하여 반환한다. 이로 인해 객체 생성의 로직을 분리하고 캡슐화할 수 있다.

 

활용 케이스:

  • 객체 생성 과정이 복잡하거나, 생성 과정에서 조건에 따라 다른 클래스의 객체를 반환해야 할 때
  • 생성할 객체의 타입을 런타임 시점에서 결정해야 할 때

간단한 활용 예시:

  • 동물(개, 고양이 등)을 생성하는 팩토리를 사용하면, 클라이언트 코드에서 구체적인 동물 클래스를 알 필요 없이, 팩토리를 통해 동물 객체를 생성하고 사용할 수 있다.

 

 

 

[OCP 잘못된 예시]

class DiscountService {
    public double calculateDiscount(String type, double price) {
        if (type.equals("fixed")) {
            return price - 1000;
        } else if (type.equals("percentage")) {
            return price * 0.9;
        }
        return price;
    }
}

 

문제점

  1. OCP(Open-Closed Principle) 위반
    • calculateDiscount 메서드는 새로운 할인 방식이 추가될 때마다 if-else 문을 수정해야 한다.
    • 이는 기존 코드를 변경해야 하므로 확장에는 닫혀있고, 수정에는 열려있는 구조이다.
  2. 하드코딩된 조건문 사용
    • 문자열("fixed", "percentage")을 비교하는 방식은 가독성이 떨어지고, 오타 발생 시 오류를 찾기 어렵다.
  3. 유지보수 어려움
    • 새로운 할인 정책을 추가하려면 calculateDiscount 메서드를 계속 수정해야 한다.
    • 이는 코드 수정 시 기존 로직에 영향을 줄 수 있으며, 테스트도 어려워진다.

 

[OCP 개선된 예시]

interface DiscountPolicy {
    double applyDiscount(double price);
}

class FixedDiscount implements DiscountPolicy {
    public double applyDiscount(double price) {
        return price - 1000;
    }
}

class PercentageDiscount implements DiscountPolicy {
    public double applyDiscount(double price) {
        return price * 0.9;
    }
}

class DiscountService {
    public double calculateDiscount(DiscountPolicy policy, double price) {
        return policy.applyDiscount(price);
    }
}

개선된 설계의 장점

 OCP(Open-Closed Principle) 준수

  • 새로운 할인 정책이 추가되더라도 기존 코드를 수정할 필요 없이 새로운 DiscountPolicy 구현체를 만들면 된다.

 유지보수성 향상

  • DiscountService는 할인 방식에 대한 세부 사항을 알 필요 없이 DiscountPolicy를 통해 동작한다.
  • 따라서 코드 수정 없이 새로운 할인 정책을 쉽게 추가할 수 있다.

 의존성 주입(DI) 적용

  • DiscountService가 특정 할인 방식에 의존하지 않고 DiscountPolicy 인터페이스를 활용하므로, 코드의 유연성이 증가한다.
  • 테스트 시에도 다양한 할인 정책을 쉽게 모킹(mocking)하여 단위 테스트를 수행할 수 있다.

 가독성과 코드 품질 향상

  • 조건문이 사라지고, 각 할인 정책이 독립적인 클래스로 분리되어 역할이 명확해진다.

 

 

처음 OCP 예시를 보면 조금 어려울 수 있다. 반복적으로 코드를 살펴보고 분석해 보자! 💪