본문 바로가기

Java

[JAVA]인터페이스(interface) 뽀개기

자바에서 말하는 인터페이스는 객체의 사용 방법을 정의한 타입이다. 인터페이스는 객체의 교환을 통해 다형성을 구현하는데 매우 중요한 역할을 한다. 이런 인터페이스는 어떤 역할을 할까?

약간 비유해보자면 인터페이스는 일종의 규격, 양식이라고 할 수 있다. 규격과 양식이 그렇다면 어떤 힘을 가지고 있는지 생각해보자.

예를 들어 학교에서 산불예방 포스터 그리기 대회 공지가 나왔다. 그 공지에는 이렇게 쓰여있었다.

  • 산불예방 포스터 그리기 대회
    • 산불 예방의 의미가 들어간 포스터를 제작해 제출하시면 됩니다.
    • 기간 : 8월 8일까지

이 대회 공지를 본 학생 A, B, C는 각각 다음과 같이 행동했다.

  • A는 A4용지에 포스터를 그려서 8월 5일에 제출했다.
  • B는 B4용지에 포스터를 그려서 컬러로 복사해 8월 7일에 제출했다.
  • C는 A1 용지에 포스터를 그려서 흑백으로 복사해 당일날 제출했다.

심사위원들은 이 제출품을 보고 판단 기준을 세우기 힘들다. 작품마다 정해진 규격이나 양식이 없어서 너무 제출품이 자유로운 것이다. 따라서 기간이 2주 연장되고 공지가 새로 올라왔다.

  • 산불예방 포스터 그리기 대회
    • 산불 예방의 의미가 들어간 포스터를 제작해 제출하시면 됩니다.
    • 양식 : A4
    • 기간 : 8월 8일까지
    • 제출 시 컬러로 복사해 복사본을 제출할 것
    • 포스터를 그리는 도구는 상관없지만 사인펜, 색연필, 연필(샤프) 권장.

이 공지는 정해진 규격과 룰이 있어 심사를 하는데 판단하기 수월해졌다. 이 예시로 인터페이스 개념이 어느 정도 적립되었을 것이다. 인터페이스는 규격과 양식, 룰과 같은 역할을 한다. 따라서 인터페이스는 동일한 목적 하에 동일한 기능을 보장하게 하기 위함이다. 이를 통해 자바의 다형성이 극대화되고 코드 수정과 유지보수가 수월해진다.

인터페이스 선언

인터페이스는 interface키워드를 사용해 선언한다. implements 키워드를 통해 일반 클래스에서 인터페이스 구현이 가능하다. 인터페이스에는 상수, 추상 메소드 선언이 가능하며 JAVA8부터 디폴트 메소드, 정적 메소드가 추가되었다.

public interface 인터페이스명 {
  //상수
  타입 상수명 = 값;
  //추상 메소드, 인터페이스에서는 public abstract를 생략해도 자동적으로 컴파일과정에서 붙는다.
  타입 메소드명(매개변수);
  //디폴트 메소드, public특성을 갖는다. 따라서 굳이 public을 앞에 안 붙여줘도 된다.
  default 타입 메소드명(매개변수) { }
  //정적 메소드, public특성을 갖는다. 따라서 굳이 public을 앞에 안 붙여줘도 된다.
  static 타입 메소드명(매개변수) { }
}
멤버 설명
상수 인터페이스에서 값을 정해줌. 정한 값을 제공받아 참조, 사용함.
추상 메소드 추상 메소드를 오버라이딩해서 구현
디폴트 메소드 인터페이스에서 메소드를 제공함. 하지만 필요시 수정해서 구현가능.
정적 메소드 인터페이스에서 제공하는 메소드이고 무조건 사용.

다음 예시를 통해서 이를 활용하고 이해해보자.
우리나라는 은행 시스템을 운영하려면 금융결제원에서 만든 Bank라는 인터페이스 가이드에 맞게 시스템을 구현해야 한다. 이 Bank 인터페이스는 다음과 같다.

Bank interface

public interface Bank {

    //상수 (고객에게 인출해 줄 수 있는 최대 금액)
    public int MAX_INTEGER = 10000000;

    //추상메소드(인출하는 메소드)
    void withDraw(int price);

