[아이티센 부트캠프] (인터페이스 1)

이언덕·2026년 3월 26일

아이티센 부트캠프

목록 보기
21/115
post-thumbnail

인터페이스

인터페이스는 처음 보면 추상적이라 어렵게 느껴질 수 있다.
그래서 이번 정리는 정의만 적는 것이 아니라, 코드가 왜 그렇게 만들어졌는지까지 같이 이해하는 방식으로 정리한다.


핵심은 하나다.

인터페이스는 “이 기능을 하려면 최소한 이 메서드들은 있어야 한다”는 규칙을 먼저 정해두는 도구다.
실제 동작은 그 규칙을 구현한 클래스가 채운다.

참고

이번 범위에서는 인터페이스를 상수와 추상 메서드로 이루어진 규칙이라고 먼저 이해하면 된다.
즉, 지금은 인터페이스가 “무엇을 해야 하는지”를 정해두는 역할에 집중해서 보면 된다.




1. 인터페이스가 왜 필요한가

클래스를 만들다 보면 “객체 종류는 달라도, 어떤 기능은 공통으로 할 수 있어야 한다”는 상황이 생긴다.

예를 들어 어떤 객체는 출력이 가능해야 하고, 어떤 객체는 계산이 가능해야 하고, 어떤 객체는 재생이 가능해야 할 수 있다.
그런데 클래스마다 메서드 이름과 방식이 제각각이면, 사용하는 쪽에서는 객체가 바뀔 때마다 코드를 다시 맞춰야 한다.

이럴 때 공통 규칙을 먼저 정해두면 훨씬 편해진다.
즉, 인터페이스는 클래스 이름보다 기능 기준으로 객체를 다룰 수 있게 해주는 도구다.

쉽게 말해 밑그림만 그려져 있는 기본 설계도라고 할 수 있다.




2. 인터페이스란 무엇인가

인터페이스는 쉽게 말해 기능 명세서에 가깝다.

  • 무엇을 해야 하는지는 정해 둔다.
  • 하지만 어떻게 할지는 구현 클래스가 정한다.

그래서 인터페이스는 보통 이런 특징을 가진다.

개념

  • 직접 객체를 만들 수 없다.
  • 실제 구현보다 규칙 제시가 목적이다.
  • 여러 클래스가 같은 기준으로 동작하게 만들 때 사용한다.
  • “이 기능을 할 수 있는 객체인가?”를 판단하는 기준이 된다.

클래스는 객체를 만들기 위한 설계도에 가깝고,
인터페이스는 구현 클래스가 따라야 하는 약속에 가깝다.




3. 인터페이스 안에는 무엇이 들어가는가

인터페이스에는 기본적으로 상수와 추상 메서드가 들어간다.

예제: InterfaceTest1 - Printable

package day8;

// Printable은 "출력 가능한 객체"가 따라야 하는 규칙이다.
interface Printable {
    // 출력 방식에 사용할 공통 기준값
    // 인터페이스 안의 변수는 자동으로 public static final 성격을 가진다.
    public static final int PRINT_TYPE1 = 1;
    static final int PRINT_TYPE2 = 2;
    final int PRINT_TYPE3 = 3;
    int PRINT_TYPE4 = 4;

    // 어떤 방식으로 출력할지 정하는 메서드
    // 인터페이스 안의 메서드는 자동으로 public abstract 성격을 가진다.
    public abstract void print(int type);

    // 출력할 페이지 수를 설정하는 메서드
    abstract void setPage(int su);

    // 현재 출력 가능한 상태인지 확인하는 메서드
    boolean isAvailable();
}

이 코드는 인터페이스의 구성요소를 잘 보여준다.

핵심 특징

  • 인터페이스 안의 변수는 일반 변수가 아니라 상수다.
  • 인터페이스 안의 메서드는 기본적으로 추상 메서드다.
  • 그래서 public static final, public abstract는 생략할 수 있다.

즉, 아래 네 줄은 전부 같은 성격이다.

