Don't Panic
in Java ☕ 17분 읽기

[Java] java 객체지향핵심

목차

01. 객체 간의 상속은 어떤 의미일까?

클래스 상속(inheritance)

  • 새로운 클래스를 정의할 때 이미 구현된 클래스를 상속(inheritance)받아 속성이나 기능을 확장한다.
  • 이미 구현된 클래스보다 더 구체적인 기능을 가진 클래스를 만들어야 할 때 기존 클래스를 상속한다.

상속 관계 다이어그램

용어 정리:

  • 상속하는 클래스: 상위 클래스, parent class, base class, super class
  • 상속받는 클래스: 하위 클래스, child class, derived class, subclass

상속의 문법:

class B extends A{


}
  • extends 키워드 뒤에는 단 하나의 클래스만 올 수 있다.
  • 자바는 단일 상속(single inheritance)만 지원한다.

상속을 구현하는 경우

  • 상위 클래스는 하위 클래스보다 더 일반적인 개념과 기능을 가진다.
  • 하위 클래스는 상위 클래스보다 더 구체적인 개념과 기능을 가진다.
  • 즉, 하위 클래스가 상위 클래스의 속성과 기능을 확장(extends)한다는 의미다.

일반적 개념과 구체적 개념의 상속 관계


02. 상속을 활용한 멤버십 클래스 구현하기

멤버십 시나리오

회사에서 고객 정보를 활용한 맞춤 서비스를 하기 위해 일반 고객(Customer)과 충성도가 높은 우수 고객(VIPCustomer)에 따른 서비스를 제공하려 한다.

  • 물품을 구매할 때 적용되는 할인율과 적립되는 보너스 포인트 비율이 다르다.
  • 여러 멤버십에 대해 각각 다양한 서비스를 제공할 수 있다.
  • 이 멤버십을 클래스 상속으로 구현해 본다.

일반 고객(Customer) 클래스 구현

  • 고객의 속성: 고객 아이디, 고객 이름, 고객 등급, 보너스 포인트, 보너스 포인트 적립 비율
  • 일반 고객은 물품 구매 시 1%의 보너스 포인트를 적립한다.
package ch01;

public class Customer {

	private int customerID;
	private String customerName;
	private String customerGrade;
	int bonusPoint;
	double bonusRatio;

	public Customer() {
		customerGrade = "SILVER";
		bonusRatio = 0.01;
	}

	public int calcPrice(int price) {
		bonusPoint += price * bonusRatio;
		return price;
	}

	public String showCustomerInfo() {
		return customerName + "님의 등급은 " + customerGrade +
				"이며, 보너스 포인트는" + bonusPoint + "입니다";

	}
}

우수 고객(VIPCustomer) 구현

우수 고객의 혜택:

  • 매출에 더 많이 기여하는 단골 고객이다.
  • 제품을 살 때 10%를 할인해 준다.
  • 보너스 포인트는 제품 가격의 5%를 적립해 준다.
  • 담당 전문 상담원이 배정된다.

구현 방향:

  • Customer 클래스에 직접 추가하는 것은 좋지 않다.
  • VIPCustomer 클래스를 따로 구현한다.
  • 이미 Customer에 구현된 내용이 중복되므로 Customer를 확장(상속)하여 구현한다.
public class VIPCustomer extends Customer{

	private int agentID;
	double salesRatio;

	public VIPCustomer() {
		customerGrade = "VIP";    //오류 발생
		bonusRatio = 0.05;
		salesRatio = 0.1;
	}

	public int getAgentID() {
		return agentID;
	}
}

Customer와 VIPCustomer 클래스 관계

protected 접근 제어자

  • 상위 클래스에 선언된 private 멤버 변수는 하위 클래스에서 접근할 수 없다.
  • 외부 클래스는 접근하지 못하지만 하위 클래스는 접근할 수 있도록 protected 접근 제어자를 사용한다.

Customer.java

	protected int customerID;
	protected String customerName;
	protected String customerGrade;

	//getter, setter 구현
	...
	public int getCustomerID() {
		return customerID;
	}

	public void setCustomerID(int customerID) {
		this.customerID = customerID;
	}

	public String getCustomerName() {
		return customerName;
	}

	public void setCustomerName(String customerName) {
		this.customerName = customerName;
	}

	public String getCustomerGrade() {
		return customerGrade;
	}

	public void setCustomerGrade(String customerGrade) {
		this.customerGrade = customerGrade;
	}

Customer와 VIPCustomer 테스트하기

public class CustomerTest {

	public static void main(String[] args) {
		Customer customerLee = new Customer();
		customerLee.setCustomerName("이순신");
		customerLee.setCustomerID(10010);
		customerLee.bonusPoint = 1000;
		System.out.println(customerLee.showCustomerInfo());


		VIPCustomer customerKim = new VIPCustomer();
		customerKim.setCustomerName("김유신");
		customerKim.setCustomerID(10020);
		customerKim.bonusPoint = 10000;
		System.out.println(customerKim.showCustomerInfo());
	}
}

03. 상속에서 클래스 생성 과정과 형 변환

하위 클래스가 생성되는 과정

  • 하위 클래스를 생성하면 상위 클래스가 먼저 생성된다.
  • new VIPCustomer()를 호출하면 Customer()가 먼저 호출된다.
  • 클래스를 상속받은 경우, 하위 클래스의 생성자에서는 반드시 상위 클래스의 생성자를 호출한다.

Customer.java

public Customer() {
		customerGrade = "SILVER";
		bonusRatio = 0.01;

		System.out.println("Customer() 생성자 호출");
}

VIPCustomer.java

public VIPCustomer() {
		customerGrade = "VIP";
		bonusRatio = 0.05;
		salesRatio = 0.1;

		System.out.println("VIPCustomer() 생성자 호출");
}

