본문 바로가기
개발공부방

[OOP] 객체 지향 프로그래밍의 4가지 특징

by mwzz6 2025. 2. 5.
객체 지향 프로그래밍(OOP, Object-Oriented Programming)은 코드를 속성(Field)과 행위(Method)를 가진 객체(Object) 단위로 나누어 이들의 상호작용으로 프로그래밍하는 방식으로 마치 현실 세계를 프로그래밍으로 표현하는 것과 같다. 이러한 객체지향 프로그래밍을 활용하면 유지보수가 쉽고 확장성이 높은 소프트웨어를 만들 수 있다. 객체 지향 프로그래밍은 추상화, 캡슐화, 상속, 다형성이라는 4가지 특징으로 정리할 수 있다.

 

1. 추상화 (Abstraction)

객체의 본질적인 특징만 추출하고, 불필요한 세부사항은 숨긴다.

 

4가지 원칙 중 가장 개념이 모호하다고 느껴지는게 추상화인데 크게 두 가지 개념이 있는 것 같다.

 

먼저 객체를 만들 때 필요한 정보만 추출해서 객체로 만드는 것이 추상화이다. 은행에 고객 정보를 관리할 때 고객의 이름이나 계좌번호는 필요하지만 고객의 취미 같은 정보는 필요하지 않다. 사람을 통해 객체를 만들 때도 친구라는 객체를 만들면 취미 정보가 필요할 수도 있다. 이처럼 필요한 정보만 추출해서 객체로 만드는 것이 추상화이다.

 

현실 세계에서 다양한 자동차들이 존재하지만 자동차 브랜드나 연료 종류, 기능 등 어느정도 공통적인 개념이 존재한다. 이 공통적인 개념을 추출하여 객체로 만드는 것이 추상화이다.

 

public abstract class Car {
    protected String brand;
    protected String fuelType;

    public Car(String brand, String fuelType) {
        this.brand = brand;
        this.fuelType = fuelType;
    }

    public abstract void accelerate();

    public void showInfo() {
        System.out.println("Brand: " + brand);
        System.out.println("FuelType: " + fuelType);
    }
}

public class GasolineCar extends Car {
    public GasolineCar(String brand) {
        super(brand, "가솔린");
    }

    @Override
    public void accelerate() {
        System.out.println("10m/s^2으로 가속합니다.");
    }
}

public class ElectricCar extends Car {
    public ElectricCar(String brand) {
        super(brand, "전기");
    }

    @Override
    public void accelerate() {
        System.out.println("20m/s^2으로 가속합니다.");
    }
}

public class Test {
    public static void main(String[] args) {
        Car bmw = new GasolineCar("BMW");
        Car tesla = new ElectricCar("TESLA");

        bmw.showInfo();
        bmw.accelerate();

        tesla.showInfo();
        tesla.accelerate();
    }
}

 

다음은 자동차라는 추상 클래스를 만들고 이를 전기차와 가솔린차라는 구체 클래스가 구현하도록 작성한 코드이다. 자동차는 모두 브랜드와 연료 종류를 갖고 있으므로 이를 출력하는 메서드는 자동차 클래스에서 구현하고 자동차마다 가속하는 정도는 다르므로 이는 구체 클래스에서 구현하도록 추상 메서드로 만들었다. 전기차와 가솔린차 클래스는 이 자동차 클래스를 구현하며 자동차 브랜드만 입력 받으면 연료 정보는 전기차면 전부 전기, 가솔린차면 전부 가솔린이므로 해당 클래스에서 구현했다. 또 가속하는 정보는 이제 연료 정보에 따라 알 수 있게 되었다고 가정해서 각각 넣어줬다. 더 세분화해서 전기차 중 어떤 자동차냐, 가솔린차 중 어떤 가솔린차냐에 따라 가속 정보를 다르게 넣고 싶다면 다시 이를 추상 클래스로 만들고 이를 구현한 구체적인 자동차 모델 클래스에서 구현하도록 할 수 있다.

 