public static final int PRINT_TYPE1 = 1;
static final int PRINT_TYPE2 = 2;
final int PRINT_TYPE3 = 3;
int PRINT_TYPE4 = 4;

겉보기에는 변수처럼 보이지만, 실제로는 프로그램 전체에서 공통으로 쓰는 기준값이다.
그래서 값을 바꿀 수 없다.


예를 들어 이런 코드는 에러가 난다.

// Printable.PRINT_TYPE1 = 10; // 에러

메서드도 마찬가지다.

public abstract void print(int type);
abstract void setPage(int su);
boolean isAvailable();

이 셋도 모두 “이 인터페이스를 구현하려면 이 메서드들을 반드시 만들어야 한다”는 뜻이다.
즉, 인터페이스에서는 메서드 내용보다 메서드의 존재 자체가 중요하다.




4. 인터페이스를 구현한다는 것은 무슨 뜻인가

인터페이스는 규칙만 주기 때문에, 실제로 사용하려면 그 규칙을 따르는 클래스가 필요하다.
이때 사용하는 키워드가 implements다.

예제: InterfaceTest1 - Printable, Shape3, Circle3

// Printable은 "출력할 수 있는 객체"가 따라야 하는 기능 규칙을 정의한 인터페이스다.
interface Printable {
    int PRINT_TYPE1 = 1;
    int PRINT_TYPE2 = 2;
    int PRINT_TYPE3 = 3;
    int PRINT_TYPE4 = 4;

    void print(int type);
    void setPage(int page);
    boolean isAvailable();
}

// 도형의 공통 기능을 위한 부모 추상 클래스
abstract class Shape3 {
    String color;

    // 자식마다 도형 종류가 다르므로 draw()는 각자 구현해야 한다.
    abstract void draw();

    // 도형 색상을 저장하는 공통 메서드
    void setColor(String color) {
        this.color = color;
    }
}

// Circle3는 Shape3를 상속받고, Printable 규칙도 함께 구현한다.
class Circle3 extends Shape3 implements Printable {
    int page;

    // Shape3에서 물려받은 추상 메서드를 구현
    void draw() {
        System.out.println(color + " 원을 그리는 기능");
    }

    // Printable의 print() 구현
    // 인터페이스 메서드는 public 성격이므로 구현할 때도 public을 붙여야 한다.
    public void print(int type) {
        System.out.println(type + "번 방식으로 " + page + "페이지를 프린팅한다.");
    }

    // Printable의 setPage() 구현
    public void setPage(int page) {
        this.page = page;
    }

    // Printable의 isAvailable() 구현
    // 여기서는 흰색이면 출력 불가, 그 외 색이면 출력 가능이라고 가정했다.
    public boolean isAvailable() {
        if (color.equals("흰색"))
            return false;
        else
            return true;
    }
}

여기서 Shape3가 같이 나와서 잠깐 헷갈릴 수 있다.
이 예제에서 Shape3는 도형의 공통 기능을 물려주기 위한 부모 클래스다.
핵심은 Circle3가 여기에 더해 Printable 규칙까지 구현했다는 점이다.


즉, 이 한 줄이 중요하다.

class Circle3 extends Shape3 implements Printable

이 말은 곧,

  • Shape3로부터는 도형의 공통 기능을 물려받고
  • Printable로부터는 출력 관련 규칙을 따라야 한다

는 뜻이다.


그래서 Circle3는 Printable에 있던 메서드들을 전부 구현해야 한다.

  • print(int type)
  • setPage(int page)
  • isAvailable()

하나라도 빠지면, 인터페이스 규칙을 끝까지 구현하지 않은 것이므로 에러가 난다.


여기서 한 가지 더 봐야 한다.
인터페이스의 메서드는 기본적으로 public abstract이다.
그래서 구현 클래스에서 메서드를 만들 때도 접근 범위를 더 좁힐 수 없어서 public을 붙여야 한다.

즉, 아래처럼 구현 메서드 앞에 public이 붙는 이유가 바로 이것이다.

public void print(int type) { ... }
public void setPage(int page) { ... }
public boolean isAvailable() { ... }

