본문 바로가기
시리즈/소프트웨어 공학

[OOP] 객체 지향 프로그래밍에 대해 알아보자 ①

by 되고싶은노력가 2025. 1. 2.

객체 지향 프로그래밍.

https://www.reddit.com/r/ProgrammerHumor/comments/418x95/theory_vs_reality/

객체 지향 프로그래밍(Object-Oriented Programming)은 컴퓨터 프로그래밍의 패러다임 중 하나로, 컴퓨터 프로그램을 어떤 데이터를 입력받아 순서대로 처리하고 결과를 도출하는 명령어들의 목록으로 보는 시각에서 벗어나 여러 독립적인 부품들의 조합, 즉 객체들의 유기적인 협력과 결합으로 파악하고자 하는 것이다.


객체 지향 프로그래밍 특징.

객체 지향적으로 프로그램을 설계하는 데에는 여러 이점들이 있는데, 가장 큰 이점 중에 하나는 객체 지향적 설계를 통해서 프로그램을 보다 유연하고 변경이 용이하게 만들 수 있다는 점이다. 마치 컴퓨터 부품을 갈아 끼울 때, 해당하는 부품만 쉽게 교체하고 나머지 부품들을 건드리지 않아도 되는 것처럼 소프트웨어를 설계할 때 객체 지향적 원리를 잘 적용해 둔 프로그램은 각각의 부품들이 각자의 독립적인 역할을 가지기 때문에 코드의 변경을 최소화하고 유지보수를 하는 데 유리하다.

 

추상화

추상이란 사물이나 표상을 어떤 성질, 공통성, 본질에 착안하여 그것을 추출하여 파악하는 것이라고 정의하고 있습니다. 여기서 핵심은 공통성과 본질을 모아 추출한다는 것인데 자바에서는 추상화를 구현할 수 있는 문법 요소로는 추상 클래스(abstract class)인터페이스(interface)가 있습니다.

public interface Vehicle {
    public abstract void start();
    void moveFoward(); // public abstract 키워드 생략 가능
    void moveBackwoard();
}

 

가장 먼저 자동차와 오토바의 공통적인 기능을 추출하여 이동 수단이라는 인터페이스를 정의한다고 했을 때, 인터페이스는 어떤 객체의 역할만을 정의하여 객체들 간의 관계를 보다 유연하게 연결하는 역할을 담당한다. 위에서 정의한 Vehicle 인터페이스를 실제로 Car 클래스가 어떻게 구현하는지 보겠습니다.

 

// Car 클래스
public class Car implements Vehicle {
    @Override
    public void moveForward() {
        System.out.println("전진");
    }
    
    @Override
    public void moveBackward() {
        System.out.println("후진");
    }
}

// MotorBike 클래스
public class MotorBike implements Vehicle {
    @Override
    public void moveForward() {
        System.out.println("전진");
    }
    
    @Override
    public void moveBackward() {
        System.out.println("후진");
    }
}

 

위에서 확인할 수 있는 것 처럼, 객체 지향 프로그래밍에서는 역할과 구현의 분리라고 하며, 이 부분이 아래에서 살펴볼 다형성과 함께 유연하고 변경이 용이한 프로그램을 설계하는 데 가장 핵심적인 부분이라 할 수 있습니다. 정리하면, 객체 지향 프로그래밍에서는 보다 유연하고 변경에 열려있는 프로그램을 설계하기 위해 역할과 구현을 분리하는데, 여기서 역할에 해당하는 부분이 인터페이스를 통해 추상화될 수 있습니다.

 

상속

상속이란 기존의 클래스를 재활용하여 새로운 클래스를 작성하는 자바의 문법 요소를 의미합니다. 상위 클래스로부터 확장된 여러 개의 하위 클래스들이 모두 상위 클래스의 속성과 기능들을 간편하게 사용할 수 있도록 합니다.

 

즉, 클래스들 간 공유하는 속성과 기능들을 반복적으로 정의할 필요 없이 딱 한 번만 정의해두고 간편하게 재사용할 수 있어 반복적인 코드를 최소화하고 공유하는 속성과 기능에 간편하게 접근하여 사용할 수 있도록 합니다.

public class Vehicle {
    String model;
    String color;
    int wheels;
}

 

public class Car extends Vehicle {
}

public class MotorBike extends Vehicle {
}

 

위와 같이 공통된 속성을 Car 와 MotorBike 에 다시 작성할 필요없이 extends 키워드를 활용하여 재사용이 가능하게 됩니다.

 

참고로 메서드 오버라이딩을 사용하여 내용을 재정의 할 수도 있는데 이것이 추상화와 상속의 핵심적인 차이라고 볼 수 있습니다. 상속 관계의 경우 인터페이스를 사용하는 구현에 비해 추상화의 정도가 낮다고 할 수 있는데, 인터페이스가 역할에 해당하는 껍데기만 정의해두고, 하위 클래스에서 구체적인 구현을 하도록 강제하는 것에 비해, 상속 관계의 경우 상황에 따라 모든 구체적인 내용들을 정의해두고 하위 클래스에서는 그것을 단순히 가져다가 재사용할 수 있습니다.

 

다형성

다형성은 객체 지향 프로그래밍의 꽃으로 흔히 불리는데 이는 어떤 객체의 속성이나 기능이 상황에 따라 여러 가지 형태를 가질 수 있는 성질을 의미합니다. 비유적으로 표현하자면, 어떤 중년 남성이 있다고 했을 때 그 남자의 역할이 남편, 자식, 회사원 등등 상황과 환경에 따라서 달라지는 것과 비슷하다고 할 수 있습니다.

 

