본문으로 바로가기

추상클래스(Abstract Class) / 인터페이스(Interface)

category JAVA 2019. 12. 9. 14:53

추상클래스(Abstract Class)


추상클래스(Abstract Class)

 

추상클래스 정의

  • 자손 클래스들에서 어자피 재정의 해서 사용되기 때문에 조상 입장에서는 구현할 필요가 없거나
    조상 레벨에서 아직 작성할 수 없는 메서드에 대해 추상클래스임을 선언한다.
  • 작성 방법 :  선언부만 작성하고 구현부를 세미콜론(;)으로 대체하고 구현부가 없다는 의미로 abstract 키워드를 선언부에 추가한다.
  • 또한, 클래스가 abstract 메서드를 포함하고 있는 경우 반드시 클래스 선언부에도 abstract 키워드를 추가해야한다.
  • 이런 형태의 프로그래밍 기법을 'abstract method design pattern'이라고 한다.

[예시]

차량관리 프로젝트를 한다. 관리포인트는 현재위치를 보고(reportPosition)하는 기능과 연로를 주입(addFuel)하는 기능이다.

처음 관리할 자동자는 디젤 연료를 사용하는 SUV차량이고, 그 다음에는 전기 차량을 추가하였다. 

다행이 이 두 자동차의 관리 포인트는 똑같다.

 

상속구조로 자동차들을 관리할 경우 아래의 코드가 나온다.

package test.mypac;
//Vehicle 클래스
public class Vehicle {
	private int curX, curY;
	
	public void reportPosition() {
		System.out.println("현재 위치 : "+curX+", "+ curY);
	}
	
	public void addFuel() {
		System.out.println("모든 운송 수단은 연료가 필요");  //실제로 이 코드를 사용할 일이 없다.
	}
}

//DieselSUV extends Vehicle 클래스
public class DieselSUV extends Vehicle{
	@Override
	public void addFuel() {
		System.out.println("주유소에서 급유");
	}
}

//ElectricCar extends Vehicle 클래스
public class ElectricCar extends Vehicle{
	public void addFuel() {
		System.out.println("급속 충전");
	}
}

//테스트 클래스
public class VehicleTest {
	public static void main(String[] args) {
		Vehicle[] vehicles={new DieselSUV(), new ElectricCar()};
		for(Vehicle tmp:vehicles) {
			tmp.addFuel();
			tmp.reportPosition();
		}
		
	}
}

 

Vehicle 클래스의 addFuel() {}의 메서드를 작성할 당시에는 어떤 차종에 연료를 주입해야하는지 알 수 없어 어떤 연료를 주입해야할지 모르는 상황이다.

그렇다고 addFuel() 메서드를 없애면 Vehicle을 통해 메서드를 호출할 수 없어 DieselSUV와 ElectricCar로 강제형 변환을 해주어야한다.

 

이런 경우, 추상 클래스를 사용할 수 있다.

package test.mypac;

public abstract class Vehicle {		//abstract 키워드 추가
	private int curX, curY;
	
	public void reportPosition() {
		System.out.println("현재 위치 : "+curX+", "+ curY);
	}
	
	public abstract void addFuel();  //abstract 키워드 추가
}


public class DieselSUV extends Vehicle{
	@Override
	public void addFuel() {
		System.out.println("주유소에서 급유");
	}
}


public class ElectricCar extends Vehicle{
	public void addFuel() {
		System.out.println("급속 충전");
	}
}

 

추상 클래스의 특징과 용도

  • abstract 클래스는 객체를 생성할 수 없다.

    (동작할 내용이 없는 메소드를 호출할 경우 오류가 나기때문에 이를 사전에 방지하기 위함)

Vehicle a=new Vehicle(); //Cannot instantiate the type Vehicle
Vehicle b=new DieselSUV();  //자식을 참조하는 것은 문제 없음
Vehicle c=new ElectricCar(); //자식을 참조하는 것은 문제 없음

 

  • 구현의 강제를 통해서 프로그램의 안정성을 향상할 수 있다.

[예시]

위에서 제시한 차량관리 프로젝트를 이어서 보자.

 

HorseCart를 추가로 관리하게 되었고, 초보 프로그래머가 관리 모듈을 작성하게 되었다.

초보 프로그래머는 <abstract를 사용하지 않은 상태인 Vehicle 클래스>를 상속받아 사용하였고 메서드를 재정의하는 것을 잊어버렸다.

 

