Interface: 본질, Android를 위한 Java와 Kotlin의 계약

저자: IT Sectr 게시일: 2026-02-18 읽는 시간: 9 분

Interface는 클래스가 구현해야 하는 추상 메서드 집합을 정의하는 계약입니다. Java와 Kotlin에서 인터페이스는 추상화와 다형성의 주요 메커니즘입니다. Java 8+에서는 인터페이스에 default 및 static 메서드를 포함할 수 있고, Kotlin에서는 기본 구현을 포함할 수 있습니다. Google Android Developers(2025)에 따르면, Android 프로젝트의 90%에서 아키텍처 계층(리포지토리, UseCase, 서비스)을 정의하는 데 인터페이스가 사용됩니다.

주요 포인트

  • Interface는 구현 없이 메서드 시그니처를 정의하는 추상 타입(Java 8 이전)
  • Implements는 클래스를 인터페이스에 연결하는 키워드; 클래스는 여러 인터페이스를 구현할 수 있음
  • Default method는 Java 8에서 하위 호환성을 위해 추가된, Java 인터페이스의 구현이 있는 메서드
  • Kotlin interface는 게터가 있는 속성과 메서드 구현을 지원하며, 많은 시나리오에서 abstract class를 대체
  • Markup interface는 마커로 사용되는 빈 인터페이스(Serializable, Cloneable, RandomAccess)

Java와 Kotlin에서 Interface란?

Interface는 추상 메서드, 상수 및 기본 메서드를 포함하는 참조 타입입니다. 인터페이스를 구현하는 클래스는 모든 추상 메서드에 대한 구현을 제공해야 합니다. Java에서 인터페이스는 상태(인스턴스 필드)를 가질 수 없습니다. Kotlin도 이 제한을 따르지만 접근자가 있는 속성을 지원합니다.

Java
public interface Repository {
    T findById(Long id);
    List findAll();
    T save(T entity);
    void deleteById(Long id);
    
    default long count() {
        return findAll().size();
    }
}

@Entity
public class UserEntity {
    private Long id;
    private String email;
    // 게터와 세터
}

public class UserRepositoryImpl implements Repository {
    private final EntityManager em;
    
    public UserEntity findById(Long id) {
        return em.find(UserEntity.class, id);
    }
    
    public List findAll() {
        return em.createQuery("FROM UserEntity", UserEntity.class)
                .getResultList();
    }
    // 다른 메서드
}

Repository<T>는 CRUD 메서드를 가진 제네릭 인터페이스입니다. default 메서드 count()는 재정의할 수 있는 기본 구현을 제공합니다. UserRepositoryImpl은 데이터 액세스에 EntityManager를 사용하여 인터페이스를 구현합니다. 이 접근 방식을 통해 실제 데이터베이스 없이 인터페이스 mock을 통해 데이터 계층을 테스트할 수 있습니다.

Interface vs abstract class: 언제 무엇을 선택할까

인터페이스와 추상 클래스 간의 선택은 공유 상태의 존재와 타입 간의 관계에 따라 달라집니다. 인터페이스는 계약(클래스가 할 수 있는 일)을 정의하고, 추상 클래스는 공통 구현(클래스가 무엇인지)을 정의합니다.

기준InterfaceAbstract Class
상태(필드)static final 상수만예, 모든 필드
생성자없음있음
다중 상속가능(implements)불가능(extends 하나)
접근 제한자public(Java 8-), default 메서드모두(private, protected, public)
사용 시기관련 없는 클래스를 위한 계약관련 클래스를 위한 공통 기반

Clean Architecture에서는 인터페이스가 내부 계층(domain)에 배치되고 구현은 외부 계층(data)에 배치됩니다. 이는 의존성 규칙(외부 계층이 내부 계층에 의존하며 그 반대는 안 됨)을 유지할 수 있게 합니다. 추상 클래스는 템플릿 메서드(Template Method 패턴)에 더 자주 사용됩니다.

Kotlin의 Interface: 속성과 위임

Kotlin 인터페이스는 Java보다 유연합니다. 추상 속성을 선언하고 메서드 구현을 제공할 수 있습니다. Java와 달리 Kotlin은 by 키워드를 통한 위임을 지원하여 Delegate 패턴을 구현할 때 보일러플레이트를 크게 줄여줍니다.

