iOS & Swift

메서드 시그니처(Method Signature) 정확히 알기, 반환 타입은 포함될까 (Java·Swift 비교)

"메서드 시그니처가 뭔가요?"라는 질문을 받으면 대부분 "메서드의 이름이랑 파라미터... 그런 거요"라고 얼버무리게 됩니다.

이석우iOS Developer7분 읽기
메서드 시그니처(Method Signature) 정확히 알기, 반환 타입은 포함될까 (Java·Swift 비교) 대표 이미지

“메서드 시그니처가 뭔가요?“라는 질문을 받으면 대부분 “메서드의 이름이랑 파라미터… 그런 거요”라고 얼버무리게 됩니다.

틀린 말은 아닌데, 한 발짝만 더 들어가면 바로 갈립니다. “그럼 반환 타입은 시그니처에 포함되나요?” 여기서부터 답이 흔들리기 시작해요. 실제로 이 질문은 기술 면접 단골이기도 하고 오버로딩이 왜 되고 안 되는지를 가르는 기준이기도 합니다.

더 재미있는 건, 이 질문의 정답이 언어마다 다르다는 점입니다. Java에서는 반환 타입이 시그니처에 들어가지 않지만 Swift에서는 반환 타입만 달라도 오버로딩이 됩니다.

이 글에서 시그니처의 정확한 정의부터, 언어별 차이, 그리고 실무에서 시그니처가 왜 중요한지까지 순서대로 정리하겠습니다.

METHOD SIGNATURE 텍스트와 함수 신분증 일러스트 — 메서드 이름 calculateTotal과 파라미터 칩, 지문 워터마크가 그려진 대표 이미지
메서드 시그니처는 함수의 신분증입니다 — 이름과 파라미터가 핵심 식별 정보예요

시그니처는 메서드의 신분증

메서드 시그니처(method signature)는 한마디로 컴파일러가 이 메서드를 다른 메서드와 구별하는 데 쓰는 식별 정보입니다.

사람으로 치면 신분증 같은 거예요. 이름만으로는 동명이인을 구별할 수 없으니, 신분증에는 이름 외에 생년월일 같은 추가 정보가 들어갑니다. 메서드도 마찬가지입니다. 같은 이름의 메서드가 여러 개 있을 수 있으니(오버로딩), 이름 외의 정보로 정확히 하나를 특정해야 합니다.

일반적으로 시그니처를 구성하는 요소는 이렇습니다.

  • 메서드 이름
  • 파라미터의 타입
  • 파라미터의 개수
  • 파라미터의 순서
// 이 네 메서드는 전부 시그니처가 다릅니다
void print(int value)              // print(int)
void print(String value)           // print(String) — 타입이 다름
void print(int a, int b)           // print(int, int) — 개수가 다름
void print(String s, int n)        // print(String, int)
void print(int n, String s)        // print(int, String) — 순서가 다름

주의할 점이 하나 있는데, 파라미터의 이름은 시그니처가 아닙니다. print(int value)print(int number)는 같은 시그니처입니다. 컴파일러 입장에서 호출부의 print(3)만 보고 구별할 방법이 없으니까요.

접근 제어자(public, private), static, final, 예외 선언(throws)도 시그니처에 포함되지 않습니다. 시그니처는 어디까지나 “어느 메서드를 호출할지 결정하는 데 필요한 정보”만 담습니다.


그래서, 반환 타입은 포함될까

Java 기준으로 답하면 포함되지 않습니다. Java 언어 명세(JLS 8.4.2)는 시그니처를 “메서드 이름과 인자 타입”으로 정의합니다.

그래서 반환 타입만 다른 오버로딩은 컴파일 에러입니다.

int parse(String input) { ... }

// 컴파일 에러: 'parse(String)' is already defined
String parse(String input) { ... }

왜 이렇게 설계했는지는 호출부를 보면 이해가 됩니다.