또 한 가지 같이 보면 좋다.
이 예제에서는 isAvailable() 안에서 color를 검사하지만, 실제 호출 흐름에서는 printOut() 안에서 먼저 setColor("파란색")를 실행한 뒤 isAvailable()을 호출한다.
그래서 이 예제에서는 color가 비어 있지 않은 상태로 검사된다고 이해하면 된다.




5. 인터페이스를 매개변수로 쓰면 왜 좋은가

인터페이스의 장점은 메서드의 매개변수 타입으로 쓸 때 가장 잘 보인다.

예제: InterfaceTest1

package day8;

// "출력이 가능한 객체"라면 따라야 하는 규칙
interface Printable {
    public static final int PRINT_TYPE1 = 1;
    static final int PRINT_TYPE2 = 2;
    final int PRINT_TYPE3 = 3;
    int PRINT_TYPE4 = 4;

    // 어떤 방식으로 출력할지 정하는 메서드
    public abstract void print(int type);

    // 출력할 페이지 수를 정하는 메서드
    abstract void setPage(int su);

    // 현재 출력 가능한 상태인지 확인하는 메서드
    boolean isAvailable();
}

// 도형의 공통 부모 클래스
abstract class Shape3 {
    String color;

    abstract void draw();

    void setColor(String color) {
        this.color = color;
    }
}

// Circle3는 Shape3를 상속받고 Printable도 구현한다.
class Circle3 extends Shape3 implements Printable {
    int page;

    void draw() {
        System.out.println(color + " 원을 그리는 기능");
    }

    public void print(int type) {
        System.out.println(type + "번 방식으로 " + page + "페이지를 프린팅한다.");
    }

    public void setPage(int page) {
        this.page = page;
    }

    public boolean isAvailable() {
        // 흰색이면 출력할 수 없다고 가정
        if (color.equals("흰색"))
            return false;
        else
            return true;
    }
}

// Shape3는 상속받았지만 Printable은 구현하지 않았다.
// 즉, "도형"으로는 볼 수 있어도 "출력 가능한 객체"로는 볼 수 없다.
class Rectangle3 extends Shape3 {
    void draw() {
        System.out.println(color + " 사각형을 그리는 기능");
    }
}

// Triangle3도 마찬가지다.
// 부모는 Shape3로 같지만, Printable 규칙을 구현하지 않았기 때문에
// printOut()의 인자로 넘길 수 없는 비교용 예시다.
class Triangle3 extends Shape3 {
    void draw() {
        System.out.println(color + " 삼각형을 그리는 기능");
    }
}

public class InterfaceTest1 {
    public static void main(String args[]) {
        // 1. Circle3 객체 생성
        // 2. Circle3는 Printable을 구현했으므로 printOut()에 전달 가능
        printOut(new Circle3());

        // Rectangle3는 Shape3의 자식이지만 Printable은 구현하지 않았음
        // 그래서 "출력이 가능한 객체"로는 볼 수 없다.
        // printOut(new Rectangle3()); // 컴파일 오류

        // Triangle3도 마찬가지로 Printable을 구현하지 않았기 때문에 전달할 수 없다.
        // printOut(new Triangle3()); // 컴파일 오류
    }

    static void printOut(Printable prt) {
        // prt는 Printable 타입으로 받고 있다.
        // 따라서 Printable에 선언된 메서드는 바로 호출할 수 있다.
        prt.setPage(100);   // 출력할 페이지 수 저장

        // prt는 Printable 타입이라 setColor()는 바로 보이지 않는다.
        // 하지만 실제 전달 객체는 Circle3이고, Circle3는 Shape3의 자식이다.
        // 그래서 Shape3 타입으로 형변환하면 부모 쪽 기능인 setColor()를 사용할 수 있다.
        ((Shape3)prt).setColor("파란색");

        // isAvailable() 안에서는 color 값을 검사한다.
        // 위에서 setColor("파란색")를 먼저 호출했기 때문에
        // color에는 이미 값이 들어간 상태다.
        if (prt.isAvailable()) {
            // 출력 가능한 상태면 print() 호출
            prt.print(Printable.PRINT_TYPE2);
        }
    }
}

