글 작성자: beaniejoy

 

  • Java 인터페이스의 특징
  • OOP의 인터페이스 분리 원칙
  • 인터페이스 상속
  • 정리

 

📌 1. Java 인터페이스 특징

Java 인터페이스는 다형성을 가장 잘 보여주는 존재입니다.

public interface RemoteController {
    void volumeUp();
    void volumeDown();
}

리모컨이라는 인터페이스를 가지고 간단한 예시를 들어보겠습니다.

@Component
public class SRemoteController implements RemoteController {

    @Override
    public void volumeUp() {
        // volume up 로직
        System.out.println("S사 리모컨 볼륨을 1단계 높였습니다.");
    }

    @Override
    public void volumeDown() {
        // volume down 로직
        System.out.println("S사 리모컨 볼륨을 1단계 낮췄습니다.");
    }
}

이런식으로 RemoteController라는 인터페이스의 구현체인 SRemoteController를 만들 수 있습니다. 이렇게 되면 SRemoteControllerRemoteController라는 일종의 타입으로 객체를 받을 수 있게 됩니다. 이것이 제가 생각하는 다형성의 핵심입니다.

@ExtendWith(SpringExtension.class)
@SpringBootTest
class RemoteControllerTest {

    @Autowired
    RemoteController remoteController;

    @Test
    public void volumeUp() {
        remoteController.volumeUp();
    }

    @Test
    public void volumeDown() {
        remoteController.volumeDown();
    }

}

JUnit Test에서 리모컨을 테스트한 것이지만 RemoteControllerTest는 리모컨을 사용하는 클라이언트에 해당한다고 볼 수 있습니다. @Autowired로 DI를 받아 올 때 RemoteController로 받아옵니다. 즉 RemoteController의 구현체를 Component로 지정만 하면 어느 것이든 교체할 수 있게 됩니다.

 

📌 2. OOP의 인터페이스 분리 원칙

ISP(Interface Segregation Principle)은 인터페이스 분리 원칙으로 OOP의 SOLID 기본 원칙 중 하나이자 SRP 개념과 어떻게 보면 비슷한 원칙입니다. 클라이언트의 관심사에 따라 알맞게 인터페이스를 분리해주는 원칙입니다.

@Component("allRemoteController")
public class AllFuncRemoteController implements RemoteController, ConsoleController {
    @Override
    public void connect() {
        // 콘솔에 접속 로직
        System.out.println("복합 리모컨을 이용해 콘솔 접속을 진행합니다.");
    }

    @Override
    public void selectContent() {
        // 콘솔 게임 선택 로직
        System.out.println("복합 리모컨을 이용해 콘솔 게임을 선택합니다.");
    }

    @Override
    public void setUp() {
        // 콘솔 환경설정 조작 로직
        System.out.println("복합 리모컨을 이용해 콘솔 환경설정을 조작합니다.");
    }

    @Override
    public void volumeUp() {
        // volume up 로직
        System.out.println("복합 리모컨 볼륨을 1단계 높였습니다.");
    }

    @Override
    public void volumeDown() {
        // volume down 로직
        System.out.println("복합 리모컨 볼륨을 1단계 낮췄습니다.");
    }
}

이상한 예시이지만 예를 들어 위의 코드처럼 여러 기능을 갖춘 리모컨이 있다고 가정해보겠습니다. 이 기능들 중에서 A라는 클라이언트는 콘솔을 조작할 수 있는 기능들에 관심이 있고 B라는 클라이언트는 기존의 볼륨 조절 기능에 관심이 있습니다. 이 때 인터페이스는 클라이언트 A, B에게 AllFuncRemoteController를 본인의 관심사에 맞게 바라볼 수 있는 일종의 창(window)을 제공해줍니다.

// For Client A.
public interface ConsoleController {
    void connect();

    void selectContent();

    void setUp();
}
// For Client B.
public interface RemoteController {

    void volumeUp();

    void volumeDown();
}

클라이언트 A, B는 각각 ConsoleController, RemoteController 인터페이스라는 창을 통해 AllFuncRemoteController 구현체를 사용할 수 있습니다. 여기서 또 등장하는 것이 인터페이스 최소주의 원칙인데 클라이언트에게 인터페이스를 통해 메서드를 외부에 제공할 때 최소한의 메서드만 제공하라는 원칙입니다. ConsoleController에만 관심있는 A에게 RemoteController 기능들도 같이 끼워서 제공할 필요가 없다는 것입니다. 오로지 관심 영역 경계 안에 해당하는 최소한의 기능들만 제공하는 것이 좋습니다.

인터페이스 분리 원칙의 장점은 모든 클라이언트가 본인의 관심 영역에 해당하는 기능들만 접근하고 불필요한 기능들의 간섭 없이 이들을 유지할 수 있다는 것입니다.

 

📌 3. 인터페이스 상속

인터페이스 분리 원칙을 통해 새로운 클라이언트가 등장했을 때 또 다른 관심 영역에 대해 새로운 인터페이스를 생성하는 방식으로 제공해줄 수 있었습니다. 이런 방법 말고도 기존 인터페이스 상속을 통해 기능을 확장하는 방법도 존재합니다.

public interface ChannelRemoteController extends RemoteController{
    void goChannelUp();

    void goChannelDown();
}

@Component
public class MyChAndVolRemoteController implements ChannelRemoteController {
    @Override
    public void goChannelUp() {
        // 채널 up 로직
        System.out.println("내 리모컨을 통해 채널 하나 높였습니다.");
    }

    @Override
    public void goChannelDown() {
        // 채널 down 로직
        System.out.println("내 리모컨을 통해 채널 하나 내렸습니다.");
    }

    @Override
    public void volumeUp() {
        // volume up 로직
        System.out.println("내 리모컨으로 볼륨을 1단계 높였습니다.");
    }

    @Override
    public void volumeDown() {
        // volume down 로직
        System.out.println("내 리모컨으로 볼륨을 1단계 낮췄습니다.");
    }
}

이런 방식으로 기존의 인터페이스 기능들로부터 확장하는 방식으로 새로운 클라이언트에게 확장된 기능들을 제공해줄 수 있습니다.

인터페이스 상속을 사용하는 상황은 다음과 같습니다.

  • 새로운 클라이언트가 상위 인터페이스를 그대로 사용하면서 새로운 기능들을 추가 확장하고 싶을 때
  • 기존 상위 인터페이스를 구현하고 있는 서브 클래스들이 많아서 상위 인터페이스를 수정하고 싶어도 수정하기 힘들 때

 

📌 4. 정리

인터페이스는 DI를 하는데 있어서 웬만하면 사용해야 하는 중요한 창입니다. 중요한 만큼 인터페이스를 어떤 방식으로 사용하는지에 대해 어딘가 어색한 예시를 들어 알아보았습니다. 이것 말고도 Spring은 인터페이스를 정말 다양한 방식으로 접목해서 사용하고 있는데 위의 방식들도 핵심이라 할 정도로 중요한 내용이니 이 글을 시작으로 인터페이스에 대해 더 깊게 알아보는 시간을 가지면 좋을 것 같네요. :)

 

틀린 내용이 있을 수 있습니다. 이에 대한 코멘트 언제나 환영입니다!