3월 25일 #객체지향 프로그래밍(OOP)

sejun-Lee·2025년 3월 26일

객체지향 프로그래밍(OOP)


프로그래밍을 처음 접할 때 "객체지향" 이라는 말을 들으면 뭔가 복잡하고 어려운 개념처럼 느껴질 수 있다.
하지만 우리는 평생을 객체지향적 관점으로 살아왔고, 그것을 프로그래밍의 패러다임으로서 정착시킨 것 뿐이다. 실제 우리 일상생활을 들여다보면 이미 객체지향적인 사고를 자연스럽게 하고 있다는 걸 알 수 있다.

예를 들어보자.

'전자기기'는 세밀하고 다양한 부품들로 구성되어 있고, 이 부품들의 상호작용이 실제로 일어나며 구동된다. 하지만 이것을 사용하는 우리들은 내부에 어떤 부품이 어떤 순서대로 일하는지 알 필요도 없고, 기기 한 대라는 관점으로 판단한다.

실제 내부에서는 복잡한 과정이 동반될 수 있지만, 사용자에게는 기능을 사용하기 위한 버튼과 버튼을 누른 결과만 보여주면 된다.

이처럼 사물(객체)은 속성(데이터)과 행동(기능)을 가지고 있다. 객체지향 프로그래밍은 이 사고방식을 그대로 코드에 옮기는 방식이다.
즉, 객체(Object)를 중심으로 프로그램을 구성하는 것이 객체지향 프로그래밍(Object-Oriented Programming)이다.


객체지향의 핵심 목표


사실 객체지향이란 것은 절대적인 오답이 있을 순 있어도, 명확한 답은 없다. 어느 수준까지 세분화해서 설계할 것인지는 프로젝트의 상황, 그리고 이를 수행하는 팀의 방향에 따라서도 충분히 다를 수 있다.

그럼에도 불구하고, 명확한 답이 없는 이 패러다임을 사용하는 것에는 분명한 목적이 있다. 아래는 다양한 상황과 프로젝트에서 객체지향을 사용하며 추구해야 할 기본적인 목표이다.

  1. 코드의 재사용성 향상
  2. 프로그램의 유지보수성 향상
  3. 복잡성을 줄이고, 사람이 이해하고 관리하기 쉬운 구조 만들기

이러한 목표를 달성하기 위해 객체지향 프로그래밍은 네 가지 주요 특성을 갖는다.


객체지향 4대 특성


1. 캡슐화 (Encapsulation)

"필요한 것만 보여주고, 나머지는 감춘다"

캡슐화는 객체 내부의 데이터나 기능을 외부에서 직접 접근하지 못하게 숨기고, 꼭 필요한 기능만 공개하는 것이다. 마치 자동문의 내부 작동 방식은 모르지만, 우리가 버튼만 누르면 문이 열리는 것과 같다.

예시 코드 
class Player
{
	// 외부에서 직접 접근 불가
    private int health = 100; 

	// 외부에서 데이터를 변경해야할 일이 있다면
	// 목적을 명확히하여 필요한 만큼만 공개한다
    public void TakeDamage(int damage)
    {
        health -= damage;
    }

    public int GetHealth()
    {
        return health;
    }
}

health는 15. 접근제한자(Access Modifiers) private이므로 외부에서 접근할 수 없고, TakeDamage, GetHealth를 통해서만 접근 가능하다. 이렇게 하면 데이터를 보호할 수 있다.


2. 상속 (Inheritance)


"부모의 기능을 자식이 물려받는다"

상속은 기존 클래스를 바탕으로 새로운 클래스를 만드는 것이다. 공통된 기능을 부모 클래스에 작성하고, 자식 클래스는 이를 물려받아 사용하거나 필요에 따라 수정(오버라이딩)할 수 있다.

예시 코드 
class PlayerCharacter
{
    public void Move() => Console.WriteLine("이동 중...");
}

class Warrior : PlayerCharacter
{
    public void Attack()
    {
	    Move();
	    Console.WriteLine("공격!");
	}
}

Warrior는 PlayerCharacter를 상속받았기 때문에 Move() 메서드도 사용할 수 있다. 만약 프로그램 내부에서 Warrior의 인스턴스 데이터를 참조하고 있다면, 마찬가지로 Warrior를 통해 부모 클래스의 Move()를 사용할 수 있다.


3. 추상화 (Abstraction)