이와 같은 경우 addFuel()메서드를 호출하면 Vehicle클래스에 있는 "모든 운송 수단은 연료가 필요"라는 말만 출력되고 원인을 찾기 힘들 것이다.

 

그런데, <abstract가 정의된 Vehicle클래스

>를 상속받아 사용한다면 재정의를 하지 않았을 경우 컴파일을 할 수 없기때문에 addFuel()메서드를 재정의 할 수 밖에 없을 것이다.

//abstract 클래스를 상속 받고 메서드를 재정의 하지 않은 경우
public class HorseCart extends Vehicle {
	//The type HorseCart must implement the inherited abstract method Vehicle.addFuel()
}

 

  • 인터페이스에 선언된 메서드 중 일부 구현 할 수 있는 메서드를 구현해서 개발의 편의를 제공하기 위해서 abstract 클래스를 사용한다.

 

 

 

 

인터페이스(interface)


인터페이스(interface)

 

인터페이스 정의

  • 사전적 정의 : 두 시스템 간에 만나는 접점
  • 인터페이스 예시 :

    1. GUI(Graphic User Interface : 사용가자 그래픽 환경을 이용하여 애플리케이션을 사용하는 방법)

    2. 이클립스에서 '저장'버튼은 사용자와 이클립스 사이의 인터페이스이다.

       - 사용자 관점 : 어떻게 저장되는지 궁금하지 않음.
                           이클립스의 저장 로직이 변경되어도 어떻게 변경되었는지 궁금하지 않음.

인터페이스(저장버튼)을 바라보는 사용자 관점

          - 이클립스 관점 : 버튼이 어떻게 클릭되는지 궁금하지 않음.
                                  abstract 메서드에 대한 구현체 클래스를 갖고 있음.

@Override
public void save() {
	//1. 변경 정보확인
    //2. OutputStream을 통해 내용 저장
    //3. 화면상에 변경 완료 표시
}

 

인터페이스의 작성

  • java에서 인터페이스는 클래스가 아니다.
    작성 방법 : public interface Remocon { }
  • 인터페스 자체로 객체가 될 수 없다.
    생성자(단독 생성불가), 초기화 블록 존재하지 않음.
  • 멤버 변수와 메서드로만 구성된다.
    1. 멤버 변수 작성 방법
      - 모든 멤버 변수는 반드시 public static final 이어야한다. 따라서 이 3개의 제어자는 생략할 수 있다.
      - 생성자가 없기 때문에 blank final 형태로 구성할 수 없다.
    2. 메서드 작성 방법
      - 모든 메서드는 public abstract(추상 메서드) 이어야한다. 따라서 이 2개의 제어자는 생략할 수 있다.
  • 인터페이스는 다중 extends(상속) 받을 수 있으며 개수의 제한은 없다.
    (클래스는 단일 extends만 가능)
  • data type의 역할을 할 수 있다.
package test.inter;

public interface MyInterface {
	//필드 : 멤버변수
	public static final int MEMBER1=10;
	int MEMBER2=20;  //public static final 생략
	//메서드
	public abstract void method1(int param);
	void method2(int param);  //public abstract 생략
}

 

인터페이스 간의 관계(extends)

  • 인터페이스 간에도 'is a'와 'has a' 관계가 존재한다.
    참고 : https://sallykim5087.tistory.com/95
  • 인터페이스는 다중 extends(상속) 받을 수 있으며 개수의 제한은 없다.
    참고 ) interface의 모든 메서드는 추상메서드로 작성되어 구현부가 중복될 우려가 없어 다중 상속이 가능하다.

    그러나 class의 경우 구현부가 중복되어 복잡도가 올라갈 우려가 있어 단일 상속만 가능하다.
package test.inter;

public interface Fightable {
	int fire();
}

public interface Transformable {
	void changeShape();
}

//Fightable, Transformable 인터페이스를 상속 받는 Heroable 클래스
public interface Heroable extends Fightable, Transformable{
	void upgrade();
}

 

 

인터페이스의 구현과 객체 참조(implements)

  • 인터페이스는 모든 메서드가 abstract형태로 구성되어 있으므로 이 메서드들은 클래스를 통해서 구현되야한다.
  • 클래스에서 인터페이스를 구현하기 위해서는 'implements(구현)' 키워드를 사용한다.
    인터페이스를 구현(implements)하는 클래스는 인터페이스에 선언된 메서드를 불려 받는 형태가 된다.
  • 인터페이스 역시 추상클래스처럼 다형성의 성질을 가진다.
    (다형성 : 조상클래스 타입으로 자식 타입의 객체를 참조할 수 있는 성질)