super 키워드

  • super는 하위 클래스가 가지는 상위 클래스에 대한 참조 값이다.
  • super()는 상위 클래스의 기본 생성자를 호출한다.
  • 하위 클래스에서 명시적으로 상위 클래스의 생성자를 호출하지 않으면 super()가 호출된다. (이때 반드시 상위 클래스의 기본 생성자가 존재해야 한다.)
  • 상위 클래스의 기본 생성자가 없고 매개변수가 있는 다른 생성자만 있는 경우, 하위 클래스의 생성자에서 super를 이용해 명시적으로 상위 클래스의 생성자를 호출해야 한다.
  • super는 생성된 상위 클래스 인스턴스의 참조 값을 가지므로, 이를 통해 상위 클래스의 메서드나 멤버 변수에 접근할 수 있다.

Customer.java

// 디폴트 생성자 없애고 매개 변수가 있는 생성자 추가
public Customer(int customerID, String customerName) {
		this.customerID = customerID;
		this.customerName = customerName;

		customerGrade = "SILVER";
		bonusRatio = 0.01;
		System.out.println("Customer(int, String) 생성자 호출");
}

VIPCustomer.java

// super를 이용하여 상위 클래스의 생성자 명시적으로 호출
public VIPCustomer(int customerID, String customerName) {
		super(customerID, customerName);

		customerGrade = "VIP";
		bonusRatio = 0.05;
		salesRatio = 0.1;

		System.out.println("VIPCustomer(int, String) 생성자 호출");
}

CustomerTest.java

public class CustomerTest {

	public static void main(String[] args) {

		Customer customerLee = new Customer(10010, "이순신");
		customerLee.bonusPoint = 1000;
		System.out.println(customerLee.showCustomerInfo());

		VIPCustomer customerKim = new VIPCustomer(10020, "김유신");
		customerKim.bonusPoint = 10000;
		System.out.println(customerKim.showCustomerInfo());
	}
}

상속에서 인스턴스 메모리의 상태

  • 항상 상위 클래스의 인스턴스가 먼저 생성된 뒤 하위 클래스의 인스턴스가 생성된다.

상속 시 인스턴스 메모리 구조

형 변환 (업캐스팅)

상위 클래스로 변수를 선언하고 하위 클래스의 생성자로 인스턴스를 생성한다.

Customer customerLee = new VIPCustomer();

상위 클래스 타입의 변수에 하위 클래스 인스턴스를 대입한다.

VIPCustomer vCustomer = new VIPCustomer();
addCustomer(vCustomer);

int addCustomer(Customer customer){


}
  • 하위 클래스는 상위 클래스의 타입을 내포하고 있으므로 상위 클래스로의 묵시적 형 변환이 가능하다.
  • 상속 관계에서 모든 하위 클래스는 상위 클래스로 형 변환(업캐스팅)된다. (그 역은 성립하지 않는다.)

형 변환과 메모리

Customer vc = new VIPCustomer();에서 vc가 가리키는 것은 무엇일까?

  • VIPCustomer() 생성자에 의해 VIPCustomer 클래스의 모든 멤버 변수에 대한 메모리는 생성되었다.
  • 하지만 변수의 타입이 Customer이므로, 실제로 접근 가능한 변수나 메서드는 Customer의 것뿐이다.

업캐스팅 시 메모리 상태 1 업캐스팅 시 접근 가능 범위 2

클래스의 계층구조가 여러 단계인 경우

Mammal-Primate-Human 다단계 상속 구조

  • Human은 내부적으로 Primate와 Mammal 타입을 모두 내포하고 있다.
Primate pHumman = new Humman();

Mammal mHumman = new Humman();

04. 메서드 재정의하기 (overriding)

하위 클래스에서 메서드 재정의하기

  • 오버라이딩(overriding): 상위 클래스에 정의된 메서드의 구현 내용이 하위 클래스에 맞지 않을 때, 하위 클래스에서 같은 이름의 메서드를 재정의하는 것.
  • VIPCustomer 클래스의 calcPrice()는 할인율이 적용되지 않으므로 재정의가 필요하다.

VIPCustomer.java

@Override
public int calcPrice(int price) {
	bonusPoint += price * bonusRatio;
	return price - (int)(price * salesRatio);
}

@Override 애노테이션 (annotation)

  • 애노테이션(annotation)은 원래 ‘주석’이라는 의미다.
  • 컴파일러에게 특별한 정보를 제공하는 역할을 한다.

애노테이션 사용 예

  • @Override 애노테이션은 ‘재정의된 메서드’라는 의미이며, 선언부가 기존 메서드와 다르면 컴파일 에러가 발생한다.

형 변환과 오버라이딩 메서드 호출

Customer vc = new VIPCustomer();
  • vc 변수의 타입은 Customer지만 인스턴스의 타입은 VIPCustomer다.
  • 자바에서는 항상 인스턴스의 메서드가 호출된다 (가상 메서드의 원리).
  • 자바의 모든 메서드는 가상 메서드(virtual method)다.

CustomerTest.java

public class CustomerTest {

	public static void main(String[] args) {

		Customer customerLee = new Customer(10010, "이순신");
		customerLee.bonusPoint = 1000;
		System.out.println(customerLee.showCustomerInfo());

		VIPCustomer customerKim = new VIPCustomer(10020, "김유신");
		customerKim.bonusPoint = 10000;
		System.out.println(customerKim.showCustomerInfo());

		int priceLee = customerLee.calcPrice(10000);
		int priceKim = customerKim.calcPrice(10000);

		System.out.println(customerLee.showCustomerInfo() + " 지불금액은 " + priceLee + "원 입니다.");
		System.out.println(customerKim.showCustomerInfo() + " 지불금액은 " + priceKim + "원 입니다.");

		Customer customerNo = new VIPCustomer(10030, "나몰라");
		customerNo.bonusPoint = 10000;
		int priceNo = customerNo.calcPrice(10000);
		System.out.println(customerNo.showCustomerInfo() + " 지불금액은 " + priceNo  + "원 입니다.");

	}
}

