JAVA Basic) 인터페이스 활용
Programming/Java 기초

JAVA Basic) 인터페이스 활용

반응형

 

목차

     


     

    <한 클래스가 여러 인터페이스를 구현하는 경우>

    한 클래스가 여러 클래스를 상속받으면 메서드 호출이 모호해지는 문제가 발생할 수 있다.
    하지만 인터페이스는 한 클래스가 여러 인터페이스를 구현할 수 있다.

    package interfaceex;
    
    public interface Buy {
    	void buy();
    }
    package interfaceex;
    
    public interface Sell {
    	void sell();
    }

    그림을 보면 Customer클래스는 Buy와 Sell 두 인터페이스를 구현하고 있다.

    • Buy인터페이스에 추상 메서드 buy()가 선언.
    • Sell인터페이스에 추상메서드 sell()이 선언.
    package interfaceex;
    
    public class Customer implements Buy, Sell{
    	// Customer클래스는 Buy와 Sell 인터페이스 둘 다 구현
    	@Override
    	public void buy() {
    		System.out.println("구매하기.");
    	}
    	
    	@Override
    	public void sell() {
    		System.out.println("판매하기.");
    	}
    }

    인터페이스는 구현 코드나 멤버 변수를 가지지 않아서 여러개를 동시에 구현할 수 있다.
    두 인터페이스에 이름이 같은 메서드가 선언되었다고 해도 구현은 클래스에서 이루어지므로, 어떤 메서드를 호출해야 하는지 모호하지 않은 것.

    두 인터페이스를 구현한 Customer클래스는 Buy형이자 Sell형인 것.

    package interfaceex;
    
    public class CustomerTest {
    	public static void main(String[] args) {
    		Customer customer = new Customer();
    		
    		Buy buyer = customer;
    		buyer.buy();
    		// Customer형인 customer를 Buy형인 buyer로 형 변환
    		
    		Sell seller = customer;
    		seller.sell();
    		// Customer형인 customer를 Sell형인 seller로 형 변환
    	
    		if(seller instanceof Customer) {
    			Customer customer2 = (Customer)seller;	//seller를 Customer형으로 다시 형변환
    			customer2.buy();
    			customer2.sell();
    			
    			System.out.println(customer);
    			System.out.println(buyer);
    			System.out.println(seller);
    			System.out.println(customer2);
    		}	//모두 같은 인스턴스 주소 값이다.
    	}
    }
    • Buy buyer = customer;처럼 customer를 Buy 인터페이스형 변수에 대입하면 형 변환이 일어나 Buy인터페이스에 선언한 메서드만 호출할 수 있다.
    • 또한 상속 관계에서와 마찬가지로 원래의 인스턴스 자료형으로 다운 캐스팅하기 위해서는 instanceof를 사용해 본래 인스턴스 자료형으로 안전하게 변환이 가능하다. 

     

    <두 인터페이스의 디폴트 메서드가 중복되는 경우>

    정적 메서드는 인스턴스 생성과 상관없이 사용할 수 있다. Customer클래스가 Buy, Sell 인터페이스를 구현하고 두 인터페이스에 pay() 정적 메서드가 있다고 하자.
    이 경우 Buy.pay()와 Sell.pay()로 인터페이스를 특정하여 호출할 수 있기 때문에 문제가 되지 않는다.

    그런데 '디폴트 메서드'는 어떻게 될까?
    '디폴트 메서드'인스턴스를 생성해야 호출할 수 있는 메서드이기 때문에 문제가 된다.

    package interfaceex;
    
    public interface Buy {
    	void buy();
    	
    	default void order() {
    		System.out.println("구매 주문");
    	}
    }
    package interfaceex;
    
    public interface Sell {
    	void sell();
    	
    	default void order() {
    		System.out.println("판매 주문");
    	}
    }

    Buy와 Sell인터페이스 모두 order() 디폴트 메서드를 가지고 있다.
    이 상태에서 두 인터페이스를 모두 구현하면 Customer클래스는 오류 메시지를 보여준다. (디폴트 메서드가 중복되니 두 인터페이스를 구현하는 Customer클래스에서 재정의를 하라는 뜻)

    -Customer클래스_디폴트 메서드를 재정의 

    package interfaceex;
    
    public class Customer implements Buy, Sell {
    	...
        
    	@Override
    	public void order() {
    		System.out.println("고객 구매 주문");
    	} //디폴트 메서드 order()을 Customer클래스에서 재정의
    }

     

    Customer클래스를 생성하여 order()메서드를 호출하면 재정의한 order()가 호출된다.
    - CustomerTest클래스

    package interfaceex;
    
    public class CustomerTest {
    	public static void main(String[] args) {
    		Customer customer = new Customer();
    		
    		Buy buyer = customer;
    		buyer.buy();
    		buyer.order(); //재정의된 메서드 호출
    		// Customer형인 customer를 Buy형인 buyer로 형 변환
    		
    		Sell seller = customer;
    		seller.sell();
    		seller.order(); //재정의된 메서드 호출
    		// Customer형인 customer를 Sell형인 seller로 형 변환
    	
    		if(seller instanceof Customer) {
    			Customer customer2 = (Customer)seller;	//seller를 Customer형으로 다시 형변환
    			customer2.buy();
    			customer2.sell();
    		}
    		customer.order(); //재정의된 메서드 호출
    	}
    }

    • 여기서 주의해야할 점은, customer가 Buy형으로 변환되고 buyer.order()를 호출하면 Buy에서 구현한 디폴트 메서드가 아닌 Customer에서 재정의한 메서드가 호출된다는 점.

    가상 메서드 원리와 동일하다.

     

    <인터페이스 간 상속>

    인터페이스 간에도 상속이 가능하다.
    인터페이스 간 상속은 구현 코드를 통해 기능을 상속하는 것이 아니므로 '형 상속(type inheritance)'이라고 부른다.

    클래스의 경우에는 하나의 클래스만 상속받을 수 있지만, 인터페이스는 여러 개를 동시에 상속받을 수 있다.
    한 인터페이스가 여러 인터페이스를 상속받으면, 상속받은 인터페이스는 상위 인터페이스에 선언한 추상 메서드를 모두 가지게 된다.

    MyInterface인터페이스는 X와 Y인터페이스를 상속받고,
    MyClass클래스는 MyInterface인터페이스를 구현한다.

    MyInterface인터페이스는 두 인터페이스를 상속받고 자신이 추상메서드를 1개 가지고 있으므로, 상속받은 후 추상 메서드를 총 3개 가지게 된다.
    따라서 MyClass 클래스가 구현해야 할 추상 메서드는 총 3개.

     

    코드를 살펴보면 다음과 같다.

    package interfaceex;
    
    public interface X {
    	void x();
    }
    package interfaceex;
    
    public interface Y {
    	void y();
    }

    그리고 또 다른 인터페이스인 MyInterface가 X와 Y인터페이스를 상속받는 방법클래스와 마찬가지로 extends예약어를 사용한다.

    package interfaceex;
    
    public interface MyInterface extends X, Y  {
    	// 인터페이스 여러개를 상속받은 MyInterface인터페이스
    	void myMethod();
    }


    MyInterface 인터페이스에는 myMethod()메서드만 선언되어 있지만, X와 Y인터페이스를 상속받았으므로 MyClass클래스에는 x(), y()메서드까지 구현해야 한다.

    package interfaceex;
    
    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()");
    	}
    }


    MyClass클래스를 실행하는 테스트 프로그램은 다음과 같다.

    package interfaceex;
    
    public class MyClassTest {
    	public static void main(String[] args) {
    		MyClass mClass = new MyClass();
    
    		X xClass = mClass;
    		xClass.x();	
    		// 상위 인터페이스 X형으로 대입하면 X에 선언한 메서드만 호출 가능.
    		
    		Y yClass = mClass;
    		yClass.y(); 
    		// 상위 인터페이스 Y형으로 대입하면 Y에 선언한 메서드만 호출 가능.
    		
    		MyInterface iClass = mClass;
    		iClass.myMethod();
    		iClass.x();
    		iClass.y();
    		// 구현한 인터페이스형 변수에 대입하면 인터페이스가 상속한 모든 메서드 호출 가능.
    	}
    }

    생성한 클래스는 상위 인터페이스형으로 변환할 수 있다. 다만, 상위 인터페이스 형으로 변환을 하면 상위 인터페이스에 선언한 메서드만 호출할 수 있다.
    mClass가 MyClass로 생성되었어도 (MyClass mClass = new MyClass();)
    X형 xClass에 대입되면 (X xClass = mClass;)
    호출할 수 있는 메서드는 X의 메서드 x()뿐이다.
    이처럼 인터페이스를 정의할 때 기능상 계층 구조가 필요한 경우에 상속을 사용하기도 한다.

    <인터페이스 구현과 클래스 상속 함께 쓰기>

    한 클래스에서 클래스 '상속'과 인터페이스 '구현'을 모두 할 수도 있다.

    다음은 Queue인터페이스를 구현하고 shelf클래스를 상속받는 BookShelf클래스를 나타낸 그림이다

    BookShelf(책장)클래스는 책을 넣은 순서대로 꺼내어 볼 수 있도록 만들려고 한다.
    BookShelf클래스를 구현하기 전에 더 큰 개념인 Shelf(선반)클래스를 먼저 만들어보자.

     

    package bookshelf;
    import java.util.ArrayList;
    
    public class Shelf {
    	protected ArrayList<String> shelf;
    	//자료를 순서대로 저장할 ArrayList 선언.
    	
    	public Shelf() {
    		shelf = new ArrayList<String>();
    	} //디폴트 생성자로 Shelf클래스를 생성하면 ArrayList가 생성
    	
    	public ArrayList<String> getShelf(){
    		return shelf;
    	}
    	
    	public int getCount() {
    		return shelf.size();
    	}
    }
    • Shelf클래스에는 자료를 순서대로 저장할 배열 객체를 선언했다. (protected ArrayList<String> shelf;)
    • 이름을 저장할 수 있도록 자료형은 String을 사용.
    • getShelf()메서드는 저장되어 있는 배열 shelf를 반환하고
    • getCount()메서드는 배열 shelf에 저장된 요소 개수를 반환한다.
    • BookShelf클래스는 Shelf클래스를 상속받아 사용하는 하위 클래스다.

    그럼 Queue인터페이스를 정의해보자.
    Queue인터페이스는 먼저 들어온 자료를 먼저 꺼내는 기능을 정의한다.

    package bookshelf;
    
    public interface Queue {
    	void enQueue(String title); //배열의 맨 마지막에 추가
    	String deQueue(); //배열의 맨 처음 항목 반환
    	int getSize(); //현재 Queue에 있는 개수 반환
    }
    • enQueue()멤서드는 입력되는 요소 값을 배열의 맨 뒤에 추가한다.
    • deQueue()는 배열에서 맨 앞에 있는 요소를 제거하고 그 값을 반환

     

    enQueue()와 deQueue()를 그림으로 표현

     

    이제 Shelf클래스와 Queue인터페이스를 사용하여 BookShelf클래스를 다음 처럼 구현이 가능하다.

    package bookshelf;
    
    public class BookShelf extends Shelf implements Queue {
    	@Override
    	public void enQueue(String title) {
    		shelf.add(title);
    	}//배열에 요소 추가 .add
    
    	@Override
    	public String deQueue() {
    		return shelf.remove(0);
    	}//배열에서 첫 번째 요소 제거 하고 반환 .remove(0)
    
    	@Override
    	public int getSize() {
    		return getCount();
    	}//배열 요소 개수 반환
    }
    • BookShelf클래스는 Shelf클래스를 상속받고 Queue인터페이스를 구현한다.
    • Shelf클래스가 가지고 있는 ArrayList배열을 사용하여 Queue인터페이스를 선언한 메서드를 모두 구현한다.

    이제 테스트 프로그램을 만들어보자.

    package bookshelf;
    
    public class BookShelfTest {
    	public static void main(String[] args) {
    		Queue shelfQueue = new BookShelf();
    		shelfQueue.enQueue("책1");
    		shelfQueue.enQueue("책2");
    		shelfQueue.enQueue("책3");
    		shelfQueue.enQueue("책4");
    		shelfQueue.enQueue("책5");
    		shelfQueue.enQueue("책6");
    
    		System.out.println("책장에는 책이 " + shelfQueue.getSize()+ "권 있습니다");
    		
    		
    		System.out.println("맨 앞의 책 <"+ shelfQueue.deQueue() + ">"
    				+ "을 뺍니다.");
    		
    		System.out.println("책장에는 책이 " + shelfQueue.getSize()+ "권 있습니다");
    		
    		System.out.println("========책을 맨 앞부터 빼기 시작합니다.=========");
    		System.out.println(shelfQueue.deQueue());
    		System.out.println(shelfQueue.deQueue());
    		System.out.println(shelfQueue.deQueue());
    		System.out.println(shelfQueue.deQueue());
    		System.out.println(shelfQueue.deQueue());
    	}
    }
    • deQueue()메서드를 사용하면 입력 순서대로 값이 출력되는 것을 볼 수 있다.

    앞으로 자바를 쓰면서 이미 제공되고 있는 클래스나 인터페이스를 사용한 프로그램을 자주 접하게 될 것이다.
    특히 실무에서는 프레임워크나 기존 소스 코드를 사용해 개발하는 경우가 많다.

    실무에서 인터페이스를 사용하는 경우

    '인터페이스'클래스가 제공할 기능을 선언하고 설계하는 것.

    만일 여러 클래스가 같은 메서드를 서로 다르게 구현한다면 어떻게 해야 할까?
    우선 인터페이스에 메서드를 선언한 다음, 인터페이스를 구현한 각 클래스에서 같은 메서드에 대해 다양한 기능을 구현하면 된다.

    이것이 바로 '인터페이스를 이용한 다형성의 구현'이다.

    한 가지 경우를 생각해보자.
    어느 회사에서 시스템을 개발했다. 이 시스템은 자료를 저장하기 위해 데이터베이스를 사용한다.
    처음에는 MySQL 데이터베이스를 사용했는데, 이 시스템을 다른 회사에 가서 설치하려고 하니 오라클 데이터베이스를 사용하여 설치해달라고 요구한다. 또 다른 회사는 MS-SQL을 사용한다고 한다.

    프로그램은 하나 뿐인데 사용하는 데이터베이스가 제각각인 것이다.
    이 프로그램의 웹 페이지나 모바일 페이지는 데이터베이스와 관계없이 수행된다.


    데이터베이스와 연관되는 코드는 프로그램의 특정 부분인 것이다. 이런 경우에 데이터베이스 기능을 수행할 인터페이스를 정의한다. 그리고 인터페이스 정의에 맞게 여러 데이터베이스 관련 모듈을 개발하면 된다.

    사용자 정보 처리 모듈

    • 사용자 정보를 데이터베이스에 입력하거나 업데이트하거나 삭제하는 기능을 UserInfoDao 인터페이스에 정의한다.
    • 그리고 여러 인터페이스에 맞게 구현하는 것을 각 클래스가 담당.
    • 웹 페이지나 그 밖의 다른 클래스에서 이 기능이 필요하면 UserInfoDao인터페이스를 구현하여 사용할 수 있다.

    인터페이스를 잘 정의하는 것이 확장성 있는 프로그램을 만드는 시작인 셈이다.

    반응형