본문 바로가기
프로그래밍/Go

Go 인터페이스의 암시적 구현과 Java 인터페이스와의 차이

by JLearn 2026. 7. 15.
반응형

Go와 Java의 인터페이스는 여러 구현체를 공통된 동작으로 다루기 위한 목적은 비슷하지만, 구현 관계를 만드는 방식은 다릅니다.

Java는 클래스가 implements를 사용해 인터페이스 구현을 명시합니다. 반면 Go는 구현 선언 없이 타입의 method set이 인터페이스가 요구하는 메서드를 모두 포함하면 자동으로 해당 인터페이스를 구현합니다.


Go 인터페이스는 method set으로 구현 여부를 판단합니다

다음 인터페이스가 있습니다.

type Printer interface {
    Print() string
}

Printer 인터페이스는 다음 메서드를 요구합니다.

Print() string

다음 구조체 타입은 Printer라는 이름을 직접 사용하지 않습니다.

type Document struct {
    Title string
}

func (d Document) Print() string {
    return d.Title
}

그러나 Document의 method set에 Print() string이 있으므로 Document는 자동으로 Printer 인터페이스를 구현합니다.

var printer Printer = Document{
    Title: "Go 인터페이스",
}

Go에서는 다음 항목으로 인터페이스 구현 여부를 판단하지 않습니다.

인터페이스 이름을 구현 타입이 사용했는가?
구조체 이름과 인터페이스 이름이 비슷한가?
별도의 implements 선언이 있는가?

실제로 확인하는 것은 메서드 집합입니다.

메서드 이름
매개변수 개수와 타입
반환값 개수와 타입
variadic 여부

메서드 이름만 같고 시그니처가 다르면 인터페이스를 구현하지 않습니다.

type Printer interface {
    Print() string
}

다음 메서드는 Printer를 구현합니다.

func (d Document) Print() string {
    return d.Title
}

다음 메서드는 매개변수가 다르므로 구현하지 않습니다.

func (d Document) Print(prefix string) string {
    return prefix + d.Title
}

다음 메서드는 반환 타입이 다르므로 구현하지 않습니다.

func (d Document) Print() error {
    return nil
}

매개변수 이름은 시그니처에 포함되지 않습니다.

type Writer interface {
    Write(data []byte) error
}
func (f File) Write(value []byte) error {
    return nil
}

인터페이스에서는 매개변수 이름이 data, 구현 메서드에서는 value이지만 타입이 같으므로 구현 관계가 성립합니다.


인터페이스를 구현하면 가능한 것

인터페이스를 구현한다고 해서 구조체 내부가 변경되거나 자동으로 코드가 추가되는 것은 아닙니다.

정확한 의미는 다음과 같습니다.

해당 타입의 값을 인터페이스 타입이 필요한 위치에 사용할 수 있다.

예를 들어 DocumentPrinter를 구현하면 인터페이스 변수에 대입할 수 있습니다.

document := Document{
    Title: "Go 문서",
}

var printer Printer = document

인터페이스를 매개변수로 받는 함수에도 전달할 수 있습니다.

func PrintContent(printer Printer) {
    fmt.Println(printer.Print())
}
PrintContent(document)

구조체 필드에도 저장할 수 있습니다.

type ReportService struct {
    printer Printer
}

생성자 매개변수로도 사용할 수 있습니다.

func NewReportService(printer Printer) *ReportService {
    return &ReportService{
        printer: printer,
    }
}

인터페이스 구현으로 가능한 일을 정리하면 다음과 같습니다.

구현 타입의 값을 인터페이스 변수에 대입할 수 있음
구현 타입의 값을 인터페이스 매개변수에 전달할 수 있음
인터페이스 타입 필드에 구현 타입의 값을 저장할 수 있음
여러 구현 타입을 하나의 인터페이스로 동일하게 다룰 수 있음

인터페이스 이름이 필요한 이유

인터페이스 이름은 구현을 등록하거나 상속 관계를 표시하기 위한 이름이 아닙니다.

type Printer interface {
    Print() string
}

Printer는 다음 역할을 표현하는 타입 이름입니다.

