Welcome! Everything is fine.

[3-2] 추상클래스 활용하기(1) ~ 인터페이스와 다형성 구현(2) 본문

자격증 및 기타 활동/J2KB

[3-2] 추상클래스 활용하기(1) ~ 인터페이스와 다형성 구현(2)

개발곰발 2021. 8. 28.
728x90

💡 추상클래스(abstact class)

추상클래스는 구현코드 없이 메서드의 선언만 있는 추상메서드를 포함한 클래스를 말한다. 물론 추상메서드뿐만 아니라 구현메서드도 들어갈 수 있다.  abstract 키워드를 사용하며, 추상클래스는 new(인스턴스화) 할 수 없다. 추상클래스는 상속을 하기 위해 만드는 클래스이기 때문에 자기 혼자 돌아가는 클래스가 아니다. 따라서 상위 클래스에서는 어떤 것을 보여줄지 정의하고, 하위 클래스에서 구체적으로 어떤 것을 보여줄지 구현한다. 즉, 상위 클래스에서 구현하지 못하고 하위 클래스에 위임해야하는 부분을 추상메서드로 만든다. 

 

다음 코드는 구현 코드는 없지만 구현부는 존재하는 메서드다.

public void display() {};

그러나 다음 코드는 구현코드는 물론 구현부도 없다. 이때는 에러가 나기 때문에 중괄호를 사용하여 구현부를 넣거나 abstract 키워드를 사용해야한다.

public void display(); // 에러

다음과 같이 수정한다.

public abstract void display();

이렇게 추상메서드를 만들었다면 클래스도 다음과 같이 abstract 키워드를 사용해 추상클래스로 만들어야 한다.

public abstract class Computer {
    public abstract void display();
}

다음 추상클래스처럼 추상클래스에는 구현메서드도 들어갈 수 있다. 구현메서드는 공통으로 사용할 메서드를 넣는다. 물론 구현메서드라도 하위 클래스에서 재정의할 수 있다.

public abstract class Computer {
    public abstract void display();
    public abstract void typing();
	
    public int add(int x, int y) {return x + y;};
	
    //공통으로 사용할 메서드지만 재정의 가능
    public void turnOn() {
	    System.out.println("전원을 켭니다.");
    }
	
    public void turnOff() {
	    System.out.println("전원을 끕니다.");
    }
}

Computer 클래스의 하위 클래스인 DeskTop 클래스와 NoteBook 클래스를 만들었다. 각각 extends 키워드를 사용하여 Computer 클래스에서 구현하지 않은 display() 메서드와 typing() 메서드를 다르게 구현했다.

public class DeskTop extends Computer{

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

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

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

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

NoteBook 클래스를 다시 구체화한 MyNoteBook 클래스도 만들었다.

public class MyNoteBook extends NoteBook{

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

이것을 출력하기 위해 만든 ComputerTest 클래스다. 여기서 주의할 점은 추상클래스는 인스턴스화 할 수 없다는 점이다. 따라서 추상클래스가 아닌 DeskTop과 MyNoteBook의 인스턴스만 생성될 수 있다.

public class ComputerTest {

    public static void main(String[] args) {
        //Computer c1 = new Computer(); 오류, 추상 클래스는 인스턴스화 할 수 없다.
        //c1.display(); 메서드 안에 구현된 코드가 없어서 생성 될 수 없다.
        Computer c2 = new DeskTop();
        //Computer c3 = new NoteBook();
		
        NoteBook c4 = new MyNoteBook();
	    c2.display();
	    c4.display();
    }
}

결과는 다음과 같다.

DeskTop display()
NoteBook display()

💡 템플릿 메서드

템플릿 메서드는 싱글톤 패턴처럼 자바의 디자인 패턴의 종류 중 하나다. 추상메서드나 구현메서드를 활용하여 전체 기능의 흐름을 정의하는 메서드. final로 선언한다. final로 선언하면 하위 클래스에서 재정의 할 수 없다. 추상클래스로 선언된 상위 클래스에 템플릿 메서드를 활용하여 전체적인 흐름을 정의하고, 하위 클래스에서 다르게 구현되어야 하는 부분은 추상 메서드로 선언해서 하위 클래스가 구현하도록 한다. 

 

Car라는 추상클래스에 기본적인 동작을 나타내는 startCar()메서드와 turnOff()메서드를 제외하고 다른 부분은 추상메서드로 구현했다. 차의 종류에 따라 달리고 멈추는 방법 등이 다르기 때문이다. washCar는 구현부만 존재하고 구현코드는 없는 메서드로, 하위 클래스에서 정의할 수도 있고 하지 않을 수도 있다. 맨 마지막에 run()메서드에 자동차가 동작하는 일련의 과정을 나열하였고, 이런 시나리오는 재정의할 수 없으므로 final 키워드를 사용했다. 이것이 바로 템플릿 메서드이다.

public abstract class Car {
	
