Post

[SE] 객체지향프로그래밍(OOP) 개념과 SOLID 원칙

객체지향 프로그래밍(Object-Oriented Programming, OOP)

프로그램을 객체들의 집합으로 모델링하여 설계하고 구현하는 방식

객체(Object): 데이터와 이 데이터를 처리하는 데 필요한 함수(메소드)를 포함하는 소프트웨어 블록

OOP는 프로그램의 재사용성, 유지보수성, 투명성을 향상시킨다.

OOP의 주요 개념

  1. 클래스(Class)
    • 같은 종류의 객체를 생성하기 위한 템플릿
    • 클래스는 객체의 데이터 형태와 사용할 수 있는 메소드를 정의한다.
  2. 객체(Object)
    • 클래스에 정의된 속성(데이터)과 기능(메소드)을 실제로 가지고 있는 실체
    • 클래스의 인스턴스라고도 한다.
  3. 상속(Inheritance)
    • 한 클래스가 다른 클래스의 속성과 메소드를 상속받아 사용할 수 있게 하는 것
    • 코드 재사용성을 높이고 중복을 줄이는 데 유용하다.
  4. 다형성(Polymorphism)
    • 하나의 인터페이스나 메소드가 다양한 방식으로 작동할 수 있게 하는 것
    • 메소드 오버로딩(같은 이름의 메소드가 다른 파라미터를 가질 수 있음)과 메소드 오버라이딩(자식 클래스가 부모 클래스의 메소드를 재정의할 수 있음)을 통해 구현된다.
  5. 캡슐화(Encapsulation)
    • 객체의 데이터(속성)와 메소드를 결합하여, 데이터의 접근을 제어하는 메커니즘
    • 데이터를 숨기고(정보 은닉), 외부에서 직접적인 접근을 제한한다.

SOLID 원칙

객체지향 설계를 할 때 따라야 할 다섯 가지 기본 원칙