Kotlin
interface ApiService {
    val baseUrl: String  // 추상 속성
    
    suspend fun fetchData(): Result>
    
    fun getEndpoint(path: String): String {
        return "$baseUrl/$path"  // 기본 구현
    }
}

class RetrofitApiService(
    override val baseUrl: String
) : ApiService {
    private val client = Retrofit.Builder()
        .baseUrl(baseUrl)
        .build()
    
    override suspend fun fetchData(): Result> {
        // 요청 구현
    }
}

// by를 통한 위임
interface Logger {
    fun log(message: String)
}

class ConsoleLogger : Logger {
    override fun log(message: String) = println(message)
}

class UserService(logger: Logger) : Logger by logger

UserService는 위임(by)을 통해 Logger 인터페이스를 구현합니다. 모든 log() 호출은 래퍼 메서드를 작성하지 않고 logger 객체로 전달됩니다. 이는 Java에서 5-10줄의 보일러플레이트 코드가 필요한 컴포지션의 예입니다.

Clean Architecture Android의 Interface

Clean Architecture for Android는 애플리케이션을 명확하게 계층으로 분리합니다. 인터페이스는 계층 간의 경계 역할을 합니다. Domain은 리포지토리와 UseCase 인터페이스를 정의하고 Data는 구현을 제공합니다. 이를 통해 비즈니스 로직을 변경하지 않고 구현을 교체할 수 있으며, Room에서 Firebase 또는 REST에서 GraphQL로 마이그레이션할 때 핵심 이점입니다.

Kotlin
// Domain layer — 인터페이스(계약)
interface UserRepository {
    suspend fun getUser(id: String): User
    suspend fun updateUser(user: User)
}

// Domain layer — use case(인터페이스에 의존)
class GetUserUseCase(
    private val repository: UserRepository
) {
    suspend operator fun invoke(id: String): Result {
        return runCatching { repository.getUser(id) }
    }
}

// Data layer — 구현
class UserRepositoryImpl(
    private val api: UserApi,
    private val dao: UserDao
) : UserRepository {
    override suspend fun getUser(id: String): User {
        val cached = dao.getUser(id)
        if (cached != null) return cached
        val remote = api.fetchUser(id)
        dao.insertUser(remote)
        return remote
    }
    // updateUser...
}

UserRepository는 domain 계층의 인터페이스입니다. GetUserUseCase는 구현이 아닌 인터페이스에 의존합니다. data 계층의 UserRepositoryImpl은 API와 로컬 데이터베이스를 결합하여 인터페이스를 구현합니다. 이 아키텍처를 사용하면 데이터베이스나 네트워크를 설정하지 않고 mock 리포지토리로 UseCases를 테스트할 수 있습니다.

Java 8+의 default 및 static 메서드

default 메서드는 하위 호환성을 깨뜨리지 않고 인터페이스를 진화적으로 확장하기 위해 Java 8에서 추가되었습니다. ArrayList가 Collection에 추가된 새로운 stream() 메서드를 구현하지 않았더라도 이전 코드는 계속 작동했을 것입니다. 인터페이스의 static 메서드는 인터페이스와 관련된 유틸리티 역할을 하며 유틸리티 클래스의 대안입니다.

Java
public interface Vehicle {
    void start();
    void stop();
    
    default void honk() {
        System.out.println("Beep!");
    }
    
    static Vehicle of(String type) {
        if ("car".equals(type)) return new Car();
        return new Bicycle();
    }
    
    // 상수
    String CATEGORY = "transport";
}

default 메서드는 diamond problem을 해결합니다. 클래스가 동일한 default 메서드를 가진 두 인터페이스를 구현하는 경우 컴파일러가 명시적 재정의를 요구합니다. static 메서드는 인터페이스 이름을 통해 호출됩니다 — Vehicle.of("car")와 같이 인스턴스 없이 호출됩니다.

인터페이스 설계의 일반적인 실수

인터페이스 설계의 실수는 취약한 코드, 테스트 복잡성 및 SOLID 위반으로 이어집니다. 세 가지 일반적인 문제를 살펴보겠습니다.

Interface Pollution — 하나의 인터페이스에 너무 많은 메서드

