[SOLID] 02. 객체지향 원리


객체지향 프로그래밍(OOP, Object-Oriented Programming)의 핵심 원리는 추상화(Abstraction), 캡슐화(Encapsulation), 상속(Inheritance), 다형성(Polymorphism)의 네 가지로 나뉜다. 이를 통해 복잡한 시스템을 설계하고 구현하는 데 유용한 구조를 제공해서 유지보수하기 쉬운 소프트웨어를 개발할 수 있다.



1. 추상화

객체지향 프로그래밍의 네 가지 주요 원리 중 하나인 추상화(Abstraction)는 복잡한 현실 세계의 개념이나 시스템을 프로그래밍 관점에서 시스템에서 중요한 속성동작만 추출하여 이를 단순화하고 프로그래밍 객체로 모델링하는 과정이다. 추상화는 중요한 공통된 속성 행동을 추출하는 작업을 의미한다. 추상화가 가능한 개체들은 개체가 소유한 특성의 이름으로 하나의 잡합을 이루어 일반화시키는 개념이다. 즉, 불필요한 세부사항은 숨기고, 중요한 부분만 드러내어 객체 간의 상호작용을 단순화하는 것이 추상화의 목적이다.


1-1. 추상화 핵심

  1. 중요한 정보에 집중
    • 중요한 정보만 표시하여 시스템의 본질적인 특성에 집중하고, 불필요하거나 세부적인 정보는 감춘다.
  2. 명확한 인터페이스 제공
    • 객체의 내부 구현은 감추고, 외부에서 상호작용할 수 있는 명확한 인터페이스만 제공한다.
  3. 복잡성 감소
    • 사용자와 개발자가 객체의 복잡한 내부 구조를 알 필요 없이 사용할 수 있도록 단순화한다.
  4. 유연성과 재사용성 향상
    • 추상화를 통해 다양한 객체가 동일한 인터페이스를 사용할 수 있으므로 다형성(Polymorphism)을 활용할 수 있다.


// 추상 클래스 정의
abstract class Animal {
    // 추상 메서드
    abstract void sound();

    void eat() {
        System.out.println("This animal eats food.");
    }
}

// 구현 클래스: 추상 클래스를 상속받아 추상 메서드를 구현
class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("Bark!");
    }
}

class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("Meow!");
    }
}

public class AbstractionExample {
    public static void main(String[] args) {
        Animal dog = new Dog();
        Animal cat = new Cat();

        dog.sound();
        cat.sound();

        dog.eat();
        cat.eat();
    }
}




2. 캡슐화

캡슐화(Encapsulation)는 객체지향 프로그래밍의 핵심 원리 중 하나로 데이터(필드)와 이를 처리하는 메서드(동작)하나의 단위(객체)로 묶고, 외부로부터 데이터를 보호하는 것을 의미한다. 즉, 객체 내부의 구현 세부사항을 감추고, 외부에서는 정의된 인터페이스를 통해서만 접근할 수 있도록 하는 원리이다. 캡슐화를 통해 객체는 자신만의 책임을 가진 독립된 단위가 되고, 외부와의 의존성을 최소화할 수 있게 되므로 캡슐화는 정보은닉을 통해 높은 응집도(cohension)낮은 결합도(coupling)를 갖도록 한다.


2-1. 캡슐화의 핵심

  1. 데이터 보호
    • 객체의 데이터를 외부에서 직접 수정하거나 접근할 수 없도록 보호함으로써, 데이터 무결성을 유지할 수 있다.
  2. 정보 은닉(Information Hiding)
    • 객체 내부의 구현 세부사항을 감추고, 외부에서는 접근 가능한 메서드(getter/setter)를 통해 상호작용한다. 이는 외부에서 객체의 내부 구조를 알 필요 없이 사용할 수 있도록 한다.
  3. 유지보수성 향상
    • 내부 구현을 변경하더라도 외부 코드에 영향을 주지 않아 유지보수가 쉽다.
    • 동일한 인터페이스를 사용하여 객체의 데이터를 관리하므로 코드 재사용성을 높일 수 있다.


class Person {
    // 필드에 private 접근 제한자를 사용
    private String name;
    private int age;

