본문 바로가기

Java

[Java] 절차 지향 VS 객체 지향

프로그래밍 방식은 크게 절차 지향 프로그램과 객체 지향 프로그램으로 나눌 수 있습니다.

 

절차 지향 프로그래밍

  • 절차 지향 프로그래밍은 이름 그대로 절차를 지향한다. 쉽게 이야기해서 실행 순서를 중요하게 생각하는 방식이다.
  • 절차 지향 프로그램의 흐름을 순차적으로 따르며 처리하는 방식이다. 즉, "어떻게"를 중심으로 프로그래밍 한다.

객체 지향 프로그래밍

  • 객체 지향 프로그래밍은 이름 그대로 객체를 지향한다. 쉽게 이야기해서 객체를 중요하게 생각하는 방식이다.
  • 객체 지향 프로그래밍은 실제 세계의 사물이나 사건을 객체로 보고, 이러한 객체들 간의 상호작용을 중심으로 프로그래밍하는 방식이다. 즉, "무엇을" 중심으로 프로그래밍 한다.

두 방식의 차이

  • 절차 지향은 데이터와 해당 데이터에 대한 처리 방식이 분산되어 있다. 반면 객체 지향 방식에서는 데이터와 그 데이터에 대한 행동(메서드)이 하나의 "객체" 안에 함께 포함되어 있다.

지금까지 클래스와 객체를 사용해서 관련 데이터를 묶어서 사용하는 방법을 학습했었습니다.

그렇다면 객체를 사용하기만 하면 객체 지향 프로그래밍이라고 할 수 있을까요?

사실 앞서 우리가 작성했던 모든 프로그래밍은 절차 지향 프로그램이였습니다.

그렇다면 무엇이 객체 지향 프로그램일까요?

 

지금부터 절차 지향에서 객체 지향으로 점진적으로 코드를 변경해보면서 객체 지향 프로그래밍을 이해 해보겠습니다.

 

음악 플레이어 만들기

요구사항

  • 음악 플레이어를 켜고 끌 수 있어야 한다.
  • 음악 플레이어의 볼륨을 증가, 감소시킬 수 있어야 한다.
  • 음악 플레이어의 상태를 확인할 수 있어야 한다.

예시 출력

음악 플레이어를 시작합니다.
음악 플레이어 볼륨: 1
음악 플레이어 볼륨: 2
음악 플레이어 볼륨: 1
음악 플레이어 상태 확인
음악 플레이어 ON, 볼륨: 1
음악 플레이어를 종료합니다.

 

public class MusicPlayerMain1 {

    public static void main(String[] args) {
        int volume = 0;
        boolean isOn = false;

        //음악 플레이어 켜기
        isOn = true;
        System.out.println("음악 플레이어를 시작합니다.");
        //볼륨 증가
        volume++;
        System.out.println("음악 플레이어 볼륨: " + volume);
        //볼륨 증가
        volume++;
        System.out.println("음악 플레이어 볼륨: " + volume);
        //볼륨 감소
        volume--;
        System.out.println("음악 플레이어 볼륨: " + volume);
        //음악 플레이어 상태
        if (isOn == true){
            System.out.println("음악 플레이어 상태: ON, 볼륨: " + volume);
        } else {
            System.out.println("음악 플레이어 상태: OFF");
        }
        //음악 플레이어 끄기
        isOn = false;
        System.out.println("음악 플레이어를 종료합니다.");
    }
}

 

순서대로 프로그램이 작동하도록 단순하게 작성해보았습니다.

 

그렇다면 앞서 작성한 코드에 클래스와 메서드를 도입해보겠습니다.

public class MusicPlayerMain3 {

