Welcome! Everything is fine.

#05. 객체 지향 프로그래밍(OOP)의 개념 / 특징 / 설계 원칙 본문

CS 스터디

#05. 객체 지향 프로그래밍(OOP)의 개념 / 특징 / 설계 원칙

개발곰발 2024. 1. 30.
728x90

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

구글에 '객체 지향 프로그래밍'이라고 검색 하면 다음과 같은 정의가 나온다.

객체 지향 프로그래밍은 컴퓨터 프로그래밍의 패러다임 중 하나이다. 객체 지향 프로그래밍은 컴퓨터 프로그램을 명령어의 목록으로 보는 시각에서 벗어나 여러 개의 독립된 단위, 즉 "객체"들의 모임으로 파악하고자 하는 것이다. 각각의 객체는 메시지를 주고받고, 데이터를 처리할 수 있다.

 

여기서 객체는 현실 세계에 있는 모든 독립된 사물을 객체라고 보고, 그 객체에서 필요한 속성을 가져와 프로그래밍하는 것을 말한다. 객체 지향 개념에서 자주 나오는 붕어빵 예시로 말하자면, 붕어빵을 하나하나 모양잡아 구워내는 것보다 붕어빵 틀을 사용해 빠르고 편리하게 구울 수 있다는 것이다. 객체 지향 설계는 상대적으로 설계가 복잡하고 처리 속도가 느리지만, 코드 재사용성이 증가하고 유지보수가 쉬워진다는 장점이 있다.

객체 지향 프로그래밍의 특징

객체 지향 프로그래밍의 특징에는 추상화, 캡슐화, 상속, 다형성이 있다. ChatGPT가 짜준 에시 코드와 함께 알아보자!

추상화

불필요한 정보를 빼고 핵심적인 기능과 속성만을 간추려 표현하는 것을 말한다. 추상 클래스와 인터페이스를 통해 추상화를 구현할 수 있다. 추상화를 통해 사용자는 세부 구현 사항을 몰라도 객체를 사용할 수 있다. 아래 예시 코드에서는 Shape라는 추상 클래스를 선언해놓고 Circle과 Square에서 draw()를 사용하고 있다.

// 추상화를 활용한 예제
abstract class Shape {
    abstract void draw(); // 추상 메서드
}

class Circle extends Shape {
    @Override
    void draw() {
        System.out.println("원을 그립니다.");
    }
}

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

// 사용 예제
public class AbstractionExample {
    public static void main(String[] args) {
        Shape circle = new Circle();
        Shape square = new Square();

        circle.draw(); // 동적 바인딩에 의해 Circle 클래스의 draw() 호출
        square.draw(); // 동적 바인딩에 의해 Square 클래스의 draw() 호출
    }
}

캡슐화

객체의 속성(데이터)과 행동(메서드)을 하나로 묶어서 외부에서의 접근을 제어하는 개념이다. 객체의 내부 구현을 외부로부터 감추고, 객체 간 인터페이스를 통해서만 상호작용할 수 있게 한다. 코드의 수정 없이 재활용하는 것을 목적으로 한다. 아래 예시 코드에서는 balance를 private으로 선언하여 외부에서의 직접적인 접근을 막고, public으로 선언된 메서드를 통해 잔약을 증가시키거나 감소시키고, 조회할 수 있다. 이로써 정보 은닉과 캡슐화가 이루어져 외부에서 불필요한 접근을 막으면서 객체의 상태를 조작할 수 있다.

// 캡슐화를 활용한 예제
class BankAccount {
    private double balance; // private으로 선언된 멤버 변수

    // public 메서드를 통해 접근 가능
    public void deposit(double amount) {
        if (amount > 0) {
            balance += amount;
            System.out.println(amount + "원을 입금했습니다.");
        }
    }

    public void withdraw(double amount) {
        if (amount > 0 && balance >= amount) {
            balance -= amount;
            System.out.println(amount + "원을 출금했습니다.");
        } else {
            System.out.println("잔액이 부족하거나 유효하지 않은 금액입니다.");
        }
    }