다형성을 더 언어스럽게 접근한다면 다형성이란 한 타입의 참조변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것을 의미합니다. 좀 더 구체적으로, 상위 클래스 타입의 참조변수로 하위 클래스의 객체를 참조할 수 있도록 하는 것입니다.

 

위 정의만 봐서는 이해하기 어렵기에 코드로 예시를 보겠습니다.

public class Main {
    public static void main(String[] args) {
        // 본래의 객체 생성 방식
        Car car = new Car();
        MotorBike motorBike = new MotorBike();
        
        // 다형성을 활용한 객체 생성 방식
        Vehicle car2 = new Car();
    }
}

 

위 코드를 확인해보면, 상위클래스 타입의 참조변수로 하위클래스 객체를 참조하는 것의 의미를 구체적으로 이해할 수 있습니다. 원래 우리가 사용했던 방식은 하위 클래스의 객체를 생성하여 하위 클래스 타입의 참조 변수에 할당해주었지만, 다형성을 활용한 객체 생성 방식에서는 하위 클래스의 객체를 생성하여 상위 클래스 타입의 참조변수 car2 에 할당해주고 있습니다.

 

그렇다면 왜 이렇게 다형성을 활용한 방식이 유용할까요?

 

첫번째로는 여러 종류의 객체를 배열로 다루는 일이 가능해집니다.

public class Main {
    public static void main(String[] args) {
    
        // 상위 클래스 타입의 객체 배열 생성
        Vehicle vehicles[] = new Vehicle[2];
        vehicles[0] = new Car();
        vehicles[1] = new MotorBike();
        
        for (Vehicle vehicle : vehicles) {
        	System.out.println(vehicle.getClass()); // 각각의 클래스 호출
        }
    }
}

// 출력값
// class Car
// class MotorBike

 

두번째로는 객체간의 결합도를 느슨하게 설계할 수 있습니다. 만약 Driver 라는 클래스에서 Car 와 MotorBike 에 대한 이동을 각각 따로 구현을 한다면 종류가 늘어날 수록 똑같은 코드를 계속해서 작성해야 할 것입니다.

public class Driver {

    void drive(Car car) {
    	car.moveForward();
        car.moveBackward();
    }
    
    void drive(MotorBike motorBike) {
    	motorBike.moveForward();
        motorBike.moveBackward();
    }
    
    void drive(Bus bus) {
    	bus.moveForward();
        bus.moveBackward();
    }
    
    ...
}

 

또 이렇게 설계를 하면 MotorBike를 MotorCycle로 수정해야하는 상황에서 고쳐야할 코드의 양도 많아지는 문제가 있습니다.

 

이를 해결하고자 역할과 구현을 구분하여 객체들 간의 직접적인 결합을 피하고 유연하고 변경이 용이한 프로그램을 설계할 수 있도록 하는 것이 다형성입니다.

 

public interface Vehicle {
    void moveForward();
    void moveBackward();
}
public class Car implements Vehicle {

    @Override
    public void moveForward() {
    	System.out.println("전진");
    }
    
    @Override
    public void moveBackward() {
    	System.out.println("후진");
    }
}
public class Driver {

    void drive(Vehicle vehicle) {
        vehicle.moveForward();
        vehicle.moveBackward();
    }
}
public class Main {
    public static void main(String[] args) {
        Car car = new Car();
        MotorBike motorBike = new MotorBike();
        Driver driver = new Driver();
        
        driver.driver(car);
        driver.driver(motorBike);
    }
}

 

Vehicle에 선언해둠으로써 Driver 클래스는 더 이상 각각의 클래스 내부의 변경이나 다른 객체가 새롭게 교체되는 것을 신경 쓰지 않아도 인터페이스에만 의존하여 수정이 있을 때마다 코드 변경을 하지 않아도 됩니다.

 

캡슐화

객체 지향의 마지막 특징인 캡슐화에 대해 살펴보겠습니다. 캡슐화란 클래스 안에 서로 연관있는 속성과 기능들을 하나의 캡슐(capsule)로 만들어 데이터를 외부로부터 보호하는 것을 말합니다.

 

캡슐화를 하는 이유로 크게 두 가지를 언급할 수 있습니다.

 

  • 데이터 보호(data protection) – 외부로부터 클래스에 정의된 속성과 기능들을 보호
  • 데이터 은닉(data hiding) – 내부의 동작을 감추고 외부에는 필요한 부분만 노출

즉, 외부로부터 클래스에 정의된 속성과 기능들을 보호하고, 필요한부분만 외부로 노출될 수 있도록 하여 각 객체 고유의 독립성과 책임 영역을 안전하게 지키고자 하는 목적이 있습니다.

 

자바 객체 지향 프로그래밍에서  캡슐화를 구현하기 위한 방법으로는 접근제어자(access modifiers)를 활용하는 것입니다.

 

접근제어자는 public, default , protected, private 총 4가지 종류의 접근 제어자가 있습니다.

 

위의 표에서 확인할 수 있는 것처럼, 접근 제어자의 접근 범위가 각각 클래스 내, 패키지 내, 다른 패키지의 하위 클래스, 그리고 패키지 외까지 각각 다른 것을 확인할 수 있습니다.