	// 차의 종류에 따라 달리고 멈추는 방법이 달라서 추상메서드로 구현
	public abstract void drive();
	public abstract void stop();
	public abstract void wiper();
	public void washCar(){}
	
	public void startCar() {
		System.out.println("시동을 켭니다.");
	}
	
	public void turnOff() {
		System.out.println("시동을 끕니다.");
	}
    
	public final void run() {
		startCar();
		drive();
		wiper();
		stop();
        washCar();
		turnOff();
	}
}

다음은 Car 클래스를 상속한 ManualCar 클래스와 AICar 클래스다. 각자 구현한 코드가 다르고, AICar 클래스에는 ManualCar 클래스에 없는 washCar() 메서드의 내용도 구현하였다. 여기서 final 메서드인 run() 메서드는 재정의할 수 없다. 

public class ManualCar extends Car {

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

	public void stop() {
		System.out.println("사람이 브레이크로 정지합니다.");

	}

	public void wiper() {
		System.out.println("사람이 수동으로 와이퍼를 조작합니다.");
		
	}
	
	//public void run() {} final 메서드이기 때문에 재정의 할수없다는 오류가 뜸

}

이때 필요에 의해 재정의하여 사용하는 washCar()메서드와 같은 메서드를 후크메서드라고한다. 후크메서드는 아무 일도 하지 않거나 기본 행동을 정의하는 메소드로, 하위클래스에서 오버라이드 할 수 있다.

public class AICar extends Car {

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

	public void stop() {
		System.out.println("자동으로 멈춥니다.");

	}

	public void wiper() {
		System.out.println("비나 눈의 양에 따라 자동으로 조절됩니다.");
		
	}
	
	public void washCar() {
		System.out.println("자동으로 세차가 됩니다.");
	}
}
public class CarTest {

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

위 코드대로 출력하면 결과는 다음과 같다. 각자 구현은 다르게 했어도 시나리오는 똑같은 것이다.

시동을 켭니다.
사람이 운전합니다.
사람이 핸들을 조작합니다.
사람이 수동으로 와이퍼를 조작합니다.
사람이 브레이크로 정지합니다.
시동을 끕니다.
====================
시동을 켭니다.
자율 주행합니다.
자동차가 스스로 방향을 전환합니다.
비나 눈의 양에 따라 자동으로 조절됩니다.
자동으로 멈춥니다.
자동으로 세차가 됩니다.
시동을 끕니다.

📃 final 예약어

- final 변수는 값이 변경 될 수 없는 상수이다

- final 변수는 오직 한 번만 값을 할당할 수 있다.

- final 메서드는 하위 클래스에서 재정의할 수 없다.

- final 클래스는 더 이상 상속되지 않는다.

💡 인터페이스(interface)

인터페이스란 모든 메서드가 구현 코드가 없는 메서드(추상 메서드)로 이루어진 클래스를 말한다. 인터페이스에 선언된 모든 메서드는 public abstract추상 메서드이다. 또한 인터페이스에 선언된 모든 변수는 public static final상수이다.

 

만약, 아래와 같이 코드를 적는다면 에러가 난다.

// 에러 발생
public class Calc {
    
    double PI = 3.14;
    int ERROR = -999999999;
    
    int add(int num1, int num2);
    int substract(int num1, int num2);
    int times(int num1, int num2);
    int divide(int num1, int num2);
}

에러를 없애려면 interface 키워드를 사용한다. 그렇게하면 abstract 키워드를 적지않아도 추상메서드가 되기 때문이다. 또한 상수 역시 public static final을 굳이 적지 않아도 된다.

public interface Calc {
	
    double PI = 3.14;
    int ERROR = -999999999;
    