2. 캡슐화 (Encapsulation)

데이터(속성)와 데이터를 처리하는 메서드를 하나의 객체로 묶고, 외부에서 접근을 제한한다.

 

캡슐화 역시 크게 두 가지 개념으로 볼 수 있다.

 

먼저 속성과 행위를 하나의 객체로 담는다는 의미로 캡슐화이고 외부에서 불필요한 접근을 차단하는 것이 캡슐화이다.

은행 계좌의 잔액은 사용자가 직접 수정하는 것이 아닌 입금과 출금을 통해서만 변경된다.

이처럼 객체가 자신의 정보를 숨기고 자신의 연산만을 통해서 접근을 허용하는 것을 정보 은닉(Information Hiding)이라고 한다.

 

public class Account {
    private String owner;
    private double balance;

    public Account(String owner, double balance) {
        this.owner = owner;
        this.balance = balance;
    }

    public void deposit(double amount) {
        this.balance += amount;
    }

    public void withdraw(double amount) {
        this.balance -= amount;
    }

    public double getBalance() {
        return this.balance;
    }
}

public class Test {
    public static void main(String[] args) {
        Account account = new Account("홍길동", 10000);

        System.out.println(account.getBalance());

        account.deposit(500);
        System.out.println(account.getBalance());

        account.withdraw(1000);
        System.out.println(account.getBalance());
    }
}

 

다음 코드에서 계좌의 모든 속성을 private으로 선언해서 외부에서 직접 속성에 접근하지 못하도록 했다. 한 계좌의 예금주는 절대 변경되지 않으니 private 선언을 통해 수정되지 않도록 했고 잔액은 입금과 출금을 통해서는 수정될 수 있다. 따라서 private으로 선언한 대신 입금과 출금 메서드를 통해 간접적으로 수정할 수 있도록 했고 getter 메서드를 통해 조회는 가능하도록 했다. 캡슐화를 적용하면 프로그래머의 실수로 인한 데이터 오염을 방지할 수 있다는 점에서 매우 중요하다.

 

3. 상속 (Inheritance)

기존 클래스를 재사용하여 새로운 클래스를 만든다.

 

스마트폰으로 바뀌어도 여전히 전화, 메시지, 카메라 기능을 사용할 수 있었고 새로운 스마트폰엔 AI 등 여러 기능이 추가되지만 여전히 기존 모델이 가지고 있던 기능들을 사용할 수 있다. 이처럼 부모 클래스의 속성과 메서드를 물려받은 자식 클래스를 만들 수 있는 것이 상속이다.

 

public class Phone {
    public void call() {
        System.out.println("make call");
    }

    public void message() {
        System.out.println("send message");
    }
}

public class SmartPhone extends Phone {
    public void ai() {
        System.out.println("use ai");
    }
}

public class Test {
    public static void main(String[] args) {
        Phone phone = new Phone();
        phone.call();  // make call
        phone.message();  // send message

        SmartPhone smartPhone = new SmartPhone();
        smartPhone.call();  // make call
        smartPhone.message();  // send message
        smartPhone.ai();  // use ai
    }
}

 

이 코드에서 스마트폰은 폰 클래스를 상속 받았고 ai 메서드를 추가로 작성했다. 상속을 통해 스마트폰 클래스는 부모 클래스의 모든 메서드를 사용할 수 있고 위와 같이 call 메서드와 message 메서드를 다시 작성하지 않고 간단하게 부모 클래스의 메서드를 사용하는 것으로 기능을 확장할 수 있다.

 

4. 다형성 (Polymorphism)

하나의 인터페이스(부모 클래스)로 여러가지 형태의 객체를 다룰 수 있다.

 

