개발 공부/[java]

[java] 김영한의 실전 자바 기본편 / 강의 정리 / 3. 객체 지향 프로그래밍 1

죽밥죽밥화이팅 2024. 3. 16. 20:41

해당 내용은 인프런에 있는 [김영한의 실전 자바 기본편] 강의를 보고 참고하여 정리한 글이다.

혹시 문제가 된다면 꼭 알려주시길 바랍니도...😢

 

 

 

절차 지향 프로그래밍 - 시작

 

절차 지향 프로그래밍 vs 객체 지향 프로그래밍

프로그래밍 방식은 크게 절차 지향, 객체 지향 프로그래밍으로 나눌 수 O

 

절차 지향 프로그래밍

  • 절차 지향 프로그래밍은 이름 그대로 절차를 지향함 / 실행 순서를 중요하게 생각하는 방식
  • 절차 지향 프로그래밍은 프로그램의 흐름을 순차적으로 따르며 처리하는 방식 / "어떻게"를 중심으로 프로그래밍

 

객체 지향 프로그래밍

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

 

둘의 중요한 차이

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

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

앞서 배운 것 처럼 단순히 객체를 사용하기만 하면 객체 지향 프로그래밍이라 할 수 있을까?

사실 지금까지 우리가 작성한 모든 프로그램은 절차 지향 프로그램

그렇담 무엇이 객체 지향 프로그래밍인가?

 

 

 

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

요구사항: 

1. 음악 플레이어를 켜고 끌 수 있어야 함

2. 음악 플레이어의 볼륨을 증가, 감소할 수 있어야 함

3. 음악 플레이어의 상태를 확인할 수 있어야 함

 

예시 출력)

음악 플레이어를 시작합니다.

음악 플레이어 볼륨: 1

음악 플레이어 볼륨: 2

음악 플레이어 볼륨: 1

음악 플레이어 상태 확인

음악 플레이어 ON, 볼륨: 1

음악 플레이어를 종료합니다.

 

 

절차 지향 음악 플레이어 1

package oop1;

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);

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

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

    }
}

 

실행 결과)

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

 

순서대로 프로그램이 작동하도록 단순하게 작성 / 점진적으로 변경 go

 

 

절차 지향 프로그래밍 2 - 데이터 묶음

앞서 작성한 코드에 클래스 도입 => MusicPlayerData라는 클래스 만들고, 음악 플레이어에 사용되는 데이터들을 묶어서 멤버 변수로 사용하자

 

 

절차 지향 프로그래밍 2 - 데이터 묶음

package oop1;

public class MusicPlayerData {
    int volume = 0;
    boolean isOn = false;
}

음악 플레이어에 사용되는 volume, isOn 속성을 MusicPlayerData 의 멤버 변수에 포함

 

package oop1;

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

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

        //볼륨 증가
        data.volume++;
        System.out.println("음악 플레이어 볼륨: " + data.volume);

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

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

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

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

    }
}

 

음악 플레이어와 관련된 데이터는 MusicPlayerData 클래스에 존재

이 클래스를 사용하도록 기존 로직 변경함

이후 로직이 복잡해져 다양한 변수들이 추가되더라도 음악 플레이어와 관련된 변수들은 MusicPlayerData data 객체에 속해있으므로 쉽게 구분 가능

 

 

절차 지향 프로그래밍 3 - 메서드 추출

코드를 보면 중복되는 부분이 있음 (해결 방법: 메서드 사용)

//볼륨 증가
data.volume++;
System.out.println("음악 플레이어 볼륨: " + data.volume);

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

 

그리고 각각 기능들은 이후에 재사용 될 가능성이 높음 

-음악 플레이어 켜기, 끄기

-볼륨 증가, 감소

-음악 플레이어 상태 출력

 

메서드를 사용해서 각각의 기능을 구분해보자

 

 

절차 지향 프로그래밍 3 - 메서드 추출

package oop1;

public class MusicPlayerMain2 {
    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 off(MusicPlayerData data) {
        data.isOn = false;
        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 showStatus(MusicPlayerData data) {
        System.out.println("음악 플레이어 상태 확인");
        if (data.isOn) {
            System.out.println("음악 플레이어 ON, 볼륨:" + data.volume);
        } else {
            System.out.println("음악 플레이어 OFF");
        }
    }
}

 

 

각각의 기능을 메서드로 만든 덕분에 기능이 모듈화 됨

장점