    public static void main(String[] args) {

        MusicPlayerData data = new MusicPlayerData();
        //음악 플레이어 켜기
        on(data);
        //볼륨 증가
        volumeUp(data);
        volumeUp(data);
        //볼륨 감소
        volumeDown(data);
        //음악 플레이어 상태
        showStatus(data);
        //음악 플레이어 끄기
        off(data);
    }
    static void on(MusicPlayerData data) {
        data.isOn = true;
        System.out.println("음악 플레이어를 시작합니다.");
    }
    static void volumeUp(MusicPlayerData data) {
        data.volume++;
        System.out.println("음악 플레이어 볼륨: " + data.volume);
    }
    static void volumeDown(MusicPlayerData data) {
        data.volume--;
        System.out.println("음악 플레이어 볼륨: " + data.volume);
    }
    static void off(MusicPlayerData data) {
        data.isOn = false;
        System.out.println("음악 플레이어를 종료합니다.");
    }
    static void showStatus(MusicPlayerData data) {
        if (data.isOn == true){
            System.out.println("음악 플레이어 상태: ON, 볼륨: " + data.volume);
        } else {
            System.out.println("음악 플레이어 상태: OFF");
        }
    }
}

 

각각의 기능을 메서드로 만든 덕분에 각각의 기능이 모듈화 되었습니다. 이를 통해 이러한 장점들이 생겼습니다.

  • 중복제거: 로직 중복이 제거되었다. 같은 로직이 필요하면 해당 메서드를 여러번 호출하면 된다.
  • 변경 영향 범위: 기능을 수정할 때 해당 메서드 내부만 변경하면 된다.
  • 메서드 이름 추가: 메서드 이름을 통해 코드를 더 쉽게 이해할 수 있다.
모듈화: 쉽게 이야기해서 레고 블럭을 생각하면 된다. 필요한 블럭을 가져다 꼽아서 사용할 수 있다. 여기서는 음악 플레이어의 기능이 필요하면 해당 기능을 메서드 호출 만으로 손 쉽게 사용할 수 있다. 이제 음악 플레이어와 관련된 메서드를 조립해서 프로그램을 작성할 수 있다.

 

 

절차 지향 프로그램의 한계

 

지금까지 클래스를 사용해서 관련된 데이터를 하나로 묶고, 또, 메서드를 사용해서 각각의 기능을 모듈화 했습니다.

덕분에 상당히 깔끔하고 읽기 좋고 유지보수 하기 좋은 코드를 작성할 수 있었습니다.

하지만 여기서 더 개선할 수 있지 않을까요?

 

저희가 작성한 코드의 한계는 바로 데이터와 기능이 분리되어 있다는 점입니다.

음악 플레이어의 데이터는 MusicPlayerMain3 에 있는 각각의 메서드에 분리되어 있습니다.

그래서 음악 플레이어와 관련된 데이터는 MusicPlayerData 를 사용해야 하고, 음악 플레이어와 관련된 기능은

MusicPlayerData 를 사용해야 하고, 음악 플레이어와 관련된 기능은 MusicPlayerMain3 의 각 메서드를 사용해야 합니다.

 

데이터와 그 데이터를 사용하는 기능은 매우 밀접하게 연관되어 있습니다. 각각의 메서드를 보면 대부분

MusicPlayerData 의 데이터를 사용합니다. 따라서 이후에 관련된 데이터가 변경되면 MusicPlayerMain3 부분의 메서드들도 함께 변경해야 합니다. 그리고 이렇게 데이터와 기능이 분리되어 있으면 유지보수 관점에서도 관리 포인트가 2곳으로

늘어나겠죠?

 

객체 지향 프로그래밍이 나오기 전까지는 지금과 같이 데이터와 기능이 분리되어 있었습니다.

따라서 지금과 같은 코드가 최선이였는데요. 하지만 객체 지향 프로그래밍이 나오면서 데이터와 기능을 온전히 하나로

묶어서 사용할 수 있게 되었습니다.

 

데이터와 기능을 하나로 온전히 묶는다는 것이 어떤 의미인지 이해하기 위해 예제를 만들어보겠습니다.

 

클래스와 메서드

 

클래스는 데이터인 멤버 변수 뿐 아니라 기능 역할을 하는 메서드도 포함할 수 있습니다.