parse("42");  // 반환값을 안 받으면? 어느 parse인지 결정 불가능

Java에서는 반환값을 무시하고 메서드를 호출하는 게 언제나 허용됩니다. 이 경우 컴파일러가 두 parse 중 어느 쪽을 부를지 결정할 근거가 없어요. 그래서 아예 시그니처에서 반환 타입을 빼 버린 겁니다. C++도 같은 이유로 반환 타입만 다른 오버로딩을 금지합니다.

한 가지 알아 두면 좋은 디테일이 있습니다. JVM 바이트코드 레벨에서는 반환 타입도 메서드를 구별하는 정보에 포함됩니다. 클래스 파일 안에서 메서드는 parse(Ljava/lang/String;)I 같은 디스크립터로 식별되는데, 마지막의 I가 반환 타입(int)이에요. 즉 “반환 타입은 시그니처가 아니다”는 Java라는 언어의 규칙이지, JVM이라는 실행 환경의 규칙은 아닙니다. 제네릭의 브리지 메서드 같은 마법이 가능한 것도 이 틈 덕분입니다.

메서드 시그니처 구성 요소 다이어그램 — 이름·파라미터 타입·개수·순서는 Java·Swift 공통 포함, 인자 레이블과 반환 타입은 Swift만 포함
같은 질문도 Java와 Swift의 답이 갈립니다 — 주황색 두 칸이 언어별 차이예요

Swift는 답이 다릅니다

같은 질문을 Swift에 던지면 답이 뒤집힙니다. Swift에서는 반환 타입만 달라도 오버로딩이 됩니다.

func random() -> Int { Int.random(in: 0...100) }
func random() -> Double { Double.random(in: 0...1) }

let n: Int = random()     // Int 버전 호출
let x: Double = random()  // Double 버전 호출

Swift 컴파일러는 호출 결과가 어떤 타입으로 쓰이는지(타입 문맥)까지 보고 오버로딩을 해소하기 때문입니다. 대신 문맥이 없으면 에러가 납니다.

let value = random()  // 컴파일 에러: ambiguous use of 'random()'

Swift의 시그니처에는 또 하나 독특한 요소가 있습니다. 인자 레이블(argument label)이 함수 이름의 일부라는 점입니다.

func move(from start: Point, to end: Point) { ... }
func move(in direction: Direction) { ... }

이 두 함수의 정식 이름은 move가 아니라 각각 move(from:to:)move(in:)입니다. 파라미터 타입이 같아도 레이블이 다르면 다른 함수예요. Java에서는 파라미터 이름이 시그니처에서 완전히 빠지는 것과 정확히 반대 방향의 설계입니다.

정리하면 이렇습니다.

구성 요소 Java Swift
메서드 이름 포함 포함
파라미터 타입·개수·순서 포함 포함
파라미터 이름(레이블) 미포함 포함 (인자 레이블)
반환 타입 미포함 포함 (문맥으로 해소)

“시그니처에 반환 타입이 포함되나요?“라는 질문에 “언어에 따라 다른데, Java는 아니고 Swift는 맞습니다”까지 답할 수 있으면 이 개념을 제대로 이해한 겁니다.


시그니처가 실무에서 중요한 세 장면

정의만 알고 끝나면 시험용 지식입니다. 시그니처가 실제로 일하는 장면은 따로 있습니다.

장면 1 — 오버로딩과 오버라이딩의 판별 기준

오버로딩은 “같은 이름, 다른 시그니처”이고, 오버라이딩은 “부모와 같은 시그니처를 다시 구현”입니다. 두 개념 모두 시그니처 없이는 정의 자체가 안 됩니다.

오버라이딩할 때 시그니처를 미세하게 틀리면, 컴파일러는 오버라이딩이 아니라 새로운 오버로딩으로 해석합니다. 부모 메서드는 멀쩡히 살아 있고 내 메서드는 호출되지 않는 조용한 버그가 되죠. Java의 @Override, Swift의 override 키워드가 존재하는 이유가 바로 이 사고를 컴파일 타임에 잡기 위해서입니다.