  • 중복제거: 로직 중복이 제거 / 같은 로직이 필요하면 해당 메서드를 여러 번 호출하면 됨
  • 변경 영향 범위: 기능을 수정할 때 해당 메서드 내부만 변경하면 됨
  • 메서드 이름 추가: 메서드 이름을 통해 코드를 더 쉽게 이해할 수 있음

모듈화: 쉽게 이야기해서 레고 블럭을 생각하면 됨 / 필요한 블럭을 가져다 사용할 수 있음

음악 플레이어의 기능이 필요하면 해당 기능을 메서드 호출 만으로 사용할 수 있음 / 이제 음악 플레이어와 관련된 메서드를 조립하여 프로그램을 작성할 수 있음

 

 

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

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

덕분에 상당히 깔끔하고 읽기 좋고, 유지보수 하기 좋은 코드를 작성할 수 있었음 / But 여기서 더 개선은 불가능한가?

 

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

음악 플레이어의 데이터는 MusicPlayerData에 있는데, 그 데이터를 사용하는 기능은 MusicPlayerMain3에 있는 각각의 메서드에 분리되어 있음 / 그래서 음악 플레이어와 관련된 데이터는 MusicPlayerData를 사용해야 하고, 음악 플레이어와 관련된 기능은 MusicPlayerMain3의 각 메서드를 사용해야 함

 

데이터와 그 데이터를 사용하는 기능은 매우 밀접하게 연관되어 있음 / 각각의 메서드를 보면 대부분 MusicPlayerData의 데이터를 사용함 / 따라서 이후에 관련 데이터가 변경되면 MusicPlayerMain3 부분의 메서드들도 함께 변경해야 함 / 이렇게 데이터 기능이 분리되어 있으면 유지보수 관점에서 관리 포인트가 2곳으로 늘어남

 

객체 지향 프로그래밍이 나오기 전까지는 지금과 같이 데이터와 기능이 분리되어 있었음 / 따라서 지금의 코드가 최선이었음 / But, 객체 지향 프로그래밍이 나오면서 데이터와 기능을 온전히 하나로 묶어서 사용할 수 있게 됨

 

데이터와 기능을 하나로 온전히 묶는다는 것이 어떤 의미인지 이해하기 위해 간단한 예제 만들어보자

 

 

 

메서드와 클래스

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

먼저 멤버 변수만 존재하는 클래스로 간단한 코드 작성해보자

 

package oop1;

public class ValueData {
    int value;
}
package oop1;

public class ValueDataMain {
    public static void main(String[] args) {
        ValueData valueData = new ValueData(); //1. new~,, 인스턴스 만들고
        add(valueData); //2. 참조값을 넘겨서 3번 증가 시키고
        add(valueData);
        add(valueData);
        System.out.println("최종 숫자 = " + valueData.value); //3. 결과값 확인
    }

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

 

실행결과

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

 

 

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

코드를 보면 데이터인 value와 value의 값을 증가시키는 기능인 add() 메서드가 서로 분리되어 있음

 

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

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

 

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

package oop1;

public class ValueData {
    int value;

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

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

클래스가 어떻게 사용되는지 보자

 

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

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

static에 대한 자세한 내용은 뒤에서 설명함

 

package oop1;

public class ValueObjectMain {
    public static void main(String[] args) {
        ValueData valueData = new ValueData(); //1. new~,, 인스턴스 만들고
        valueData.add();
        valueData.add();
        valueData.add();
        System.out.println("최종 숫자 = " + valueData.value); //3. 결과값 확인
    }
}

 

실행 결과)

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

 

인스턴스 생성

ValueData valueData = new ValueData(); //1. new~,, 인스턴스 만들고

 

인스턴스의 메서드를 호출하는 방법은 멤버 변수를 사용하는 방법과 동일함 / .(dot)를 찍어서 객체 접근한 다음에 원하는 메서드 호출

ValueData valueData = new ValueData(); //1. new~,, 인스턴스 만들고
x002.add(); // 2. x002 ValueObject 인스턴스에 있는 add()메서드 호출

3. add() 메서드를 호출하면 메서드 내부에서 value++ 을 호출하게 됨 / 이때 value에 접근해야 하는데, 기본으로 본인 인스턴스에 있는 멤버 변수에 접근  / 본인 인스턴스가 x002 참조값을 사용하므로 자기 자신인 x002.value에 접근하게 됨

4. ++연산으로 value의 값을 하나 증가시킴

 

정리

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