Java / / 2024. 7. 9. 17:22

자바 인터페이스의 확장: 다재다능한 API 설계를 위한 디폴트 및 정적 메서드 활용

🛠️ 마커 인터페이스의 역할

 

마커 인터페이스는 특별한 메서드나 속성을 가지지 않고, 단지 특정 클래스가 어떤 특정 기능을 수행할 수 있음을 나타내기 위해 사용되는 인터페이스입니다. 자바 프로그래밍 언어에서 주로 사용되며, 다른 객체지향 프로그래밍 언어에서도 비슷한 개념이 존재할 수 있습니다. 🎯

 

🛠️ 마커 인터페이스의 역할

 

마커 인터페이스의 주요 역할은 런타임에서 특정 클래스가 특정 속성을 가지고 있음을 나타내는 것입니다. 이를 통해 JVM이나 프레임워크가 해당 클래스에 대해 특별한 처리를 할 수 있습니다.

 

🌟 대표적인 마커 인터페이스

 

1. Serializable 📦

이 인터페이스를 구현한 클래스는 객체를 직렬화할 수 있습니다. 직렬화는 객체의 상태를 바이트 스트림으로 변환하여 파일 저장이나 네트워크 전송을 가능하게 합니다.

2. Cloneable 🔄

이 인터페이스를 구현한 클래스는 객체를 복제할 수 있습니다. Object 클래스의 clone() 메서드를 호출하여 객체의 복제본을 생성할 수 있습니다.

3. Remote 🌐

이 인터페이스를 구현한 클래스는 원격 메서드 호출을 지원합니다. RMI(Remote Method Invocation)에서 사용됩니다.

 

참고: 새로운 인터페이스를 정의하면 새로운 참조 데이터 타입이 정의됩니다. 🆕

 

🧩 함수 호출 방식: Call by Value vs Call by Reference

 

함수 호출 방식에서 “콜 바이 밸류(Call by Value)“와 “콜 바이 레퍼런스(Call by Reference)“는 중요한 개념입니다. 이 두 가지 방식은 함수가 호출될 때 인자(매개변수)가 함수 내부로 어떻게 전달되는지를 정의합니다.

 

1. 콜 바이 밸류 (Call by Value) 📤

함수가 호출될 때 인자의 값을 함수로 복사하여 전달하는 방식입니다. 함수 내부에서 인자의 값이 변경되더라도 원래의 값은 변경되지 않습니다.

2. 콜 바이 레퍼런스 (Call by Reference) 📥

함수가 호출될 때 인자의 참조를 전달하여 함수 내부에서 원래 값이 변경될 수 있도록 하는 방식입니다.

 

📚 Class Methods & Static Methods

 

자바 프로그래밍 언어는 정적 변수뿐만 아니라 정적 메서드도 지원합니다. 선언에 static 수정자가 있는 정적 메서드는 클래스의 인스턴스를 생성할 필요 없이 클래스 이름으로 호출해야 합니다.

 

예를 들어:

ClassName.methodName(args);

 

또한, 정적 메서드는 객체 참조로도 호출할 수 있습니다:

instanceName.methodName(args);

 

예시:

public class Main {
    public static void main(String[] args) {
        // GroupedClass 인스턴스 생성
        GroupedClass gr1 = new GroupedClass();
        GroupedClass gr2 = new GroupedClass();
        GroupedClass gr3 = new GroupedClass();
        
        // 인터페이스로 변수 선언 (다형성)
        GroupedInterface gr1Interface = gr1;
        GroupedInterface gr2Interface = gr2;
        GroupedInterface gr3Interface = gr3;
        
        // 각 인스턴스의 메서드 호출
        gr1.groupedMethod(); // Implementing groupedMethod
        gr2.groupedMethod(); // Implementing groupedMethod
        gr3.groupedMethod(); // Implementing groupedMethod
    }
}

 

🔍 코드에서 마커 인터페이스와 인터페이스 사용 예시

 

Relatable 인터페이스를 구현한 클래스의 인스턴스를 비교하는 방법은 다음과 같습니다:

public static Object findLargest(Object object1, Object object2) {
    Relatable obj1 = (Relatable)object1;
    Relatable obj2 = (Relatable)object2;

    if (obj1.isLargerThan(obj2) > 0) {
        return object1;
    } else {
        return object2;
    }
}

 

