💻 프로그래밍

인터페이스

Interface

클래스가 구현해야 할 메서드를 정의하는 계약. Java, TypeScript, Go에서 다형성 구현에 사용.

📖 상세 설명

인터페이스(Interface)는 클래스나 객체가 구현해야 할 메서드 시그니처를 정의하는 계약입니다. 구현 세부사항 없이 "무엇을 해야 하는가"만 명시하여, 서로 다른 구현체들이 동일한 방식으로 사용될 수 있게 합니다. 이는 다형성의 핵심 메커니즘입니다.

Java에서 인터페이스는 interface 키워드로 선언하고, 클래스가 implements로 구현합니다. 모든 메서드는 기본적으로 public abstract이며, Java 8부터는 default와 static 메서드도 가질 수 있습니다. 하나의 클래스가 여러 인터페이스를 구현할 수 있어 다중 상속의 대안이 됩니다.

TypeScript에서 인터페이스는 객체의 구조(shape)를 정의합니다. 프로퍼티, 메서드, 인덱스 시그니처를 명시하여 타입 안정성을 제공합니다. 특히 TypeScript는 덕 타이핑(structural typing)을 사용하므로, 명시적으로 구현 선언을 하지 않아도 구조가 맞으면 해당 인터페이스 타입으로 사용할 수 있습니다.

Go에서는 인터페이스가 암묵적으로 구현됩니다. 특정 메서드 집합을 가진 타입은 자동으로 해당 인터페이스를 만족합니다. 빈 인터페이스(interface{})는 모든 타입을 받을 수 있어 제네릭의 역할을 하기도 합니다. 이러한 접근법은 느슨한 결합을 자연스럽게 유도합니다.

💻 코드 예제

// TypeScript 인터페이스 예제

// 기본 인터페이스 정의
interface User {
    id: number;
    name: string;
    email: string;
    createdAt?: Date;  // 선택적 프로퍼티
}

// 메서드가 있는 인터페이스
interface Repository<T> {
    findById(id: number): Promise<T | null>;
    findAll(): Promise<T[]>;
    create(data: Omit<T, 'id'>): Promise<T>;
    update(id: number, data: Partial<T>): Promise<T>;
    delete(id: number): Promise<boolean>;
}

// 인터페이스 구현
class UserRepository implements Repository<User> {
    private users: User[] = [];

    async findById(id: number): Promise<User | null> {
        return this.users.find(u => u.id === id) || null;
    }

    async findAll(): Promise<User[]> {
        return [...this.users];
    }

    async create(data: Omit<User, 'id'>): Promise<User> {
        const user = { id: Date.now(), ...data };
        this.users.push(user);
        return user;
    }

    async update(id: number, data: Partial<User>): Promise<User> {
        const index = this.users.findIndex(u => u.id === id);
        this.users[index] = { ...this.users[index], ...data };
        return this.users[index];
    }

    async delete(id: number): Promise<boolean> {
        const index = this.users.findIndex(u => u.id === id);
        if (index !== -1) {
            this.users.splice(index, 1);
            return true;
        }
        return false;
    }
}

🗣️ 실무 대화 예시

아키텍트: "결제 모듈을 외부 서비스로 교체해야 하는데, 기존 코드를 많이 수정해야 할 것 같습니다."

기술 리더: "PaymentGateway 인터페이스를 정의해서 추상화하면 어떨까요? 구현체만 교체하면 클라이언트 코드는 그대로 유지됩니다."

아키텍트: "좋은 생각이에요. 의존성 주입으로 구현체를 교체하면 테스트할 때 Mock도 쉽게 넣을 수 있겠네요."

기술 리더: "맞습니다. 인터페이스에 의존하면 느슨한 결합이 되어서 확장성도 좋아집니다."

면접관: "추상 클래스와 인터페이스의 차이점은 무엇인가요?"

지원자: "추상 클래스는 상태(필드)와 구현된 메서드를 가질 수 있고 단일 상속만 가능합니다. 인터페이스는 순수한 계약으로 다중 구현이 가능합니다. Java 8부터는 인터페이스도 default 메서드를 가질 수 있지만, 용도가 다릅니다."

면접관: "어떤 경우에 인터페이스를 사용하나요?"

지원자: "서로 관련 없는 클래스들이 동일한 동작을 해야 할 때, 또는 다중 타입이 필요할 때 인터페이스를 사용합니다. 상속 관계가 있고 공통 구현을 공유해야 하면 추상 클래스가 적합합니다."

리뷰어: "구체 클래스에 직접 의존하고 있네요. 테스트하기 어려울 것 같습니다."

작성자: "어떻게 개선하면 좋을까요?"

리뷰어: "인터페이스를 추출해서 의존성을 역전시키세요. 생성자에서 인터페이스 타입으로 받으면 테스트 시 Mock 객체를 주입할 수 있습니다."

작성자: "SOLID의 D(의존성 역전 원칙)을 적용하는 거군요. 리팩토링하겠습니다."

⚠️ 주의사항

🔗 관련 용어

📚 더 배우기