모든 동물은 울음소리가 있지만 개는 멍멍, 고양이는 야옹, 소는 음메처럼 동물마다 소리는 다르다. 사칙연산의 경우 더하거나 빼는 등의 개념은 정수든 실수든 똑같지만 프로그래밍 언어에서 다른 타입으로 수를 다룰 경우 하나의 메서드에 이를 처리하기는 어렵다. 이처럼 공통적인 개념은 있지만 실제 동작은 다른 상황이 있고 같은 기능을 여러 방식으로 다르게 동작할 수 있도록 하는 것이 다형성이다.

 

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

    public double add(double a, double b) {
        return a + b;
    }
}

public class Test {
    public static void main(String[] args) {
        Calculator calculator = new Calculator();

        System.out.println(calculator.add(10, 20));  // 30
        System.out.println(calculator.add(15.2, 14.7));  // 29.9
    }
}

 

위 코드는 정수의 덧셈을 하는 add 메서드와 실수의 덧셈을 하는 add 메서드를 작성한 예시이다. 이처럼 메서드 시그니처가 다를 경우 동일한 이름의 메서드를 선언할 수 있고 이를 메서드 오버로딩(Method Overloading)이라고 한다. 이를 통해 사용자는 add 메서드 하나만으로 정수의 덧셈과 실수의 덧셈을 모두 수행하는 경험을 얻을 수 있다.

 

public abstract class Animal {
    protected String name;

    public Animal(String name) {
        this.name = name;
    }

    abstract void makeSound();

    public void eat() {
        System.out.println(name + "이/가 음식을 먹습니다.");
    }

    void sleep() {
        System.out.println(name + "이/가 잠을 잡니다.");
    }
}

public class Dog extends Animal {
    public Dog(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println(name + "이/가 멍멍! 하고 짖습니다.");
    }

    @Override
    void sleep() {
        System.out.println(name + "이/가 개집에서 잠을 잡니다.");
    }
}

public class Cat extends Animal {
    public Cat(String name) {
        super(name);
    }

    @Override
    void makeSound() {
        System.out.println(name + "이/가 야옹! 하고 웁니다.");
    }
}

public class Test {
    public static void main(String[] args) {
        Dog dog = new Dog("바둑이");
        Cat cat = new Cat("나비");

        dog.makeSound();  // 바둑이이/가 멍멍! 하고 짖습니다.
        dog.eat();  // 바둑이이/가 음식을 먹습니다.
        dog.sleep();  // 바둑이이/가 개집에서 잠을 잡니다.
        cat.makeSound();  // 나비이/가 야옹! 하고 웁니다.
        cat.eat();  // 나비이/가 음식을 먹습니다.
        cat.sleep();  // 나비이/가 잠을 잡니다.

        Animal dog2 = new Dog("바둑이2");
        Animal cat2 = new Cat("나비2");

        dog2.makeSound();  // 바둑이2이/가 멍멍! 하고 짖습니다.
        dog2.eat();  // 바둑이2이/가 음식을 먹습니다.
        dog2.sleep();  // 바둑이2이/가 개집에서 잠을 잡니다.
        cat2.makeSound();  // 나비2이/가 야옹! 하고 웁니다.
        cat2.eat();  // 나비2이/가 음식을 먹습니다.
        cat2.sleep();  // 나비2이/가 잠을 잡니다.
    }
}

 

위 코드는 동물 클래스와 이를 상속 받은 개, 고양이 클래스를 작성한 예시로 동물은 음식을 먹는다는 공통점이 있지만 동물마다 울음소리는 다르다는 차이점이 있다. 따라서 이를 추상 메서드로 만들고 실제 울음소리는 이를 상속받은 클래스에서 구현하도록 했다. 또한 잠을 자는 장소의 경우 개만 개집에서 자는 것으로 변경을 했다. 이때 부모 클래스의 메서드를 자식 클래스에서 다시 정의해서 자식 클래스의 메서드가 적용되도록 하는 것을 메서드 오버라이딩이라고 하며 @Override 어노테이션으로 메서드 오버라이딩을 했음을 표현했다.