Go언어 functions 와 methods 의 차이 (번역)

hyomin·2020년 1월 2일
3

golang-grammer

목록 보기
2/3
post-thumbnail

이 포스트는 저의 이해를 돕기 위해 Golang functions vs methods을 번역한 것입니다. (오역이 있을 수 있습니다.)

개요

이 포스트는 Go의 함수(function)와 메소드(method)의 차이와 언제 구분해서 쓰는게 가장 바람직한지를 설명합니다.

함수와 메소드는 둘다 Go에서 광범위하게 사용되는데, 추상화를 제공해주며 우리가 만든 프로그램을 더 읽기 쉽게 그리고 추론하기 쉽게해줍니다. 겉으로 보기에 함수와 메소드는 둘다 비슷해 보이지만, 우리의 코드가 더 읽기 쉬워지게 해주는 의미있는 중요한 차이점들이 있습니다.

Syntax

Declaration syntax

함수는 인수 타입, 리턴값, 그리고 바디를 명시함으로써 선언할 수 있습니다:

type Person struct {
  Name string
  Age int
}

// 이 함수는 `Person`이라는 새로운 인스턴스를 반환합니다.
func NewPerson(name string, age int) *Person {
  return &Person{
    Name: name,
    Age: age,
  }
}

한편, 메소드는 추가적으로 리시버receiver(객체지향에서는 메소드를 일부분으로 가지는 "클래스"가 여기에 해당할 수 있습니다.)를 명시함으로써 선언할 수 있습니다:

// 포인터 타입`Person`은 `isAdult`메소드의 리시버입니다.
func (p *Person) isAdult() bool {
  return p.Age > 18
}

위 메소드 선언에서, 우리는 *Person타입에(on the *Person type.) isAdult메소드를 선언했습니다.

Execution syntax

함수는 인수를 명시함으로써 독립적으로 호출되며, 메소드는 그들의 리시버 타입에 따라 호출됩니다.
(Functions are called independently with the arguments specified, and methods are called on the type of their receiver:)

p := NewPerson("John", 21)

fmt.Println(p.isAdult())
// true

Interchangeability(교체가능성)

함수와 메소드는 이론상으로는 교체될 수 있습니다. 예를 들어, 우리는 isAdult메소드를 함수로 바꿀 수 있으며, NewPerson함수를 메소드로 가질 수 있습니다:

type PersonFactory struct {}

// `NewPerson`는 이제 `PersonFactory` 구조체(struct)의 메소드입니다. 
func (p *PersonFactory) NewPerson(name string, age int) *Person {
  return &Person{
    Name: name,
    Age: age,
  }
}

// `isAdult`는 이제 인수로 `Person`을 전달하는 함수입니다. 
// 리시버 대신에
func isAdult(p *Person) bool {
  return p.Age > 18
}

이 경우에 실행할 때의 문법은 다소 위화감이 있습니다:

factory := &PersonFactory{}

p := factory.NewPerson("John", 21)

fmt.Println(isAdult(p))
// true

위 코드는 필요 이상으로 불필요하고 복잡해 보입니다. 이를 통해 우리는 메소드와 함수의 차이가 주로 문법적인것임을 알 수 있고, 우리는 용도(use case)에 맞게 적절한 추상화기법을 사용하는게 좋을 것입니다.

Use cases

자 이제 Go 어플리케이션에서 맞닥뜨릴 수 있는 흔한 유스케이스와 각 케이스에 사용되는 적절한 추상화(함수 또는 메소드)를 살펴봅시다:

Method chaining

한 가지 아주 유용한 메소드의 속성은 우리의 코드를 클린하게 유지하면서도 메소드들을 같이 묶을 수 있다는 것입니다(the ability to chain them together). Person의 몇몇 속성을 체이닝을 사용하여 세팅하는 예를 봅시다:

type Person struct {
	Name string
	Age  int
}

func (p *Person) withName(name string) *Person {
	p.Name = name
	return p
}

func (p *Person) withAge(age int) *Person {
	p.Age = age
	return p
}

func main() {
	p := &Person{}

	p = p.withName("John").withAge(21)
	
  fmt.Println(*p)
  // {John 21}
}

만약 우리가 함수로 같은 걸 하려했다면, 꽤 끔찍하게 보였을 것입니다:

p = withName(withAge(p, 18), "John")

Stateful vs stateless execution

상태 실행 vs 무상태 실행

서로 교체가능함을 보여줬던 예제에서, 우리는 person의 새로운 인스턴스를 생성하기 위에 PersonFactory오브젝트를 사용하는 것을 보았습니다. 알고 보면, 이는 안티패턴(anti-pattern)이며 피해야 할 방법입니다.

무상태 실행을 위해, NewPerson함수와 같은 함수를 먼저 선언하는 것이 훨씬 좋습니다.

“Stateless” here means any piece of code that always returns the same output for the same input (여기서 말하는 "무상태"란 어떤 코드도 언제나 같은 인풋에 대해 같은 아웃풋을 반환한다는 것을 의미한다.)

여기서 당연하게 귀결되는 것은, 만약 어떤 함수가 특정 타입에 대해 많은 값들을 읽고 수정한다면, 그 함수는 아마 그 타입에 대해 메소드이어야할 수도 있습니다.

Semantics

시멘틱이란 코드를 읽는 방법에 관한 것입니다. 만약 당신이 코드를 언어로 소리내어 읽는다고 쳤을 때, 어느 쪽이 더 이해하기 쉽나요?

isAdult를 함수와 메소드로 실행하는 것을 살펴봅시다.

customer := NewPerson("John", 21)

// Method
customer.isAdult()

// Function
isAdult(customer)

여기 customer.isAdult()isAdult(customer)와 비교하면 "Is the customer an adult?"라는 질문에 훨씬 더 읽기 좋다는 것을 알 수 있습니다. 게다가 “is x and adult?”라고 당신이 물었을 때, 그것은 문맥상 항상 person x에 대한 질문으로 받아들이게 될 것입니다.

결론

비록 우리가 Go에서의 함수와 메소드의 주된 차이점들과 유스케이스에 대해 알아봤지만, 항상 예외는 존재합니다! 그 어떤 규칙도 확정해서 받아들이지 않는 것은 중요합니다.

끝으로, 함수와 메소드의 차이는 코드를 읽을 때 어떤 결과를 초래하는 지에 있습니다. 만약 당신이나 다인 팀이 더 읽기 편하다고 느끼는 어떤 방법이 있다면, 그 방법이 옳은 추상화방식입니다!

번역 후기

좋은 포스트를 발견하여 나도 이해하고 싶어서 처음으로 번역을 해봤는데, 도움이 많이 된 것 같다.

0개의 댓글