마지막 부분(Customer customerNo = new VIPCustomer(...))은 상위 클래스로 변수를 선언하고 하위 클래스의 생성자로 인스턴스를 생성한 형 변환 사례다. 실행하면 상위 클래스인 Customer가 아니라 하위 클래스인 VIPCustomer의 calcPrice() 결과가 호출된다.


05. 메서드 재정의와 가상 메서드 원리

메서드는 어떻게 호출되고 실행되는가?

  • 메서드(함수)의 이름은 주소 값을 나타낸다.
  • 메서드는 명령어의 집합(set)이며, 프로그램이 로드되면 메서드 영역(코드 영역)에 명령어 집합이 위치한다.
  • 해당 메서드가 호출되면 명령어 집합이 있는 주소를 찾아 명령어를 실행한다.
  • 이때 메서드에서 사용하는 변수들은 스택 메모리에 위치한다.
  • 따라서 다른 인스턴스라도 같은 메서드의 코드는 동일하므로 같은 메서드가 호출된다.
  • 인스턴스가 생성되면 변수는 힙 메모리에 따로 생성되지만, 메서드 명령어 집합은 처음 한 번만 로드된다.
public class TestMethod {

	int num;

	void aaa() {
		System.out.println("aaa() 호출");
	}

	public static void main(String[] args) {

		TestMethod a1 = new TestMethod();
		a1.aaa();

		TestMethod a2 = new TestMethod();
		a2.aaa();
	}

}

인스턴스와 메서드 영역 메모리 구조

가상 메서드의 원리

  • 가상 메서드 테이블(virtual method table)이 각 메서드에 대한 주소(address)를 가지고 있다.
  • 재정의된 경우에는 재정의된 메서드의 주소를 가리킨다.

가상 메서드 테이블 구조

calcPrice 가상 메서드 호출 흐름


06. 다형성과 다형성을 사용하는 이유

다형성(polymorphism)이란?

  • 하나의 코드가 여러 자료형으로 구현되어 실행되는 것이다.
  • 같은 코드에서 여러 다른 실행 결과가 나온다.
  • 정보 은닉, 상속과 더불어 객체지향 프로그래밍의 가장 큰 특징 중 하나다.
  • 다형성을 잘 활용하면 유연하고 확장성 있으며 유지보수가 편리한 프로그램을 만들 수 있다.

다형성의 예

class Animal{

	public void move() {
		System.out.println("동물이 움직입니다.");
	}

	public void eating() {

	}
}

class Human extends Animal{
	public void move() {
		System.out.println("사람이 두발로 걷습니다.");
	}

	public void readBooks() {
		System.out.println("사람이 책을 읽습니다.");
	}
}

class Tiger extends Animal{

	public void move() {
		System.out.println("호랑이가 네 발로 뜁니다.");
	}

	public void hunting() {
		System.out.println("호랑이가 사냥을 합니다.");
	}
}


class Eagle extends Animal{
	public void move() {
		System.out.println("독수리가 하늘을 날아갑니다.");
	}

	public void flying() {
		System.out.println("독수리가 날개를 쭉 펴고 멀리 날아갑니다");
	}
}



public class AnimalTest {

	public static void main(String[] args) {

		Animal hAnimal = new Human();
		Animal tAnimal = new Tiger();
		Animal eAnimal = new Eagle();

		AnimalTest test = new AnimalTest();
		test.moveAnimal(hAnimal);
		test.moveAnimal(tAnimal);
		test.moveAnimal(eAnimal);

		ArrayList<Animal> animalList = new ArrayList<Animal>();
		animalList.add(hAnimal);
		animalList.add(tAnimal);
		animalList.add(eAnimal);

		for(Animal animal : animalList) {
			animal.move();
		}
	}

	public void moveAnimal(Animal animal) {
		animal.move();

	}
}

moveAnimal(Animal animal) 안의 animal.move()는 코드가 한 줄이지만, 어떤 자료형이 전달되느냐에 따라 move()의 동작 내용이 달라진다.

다형성을 사용하는 이유

  • 다른 동물을 추가하는 경우에도, 상속과 메서드 재정의를 활용하면 확장성 있는 프로그램을 만들 수 있다.
  • 그렇지 않으면 수많은 if-else if문이 구현되어 코드의 유지보수가 어려워진다.

다형성으로 처리하는 고객 구조

  • 상위 클래스에서는 공통적인 부분을 제공하고, 하위 클래스에서는 각 클래스에 맞는 기능을 구현한다.
  • 여러 클래스를 하나의 타입(상위 클래스)으로 핸들링할 수 있다.

다형성을 활용한 멤버십 프로그램 확장

일반 고객과 VIP 고객 중간 멤버십을 만든다.

고객이 늘어, 일반 고객보다는 많이 구매하고 VIP보다는 적게 구매하는 고객에게도 혜택을 주기로 했다. GOLD 고객 등급을 만들고 혜택은 다음과 같다.

  1. 제품을 살 때 10%를 할인해 준다.
  2. 보너스 포인트는 2%를 적립해 준다.

GoldCustomer.java

public class GoldCustomer extends Customer{

	double saleRatio;

	public GoldCustomer(int customerID, String customerName){
		super(customerID, customerName);

		customerGrade = "GOLD";
		bonusRatio = 0.02;
		saleRatio = 0.1;

	}

	public int calcPrice(int price){
		bonusPoint += price * bonusRatio;
		return price - (int)(price * saleRatio);
	}
}

VIPCustomer.java

//showCustomerInfo() 재정의
public String showCustomerInfo(){
		return super.showCustomerInfo() + " 담당 상담원 번호는 " + agentID + "입니다";  
}

CustomerTest.java

public class CustomerTest {