    int add(int num1, int num2);
    int substract(int num1, int num2);
    int times(int num1, int num2);
    int divide(int num1, int num2);
}

interface에 정의된 추상메서드는 implements를 이용하여 불러오고, 이렇게 implements한 클래스에서 구현한다. 아래 코드에서는 times()메서드와 divided()메서드를 구현하지 않았기 때문에 abstract 키워드를 사용하였다.

public abstract class Calculater implements Calc{

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

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

위의 추상클래스 Calculater를 상속하여 다시 새로운 클래스에서 나머지 내용을 구현해도 된다.

public class CompleteCalc extends Calculater{

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

    @Override
    public int divide(int num1, int num2) {
	    if(num2 !=  0) {
		    return num1 / num2;
	    }
	    return ERROR;
    }
    
    public void showInfo() {
        System.out.println("Calc 인터페이스를 구현하였습니다.");
    }
}
public class CalculatorTest {

    public static void main(String[] args) {

        int num1 = 10;
        int num2 = 2;
		
        Calc calc = new CompleteCalc();
        System.out.println(calc.add(num1, num2));
		
        calc.showInfo(); // 에러 발생
    }
}

출력하면 다음과 같다. 여기서 주의할 점은 CompleteCalc 클래스에서 구현한 showInfo() 메서드는 불러올 수 없다는 점이다. 이 타입이 Calc이기 때문에 호출하려면 다운캐스팅이 필요하다.

12

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

✔ 상속에서의 형 변환과 동일한 의미이다.

✔ 클래스 상속과 달리 구현코드가 없기 때문에 여러 인터페이스를 구현할 수 있다. 

✔ 형 변환시 사용할 수 있는 메서드는 인터페이스에서 선언된 메서드만 사용할 수 있다.

✔ 인터페이스는 "Client Code"와 서비스를 제공하는 "객체" 사이의 약속이다.

 어떤 객체가 어떤 interface 타입이라 함은 그 interface가 제공하는 메서드를 구현했다는 의미이다.

 Client는 어떻게 구현되었는지 상관없이 interface의 정의만을 보고 사용할 수 있다.(ex. JDBC)

 다양한 구현이 필요한 인터페이스를 설계하는 일은 매우 중요한 일이다.

 

다음 예시를 하나 더 보자. 인터페이스의 다형성에 대해 이해할 수 있다. 설명은 위 예시와 비슷하므로 최소화하겠다.

public interface Scheduler {
	
    // 여기에서는 일단 해야할 일들을 정의만 함.
    void getNextCall();
    void sendCallToAgent();
}
public class LeastJob implements Scheduler{

    @Override
    public void getNextCall() {
        System.out.println("상담 전화를 순서대로 대기열에서 가져옵니다.");
    }

    @Override
    public void sendCallToAgent() {
        System.out.println("현재 상담업무가 없거나 상담 대기가 가장 적은 상담원에게 할당합니다.");
    }
}
public class PriorityAllocation implements Scheduler{

    @Override
    public void getNextCall() {
        System.out.println("고객의 등급이 가장 높은 고객의 전화를 먼저 가져옵니다.");	
    }

    @Override
    public void sendCallToAgent() {
        System.out.println("업무 skill이 가장 높은 상담원이 대기열에 앞에 우선 배분합니다.");	
    }
}
public class RoundRobin implements Scheduler{

    @Override
    public void getNextCall() {
        System.out.println("상담 전화를 순서대로 대기열에서 가져옵니다.");
    }

    @Override
    public void sendCallToAgent() {
        System.out.println("다음 순서 상담원에게 배분합니다.");
    }
}

최종 출력 단계에서 똑같은 코드를 불렀지만 각 인스턴스가 무엇이냐에 따라 해당되는 구현코드가 달라지는 것, 이것이 바로 다형성이다.

import java.io.IOException;

public class SchedulerTest {

	public static void main(String[] args) throws IOException {
		
    // R, L, P 입력값에 따라 출력
    System.out.println("전화 상담 배분방식을 선택하세요. R, L, 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();
    }
}

출력 결과는 다음과 같다.

전화 상담 배분방식을 선택하세요. R, L, P
R
상담 전화를 순서대로 대기열에서 가져옵니다.
다음 순서 상담원에게 배분합니다.
전화 상담 배분방식을 선택하세요. R, L, P
L
상담 전화를 순서대로 대기열에서 가져옵니다.
현재 상담업무가 없거나 상담 대기가 가장 적은 상담원에게 할당합니다.
전화 상담 배분방식을 선택하세요. R, L, P
P
고객의 등급이 가장 높은 고객의 전화를 먼저 가져옵니다.
업무 skill이 가장 높은 상담원이 대기열에 앞에 우선 배분합니다.