이 메서드는 두 객체를 비교하여 더 큰 객체를 반환합니다. 🎯

 

📏 인터페이스 사용 예시: 자동차 제어 시스템

 

소프트웨어 엔지니어링에서는 서로 다른 프로그래머 그룹이 소프트웨어가 상호작용하는 방식을 설명하는 “계약(contract)“에 동의하는 것이 중요합니다. 각 그룹은 다른 그룹의 코드가 어떻게 작성되는지 전혀 알지 못해도 자신의 코드를 작성할 수 있어야 합니다.

 

예를 들어, 컴퓨터로 제어되는 로봇 자동차가 인간 운전자 없이 도시 거리를 통해 승객을 수송하는 미래 사회를 상상해 보십시오. 🚗💨

 

자동차 제조업체는 정지, 출발, 가속, 좌회전 등 자동차를 작동하는 소프트웨어를 작성합니다.

전자 유도 장치 제조업체는 GPS 위치 데이터와 교통 상황의 무선 전송을 수신하고 해당 정보를 사용하여 자동차를 운전하는 컴퓨터 시스템을 만듭니다.

 

자동차 제조업체는 자동차를 움직이기 위해 호출할 수 있는 메서드를 설명하는 업계 표준 인터페이스를 게시해야 합니다. 그런 다음, 안내 시스템 제조업체는 인터페이스에 설명된 메서드를 호출하여 자동차에 명령을 내리는 소프트웨어를 작성할 수 있습니다. 두 산업 그룹 모두 다른 그룹의 소프트웨어가 어떻게 구현되는지 알 필요가 없습니다. 🎯

 

💡 Interfaces in Java

 

자바 프로그래밍 언어에서 인터페이스는 클래스와 유사한 참조 타입으로, 상수, 메서드 시그니처, 디폴트 메서드, 정적 메서드, 중첩 타입만을 포함할 수 있습니다. 인터페이스는 인스턴스화할 수 없으며, 클래스에 의해 구현되거나 다른 인터페이스에 의해 확장될 수만 있습니다.

 

인터페이스 예시:

public interface OperateCar {
   int turn(Direction direction, double radius, double startSpeed, double endSpeed);
   int changeLanes(Direction direction, double startSpeed, double endSpeed);
   int signalTurn(Direction direction, boolean signalOn);
   int getRadarFront(double distanceToCar, double speedOfCar);
   int getRadarRear(double distanceToCar, double speedOfCar);
}

 

인터페이스를 사용하려면, 해당 인터페이스를 구현하는 클래스를 작성해야 합니다. 클래스가 인터페이스를 구현하면, 인터페이스에 선언된 각 메서드에 대한 메서드 본문을 제공합니다.

 

클래스 예시:

public class OperateBMW760i implements OperateCar {

    public int signalTurn(Direction direction, boolean signalOn) {
        // BMW의 방향 지시등을 켜거나 끄는 코드
    }

    // 필요에 따라 다른 멤버 추가
}

 

인터페이스를 사용하려면, 해당 인터페이스를 구현하는 클래스를 작성해야 합니다. 클래스가 인터페이스를 구현하면, 인터페이스에 선언된 각 메서드에 대한 메서드 본문을 제공합니다.

 

클래스 예시:

public class OperateBMW760i implements OperateCar {

    public int signalTurn(Direction direction, boolean signalOn) {
        // BMW의 방향 지시등을 켜거나 끄는 코드
    }

    // 필요에 따라 다른 멤버 추가
}

 

🚀 Interfaces as APIs

 

로봇 자동차의 예는 업계 표준 API(응용 프로그래밍 인터페이스)로 사용되는 인터페이스를 보여줍니다. API는 상용 소프트웨어 제품에서도 흔히 사용됩니다.

 

예를 들어, 디지털 이미지 처리 방법 패키지를 제공하는 회사는 고객에게 공개되는 인터페이스를 구현하기 위해 클래스를 작성합니다. 그런 다음 그래픽 프로그램을 만드는 회사는 인터페이스에 정의된 서명 및 반환 유형을 사용하여 이미지 처리 방법을 호출합니다. 이미지 처리 회사의 API는 (고객에게) 공개되지만 API 구현은 극비로 유지됩니다. 🔒

 

