객체지향 프로그래밍

박병욱·2025년 5월 19일

Java

목록 보기
3/38
post-thumbnail

🚩 절차지향 프로그래밍

프로그래밍 방식에는 크게 절차지향 프로그래밍객체지향 프로그래밍으로 구분할 수 있다.

<절차지향 프로그래밍>

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

<객체지향 프로그래밍>

  • 말 그대로 객체를 중요하게 생각하는 방식이다.
  • 실제 세계의 사물이나 사건을 객체로 보고, 이런 객체들 간의 상호작용을 중심으로 프로그래밍하는 방식이다. 즉, “무엇을” 중심으로 프로그래밍 하는 것이다.

 

둘의 중요한 차이가 있는데, 절차 지향은 데이터와 해당 데이터에 대한 처리 방식이 분리되어 있는 반면, 객체지향은 데이터와 그 데이터에 대한 행동, 즉 메서드가 하나의 객체 안에 포함되어 있다는 것이다.

지금까지는 클래스와 객체를 사용해서 관련 데이터를 묶어서 사용했다. 그럼 그게 객체 지향 프로그래밍 아닌가? 지금까지 수행했던 프로그래밍은 모두 절차 지향적이었다. “객체지향” 이라는 의미를 더 명확히 이해하기 위해 절차 지향부터 점진적으로 코드를 변경해 가면서 이해해보자.


🎼 문제 : 음악 플레이어 만들기

<요구 사항>

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

 

먼저 절차지향적으로 프로그래밍 했을 때는 코드가 아래와 같을 것이다.

package oop1;

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

        isOn = true;
        System.out.println("음악 플레이어를 시작합니다.");

        volumn++;
        System.out.println("음악 플레이어 볼륨:" + volumn);
        volumn++;
        System.out.println("음악 플레이어 볼륨:" + volumn);

        volumn--;
        System.out.println("음악 플레이어 볼륨:" + volumn);

        System.out.println("음악 플레이어 상태 확인");
        if (isOn) {
            System.out.println("음악 플레이어 ON, 볼륨:" + volumn);
        } else {
            System.out.println("음악 플레이어 OFF");
        }

        isOn = false;
        System.out.println("음악 플레이어를 종료합니다.");

    }
}

 

이제 MusicPlayerData라는 클래스를 도입해서, 사용되는 데이터들을 묶어 멤버 변수로 사용해보자.

package oop1;

public class MusicPlayerData {
    int volumn;
    boolean isOn;
}
package oop1;

public class MusicPlayerMain2 {
    public static void main(String[] args) {
        MusicPlayerData data = new MusicPlayerData();

        data.isOn = true;
        System.out.println("음악 플레이어를 시작합니다.");

        data.volumn++;
        System.out.println("음악 플레이어 볼륨:" + data.volumn);
        data.volumn++;
        System.out.println("음악 플레이어 볼륨:" + data.volumn);

        data.volumn--;
        System.out.println("음악 플레이어 볼륨:" + data.volumn);

        System.out.println("음악 플레이어 상태 확인");
        if (data.isOn) {
            System.out.println("음악 플레이어 ON, 볼륨:" + data.volumn);
        } else {
            System.out.println("음악 플레이어 OFF");
        }

        data.isOn = false;
        System.out.println("음악 플레이어를 종료합니다.");
    }
}

MusicPlayerData 클래스를 사용해서 기존 로직을 변경했다. 이후에 로직이 더 복잡해져서 다양한 변수들이 추가되더라도 클래스의 멤버 변수로 추가함으로써 쉽게 사용하고, 구분할 수 있을 것이다.

 

하지만, 현재 코드에서 중복되는 부분들이 상당히 많다. 그리고 각각의 기능들은 이후에 재사용 될 가능성이 매우 높기 때문에 메서드를 사용해서 각각의 기능을 구분해보도록 하자.

package oop1;

public class MusicPlayerMain3 {
    public static void main(String[] args) {
        MusicPlayerData data = new MusicPlayerData();

		// 플레이어를 키는 기능
        on(data);

		// 볼륨을 높이는 기능
        volumnUp(data);
        volumnUp(data);

		// 볼륨을 낮추는 기능
        volumnDown(data);

		// 플레이어의 상태를 확인하는 기능
        status(data);

		// 플레이어를 끄는 기능
        off(data);
    }

