[자바/Java] 인터페이스(Interface) 란?

resilient

·

2021. 8. 9. 16:06

728x90
반응형

 

저번 시간에는 자바에서 다형성을 띄고 있는 것들 중 '오버 로딩'에 대해서 정리해보았다.

다시 한번 말하지만 자바는 OOP 즉, 객체 지향 언어이고 OOP의 큰 장점이자 OOP에서 가장 중요한 개념 중 하나로는 다형성(Polymorphism)이 있다. 

이번 시간에는 자바의 다형성을 극대화해서 개발코드를 간결하고 효율적으로 만들고 유지보수를 쉽게 하게 해주는 인터페이스에 대해 알아보려고 한다.

 

인터페이스란?

동일한 목적 하에 동일한 기능을 수행하게끔 강제하는 것이 바로 인터페이스의 역할이자 개념이다.

간단하게 얘기하면 구현된 것은 없지만 밑그림이 그려져 있는 '설계도'라고 생각하면 된다. 예시를 들어보자.

 

빵가게 사장이 다음날 까지 3명의 직원한테 내일 판매할 식빵을 만들라고 했다.

1번 직원은 쌀가루를 이용해서 10cm짜리 식빵을 만들었고

2번 직원은 밀가루를 이용해서 12cm 짜리 식빵을 만들었고

3번 직원은 밀가루를 이용해서 50cm 짜리 식빵을 만들었다.

 

다음 날 사장이 빵가게 도착해서 확인해보면 당황할 수밖에 없을 것이다. 어떤 식빵을 판매해야 할지도, 그리고 왜 이렇게 식빵을 만들었는지도 의아해할 것이다.

왜 이런일이 발생했을까? 설계도 즉, 기준이나 규격이 없기 때문이다.

사장은 직원들한테 밀가루를 이용해서 30cm짜리 식빵을 만들어놔 라고 말을 했어야 했다.

그래야 3명의 직원들이 만든 식빵을 다음날 판매 할 수 있었을 것이다.

 

그렇다면, 인터페이스를 어떻게 사용할까?

인터페이스를 작성하는 것은 클래스를 작성하는 것과 같다. 다만 class 키워드가 아닌 interface키워드를 사용한다.

또한 인터페이스에서는 4가지를 정의 또는 구현 할 수 있다.

public interface 인터페이스명 {

  //상수,멤버변수

  타입 상수명 = 값;
  
  //추상 메소드
  
  타입 메소드명(매개변수, ... );

  //디폴트 메소드

  default 타입 메소드명(매개변수, ... ){

    //구현부

  }

  //정적(static) 메소드

  정적(static) 타입 메소드명(매개변수) {
  
    //구현부
  }

}

 

  • 상수, 멤버 변수 : 인터페이스에서 값을 정해줄 테니 함부로 바꾸지 말고 제공해주는 값만 참조해라 (절대적)
  • 추상메소드 : 가이드만 줄 테니 추상 메서드를 오버 라이딩해서 재 구현해라. (강제적)
  • 디폴트 메서드 : 인터페이스에서 기본적으로 제공해주지만, 맘에 안 들면 오버 라이딩해서 각자 구현해서 써라. 디폴트 메서드를 사용하려면 인스턴스가 꼭 있어야 사용할 수 있다. (선택적)
  • 정적(static) 메서드 : 인터페이스에서 제공해주는 것으로 무조건 사용해라. 인스턴스 안 만들고 바로 쓸 수 있다. (절대적)

 

아래 예시를 같이 보자. 계산기 프로그램을 만들기 위해 먼저 Calc이라는 인터페이스를 선언해주고 멤버들을 넣어주었다. 인터페이스 안의 멤버들은 주석 처리된 부분과 같은 제약조건이 있다.

public interface Calc {
	
	double PI = 3.14; //상수,멤버변수에는 public static final이 들어가있다.
	int ERROR = -999999999;
	
	int add(int num1, int num2);//추상메서드에는 public abstract 이 들어가있다.
	int substract(int num1, int num2);
	int times(int num1, int num2);
	int divide(int num1, int num2);
    
    default void description(){ // 디폴트 메소드 오버라이드 해서 사용
		System.out.println("정수 계산기를 구현합니다");
		myMethod();
	}
    private void myMethod() {
		System.out.println("private 메서드 입니다.");
	}
	
	private static void myStaticMethod() {
		System.out.println("private static 메서드 입니다.");
	}
    
    
    static int total(int[] arr){  //정적 메소드 (그대로 사용해야한다 인스턴스없어도가능)
		
		int total = 0;
		
		for(int i: arr){
			total += i;
		}
		myStaticMethod();
		return total;
	}
}

 