    public double getBalance() {
        return balance;
    }
}

// 사용 예제
public class EncapsulationExample {
    public static void main(String[] args) {
        BankAccount account = new BankAccount();
        account.deposit(1000);
        account.withdraw(500);
        System.out.println("잔액: " + account.getBalance() + "원");
    }
}

상속

부모 클래스의 특성을 자식 클래스가 물려받는 개념이다.자식 클래스에서는 부모 클래스의 모든 속성과 메서드를 상속받아 사용할 수 있다. 아래 예시 코드에서는 Dog 클래스가 Animal이라는 부모클래스를 상속받고 있다. Animal 클래스에 공통 로직을 정의하고, 이 클래스는 Dog 말고도 다양한 동물 클래스에서 활용될 수 있어 코드 재사용성이 증가한다. 또한 계층 구조를 통해 객체 간의 관계를 명확하게 표현할 수 있다는 장점이 있다.

// 부모 클래스
class Animal {
    void eat() {
        System.out.println("동물이 먹습니다.");
    }
}

// 자식 클래스
class Dog extends Animal {
    void bark() {
        System.out.println("개가 짖습니다.");
    }
}

// 사용 예제
public class InheritanceExample {
    public static void main(String[] args) {
        Dog myDog = new Dog();
        myDog.eat();  // 부모 클래스의 메서드 호출
        myDog.bark(); // 자식 클래스의 메서드 호출
    }
}

다형성

하나의 인터페이스나 기능을 다양한 방식으로 구현할 수 있는 능력을 말한다. 하나의 변수명이나 함수명이 상황에 따라 다르게 해석될 수 있다. 메서드 오버로딩(Overloading)과 메서드 오버라이딩(Overriding)을 통해 구현한다.

// 다형성을 활용한 예제
class Animal {
    void makeSound() {
        System.out.println("동물 소리를 낸다.");
    }
}

class Dog extends Animal {
    @Override
    void makeSound() {
        System.out.println("개가 짖습니다.");
    }
}

class Cat extends Animal {
    @Override
    void makeSound() {
        System.out.println("고양이가 야옹합니다.");
    }
}

// 사용 예제
public class PolymorphismExample {
    public static void main(String[] args) {
        // 다형성을 활용하여 각 객체를 부모 클래스 타입으로 참조
        Animal myDog = new Dog();
        Animal myCat = new Cat();

        // 동적 바인딩에 의해 실제 객체의 메서드가 호출됨
        myDog.makeSound();  // "개가 짖습니다." 출력
        myCat.makeSound();  // "고양이가 야옹합니다." 출력
    }
}

객체 지향 프로그래밍의 장단점

장점

  • 코드 재사용성
  • 유지보수성
  • 확장성
  • 보안 강화

단점

  • 성능 저하
  • 초기 학습이 어려움 
  • 자원 소모
  • 복잡성, 클래스 및 객체 관리 어려움

객체 지향 설계 원칙(SOLID)

객체 지향 설계에서는 SOLID라고 부르는 5가지의 설계 원칙이 존재한다.

SRP - 단일 책임 원칙 (Single Responsibility Principle)

클래스는 하나의 책임(목적)만을 가져야 하며, 그 책임을 변경하는 이유는 단 하나여야한다.

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

소프트웨어 개체(클래스, 모듈, 함수 등)는 확장에는 열려 있어야 하고, 수정에는 닫혀 있어야 한다. 클래스를 수정해야 한다면 그  클래스를 상속하여 수정한다.
LSP - 리스코프 치환 원칙 (Liskov Substitution Principle)

하위 타입은 상위 타입을 대체할 수 있어야 한다. 즉, 부모 클래스 객체를 대체하더라도 프로그램의 의미나 동작은 변하지 않아야 한다.

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

클라이언트는 자신이 사용하지 않는 메서드에 의존 관계를 맺으면 안된다.

DIP - 의존성 역전 원칙 (Dependency Inversion Principle)

고수준 모듈은 저수준 모듈에 의존해서는 안 되며, 둘 모두 추상화에 의존해야한다.