	public static void main(String[] args) {

		ArrayList<Customer> customerList = new ArrayList<Customer>();

		Customer customerLee = new Customer(10010, "이순신");
		Customer customerShin = new Customer(10020, "신사임당");
		Customer customerHong = new GoldCustomer(10030, "홍길동");
		Customer customerYul = new GoldCustomer(10040, "이율곡");
		Customer customerKim = new VIPCustomer(10050, "김유신", 12345);

		customerList.add(customerLee);
		customerList.add(customerShin);
		customerList.add(customerHong);
		customerList.add(customerYul);
		customerList.add(customerKim);

		System.out.println("====== 고객 정보 출력 =======");

		for( Customer customer : customerList){
			System.out.println(customer.showCustomerInfo());
		}

		System.out.println("====== 할인율과 보너스 포인트 계산 =======");

		int price = 10000;
		for( Customer customer : customerList){
			int cost = customer.calcPrice(price);
			System.out.println(customer.getCustomerName() +" 님이 " +  + cost + "원 지불하셨습니다.");
			System.out.println(customer.getCustomerName() +" 님의 현재 보너스 포인트는 " + customer.bonusPoint + "점입니다.");
		}
	}
}
  • ArrayList<Customer> customerList = new ... : Customer 객체를 담는 배열 리스트를 선언했다.
  • 추가 요구사항이 생겼을 때, 상속이 있으면 기존 코드를 일부 고치거나 몇 부분만 추가해서 대응할 수 있다. 이것이 다형성과 상속을 사용하는 이유다.

멤버십 확장 실행 결과


07. 상속은 언제 사용할까?

IS-A 관계 (is a relationship : inheritance)

  • 일반적인(general) 개념과 구체적인(specific) 개념 사이의 관계다.
  • 상위 클래스: 하위 클래스보다 일반적인 개념 (예: Employee)
  • 하위 클래스: 상위 클래스보다 구체적인 개념이 더해짐 (예: Engineer, Manager…)

주의할 점:

  • 상속은 클래스 간 결합도가 높은 설계다.
  • 상위 클래스의 수정이 많은 하위 클래스에 영향을 미칠 수 있다.
  • 계층구조가 복잡하거나 hierarchy가 깊으면 좋지 않다.

HAS-A 관계 (composition)

  • 클래스가 다른 클래스를 포함하는 관계다 (변수로 선언).
  • 코드 재사용의 가장 일반적인 방법이다.
  • 예: Student가 Subject를 포함한다.
  • 예: Library를 구현할 때 ArrayList를 생성하여 사용한다.
  • 상속하지 않는다.

HAS-A 관계에서는, 이미 클래스가 있다고 해서 무조건 상속으로 재사용하는 것이 아니다. 예를 들어 어떤 학생이 수업을 듣고 과목 코드가 있더라도, Student 클래스가 Subject를 상속받는 것은 아니다.


08. 다운 캐스팅과 instanceof

다운 캐스팅(downcasting)

  • 업캐스팅된 클래스를 다시 원래의 타입으로 형 변환하는 것이다.
  • 하위 클래스로의 형 변환은 명시적으로 해야 한다.
Customer vc = new VIPCustomer();              //묵시적
VIPCustomer vCustomer = (VIPCustomer)vc;      //명시적

instanceof로 인스턴스의 형 체크

  • instanceof는 원래 인스턴스의 형이 맞는지 여부를 체크하는 키워드로, 맞으면 true, 아니면 false를 반환한다.

AnimalTest.java

public void testDownCasting(ArrayList<Animal> list) {

		for(int i =0; i<list.size(); i++) {
			Animal animal = list.get(i);

			if ( animal instanceof Human) {
				Human human = (Human)animal;
				human.readBooks();
			}
			else if( animal instanceof Tiger) {
				Tiger tiger = (Tiger)animal;
				tiger.hunting();
			}
			else if( animal instanceof Eagle) {
				Eagle eagle = (Eagle)animal;
				eagle.flying();
			}
			else {
				System.out.println("error");
			}

		}
}

09. 추상 클래스(abstract class) 구현하기

추상 클래스란?

  • 구현 코드 없이 메서드의 선언만 있는 추상 메서드(abstract method)를 포함한 클래스다.
  • 메서드 선언(declaration): 반환 타입, 메서드 이름, 매개변수로 구성된다.
  • 메서드 정의(definition): 메서드 구현(implementation)과 같은 의미로, 구현부(body, { })를 가진다.
  • 예시:
    • int add(int x, int y); // 선언
    • int add(int x, int y){ } // 구현부가 있음, 추상 메서드 아님
  • abstract 예약어를 사용한다.
  • 추상 클래스는 new 할 수 없다 (인스턴스화 할 수 없다).

추상 클래스 구현하기

  • 메서드에 구현 코드가 없으면 abstract로 선언한다.
  • abstract로 선언된 메서드를 가진 클래스는 abstract로 선언한다.
  • 모든 메서드가 구현된 클래스라도 abstract로 선언되면 추상 클래스이므로 인스턴스화할 수 없다.
  • 추상 클래스의 추상 메서드는 하위 클래스가 상속하여 구현한다.
    • 추상 메서드: 하위 클래스가 반드시 구현해야 하는 메서드
    • 구현된 메서드: 하위 클래스가 공통으로 사용하는 메서드 (필요에 따라 하위 클래스에서 재정의)

예제 구현하기:

Computer 추상 클래스 계층 구조

Computer.java

public abstract class  Computer {

	abstract void display();
	abstract void typing();

	public void turnOn() {
		System.out.println("전원을 켭니다.");
	}

	public void turnOff() {
		System.out.println("전원을 끕니다.");
	}
}

DeskTop.java

public class DeskTop extends Computer{

	@Override
	void display() {
		System.out.println("DeskTop display");
	}

	@Override
	void typing() {
		System.out.println("DeskTop typing");
	}