이 원칙들은 소프트웨어 설계를 더 이해하기 쉽고, 유연하며, 유지보수가 쉬운 코드로 만들어준다.

  1. 단일 책임 원칙(Single Responsibility Principle, SRP)

    • 한 클래스는 하나의 책임만 가져야 한다.
    • 클래스가 변경되어야 하는 이유는 하나여야 한다.
  2. 개방-폐쇄 원칙(Open-Closed Principle, OCP)

    • 소프트웨어 엔티티(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하지만, 변경에는 닫혀 있어야 한다.
    • 소프트웨어의 기능을 변경하거나 확장할 때, 기존의 코드를 변경하지 않으면서도 기능의 추가나 변경이 가능해야 한다.
    • 예를 들어, 새로운 기능을 추가하기 위해 이미 검증된 클래스를 수정하기보다 해당 클래스를 확장하여 새로운 기능을 구현한다.
  3. 리스코프 치환 원칙(Liskov Substitution Principle, LSP)

    • 서브타입은 언제나 기반 타입으로 교체할 수 있어야 한다.
    • 즉, 특정 클래스를 사용하는 코드를 변경하지 않고도 해당 클래스의 서브 클래스로 교체할 수 있어야 한다.
    • 서브클래스는 기반 클래스의 행동 규약을 모두 준수해야 하며, 서브클래스의 객체가 기반 클래스의 객체로 예상되는 모든 곳에서 사용될 수 있어야 한다.(서브클래스가 부모 클래스를 완전히 대체할 수 있어야 한다.)

    • 예시: 직사각형과 정사각형
      • 직사각형 클래스가 있으며, 이 클래스를 상속받아 정사각형 클래스를 만든다고 가정한다. 직사각형 클래스에는 너비(width)와 높이(height) 속성이 있고, 이 두 속성을 독립적으로 설정할 수 있다.
      • 정사각형에서는 너비와 높이가 항상 같아야 하므로, 직사각형 클래스의 세터 메소드(setter)를 그대로 상속받으면 문제가 발생한다.
      • 이 상황에서 정사각형 클래스는 직사각형 클래스의 대체가 될 수 없다. 직사각형을 사용하는 코드에 정사각형 객체를 넣어도 동일하게 작동하지 않기 때문이다. 이 예시는 LSP 원칙을 위반한다.
  4. 인터페이스 분리 원칙(Interface Segregation Principle, ISP)

    • 한 클래스는 자신이 사용하지 않는 인터페이스는 구현하지 않아야 한다.
    • 클라이언트는 자신이 사용하지 않는 메소드에 의존하지 않아야 한다.
    • 큰 단일 인터페이스보다는 구체적인 여러 개의 인터페이스가 낫다는 것을 의미힌다.
    • 클래스가 자신이 사용하지 않는 인터페이스를 구현하도록 강제하는 대신, 필요한 기능만을 포함하는 인터페이스를 여러 개 정의하여, 각 클래스가 필요한 인터페이스만을 구현하도록 한다.
    • 예시: 다기능 프린터
      • 다기능 프린터는 인쇄, 스캔, 팩스 보내기 등 여러 기능을 가질 수 있다. 초기 설계에서는 모든 기능을 포함하는 하나의 인터페이스 IMultiFunctionDevice를 가정할 수 있는데, 이 인터페이스에는 print(), scan(), fax() 메서드가 포함되어 있다.
      • 그런데 특정 프린터 모델은 인쇄 기능만 제공하고 스캔이나 팩스 기능은 제공하지 않을 수 있다. 하지만 IMultiFunctionDevice 인터페이스를 구현하는 경우 모든 메서드를 구현해야 하므로, 사용하지 않는 기능에 대한 메서드도 빈 구현을 제공해야 한다. 이는 ISP 원칙에 위배된다.
      • 인터페이스를 기능별로 분리하여 IPrinter, IScanner, IFax와 같은 인터페이스를 생성하고, 각각의 인터페이스는 해당 기능과 관련된 메서드만을 포함함으로써 인터페이스 분리 원칙을 준수할 수 있다.
  5. 의존성 역전 원칙(Dependency Inversion Principle, DIP)

    • 고수준 모듈은 저수준 모듈에 의존하면 안 되며, 둘 다 추상화에 의존해야 한다.
    • 전통적인 프로그래밍에서 보이는 고수준 모듈이 저수준 모듈에 의존하는 구조를 뒤집어, 두 모듈 모두 추상화에 의존하게 만드는 설계 원칙이다.
    • 구체적인 클래스보다는 인터페이스나 추상 클래스와 같은 추상화된 것에 의존하라는 뜻으로, 세부 사항이 추상화에 의존하게 하여, 코드의 결합도를 줄이고 확장성과 유연성을 높인다.
    • 예시: 메시지 전송 시스템
      • EmailSender와 SmsSender라는 두 개의 저수준 모듈이 있으며, Notification이라는 고수준 모듈이 이 두 모듈에 직접 의존하고 있다.
      • Notification 모듈이 EmailSender와 SmsSender에 직접 의존하게 되면, 새로운 메시지 전송 방법(예: Slack 메시지)을 추가하려 할 때 Notification 모듈도 변경해야 한다. 이는 DIP 원칙에 위배되며, 유연성과 확장성에 문제를 일으킨다.
      • DIP 적용하기 위해서는 먼저, 모든 메시지 전송 클래스가 구현해야 하는 IMessageSender 인터페이스를 정의한다. 이 인터페이스는 메시지를 보내는 행위(sendMessage)를 추상화한다. 저수준 모듈인 EmailSender와 SmsSender 클래스가 IMessageSender 인터페이스를 구현하도록 조정하고, 고수준 모듈인 Notification 클래스가 구체적인 메시지 전송 방법(이메일, SMS) 대신 IMessageSender 인터페이스에 의존하도록 변경한다. 이로써, Notification은 더 이상 저수준 모듈에 의존하지 않게 된다.
This post is licensed under CC BY 4.0 by the author.