    //추상메소드(입금하는 메소드)
    void deposit(int price);

    //defualt 메소드(고객의 휴면계좌 찾아주는 메소드 : 재구현은 선택사항)
    default String findDormancyAccount(String custId){
        System.out.println("**금융개정법안 00이후 고객의 휴면계좌 찾아주기 운동**");
        System.out.println("**금융결제원에서 제공하는 로직**");
        return "00은행 000-000-0000-00";
    }

    //JAVA8에서 가능한 정적 메소드(블록체인 인증을 요청하는 메소드)
    static void BCAuth(String bankName){
        System.out.println(bankName+" 에서 블록체인 인증을 요청합니다.");
        System.out.println("전 금융사 공통 블록체인 로직 수행");
    }
}

위 코드를 통해 알 수 있는 것은

  • 고객에게 인출해줄 수 있는 금액이 상수로써 결정되어있다.
  • 인출, 입금 메소드는 회사 측에서 오버라이딩해 구현해야 한다.
  • 휴면계좌를 찾아주는 메소드는 제공한 그대로 사용해도 되고 재구현해 사용해도 된다.
  • 블록체인 인증 요청 메소드는 무조건 제공한 대로 사용해야 한다.

이를 통해 A은행은 Bank 인터페이스를 참고해 다음과 같은 시스템을 구축했다.

public class ABank implements Bank{//implements Bank는 Bank 인터페이스를 기반으로 만들었는 것이다.

    @Override
    public void withDraw(int price) {
        System.out.print("A은행만의 인출 로직...");
        if(price < Bank.MAX_INTEGER){
            System.out.println(price+" 원을 인출한다.");  
        }else{
            System.out.println(price+" 원을 인출실패.");  
        }
    }

    @Override
    public void deposit(int price) {
        System.out.println("A은행만의 입금 로직..."+price+" 원을 입금한다.");

    }

}

A은행은 디폴트 메소드를 재정의하지 않음으로써 Bank 인터페이스에서 제공한 휴면 계좌 찾는 메소드를 그대로 사용하거나 사용하지 않는다는 것이다.

B은행은 다음과 같은 시스템을 구축했다.

public class BBank implements Bank{

    @Override
    public void withDraw(int price) {
        System.out.println("B은행만의 인출 로직...");
        if(price < Bank.MAX_INTEGER){
            System.out.println(price+" 원을 인출한다.");  
        }else{
            System.out.println(price+" 원을 인출실패.");
        }
    }

    @Override
    public void deposit(int price) {
        System.out.println("B은행만의 입금 로직..."+price+" 원을 입금한다.");
        System.out.println("B은행은 별도의 후행처리 작업을 따로 한다.");

    }

    //JAVA8에서 가능한 defualt 메소드(고객의 휴면계좌 찾아주는 메소드)
    @Override
    public String findDormancyAccount(String custId){
        System.out.println("**금융개정법안 00이후 고객의 휴면계좌 찾아주기 운동**");
        System.out.println("*B은행만의 로직 적용*");
        return "00은행 000-000-0000-00";
    }

}

B은행은 휴면 계좌를 찾아주는 메소드를 재구현해서 사용하고 있다.

이번에는 C은행의 시스템을 보자

public class CBank{

    public void CWithDraw(int price) {
        System.out.print("C은행만의 인출 로직...");
        System.out.println(price+" 원을 인출한다.");  
    }

    public void CDeposit(int price) {
        System.out.println("C은행만의 입금 로직..."+price+" 원을 입금한다.");
    }

    public void CFindDormancyAccount(){
        System.out.println("C은행만의 휴면계좌 찾아주기 로직");
    }

}

C은행의 경우 Bank 인터페이스를 무시하고 implements를 하지 않은 채로 자사만의 메소드를 구현했다. 이 경우 금융결제원의 서비스를 사용할 수 없게 되고 호환성이 사라지는 등 불이익이 발생하게 된다.

이렇게 인터페이스를 구축해놓으면 공통의 규격이 생겨 다른 회사나 학교 등 큰 규모의 금융 업무를 처리할 때 갑자기 주관 은행사가 바뀌더라도 인스턴스만 손을 봐줘서 단시간 내에 재기능을 수행할 수 있는 다형성이 생긴다.

