[Java] 객체지향 프로그래밍 - 인터페이스

인터페이스(Interface)

일종의 추상 클래스라 보시면 되요. 하지만 조금 달라요. 인터페이스Interface는 추상 메서드를 가지지만 추상화 정도가 높아서 추상 클래스와 달리 구체화된 일반 메서드 또는 멤버 변수를 구성원으로 가질 수 없어요. 오직 추상 메서드와 상수만을 멤버로 가질 수 있고 그 외에 다른 어떤 요소도 허용하지 않아요.

인터페이스를 작성하는 것은 클래스 작성 방식과 같아요. 그대신 class가 아닌 interface를 사용해야 해요. 그리고 접근 제어자로 public 또는 default만 사용할 수 있어요.

그리고 아래와 같은 제약사항이 있어요.

  • 모든 멤버 변수는 public static final이어야 하며, 생략이 가능해요
  • 모든 메서드는 public abstract이어야 하며, 생략이 가능해요
    (클래스 메서드와 디폴트 메서드 제외)
interface 인터페이스이름{
	public static final 타입 변수 =;
    public 타입 변수 =; // 위와 동일
    public abstract 메서드이름(매개변수);
    메서드이름(매개변수);	// 위와 동일
}

원래 인터페이스의 모든 메서드는 추상 메서드여야 했는데 JDK1.8로 넘어가면서 인터페이스에 static 메서드와 디폴트 메서드를 추가할 수 있게 되었어요.

interface Impl{
    static void FUNC() {}				// static 메서드
    default void func() { 				// default 메서드,
    									// 선언 안되면 얘를 실행해요. 접근 제어자는 public이에요
    	System.out.println("Impl");
    }
}

class A implements Impl{
    @Override
    public void func() {				// 물론 상속도 되요
    	System.out.println("A");
    }
}

여기서 디폴트 메서드는 인터페이스 변경에 아주 유용해요. 디폴트 메서드는 추상 메서드의 기본적인 구현을 제공해요. 그래서 인터페이스가 메서드가 추가되어도 그 인터페이스를 상속하는 모든 클래스들을 찾아서 일일히 오버라이딩할 번거로움이 개선되요. 이전부터 항상 고민해왔던 문제를 JDK 개발자 분들의 고민 끝에 개선한 결과물이라 보시면 되요.

물론 추가를 하게 되면 당연히 기존 메서드와 이름 중복이 되어 충돌이 발생하겠죠. 이를 해결하는 규칙은 아래와 같아요.

1. 여러 인터페이스의 디폴트 메서드 간의 충돌
인터페이스를 구현한 클래스에서 디폴트 메서드를 오버라이딩해요
2. 디폴트 메서드와 부모 클래스의 메서드 간의 충돌
조상 클래스의 메서드가 상속되고, 디폴트 메서드는 무시되요

interface a {
    default void func() {
        System.out.println("a");
    }
}

interface b {
    default void func() {
        System.out.println("b");
    }
}

// 다중 상속으로 메서드 중복이어도 오버라이딩하면 그만!
class B implements a, b{
	@Override 
    public void func() {

    }
}

JDK1.9 이후 private 메서드가 가능해요

JDK1.9로 넘어오면서 private 메서드도 사용할 수 있게 되요. 아래와 같은 특성을 가져요.

  • 메소드 body가 있고 abstract이 아니에요
  • static 이거나 non-static 일 수 있어요
  • 구현 클래스와 인터페이스가 상속되지 않아요
  • 인터페이스에서 다른 메소드를 호출 할 수 있어요

출처: https://flyburi.com/605 [버리야 날자]

활용의 예를 한가지 들자면, 기존에 private을 사용할 수 없기 때문에 하나의 디폴트 메서드에는 오직 디폴트 메서드에서만 사용하는 로직을 하나에 다 집어 넣어야 했어요. 왜냐하면 디폴트 메서드를 나눠서 구현하면 상속 받은 클래스가 이를 잘못 사용하기 때문이에요. 그런데 private 허용되면서 보다 깔끔하면서도 접근 제어 영역을 정할 수 있는 코드가 완성되는거죠.

JDK1.9 전

public interface VeryImportantInterface {
	// 기존
	default void coreFunc() {
    	// ㅁ애애애애우 중요하면서 만 줄이 넘어가는 코드야
        // 여기 로직들은 다 건들면 안돼
	}
    // 개선
    default void coreFunc() {
    	// 너무 기니까 관리하기 힘들어... 나눠야지
    	coreFuncLevel1();
    	coreFuncLevel2();
    }
    // 앗! 근데 default다 보니까 상속하는 애들이 잘못 사용할 수도 있을텐데...
    // Level1의 로직을 오버라이딩해서 바꿨어
    // 상속한 애가 Level2만 필요하다고 써버렸어... 이러면 터질텐데 ㅠ
    default void coreFuncLevel1();
    default void coreFuncLevel2();
}

JDK1.9 후

public interface VeryImportantInterface {
	// 기존
	default void coreFunc() {
    	// ㅁ애애애애우 중요하면서 만 줄이 넘어가는 코드야
        // 여기 로직들은 다 건들면 안돼
	}
    // 개선
    default void coreFunc() {
    	// 너무 기니까 관리하기 힘들어... 나눠야지
    	coreFuncLevel1();
    	coreFuncLevel2();
    }
    