그럼 위의 Calc 인터페이스에 있는 메서드들을 구현해보자. Calculator라는 클래스에서 Calc에 있는 메서드를 구현한다는 의미로 implements를 사용해서 작성해줬다.

 

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;
	}
	
}

 

위에 Calculator 클래스는 Calc 인터페이스에서 선언한 메서드가 모두 구현되지 않았다. 그럼 완벽한 Calculator를 구현해보도록 하자. Calc을 구현한 Calculator을 extends(상속) 받아서 CompleteCalc이라는 클래스를 만들었다.

아래와 같이 구현하면 Calc 인터페이스에 선언했던 모든 메서드를 사용할 수 있게 된다.

 

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 num1/num2;
		else 
			return Calc.ERROR;
	}
	
	public void showInfo(){
		System.out.println("Calc 인터페이스를  전부 구현하였습니다" );
	}

	@Override   //Calc인터페이스안에있는 디폴트 메서드를 오버라이드로 가져와서사용
	public void description() {
		super.description();
	}
}

 

그럼 위에 계산기를 테스트해볼 수 있는 main CalculatorTest클래스를 만들어서 테스트해보자.

public class CalculatorTest {

	public static void main(String[] args) {

		int num1 = 10;
		int num2 = 5;
		
		CompleteCalc calc = new CompleteCalc();
		System.out.println(calc.add(num1, num2));
		System.out.println(calc.substract(num1, num2));
		System.out.println(calc.times(num1, num2));
		System.out.println(calc.divide(num1, num2));
		calc.showInfo();
		
		Calc newCalc = calc;
		
		calc.description();   //디폴트 메소드 호출
				
	    int[] arr = {1,2,3,4,5};
		System.out.println(Calc.total(arr));  //정적 메소드 사용하기

	}
}

 

인터페이스의 상속

 

인터페이스 만들 때는 클래스와 다르게 여러 개를 상속받을 수 있다.

 

아래와 같이 X라는 인터페이스에 x메서드를 만들고, Y라는 인터페이스에 y메서드를 만들면 MyInterface라는 인터페이스에서는 X, Y인터페이스 둘 다 상속받아서 사용 가능하다.

 

그렇게 되면 MyInterface를 구현한 MyClass에서는 X, Y 인터페이스 안의 x,y메서드를 구현해줘야 한다. 물론 MyClass에서 다른 메소드를 정의해줄 수도 있다.

public interface X {
	
	void x();

}
public interface Y {
	
	void y();

}
public interface MyInterface extends X, Y{

	void myMethod();
}
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 클래스를 만들어보자.

 

public class MyClassTest {

	public static void main(String[] args) {

		MyClass mClass = new MyClass();
		X xClass = mClass;
		xClass.x();
		
		Y yClass = mClass;
		yClass.y();
		
		MyInterface iClass = mClass;
		iClass.myMethod();
		iClass.x();
		iClass.y();
	}
}

 

인터페이스를 이용한 다형성

다형성에 대해 배웠을 때 자식 클래스의 인스턴스를 부모 타입의 참조 변수로 참조하는 것이 가능했다.

인터페이스 또한 구현한 클래스의 부모라 할 수 있으므로 인터페이스 타입의 참조변수를  구현한 클래스의 인스턴스를 참조할 수 있다.

위에 MyClassTest 예제를 보자. X라는 인터페이스를 MyClass가 구현했기 때문에

X xClass = mClass; 부분을 보면 X는 인스턴스이지만 mClass(=new MyClass) 인스턴스를 X타입의 참조 변수로 참조하는 것이 가능하다고 볼 수 있다.

 

메서드의 리턴 타입으로 인터페이스의 타입을 지정하는 것 역시 가능하다

리턴 타입이 인터페이스라는 것은 메서드가 해당 인터페이스에서 제공하는 메서드를 구현한 클래스의 인스턴스를 반환한다는 것이다.

 

인터페이스의 장점

마지막으로 그럼 인터페이스를 왜 사용하는지, 장점에 대해 알아보자.

  • 개발 시간을 단축시킬 수 있다.
    • 메서드를 호출하는 쪽에서는 메서드를 어떻게 구현했는지와는 상관없이 선언 부만 가져와서 사용하면 되기 때문이다.
  • 표준화가 가능하다(설계도가 있기 때문에)
  • 서로 관계없는 클래스들의 관계를 맺어 줄 수 있다
    • 아무 관계없는 클래스들에게 하나의 인터페이스를 공통적으로 구현하도록 함으로써 관계를 맺을 수 있다.
  • 독립적인 프로그래밍이 가능하다.
    • 인터페이스를 사용하면 클래스의 선언과 구현을 구분시킬 수 있기 때문에 실제 구현에 독립적인 프로그램을 작성하는 것이 가능하다.
반응형