먼저 멤버 변수만 존재하는 클래스로 간단한 코드를 작성해보겠습니다.

 

public class ValueData {
    int value;
}
public class ValueDataMain {

    public static void main(String[] args) {

        ValueData valueData = new ValueData();
        add(valueData);
        add(valueData);
        add(valueData);
        System.out.println("최종 숫자 = " + valueData.value);
    }

    static void add(ValueData valueData) {
        valueData.value++;
        System.out.println("숫자 증가 value = " + valueData.value);
    }
}

 

실행 결과

숫자 증가 value = 1
숫자 증가 value = 2
숫자 증가 value = 3
최종 숫자 = 3

 

ValueData 라는 인스턴스를 생성하고 외부에서 ValueData.value 에 접근해 숫자를 하나씩 증가하는 단순한 코드입니다.

코드를 보면 데이터인 valuevalue의 값을 증가시키는 기능인 add( ) 메서드가 서로 분리되어 있습니다.

 

자바 같은 객체 지향 언어는 클래스 내부에 속성(데이터)과 기능(메서드)을 함께 포함할 수 있습니다.

클래스 내부에 멤버 변수 뿐만 아니라 메서드도 함께 포함할 수 있다는 뜻입니다.

 

이번에는 숫자를 증가시키는 기능도 클래스에 함께 포함해서 새로운 클래스를 정의해보겠습니다.

public class ValueObject {
    int value;

    void add() {
        value++;
        System.out.println("숫자 증가 value = " + value);
    }
}

 

이 클래스에는 데이터인 value와 해당 데이터를 사용하는 기능인 add( ) 메서드를 함께 정의하였습니다.

 

※ 참고: 여기서 만드는 add( ) 메서드는 static 키워드를 사용하지 않는다.

메서드는 객체를 생성해야 호출할 수 있다. 그런데 static 이 붙으면 객체를 생성하지 않고도 메서드를 호출할 수 있다.

 

public class ValueObjectMain {

    public static void main(String[] args) {

        ValueObject valueObject = new ValueObject();
        valueObject.add();
        valueObject.add();
        valueObject.add();
        System.out.println("최종 숫자 = " + valueObject.value);
    }
}

 

실행 결과

숫자 증가 value = 1
숫자 증가 value = 2
숫자 증가 value = 3
최종 숫자 = 3

 

인스턴스 생성

ValueObject valueObject = new ValueObject();

 

valueObject 라는 객체를 생성했다.

이 객체는 멤버 변수 뿐만 아니라 내부에 기능을 수행하는 add( ) 메서드도 함께 존재한다.

 

 

인스턴스의 메서드 호출

 

인스턴스의 메서드를 호출하는 방법은 멤버 변수를 사용하는 방법과 동일하다.

.(점) 을 찍어서 객체에 접근한 다음에 원하는 메서드를 호출하면 된다.

valueObject.add() // 1
x002.add() // 2: x002 ValueObject 인스턴스에 있는 add() 메서드를 호출한다.

 

3: add( ) 메서드를 호출하면 메서드 내부에서 value++ 을 호출하게 된다. 이때, value 에 접근해야 하는데, 기본으로 본인

인스턴스에 있는 멤버 변수에 접근한다. 본인 인스턴스가 x002 참조값을 사용하므로 자기 자신인 x002.value 에 접근하게 된다.

4: ++ 연산으로 value 값을 하나 증가시킨다.

 

정리

  • 클래스는 속성(데이터, 멤버변수)과 기능(메서드)를 정의할 수 있다.
  • 객체는 자신의 메서드를 통해 자신의 멤버 변수에 접근할 수 있다.
  • 객체의 메서드 내부에서 접근하는 멤버 변수는 객체 자신의 멤버 변수이다.

 

객체 지향 프로그래밍

 

지금까지 개발한 음악 플레이어는 데이터와 기능이 불리되어 있었습니다.