/*
출력결과
2번 방식으로 100페이지를 프린팅한다.
*/

이 코드의 핵심은 printOut(Printable prt)다.

이 메서드는 Circle3라는 특정 클래스를 요구하지 않는다.
대신 Printable 규칙을 만족하는 객체면 받을 수 있다.


즉, 사용하는 쪽에서는 이렇게 생각하면 된다.

  • 너가 정확히 어떤 클래스인지는 중요하지 않다.
  • 대신 Printable 규칙만 지키면 된다.

이게 인터페이스의 핵심이다.
클래스 이름에 묶이지 않고, 기능 기준으로 객체를 받게 된다.




6. 왜 Circle3는 되고 Rectangle3는 안 되는가

초보자가 여기서 가장 많이 헷갈린다.

둘 다 Shape3를 상속받았는데 왜 하나는 되고 하나는 안 될까?


답은 단순하다.

  • Circle3는 Printable을 구현했다.
  • Rectangle3는 Printable을 구현하지 않았다.

즉, printOut()은 “도형이냐 아니냐”를 보는 것이 아니다.
Printable 규칙을 만족하느냐를 보는 것이다.


그래서 이 코드는 가능하다.

printOut(new Circle3());

하지만 이 코드는 안 된다.

// printOut(new Rectangle3()); // 컴파일 오류

왜냐하면 Rectangle3에는

  • print()
  • setPage()
  • isAvailable()

가 없기 때문이다.

즉, 부모가 같다고 해서 같은 역할을 할 수 있는 것은 아니다.
인터페이스는 상속과 별개로 기능 기준을 정하는 장치다.




7. 인터페이스 타입으로 보면 왜 보이는 기능이 제한되는가

printOut() 안에서 prt의 타입은 Printable이다.

그래서 아래 메서드들은 바로 사용할 수 있다.

prt.setPage(100);
prt.isAvailable();
prt.print(Printable.PRINT_TYPE2);

왜냐하면 이 메서드들은 모두 Printable 안에 선언되어 있기 때문이다.


그런데 setColor()는 바로 사용할 수 없다.

// prt.setColor("파란색"); // 불가능

이유는 setColor()가 Printable의 기능이 아니라 Shape3의 기능이기 때문이다.


그래서 이렇게 형변환을 한다.

((Shape3)prt).setColor("파란색");

여기서 한 번 더 확인해야 할 부분이 있다.
isAvailable() 안에서는 color.equals("흰색")를 사용한다.
그런데 이 예제에서는 isAvailable()을 호출하기 전에 먼저 setColor("파란색")을 실행하므로 color에는 값이 들어간 상태다.

여기서 꼭 이해할 점

이 형변환은 아무 Printable 객체에나 무조건 되는 것이 아니다.
이번 예제에서 가능한 이유는 현재 전달된 객체가 Circle3이고, Circle3가 Shape3를 상속하고 있기 때문이다.


즉,

  • 실제 객체는 Circle3
  • Circle3는 Shape3의 자식
  • 그래서 Shape3 타입으로 보는 것이 가능

한 것이다.


반대로 실제 객체가 Shape3와 전혀 관계없는 다른 Printable 구현 클래스였다면,
이 형변환은 안 된다.


따라서 여기서 중요한 것은
객체가 바뀌는 것이 아니라, 객체를 바라보는 타입이 바뀐다는 점이다.

  • Printable 타입으로 보면 Printable에 있는 기능만 보이고
  • Shape3 타입으로 보면 Shape3에 있는 기능도 보인다

이 차이를 이해해야 한다.
이 예제의 형변환은 인터페이스 타입으로 보면 보이는 기능이 제한된다는 점을 보여주기 위한 예시다.




8. 인터페이스도 상속할 수 있다

상속은 클래스만 되는 것이 아니다.
인터페이스도 인터페이스끼리 상속할 수 있다.

그리고 여기서 중요한 차이가 하나 있다.

클래스는 단일상속만 가능하지만, 인터페이스는 다중상속이 가능하다.

