[C#] 인터페이스(Interface), virtual과의 차이

Gee·2025년 3월 6일

인터페이스?

  • 클래스는 객체의 청사진. 인터페이스는 클래스의 청사진.
  • 클래스가 해야 하는 행동을 결정함. 즉, 클래스가 어떤 메서드를 가질지 결정함.
  • 인터페이스(Interface)는 클래스가 따라야 할 규칙(계약, Contract)을 정의하는 추상적인 개념.
  • 즉, 인터페이스는 "이 클래스는 반드시 이런 기능을 가져야 한다"라고 강제하는 역할을 함
  • 인터페이스를 통해 클래스들은 공통적인 동작을 정의하고, 이러한 동작을 구현하는 클래스들은 해당 인터페이스를 구현(implement)함으로써 공통 규약을 준수할 수 있다.

인터페이스의 특징

  1. C#에서는 interface 키워드를 사용해서 만들고, 내부에는 구현되지 않은 메서드의 형태(이름, 매개변수, 반환 타입)만 정의할 수 있다.
  2. 추상화 : 인터페이스는 추상적인 개념으로, 메서드 구현을 갖지 않음. 메서드의 형태(시그니처)만을 가진다. 따라서 인터페이스는 인스턴스화될 수 없으며 구현체가 필요하다. (인터페이스는 필드를 갖지 않음)
  3. 메서드 시그니쳐 : 인터페이스는 구현 클래스가 반드시 구현해야 하는 메서드들의 시그니쳐를 정의한다. 메서드의 이름, 매개변수, 반환 타입이 포함됨
  4. 다중 상속 가능 : 하나의 클래스가 여러 인터페이스를 동시에 구현할 수 있다. 클래스는 하나의 클래스만 상속받을 수 있지만, 여러 인터페이스를 동시에 구현 가능. 이를 통해 다중 상속을 흉내내는 것이 가능하다.
  5. 강제적 구현 : 클래스가 인터페이스를 구현하면, 인터페이스를 상속하는 실체 클래스는 반드시 인터페이스에서 선언된 메서드를 모두 구현(Override)해야 한다.
  6. 인터페이스 간 확장 : 인터페이스는 다른 인터페이스를 확장할 수 있다. 이를 통해 더 큰 범위의 공통 동작을 정의할 수 있다.
  7. new 연산자로 객체 생성 불가. 단독으로 인스턴스화 할 수 없다.

예시 코드

// 인터페이스 정의
public interface IDamageable  
{
    void TakeDamage(int damage);  // 메서드의 형태만 정의 (구현 X)
}

// 인터페이스 구현
public class Player : IDamageable 
{
    public int health = 100;

    // 반드시 IDamageable의 메서드를 구현해야 함
    public void TakeDamage(int damage)  
    {
        health -= damage;
        Debug.Log("플레이어가 피해를 입었습니다! 현재 체력: " + health);
    }
}
  • IDamageable 인터페이스는 TakeDamage(int damage) 메서드를 정의함.
  • Player 클래스는 IDamageable을 구현(implements)했으므로 TakeDamage()를 필수로 구현해야 한다. (안그럼 에러 뜨더라고)

인터페이스를 사용하는 이유

  • 코드는 결합도가 낮아야 한다.
    • 결합도가 높다는 것은 클래스 간 의존도가 높다는 것 -> 유연성이 떨어지게 된다. 구체적 구현 클래스가 아닌 작은 단위의 여러 인터페이스를 사용하자.
  • 코드의 일관성 유지 : 같은 기능을 가진 여러 클래스가 같은 메서드를 반드시 가지게 됨.
  • 유연한 설계 : 특정 클래스의 구현에 의존하지 않고, 다양한 객체를 다룰 수 있음.
    • (갤럭시 3이 갤럭시 2를 상속받는 것보다는 와이파이 기능, 카메라 기능 등을 인터페이스로 따로 빼는 것. 이렇게 하면 더 이상 필요 없는 기능은 상속을 안 받으면 됨)
  • 다형성 활용 : 하나의 리스트나 변수로 여러 개의 타입을 처리할 수 있음.
  • 유닛 테스트 편리 : 의존성 주입(DI)이 쉬워져서 테스트하기 좋아짐.
  • 협업의 관점
    • 개발 기간을 단축
      • 인터페이스라는 구현되지 않은 틀만 작성하면 구현 클래스에서도 코드 작성 및 개발 가능
    • 표준화가 가능
      • 여러 명의 개발자가 작업해도 정형화된 작업이 가능
    • 독립적인 프로그래밍이 가능
      • 선언은 인터페이스에서 구현은 클래스에서.

예시 코드

public interface Payment
{
	public void Pay();
}

public class Card : Payment
{
		public void Pay(){}
}

public class Cash : Payment
{
		public void Pay(){}
}

public class QR : Payment
{
		public void Pay(){}
}

(x)
public class Store
{
		Card card;
		Cash cash;
		QR qr;
}


(o)
public class Store
{
		Payment payment;
		payment.Pay();
}
  • Card, Cash, QR 모두 '무언가를 지불한다'는 바뀌지 않음. 그래서 그것을 인터페이스로 지정. (이것이 인터페이스의 역할)
  • 이 인터페이스를 안 만들어 놨다면 Store 클래스는 (x) 처럼 Card, Cash, QR에 대한 정보들을 담아 놓을 준비를 해야 함.
  • 다 조건문이 될 것. 상대방이 카드를 냈는지 확인 -> 카드가 아님 -> 캐시 확인 -> 캐시인지 확인 -> 캐시가 아님 -> ...
  • 또한 새로운 지불 방식이 생겼을 때 에러가 발생.
  • 하지만 이 모든 결제 방식을 (o) 처럼 Payment라고 하는 인터페이스로 받는다고 생각해 보자. 그럼 상대방이 카드라는 클래스를 내든 캐시라는 클래스를 내든 모두 지불 방식이기 때문에 Payment라는 인터페이스가 상속받는다는 약속을 해놨음.
  • 그러면 GetComponent를 Card, Cash, QR로 찾는 게 아니라 Payment로 찾게 되는 것.
  • Payment로 찾고 거기에 상속받는 구체적인 Pay()라는 함수를 호출해 주면 되는 것이다.

virtual/override와의 차이

  • 둘 다 메서드를 강제하는 역할을 하지만 차이가 있다.
  • virtual은 기본 동작을 제공하고, interface는 강제하는 것
virtual / overrideinterface (인터페이스)
목적부모 클래스의 메서드를 자식이 "선택적으로" 재정의특정 기능을 "무조건" 구현하도록 강제
구현 여부부모 클래스에서 기본 구현 제공 가능기본 구현 제공 불가 (형태만 정의)
상속 관계클래스 상속(O) → 단일 상속클래스 상속(X) → 다중 구현 가능
객체 생성부모 클래스도 new로 객체 생성 가능인터페이스 자체는 객체 생성 불가
사용 방식virtual 키워드로 부모 메서드 선언, override로 자식이 재정의interface 키워드로 선언, implements로 클래스가 구현
// 부모 클래스 (기본 구현 포함)
public class Animal
{
    public virtual void Speak()  // virtual 메서드
    {
        Debug.Log("동물이 소리를 냅니다.");
    }
}

// 자식 클래스 (오버라이드)
public class Dog : Animal
{
    public override void Speak()  // override로 재정의
    {
        Debug.Log("멍멍!");
    }
}
  • virtual / override (클래스 상속) 예시 코드.
  • virtual을 사용하면 부모 클래스에서 기본 구현을 제공할 수 있음.
  • override를 사용하면 자식 클래스에서 원하는 대로 변경 가능.
  • 자식 클래스는 반드시 오버라이드할 필요는 없음. (선택 사항)
// 인터페이스 정의 (구현 X)
public interface IDamageable
{
    void TakeDamage(int damage);  // 형태만 정의
}

// 인터페이스 구현 (반드시 메서드를 정의해야 함)
public class Player : IDamageable
{
    public int health = 100;

    public void TakeDamage(int damage)  // 무조건 구현해야 함
    {
        health -= damage;
        Debug.Log("플레이어가 피해를 입었습니다! 현재 체력: " + health);
    }
}
  • 인터페이스 (interface) 예시 코드.
  • 인터페이스는 구현을 제공하지 않음. 메서드의 형태만 정의.
  • Player 클래스는 무조건 TakeDamage()를 구현해야 함 (선택 불가).
  • 클래스 상속이 아니라 "기능 추가"의 느낌.
구분virtual / overrideinterface
기본 구현O (기본 메서드 가능)X (형태만 정의)
상속 관계단일 상속 (부모-자식)다중 구현 가능
재정의 필요선택 가능 (안 해도 됨)무조건 구현해야 함
사용 목적기본 기능을 제공하고 필요할 때 재정의특정 기능을 강제
  • 그렇다면 각각 언제 사용하면 좋을까?
  • virtual / override : 부모 클래스에서 기본 기능을 제공하고, 필요하면 재정의 가능하도록 하고 싶을 때 사용
  • interface: 특정 기능이 무조건 필요하고, 클래스 계층과 상관없이 여러 개의 타입에 적용하고 싶을 때 사용
public class Character
{
    public virtual void TakeDamage(int damage)
    {
        Debug.Log("기본적인 피해 처리");
    }
}

public class Player : Character
{
    public override void TakeDamage(int damage)
    {
        Debug.Log("플레이어가 " + damage + "만큼 피해를 입음");
    }
}
  • 만약 IDamageable 인터페이스를 virtual / override로 구현한다면 이런 형태가 될 것.
  • 이렇게 하면 기본 구현을 제공하면서 필요할 때만 변경이 가능하다.
  • 하지만 적과 플레이어가 같은 기능을 가져야 할 때는 interface IDamageable을 사용하는 게 더 좋음.
  • interface IDamageable을 사용하면 적과 플레이어 모두 TakeDamage()를 무조건 구현해야 해서 보다 유연해지기 때문이다.
profile
...

0개의 댓글