Print() string 메서드를 제공하는 값

인터페이스를 사용하는 코드는 이 이름을 통해 자신이 필요한 동작을 선언합니다.

func PrintContent(printer Printer) {
    fmt.Println(printer.Print())
}

이 함수는 Document라는 구체 타입 전체가 필요한 것이 아닙니다.

필요한 것
└── Print() string

알 필요가 없는 것
├── 구체 타입 이름
├── 구조체 필드
└── 그 타입이 가진 다른 메서드

이름 없는 인터페이스를 직접 사용할 수도 있습니다.

func PrintContent(printer interface {
    Print() string
}) {
    fmt.Println(printer.Print())
}

기술적으로는 같은 요구사항을 표현하지만, 이름 있는 인터페이스를 사용하면 같은 메서드 집합을 반복하지 않아도 되고 코드의 역할을 더 명확하게 표현할 수 있습니다.

type Printer interface {
    Print() string
}
func PrintContent(printer Printer)
func StorePreview(printer Printer)
func SendPreview(printer Printer)

인터페이스 이름은 다음 목적을 가집니다.

필요한 메서드 집합을 하나의 타입으로 표현
함수와 구조체가 요구하는 역할을 설명
같은 인터페이스 정의의 반복 제거
여러 구현체를 동일한 방식으로 사용
구현체와 사용하는 코드의 결합 감소

Reader, Writer, Closer, Saver처럼 하나의 동작을 표현하는 인터페이스에는 메서드 이름에 -er를 붙인 형태가 자주 사용됩니다.

type Saver interface {
    Save() error
}
type Closer interface {
    Close() error
}

인터페이스 이름은 구현 여부를 결정하지는 않지만, 코드에서 요구하는 역할과 의도를 표현합니다.


구조체 타입과 인터페이스 타입으로 사용할 때의 차이

다음 Document 타입에는 두 메서드가 있습니다.

type Document struct {
    Title string
}

func (d Document) Print() string {
    return d.Title
}

func (d Document) Save() error {
    fmt.Println("저장:", d.Title)
    return nil
}

구조체 타입 그대로 사용하면 Document가 가진 메서드를 모두 호출할 수 있습니다.

document := Document{
    Title: "Go 문서",
}

fmt.Println(document.Print())
document.Save()

이때 document의 정적 타입은 Document입니다.

Document
├── Print()
└── Save()

인터페이스 타입에 대입하면 인터페이스가 정의한 메서드만 직접 사용할 수 있습니다.

type Printer interface {
    Print() string
}
var printer Printer = document

fmt.Println(printer.Print())

다음 호출은 컴파일되지 않습니다.

printer.Save()

실제 값은 Document이지만 printer 변수의 정적 타입은 Printer이므로 Printer에 정의된 메서드만 직접 호출할 수 있습니다.

printer의 정적 타입 → Printer
printer의 동적 타입 → Document
printer의 동적 값   → Document{Title: "Go 문서"}

printer.Print()를 호출하면 실제로는 동적 타입인 Document의 메서드가 실행됩니다.

func (d Document) Print() string {
    return d.Title
}

인터페이스에 대입해도 원래 document 변수의 타입이 변경되는 것은 아닙니다.

document.Save()
printer.Print()

하나의 값을 서로 다른 타입의 관점에서 사용하는 것입니다.

document → Document가 제공하는 전체 기능으로 사용
printer  → Printer가 요구하는 기능으로만 사용

인터페이스를 통해 Save()도 호출해야 한다면 해당 메서드를 포함하는 인터페이스가 필요합니다.

type PrinterSaver interface {
    Print() string
    Save() error
}
var value PrinterSaver = document

value.Print()
value.Save()

작은 인터페이스를 조합할 수도 있습니다.

type Printer interface {
    Print() string
}

type Saver interface {
    Save() error
}

type PrinterSaver interface {
    Printer
    Saver
}

여러 타입과 여러 인터페이스의 관계

하나의 타입은 여러 인터페이스를 동시에 구현할 수 있습니다.

type Printer interface {
    Print() string
}

