-
객체 지향 프로그래밍카테고리 없음 2024. 11. 12. 17:46
우선 객체 지향 프로그래밍이 어떤 느낌인지 절차적 프로그래밍과 같이 예시를 들어보면 그 차이점이 보인다.
아래 예시는 은행 계좌 관리 시스템을 각각 C(절차적 프로그래밍)와 C++(객체 지향 프로그래밍)으로 구현한 것이다.
1. C (절차적 프로그래밍)
#include <stdio.h> typedef struct { char owner[50]; double balance; } Account; void deposit(Account* acc, double amount) { acc->balance += amount; } void withdraw(Account* acc, double amount) { if (acc->balance >= amount) { acc->balance -= amount; } else { printf("잔액이 부족합니다.\n"); } } void show_balance(Account* acc) { printf("%s님의 잔액은 %.2f원입니다.\n", acc->owner, acc->balance); } int main() { Account myAccount = {"홍길동", 1000.0}; deposit(&myAccount, 500); show_balance(&myAccount); withdraw(&myAccount, 300); show_balance(&myAccount); return 0; }
위 코드에서는 Account라는 구조체와 관련 함수들이 데이터를 처리하는 방식으로 프로그램이 구성된다.
2.C++ (객체 지향 프로그래밍)
#include <iostream> #include <string> class Account { private: std::string owner; double balance; public: Account(const std::string& name, double initial_balance) : owner(name), balance(initial_balance) {} void deposit(double amount) { balance += amount; } void withdraw(double amount) { if (balance >= amount) { balance -= amount; } else { std::cout << "잔액이 부족합니다.\n"; } } void show_balance() const { std::cout << owner << "님의 잔액은 " << balance << "원입니다.\n"; } }; int main() { Account myAccount("홍길동", 1000.0); myAccount.deposit(500); myAccount.show_balance(); myAccount.withdraw(300); myAccount.show_balance(); return 0; }
위 코드에서는 Account가 클래스로 정의되어 데이터(속성)와 이를 다루는 메서드(기능)가 하나의 단위로 묶인다.
음.. 뭔가 보인다. 절차적 프로그래밍에는 클래스 같은게 없어서, 행위와 속성을 하나로 묶을 수 없는 것 같다.
속성을 담는 구조체와 행위를 담는 함수를 따로 작성하다보니, 프로그램이 복잡해지면 이들을 관리하기 어려울 것이다.
반면 객체 지향 프로그래밍에서는 클래스 안에 속성과 행위가 묶여있어, Account의 행위를 Account 안에서 찾을 수 있다.추후에 Account의 행위나 속성이 추가되어 프로그램이 복잡해지더라도, 이를 관리하기가 쉬울 것이다.
그래서 객체지향 프로그래밍이란?
객체 지향 프로그래밍 이란, 프로그램을 여러 '객체'로 구성하고, 객체들의 상호작용으로 프로그램을 동작하도록 만드는 프로그래밍 접근 방식을 말한다.
용어 정리
- 객체(object)
객체는 프로그램 내에서 속성과 행위를 가지는 단위이다.
- 클래스(class)
클래스는 객체를 찍어내는 틀이다. 클래스는 객체가 가지는 속성(데이터)과 행위(메서드)를 정의한다.
- 인스턴스(instance)
인스턴스는 클래스를 통해 생성된 객체를 말한다. Account 클래스를 통해 생성된 myAccount 객체는 인스턴스이다.
- 함수(function)
함수는 특정 작업을 수행하는 독립적인 코드 블록을 말한다. 절차적 프로그래밍에서는 함수를 중심으로 프로그램을 구조화한다.
- 메서드(method)
메서드는 클래스에 소속된 함수를 말한다. 즉, 특정 객체와 연관된 행위를 정의한 함수이다.
그런데 객체 지향 프로그래밍은 어떻게 등장하게 되었나?
그걸 알기 위해서는 객체 지향 프로그래밍이라는 것이 등장하기 이전에 어떤 프로그래밍 패러다임(접근 방법)이 있었는지 알아야한다.
- 순차적 프로그래밍(Sequential Programming)
순차적 프로그래밍은 말 그대로 프로그램이 위에서 아래로 순서대로 실행되는 프로그래밍 방식이다. 이렇게 코드를 짜면 흐름이 매우 직관적으로 보인다. 이러한 방식은 이해하고 구현하기는 쉽지만, 복잡한 문제를 해결하거나 코드를 재사용하는 데 한계가 있었다.
그래서 절차적 프로그래밍이 등장했다.
- 절차적 프로그래밍(Procedural Programming)
절차적 프로그래밍은 절차적으로 실행하는 프로그래밍 방식을 의미하는게 아니고, '프로시저(Procedure)'를 이용한 프로그래밍 방식을 말한다. 프로시저란 특정 작업을 수행하기 위해 독립적으로 정의된 코드의 집합을 말한다. 프로시저를 간단히 말하면 함수다. 그러면 절차적 프로그래밍은 함수를 이용한 프로그래밍 방식이네요!
절차적 프로그래밍은 반복 동작을 함수로 모듈화하여 코드를 재사용할 수 있도록 만들었다. 그러나 절차적 프로그래밍에도 한계점이 있었는데, 반복 동작을 함수로 나타낸다고 해도, 이 함수가 어떤 데이터와 어떻게 연관되는지는 명확하지 않다는 것이다.
무슨 말인지 이해하기 위해 위에서 작성했던 은행 계좌 관리 시스템의 예시를 살펴보자.
절차적 프로그래밍으로 이를 구현하면 아래처럼 될 것이다.
- '계좌'라는 자료형을 구현(소유자, 잔액, ...)
- '계좌'에 대한 '함수'를 구현(입금, 출금, ...)
그런데 절차적 프로그래밍에서 계좌와 계좌에 관한 함수는 서로 묶여있지 않아서, 둘의 연관 여부는 단 번에 알아차리기 어렵다. 그래서 계좌에 관한 함수가 있더라도, 이게 계좌와 연관된 행동인지는 명확하지 않다. 이렇다 보니 프로그램이 복잡해지면, 어떤 함수가 어떤 데이터와 연관되어있는지 파악하기 어려워진다.
그래서 이를 묶기 위해서 객체 지향 프로그래밍이 등장했다.
*모듈화: 프로그램을 독립적이고 재사용 가능한 작은 단위(모듈)로 나누는 것.
- 객체 지향 프로그래밍(Object-Oriented Programming)
어떤 개념에 대한 속성과 행동을 '객체' 형태로 묶어서 함께 관리하기 위해 객체 지향 프로그래밍 접근 방식이 등장했다. 핵심은 객체의 내부에 속성(데이터)과 행동(메서드)이 함께 존재한다는 것이다. 객체 지향으로 프로그램을 구현하면 객체의 속성과 행동을 객체에 묶어서 관리하는 것이 가능해진다. 추상적이었던 동작이 직관적으로 보이고, 코드 가독성이 증가한다.
이처럼 프로그램의 데이터와 이를 처리하는 메서드를 객체 단위로 묶어서, 추상적인 동작을 보다 직관적으로 관리하기 위해 객체 지향 프로그래밍 패러다임이 등장했다.
더 간단하게는, 데이터와 메서드를 객체로 묶어서 직관적으로 관리하기 위해 객체 지향 프로그래밍이 등장했다.
객체 지향 프로그래밍은 어떤 장점을 가지는가?
이거를 설명하면서 자연스럽게 객체 지향 프로그램의 특징 4가지가 나온다.
- 데이터 보호, 캡슐화(Encapsulation)
캡슐화를 통해 객체의 데이터와 이를 처리하는 메서드를 묶어서 외부에서 객체의 내부 상태를 변경하지 못하도록 은닉/보호할 수 있다. 이는 코드의 안정성, 일관성을 높이고 객체 내부의 변화를 최소화하고 관리하기 쉽게 만든다.
캡슐화(Encapsulation)는 객체 지향 프로그래밍의 특징 중 하나로, 데이터와 메서드를 하나의 단위(객체)로 묶고, 외부에서는 접근할 수 없도록 보호하여 객체 내부의 데이터를 안전하게 관리하는 기법이다.
캡슐화의 장점을 이해하기 위해 예시를 들어보자. 아래는 C#으로 작성된 캡슐화를 적용한 은행 계좌 관리 시스템이다.
public class BankAccount { private double balance; // 잔액이 private으로 보호됨 public double GetBalance() { return balance; } public void Deposit(double amount) { if (amount > 0) { balance += amount; } else { Console.WriteLine("입금액은 0보다 커야 합니다."); } } public void Withdraw(double amount) { if (amount > 0 && amount <= balance) { balance -= amount; } else { Console.WriteLine("출금할 수 없습니다. 잔액이 부족하거나 잘못된 금액입니다."); } } }
balance는 private로 설정되어 외부에서 직접 접근할 수 없다. 대신 Deposit과 Withdraw 메서드를 통해서만 잔액을 변경할 수 있다. 이로 인해 다음과 같은 장점이 생긴다.
캡슐화가 주는 장점
- 객체 내부의 데이터를 보호 → 객체간 결합도를 낮춤
외부에서 balance에 직접 접근하지 못하게 하여 잘못된 값이 저장되는 것을 방지하고, Withdraw와 Deposit에서 출금 금액과 입금 금액을 제한하여 잘못된 입력을 방지한다.
- 코드의 일관성 증가 → 관리가 쉬워짐.
balance를 변경하는 모든 논리는 Withdraw와 Deposit 메서드에 캡슐화되어 있어, 잔액을 변경하는 로직을 일관되게 관리할 수 있다. → 만약 잔액 변경 로직을 수정해야 하면, 해당 메서드 내에서만 수정하면 된다.
- 재사용성이 높아짐, 상속(Inheritance)
상속과 같은 기능을 통해 기존에 작성한 클래스를 확장하여 재사용할 수 있다. 이는 코드의 중복을 줄이고, 유지보수를 용이해지고, 확장성이 높아진다.
상속(Inheritance)은 객체 지향 프로그래밍의 특징 중 하나로, 기존에 정의된 클래스의 특성과 동작을 자식 클래스가 물려받아 재사용하거나 확장하는 것을 의미한다.
상속을 사용한 장점을 알아보기 위해 예시를 들어보자.
아래는 상속을 사용하지 않은 경우와 상속을 사용한 경우의 동물 관리 시스템이다.
1. 상속을 사용하지 않은 경우
public class Dog { public string Name; public void MakeSound() { Console.WriteLine("멍멍"); } public void Fetch() { Console.WriteLine($"{Name}가 공을 가져옵니다."); } } public class Cat { public string Name; public void MakeSound() { Console.WriteLine("야옹"); } }
- 위와 같이 작성하면 Dog와 Cat 클래스에 공통적으로 Name과 MakeSound 메서드가 존재하지만, 각각 별도로 정의되어 있다.새로운 동물 종류를 추가할 때마다 공통 속성과 메서드를 계속 중복해서 작성해야 한다.
2. 상속을 사용한 경우
public class Animal { public string Name; public virtual void MakeSound() { Console.WriteLine("기본 동물 소리"); } } public class Dog : Animal { public override void MakeSound() { Console.WriteLine("멍멍"); } public void Fetch() { Console.WriteLine($"{Name}가 공을 가져옵니다."); } } public class Cat : Animal { public override void MakeSound() { Console.WriteLine("야옹"); } }
상속이 주는 장점
- 코드의 중복 감소, 재사용성이 높아짐
Animal이라는 부모 클래스를 정의하고, 모든 동물이 공통으로 가질 속성(Name)과 메서드(MakeSound)를 이 클래스에 구현한다. Dog와 Cat 클래스는 Animal을 상속받아 공통된 속성과 메서드를 공유할 수 있다. 새로운 동물을 추가할 때 Animal을 상속받기만 하면 되므로, 공통 기능을 중복으로 작성할 필요가 없다.
- 공통 기능에 대한 유지보수가 용이해짐
공통된 기능을 Animal 클래스에서 관리하기 때문에, 만약 MakeSound 메서드의 기본 동작을 수정하고 싶다면 Animal 클래스에서만 수정하면 된다. Dog와 Cat 클래스는 별도로 수정할 필요가 없다.
- 확장성이 높아짐, 다형성(Polymorphism)
다형성을 통해 하나의 인터페이스에 대해 여러 형태로 동작할 수 있는 능력을 제공한다. 이를 통해 유연하고 확장 가능한 코드를 작성할 수 있다.
다형성(Polymorphism)은 객체지향 프로그래밍의 특징 중 하나로, 하나의 인터페이스나 메서드가 다양한 방식으로 동작할 수 있도록 하는 특성을 말한다.
다형성의 대표적인 예시로 메서드 오버라이딩(Method Overriding)과 인터페이스를 활용한 다형성을 들 수 있다.
기본 클래스와 상속 구조
public class Animal { // virtual 키워드를 사용하여 자식 클래스가 이 메서드를 재정의할 수 있도록 함 public virtual void MakeSound() { Console.WriteLine("기본 동물 소리"); } } public class Dog : Animal { // override 키워드를 사용하여 부모 클래스의 메서드를 재정의함 public override void MakeSound() { Console.WriteLine("멍멍"); } } public class Cat : Animal { public override void MakeSound() { Console.WriteLine("야옹"); } }
다형성을 활용한 코드
public class Program { public static void Main(string[] args) { // 부모 타입의 리스트에 다양한 자식 객체를 저장함 List<Animal> animals = new List<Animal> { new Dog(), new Cat(), new Animal() // 기본 Animal 객체 }; // 다형성의 예시 - 부모 클래스 타입으로 자식 메서드를 호출 foreach (var animal in animals) { animal.MakeSound(); // 각 클래스의 재정의된 메서드가 호출됨 } } }
실행결과
멍멍 야옹 기본 동물 소리
다형성이 주는 장점
- 하나의 인터페이스가 다양한 형태로 동작
foreach 루프에서 animal.MakeSound()를 호출할 때, 실제로 실행되는 메서드는 해당 객체의 타입에 따라 달라진다. Dog 객체에서는 "멍멍"이, Cat 객체에서는 "야옹"이 출력된다. 이는 런타임에 결정되므로, 동일한 인터페이스를 통해 서로 다른 동작을 수행할 수 있게 된다.
- 확장성이 높아짐
새로운 동물 종류를 추가할 때 Animal을 상속받아 필요한 메서드를 재정의(오버라이딩)하거나 새로운 메서드를 추가하기만 하면 된다. 예를 들어, Bird라는 클래스를 추가하려면 간단히 Animal을 상속하고 MakeSound 메서드를 오버라이드하거나 Fly 메서드와 같은 새로운 기능을 추가할 수 있다.
*다형성에서도 상속이라는 말이 나오는데 둘은 어떻게 다른가요?
상속은 부모 자식간에 공통 기능을 공유하고 재사용하는 것에 초점이 맞춰져 있고, 다형성은 상속을 통해 정의된 공통 메서드를 다양한 방식으로 재정의하고, 이를 하나의 참조타입(부모 클래스 혹은 인터페이스)로 일관되게 다루는 것에 중점을 둔다.
- 복잡성을 줄이고 명확해짐, 추상화(Abstraction)
추상화를 통해 코드의 복잡성을 줄이고, 명확한 인터페이스를 제공하여 사용자/개발자가 코드를 사용할 때 쉽게 다룰 수 있게 만들어준다.
추상화(Abstraction)는 객체 지향 프로그래밍의 특징 중 하나로, 불필요한 세부 구현을 숨기고 중요한 부분만 노출하여 코드를 쉽게 이해하고 사용할 수 있도록 돕는 것을 말한다.
추상화를 구체적으로 이해하기 위해 추상 클래스를 활용하여 자동차 클래스를 만든 예시를 보자.
1. 추상화가 없는 경우
public class Car { public void StartEngine() { Console.WriteLine("엔진이 시동되었습니다."); } public void Drive() { Console.WriteLine("자동차가 주행합니다."); } public void Stop() { Console.WriteLine("자동차가 멈췄습니다."); } // 구체적인 세부 구현이 포함됨 public void TurnOnRadio() { Console.WriteLine("라디오를 켭니다."); } }
위 예시에서 Car 클래스는 구체적인 동작을 모두 포함하고 있다. 하지만 다른 유형의 차를 추가하고 이와 관련된 기능을 추가하려면 기존 코드를 수정해야 할 가능성이 높다. 세부 구현이 노출되면 사용자는 이러한 모든 메서드에 접근할 수 있으며, 불필요한 복잡성이 발생할 수 있다.
2. 추상화를 적용한 경우
public abstract class Vehicle { // 핵심적인 인터페이스만 정의 (추상 메서드) public abstract void StartEngine(); public abstract void Drive(); public abstract void Stop(); } public class Car : Vehicle { public override void StartEngine() { Console.WriteLine("자동차 엔진이 시동되었습니다."); } public override void Drive() { Console.WriteLine("자동차가 주행합니다."); } public override void Stop() { Console.WriteLine("자동차가 멈췄습니다."); } } public class Truck : Vehicle { public override void StartEngine() { Console.WriteLine("트럭 엔진이 시동되었습니다."); } public override void Drive() { Console.WriteLine("트럭이 주행합니다."); } public override void Stop() { Console.WriteLine("트럭이 멈췄습니다."); } }
추상화가 주는 장점
- 불필요한 세부 구현 숨기기
Vehicle이라는 추상 클래스는 StartEngine(), Drive(), Stop()과 같은 핵심 동작을 정의한다. 이 추상 클래스는 구체적인 구현이 아닌 메서드의 인터페이스만 제공하기 때문에, 구체적인 동작은 자식 클래스(Car와 Truck)에서 정의된다. 외부에서는 Vehicle의 공통된 인터페이스만을 알면 되므로, 구체적인 세부 동작에 신경 쓰지 않고도 Car나 Truck을 다룰 수 있다.
- 명확한 인터페이스 제공 → 동일 인터페이스로 다양한 동작 사용
Vehicle 클래스가 정의하는 추상 메서드는 모든 하위 클래스가 반드시 구현해야 하는 메서드이므로, 사용자는 모든 Vehicle 객체가 동일한 방식으로 동작한다는 보장을 받을 수 있다. 즉, 코드를 사용하는 입장에서 Vehicle 객체에 대해 StartEngine(), Drive(), Stop()을 호출하면 어떤 구체적인 동작이든 동일한 인터페이스를 통해 사용할 수 있다.
그래서 종합적으로 객체 지향 프로그래밍이 왜 좋냐면..
객체 지향 프로그래밍의 장점은 추상화, 캡슐화, 상속, 다형성이라는 특징들에서 비롯된다. 이들 특징은 프로그램을 구조화하고, 재사용성과 가독성을 높이며, 유지보수를 편리하게 하고, 확장성을 제공하는데 기여한다.
그런데 객체 지향 프로그래밍, 단점도 있지 않을까?
- 설계하는데 시간이 오래 걸림
- 단순한 프로그램을 만드는 경우 오히려 복잡해짐
- 객체간 상호작용이 많아지면 처리속도가 저하됨
참고