[Java] 객체지향 프로그래밍 4가지 특징, 핵심 원리 | 추상화, 상속, 다형성, 캡슐화

dyomi·2024년 7월 4일

객체지향 프로그래밍은 우리가 보고 인지하는 실제 세계를 흉내 내어 가장 기본적인 단위인 객체들을 만들고, 그것들 간의 유기적인 상호작용을 규정하여 프로그램을 발전시키는 프로그래밍 방법론이다.

이러한 객체지향 프로그래밍은 추상화, 상속, 다형성, 캡슐화라는 4가지 특징을 가지고 있다.

추상화

추상화란, 객체의 공통적인 속성과 기능을 추출하여 정의하는 것을 의미한다.

예를 들어, 자동차와 자전거, 오토바이 모두 이동수단이라는 공통점을 가지고 있으며, 전진과 후진이라는 특징을 가지고 있다.
이것을 코드로 표현을 하면, 이동수단이라는 상위 클래스를 만들어서 전진과 후진이라는 기능을 공통 메서드로 만들 수 있고, 이러한 기능적인 요소 이외에도 공통적인 속성이 있다면, 따로 추출해서 공통 변수라는 것도 만들 수 있다.

이제 이렇게 만든어 놓은 상위 클래스를 상속받아 하위 클래스인 자동차 혹은 오토바이, 자전거 등을 구현할 수 있다.

이러한 추상화라는 개념을 사용하면 역할과 구현을 분리해서 개발할 수 있기 때문에 언제든 이 구현체를 다른 구현체로 바꿔서 사용할 수 있고, 공통적인 기능은 상속받아 사용하기 때문에 구조를 쉽게 확장해 나갈 수 있다.

또한 공통적인 기능에 수정이 필요할 경우, 상위 클래스만 수정하면 모든 하위 클래스에 적용되기 때문에 유지보수와 수정에 용이하고, 직관적이고 가독성 있는 코드를 작성할 수 있다.

상속

상속이란, 추상화를 통해 분리한 상위 클래스의 공통 기능과 속성을 하위 클래스가 물려 받는 것을 의미한다.

이 상속이라는 개념을 이용하면 여러개로 확장된 하위 클래스들이 상위 클래스의 속성과 기능을 간편하게 사용할 수 있어 코드의 중복을 줄일 수 있고, 변경을 최소화 할 수 있다.

또한 자바에서 상속을 받기 위한 방법은 추상클래스를 extends 하는 방법과, 인터페이스를 implements 하는 방법이 있다.

이 두 가지의 차이점은 무엇일까?

우선 추상클래스 같은 경우에는 구현체가 있는 메서드를 만들 수 있기 때문에 부모 클래스에서 정의해 놓은 메서드를 그대로 상속 받아서 따로 정의하지 않아도 사용이 가능하다.

하지만 인터페이스의 경우 모든 메서드가 구현체 없이 선언만 되어 있기 때문에 자식 클래스에서 메서드명에 맞추어 구현해야 한다는 차이점이 있다.

또한 추상클래스는 하나의 클래스만 상속받을 수 있고, 인터페이스의 경우 갯수가 정해져있지 않다.

추상화와 상속 예시 코드

// 추상 클래스: 이동수단
abstract class Vehicle {
    // 추상 메서드: 전진 기능
    abstract void moveForward();

    // 추상 메서드: 후진 기능
    abstract void moveBackward();
}

// 자동차 클래스
class Car extends Vehicle {
    private String brand;

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

    // 메서드 오버라이딩: 전진 기능
    @Override
    void moveForward() {
        System.out.println(brand + " car is moving forward.");
    }

    // 메서드 오버라이딩: 후진 기능
    @Override
    void moveBackward() {
        System.out.println(brand + " car is moving backward.");
    }
}


public class Main {
    public static void main(String[] args) {
        Vehicle car = new Car("Toyota");
        car.moveForward();
        car.moveBackward();
    }
}

다형성

다형성이란, 어떤 객체의 속성이나 기능을 상황에 따라 여러가지 형태를 가질 수 있는 성질을 의미한다.

예를 들어, 이동수단이라는 클래스에 앞으로 가는 전진이라는 행위는 자동차의 바퀴로도 갈 수 있고, 비행기의 날개로도 갈 수가 있다.
이러한 각각의 기능들을 따로 정의해서 개발할 수 있겠지만, 다형성을 이용하면 역할과 구체적인 구현을 분리해서 상황에 따라 하나의 기능이 여러가지 형태로 바뀔 수 있게 개발이 가능해진다.

자바에서 대표적인 예로는 메서드 오버라이딩오버로딩이 존재한다.