type Saver interface {
    Save() error
}
type File struct {
    Name string
}

func (f File) Print() string {
    return f.Name
}

func (f File) Save() error {
    fmt.Println("파일 저장:", f.Name)
    return nil
}

File은 두 인터페이스 이름을 구현 선언에 사용하지 않았지만 PrinterSaver를 모두 구현합니다.

file := File{Name: "report.txt"}

var printer Printer = file
var saver Saver = file

인터페이스 타입에 따라 사용할 수 있는 메서드는 달라집니다.

fmt.Println(printer.Print())
saver.Save()

다음 호출은 컴파일되지 않습니다.

printer.Save()

printer의 동적 타입은 File이지만 정적 타입은 Printer이기 때문입니다.

여러 타입이 하나의 인터페이스를 구현할 수도 있습니다.

type Document struct {
    Title string
}

func (d Document) Print() string {
    return "문서: " + d.Title
}
type Image struct {
    Filename string
}

func (i Image) Print() string {
    return "이미지: " + i.Filename
}

두 타입 모두 Printer를 구현합니다.

func PrintContent(printer Printer) {
    fmt.Println(printer.Print())
}
PrintContent(Document{Title: "설계서"})
PrintContent(Image{Filename: "diagram.png"})

출력:

문서: 설계서
이미지: diagram.png

PrintContent 함수는 DocumentImage의 구체적인 구조를 알 필요가 없습니다. Print() string 메서드를 제공한다는 사실만 알면 됩니다.


pointer receiver와 인터페이스 구현

인터페이스 구현은 타입의 method set을 기준으로 판단합니다.

type Saver interface {
    Save() error
}

다음 메서드는 pointer receiver로 선언되어 있습니다.

type File struct {
    Name string
}

func (f *File) Save() error {
    fmt.Println("저장:", f.Name)
    return nil
}

이 경우 *FileSaver를 구현하지만 File 값은 구현하지 않습니다.

file := File{Name: "report.txt"}

var saver Saver = &file

다음 대입은 컴파일되지 않습니다.

var saver Saver = file

method set은 다음처럼 구분됩니다.

File의 method set
→ receiver가 File인 메서드

*File의 method set
→ receiver가 File 또는 *File인 메서드

메서드 직접 호출에서는 주소를 구할 수 있는 변수에 대해 자동 주소 변환이 적용될 수 있습니다.

file.Save()

이 호출은 가능하지만, 인터페이스 대입은 method set을 그대로 기준으로 판단하므로 File 값이 Saver를 구현하는 것은 아닙니다.

메서드를 직접 호출할 수 있다는 사실과 해당 값 타입이 인터페이스를 구현한다는 사실은 구분해야 합니다.


인터페이스 이름이 달라도 메서드가 같으면 구현할 수 있습니다

다음 두 인터페이스는 이름은 다르지만 같은 메서드를 요구합니다.

type Printer interface {
    Print() string
}

type Renderer interface {
    Print() string
}
type Document struct {
    Title string
}

func (d Document) Print() string {
    return d.Title
}

Document는 두 인터페이스를 모두 구현합니다.

document := Document{
    Title: "Go",
}

var printer Printer = document
var renderer Renderer = document

구현 여부는 인터페이스 이름이 아니라 메서드 집합으로 결정되기 때문입니다.

Document가 Printer를 구현하는 이유
→ Print() string 메서드가 있기 때문

Document가 Renderer를 구현하는 이유
→ Print() string 메서드가 있기 때문

다만 인터페이스 이름은 코드에서 서로 다른 역할과 설계 의도를 표현할 수 있습니다.

현재 메서드 집합이 같더라도 이후 한쪽 인터페이스에 다른 메서드가 추가되면 요구사항은 달라질 수 있습니다.


인터페이스는 사용하는 쪽에서 정의할 수 있습니다

Go의 암시적 구현에서는 기존 타입을 수정하지 않고도 새로운 인터페이스를 정의할 수 있습니다.

먼저 기존 타입이 있다고 가정합니다.

type Console struct{}

func (Console) Write(message string) error {
    fmt.Println(message)
    return nil
}