	@Override
	public void turnOff() {
		System.out.println("Desktop turnoff");
	}
}

NoteBook.java

public abstract class NoteBook extends Computer{
	@Override
	public void typing() {
		System.out.println("NoteBook typing");		
	}
}

MyNoteBook.java

public class MyNoteBook extends NoteBook{

	@Override
	void display() {
		System.out.println("MyNoteBook display");		
	}
}

ComputerTest.java

public class ComputerTest {

	public static void main(String[] args) {
		Computer computer = new DeskTop();
		computer.display();
		computer.turnOff();

		NoteBook myNote = new MyNoteBook();
	}
}

10. 추상 클래스의 응용 - 템플릿 메서드 패턴

템플릿 메서드

  • 추상 메서드나 구현된 메서드를 활용하여 코드의 흐름(시나리오)을 정의하는 메서드다.
  • final로 선언하여 하위 클래스에서 재정의할 수 없게 한다.
  • 프레임워크에서 많이 사용되는 설계 패턴이다.
  • 추상 클래스로 선언된 상위 클래스에서 템플릿 메서드로 전체적인 흐름을 정의하고, 하위 클래스마다 다르게 구현되어야 하는 부분은 추상 메서드로 선언하여 하위 클래스에서 구현하게 한다.

템플릿 메서드 예제

Car 템플릿 메서드 클래스 구조

Car.java

public abstract class Car {

	public abstract void drive();
	public abstract void stop();

	public void startCar() {
		System.out.println("시동을 켭니다.");
	}

	public void turnOff() {
		System.out.println("시동을 끕니다.");
	}

	final public void run() {
		startCar();
		drive();
		stop();
		turnOff();
	}
}

ManualCar.java

public class ManualCar extends Car{

	@Override
	public void drive() {
		System.out.println("사람이 운전합니다.");
		System.out.println("사람이 핸들을 조작합니다.");		
	}

	@Override
	public void stop() {
		System.out.println("브레이크를 밟아서 정지합니다.");		
	}

}

AICar.java

public class AICar extends Car{

	@Override
	public void drive() {
		System.out.println("자율 주행합니다.");
		System.out.println("자동차가 스스로 방향을 바꿉니다.");
	}

	@Override
	public void stop() {
		System.out.println("스스로 멈춥니다.");		
	}
}

CarTest.java

public class CarTest {

	public static void main(String[] args) {
		Car aiCar = new AICar();
		aiCar.run();
		System.out.println("=================");
		Car manualCar = new ManualCar();
		manualCar.run();
	}
}

템플릿 메서드 실행 결과

final 예약어

  • final 변수: 값이 변경될 수 없는 상수

    public static final double PI = 3.14;

  • final 메서드: 하위 클래스에서 재정의할 수 없는 메서드

  • final 클래스: 상속할 수 없는 클래스

여러 자바 파일에서 사용하는 상수 값 정의

Define.java

public class Define {

	public static final int MIN = 1;
	public static final int MAX = 999999;
	public static final double PI = 3.14;
	public static final String GREETING = "Good Morning!";
	public static final int MATH_CODE = 1001;
	public static final int CHEMISTRY_CODE = 1002;

}

UsingDefine.java

public class UsingDefine {

	public static void main(String[] args) {

		System.out.println(Define.GREETING);
		System.out.println(Define.MIN);
		System.out.println(Define.MAX);
		System.out.println(Define.MATH_CODE);
		System.out.println(Define.CHEMISTRY_CODE);
		System.out.println("원주률은" + Define.PI + "입니다.");
	}

}

11. 인터페이스(interface)

인터페이스란?

인터페이스 개념 다이어그램

  • 모든 메서드가 추상 메서드로 선언된다 (public abstract).
  • 모든 변수는 상수로 선언된다 (public static final).
interface 인터페이스 이름{

    public static final float pi = 3.14F;
    public void makeSomething();
}
  • 자바 8부터는 디폴트 메서드(default method)와 정적 메서드(static method)를 제공하여 일부 구현 코드를 가질 수 있다.

인터페이스 정의와 구현

Calc 인터페이스 구현 계층 구조

Calc.java

public interface Calc {

	double PI = 3.14;
	int ERROR = -99999999;

	int add(int num1, int num2);
	int substract(int num1, int num2);
	int times(int num1, int num2);
	int divide(int num1, int num2);

}

Calculator.java

public abstract class Calculator implements Calc{

	@Override
	public int add(int num1, int num2) {
		return num1 + num2;
	}

	@Override
	public int substract(int num1, int num2) {
		return num1 - num2;
	}
}

CompleteCalc.java

public class CompleteCalc extends Calculator{

	@Override
	public int times(int num1, int num2) {
		return num1 * num2;
	}

	@Override
	public int divide(int num1, int num2) {
		if( num2 == 0 )
			return ERROR;
		else
			return num1 / num2;
	}

	public void showInfo() {
		System.out.println("모두 구현하였습니다.");
	}
}

CalculatorTest.java

public class CalculatorTest {

	public static void main(String[] args) {
		Calc calc = new CompleteCalc();
		int num1 = 10;
		int num2 = 2;

		System.out.println(num1 + "+" + num2 + "=" + calc.add(num1, num2));
		System.out.println(num1 + "-" + num2 + "=" +calc.substract(num1, num2));
		System.out.println(num1 + "*" + num2 + "=" +calc.times(num1, num2));
		System.out.println(num1 + "/" + num2 + "=" +calc.divide(num1, num2));
	}
}

계산기 인터페이스 실행 결과

인터페이스 구현과 형 변환

  • 인터페이스를 구현한 클래스는 인터페이스 형으로 선언한 변수로 형 변환할 수 있다.

    Calc calc = new CompleteCalc();

  • 상속에서의 형 변환과 같은 의미다.

  • 클래스 상속과 달리 구현 코드가 없으므로 여러 인터페이스를 구현할 수 있다 (cf. extends).

  • 형 변환되는 경우, 인터페이스에 선언된 메서드만 사용할 수 있다.