[예시]

클래스에서 인터페이스를 implements한 경우,

abstract 클래스처럼 물려받은 abstract 메서드 들을 재정의 하지 않으면 컴파일 오류가 발생한다.

또는 클래스 자체를 abstract로 설정하여 객체 생성을 포기하고 상속 전용의 클래스로 활용할 수도 있다.

package test.inter;
//override 예시
public class IronMan implements Heroable{
	//멤버 변수 : 필드
	int weaponDamage=100;
	//implements : 추상매서드 구현하기
	@Override
	public int fire() {
		System.out.println("빔 발사 : "+this.weaponDamage+"만큼의 데미지를 줌");
		return this.weaponDamage;
	}

	@Override
	public void changeShape(boolean isHeroMode) {
		if(isHeroMode) {
			System.out.println("장갑 장착");
		}else {
			System.out.println("장갑 제거");
		}
	}

	@Override
	public void upgrade() {
		int before=weaponDamage;
		weaponDamage+=weaponDamage*0.1;
		System.out.println("무기 성능 개선 : "+before+"-->"+weaponDamage);
	}
}
package test.inter;
//다형성 테스트
public class IronManTest {
	public static void main(String[] args) {
		IronMan iman=new IronMan();
		Object obj=iman;
		Heroable hero=iman;
		Transformable trans=iman;
		Fightable fight=iman;
	}
}

 

인터페이스의 필요성 

<인터페이스가 필요한 이유>

  • 구현의 강제로 표준화
  • 인터페이스를 통한 간접적인 클래스 사용으로 손쉬운 모듈 교체 지원
  • 서로 상속의 관계가 없는 클래스들에게 인터페이스를 통한 관계부여로 다형성 확장
  • 모듈 간 독립적 프로그래밍으로 개발 시간 단축

(1) 구현의 강제

인터페이스에 선언된 모든 메서드들은 abstract이기 때문에 인터페이스를 구현하는 하위 클래스들은 모두 이 메서드 들을 구현해야한다. 따라서 실수로 필요한 기능을 빼먹는 경우가 발생하지 않을 것이다.

 

(2) 손쉬운 모듈 교체 지원 어려움

  • 인터페이스를 통한 간접 적인 클래스 사용으로 손쉬운 모듈 교체가 가능하다
  • 객체를 클래스로 참조하지 않고 인터페이스를 사용하게 된다면 뒤에서 동작하는 구현 클래스가 바뀌더라도 인터페이스를 사용하는 코드는 변경되지 않아도 된다.

[예시]

프린터를 사용하는 경우,

프린터는 종류가 많다. 만약 Printer 인터페이스를 통해 LaserPrinter와 DotPrinter를 사용하고 있다면,

LaserPrinter 프린터에서 DotPrinter로 변경하는데 있어서 부담이 덜할 것이다.

package inter.printer;
//인터페이스
public interface Printer {
	void print(String fileName);
}

//LaserPrinter implements Printer 클래스
public class LaserPrinter implements Printer {
	@Override
	public void print(String fileName) {
		System.out.println("LaserPrinter로 출력 : "+fileName);
	}
}

//DotPrinter implements Printer 클래스
public class DotPrinter implements Printer {
	@Override
	public void print(String fileName) {
		System.out.println("DotPrinter로 출력 : "+fileName);
	}
}

//PrintClient 클래스
public class PrintClient {
	private Printer printer;
	
	public void setPrinter(Printer printer) {
		this.printer=printer;
	}
	
	public void printThis(String fileName) {
		printer.print(fileName);
	}
}

//출력 테스트
public class PrintClientTest {
	public static void main(String[] args) {
		PrintClient p=new PrintClient();
		p.setPrinter(new DotPrinter());
		p.printThis("hello");
		System.out.println("------------------------");
		p.setPrinter(new LaserPrinter());
		p.printThis("안녕");
	}
}

/*
PrintClient객체를 만들고 LaserPrinter 또는 DotPrinter 객체를 설정하고 printThis()를 호출한다.
PrintClient 내부에서는 각각의 클래스를 직접 사용하지 않고 인터페이스를 통해 접근하기 때문에
구현 클래스 변경에 따른 코드의 변경이 전혀 필요없다.
*/

 

