ABOUT

성능과 운영 안정성을 함께 끌어올리는 개발자입니다.

92% Positional Error Reduction
79% p95 Latency Improvement
90%+ Long Tasks Reduction

2022.02 · 한국장학재단

우수 멘티

한국장학재단 사회 리더 대학생 멘토링 IT

2022.10 · 동작구청

우수 인재상

동작구청 우수 SW 인재

2025.05 · (주) 그랩

프로그래밍 우수상

(주) 그랩 우수 프로그램 개발

2025.05 · AWSKRUG

AWS한국사용자모임 발표

AI agent 스크립트 튜닝 관련 발표

ComputerScience

Development

Engineering

Trouble Shooting

GUESTBOOK

첫 마음부터
함께 나누는 온기

방명록 작성하러 가기

SUBSCRIBE

최신소식을
편하게 만나보세요.

인터페이스(Interface)

도입
“구현을 바꿔도 시스템이 흔들리지 않게 만드는 계약”입니다.

인터페이스를 “추상 클래스랑 비슷한 문법” 정도로 기억하기 쉽습니다.

하지만 인터페이스는 단순 문법이 아니라, 변경 비용을 통제하고 테스트/확장을 가능하게 하는 설계 도구입니다.

DB가 바뀌거나, 외부 API가 교체되거나, 캐시 / 메시징이 추가되더라도 서비스 로직이 크게 흔들리지 않게 만드는 핵심 장치가 바로 “계약”입니다.

인터페이스는 그 계약을 코드 레벨에서 가장 명확하게 표현하는 방법 중 하나입니다.

정의
“무엇을 할 수 있는지(What)”를 정의하고, “어떻게 구현할지(How)”는 구현체에 맡기는 행동(메서드) 중심의 계약입니다.

즉, 인터페이스는 “기능 목록”이 아니라 의존성을 역전시키는 경계입니다.

상위 계층(Service/Domain)이 하위 구현(DB/Redis/외부 API)의 구체 타입에 직접 의존하지 않고, 인터페이스(계약)에 의존하게 만들면 구현을 교체하거나 테스트를 구성하기가 훨씬 쉬워집니다.

인터페이스는 객체지향에서 변경에 강한 코드를 만들기 위한 핵심 장치입니다.

코드는 “구현체 클래스”가 아니라 “인터페이스(계약)”에 의존하게 만들고, 실제 동작은 구현체가 책임지도록 분리합니다.

이렇게 하면 구현을 바꿔도(예: ArrayList → LinkedList), 사용하는 쪽 코드는 거의 바꾸지 않아도 됩니다. 즉, 인터페이스는 결합도를 낮추고, 확장성/테스트 용이성을 높입니다.

 

핵심 메시지

“인터페이스는 코드를 분리하는 문법이 아니라,
변경과 테스트를 설계하는 계약이다.”

- 구현을 숨길수록 시스템은 강해진다 -
왜 백엔드에서 중요한가?
인터페이스는 실무에서 3가지를 해결합니다: 변경, 확장, 테스트
문제 인터페이스가 하는 일 실무 예시
구현 교체 계약 고정, 구현만 변경 Local 저장소 → S3, MySQL → PostgreSQL
기능 확장 다형성으로 확장 결제 벤더 추가, 알림 채널(Email/SMS/Push) 추가
테스트 Fake/Mock 주입 DB 없이 서비스 로직 단위 테스트, 외부 API 호출 없이 시나리오 검증
실전 예시
외부 API 연동(벤더 교체 가능성)이 있는 경우, 인터페이스가 진짜 힘을 발휘합니다.
// 1) 계약: 무엇을 할 수 있는지 정의 interface SmsSender { void send(String phone, String message); } // 2) 구현 A: Vendor1 class Vendor1SmsSender implements SmsSender { public void send(String phone, String message) { // vendor1 API 호출 (생략) } } // 3) 구현 B: Vendor2 (교체/추가 가능) class Vendor2SmsSender implements SmsSender { public void send(String phone, String message) { // vendor2 API 호출 (생략) } } // 4) 상위 계층은 인터페이스에만 의존 class SignUpService { private final SmsSender smsSender; public SignUpService(SmsSender smsSender) { this.smsSender = smsSender; } public void sendVerificationCode(String phone) { String code = "123456"; // 생성 로직 생략 smsSender.send(phone, "[인증코드] " + code); } } 

💡 TIP / 백엔드 실무 포인트

“SmsSender 인터페이스만 만들면 끝”이 아닙니다. 실무에서는 실패 처리 전략이 계약에 같이 들어가야 안정적입니다.
예: 타임아웃/재시도/백오프, 실패 시 예외 타입, 장애 전파 범위(서킷 브레이커), 멱등성 키 등

 