15개 이상의 메서드를 포함하는 인터페이스는 Interface Segregation Principle(ISP)을 위반합니다. 예로는 10개 이상의 메서드가 있는 오래된 java.util.Dictionary가 있습니다. 해결책: 여러 개의 작은 인터페이스(ReadableRepository, WritableRepository, SearchableRepository)로 분할합니다. 클라이언트(서비스)는 필요한 메서드에만 의존합니다.

과도한 추상화 — 모든 클래스에 인터페이스

다형성에 대한 실제 필요 없이 모든 클래스에 인터페이스를 만드는 것은 Interface overkill 안티패턴입니다. 징후: 인터페이스에 정확히 하나의 구현이 있고 프로젝트에 대안을 추가할 계획이 없습니다. 해결책: 두 번째 구현 옵션이 나타나거나 테스트를 위한 mock이 필요할 때만 인터페이스를 추가합니다.

자주 묻는 질문

Java에서 interface와 abstract class의 차이점은?

Interface는 계약(메서드 시그니처)만 정의하고 상태를 가질 수 없으며 다중 상속을 지원합니다. Abstract class는 필드, 생성자 및 구현된 메서드를 포함할 수 있지만 클래스는 하나의 추상 클래스만 상속할 수 있습니다. Java 8부터 인터페이스는 default 및 static 메서드를 획득하여 격차를 줄였습니다.

인터페이스가 다른 인터페이스를 상속할 수 있나요?

네, Java와 Kotlin에서 인터페이스는 상속을 지원합니다. public interface AdvancedRepository<T> extends Repository<T>, Pageable은 두 개의 다른 인터페이스를 결합한 인터페이스입니다. AdvancedRepository를 구현하는 클래스는 두 부모 인터페이스의 모든 메서드를 구현해야 합니다. 다중 상속은 인터페이스에만 허용됩니다.

Java의 함수형 인터페이스란?

함수형 인터페이스는 단일 추상 메서드(SAM — Single Abstract Method)를 가진 인터페이스입니다. @FunctionalInterface 애너테이션이 이 제한을 보장합니다. 예: Runnable, Callable, Comparator, Consumer. 함수형 인터페이스는 Java 8 람다 표현식의 기초입니다: () -> System.out.println()은 Runnable을 구현합니다.

Java 인터페이스에 default 메서드가 필요한 이유는?

default 메서드는 모든 구현 클래스를 수정하지 않고 인터페이스에 새 메서드를 추가할 수 있게 합니다. 예를 들어, Java 8은 Collection에 stream()을 default 메서드로 추가했습니다. 이 메커니즘이 없으면 foreach(), stream() 및 기타 메서드가 JDK의 수천 개 클래스를 변경해야 했을 것입니다. Default는 하위 호환 가능한 확장 방식입니다.

Kotlin은 다중 상속 문제를 어떻게 해결하나요?

Kotlin은 클래스의 다중 상속을 금지하지만 인터페이스의 다중 구현을 허용합니다. 두 인터페이스가 동일한 시그니처와 기본 구현을 가진 메서드를 가지고 있으면 컴파일러가 super<InterfaceName>.method() 호출로 명시적 재정의를 요구합니다. 이는 컴파일 수준에서 diamond problem을 해결합니다.

요약

  • Interface는 클래스가 구현해야 하는 메서드를 정의하는 계약; Java와 Kotlin의 다형성 주요 메커니즘
  • Java 8+는 인터페이스에 default 및 static 메서드를 추가하여 추상 클래스와의 격차를 줄임
  • Kotlin interfaces는 추상 속성, 기본 구현 및 by를 통한 위임 지원
  • Clean Architecture는 Domain과 Data 계층 간의 경계로 인터페이스 사용
  • Interface Segregation Principle은 큰 인터페이스를 특화된 인터페이스로 분할 필요
  • 함수형 인터페이스(Single Abstract Method)는 람다 표현식과 Stream API의 기초
  • 권장: 두 번째 구현이 나타나거나 테스트용 mock이 필요할 때 인터페이스 추가

턴키 방식의 모바일 애플리케이션을 개발해 드립니다

IT Sectr는 2017년부터 스타트업과 기업을 위한 iOS 및 Android 애플리케이션을 만듭니다. 저희가 상담해 드리고 최적의 솔루션을 제안하겠습니다.

프로젝트 논의

더 읽어보기