🌟 인터페이스 정의하기 (Defining an Interface)

 

인터페이스는 다음과 같이 선언됩니다:

 

액세스 수정자: public 또는 package-private

interface 키워드: interface

인터페이스 이름

상위 인터페이스 목록: extends 키워드 뒤에 쉼표로 구분

인터페이스 본문: { }

 

예시:

public interface GroupedInterface extends Interface1, Interface2, Interface3 {

    // 상수 선언
    double E = 2.718282; // 자연 로그의 밑수 🌱
 
    // 메서드 시그니처
    void doSomething(int i, double x);
    int doSomethingElse(String s);
}

 

public 액세스 지정자: 모든 패키지의 모든 클래스에서 인터페이스를 사용할 수 있습니다. 🌐

package-private: 동일한 패키지 내의 클래스에서만 사용 가능합니다. 🔒

 

🔄 인터페이스 확장 (Extending Interfaces)

 

인터페이스는 다른 인터페이스나 클래스를 확장할 수 있습니다.

 

단일 클래스 확장: 클래스는 오직 하나의 다른 클래스를 확장할 수 있습니다.

다중 인터페이스 확장: 인터페이스는 여러 인터페이스를 확장할 수 있습니다.

interface PackagePrivateInterface {
    void someMethod();
}

 

📝 인터페이스 본문 (The Interface Body)

 

인터페이스 본문에는 다음이 포함될 수 있습니다:

 

abstract 메서드: 구현 없이 선언된 메서드

default 메서드: default 키워드로 정의

static 메서드: static 키워드로 정의

 

예시:

public interface ExampleInterface {

    // abstract 메서드 (구현 없음)
    void abstractMethod();
    
    // default 메서드 (구현 포함)
    default void defaultMethod() {
        System.out.println("This is a default method");
    }

    // static 메서드 (구현 포함)
    static void staticMethod() {
        System.out.println("This is a static method");
    }
}

 

상수 선언: 인터페이스에 정의된 모든 상수는 암시적으로 public, static, final입니다.

 

🚀 인터페이스 구현하기 (Implementing an Interface)

 

인터페이스를 구현하려면, 클래스 선언에 implements 키워드를 포함합니다.

public class ExampleClass implements ExampleInterface {

    @Override
    public void abstractMethod() {
        System.out.println("Implementing abstract method");
    }
}

 

implements 절: 클래스가 구현하는 인터페이스를 쉼표로 구분하여 나열합니다.

 

📏 예제 인터페이스: Relatable

public interface Relatable {
    
    // this와 other는 동일한 클래스의 인스턴스여야 함
    // 1, 0, -1을 반환 (this가 other보다 크면 1, 작으면 -1, 같으면 0)
    public int isLargerThan(Relatable other);
}

 

Relatable 인터페이스 구현 예시: RectanglePlus

public class RectanglePlus implements Relatable {
    public int width = 0;
    public int height = 0;
    public Point origin;

    // 네 가지 생성자
    public RectanglePlus() {
        origin = new Point(0, 0);
    }
    public RectanglePlus(Point p) {
        origin = p;
    }
    public RectanglePlus(int w, int h) {
        origin = new Point(0, 0);
        width = w;
        height = h;
    }
    public RectanglePlus(Point p, int w, int h) {
        origin = p;
        width = w;
        height = h;
    }

    // 사각형 이동 메서드
    public void move(int x, int y) {
        origin.x = x;
        origin.y = y;
    }

    // 사각형 면적 계산 메서드
    public int getArea() {
        return width * height;
    }
    
    // Relatable 인터페이스의 메서드 구현
    @Override
    public int isLargerThan(Relatable other) {
        RectanglePlus otherRect = (RectanglePlus) other;
        return Integer.compare(this.getArea(), otherRect.getArea());
    }
}

 

RectanglePlus 클래스는 Relatable 인터페이스를 구현하므로, 두 RectanglePlus 객체의 크기를 비교할 수 있습니다. 📏

 

🧩 타입으로서의 인터페이스 사용 (Using an Interface as a Type)

 