    // Getter: 외부에서 데이터를 읽을 수 있도록 제공
    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    // Setter: 외부에서 데이터를 수정할 수 있도록 제공
    public void setName(String name) {
        if (name != null && !name.isEmpty()) {
            this.name = name;
        } else {
            System.out.println("Invalid name.");
        }
    }

    public void setAge(int age) {
        if (age > 0) {
            this.age = age;
        } else {
            System.out.println("Invalid age.");
        }
    }
}

public class EncapsulationExample {
    public static void main(String[] args) {
        Person person = new Person();

        // setter를 사용하여 데이터 설정
        person.setName("John");
        person.setAge(25);

        // getter를 사용하여 데이터 읽기
        System.out.println("Name: " + person.getName());
        System.out.println("Age: " + person.getAge());

        // 잘못된 데이터 입력
        person.setAge(-5);
    }
}




3. 일반화(상속)

일반화는 여러 개체들이 가진 공통된 특성을 부각시켜 하나의 개념이나 법칙으로 성립하는 과정을 말한다. 객체지향의 원리 관점에서는 일반화(Generalization)관계는 상위 클래스(부모 클래스)와 하위 클래스(자식 클래스) 간의 상속(Inheritance)관계를 의미한다. 이는 일반화 관계는 상속을 통해 구현되며 is-a” 관계로 표현된다. 상위 클래스(Superclass)가 공통된 속성과 동작을 정의하면 하위 클래스(Subclass)는 상위 클래스의 모든 속성과 동작을 상속받고, 추가로 고유한 속성과 동작을 정의한다. 그래서 상위 클래스의 속성과 동작을 재사용하거나 확장하는 데 사용된다.


3-1. 일반화 핵심

  1. “is-a” 관계
    • 하위 클래스는 상위 클래스의 특성을 가지고 있으며, 상위 클래스의 일종으로 본다.
  2. 코드 재사용성
    • 하위 클래스가 상위 클래스의 속성과 메서드를 상속받아 중복 코드를 줄이고 재사용성을 높인다.
  3. 다형성(Polymorphism) 활용 가능
    • 상위 클래스의 참조 변수로 하위 클래스 객체를 다룰 수 있어 유연한 코딩이 가능하다.
  4. 확장 가능성
    • 하위 클래스는 상위 클래스의 기능을 그대로 사용할 뿐 아니라, 고유한 동작을 추가하거나 상위 클래스의 메서드를 오버라이딩(Overriding)하여 확장할 수 있다.
// 상위 클래스 정의
class Animal {
    void eat() {
        System.out.println("This animal eats food.");
    }
}

// 하위 클래스 정의
class Dog extends Animal {
    void bark() {
        System.out.println("Bark! Bark!");
    }
}

public class GeneralizationExample {
    public static void main(String[] args) {
        Animal animal = new Animal();
        animal.eat(); 

        Dog dog = new Dog();
        dog.eat();     // 상위 클래스의 메서드 사용
        dog.bark();    // 하위 클래스의 메서드 사용

        // 다형성
        Animal polyAnimal = new Dog();
        polyAnimal.eat();
    }
}


3-2. 일반화의 위임

일반화(Generalization)는 상속 관계에서 상위 클래스하위 클래스 간의 “is-a(상속)” 관계로 상위 클래스는 공통 속성과 동작을 정의하며, 하위 클래스는 이를 상속받아 재사용하거나 확장하는 형태이다. 위임(Delegation)은 객체가 특정 작업을 스스로 처리하지 않고, 해당 작업을 처리하는 책임을 다른 객체에게 위임(Delegation)하는 설계 방식입니다. 위임은 has-a(포함)관계를 통한 형태이다.

3-2.1. 일반화의 위임이란?

일반화의 위임(Delegation in Generalization)은 객체지향 설계에서 하위 클래스가 자신의 작업 일부를 상위 클래스나 다른 객체에 위임하는 설계를 의미한다. 이는 상속 기반의 일반화 관계와 구성을 결합하여, 코드 재사용성과 유연성을 동시에 극대화하려는 설계 접근 방식이다.

3-2.2. 일반화의 위임 핵심

  • 상위 클래스는 공통 동작과 속성을 정의하고, 구체적인 구현은 하위 클래스가 위임하거나 상속받아 구현
  • 책임 분리: 하위 클래스가 모든 작업을 처리하지 않고, 관련 작업은 다른 객체에 위임
  • 유연성 향상: 위임을 활용하면 코드의 의존성을 줄이고, 객체 간 협력 관계를 강화