(3) 다형성 확장 어려움(예시 코드 이해 필요)

서로 상속 관계가 없는 클래스들에게 인터페이스를 통한 관계를 맺어서 다형성을 확장할 수 있다.

package inter.relation;

public class Phone {}
public class HandPhone extends Phone implements Chargeable{
	@Override
	public void charge() {
		System.out.println("handphone 충전중");
	}
}

public class Camera {}
public class DigitalCamera extends Camera implements Chargeable{
	@Override
	public void charge() {
		System.out.println("camera 충전중");
	}
}

public interface Chargeable {
	void charge();
}

public class RelationTest {
	void goodCase() {
		Chargeable[] objs={new HandPhone(), new DigitalCamera()};
		for(Chargeable obj:objs) {
			obj.charge();
		}
	}
	
	public static void main(String[] args) {
		RelationTest rct=new RelationTest();
		rct.goodCase();
	}
}

 

(4) 모듈 간 독립적 프로그래밍

모듈간 독립적 프로그래밍ㅇ로 개발 기간이 단축된다.

[예시]

Calculator를 두 팀이 만들기로 하였다. A팀은 UI를 개발하고, B팀은 계산기 내부 로직을 작성하는 팀이다.

A팀과 B팀의 작업물은 유기적으로 연결되어 있기 때문에 얼핏보면 한팀이 작업을 끝내야 다음팀이 일을 진행할 수 있을 것 처럼보인다.

 

그런데 이때 인터페이스를 사용하면 동시에 작업을 진행할 수 있다.

A팀은 UI를 구성하는데 있어서 구체적인 로직은 필요가 없고 단순히 '두개의 숫자를 넘겨주면 결과로 숫자 하나를 넘겨받는다(파라미터, 리턴값)'는 정도만 필요하다.

아래의 코드는 A,B 팀이 협의 해서 작성한 인터페이스다.

package inter.cowork.comm;

public interface Calculator {
	int add(int a, int b);
}

각팀은 작업을 개별적으로 동시에 진행하면된다.

A 팀은 로직을 제대로 구현할 필요는 없으므로 형식만 맞추는 것으로 충분하다. 이렇게 구현한 클래스를 통상 stub이라고 부른다.

//calculatorstub 작성. 
package inter.cowrok.ateam;

import inter.cowork.comm.Calculator;

public class CalculatorStub implements Calculator{
	@Override
	public int add(int a, int b) {
		System.out.println("파라미터 확인 : "+a+", "+b);
		return 0;
	}
}
//CalculatorStub를 임시로 사용
package inter.cowrok.ateam;

import java.util.Scanner;

import inter.cowork.comm.Calculator;

public class CalculatorClient {
	Calculator calLogic=new CalculatorStub();
	
	public void add() {
		System.out.println("첫번째 정수 입력");
		Scanner scanner=new Scanner(System.in);
		int a=scanner.nextInt();
		System.out.println("두번째 정수 입력");
		int b=scanner.nextInt();
		System.out.println("결과 : "+a+"+"+b+"="+calcLogic.add(a,b)); //CalculatorStub 객체로 결과 출력
	}
}
package inter.cowrok.ateam;

public class CalculatorTest {
	public static void main(String[] args) {
		CalculatorClient client=new CalculatorClient();
		client.add();
	}
}

 

B팀은 인터페이스를 제대로 구현한 클래스를 만든다. 이런 클래스를 통상 'impl'이라고 부른다.

//인터페이스 implements한 클래스
package inter.cowrok.bteam;

import inter.cowork.comm.Calculator;

public class Calculatorimpl implements Calculator{
	@Override
	public int add(int a, int b) {
		System.out.println("파라미터 확인 : "+a+", "+b);
		return a+b;
	}
}
//로직이 잘 돌아가는지 확인
package inter.cowrok.bteam;

import inter.cowork.comm.Calculator;

public class CalculatorLogicTest {
	public static void main(String[] args) {
		Calculator calcLogic=new Calculatorimpl();
		int result=calcLogic.add(100, 200);
		System.out.println("결과 확인 : "+result);
	}
}

 

A팀의 작업내용(CalculatorClient 클래스)에서 Calculator calLogic=new CalculatorStub();를 

Calculator calcLogic=new Calculatorimpl();로 변경해주기만 하면 완성이된다.

 

 

인터페이스에 추가할 수 있는 다양한 메서드

책 P.293 참고.