새로운 인터페이스를 정의하면 새로운 참조 데이터 타입이 정의됩니다.

 

예를 들어, Relatable을 구현하는 클래스의 객체들을 비교하려면 다음과 같은 메서드를 사용할 수 있습니다:

public Object findLargest(Object object1, Object object2) {
    Relatable obj1 = (Relatable) object1;
    Relatable obj2 = (Relatable) object2;
    return obj1.isLargerThan(obj2) > 0 ? object1 : object2;
}

 

다양한 클래스에서 Relatable을 구현하면, 해당 클래스의 객체들을 findLargest() 메서드로 비교할 수 있습니다. 이때, 두 객체는 동일한 클래스에 속해야 합니다.

 

🛠️ 인터페이스 진화 (Evolving Interfaces)

 

인터페이스를 수정하거나 확장할 수 있습니다.

 

새로운 메서드를 추가:

public interface DoItPlus extends DoIt {
   boolean didItWork(int i, double x, String s);
}

 

default 메서드 추가:

public interface DoIt {
   void doSomething(int i, double x);
   int doSomethingElse(String s);
   
   default boolean didItWork(int i, double x, String s) {
       // 메서드 구현
   }
}

 

기존 인터페이스에 새로운 메서드를 추가할 때, default 메서드나 static 메서드를 정의하면 기존 코드를 중단하지 않으면서도 새로운 기능을 제공할 수 있습니다. 🆕

 

🚗 Default Methods와 인터페이스 확장 🛠️

 

인터페이스 섹션에서는 자바의 Default Methods와 관련된 개념을 설명합니다. 기본 메서드는 라이브러리 인터페이스에 새 기능을 추가하면서도 기존 코드와의 호환성을 유지할 수 있는 강력한 도구입니다. 🚀

 

🚘 컴퓨터 제어 자동차와 인터페이스

 

자동차 제조업체가 컴퓨터 제어 자동차를 만들 때, 자동차에 새로운 기능(예: 비행)을 추가하려면 기존 소프트웨어와의 호환성을 유지하면서 새로운 메서드를 정의해야 합니다. 하지만 이 새로운 메서드를 어디에 선언할까요?

 

정적 메서드로 추가하면, 프로그래머가 이를 필수적인 메서드로 간주하지 않을 수 있습니다.

Default Methods를 사용하면 기존 인터페이스에 새로운 기능을 추가하면서도 이전 인터페이스를 구현한 클래스를 깨뜨리지 않고 기능을 확장할 수 있습니다. 🔧

 

🕒 TimeClient 인터페이스와 Default Methods

import java.time.*;

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                        int hour, int minute, int second);
    LocalDateTime getLocalDateTime();
}

 

이 인터페이스를 구현하는 SimpleTimeClient 클래스는 시간과 날짜를 설정하고 출력하는 기능을 제공합니다. 🌟

package defaultmethods;

import java.time.*;

public class SimpleTimeClient implements TimeClient {
    
    private LocalDateTime dateAndTime;
    
    public SimpleTimeClient() {
        dateAndTime = LocalDateTime.now();
    }
    
    public void setTime(int hour, int minute, int second) {
        LocalDate currentDate = LocalDate.from(dateAndTime);
        LocalTime timeToSet = LocalTime.of(hour, minute, second);
        dateAndTime = LocalDateTime.of(currentDate, timeToSet);
    }
    
    public void setDate(int day, int month, int year) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime currentTime = LocalTime.from(dateAndTime);
        dateAndTime = LocalDateTime.of(dateToSet, currentTime);
    }
    
    public void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second) {
        LocalDate dateToSet = LocalDate.of(day, month, year);
        LocalTime timeToSet = LocalTime.of(hour, minute, second); 
        dateAndTime = LocalDateTime.of(dateToSet, timeToSet);
    }
    
    public LocalDateTime getLocalDateTime() {
        return dateAndTime;
    }
    
    public String toString() {
        return dateAndTime.toString();
    }
    
    public static void main(String... args) {
        TimeClient myTimeClient = new SimpleTimeClient();
        System.out.println(myTimeClient.toString());
    }
}

 

🗺️ ZonedDateTime 기능 추가하기

 