예제: InterfaceTest2 - 인터페이스 상속

package day8;

// 첫 번째 규칙
interface Inter1 {
    void expr1();
}

// 두 번째 규칙
interface Inter2 {
    void expr2();
}

// Inter3는 Inter1, Inter2를 동시에 상속받는다.
// 그래서 expr1(), expr2() 규칙을 모두 물려받고
// 자기 규칙인 expr3()까지 추가로 가진다.
interface Inter3 extends Inter1, Inter2 {
    void expr3();
}

여기서 Inter3는 Inter1, Inter2를 모두 상속받는다.

즉, Inter3는 자기 메서드인 expr3()만 가지는 것이 아니라
위에서 물려받은

  • expr1()
  • expr2()

도 함께 가진다.

결국 Inter3는 세 개의 메서드 규칙을 가진 인터페이스가 된다.




9. 인터페이스를 구현한 클래스는 무엇을 해야 하는가

InterClass는 Inter3를 구현한다.

예제: InterfaceTest2 - 구현 클래스

// InterClass는 Inter3를 구현한다.
// 그런데 Inter3는 이미 Inter1, Inter2를 상속받았기 때문에
// 결국 expr1(), expr2(), expr3()를 모두 구현해야 한다.
class InterClass implements Inter3 {
    // Inter1에서 온 메서드 구현
    public void expr1() {
        System.out.println("expr1() 메서드 오버라이딩");
    }

    // Inter2에서 온 메서드 구현
    public void expr2() {
        System.out.println("expr2() 메서드 오버라이딩");
    }

    // Inter3 자신이 가진 메서드 구현
    public void expr3() {
        System.out.println("expr3() 메서드 오버라이딩");
    }
}

여기서 중요한 점은 InterClass가 Inter3만 구현했는데도
왜 메서드 세 개를 모두 만들어야 하느냐는 것이다.

이유는 Inter3가 이미 Inter1, Inter2를 상속받고 있기 때문이다.


즉, InterClass implements Inter3는 사실상

  • expr1()
  • expr2()
  • expr3()

를 모두 책임져야 한다는 뜻이다.

하나라도 빠지면 규칙을 완성하지 못했기 때문에 에러가 난다.




10. 인터페이스 다형성

이제 인터페이스 타입 변수에 구현 객체를 담는 구조를 보자.

예제: InterfaceTest2

package day8;

interface Inter1 {
    void expr1();
}

interface Inter2 {
    void expr2();
}

interface Inter3 extends Inter1, Inter2 {
    void expr3();
}

class InterClass implements Inter3 {
    public void expr1() {
        System.out.println("expr1() 메서드 오버라이딩");
    }

    public void expr2() {
        System.out.println("expr2() 메서드 오버라이딩");
    }

    public void expr3() {
        System.out.println("expr3() 메서드 오버라이딩");
    }
}

public class InterfaceTest2 {
    public static void main(String args[]) {
        System.out.println("InterClass 객체를 Inter1 타입 변수에 대입");
        // 실제 객체는 InterClass지만, Inter1 타입으로 참조하므로 expr1()만 바로 보인다.
        Inter1 obj1 = new InterClass();
        obj1.expr1();
        // obj1.expr2(); // 불가능
        // obj1.expr3(); // 불가능

        System.out.println("InterClass 객체를 Inter2 타입 변수에 대입");
        // 실제 객체는 똑같이 InterClass지만, Inter2 타입으로 보면 expr2()만 바로 보인다.
        Inter2 obj2 = new InterClass();
        obj2.expr2();
        // obj2.expr1(); // 불가능
        // obj2.expr3(); // 불가능

        System.out.println("InterClass 객체를 Inter3 타입 변수에 대입");
        // Inter3는 Inter1, Inter2를 상속받았으므로 세 메서드를 모두 사용할 수 있다.
        Inter3 obj3 = new InterClass();
        obj3.expr1();
        obj3.expr2();
        obj3.expr3();
    }
}