나중에 사용하는 코드에서 인터페이스를 정의합니다.

type MessageWriter interface {
    Write(string) error
}

Console 타입을 수정하지 않아도 MessageWriter를 구현합니다.

func SendMessage(writer MessageWriter, message string) error {
    return writer.Write(message)
}
SendMessage(Console{}, "작업 완료")

Go에서는 구현체가 큰 인터페이스를 미리 정의하기보다, 사용하는 쪽에서 실제로 필요한 작은 인터페이스를 정의하는 방식이 자주 사용됩니다.

type ProductSaver interface {
    Save(Product) error
}
type ProductService struct {
    saver ProductSaver
}

저장소 구현은 이 인터페이스를 직접 알지 못해도 됩니다.

type PostgreSQLRepository struct{}

func (r *PostgreSQLRepository) Save(product Product) error {
    return nil
}

*PostgreSQLRepository의 method set이 ProductSaver의 요구사항을 만족하므로 자동으로 구현합니다.


구현 여부를 컴파일 시점에 확인하는 방법

Go는 별도의 인터페이스 구현 선언을 요구하지 않습니다.

필요하면 다음 패턴으로 구현 여부를 명시적으로 확인할 수 있습니다.

value receiver를 사용하는 경우:

var _ Printer = Document{}

pointer receiver를 사용하는 경우:

var _ Saver = (*File)(nil)

예:

type Saver interface {
    Save() error
}

type File struct{}

func (f *File) Save() error {
    return nil
}

var _ Saver = (*File)(nil)

이 코드는 실제 값을 사용하기 위한 선언이 아닙니다.

*File 타입의 값을 Saver 타입에 대입할 수 있는지 컴파일러가 검사하라.

필요한 메서드가 누락되면 컴파일 오류가 발생합니다.

이 방식은 구현 관계를 코드에 표시하거나 인터페이스 변경에 따른 오류를 빠르게 확인할 때 사용할 수 있지만 필수 문법은 아닙니다.


Java와 Go 인터페이스의 차이

Java는 구현 관계를 명시합니다

Java에서는 클래스가 인터페이스를 구현할 때 implements를 사용합니다.

interface Printer {
    String print();
}
class Document implements Printer {

    @Override
    public String print() {
        return "문서 출력";
    }
}

Document는 선언을 통해 Printer를 구현한다고 명시합니다.

Java 컴파일러는 implements Printer를 확인하고 인터페이스의 추상 메서드를 모두 구현했는지 검사합니다.

class Document implements Printer {
    // print() 미구현
}

일반 클래스가 인터페이스 메서드를 구현하지 않으면 컴파일 오류가 발생합니다.

미구현 메서드를 남기려면 클래스를 abstract로 선언해야 합니다.

abstract class Document implements Printer {
}

추상 클래스는 직접 객체를 만들 수 없습니다.

Java에서는 같은 메서드를 가지고 있더라도 implements 관계가 없으면 인터페이스 타입으로 사용할 수 없습니다.

class Image {
    public String print() {
        return "image";
    }
}
Printer printer = new Image(); // 컴파일 오류

Java는 타입 이름과 명시적으로 선언된 관계가 중요합니다.

Go는 구현 관계를 선언하지 않습니다

Go에는 implements@Override가 없습니다.

type Printer interface {
    Print() string
}
type Document struct {
    Title string
}

func (d Document) Print() string {
    return d.Title
}

DocumentPrinter를 직접 언급하지 않았지만 method set이 요구사항을 만족하므로 자동으로 구현합니다.

var printer Printer = Document{
    Title: "Go 문서",
}

Go에서는 다음과 같이 판단합니다.

Document가 Printer를 구현한다고 선언했는가?
→ 확인하지 않음

Document의 method set이 Printer의 요구사항을 만족하는가?
→ 만족하면 구현

Go에서는 인터페이스 메서드를 구현하지 않은 구조체 값도 생성할 수 있습니다.

type Document struct {
    Title string
}

document := Document{
    Title: "Go 문서",
}

