컴퓨터공학

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

nyambu 2025. 3. 17. 17:00

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

1. 객체지향 프로그래밍(OOP)란?

 객체지향 프로그래밍(Object-Oriented Programming, OOP)은 **객체(Object)**를 중심으로 프로그램을 구성하는 프로그래밍 패러다임이다. 객체란 데이터(속성, 상태)와 메서드(동작, 행동)를 포함하는 독립적인 모듈을 의미하며, 객체 간의 관계를 통해 프로그램이 동작한다.

 

📌 OOP의 핵심 개념

 객체지향 프로그래밍은 다음과 같은 4가지 개념을 기반으로 한다.

  1. 캡슐화(Encapsulation)
    • 객체의 데이터를 외부에서 직접 접근하지 못하도록 보호하고, 필요한 경우 메서드를 통해 접근을 제한하는 개념
    • **정보 은닉(Information Hiding)**을 통해 유지보수성과 보안성을 높일 수 있음
    • 예제: private, protected 접근 제어자를 사용하여 클래스 내부 데이터 보호
  2. 추상화(Abstraction)
    • 객체의 핵심적인 속성과 동작만을 표현하고, 불필요한 세부 사항은 숨기는 개념
    • 추상 클래스(Abstract Class)와 인터페이스(Interface)를 활용하여 설계 가능
    • 예제: Car 클래스를 정의할 때, "차량의 브랜드", "타이어 개수" 등의 핵심 속성만 포함하고 내부 엔진 구조는 숨김
  3. 상속(Inheritance)
    • 기존 클래스를 확장하여 새로운 클래스를 만들 수 있는 기능
    • 코드의 재사용성 증가, 유지보수 용이
    • 예제: class Dog extends Animal → Dog 클래스가 Animal 클래스를 상속받아 "걷는다", "먹는다" 등의 기능을 공유
  4. 다형성(Polymorphism)
    • 같은 인터페이스나 부모 클래스를 상속받은 객체가 서로 다른 방식으로 동작할 수 있는 기능
    • 오버로딩(Overloading): 같은 이름의 메서드를 매개변수 형태에 따라 다르게 사용
    • 오버라이딩(Overriding): 부모 클래스의 메서드를 자식 클래스에서 재정의하여 사용하는 방식
    • 예제: Animal 클래스에서 sound() 메서드를 정의하고, Dog 클래스와 Cat 클래스에서 각각 "멍멍", "야옹" 출력하도록 오버라이딩

2. OOP의 장점과 한계

1) OOP의 장점

  • 코드 재사용성 증가 → 상속을 통해 기존 코드를 재사용 가능
  • 유지보수 용이 → 코드가 모듈화되어 있어 수정이 쉬움
  • 확장성 증가 → 새로운 기능을 추가하기 쉬움
  • 데이터 보안 강화 → 캡슐화를 통해 외부 접근을 제한

2) OOP의 한계

  • 설계가 어렵고 시간이 많이 걸림
  • 객체 간의 관계가 많아지면 복잡도가 증가할 수 있음
  • 성능 오버헤드 발생 가능 (객체 생성 및 메모리 관리 부담)

📌 결론: OOP는 유지보수성과 확장성이 뛰어나지만, 올바른 설계가 필수적이다. 이 문제를 해결하기 위해 SOLID 원칙이 등장하였다.


3. SOLID 원칙이란?

 SOLID 원칙은 객체지향 설계를 보다 효과적으로 하기 위한 5가지 원칙을 의미한다. 이 원칙을 따르면 유지보수성이 높고 확장성이 뛰어난 코드 작성이 가능하다.

 

📌 SOLID 원칙 5가지

원칙
설명
S: 단일 책임 원칙 (SRP) 클래스는 단 하나의 책임만 가져야 한다.
O: 개방-폐쇄 원칙 (OCP) 기존 코드를 변경하지 않고 기능을 확장할 수 있어야 한다.
L: 리스코프 치환 원칙 (LSP) 자식 클래스는 부모 클래스의 기능을 변경 없이 확장해야 한다.
I: 인터페이스 분리 원칙 (ISP) 인터페이스는 특정 기능에 맞게 분리하여 설계해야 한다.
D: 의존 역전 원칙 (DIP) 구체적인 구현이 아닌, 추상적인 개념에 의존해야 한다.

4. SOLID 원칙 상세 설명

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

  • 클래스는 하나의 기능만 담당해야 하며, 하나의 변경 이유만 가져야 함
  • 여러 개의 책임을 가지면 유지보수가 어려워지고, 코드 수정 시 예상치 못한 오류가 발생할 가능성이 높아짐

 잘못된 예시 (책임이 여러 개)

class Report {
    public void generateReport() { /* 보고서 생성 */ }
    public void printReport() { /* 보고서 출력 */ }
}

 

올바른 예시 (책임을 분리)

class ReportGenerator {
    public void generateReport() { /* 보고서 생성 */ }
}
class ReportPrinter {
    public void printReport() { /* 보고서 출력 */ }
}

 

4-2. 개방-폐쇄 원칙(Open-Closed Principle, OCP)

  • 확장은 가능하지만, 기존 코드는 변경하지 않아야 함
  • 기존 기능을 수정하지 않고도 새로운 기능을 추가할 수 있어야 함

 올바른 예시 (추상 클래스를 사용하여 확장 가능하도록 설계)

abstract class Payment {
    abstract void pay();
}

class CreditCardPayment extends Payment {
    void pay() { System.out.println("신용카드 결제"); }
}

class PayPalPayment extends Payment {
    void pay() { System.out.println("PayPal 결제"); }
}

 

4-3. 리스코프 치환 원칙(Liskov Substitution Principle, LSP)

  • 자식 클래스는 부모 클래스를 대체할 수 있어야 한다.
  • 자식 클래스가 부모 클래스의 기능을 변경하면 안 됨

❌ 잘못된 예시 (자식 클래스가 부모의 기능을 변경함)

class Rectangle {
    int width, height;
    void setWidth(int w) { width = w; }
    void setHeight(int h) { height = h; }
}

class Square extends Rectangle {
    void setWidth(int w) { width = height = w; }  // 부모 클래스의 기능을 변경
}

 

4-4. 인터페이스 분리 원칙(Interface Segregation Principle, ISP)

  • 클라이언트가 사용하지 않는 기능을 강요하지 않아야 한다.
  • 인터페이스를 기능별로 나누어 최소한의 기능만 포함해야 한다.

 잘못된 예시 (불필요한 메서드 포함)

interface Worker {
    void work();
    void eat();  // 모든 Worker가 식사하는 기능이 필요하지 않을 수도 있음
}

 

올바른 예시 (인터페이스를 분리함)

interface Workable {
    void work();
}

interface Eatable {
    void eat();
}

 

4-5. 의존 역전 원칙(Dependency Inversion Principle, DIP)

  • 구체적인 구현이 아닌, 추상적인 인터페이스에 의존해야 함
  • 상위 모듈이 하위 모듈에 의존하지 않고, 둘 다 추상화된 개념에 의존해야 한다.

 올바른 예시 (인터페이스를 활용하여 유연한 구조 설계)

interface Database {
    void connect();
}

class MySQLDatabase implements Database {
    public void connect() { System.out.println("MySQL 연결"); }
}

class Application {
    private Database db;
    Application(Database db) { this.db = db; }
}