/*
출력결과
InterClass 객체를 Inter1 타입 변수에 대입
expr1() 메서드 오버라이딩
InterClass 객체를 Inter2 타입 변수에 대입
expr2() 메서드 오버라이딩
InterClass 객체를 Inter3 타입 변수에 대입
expr1() 메서드 오버라이딩
expr2() 메서드 오버라이딩
expr3() 메서드 오버라이딩
*/

이 예제는 인터페이스 다형성을 잘 보여준다.

코드 흐름

  • 실제 객체는 전부 new InterClass()다.
  • 하지만 참조 변수 타입은 각각 다르다.
  • 그래서 호출 가능한 메서드 범위도 달라진다.

예를 들어

Inter1 obj1 = new InterClass();

이 경우 실제 객체는 InterClass지만,
obj1은 Inter1 타입으로 보고 있으므로 expr1()만 바로 사용할 수 있다.




반대로

Inter3 obj3 = new InterClass();

이 경우 Inter3는 Inter1, Inter2를 상속받았기 때문에

  • expr1()
  • expr2()
  • expr3()

를 모두 사용할 수 있다.


즉, 실제 객체는 같아도 어떤 타입으로 참조하느냐에 따라 보이는 기능 범위가 달라진다.
이것이 인터페이스 다형성이다.




11. 인터페이스의 장점

여기까지 본 내용을 정리하면 인터페이스의 장점은 분명하다.

1) 사용하는 쪽과 만드는 쪽을 나눌 수 있다

인터페이스만 먼저 정해두면,
사용하는 쪽은 그 규칙을 기준으로 코드를 작성할 수 있다.


2) 규칙을 통일할 수 있다

같은 기능을 하는 클래스들이 모두 같은 이름과 같은 형태의 메서드를 갖게 만들 수 있다.


3) 서로 관련 없는 클래스도 같은 기준으로 묶을 수 있다

같은 부모가 없어도, 같은 인터페이스를 구현하면 공통 기준으로 다룰 수 있다.


4) 클래스 이름보다 기능 기준으로 코드를 짤 수 있다

특정 클래스 하나에 직접 의존하지 않고,
“이 기능을 할 수 있는 객체인가?”를 기준으로 코드를 만들 수 있다.




12. 헷갈리기 쉬운 부분

1) 인터페이스는 직접 객체를 만들 수 없다

// Printable p = new Printable(); // 불가능

인터페이스는 규칙일 뿐이므로 직접 객체를 만들 수 없다.
객체는 인터페이스를 구현한 클래스에서 만들어야 한다.


2) 인터페이스를 구현하면 메서드를 반드시 완성해야 한다

구현한다고 적었다면, 그 안에 선언된 메서드는 모두 만들어야 한다.


3) 인터페이스 타입으로 보면 인터페이스에 있는 기능만 보인다

실제 객체가 더 많은 기능을 가지고 있어도,
참조 타입이 인터페이스면 그 인터페이스에 선언된 기능만 바로 사용할 수 있다.


4) 상속과 인터페이스 구현은 다른 이야기다

  • 상속은 “어느 클래스를 물려받았는가”
  • 인터페이스는 “어떤 규칙을 따르는가”

를 보는 것이다.


5) 인터페이스 다형성은 기능 중심의 다형성이다

부모-자식 클래스 관계만 보는 것이 아니라,
같은 인터페이스를 구현했는지를 기준으로 객체를 다룬다.




13. 참고

인터페이스는 이후에 default 메서드와 static 메서드도 배우게 된다.
이번 범위에서는 먼저 인터페이스의 기본 구조, 구현, 상속, 다형성을 확실히 이해하는 것이 더 중요하다.



핵심만 다시 정리하면

  • 인터페이스는 규칙을 먼저 정하는 도구다.
  • 구현 클래스는 그 규칙을 실제로 완성한다.
  • 인터페이스를 매개변수나 참조 변수 타입으로 쓰면, 클래스 이름보다 기능 기준으로 객체를 다룰 수 있다.
  • 그래서 코드가 더 유연해지고, 여러 객체를 같은 방식으로 사용할 수 있다.

0개의 댓글