간단하게 오버라이딩은 상위 클래스에 있는 메서드를 상속받아서 하위 클래스에서 재정의 한것을 의미하고, 오버로딩은 한 클래스내에서 이미 선언된 메서드명과 동일한 메서드를 매개변수 타입이나 갯수를 다르게 설정하여 생성하는 것을 의미한다.

메서드 오버라이딩 (Method Overriding) 예시 코드

// 상위 클래스: 이동수단
class Vehicle {
    void move() {
        System.out.println("이동수단이 이동합니다.");
    }
}

// 하위 클래스: 자동차
class Car extends Vehicle {
    @Override
    void move() {
        System.out.println("자동차가 도로를 달립니다.");
    }
}

// 하위 클래스: 비행기
class Airplane extends Vehicle {
    @Override
    void move() {
        System.out.println("비행기가 하늘을 납니다.");
    }
}

public class Main {
    public static void main(String[] args) {
        Vehicle vehicle1 = new Car();
        Vehicle vehicle2 = new Airplane();

        vehicle1.move(); // 자동차가 도로를 달립니다.
        vehicle2.move(); // 비행기가 하늘을 납니다.
    }
}

메서드 오버로딩 (Method Overloading) 예시 코드

// 클래스: 계산기
class Calculator {
    // 정수 더하기 메서드
    int add(int a, int b) {
        return a + b;
    }

    // 실수 더하기 메서드
    double add(double a, double b) {
        return a + b;
    }

    // 문자열 연결 메서드
    String add(String a, String b) {
        return a + b;
    }
}

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

        System.out.println(calc.add(1, 2));        // 정수 더하기: 3
        System.out.println(calc.add(1.5, 2.5));    // 실수 더하기: 4.0
        System.out.println(calc.add("Hello, ", "world!")); // 문자열 연결: Hello, world!
    }
}

캡슐화

캠슐화란, 클래스 안에 서로 연관된 속성과 기능들을 하나의 캡슐로 만들어서 데이터를 외부로부터 보호하는 것을 말한다.

이 개념을 이용하면 외부에서 알 필요가 없는 정보를 숨길 수 있고, 사용자의 입장에서는 의도치 않게 그 정보를 변경하거나 접근하는 사태를 방지할 수 있다. 그리고 어떻게 구현되어 있는지 몰라도 사용하는 방법만 안다면 쉽게 사용할 수 있는 특징이 있기 때문에 실제 구현체의 수정이 필요하더라도 사용자의 입장에서는 따로 변경되거나 수정되어야 하는 부분이 필요가 없다.

자바에서는 접근제어자라는 개념을 이용하면 이러한 정보 은닉과 제어권한 설정을 할 수 있다.

// 자동차 클래스
class Car {
    private String brand;  // 브랜드명
    private int speed;     // 현재 속도

    public Car(String brand) {
        this.brand = brand;
        this.speed = 0;
    }

    public void accelerate() {
        speed += 10;
        System.out.println(brand + " car is accelerating. Current speed: " + speed);
    }

    public void brake() {
        speed -= 5;
        if (speed < 0) {
            speed = 0; 
        }
        System.out.println(brand + " car is braking. Current speed: " + speed);
    }

    public int getSpeed() {
        return speed;
    }
}


public class Main {
    public static void main(String[] args) {
        Car myCar = new Car("Toyota");

        // 메서드를 통해 기능 사용
        myCar.accelerate();
        myCar.accelerate();
        myCar.brake();
    }
}


🌟 추가 질문

추상화를 할 것인가, 안할 것인가는 개발자가 결정해야하는 부분이다.

만약 스포츠카와 버스라는 객체를 만들어야 한다고 가정했을 때, 각각의 요구사항이 다르고 공통된 부분이 없다.

이때 추상화를 할 것인가, 하지 않을 것인가 결정해야 하는 상황에서

누군가는 명확하게 자동차라는 클래스로 추상화가 가능하고, 지금은 요구 사항이 없지만 추후에 늘어날 요구 사항을 대비해 미리 만들어 놓을 수 있는 부분이고,

누군가는 당장 공통되는 부분이 없는 상황에서 굳이 클래스를 늘리면서까지 지금 할 필요가 없다고 생각할 수도 있다.

이 외에도 가장 이상적으로 생각하는 상속의 레벨이라던가, 접근제어자를 선택할때 어떤 레벨을 디폴트로 할 것인가 등 다양한 상황들이 존재한다.

정답이 있는 것은 아니지만, 내가 무언가 결정을 한다면 왜 이러한 선택을 했는지 정확하게 설명할 수 있어야 하고, 그것에 대한 합당한 이유를 다른 사람에게 설명할 수 있어야 한다.



참고자료
객체 지향 프로그래밍의 4가지 특징ㅣ추상화, 상속, 다형성, 캡슐화

profile
기록하는 습관

0개의 댓글