[Java] 김영한의 실전 자바 기본편 - #3 객체 지향 프로그래밍
[김영한의 실전 자바 - 기본편↗️]을 듣고 정리한 글입니다.
1. 절차 지향 프로그래밍
1.1 프로그래밍 패러다임
- 프로그래밍 방식은 크게 절차 지향 프로그래밍과 객체 지향 프로그래밍으로 나눌 수 있다.
- 지금까지 우리가 작성한 모든 프로그램(실전자바 #1~ #2)는 절차 지향 프로그램이다.
절차 지향 프로그래밍 (Procedural Programming)
- 실행 순서를 중요시하며, 프로그램의 흐름을 순차적으로 따르는 방식이다.
- “어떻게”를 중심으로 프로그래밍하며, 데이터와 해당 데이터에 대한 처리 방식이 분리되어 있다.
객체 지향 프로그래밍 (Object-Oriented Programming)
- 실제 세계의 사물이나 사건을 객체로 보고, 객체 간의 상호작용을 중심으로 프로그래밍하는 방식이다.
- 무엇을” 중심으로 프로그래밍하며, 데이터와 해당 데이터에 대한 행동(메서드)이 하나의 ‘객체’ 안에 포함된다.
중요한 차이점
- 절차 지향: 데이터와 처리 방식의 분리
- 객체 지향: 데이터와 행동(메서드)의 통합
1.2 객체 지향 프로그래밍
아래 문제를 절차 지향에서 객체 지향으로 점진적으로 코드를 변경하면서 객체 지향 프로그래밍을 이해해보자.
문제
음악 플레이어 만들기
요구 사항
- 음악 플레이어를 켜고 끌 수 있어야 한다.
- 음악 플레이어의 볼륨을 증가 및 감소할 수 있어야 한다.
- 음악 플레이어의 상태를 확인할 수 있어야 한다.
출력 예시
음악 플레이어를 시작합니다
음악 플레이어 볼륨: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 데이터 묶음
앞서 작성한 코드에 클래스를 도입하자.
MusicPlayerData
라는 클래스를 생성하여 음악 플레이어와 관련된 데이터(볼륨과 전원 상태)를 멤버 변수로 사용하자.
① 먼저, 음악 플레이어와 관련된 데이터를 묶는 MusicPlayerData
클래스를 정의하자.
package oop1;
public class MusicPlayerData {
int volume = 0; // 볼륨
boolean isOn = false; // 전원 상태
}
② 이제 MusicPlayerMain2
클래스를 수정하여 MusicPlayerData
클래스를 사용하자.
이후에 프로그램 로직이 더 복잡해져서 다양한 변수들이 추가되더라도 음악 플레이어와 관련된 변수들은 MusicPlayerData data
객체에 속해있으므로 쉽게 구분할 수 있다.
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("음악 플레이어를 종료합니다");
}
}
1.3 메서드 추출
코드를 보면 다음과 같이 중복되는 부분들이 있다.
//볼륨 증가
data.volume++;
System.out.println("음악 플레이어 볼륨:" + data.volume);
//볼륨 증가
data.volume++;
System.out.println("음악 플레이어 볼륨:" + data.volume);
- 그리고 각각의 기능들은 이후에 재사용 될 가능성이 높다.
- 음악 플레이어 켜기, 끄기
- 볼륨 증가, 감소
- 음악 플레이어 상태 출력
메서드를 사용해서 중복된 로직을 제거하고 각 기능을 메서드로 추출해보자.
개선된 MusicPlayerMain3 클래스
package oop1;
/**
* 메서드 추출
*/
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 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");
}
}
}
각각의 기능을 메서드로 만든 덕분에 각각의 기능이 모듈화 되었다. 덕분에 다음과 같은 장점이 생겼다.
- 중복 제거: 로직 중복이 제거되었다. 같은 로직이 필요하면 해당 메서드를 여러번 호출하면 된다.
- 변경 영향 범위: 기능을 수정할 때 해당 메서드 내부만 변경하면 된다.
- 메서드 이름 추가: 메서드 이름을 통해 코드를 더 쉽게 이해할 수 있다.
1.4 절차 지향 프로그래밍의 한계
클래스를 사용해서 관련된 데이터를 하나로 묶고, 또 메서드를 사용해서 각각의 기능을 모듈화했다.
하지만, 음악 플레이어의 데이터(MusicPlayerData)와 기능(MusicPlayerMain3)이 분리되어 있어 아래와 같은 문제를 야기할 수 있다.
- 데이터 변경 시 기능을 사용하는 메서드들도 함께 수정해야 할 수 있다.
- 코드 관리 포인트가 두 곳으로 나뉘어 유지보수의 복잡성이 증가한다.
객체 지향 프로그래밍으로의 전환 필요성
- 객체 지향 프로그래밍이 나오기 전까지는 지금과 같이 데이터와 기능이 분리되어 있었다.
- 객체 지향 프로그래밍이 나오면서 데이터와 기능을 하나의 객체로 묶어 관리할 수 있게 되었다.
- 따라서 기능 변경 시 관련 데이터와 메서드가 동일 객체 내에 존재하므로, 코드의 변경 범위가 줄어든다.
2. 클래스와 메서드
자바 같은 객체 지향 언어는 클래스 내부에 속성(데이터)과 기능(메서드)을 함께 포함할 수 있다. 클래스 내부에 멤버 변수 뿐만 아니라 메서드도 함께 포함할 수 있다는 뜻이다
- 먼저, 멤버 변수만 존재하는 간단한 클래스를 작성해 보자.
package oop1;
public class ValueData {
int value; // 데이터인 멤버 변수
}
package oop1;
public class ValueDataMain {
public static void main(String[] args) {
ValueData valueData = new ValueData(); // ValueData 인스턴스 생성
add(valueData); // valueData 객체에 대해 add() 메서드 호출
add(valueData);
add(valueData);
System.out.println("최종 숫자=" + valueData.value); // 최종 값 출력
}
static void add(ValueData valueData) { // ValueData 객체를 인자로 받는 메서드
valueData.value++; // value 값을 증가시킴
System.out.println("숫자 증가 value=" + valueData.value); // 현재 value 값 출력
}
}
- 참고!
- 여기서 만드는
add()
메서드에는 static 키워드를 사용하지 않는다. - 메서드는 객체를 생성해야 호출할 수 있다. 그런데
static
이 붙으면 객체를 생성하지 않고도 메서드를 호출할 수 있다.
- 여기서 만드는
실행 결과
위 코드를 보면 데이터인 value
와 value
의 값을 증가시키는 기능인 add()
메서드가 서로 분리되어 있다.
데이터와 기능을 함께 포함하는 클래스
- 다음으로, 숫자를 증가시키는 기능을 클래스 내부에 포함한
ValueObject
클래스를 정의해 보자.
package oop1;
public class ValueObject {
int value; // 데이터인 멤버 변수
// value를 증가시키는 메서드
void add() {
value++; // value 값을 증가시킴
System.out.println("숫자 증가 value=" + value); // 현재 value 값 출력
}
}
package oop1;
public class ValueObjectMain {
public static void main(String[] args) {
ValueObject valueObject = new ValueObject(); // ValueObject 인스턴스 생성
valueObject.add(); // add() 메서드 호출
valueObject.add();
valueObject.add();
System.out.println("최종 숫자=" + valueObject.value); // 최종 값 출력
}
}
실행 결과
실행 절차
valueObject
는ValueObject
인스턴스이다.add()
메서드를 호출하면 메서드 내부에서value++
가 실행된다.- 이때 value에 접근하는 것은
valueObject
인스턴스의value
이다.
정리!
- 클래스: 속성(데이터, 멤버 변수)과 기능(메서드)을 정의할 수 있다
- 객체: 자신의 메서드를 통해 자신의 멤버 변수에 접근할 수 있다.
- 객체의 메서드: 내부에서 접근하는 멤버 변수는 객체 자신의 멤버 변수이다.
3. 객체 지향 프로그래밍
지금까지 개발한 음악 플레이어는 데이터와 기능이 분리되어 있었다. 이제 데이터와 기능을 하나로 묶어서 음악 플레이어라는 개념을 온전히 하나의 클래스에 담아보자.
- 프로그램의 실행 순서 보다는 음악 플레이어 클래스를 만드는 것 자체에 집중해야 한다.
- 음악 플레이어가 어떤 속성(데이터)을 가지고 어떤 기능(메서드)을 제공하는지 이 부분에 초점을 맞추어야 한다.
음악 플레이어의 속성과 메서드
- 속성:
volume
,isOn
- 기능:
on()
,off()
,volumeUp()
,volumeDown()
,showStatus()
3.1 객체 지향 음악 플레이어
음악 플레이어 클래스 설계
MusicPlayer
클래스를 정의하여 음악 플레이어의 속성과 기능을 함께 포함하도록 하자.
package oop;
public class MusicPlayer {
int volume = 0; // 볼륨
boolean isOn = false; // 전원 상태
// 음악 플레이어 켜기
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() {
System.out.println("음악 플레이어 상태 확인");
if (isOn) {
System.out.println("음악 플레이어 ON, 볼륨: " + volume);
} else {
System.out.println("음악 플레이어 OFF");
}
}
}
음악 플레이어 사용
위에서 정의한 MusicPlayer
클래스를 사용하여 음악 플레이어를 조작하는 코드를 작성하자.
package oop1;
/**
* 객체 지향 프로그래밍을 활용한 음악 플레이어 사용 예제
*/
public class MusicPlayerMain4 {
public static void main(String[] args) {
MusicPlayer player = new MusicPlayer(); // MusicPlayer 객체 생성
// 음악 플레이어 켜기
player.on();
// 볼륨 조절
player.volumeUp();
player.volumeUp();
player.volumeDown();
// 음악 플레이어 상태 확인
player.showStatus();
// 음악 플레이어 끄기
player.off();
}
}
실행 결과
음악 플레이어를 시작합니다
음악 플레이어 볼륨: 1
음악 플레이어 볼륨: 2
음악 플레이어 볼륨: 1
음악 플레이어 상태 확인
음악 플레이어 ON, 볼륨: 1
음악 플레이어를 종료합니다
- MusicPlayer 를 사용하는 코드를 보자. MusicPlayer 객체를 생성하고 필요한 기능(메서드)을 호출하기만 하면 된다.
- MusicPlayer 를 사용하는 입장에서는 MusicPlayer 의 데이터인 volume , isOn 같은 데이터는 전혀 사용하지 않는다.
- MusicPlayer 를 사용하는 입장에서는 이제 MusicPlayer 를 사용하는 입장에서는 단순하게 MusicPlayer 가 제공하는 기능 중에 필요한 기능을 호출해서 사용하기만 하면 되기 때문에, MusicPlayer 내부에 어떤 속성(데이터)이 있는지 전혀 몰라도 된다.
3.2 캡슐화
속성과 기능을 하나로 묶어서 필요한 기능을 메서드를 통해 외부에 제공하는 것을 캡슐화라 한다.
- 사용자는
MusicPlayer
의 내부 데이터(예: volume, isOn)에 직접 접근할 필요 없이 제공된 메서드만 사용하여 플레이어를 조작한다. - 즉,
MusicPlayer
클래스는 데이터(볼륨 및 전원 상태)와 기능(켜기, 끄기, 볼륨 조절, 상태 출력)을 하나의 단위로 묶어 캡슐화의 원칙을 잘 보여준다.
캡슐화 장점
- 캡슐화를 통해 유지보수가 용이해진다.
- 예를 들어, 내부 필드 이름이 변경되거나 메시지 출력 방식이 변경되더라도, 외부 코드에서 MusicPlayer를 사용하는 부분은 수정할 필요가 없다.
- 다만, 메서드 이름이 변경된다면, 호출하는 코드도 함께 변경해야 한다.
4. 문제와 풀이
4.1 절차 지향 직사각형 프로그램을 객체 지향으로 변경하기
문제 설명
- 다음은 직사각형의 넓이(Area), 둘레 길이(Perimeter), 정사각형 여부(square)를 구하는 프로그램이다.
- 절차 지향 프로그래밍 방식으로 되어 있는 코드를 객체 지향 프로그래밍 방식으로 변경해라.
요구 사항
Rectangle
클래스를 만들어라.RectangleOopMain
에 해당 클래스를 사용하는main()
코드를 만들어라.
절차 지향 코드
package oop.ex;
public class RectangleProceduralMain {
public static void main(String[] args) {
int width = 5;
int height = 8;
int area = calculateArea(width, height);
System.out.println("넓이: " + area);
int perimeter = calculatePerimeter(width, height);
System.out.println("둘레 길이: " + perimeter);
boolean square = isSquare(width, height);
System.out.println("정사각형 여부: " + square);
}
static int calculateArea(int width, int height) {
return width * height;
}
static int calculatePerimeter(int width, int height) {
return 2 * (width + height);
}
static boolean isSquare(int width, int height) {
return width == height;
}
}
실행 결과
넓이: 40
둘레 길이: 26
정사각형 여부: false
내 풀이
package _03_oop.ex;
public class RectangleOopMain {
public static void main(String[] args) {
Rectangle rectangle = new Rectangle();
rectangle.width = 5;
rectangle.height = 8;
// 넓이
rectangle.calculateArea();
// 둘레
rectangle.calculatePerimeter();
// 정사각형 여부
rectangle.isSquare();
}
}
package _03_oop.ex;
public class Rectangle {
int width;
int height;
// 넓이
void calculateArea() {
int area = width * height;
System.out.println("넓이: " + area);
}
// 둘레
void calculatePerimeter() {
int perimeter = 2 * (width + height);
System.out.println("둘레 길이: " + perimeter);
}
// 정사각형 여부
void isSquare() {
boolean square = width == height;
System.out.println("정사각형 여부: " + square);
}
}
4.2 객체 지향 계좌
문제 설명
은행 계좌를 객체로 설계해야 한다.
요구 사항
Account
클래스를 만들어라.int balance
: 잔액deposit(int amount)
: 입금 메서드- 입금시 잔액이 증가한다.
withdraw(int amount)
: 출금 메서드- 출금시 잔액이 감소한다.
- 만약 잔액이 부족하면 잔액 부족을 출력해야 한다.
AccountMain
클래스를 만들고main()
메서드를 통해 프로그램을 시작해라.- 계좌에 10000원을 입금해라.
- 계좌에서 9000원을 출금해라.
- 계좌에서 2000원을 출금 시도해라. ->
잔액 부족 출력
을 확인해라. - 잔고를 출력해라.
잔고: 1000
실행 결과
잔액 부족
잔고: 1000
내 풀이
package _03_oop.ex;
public class Account {
int balance; // 잔액
// 입금
void deposit(int amount) {
balance += amount;
}
// 출금
void withdraw(int amount) {
if (balance > amount) {
balance -= amount;
} else {
System.out.println("잔액 부족");
}
}
// 잔액 확인
void showAmout() {
System.out.println("잔고: " + balance);
}
}
package _03_oop.ex;
public class AccountMain {
public static void main(String[] args) {
Account account=new Account();
// 입금
account.deposit(10000);
// 출금
account.withdraw(9000);
account.withdraw(2000);
// 잔액 확인
account.showAmout();
}
}
댓글남기기