좋은 인터페이스 설계 원칙
인터페이스는 “작고 명확할수록” 유지보수성이 올라갑니다.
1
작은 계약(Interface Segregation) 한 인터페이스가 너무 많은 메서드를 가지면 구현/테스트/교체가 어려워짐
2
의도를 드러내는 이름 Manager/Handler/Util 같은 이름은 계약의 의미를 흐림
3
반환/예외 규약을 포함하기 null 반환인지, 예외인지, 실패 상태를 어떻게 표현할지 계약으로 고정
4
누수가 없도록 만들기(Leaky 방지) 호출자가 내부 구현(DB 스키마/벤더 에러코드)을 알아야 쓰는 계약은 실패

👍 GOOD

  • 작고 명확한 기능 단위로 분리됨
  • 호출자가 실패/예외 규약을 이해하기 쉬움
  • 구현 교체가 자연스럽고 테스트가 쉬움
  • 비즈니스 용어(도메인)가 이름에 반영됨

👎 BAD

  • “만능 인터페이스”로 모든 기능을 몰아넣음
  • 구현체마다 의미가 다른 메서드(계약 불일치)
  • 내부 구현 디테일(테이블/벤더 에러)을 외부가 알아야 함
  • 서비스 코드가 특정 구현체로 캐스팅해서 사용

💡 TIP / “인터페이스를 언제 만들까?”

인터페이스는 “처음부터 무조건”이 아니라, 변경이 실제로 발생하거나 발생 가능성이 큰 지점에 두는 게 효율적입니다.
대표: 외부 연동(Client), 저장소(Repository/Storage), 정책(Policy), 메시징(Publisher), 캐시(CacheStore)

 

구현체에 직접 의존”하면 변경 비용이 폭발합니다.

예를 들어 결제 로직이 특정 PG사의 SDK에 직접 묶여 있다면, PG를 바꾸는 순간 코드 곳곳이 수정됩니다. 반대로 “PaymentGateway” 같은 인터페이스로 감싸면, PG 변경은 구현체 교체로 끝납니다.

 


자바에서의 인터페이스
자바 인터페이스는 계약 + 다형성을 제공합니다.

자바에서 interface는 “메서드 시그니처(무엇을 제공하는지)”를 정의합니다. 구현체 클래스는 이 계약을 반드시 구현해야 하며, 호출자는 구현체가 무엇인지 몰라도 인터페이스 타입으로 동일한 방식으로 사용할 수 있습니다.

 

언제 인터페이스를 만들까?
“바뀔 가능성”과 “대체 가능성”이 기준입니다.

1

외부 의존성이 있는 지점 DB, 메시징, 결제, 이메일/푸시 같은 외부 시스템은 바뀔 수 있으니 인터페이스로 감싼다.
2
테스트에서 대체가 필요한 지점 Mock/Fake 구현체로 빠르게 테스트하고 싶으면 인터페이스가 큰 힘이 된다.
3

여러 구현 전략이 공존하는 지점 예: 캐시 전략(Local/Redis), 저장 전략(File/S3)처럼 전략이 갈릴 때 “전략 인터페이스”가 유리하다.

// Java - 인터페이스(계약) public interface Notifier { void send(String to, String message); } // 구현체 1: 이메일 public class EmailNotifier implements Notifier { @Override public void send(String to, String message) { System.out.println("Email to " + to + ": " + message); } } // 구현체 2: SMS public class SmsNotifier implements Notifier { @Override public void send(String to, String message) { System.out.println("SMS to " + to + ": " + message); } } // 사용하는 쪽(호출자)은 Notifier만 알면 된다. public class AlarmService { private final Notifier notifier; public AlarmService(Notifier notifier) { this.notifier = notifier; } public void alert(String user, String msg) { notifier.send(user, msg); } }

✅ 핵심 요약

  • ✔️ 인터페이스는 구현을 숨기고 계약을 고정해 변경 비용을 줄이는 백엔드 설계 도구다.
  • ✔️ 실무에서는 외부 연동/스토리지/캐시/메시징처럼 “바뀔 수 있는 곳”에서 효과가 크다.
  • ✔️ 좋은 인터페이스는 작고 명확하며, 반환/예외 규약까지 포함해 호출자가 쉽게 사용할 수 있다.
  • ✔️ “너무 이른 추상화”는 오히려 복잡도를 늘릴 수 있으니, 변경 가능성이 큰 지점부터 적용하는 것이 현실적이다.
728x90