즉, 일반화의 위임은 상속과 위임을 조합하여 설계의 재사용성, 유연성, 책임 분리를 극대화하는 방법이다.


예제 1)

// 상위 클래스
class Animal {
    void eat() {
        System.out.println("This animal eats food.");
    }
}

// 하위 클래스: 상속과 위임 결합
class Dog extends Animal {
    private SoundBehavior soundBehavior;  // 위임할 객체 포함

    public Dog(SoundBehavior soundBehavior) {
        //위임할 객체 set
        this.soundBehavior = soundBehavior;
    }

    void performSound() {
        //SoundBehavior 객체에 위임
        soundBehavior.makeSound();
    }
}

// 인터페이스: 위임할 동작 정의
interface SoundBehavior {
    void makeSound();
}

// 구체적인 위임 동작 구현
class BarkBehavior implements SoundBehavior {
    @Override
    public void makeSound() {
        System.out.println("Bark! Bark!");
    }
}

public class DelegationInGeneralizationExample1 {
    public static void main(String[] args) {
        // 위임 객체 생성
        SoundBehavior barkBehavior = new BarkBehavior();

        // Dog 객체 생성 및 위임 설정
        Dog dog = new Dog(barkBehavior);

        // 상위 클래스 동작 호출
        dog.eat();

        // 위임 동작 호출
        dog.performSound();
    }
}


예제 2)

// 버튼 동작 인터페이스
interface ClickAction {
    void onClick();
}

// 위임 클래스
class OpenFileAction implements ClickAction {
    @Override
    public void onClick() {
        System.out.println("File opened.");
    }
}

class SaveFileAction implements ClickAction {
    @Override
    public void onClick() {
        System.out.println("File saved.");
    }
}

// 버튼 클래스
class Button {
    private ClickAction clickAction;  // 동작 위임

    public Button(ClickAction clickAction) {
        this.clickAction = clickAction;
    }

    void click() {
        clickAction.onClick();  // 동작 위임
    }
}

public class DelegationInGeneralizationExample2 {
    public static void main(String[] args) {
        // 버튼에 동작 위임
        Button openButton = new Button(new OpenFileAction());
        Button saveButton = new Button(new SaveFileAction());

        // 버튼 클릭
        openButton.click();  // File opened.
        saveButton.click();  // File saved.
    }
}



4. 다형성

다형성(Polymorphism)은 객체지향 프로그래밍의 주요 원리 중 하나로, 하나의 인터페이스나 메서드가 다양한 형태로 동작할 수 있는 능력을 의미한다. 동일한 메시지(메서드 호출)가 객체의 타입에 따라 다르게 동작하도록 설계할 수 있으며, 런타임에서 동적 바인딩(Dynamic Binding)을 활용하여 객체의 타입에 따라 다른 동작을 실행한다. 이를 통해 코드의 유연성, 확장성, 재사용성을 극대화할 수 있다.


4-1. 다형성의 핵심

다형성은 객체지향 프로그래밍에서 유연하고 확장 가능한 설계를 가능하게 하는 중요한 원리로 이를 통해 상속, 오버라이딩, 오버로딩, 인터페이스 등을 활용하여 다양한 객체를 일관되게 처리하고, 변경에 강한 구조를 설계할 수 있게 도와준다.

상속(Inheritance)

다형성은 주로 상속 관계에서 발생하며, 상위 클래스 타입으로 하위 클래스 객체를 참조하여 동일한 인터페이스로 다양한 객체를 처리할 수 있다.

class Animal {
    void sound() {
        System.out.println("동물 우는 소리");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("멍멍!");
    }
}

class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("야옹!");
    }
}

메서드 오버라이딩(Overriding)

하위 클래스는 상위 클래스의 메서드를 재정의(Override)하여 메서드 재정의해서 동작하도록 구현한다. 런타임에 객체의 실제 타입에 따라 호출될 메서드가 결정된다.(동적 바인딩)

class Animal {
    void sound() {
        System.out.println("동물 우는 소리");
    }
}

class Dog extends Animal {
    @Override
    void sound() {
        System.out.println("멍멍!");
    }
}