인터페이스 형 변환과 사용 가능 메서드


12. 인터페이스는 왜 쓰는가?

인터페이스가 하는 일

  • 클래스나 프로그램이 제공하는 기능을 명시적으로 선언한다.
  • 일종의 클라이언트 코드와의 약속이며, 클래스나 프로그램이 제공하는 명세(specification)다.
  • 클라이언트 프로그램은 인터페이스에 선언된 메서드 명세만 보고도 이를 구현한 클래스를 사용할 수 있다.
  • 어떤 객체가 하나의 인터페이스 타입이라는 것은, 그 인터페이스가 제공하는 모든 메서드를 구현했다는 의미다.
  • 인터페이스를 구현한 다양한 객체를 사용한다 — 다형성.
  • 예: JDBC 인터페이스

13. 인터페이스를 활용한 다형성 구현 (DAO 구현하기)

데이터베이스를 직접 짜진 않고 자바로 비슷하게 구현한다.

MySQL을 쓰다가 Oracle을 쓰면 오류가 나는데, 이런 버전 관리를 하나의 소스 안에서 할 수 있으면 좋을 것이다. 이를 인터페이스로 정의하고 Oracle 버전, MySQL 버전, MSSQL 버전으로 각각 구현해 보면 어떨까? 그것이 아래 그림이다. 똑같은 인터페이스가 어떻게 다형성으로 활용되는지 알아보자.

인터페이스와 다형성

  • 하나의 인터페이스를 여러 객체가 구현하게 되면, 클라이언트 프로그램은 인터페이스의 메서드를 활용하여 여러 객체의 구현을 사용할 수 있다 (다형성).

여러 예시:

정렬 정책 다형성 예 DAO 다형성 구조

인터페이스를 활용한 DAO 구현하기

  • DB에 회원 정보를 넣는 DAO(data access object)를 여러 DB 제품이 지원될 수 있게 구현한다.
  • 환경 파일(db.properties)에서 데이터베이스의 종류 정보를 읽고, 그 정보에 맞게 DAO 인스턴스를 생성하여 실행되도록 한다.
  • 소스 계층 구조(source hierarchy):

UserInfo DAO 소스 계층 구조

db.properties 설정 화면

여기 db.properties를 직접 넣어줘야 한다 (프로젝트에 바로 넣어 주자).

그리고 UserInfoClient에서:

Properties prop = new Properties();
		prop.load(fis);

		String dbType = prop.getProperty("DBTYPE");

		UserInfo userInfo = new UserInfo();
		userInfo.setUserId("12345");
		userInfo.setPasswd("!@#$%");
		userInfo.setUserName("이순신");

이 중 String dbType = prop.getProperty("DBTYPE"); 부분이 앞서 properties에서 설정한 내용을 가져온다. MySQL을 넣었으니 MySQL 정보가 나온다.

DBTYPE 읽어온 결과

그 아래 부분은 유저를 생성하는 코드다. 사실 유저 생성은 웹에서 하고 넘어오지만, 여기서는 직접 생성해 본다.

UserInfo.java (사용자 정보 클래스)

public class UserInfo {

	private String userId;
	private String passwd;
	private String userName;

	public String getUserId() {
		return userId;
	}

	public void setUserId(String userId) {
		this.userId = userId;
	}

	public String getPasswd() {
		return passwd;
	}

	public void setPasswd(String passwd) {
		this.passwd = passwd;
	}

	public String getUserName() {
		return userName;
	}

	public void setUserName(String userName) {
		this.userName = userName;
	}
}

UserInfoDao.java (DAO에서 제공되어야 할 메서드를 선언한 인터페이스)

public interface UserInfoDao {

	void insertUserInfo(UserInfo userInfo);
	void updateUserInfo(UserInfo userInfo);
	void deleteUserInf(UserInfo userInfo);
}

UserInfoMySqlDao.java (UserInfoDao 인터페이스를 구현한 MySQL 버전 DAO)

public class UserInfoMySqlDao implements UserInfoDao{

	@Override
	public void insertUserInfo(UserInfo userInfo) {
		System.out.println("insert into MYSQL DB userId =" + userInfo.getUserId() );		
	}

	@Override
	public void updateUserInfo(UserInfo userInfo) {
		System.out.println("update into MYSQL DB userId = " + userInfo.getUserId());		
	}

	@Override
	public void deleteUserInf(UserInfo userInfo) {
		System.out.println("delete from MYSQL DB userId = " + userInfo.getUserId());

	}

}

UserInfoOracleDao.java (UserInfoDao 인터페이스를 구현한 Oracle 버전 DAO)

public class UserInfoOracleDao implements UserInfoDao{

	public void insertUserInfo(UserInfo userInfo){
		System.out.println("insert into ORACLE DB userId =" + userInfo.getUserId() );
	}

	public void updateUserInfo(UserInfo userInfo){
		System.out.println("update into ORACLE DB userId = " + userInfo.getUserId());
	}

	public void deleteUserInf(UserInfo userInfo){
		System.out.println("delete from ORACLE DB userId = " + userInfo.getUserId());
	}
}

UserInfoClient.java (UserInfoDao 인터페이스를 활용하는 클라이언트 프로그램)

public class UserInfoClient {

	public static void main(String[] args) throws IOException {

		FileInputStream fis = new FileInputStream("db.properties");

		Properties prop = new Properties();
		prop.load(fis);

		String dbType = prop.getProperty("DBTYPE");

		UserInfo userInfo = new UserInfo();
		userInfo.setUserId("12345");
		userInfo.setPasswd("!@#$%");
		userInfo.setUserName("이순신");


		UserInfoDao userInfoDao = null;

		if(dbType.equals("ORACLE")){
			userInfoDao = new UserInfoOracleDao();
		}
		else if(dbType.endsWith("MYSQL")){
			userInfoDao = new UserInfoMySqlDao();
		}
		else{
			System.out.println("db support error");
			return;
		}

		userInfoDao.insertUserInfo(userInfo);
		userInfoDao.updateUserInfo(userInfo);
		userInfoDao.deleteUserInf(userInfo);
	}
}