"공통된 기능은 간추려서 틀만 만든다"

추상화는 객체가 제공해야 할 핵심 동작만 간추려서 정의하고, 그 구현은 각자 다르게 맡기는 구조를 구성하는 것을 말한다.
여러 다양한 객체들이 공통된 속성이나 기능을 가진다면, 해당 요소들을 하나의 공통된 추상화 객체로 구성하게 된다.

실제로 우리는 다양한 몬스터에 공통적으로 있는 '걷다', '공격하다' 라는 기능을 처음부터 세부적으로 구현하기보다, 다양한 몬스터들이 공통적으로 가져야 할 속성들을 따로 모아서 추상화해놓는 방식으로 구현하게 될것이다.

예시 코드 
abstract class Enemy
{
	// 구체적인 동작은 정의하지 않음
	public abstract void Walk();
    public abstract void Attack(); 
}

class Goblin : Enemy
{
	public override void Walk()
    {
	    Console.WriteLine("5의 속도로 걷는다!");
	}
    public override void Attack()
    {
	    Console.WriteLine("고블린이 공격한다!");
	}
}

class Dragon : Enemy
{
	public override void Walk()
    {
	    Console.WriteLine("20의 속도로 걷는다!");
	}
    public override void Attack()
    {
	    Console.WriteLine("드래곤이 불을 뿜는다!");
	}
}

Enemy는 공통적으로 Walk(), Attack() 기능을 제공하지만, 실제 동작은 각 클래스가 알아서 구현한다.


4. 다형성 (Polymorphism)


"하나의 인터페이스로 다양한 동작을 구현할 수 있다"

다형성은 같은 이름의 메서드가 상황에 따라 다르게 동작하는 것이다. 이건 유연한 코드 구성의 핵심이 된다.
다형성은 17. 오버로딩(Overloading), 18. 오버라이딩(Overriding)과도 깊게 엮여있다.
코드에 대한 설명은 해당 포스팅에서 다루도록 하겠다.

예시 코드 
class Enemy
{
    public virtual void Attack() => Console.WriteLine("적이 공격한다.");
}

class Goblin : Enemy
{
    public override void Attack() => Console.WriteLine("고블린이 공격한다!");
}

class Orc : Enemy
{
    public override void Attack() => Console.WriteLine("오크가 공격한다!");
}

void Battle(Enemy enemy)
{
    enemy.Attack(); // 실제로는 Goblin, Orc에 따라 다른 동작!
}

동일한 Battle 함수가 다양한 Enemy 타입을 받아들일 수 있다. 이를 통해 확장성과 유지보수성이 높아진다.


SOLID 원칙


객체지향이 강력한 만큼, 잘못 설계하면 오히려 복잡하고 유지보수 어려운 코드가 될 수 있다. 그래서 객체지향 설계 원칙 중 대표적으로 SOLID 원칙이라는 것이 존재한다.

객체지향 자체는 물론 정답이 없지만, 객체지향 프로그램을 설계함에 있어 권장사항에 해당한다고 보면 되겠다.

SOLID는 객체지향 설계에서 지켜야 할 5가지 원칙의 앞글자를 따온 것이다.

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


  • 클래스는 하나의 책임만 가져야 한다.
  • 하나의 클래스가 너무 많은 일을 하게 되면, 변경에 취약해진다.

2. 개방/폐쇄 원칙 (Open/Closed Principle)


  • 확장에는 열려 있고, 변경에는 닫혀 있어야 한다.
  • 기존 코드를 수정하지 않고 새로운 기능을 추가할 수 있어야 한다.

3. 리스코프 치환 원칙 (Liskov Substitution Principle)


  • 자식 클래스는 부모 클래스의 기능을 대체할 수 있어야 한다.
  • 부모 객체를 사용하는 곳에 자식 객체를 넣어도 문제가 없어야 한다.

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


  • 필요하지 않은 기능은 강요하지 않아야 한다.
  • 하나의 큰 인터페이스보다는, 목적에 맞게 작은 인터페이스 여러 개로 나누는 것이 좋다.

5. 의존성 역전 원칙 (Dependency Inversion Principle)


구체화된 클래스가 아닌 추상화(인터페이스, 추상 클래스)에 의존해야 한다.
구현이 아닌 개념에 의존하면, 코드가 더 유연해진다.

profile
초보 개발자

0개의 댓글