DocumentPrinter를 구현하지 않더라도 구조체 정의와 값 생성에는 문제가 없습니다.

다만 인터페이스 타입으로 사용하려는 위치에서는 컴파일 오류가 발생합니다.

var printer Printer = document

DocumentPrint() string 메서드가 없다면 이 대입은 허용되지 않습니다.

검사 시점의 차이는 다음과 같습니다.

Java
클래스 선언
→ implements 관계 확인
→ 메서드 구현 여부 검사
→ 미구현 일반 클래스는 컴파일 오류

Go
struct 정의 및 값 생성
→ 인터페이스와 독립적

인터페이스 변수 대입 또는 함수 전달
→ method set 검사
→ 만족하지 않으면 컴파일 오류

인터페이스 타입에서의 메서드 제한은 비슷합니다

Java:

Document document = new Document();

document.print();
document.save();

Printer printer = document;

printer.print();
// printer.save(); // 컴파일 오류

Go:

document := Document{}

document.Print()
document.Save()

var printer Printer = document

printer.Print()
// printer.Save() // 컴파일 오류

두 언어 모두 인터페이스 타입 변수에서는 인터페이스가 정의한 메서드만 직접 호출할 수 있습니다.

차이는 구현 관계를 만드는 방식입니다.

구분 Java Go
구현 선언 implements 필요 선언하지 않음
구현 판단 명시된 타입 관계 method set
메서드 누락 일반 클래스 선언에서 오류 인터페이스 대입·전달 시 오류
구현 메서드 표시 @Override 사용 별도 표시 없음
기존 타입 수정 일반적으로 implements 추가 필요 수정 없이 인터페이스 만족 가능
인터페이스 타입 호출 범위 인터페이스 메서드만 인터페이스 메서드만

Go에서는 인터페이스 메서드를 재정의한다고 표현하기보다 다음 표현이 더 정확합니다.

타입의 method set이 인터페이스를 만족한다.

정리

Go 인터페이스는 구현 타입이 인터페이스 이름을 선언하지 않아도 됩니다.

type Printer interface {
    Print() string
}
type Document struct {
    Title string
}

func (d Document) Print() string {
    return d.Title
}

Document의 method set이 Printer가 요구하는 메서드를 포함하므로 자동으로 구현합니다.

인터페이스를 구현하면 해당 타입의 값을 인터페이스 변수, 함수 매개변수, 구조체 필드 등에 사용할 수 있습니다.

var printer Printer = Document{
    Title: "Go 문서",
}

인터페이스 타입으로 대입하면 해당 인터페이스에 정의된 메서드만 직접 사용할 수 있습니다.

document.Print()
document.Save()

var printer Printer = document

printer.Print()
// printer.Save() // 호출 불가

인터페이스 이름은 구현을 등록하는 이름이 아니라 필요한 메서드 집합과 코드가 요구하는 역할을 표현하는 타입 이름입니다.

구현을 등록하는 이름       → 아님
상속 관계를 나타내는 이름   → 아님
필요한 메서드 집합의 타입명 → 맞음
코드가 요구하는 역할의 이름 → 맞음

Java와 Go의 핵심 차이는 다음과 같습니다.

Java
→ implements를 통해 구현 관계를 명시

Go
→ 타입의 method set이 인터페이스를 만족하면 암시적으로 구현

Go 인터페이스를 이해할 때는 다음 내용을 기억하면 됩니다.

인터페이스 이름은 구현 여부를 결정하지 않습니다.
인터페이스 구현 여부는 method set으로 결정됩니다.
인터페이스 타입에서는 정의된 메서드만 직접 사용할 수 있습니다.
실제 메서드 실행은 인터페이스에 저장된 동적 타입이 담당합니다.

참고 자료

반응형

'프로그래밍 > Go' 카테고리의 다른 글

Goroutine과 channel  (1) 2026.07.18
Go embedding과 메서드 승격 이해하기  (0) 2026.07.15
Go struct와 receiver  (0) 2026.07.15
Go struct, method, interface, enum 패턴과 embedding  (0) 2026.07.15
Go 포인터, string, rune  (0) 2026.07.14

댓글