그렇다면 이제 Bank 인터페이스를 바탕으로 만들어진 ABank()와 같은 클래스를 사용해보자. 이 때 주의할 사항은 다음과 같이 인터페이스를 사용하는 것이 아니다.

ABank aBank = new ABank(); (x)

인터페이스로 구현 객체를 사용하려면 다음과같이 인터페이스 변수를 선언하고 구현 객체를 대입해야 한다.

Bank bank = new ABank(); 

그럼 이제 예제를 참고해보자.

public class Main {

    public static void main(String[] args) {

        Bank bank = new ABank();
        bank.withDraw(100);
        bank.deposit(100);
        bank.findDormancyAccount("763231");
        Bank.BCAuth("ABank");
        //bank를 손쉽게 B은행으로 바꾸고 인스턴스를 수정한다.
        bank = new BBank();
        bank.withDraw(100);
        bank.deposit(100);
        bank.findDormancyAccount("4311");
        Bank.BCAuth("BBank");
    }
 }

인터페이스는 이와 같이 인터페이스를 바탕으로 만들어진 클래스에 대해 동일한 동작을 수행하게 된다. JAVA8부터는 디폴트 메소드가 생기면서 유연성 또한 확보되었다.

인터페이스의 상속

인터페이스도 다른 인터페이스를 상속할 수 있다. 인터페이스는 클래스와 달리 다중 상속을 허용한다. 다음과 같이 extends 키워드 뒤에 상속할 인터페이스들을 나열할 수 있다.

public interface 하위인터페이스 extends 상위인터페이스1, 상위인터페이스2 { ... }

하위 인터페이스를 구현하는 클래스는 하위 인터페이스의 메소드 뿐만 아니라 상위 인터페이스들의 모든 메소드에 대해 실체 메소드를 가지고 있어야 된다. 이 덕분에 하위 인터페이스로 구현된 구현 클래스는 상위 및 하위 인터페이스 타입으로 모두 변환 가능하다.

하위인터페이스 변수 = new 구현클래스(...);
상위인터페이스1 변수 = new 구현클래스(...);
상위인터페이스2 변수 = new 구현클래스(...);

상위 인터페이스로 타입 변환된 구현 클래스는 해당하는 상위 인터페이스에 존재하는 메소드만 사용 가능하다. 상, 하위 인터페이스에 있는 모든 메소드를 이용하려면 하위 인터페이스로 타입 변환을 하면 된다.
다음 예시는 부모 인터페이스인 InterA, InterB와 이 두 인터페이스를 상속받은 InterC가 있고 InterC 인터페이스의 구현 클래스인 ImpleEx를 활용해 Main.java를 실행한 것이다.

InterA.java

public interface InterA {
    public void methodA();//추상 메소드
}

InterB.java

public interface InterB {
    public void methodB();//추상 메소드
}

InterC.java

public interface InterC extends InterA, InterB {//InterA와 InterB 인터페이스를 상속받음
    public void methodC();//추상 클래스
}

ImpleEx.java

public class ImpleEx implements InterC {//InterC인터페이스를 구현한 ImpleEx
    //InterC가 InterA와 InterB를 상속받았기 때문에 세 추상 메소드 모두 작성해야함.
    public void methodA() {
        System.out.println("methodA실행");
    }
    public void methodB() {
        System.out.println("methodB실행");
    }
    public void methodC() {
        System.out.println("methodC실행");
    }
}

Main.java

public class Main {
    public static void main(String[] args) {
        ImpleEx impleEx = new ImpleEx();

        InterA interA = impleEx;//InterA로 타입 변환되어 methodA메소드만 사용 가능
        interA.methodA();
        InterB interB = impleEx;//InterB로 타입 변환되어 methodB메소드만 사용 가능
        interB.methodB();
        InterC interC = impleEx;//InterC로 타입 변환됨. InterC는 A, B, C메소드 모두 사용 가능
        interA.methodA();
        interB.methodB();
        interC.methodC();
    }
}

Main 실행 결과

methodA실행
methodB실행
methodA실행
methodB실행
methodC실행

출처 : 이 글은 여기에서 상당량 참고하였습니다.