class Cat extends Animal {
    @Override
    void sound() {
        System.out.println("야옹!");
    }
}

public class Main {
    public static void main(String[] args) {
        Animal animal = new Cat();
        animal.sound();
    }
}

메서드 오버로딩(Overloading)

같은 이름의 메서드를 다른 매개변수로 정의하여 다양한 방식으로 사용할 수 있게 한다. 컴파일 시간에 메서드 호출이 결정되므로 정적 다형성(Static Polymorphism)이라고도 한다.

예시:

class Calculator {
    int add(int a, int b) {
        return a + b;
    }

    int add(int a, int b, int c) {
        return a + b + c;
    }
}

public class Main {
    public static void main(String[] args) {
        Calculator calc = new Calculator();
        System.out.println(calc.add(2, 3));
        System.out.println(calc.add(2, 3, 4));
    }
}

인터페이스(Interface)와 다형성**

다형성은 인터페이스를 통해 공통된 동작을 정의하고, 여러 클래스가 이를 구현(Implementation)한다. 인터페이스 타입으로 다양한 구현 객체를 하나의 인터페이스로 다룰 수 있어 객체 간의 구현 의존성을 제거하고 확장성을 제공합니다. 런타임 동작을 유연하게 변경할 수 있다.

interface Vehicle {
    void move();
}

class Car implements Vehicle {
    @Override
    public void move() {
        System.out.println("자동차 이동");
    }
}

class Bike implements Vehicle {
    @Override
    public void move() {
        System.out.println("자전거 이동");
    }
}

public class Main {
    public static void main(String[] args) {
        Vehicle vehicle = new Car();  // 인터페이스 타입으로 객체 참조
        vehicle.move();            
        
        vehicle = new Bike();
        vehicle.move();
    }
}

동적 바인딩(Dynamic Binding)

메서드 호출이 런타임에 객체의 실제 타입에 따라 결정되며, 다형성의 핵심 원리로 상위 클래스 타입의 참조 변수가 하위 클래스의 재정의된 메서드를 호출한다.



5. 객체지향 원리의 목적

객체지향 프로그래밍(OOP)의 주요 목적현실 세계의 복잡한 시스템을 보다 쉽게 설계, 구현, 관리할 수 있도록 하는 것이다.

1) 코드의 재사용성 향상

  • 상속(Inheritance): 상위 클래스에서 정의한 공통 속성과 동작을 하위 클래스에서 재사용
  • 구성(Composition): 다른 객체를 포함하여 기능을 확장

2) 유지보수성 강화

  • 캡슐화(Encapsulation): 데이터와 동작을 객체 내부에 감추고, 필요한 인터페이스만 외부에 노출
  • 정보 은닉(Information Hiding): 내부 구현을 보호하여 외부 코드와의 의존성을 줄임

3) 복잡한 시스템의 단순화

  • 추상화(Abstraction): 중요한 속성과 동작만 추출하고, 불필요한 세부사항은 숨김
  • 객체(Object)를 통해 현실 세계의 사물을 모델링하여 문제를 쉽게 이해하고 접근

4) 유연성과 확장성 제공

  • 다형성(Polymorphism): 하나의 인터페이스로 다양한 객체를 다룰 수 있어 유연한 설계 가능
  • 설계 패턴(Design Patterns): 객체지향 원리를 활용하여 확장 가능한 구조 설계

5) 데이터 보호 및 무결성 유지

  • 캡슐화: 데이터를 접근제어자를 통해 제한하고 getter/setter 을 통해 상태 관리
  • 정보 은닉: 외부에서 직접 데이터를 변경할 수 없도록 보호

6) 현실 세계의 모델링

  • 객체를 통해 속성과 동작을 정의
  • 클래스와 객체 간의 관계(상속, 일반화, 집합 등)를 모델링

7) 생산성과 효율성 증가

  • 모듈화된 설계를 통해 팀 간 작업 분업
  • 공통된 인터페이스와 클래스를 기반으로 한 협업


◼︎ 객체지향 원리 주요 목적

원리주요 목적
추상화시스템의 복잡성을 줄이고 중요한 부분에 집중
캡슐화데이터 보호, 무결성 유지, 유지보수 용이
상속코드 재사용성과 확장성 향상
다형성유연한 설계, 다양한 객체를 동일한 인터페이스로 처리 가능