이제 데이터와 기능을 하나로 묶어서 음악 플레이어라는 개념을 온전히 하나의 클래스에 담아볼 것 입니다.

프로그램을 작성하는 절차도 중요하지만 지금은 음악 플레이어라는 개념을 객체로 온전히 만드는 것이 더 중요합니다.

 

음악 플레이어

  • 속성: volume, isOn
  • 기능: on( ), off( ), volumeUp( ), volumeDown( ), showStatus( )
public class MusicPlayer {

    int volume;
    boolean isOn;

    void on() {
        isOn = true;
        System.out.println("음악 플레이어를 시작합니다.");
    }
    void off() {
        isOn = false;
        System.out.println("음악 플레이어를 종료합니다.");
    }
    void volumeUp() {
        volume++;
        System.out.println("볼륨: " + volume);
    }
    void volumeDown() {
        volume--;
        System.out.println("볼륨: " + volume);
    }
    void showStatus() {
        if (isOn == true){
            System.out.println("음악 플레이어 상태: ON, 볼륨: " + volume);
        } else {
            System.out.println("음악 플레이어 상태: OFF");
        }
    }
}
public class MusicPlayerMain {

    public static void main(String[] args) {
    
        MusicPlayer musicPlayerObject = new MusicPlayer();
        musicPlayerObject.on();
        musicPlayerObject.volumeUp();
        musicPlayerObject.volumeUp();
        musicPlayerObject.volumeDown();
        musicPlayerObject.showStatus();
        musicPlayerObject.off();
    }
}

 

MusicPlayer 를 사용하는 코드를 봐봅시다.

MusicPlayer 객체를 생성하고 필요한 기능(메서드)을 호출하기만 하면 됩니다.

필요한 모든 것은 MusicPlayer 안에 들어있습니다.

  • MusicPlayer를 사용하는 입장에서는  MusicPlayer 의 데이터인 voulme, isOn 같은 데이터는 전혀 사용하지 않는다.
  • MusicPlayer를 사용하는 입장에서는 이제  MusicPlayer 내부에 어떤 속성(데이터)가 있는지 전혀 몰라도 된다.
  • MusicPlayer를 사용하는 입장에서는 단순하게 MusicPlayer가 제공하는 기능 중에 필요한 기능을 호출해서 사용하기만 하면 된다.
캡슐화

MusicPlayer 를 보면 음악 플레이어를 구성하기 위한 속성기능이 마치 하나의 캡슐에 쌓여있는 것 같다.
이렇게 속성과 기능을 하나로 묶어서 필요한 기능을 메서드를 통해 외부에 제공하는 것을 캡슐화 라고 한다.



겍체 지향 프로그래밍 덕분에 음악 플레이어 객체를 사용하는 입장에서 진짜 음악 플레이어를 만들고 사용하는 것 처럼

친숙하게 느껴집니다. 그래서 코드가 더 읽기 쉬운 것은 물론이고, 속성과 기능이 한 곳에 있기 때문에 변경도 더 쉬워집니다. 예를 들어서 MusicPlayer 내부 코드가 변하는 경우에 다른 코드는 변경하지 않아도 되는데요.


MusicPlayer의 volume 이라는 필드 이름이 다른 이름으로 변한다고 할 때

MusicPlayer 내부만 변경하면 되고, MusicPlayer 를 사용하는 개발자는 코드를 전혀 변경하지 않아도 됩니다.
물론 외부에서 호출하는  MusicPlayer 의메서드이름을 변경한다면 MusicPlayer 를 사용하는 곳의 코드도 변경해야 합니다.

'Java' 카테고리의 다른 글

[Java] 패키지  (1) 2024.03.07
[Java] 당신만 모르는 생성자가 필요한 진짜 이유  (1) 2024.03.05
[Java] 변수의 초기화, null  (0) 2024.02.28
[Java] 기본형 VS 참조형  (0) 2024.02.28
[Java] 클래스에 배열 사용  (0) 2024.02.26