    static void on(MusicPlayerData data) {
        data.isOn = true;
        System.out.println("음악 플레이어를 시작합니다.");
    }

    static void off(MusicPlayerData data) {
        data.isOn = false;
        System.out.println("음악 플레이어를 종료합니다.");
    }

    static void volumnUp(MusicPlayerData data) {
        data.volumn++;
        System.out.println("음악 플레이어 볼륨:" + data.volumn);
    }

    static void volumnDown(MusicPlayerData data) {
        data.volumn--;
        System.out.println("음악 플레이어 볼륨:" + data.volumn);
    }

    static void status(MusicPlayerData data) {
        System.out.println("음악 플레이어 상태 확인");
        if (data.isOn) {
            System.out.println("음악 플레이어 ON, 볼륨:" + data.volumn);
        } else {
            System.out.println("음악 플레이어 OFF");
        }
    }
}

이제 각각의 기능을 메서드로 만들어 모듈화시켰다. 덕분에 로직의 중복이 제거되었다. 이제 같은 로직이 필요하다면 해당 메서드를 원하는 만큼 호출하면 되고 기능을 수정하고 싶을 때, 해당 메서드 내부만 변경하면 된다.

 

🤔 모듈화가 뭐지?

“레고 블록” 을 상상하면 된다. 필요한 블록을 가져다 꼽아서 사용할 수 있다. 현재 음악 플레이어의 기능이 필요하면 해당 기능을 메서드 호출 만으로 손쉽게 사용할 수 있다. 이제 관련된 메서드를 조립해서 프로그램을 작성할 수 있다는 말이다.

 

💦 절차지향 프로그래밍의 한계

이처럼 클래스를 사용해서 관련된 데이터를 묶고, 메서드를 사용해서 각각의 기능을 모듈화했다. 지금도 충분히 읽기 쉽고, 깔끔한 코드라고 생각된다. 근데 여기서 코드를 더 개선할 방법은 없을까?

현재 작성한 코드의 한계는 “데이터와 기능이 분리” 되어 있다는 것이다. 현재 MusicPlayerData에 있는 데이터를 사용하는 기능은 MusicPlayerMain3에 있는 각각의 메서드에 분리되어 있다. 그래서 음악 플레이어와 관련된 데이터는 MusicPlayerData에서 끌어와야 하고, 기능은 MusicPlayerMain3의 각각의 메서드를 사용해야 하는 것이다.

데이터와 그 데이터를 사용하는 기능은 매우 밀접하게 연관되어 있다. 위 코드에서 각각의 메서드를 보면, 대부분 MusicPlayerData의 데이터를 사용한다. 따라서 나중에 관련 데이터가 변경되면 MusicPlayerMain3로 다시 가서 관련된 메서드도 변경해야 할 것이다. 이런 식으로 분리되어 있으니 유지보수 관점에서는 두 군데를 신경 써야 하는 것이다.

객체지향 프로그래밍이 등장하기 전까지는 이런 식으로 데이터와 기능이 분리되어 있었다. 하지만, 객체지향 프로그래밍이 나오면서 데이터와 기능을 온전히 하나로 묶어 사용할 수 있게 되었다.


👭 클래스와 메서드

지금까지는 클래스에 멤버 변수만 넣었지만, 사실 기능 역할을 하는 메서드도 포함할 수 있다. 간단한 예제를 살펴보자.

package oop1;

public class ValueData {
    int value;
}
package oop1;

public class ValueDataMain {

    public static void main(String[] args) {
        ValueData vd = new ValueData();

        add(vd);
        add(vd);
        add(vd);

        System.out.println("최종 숫자=" + vd.value);
    }

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

위의 코드를 보면, value 데이터와 value의 값을 증가시키는 add() 메서드가 서로 분리되어 있는 것을 볼 수 있다. 이제 add() 메서드도 클래스에 포함시켜 새로운 클래스를 정의해보도록 하자.

 

<ValueObject 클래스 정의>

package oop1;

public class ValueObject {
    int value;

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

이처럼 ValueObject 클래스와 같이 value 데이터와 add() 메서드를 함께 정의할 수 있다.

 

🤔 add() 메서드에 왜 static 키워드가 안 붙지?

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

이제 ValueObject 클래스를 사용하는 코드를 작성해보자.

package oop1;

public class ValueObjectMain {