TimeClient 인터페이스에 표준 시간대(ZonedDateTime) 기능을 추가해 보겠습니다. 기본 메서드를 사용하여 이 기능을 추가하면, 기존 클래스를 수정하지 않고도 새로운 기능을 구현할 수 있습니다. 🕰️

 

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                        int hour, int minute, int second);
    LocalDateTime getLocalDateTime();                           
    ZonedDateTime getZonedDateTime(String zoneString);
}

public interface TimeClient {
    void setTime(int hour, int minute, int second);
    void setDate(int day, int month, int year);
    void setDateAndTime(int day, int month, int year,
                               int hour, int minute, int second);
    LocalDateTime getLocalDateTime();
    
    static ZoneId getZoneId (String zoneString) {
        try {
            return ZoneId.of(zoneString);
        } catch (DateTimeException e) {
            System.err.println("Invalid time zone: " + zoneString +
                "; using default time zone instead.");
            return ZoneId.systemDefault();
        }
    }
        
    default ZonedDateTime getZonedDateTime(String zoneString) {
        return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
    }
}

 

이 인터페이스를 사용하면, SimpleTimeClient 클래스를 수정하지 않고도 새로운 getZonedDateTime 메서드를 사용할 수 있습니다. 🎉

 

🛠️ Default Methods와 인터페이스 확장

 

인터페이스를 상속하면서 기본 메서드가 포함된 경우, 다음과 같은 선택지가 있습니다:

 

1. Default 메서드를 상속: 아무런 언급 없이 기본 메서드를 그대로 상속합니다.

2. Abstract 메서드로 선언: 기본 메서드를 다시 선언하여 추상 메서드로 만듭니다.

3. Override: 기본 메서드를 재정의하여 새로운 동작을 구현합니다. 🔄

 

예를 들어:

public interface AnotherTimeClient extends TimeClient { }

 

이 인터페이스를 구현하는 모든 클래스는 TimeClient.getZonedDateTime의 기본 구현을 사용하게 됩니다.

 

🧩 인터페이스 확장 예시

 

예를 들어, AbstractZoneTimeClient 인터페이스를 구현하는 모든 클래스는 getZonedDateTime 메서드를 구현해야 합니다. 이 메서드는 다른 모든 추상 메서드와 동일하게 처리됩니다. 📏

public interface AbstractZoneTimeClient extends TimeClient {
    public ZonedDateTime getZonedDateTime(String zoneString);
}

public interface HandleInvalidTimeZoneClient extends TimeClient {
    default public ZonedDateTime getZonedDateTime(String zoneString) {
        try {
            return ZonedDateTime.of(getLocalDateTime(),ZoneId.of(zoneString)); 
        } catch (DateTimeException e) {
            System.err.println("Invalid zone ID: " + zoneString +
                "; using the default time zone instead.");
            return ZonedDateTime.of(getLocalDateTime(),ZoneId.systemDefault());
        }
    }
}

 

🛠️ Static Methods와 인터페이스

 

Default 메서드 외에도, 인터페이스에서 static 메서드를 정의할 수 있습니다. 이러한 메서드는 특정 기능을 쉽게 활용할 수 있도록 도우미 메서드를 제공하는 데 유용합니다. 🧑‍💻

public interface TimeClient {
    static public ZoneId getZoneId (String zoneString) {
        try {
            return ZoneId.of(zoneString);
        } catch (DateTimeException e) {
            System.err.println("Invalid time zone: " + zoneString +
                "; using default time zone instead.");
            return ZoneId.systemDefault();
        }
    }

    default public ZonedDateTime getZonedDateTime(String zoneString) {
        return ZonedDateTime.of(getLocalDateTime(), getZoneId(zoneString));
    }    
}

 

🔄 Comparator 인터페이스의 향상

 

Comparator 인터페이스는 Default 및 Static 메서드를 활용하여 더 표현력 있는 라이브러리 메서드를 만들 수 있습니다. 🎯

myDeck.sort(Comparator.comparing(Card::getRank)
    .thenComparing(Comparator.comparing(Card::getSuit)));

 

또한, 람다 표현식과 메서드 참조를 사용하여 더 간단하고 직관적인 코드를 작성할 수 있습니다.

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유