    // 헤헤 이러면 아무도 접근 못해 오직 인터페이스만 할 수 있어
    // 이건 abstract가 아니라서 오버라이딩도 불가능해!
   	private void coreFuncLevel1() {
    	// 매우 중요한 코드 1단계
	}
    private void coreFuncLevel2() {
    	// 매우 중요한 코드 2단계
	}
}

인터페이스 상속

인터페이스는 인터페이스로부터만 상속이 가능해요. 클래스와 달리 다중 상속, 즉 여러 개의 인터페이스로부터 상속 받는 것이 가능해요.

interface Movable { void move(int x, int y); }
interface Attackable { void attack(); }
interface Fightable extends Movable, Attackable {
	// 굳이 선언 안해도 move()와 attack()이 있어요
}

이렇게 Fightable 인터페이스는 MovableAttackable 추상 메서드를 멤버로 가지게 할 수 있어요.

그리고 인터페이스만 있으면 아무 것도 할 수 없겠죠? 당연히 클래스에 상속을 해서 본격적으로 구체화를 해줘야 해요. implements 키워드를 통해 상속이 가능해요. 당연하게도 클래스는 인터페이스에 한해서 다중 상속이 가능해요.

class User extends Person implements Movable, Attackable {
	// 인터페이스 추상 메서드 구현
}
// 위랑 같아요. Fightable은 두 인터페이스를 상속했으니까요
class User extends Person implements Fightable {
	// 인터페이스 추상 메서드 구현
}

// 되긴 해요. 다만 TMI라고 경고 메시지 떠요.
class User extends Person implements Fightable {
	// 인터페이스 추상 메서드 구현
}

인터페이스 다중 상속

다중 상속의 문제점에서 두 부모로부터 상속 받는 메서드 중에 이름이 같다면 모호성이 발생해 에러가 발생해요. 그래서 자바는 클래스간 다중 상속을 허용하지 않아요. 하지만 인터페이스가 이를 대체할 수 있어요. 그런데 이러면 '다중 상속을 막은 의미가 없지 않을까?'라는 생각을 하게 되요.

인터페이스는 다중 상속이 가능하다는 것일 뿐이지, 이걸로 다중 상속을 구현하는 경우는 거의 없어요. 추상 메서드를 선언해 설계의 완성도를 높이는 것이 목적이기 때문이죠.

그렇다 해서 다중 상속의 장점을 활용하지 못하는 것은 아니에요. 예를 들어 설명해볼게요.

interface VeryImportantable {
	default void func() {
    	// 매ㅇ애ㅐㅔ애애애우 중요하면서 만줄짜리 코드
	}
}
interface VeeeryImportantable {
	default void func() {
    	// 매ㅇ애ㅐㅔ애애애애애애애우 중요하면서 만줄짜리 코드
	}
}

이렇게 매우 중요하기 때문에 인터페이스에 디폴트 메서드로 구현해 놓은 상태에요. 근데 이 두 인터페이스를 모두 써야하는 클래스가 필요해진거에요.

class App implements VeryImportantable, VeeeryImportantable {
	
    public void func() {
    	// 오버라이딩은 했지만...어떤 코드를 가져와야 하지?
    }
}

이러면 문법상 문제는 되지 않지만, 필요에 따라 Very를 써야하는 경우도 있고, Veery를 써야하는 경우가 있어요. 물론 그대로 복붙하면 되겠지만 이는 비효율적이에요. 만약에 디폴트 메서드가 스펙이 바뀌어서 로직이 수정되면 이를 사용했던 애들도 전부 적용해야 겠죠? 심지어 그 클래스 개수가 10개만 되도...

그래서, 두 인터페이스 중 더 비중이 높은 쪽을 implements하고 다른 한쪽은 클래스 내부에 멤버로 포함시키는 방식으로 처리하면 유지보수적으로 보더라도 이후 수정을 편하게 할 수 있어요.

class App implements VeeeryImportantable {
	VeryImportantable i = new AppImpl();
    
    // 인터페이스 안에 있었던 추상 메서드들
	public void exec() {
		i.exec();
    }
    // 인터페이스 안에 있었던 추상 메서드들
	public void exec2() {
		i.exec2();
    }
    ...
}

class AppImpl implements VeryImportantable {

    // 인터페이스 안에 있었던 추상 메서드들
	public void exec() {
    }
    // 인터페이스 안에 있었던 추상 메서드들
	public void exec2() {
    }
    ...
}

이렇게 다중 상속을 활용하면서도 이후 수정이 편한 코드를 만들 수 있어요.

인터페이스 다형성

다형성의 개념 덕에 자식 클래스의 인스턴스를 부모 타입 참조 변수로 참조하는 것이 가능해요.

interface a {}
class A implement a {}

a aInst = new A();		// Up-casting
A aInst2 = (a)aInst;	// Down-casting

기존 클래스간 다형성의 규칙과 동일해요(자세한건 링크를 참고하세요 ㅎㅎ).

참고

버리야 날자님 블로그
baeldung - java interface private method

좋은 웹페이지 즐겨찾기