    public static void main(String[] args) {
        ValueObject vd = new ValueObject();
        vd.add();
        vd.add();
        vd.add();

        System.out.println("최종 숫자=" + vd.value);
    }
}

확인해보면 결과는 동일하다. 이 과정을 그림으로 한번 이해해보자.

 

<인스턴스 생성>

ValueObject라는 객체를 생성했는데, 이 객체는 멤버 변수 뿐만 아니라 add() 메서드도 함께 존재한다.

 

<메서드 호출 시>

인스턴스의 메서드를 호출하는 것은 멤버 변수를 사용하는 방법과 동일하다. 메서드를 호출하면, 메서드 내부에서 value++를 호출하게 되는 것이다. 이때 value에 접근하게 되는데, “자기 자신의 멤버 변수에 접근하는 것” 이다. 본인 인스턴스가 x002를 참조값을 사용하기 때문에 자기 자신인 x002.value에 접근한다는 말이다. 그리고 value 값을 하나 증가시킨다.


🍱 객체지향 프로그래밍

이제 데이터와 기능을 하나로 묶어서 음악 플레이어라는 개념을 온전하게 하나의 클래스에 담아보도록 하자. 그러기 위해서는, 프로그램의 실행 순서보다는 음악 플레이어 클래스를 만드는 것 자체에 집중해야 한다. 어떤 속성(데이터)을 가지고, 어떤 기능(메서드)을 제공하는지에 집중하자는 것이다.

  • 속성 : volumn, isOn
  • 기능 : on(), off(), volumnUp(), volumnDown(), showStatus()

 

package oop1;

public class MusicPlayer {
    int volumn;
    boolean isOn;

    void on() {
        isOn = true;
        System.out.println("음악 플레이어를 시작합니다.");
    }

    void off() {
        isOn = false;
        System.out.println("음악 플레이어를 종료합니다.");
    }

    void volumnUp() {
        volumn++;
        System.out.println("음악 플레이어 볼륨:" + volumn);
    }

    void volumnDown() {
        volumn--;
        System.out.println("음악 플레이어 볼륨:" + volumn);
    }

    void showStatus() {
        if (isOn) {
            System.out.println("음악 플레이어 ON, 볼륨:" + volumn);
        } else {
            System.out.println("음악 플레이어 OFF");
        }
    }
}

MusicPlayer 클래스에 음악 플레이어에 필요한 모든 속성과 기능을 정의했다. 이제 이 클래스만 있으면 온전한 음악 플레이어를 생성해서 사용할 수 있는 것이다.

package oop1;

public class MusicPlayerMain4 {
    public static void main(String[] args) {
        MusicPlayer mp = new MusicPlayer();

        mp.on();
        mp.volumnUp();
        mp.volumnUp();
        mp.volumnDown();
        mp.showStatus();
        mp.off();
    }
}

이처럼 MusicPlayer를 사용하는 입장에서는 MusicPlayer의 데이터인 volumn, isOn 같은 데이터는 전혀 사용하지 않는다. 이제 MusicPlayer 내부에 어떤 속성이 있는지 전혀 몰라도 된다는 것이다. 단순히 클래스가 제공하는 기능 중에 필요한 기능을 호출해서 사용하기만 하면 된다.

 

💊 캡슐화

MusicPlayer를 보면 인스턴스를 구성하기 위한 속성과 기능이 마치 하나의 캡슐에 쌓여있는 것 같다. 이런 식으로 속성과 기능을 하나로 묶어 외부에 제공하는 것을 “캡슐화” 라고 한다. 사용자 입장에서는 코드를 더 쉽게 읽을 수 있고, 변경하는 것도 손쉽게 할 수 있다. 예를 들어, MusicPlayer의 내부 코드가 변한다고 해도 다른 코드는 변경하지 않아도 된다. 속성 이름이 변해도 클래스 내부만 변경하면 되고, 출력 메시지를 변경할 때도 클래스 내부만 쳐다보면 된다. 이처럼 MusicPlayer를 사용하는 개발자는 코드를 전혀 변경하지 않아도 된다.

0개의 댓글