인터페이스 활용 예제
그럼, 이제 좀 더 구체적인 코드 예제를 통해서 왜 인터페이스를 사용하고, 인터페이스가 가지는 장점이 무엇인지에 대한 좀 더 구체적인 내용들을 살펴보도록 하겠습니다.
먼저 인터페이스를 사용하지 않는 경우에 발생할 수 있는 어려움을 가상의 시나리오를 통해서 알아보고, 인터페이스가 이를 어떻게 보완할 수 있는지에 대해서 설명하도록 하겠습니다.
먼저 다음의 시나리오를 생각해 봅시다.
카페를 운영하는 사람이 있습니다.
단골손님들은 매일 마시는 음료가 정해져 있습니다.
단골손님A는 항상 아이스 아메리카노를 주문합니다.
단골손님B는 매일 아침 딸기라떼를 구매합니다.
위 내용을 코드로 바꿔보면 다음과 같이 작성할 수 있습니다.
//카페 손님
public class CafeCustomer {
public String CafeCustomerName;
public void setCafeCustomerName(String cafeCustomerName) {
this.CafeCustomerName = cafeCustomerName;
}
}
//CafeCustomer 클래스로부터 단골손님A와 단골손님B 상속
public class CafeCustomerA extends CafeCustomer {
}
public class CafeCustomerB extends CafeCustomer {
}
//카페 사장님
public class CafeOwner {
public void giveItem(CafeCustomerB cafeCustomerB) {
System.out.println("give a glass of strawberry latte to CafeCustomer B");
}
public void giveItem(CafeCustomerA cafeCustomerA) {
System.out.println("give a glass of iced americano to CafeCustomer A");
}
}
//메뉴 주문
public class OrderExample {
public static void main(String[] args) throws Exception {
CafeOwner cafeowner = new CafeOwner();
CafeCustomerA a = new CafeCustomerA();
CafeCustomerB b = new CafeCustomerB();
cafeowner.giveItem(a);
cafeowner.giveItem(b);
}
}
// 출력값
give a glass of iced americano to CafeCustomer A
give a glass of strawberry latte to CafeCustomer B
위의 예시를 보면, 단골손님 A와 단골손님 B는 CafeCustomer 클래스로부터 확장되었고, 카페 주인은 CafeOwner 클래스로 정의했습니다.
그리고 단골손님 A와 B가 올 때 메서드 오버로딩을 사용하여 giveItem 메서드를 호출하고, OrderExample 클래스에서 객체를 생성하여 실행시키면 출력값과 같은 메시지가 반환되고 있습니다.
그런데 만약 단골손님이 두 명이 아니라 계속 늘어나면 어떨까요?
이 경우 매번 CafeOwner는 오버로딩한 메서드를 만들어야 하기 때문에 매우 번거로워질 수 있습니다.
이런 경우에 인터페이스를 활용할 수 있습니다.
인터페이스를 사용하여 코드를 작성하는 예제를 순차적으로 살펴보도록 하겠습니다.
먼저 class 키워드 대신 interface 키워드를 사용하여 Customer 인터페이스를 생성합니다.
앞서 설명했듯, 인터페이스는 상수와 추상 메서드만을 구성 멤버로 가집니다.
public interface Customer {
// 상수
// 추상 메서드
}
다음으로, 앞서 학습한 것처럼 implements 키워드를 사용하여 Customer 인터페이스를 각각 구현한 CafeCustomerA와 CafeCustomerB를 정의합니다.
public class CafeCustomerA implements Customer {
}
public class CafeCustomerB implements Customer {
}
위에서 인터페이스 없이 작성한 코드에서는 손님의 수가 늘어날수록 구현 클래스를 계속 추가하여 만들어줘야 하는 번거로움이 존재했지만, 인터페이스와 앞서 학습한 참조변수의 형 변환을 사용하면 아래의 코드처럼 Customer 타입을 매개변수로 전달함으로써 추가적인 손님이 등장할 때마다 매번 새롭게 메서드를 작성해야 하는 번거로움을 없앨 수 있습니다.
// 기존 코드
public class CafeOwner {
public void giveItem(CafeCustomerB cafeCustomerB) {
System.out.println("give a glass of strawberry latte to CafeCustomer B");
}
public void giveItem(CafeCustomerA cafeCustomerA) {
System.out.println("give a glass of iced americano to CafeCustomer A");
}
}
// 인터페이스를 활용하여 작성한 코드
public class CafeOwner {
public void giveItem(Customer customer) {
System.out.println("??????????");
}
}
하지만 또 한 가지 문제점이 존재하는데, 현재 작성된 코드로는 각 단골손님이 주문한 내용을 개별적으로 주문하기가 어렵다는 것입니다.
이 문제를 해결하기 위해 기존의 인터페이스에 getOrder라는 추상 메서드를 인터페이스 Customer에 추가하고, 이를 활용하여 코드를 재작성해 봅시다.
Customer 인터페이스에 불완전하게 정의되어 있는 getOrder() 메서드가 각각의 객체에 맞게 구현부에서 정의되고 있는 모습을 확인해 보실 수 있습니다.
public interface Customer {
public abstract String getOrder();
}
public class CafeCustomerA implements Customer {
public String getOrder(){
return "a glass of iced americano";
}
}
public class CafeCustomerB implements Customer {
public String getOrder(){
return "a glass of strawberry latte";
}
}
그리고 위와 같이 CafeOwner 클래스를 재정의하여 매개변수로 Customer 타입이 입력될 수 있게끔 해주면, 매개변수의 다형성에 의해 Customer를 통해 구현된 객체 모두가 들어올 수 있습니다.
public class CafeOwner {
public void giveItem(Customer customer) {
System.out.println("Item : " + customer.getOrder());
}
}
다시 OrderExample 클래스를 통해 테스트를 해보면 다음과 같은 결과를 얻을 수 있습니다.
public class OrderExample {
public static void main(String[] args) throws Exception {
CafeOwner cafeowner = new CafeOwner();
Customer cafeCustomerA = new CafeCustomerA();
Customer cafeCustomerB = new CafeCustomerB();
cafeowner.giveItem(cafeCustomerA);
cafeowner.giveItem(cafeCustomerB);
}
}
// 출력값
Item : a glass of iced americano
Item : a glass of strawberry latte
이제 이 모든 작업들을 하나로 이어 붙여볼까요?
interface Customer {
String getOrder();
}
class CafeCustomerA implements Customer {
public String getOrder(){
return "a glass of iced americano";
}
}
class CafeCustomerB implements Customer {
public String getOrder(){
return "a glass of strawberry latte";
}
}
class CafeOwner {
public void giveItem(Customer customer) {
System.out.println("Item : " + customer.getOrder());
}
}
public class OrderExample {
public static void main(String[] args) throws Exception {
CafeOwner cafeowner = new CafeOwner();
Customer cafeCustomerA = new CafeCustomerA();
Customer cafeCustomerB = new CafeCustomerB();
cafeowner.giveItem(cafeCustomerA);
cafeowner.giveItem(cafeCustomerB);
}
}
// 출력값
Item : a glass of iced americano
Item : a glass of strawberry latte
어떤가요?
최초에 인터페이스를 사용하지 않았을 때 손님의 수만큼 giveItem() 메서드가 필요했던 CafeOwner 클래스가 Customer 인터페이스 사용 후에 단 한 개의 giveItem 메서드로 구현이 가능해졌습니다.
여기서 중요한 부분은 메서드의 개수가 줄었다는 점보다는 CafeOwner 클래스가 더 이상 손님에게 의존적인 클래스가 아닌 독립적인 기능을 수행하는 클래스가 되었다는 점입니다.
위의 코드 예제를 반복적으로 보면서 인터페이스를 어떻게 사용하고, 인터페이스가 어떻게 유용할 수 있는지 천천히 고민해 봅시다.
다형성(Polymorphism)
이번 챕터에서는 세 번째 객체지향의 기둥이자 자바 객체지향 프로그래밍의 가장 핵심적인 부분이라 할 수 있는 다형성에 대해서 알아보겠습니다.
다음의 학습 목표를 통해 이번 챕터의 학습 내용을 먼저 확인해 봅시다.
학습 목표
- 자바 객체지향 프로그래밍에서 다형성이 가지는 의미와 장점을 이해할 수 있다.
- 참조변수의 타입 변환에 대한 내용을 이해하고, 업캐스팅과 다운캐스팅의 차이를 설명할 수 있다.
- instanceof 연산자를 언제 어떻게 활용할 수 있는지 이해하고 설명할 수 있다.
- 코딩 예제를 실제로 입력해 보면서 다형성이 실제로 어떻게 활용되는지 이해할 수 있다.
다형성
이제 세 번째 객체지향 프로그래밍의 기둥이자 객체지향 프로그래밍에서 가장 중요한 부분이라 할 수 있는 다형성(polymorphism)에 대해서 알아보겠습니다.
일반적인 의미에서 다형성이란 "여러 개"를 의미하는 poly와 어떤 ‘형태' 또는 ‘실체’를 의미하는 morphism의 결합어로 하나의 객체가 여러 가지 형태를 가질 수 있는 성질을 의미합니다.
그렇다면 자바에서 다형성이란 무엇일까요?
자바 프로그래밍에서 다형성은 한 타입의 참조 변수를 통해 여러 타입의 객체를 참조할 수 있도록 만든 것을 의미합니다.
좀 더 구체적으로 이야기하면, 상위 클래스 타입의 참조 변수를 통해서 하위 클래스의 객체를 참조할 수 있도록 허용한 것이라 할 수 있습니다.
다음의 예시를 통해 한번 살펴보겠습니다. 꼭 코드를 직접 입력하면서 흐름을 이해해 보세요.
// 참조 변수의 다형성 예시
class Friend {
public void friendInfo() {
System.out.println("나는 당신의 친구입니다.");
}
}
class BoyFriend extends Friend {
public void friendInfo() {
System.out.println("나는 당신의 남자친구입니다.");
}
}
class GirlFriend extends Friend {
public void friendInfo() {
System.out.println("나는 당신의 여자친구입니다.");
}
}
public class FriendTest {
public static void main(String[] args) {
Friend friend = new Friend(); // 객체 타입과 참조 변수 타입의 일치
BoyFriend boyfriend = new BoyFriend();
Friend girlfriend = new GirlFriend(); // 객체 타입과 참조 변수 타입의 불일치
friend.friendInfo();
boyfriend.friendInfo();
girlfriend.friendInfo();
}
}
// 출력값
// 나는 당신의 친구입니다.
// 나는 당신의 남자친구입니다.
// 나는 당신의 여자친구입니다.
앞서 우리는 객체의 타입과 일치하는 타입의 참조 변수를 사용해 왔습니다.
위의 예시에서 참조 변수 friend와 boyfriend 모두 각각 Friend와 BoyFriend라는 타입과 일치하는 참조 변수 타입을 사용하는 것을 확인할 수 있습니다.
하지만 그다음 라인을 확인해 보면 GirlFriend 클래스의 인스턴스를 생성하고 그것을 Friend 타입의 참조 변수 girlfriend에 할당하고 있습니다.
원래라면 타입을 일치시키기 위해 GirlFriend를 참조 변수의 타입으로 지정해주어야 하지만, 그러지 않고 상위 클래스 Friend를 타입으로 지정해주고 있습니다.
이 경우, 상위 클래스를 참조 변수의 타입으로 지정했기 때문에 자연스럽게 참조 변수가 사용할 수 있는 멤버의 개수는 상위 클래스의 멤버의 수가 됩니다.
이것이 앞서 설명했던 ‘상위 클래스 타입의 참조 변수로 하위 클래스의 객체를 참조하는 것’이자 다형성의 핵심적인 부분이라 할 수 있습니다.
사용할 수 있는 멤버가 오히려 줄어드는 데 이게 어떻게 장점이 될까요?
이번 챕터와 다음 챕터의 추상화에 대한 내용을 학습해가면서 천천히 알아봅시다.
다음으로, 또 한 가지 기억해야 할 다형성의 핵심 중에 하나는 상위 클래스의 타입으로 하위 클래스 타입의 객체를 참조하는 것은 가능하지만, 그 반대는 성립되지 않는다는 것입니다.
다음의 예시를 확인해 봅시다.
public class FriendTest {
public static void main(String[] args) {
Friend friend = new Friend(); // 객체 타입과 참조 변수 타입의 일치 -> 가능
BoyFriend boyfriend = new BoyFriend();
Friend girlfriend = new GirlFriend(); // 객체 타입과 참조 변수 타입의 불일치 -> 가능
// GirlFriend friend1 = new Friend(); -> 하위클래스 타입으로 상위클래스 객체 참조 -> 불가능
friend.friendInfo();
boyfriend.friendInfo();
girlfriend.friendInfo();
}
}
위의 예시에서 확인할 수 있듯이 상위 클래스인 Friend 타입으로 하위 클래스 GirlFriend를 참조하는 것은 가능하지만, 그 반대로 하위 클래스 GirlFriend의 타입으로 상위 클래스 객체 Friend를 참조하는 것은 불가능합니다.
그 이유는 실제 객체인 Friend의 멤버 개수보다 참조 변수 friend1이 사용할 수 있는 멤버 개수가 더 많기 때문입니다.
좀 더 구체적으로 설명하면, 실제 참조하고 있는 인스턴스의 멤버를 기준으로 참조 변수의 타입의 멤버가 실제 인스턴스의 멤버 수보다 작은 것은 실제 사용할 수 있는 기능을 줄이는 것이기에 허용되지만, 그 반대의 경우는 참조하고 있는 인스턴스에 실제로 구현된 기능이 없어 사용이 불가하기 때문입니다.
마지막으로, 조금 다른 맥락에서 앞서 배웠던 메서드 오버라이딩과 메서드 오버로딩 또한 다형성의 한 예시라 할 수 있습니다.
메서드 오버라이딩과 메서드 오버로딩이 둘 다 같은 이름의 메서드를 재사용 또는 덮어쓰기로 다르게 사용한다는 점을 기억할 때, 앞서 "하나의 객체가 여러 가지 형태를 가질 수 있는 성질"라 정의할 수 있었던 다형성의 의미와 궤를 같이하는 것이라 이해할 수 있습니다.
아직 다형성이 어떤 것이고, 어떻게 유용하게 활용될 수 있는지 감이 아직 이해가 어려우리라 생각합니다.
앞으로 많은 예제를 보면서 점차 익숙해질 수 있으니 당장 이해가 어렵다면, '아 이런 거구나'라는 대략적인 개념을 먼저 최대한 이해해 보고, 각자의 학습 진행도에 맞게 추가적인 레퍼런스와 코드 예제를 참고하면서 점진적으로 이해를 확장하시길 권장드립니다.
참조 변수의 타입 변환
앞서 기본 자료형의 형 변환에서 배웠던 것처럼, 참조 변수도 타입 변환이 가능합니다.
참조 변수의 타입 변환은 다르게 설명하면 사용할 수 있는 멤버의 개수를 조절하는 것을 의미하는데, 이는 자바의 다형성을 이해하기 위해서 꼭 필요한 개념입니다.
타입 변환을 위해서는 다음의 세 가지 조건을 충족해야 합니다.
- 서로 상속 관계에 있는 상위 클래스 - 하위 클래스 사이에만 타입 변환이 가능합니다.
- 하위 클래스 타입에서 상위 클래스 타입으로의 타입 변환(업캐스팅)은 형 변환 연산자(괄호)를 생략할 수 있습니다.
- 반대로 상위 클래스에서 하위 클래스 타입으로 변환(다운캐스팅)은 형 변환 연산자(괄호)를 반드시 명시해야 합니다.
- 또한, 다운 캐스팅은 업 캐스팅이 되어 있는 참조 변수에 한해서만 가능합니다.
사실 지금은 업캐스팅과 다운캐스팅에 대한 내용보다는 서로 상속 관계에 있는 클래스들 사이에서만 타입 변환이 가능하다는 사실을 기억하는 것으로 충분합니다.
너무 많은 시간을 타입 변환을 괄호로 명시해야 하느냐 생략해도 되느냐라는 상대적으로 작은 부분에 쓰기보다는 기본적인 큰 원칙을 기억하고, 그때그때 부딪치고 에러를 확인하면서 익숙해지는 것도 좋은 학습 방법이 될 수 있습니다.
그러면 이제 예시를 통해 좀 더 자세한 내용을 살펴보겠습니다.
public class VehicleTest {
public static void main(String[] args) {
Car car = new Car();
Vehicle vehicle = (Vehicle) car; // 상위 클래스 Vehicle 타입으로 변환(생략 가능)
Car car2 = (Car) vehicle; // 하위 클래스 Car타입으로 변환(생략 불가능)
MotorBike motorBike = (MotorBike) car; // 상속 관계가 아니므로 타입 변환 불가 -> 에러발생
}
}
class Vehicle {
String model;
String color;
int wheels;
void startEngine() {
System.out.println("시동 걸기");
}
void accelerate() {
System.out.println("속도 올리기");
}
void brake() {
System.out.println("브레이크!");
}
}
class Car extends Vehicle {
void giveRide() {
System.out.println("다른 사람 태우기");
}
}
class MotorBike extends Vehicle {
void performance() {
System.out.println("묘기 부리기");
}
}
위의 예시를 보면 먼저 Vehicle 클래스가 있고, 이로부터 각각 상속을 받아 만들어진 Car와 MotorBike 클래스가 있습니다.
먼저 Car 클래스의 인스턴스 객체 car를 생성하고, 그 객체를 가리키는 참조 변수 vehicle의 타입을 Vehicle로 지정하여 참조 변수의 타입 변환을 실행하였습니다. 그 후 반대로 vehicle을 하위 클래스 타입인 Car로 타입 변환하여 참조 변수 car2에 할당하였습니다.
이처럼 상속 관계에 있는 클래스 간에는 상호 타입 변환이 수행될 수 있습니다. 다만 하위 클래스를 상위 클래스 타입으로 변환하는 경우 타입 변환 연산자(괄호)를 생략할 수 있는 반면, 그 반대의 경우는, 업 캐스팅이 되어있다는 전제하에 다운 캐스팅을 할 수 있으며, 타입 변환 연산자를 생략할 수 없다는 점에서만 차이가 있다고 할 수 있습니다.
한편, Car 클래스와 MotorBike 클래스는 상속 관계가 아니므로 타입 변환이 불가하여 에러가 발생하는 것을 확인하실 수 있습니다.
정리하면, 참조 변수의 타입 변환은 서로 상속 관계에 있는 관계에서는 양방향으로 수행될 수 있으나, 상위 클래스로의 타입 변환이냐(괄호 생략 가능) 아니면 하위 클래스로의 타입 변환이냐(괄호 생략 불가)에 따라서 약간의 차이가 있습니다.
왜 참조 변수의 형 변환이 다형성에 있어서 핵심적인 개념인지에 대해서는 이어지는 챕터를 통해 이해해 보도록 하겠습니다.
instanceof 연산자
instanceof 연산자는 앞서 배웠던 참조 변수의 타입 변환, 즉 캐스팅이 가능한지 여부를 boolean 타입으로 확인할 수 있는 자바의 문법 요소입니다.
잠시 앞의 내용을 복습해 보면, 캐스팅 가능 여부를 판단하기 위해서는 두 가지, 즉 ‘객체를 어떤 생성자로 만들었는가’와 ‘클래스 사이에 상속관계가 존재하는가’를 판단해야 합니다.
프로젝트의 규모가 커지고, 클래스가 많아지면 매번 이러한 정보를 확인하는 것이 어려워집니다.
이를 해결하기 위해 자바는 instanceof라는 연산자를 제공합니다.
참조_변수 instanceof 타입
만약 참조_변수 instanceof 타입을 입력했을 때 리턴 값이 true가 나오면 참조 변수가 검사한 타입으로 타입 변환이 가능하며, 반대로 false가 나오는 경우에는 타입 변환이 불가능합니다.
만약에 참조 변수가 null인 경우에는 false를 반환합니다. 좀 더 구체적인 예시를 통해서 살펴보도록 하겠습니다.
public class InstanceOfExample {
public static void main(String[] args) {
Animal animal = new Animal();
System.out.println(animal instanceof Object); //true
System.out.println(animal instanceof Animal); //true
System.out.println(animal instanceof Bat); //false
Animal cat = new Cat();
System.out.println(cat instanceof Object); //true
System.out.println(cat instanceof Animal); //true
System.out.println(cat instanceof Cat); //true
System.out.println(cat instanceof Bat); //false
}
}
class Animal {};
class Bat extends Animal{};
class Cat extends Animal{};
위의 예시를 보시면 Animal 클래스가 있고, Bat과 Cat 클래스가 각각 Animal 클래스를 상속받고 있습니다. 그리고 각각 객체를 생성하여 Animal 타입의 참조 변수에 넣고 instanceof 키워드를 사용하여 형 변환 여부를 확인하고 있습니다.
Cat 객체를 예로 들어보면, 생성된 객체는 Animal 타입으로 선언되어 있지만 다형적 표현 방법에 따라 Object와 Animal 타입으로도 선언될 수 있다는 점을 확인하실 수 있습니다.
이렇듯 소스 코드가 길어지는 등 일일이 생성 객체의 타입을 확인하기가 어려운 상황에서 instanceof 연산자는 형 변환 여부를 확인하여 에러를 최소화하는 매우 유용한 수단이 될 수 있습니다.
다형성의 활용 예제
이제 자바 객체지향 프로그래밍에서 다형성이 실제로 어떻게 활용될 수 있는지를 실제 코드 예제를 통해서 좀 더 살펴보도록 하겠습니다.
아래 코드는 눈으로만 확인하거나 Copy & Paste하지 말고 꼭 직접 입력하면서 흐름을 이해해 보세요.
class Coffee {
int price;
public Coffee(int price) {
this.price = price;
}
}
class Americano extends Coffee {};
class CaffeLatte extends Coffee {};
class Customer {
int money = 50000;
}
여기 손님이 카페에 방문하여 커피를 소비하는 시나리오를 예로 들어보겠습니다.
이 예제에는 4개의 클래스가 있습니다. 먼저, 커피의 가격 정보를 담고 있는 Coffee 클래스가 있고, 이를 상속받는 Americano 클래스와 CaffeLatte 클래스가 아직 구현부가 작성되지 않은 상태로 존재합니다. 마지막으로 Customer 클래스는 커피를 구매하는 손님을 의미하며, 기본적으로 5만 원의 돈을 가지고 있다고 가정해 봅시다.
다음으로 이 5만 원의 돈을 가지고 아메리카노 한 잔과 카페라떼 한 잔을 구입하는 경우를 생각해 보면, 어떻게 할 수 있을까요?
아마 다음과 같은 기능의 메서드를 Customer 클래스에 추가해 볼 수 있을 것 같습니다.
void buyCoffee(Americano americano) { // 아메리카노 구입
money = money - americano.price;
}
void buyCoffee(CaffeLatte caffeLatte) { // 카페라테 구입
money = money - caffeLatte.price;
}
사야 하는 커피가 무엇인지 구분하기 위해서는 매개 변수로 커피에 대한 정보를 전달받아야 하기 때문에 매개 변수로 각각 Americano 타입과 CaffeLatte 타입의 객체를 전달해 주었습니다.
그런데 혹시 좀 불편하게 느껴지시지 않나요?
만약에 손님이 구입해야 하는 커피의 종류가 한두 개가 아니라 수십 수백 가지가 된다면 그 경우에는 매번 새로운 타입을 매개 변수로 전달해 주는 buyCoffee 메서드를 계속 추가해주어야 할 것입니다.
이런 경우 앞서 배웠던 객체의 다형성을 활용하여 아래와 같이 문제를 해결할 수 있습니다.
void buyCoffee(Coffee coffee) { // 매개 변수의 다형성
money = money - coffee.price;
}
즉 다형성이 가지는 특성에 따라 매개 변수로 각각의 개별적인 커피의 타입이 아니라 상위클래스인 Coffee의 타입을 매개 변수로 전달받으면, 그 하위클래스 타입의 참조변수면 어느 것이나 매개 변수로 전달될 수 있고 이에 따라 매번 다른 타입의 참조변수를 매개 변수로 전달해주어야 하는 번거로움을 훨씬 줄일 수 있습니다.
이제 전체적인 코드를 보면서 다시 흐름을 이해해 보겠습니다.
package package2;
public class PolymorphismEx {
public static void main(String[] args) {
Customer customer = new Customer();
customer.buyCoffee(new Americano());
customer.buyCoffee(new CaffeLatte());
System.out.println("현재 잔액은 " + customer.money + "원 입니다.");
}
}
class Coffee {
int price;
public Coffee(int price) {
this.price = price;
}
}
class Americano extends Coffee {
public Americano() {
super(4000); // 상위 클래스 Coffee의 생성자를 호출
}
public String toString() {return "아메리카노";}; //Object클래스 toString()메서드 오버라이딩
};
class CaffeLatte extends Coffee {
public CaffeLatte() {
super(5000);
}
public String toString() {return "카페라떼";};
};
class Customer {
int money = 50000;
void buyCoffee(Coffee coffee) {
if (money < coffee.price) { // 물건 가격보다 돈이 없는 경우
System.out.println("잔액이 부족합니다.");
return;
}
money = money - coffee.price; // 가진 돈 - 커피 가격
System.out.println(coffee + "를 구입했습니다.");
}
}
// 출력값
아메리카노를 구입했습니다.
카페라떼를 구입했습니다.
현재 잔액은 41000원 입니다.
위의 코드 예제에서 앞서 설명드린 내용은 제외하고 실제 객체를 생성하여 아메리카노와 카페라떼 한 잔을 구입하는 코드만 한번 간단히 살펴보도록 하겠습니다.
앞서 우리는 객체지향 설계의 다형성을 활용하여 buyCoffee() 메서드의 매개 변수로 Coffee 타입을 전달해 주었습니다.
이제 객체를 생성하고 참조변수를 사용할 때 Coffee 클래스를 상속받기만 하면 buyCoffee(Coffee coffee) 메서드의 매개 변수로 전달할 수 있습니다.
하나의 간단한 예시이지만 이렇게 자바의 다형성을 잘 활용하면 많은 중복되는 코드를 줄이고 보다 편리하게 코드를 작성하는 것이 가능해집니다.
이 외에도 여러 종류의 객체를 배열로 다룰 수 있는 등 다형성은 객체지향 프로그래밍에 없어서는 안 되는 매우 중요한 역할을 담당한다고 할 수 있습니다.
다형성에 대한 부분은 객체지향의 가장 핵심적이고 중요한 부분이기 때문에 아래 옵셔널(optional) 코드 예제 등 최대한 많은 코드 예제를 찾아보고, 실제로 코드 작성을 시도해 보면서 그 용법과 활용에 익숙해질 수 있도록 연습해 주세요.
실습 - 레스토랑 키오스크 프로그램(객체지향)
개요
💡 레스토랑 키오스크 프로그램 만들기 본 예제는 출력된 안내 사항에 맞게 원하는 메뉴와 수량을 입력하여 주문을 진행하는 프로그램을 만들어 봅니다. (※ 본 예제를 통해 객체지향을 및 가볍게 다뤄보도록 합니다)
🖥 [레스토랑 키오스크 프로그램] 출력 예시
[안내]안녕하세요. 김밥천국에 오신 것을 환영합니다.
------------------------------
[안내]원하시는 메뉴의 번호를 입력하여 주세요.
1) 김밥(1000원) 2) 계란 김밥(1500원) 3) 충무 김밥(1000원) 4) 떡볶이(2000원)
0
[안내]메뉴에 포함된 번호를 입력하여 주세요.
[안내]원하시는 메뉴의 번호를 입력하여 주세요.
1) 김밥(1000원) 2) 계란 김밥(1500원) 3) 충무 김밥(1000원) 4) 떡볶이(2000원)
1
------------------------------
[안내]선택하신 메뉴의 수량을 입력하여 주세요.
(※ 최대 주문 가능 수량 : 99)
100
[경고]100개는 입력하실 수 없습니다.
[경고]수량 선택 화면으로 돌아갑니다.
------------------------------
[안내]선택하신 메뉴의 수량을 입력하여 주세요.
(※ 최대 주문 가능 수량 : 99)
10
[안내]주문하신 메뉴의 총 금액은10000원 입니다.
[안내]이용해 주셔서 감사합니다.
💡 [김밥천국]에서 보내온 프로그래밍 요청서
✏️ [김밥천국을 위한 프로그램 기능]본 프로그램은 아래와 같이 세 가지 기능을 추가해 주세요. 1. 메뉴 선택2. 수량 입력3. 결제 금액 출력 (※ 메뉴 선택과 수량 입력 단계에서 보기와 다른 값 혹은 과한 수량이 입력되더라도 프로그램이 종료되지 않도록 합니다)
🛠 [김러키]에게서 내려온 프로그래밍 참고 사항
⚙ 위 내용은 어떻게 코딩할 수 있을까요?프로그램의 기능이 많아질수록 정의해야 하는 메서드도 점차 많아지게 됩니다. 그렇게 되면 하나의 .java 파일에 매우 많은 코드가 작성되며 이는 가독성 측면이나 유지보수 측면에서 효율적이지 못한 프로그램이 됩니다. 그렇기에 본 프로그램은 기능들이 담겨있는 파일과 프로그램이 실행되는 파일로 나눠 작성하여 봅시다.
▼ [ 1단계 ] 프로그램에 필요한 기능들 생각해 보기
▼ [ 2단계 ] 프로그램에 필요한 메서드 정의 및 파일 단위로 분할
▼ [ 3단계 ] 프로그램 메서드들의 흐름도를 그려보기
✏️ .java 파일 나누기 객체 지향 프로그래밍(Object Oriented Programming; 이하 OOP)은 객체를 활용하여 프로그래밍하는 것입니다. 객체는 일종의 개념이며 데이터와 코드를 포함하는 필드의 형태를 띠고 있습니다. 좀 더 쉽게 표현하자면, 주어진 데이터가 클래스를 통과하여 생성된 변수는 클래스 내부에 정의된 기능을 활용할 수 있게 됩니다. 이러한 방식을 활용하여 프로그램의 실질적인 동작 수행 기능을 따로 묶고, 프로그램 실행 기능을 묶어서 파일을 분할할 수 있습니다.
🌪️ 재귀 함수 재귀 함수는 정의 단계에서 자신을 재참조하는 함수를 뜻합니다. 쉽게 말해 A() 메서드를 정의하였는데, A() 메서드 안에 A() 메서드가 들어 있는 것입니다. 이러한 방식은 왜 사용되는 것일까요? 프로그램을 제작하다 보면 반복문을 다수 사용하게 되는 경우가 생깁니다. 그러한 상황에서 너무 많은 반복문 구조보다는 자신을 다시 호출하는 식으로 코드를 정의하면 좀 더 직관적이고 이해하기 쉬운 경우가 많습니다. 본 프로그램에서는 아직 재귀 함수를 학습하지 않아서, 코드가 모두 구현되어 있습니다.