db.properties 환경 파일이 MYSQL일 때:

DBTYPE=MYSQL

실행 결과:

MySQL DAO 실행 결과

db.properties 환경 파일이 ORACLE일 때:

DBTYPE=ORACLE

실행 결과:

Oracle DAO 실행 결과

말한 대로 저 설정 부분만 바꾸면 Oracle 결과가 나온다. 즉, db.properties의 설정만 바꾸면 원하는 DB를 사용할 수 있으며, 이것이 다형성이다. 실제 구현 코드와 상관없는 부분은 바뀌지 않고, 바뀌어야 할 부분만 바뀐다.


14. 인터페이스의 여러 가지 요소

상수

  • 모든 변수는 상수(public static final)로 변환된다.
double PI = 3.14;
int ERROR = -999999999;

추상 메서드

  • 선언된 모든 메서드는 추상 메서드(public abstract)다.

디폴트 메서드 (자바 8 이후)

  • 구현을 가지는 메서드로, 인터페이스를 구현하는 클래스들에서 공통으로 사용할 수 있는 기본 메서드다.
  • default 키워드를 사용한다.
default void description() {
	System.out.println("정수 계산기를 구현합니다.");
	myMethod();
}
  • 구현하는 클래스에서 재정의할 수 있다.
@Override
public void description() {
	System.out.println("CompleteCalc에서 재정의한 default 메서드");
	//super.description();
}
  • 인터페이스를 구현한 클래스의 인스턴스가 생성되어야 사용할 수 있다.

정적 메서드 (자바 8 이후)

  • 인스턴스 생성과 상관없이 인터페이스 타입으로 사용할 수 있는 메서드다.
static int total(int[] arr) {
	int total = 0;

	for(int i: arr) {
		total += i;
	}
	mystaticMethod();
	return total;
}

private 메서드 (자바 9 이후)

  • 인터페이스를 구현한 클래스에서 사용하거나 재정의할 수 없다.
  • 인터페이스 내부에서만 사용하기 위해 구현하는 메서드다.
  • default 메서드나 static 메서드에서 사용한다.

(이미 제공하고 있는 디폴트 메서드를 재정의할 수도 있다.)

private void myMethod() {
	System.out.println("private method");
}

private static void mystaticMethod() {
	System.out.println("private static method");
}

private 메서드는 인터페이스 내에서 구현한다. 구현하는 쪽에서 사용하는 것도, 재정의할 수 있는 것도 아니며, 단지 인터페이스 내에서만 사용한다.

참고: private 메서드는 자바 9 버전부터 지원한다. 8 버전에서는 사용할 수 없으니 주의하자.


15. 여러 인터페이스 구현하기, 인터페이스의 상속

여러 인터페이스 구현

  • 자바의 인터페이스는 구현 코드가 없으므로, 하나의 클래스가 여러 인터페이스를 구현할 수 있다.
  • 디폴트 메서드가 중복되는 경우에는 구현하는 클래스에서 재정의해야 한다.
  • 여러 인터페이스를 구현한 클래스가 특정 인터페이스 타입으로 형 변환되면, 해당 인터페이스에 선언된 메서드만 사용할 수 있다.

여러 인터페이스 구현 구조

Sell.java

public interface Sell {

	void sell();


}

Buy.java

public interface Buy {

	void buy();

}

Customer.java

public class Customer implements Buy, Sell{
  //여기서 buy,sell 구현
	@Override
	public void sell() {
		System.out.println("customer sell");
	}

	@Override
	public void buy() {
		System.out.println("customer buy");		
	}

  @Override
	public void order() {
		System.out.println("customer order");
	}


	public void sayHello() {
		System.out.println("Hello");
	}
}

order() 부분을 다음과 같이 작성했다면, 각각 Buy와 Sell의 메서드를 함께 쓰겠다는 뜻이다.

@Override
public void order() {
  System.out.println("customer order");
  Buy.super.order();
  Sell.super.order();
}

CustomerTest.java

public class CustomerTest {

	public static void main(String[] args) {

		Customer customer = new Customer();
		customer.buy();
		customer.sell();
		customer.sayHello();

		Buy buyer = customer;
		buyer.buy();

		Sell seller = customer;
		seller.sell();

	}
}

디폴트 메서드가 중복되는 경우

  • 구현 코드를 가지고 인스턴스가 생성된 경우에만 호출되는 디폴트 메서드가 두 인터페이스에서 중복되면, 구현하는 클래스에서 반드시 재정의해야 한다.

Sell.java

public interface Sell {

	void sell();

	default void order() {
		System.out.println("판매 주문");
	}
}

Buy.java

public interface Buy {

	void buy();

	default void order() {
		System.out.println("구매 주문");
	}
}

Customer.java

public class Customer implements Buy, Sell{

	@Override
	public void sell() {
		System.out.println("customer sell");
	}

	@Override
	public void buy() {
		System.out.println("customer buy");		
	}

	public void sayHello() {
		System.out.println("Hello");
	}

	@Override
	public void order() {
		System.out.println("customer order");
	}

}

CustomerTest.java

public class CustomerTest {

	public static void main(String[] args) {

		Customer customer = new Customer();
		customer.buy();
		customer.sell();
		customer.sayHello();

		Buy buyer = customer;
		buyer.buy();

		Sell seller = customer;
		seller.sell();

		buyer.order();
		seller.order();

	}
}

인터페이스의 상속

  • 인터페이스 사이에도 상속을 사용할 수 있다.
  • extends 키워드를 사용한다.
  • 인터페이스는 다중 상속이 가능하며, 구현 코드의 상속이 아니므로 ‘타입 상속’이라고 한다.