장면 2 — 인터페이스·프로토콜 채택

인터페이스(프로토콜)를 구현한다는 건 결국 요구된 시그니처와 정확히 일치하는 메서드를 제공한다는 뜻입니다.

Swift에서 델리게이트 메서드를 구현했는데 호출이 안 되는 경우, 상당수가 시그니처 불일치입니다. 파라미터 타입이 옵셔널이냐 아니냐, 레이블이 forat이냐 같은 한 끗 차이로 프로토콜 요구사항과 다른 별개의 메서드가 되어 버리는 거예요. 컴파일러가 잡아 주는 필수 요구사항과 달리, 옵셔널 요구사항은 조용히 무시되기 때문에 더 찾기 어렵습니다.

장면 3 — 시그니처 변경은 곧 호환성 파괴

라이브러리나 팀 공용 모듈을 만들 때, 공개 메서드의 시그니처는 외부와 맺은 계약입니다.

파라미터를 하나 추가하거나, 타입을 Int에서 Int64로 바꾸거나, Swift에서 인자 레이블 하나만 고쳐도 그 메서드를 호출하던 모든 코드가 깨집니다. 이게 바로 breaking change(호환성 파괴 변경)이고, 시맨틱 버저닝에서 메이저 버전을 올려야 하는 대표 사유입니다.

그래서 성숙한 라이브러리들은 시그니처를 바꾸는 대신 새 시그니처의 메서드를 추가하고 기존 것을 deprecated로 유지하는 전략을 씁니다. 기존 시그니처라는 계약을 일방적으로 파기하지 않는 거예요.

공개 API 계약서를 수정하자 코드 다리가 부서지는 breaking change 일러스트 — 메서드 시그니처 변경의 호환성 파괴
공개 시그니처에 펜을 대는 순간, 그 메서드를 부르던 다리가 무너집니다

시그니처를 설계할 때 기억할 것

시그니처가 계약이라는 관점에 서면, 설계 기준도 명확해집니다.

한 번 공개한 시그니처는 바꾸기 어렵다는 전제로 설계하세요. 내부 메서드는 마음껏 고쳐도 되지만 public API의 시그니처는 첫 공개 전에 가장 많이 고민해야 합니다. 나중에 고치는 비용이 수십 배로 뜁니다.

파라미터가 늘어날 조짐이 보이면 타입으로 묶으세요. 파라미터 4~5개짜리 시그니처는 순서 실수를 부르고 하나 추가할 때마다 breaking change가 됩니다. 설정 객체나 구조체 하나로 묶으면 시그니처를 유지한 채 확장할 수 있습니다.

같은 타입의 파라미터가 연달아 오면 위험 신호입니다. transfer(account1, account2)에서 어느 쪽이 출금 계좌인지 시그니처만 보고 알 수 없다면, Swift라면 transfer(from:to:)처럼 레이블로, Java라면 파라미터를 의미 있는 타입으로 감싸서 시그니처 자체가 용법을 설명하게 만드는 편이 좋습니다.


정리

  • 메서드 시그니처는 컴파일러가 메서드를 구별하는 식별 정보로, 이름 + 파라미터의 타입·개수·순서가 공통 핵심입니다.
  • 반환 타입 포함 여부는 언어마다 다릅니다. Java·C++은 미포함(반환 타입만 다른 오버로딩 불가), Swift는 포함(타입 문맥으로 해소)이고, Swift는 인자 레이블도 함수 이름의 일부입니다.
  • 시그니처는 오버로딩·오버라이딩의 판별 기준이자, 인터페이스 구현의 일치 조건이며, 공개 API에서는 외부와 맺은 계약입니다.
  • 공개 시그니처 변경은 breaking change입니다. 바꾸는 대신 추가하고, 처음 설계할 때 가장 공들이세요.

함께 보면 좋은 글