인터페이스 상속 구조

X.java

public interface X {

	void x();
}

Y.java

public interface Y {

	void y();
}

MyInterface.java

public interface MyInterface extends X, Y{

	void myMethod();
}

MyClass.java

public class MyClass implements MyInterface{

	@Override
	public void x() {
		System.out.println("x()");
	}

	@Override
	public void y() {
		System.out.println("y()");		
	}

	@Override
	public void myMethod() {
		System.out.println("myMethod()");		
	}
}

MyClassTest.java

public class MyClassTest {

	public static void main(String[] args) {

		MyClass mClass = new MyClass();

		X xClass = mClass;
		xClass.x();


		Y yClass = mClass;
		yClass.y();

		MyClass iClass = mClass;
		iClass.x();
		iClass.y();
		iClass.myMethod();
	}

}

클래스 상속과 인터페이스 구현 함께 쓰기

  • 실무에서 프레임워크나 오픈소스와 연동되는 구현을 하다 보면, 클래스 상속과 인터페이스 구현을 함께 사용하는 경우가 많다.

Shelf 상속 + Queue 구현 구조

요구사항:

  • 책이 순서대로 대여되는 도서관을 구현한다.
  • 책을 보관하는 자료 구조가 Shelf에 구현된다 (ArrayList).
  • Queue 인터페이스를 구현한다.
  • BookShelf는 Shelf 클래스를 상속받고 Queue를 구현한다.

Shelf.java

public class Shelf {

	 protected ArrayList<String> shelf;

	 public Shelf() {
		 shelf = new ArrayList<String>();
	 }

	 public ArrayList<String> getShelf(){
		 return shelf;
	 }

	 public int getCount() {
		 return shelf.size();
	 }

}

Queue.java

public interface Queue {

	void enQueue(String title);
	String deQueue();

	int getSize();
}

BookShelf.java

public class BookShelf extends Shelf implements Queue{

	@Override
	public void enQueue(String title) {
		shelf.add(title);
	}

	@Override
	public String deQueue() {
		return shelf.remove(0);
	}

	@Override
	public int getSize() {
		return getCount();
	}

}

public class BookShelf extends Shelf implements Queue 부분을 보면, BookShelf가 Shelf를 상속하면서 동시에 Queue를 구현하고 있다.

BookShelfTest.java

public class BookShelfTest {

	public static void main(String[] args) {

		Queue bookQueue = new BookShelf();
		bookQueue.enQueue("태백산맥1");
		bookQueue.enQueue("태백산맥2");
		bookQueue.enQueue("태백산맥3");

		System.out.println(bookQueue.deQueue());
		System.out.println(bookQueue.deQueue());
		System.out.println(bookQueue.deQueue());
	}

}

BookShelf 큐 실행 결과


16. 복습해 보세요

추상 클래스와 템플릿 메서드

Player가 있고, Player는 GameLevel 속성을 가집니다. 각 GameLevel 단계마다 run(), jump(), turn() 세 가지 기능이 업그레이드됩니다.

  • 초보자 레벨: 천천히 달립니다. run()만 가능
  • 중급자 레벨: 빠르게 달리고 점프할 수 있습니다. run(), jump() 가능
  • 고급자 레벨: 엄청 빠르게 달리고, 높게 점프하고, 턴할 수 있습니다. run(), jump(), turn() 가능

Player는 한 번에 하나의 레벨 상태만 가질 수 있습니다.

Player가 play() 중에 레벨에 있는 go(int count) 메서드를 호출하면, run() 하고 count 횟수만큼 jump() 한 뒤 turn() 합니다. 다음 클래스 다이어그램을 참고하여, 각 레벨에서 go()가 호출될 때 다음과 같이 출력되도록 하세요.

Player-GameLevel 클래스 다이어그램 go() 호출 시 출력 결과

인터페이스를 활용한 정책 프로그래밍

고객 센터에 전화 상담을 하는 상담원들이 있습니다. 고객에게서 전화가 오면 대기열에 저장되고 각 상담원에게 배분됩니다.

배분 정책은 크게 세 가지가 있습니다.

  1. 모든 상담원이 동일한 상담 건수를 처리하도록 상담원 순서대로 배분합니다.
  2. 쉬고 있거나, 할당된 통화 수가 가장 적은 상담원에게 배분합니다.
  3. 고객의 등급에 따라, 등급이 높은 고객은 업무 능력이 우수한 상담원에게 배분합니다.

세 가지 정책은 필요에 따라 바뀌어 운영될 수 있습니다. 다음 클래스 다이어그램을 참고하여 각 배분 규칙이 적용되도록 구현해 보세요.

Scheduler 배분 정책 클래스 다이어그램

테스트 프로그램은 다음과 같습니다. 문자를 입력받아, 입력된 문자에 따라 배분 규칙을 수행하도록 합니다.

public class SchedulerTest {

	public static void main(String[] args) throws IOException {

		System.out.println("전화 상담원 할당 방식을 선택하세요");
		System.out.println("R : 한명씩 차례대로");
		System.out.println("L : 대기가 적은 상담원 우선");
		System.out.println("P : 우선순위가 높은 고객우선 숙련도 높은 상담원");

		int ch = System.in.read();
		Scheduler scheduler = null;

		if ( ch == 'R' || ch =='r') {
			scheduler = new RoundRobin();
		}
		else if ( ch == 'L' || ch =='l') {
			scheduler = new LeastJob();
		}
		else if ( ch == 'P' || ch =='p') {
			scheduler = new PriorityAllocation();
		}
		else {
			System.out.println("지원되지 않는 기능입니다.");
			return;
		}

		scheduler.getNextCall();
		scheduler.sendCallToAgent();
	}

}

Scheduler 정책 실행 결과