공부하고 기록하는, 경제학과 출신 개발자의 노트

학습일지/Language

Writing Beautiful Package in Go

inspirit941 2022. 5. 29. 20:30
반응형

Golang UK Conference 2017 발표.

https://youtu.be/cmkKxNN7cs4

  • go로 개발한 오픈소스 패키지가 여러 사람들에게 유용하고 쉽게 쓰이려면 어떻게 해야 하는지를 설명한 강연

 

스크린샷 2022-05-25 오후 1 27 17

  • package는 go파일 (_test.go 포함) 로 구성된 하나의 디렉토리.
  • 다른 프로젝트에서 import해서 사용할 수 있음
    • exported / internal 두 종류가 있음
  • main 패키지 말고. main 패키지는 command를 의미함

 

스크린샷 2022-05-25 오후 1 49 50

 

스크린샷 2022-05-25 오후 1 38 58

user-centred Design

  • 결국 사람이 쓰는 거니까, 개발하려는 프로덕트의 최종 사용자의 요구사항, 제한조건을 고려해서 설계해야 한다.

 

스크린샷 2022-05-25 오후 1 35 14

 

스크린샷 2022-05-25 오후 1 39 30

  • 따라서 고민해야 할 점
    • 누가 쓸 건지
    • 하려는 건 무엇인지
    • 왜 하려는 건지
    • 굳이 내 패키지를 쓰려는 이유는 뭔지
    • PoC 수준일지? Bigger System일지?

 

스크린샷 2022-05-25 오후 1 58 48

 

스크린샷 2022-05-25 오후 2 00 31

 

스크린샷 2022-05-25 오후 2 01 02스크린샷 2022-05-25 오후 4 10 07

Single Method Interface

  • 인터페이스의 목적은, 사람들이 그걸 구현하길 바라는 것.
    • 구현하기 쉬우려면, 인터페이스에서 요구하는 메소드의 개수는 최소한이어야 한다.

 

스크린샷 2022-05-25 오후 4 13 50

  • 예컨대 function Adaptor 형태로 사용자가 인터페이스를 구현할 수 있음.
    • 이런 형태는 인터페이스에 정의된 메소드가 단 하나만 있을 때 가능하다.
  • type struct를 정의하는 대신 function으로 정의. More Simpler / versatile한 활용이 가능함.

 

스크린샷 2022-05-25 오후 4 16 23

  • 위 예시는, http 요청을 받아서 응답으로 status code를 리턴하는 구조.
    • NotFoundHandler를 int로 casting해서 사용할 수 있게 됨.

이렇게 single method interface를 정의해두면 보다 직관적이고 편리하게 사용할 수 있다.

Structuring Code Properly

스크린샷 2022-05-25 오후 4 19 18

패키지를 사용할지 말지 판단할 때, 보통 패키지의 구조를 먼저 보기 마련이다.

  • 로컬에서 테스트해봤고 잘 돌아간다는 자신이 있다고 해도, 사용자는 패키지에서 제일 먼저 테스트코드를 보고 작동방식을 파악한다.

 

스크린샷 2022-05-25 오후 4 31 14

Subfolder convention

  • cmd: for command
  • pkg: for package code
  • testdata: for test
  • internal: for internal stuff, only your code will import
  • docs

 

스크린샷 2022-05-25 오후 4 33 16

  • multiple go files: 해당 디렉토리에서 가장 연관성이 높은 것을 디렉토리 상단에, 관련성이 높을수록 가까운 거리에.

 

스크린샷 2022-05-25 오후 4 35 29

 

스크린샷 2022-05-25 오후 4 38 17

Leave Concurrency to the user.

  • awesome하고 cool한, go친화적이고 성능을 높이기 위해 concurrency를 사용하고 싶을 수 있지만
  • 하지만, 이건 사용자에게 함수를 blocking하게 사용할 수 있는 권리가 없는 형태. 활용 방법이 제한된다.
    • 사용자 스스로가 concurreny control을 하고 싶다거나... 선택지를 줘야 함.
  • 물론 이건 절대적인 규칙이나 convention이 아님. 써야만 하는 경우가 당연히 있을 수 있음.
  • 일반적인 경우엔 그렇다는 뜻.

 

TDD 방식

스크린샷 2022-05-25 오후 4 42 05

패키지 구조를 설계하기 전에 TDD 형태로 개발하는 것도 좋은 방법이다.

  • 물론, 기존에 만들어진 코드의 unit 테스트 코드를 짜는 작업은 세상에서 제일 지루한 방법임.
  • TDD 방식으로 코드를 만든다는 건
    • coverage.
    • use the package even before it exists. -> 설계에 도움이 됨.
  • API FootPrint를 시작 단계에서부터 생각할 수 있음.

Convention을 존중한다

 

스크린샷 2022-05-25 오후 4 45 28

  • standard Library나 다른 패키지와 가능한 비슷하게 하자.
  • Be Obvious, not Clever.

Naming Things

 

스크린샷 2022-05-25 오후 4 47 40

 

스크린샷 2022-05-25 오후 5 20 13

  • 패키지의 이름 자체가 part of API인 거 감안하고 명명한다.
    • 패키지 + 함수 간 Redundancy를 최대한 줄인다.

Expose yourself to the API footprint, from the beginning

스크린샷 2022-05-25 오후 5 20 27

TDD 방식을 사용한다면 쉽게 충족할 수 있는 목표 중 하나.

  • test code는 별도의 패키지로

 

스크린샷 2022-05-25 오후 5 26 13

로그는 가급적이면 넣지 말자. 넣는다면, 패키지 사용자가 로그 설정을 off할 수 있는 선택지를 줘야 한다.

 

스크린샷 2022-05-25 오후 5 26 27

default Value를 패키지 함수에서 설정한다.

  • type struct로 정의해둘 경우, default value를 사용자가 수정해서 사용할 수도 있다.

Constructor는 되도록이면 사용하지 않는다.

 

스크린샷 2022-05-25 오후 5 31 30

go는 struct를 정의해서 사용할 수 있다. 위 두 개의 예시를 비교하면

  • NewBrewer 메소드 내에서 무엇이 어떻게 이루어지는지를 알 수 없다. 생성자 메소드가 무엇을 어떻게 처리하는지를 사용자가 추가로 확인해야 하는 번거로움이 있음.
  • struct를 생성하고 필드를 할당하는 로직이 사용자에게 더 직관적이다.
  • api footprint 범위를 줄일 수 있다. 불필요한 메소드를 쓰지 않아도 되기 때문

기계적으로 interface를 생성하는 것도 지양한다.

 

스크린샷 2022-05-25 오후 5 35 17

  • struct인 FormatGreeter와 interface인 Greeter. 둘 다 필요할 이유가 없고, 사용자는 둘 중 뭘 써야 하는지 헷갈린다.

 

스크린샷 2022-05-25 오후 5 37 37

  • 만약 interface를 사용할 거라면, 내부적으로 동작하는 struct는 숨기면 된다.

Go-like Name : short and obvious.

 

스크린샷 2022-05-25 오후 5 39 01

  • 자바나 루비 계열 언어는 메소드명이 길다. 메소드명으로 많은 정보를 알려주려고 함.
  • go의 경우 smaller / succinct (간결한) 한 방식을 사용한다.

context는 first argument로

스크린샷 2022-05-25 오후 5 41 48

http request를 사용한다면, 사용자에게 http.Client 를 입력받는다.

스크린샷 2022-05-25 오후 5 41 14

  • 사용자가 http.Client를 생성하고, proxy나 timeout, redirect 등을 알아서 세팅하도록 유도한다.
  • 사용자가 값을 입력하지 않아도 http.DefaultClient 사용할 수 있음.

 

Global Variable 남용하지 말자.

스크린샷 2022-05-25 오후 6 22 15

  • flag나 init도 가급적 사용하지 않는 걸 추천한다. misused / not clear what's going to happen.
  • standard library를 호출해놓고 커스텀하게 수정하지 않는다.
    • http.DefaultClient를 호출해놓고 timeout을 별도로 설정하는 경우라던가
  • 내가 개발한 패키지를 Import했을 때 side effect가 발생해서는 안 된다.

 

Subpackage도 그저 패키지로 취급되므로, 헷갈리지 않게 명명한다.

 

스크린샷 2022-05-25 오후 6 26 18

  • 위 예시를 보면 "github.com/matryer/vice/test" 패키지를 import하고 있다. 이름에는 아무런 문제가 없음
    • 그런데 이 패키지를 코드에서 호출하려면 test.Transport() 이 된다. test 패키지가 무엇에 대한 test 패키지인지 코드에서 명확하게 드러니지 않음
  • 따라서 "github.com/matryer/vice/vicetest" 처럼. subpackage라고 해도 코드에서는 독립된 패키지처럼 사용된다.

물론 package import 경로에서는 중복이 발생한다. 하지만 코드에서의 가독성을 위해서는 수긍할 만한 중복이라고 본다.

 

Logo 넣자

스크린샷 2022-05-25 오후 6 31 14

  • 입증핢 만한 근거는 딱히 없지만, 로고 넣으면 사용자에게 기억되기도 / 더 많은 기여자가 찾아올 수 있다.

 

Quality Package 확인할 수 있는 기준

스크린샷 2022-05-25 오후 7 12 37

  • go fmt 적용했는가
  • _test.go 파일이 많이 있는가?
  • 코드 가독성이 좋은가?
  • 다른 언어의 프로젝트를 그대로 포팅한 느낌인가? (자바식 이름을 고집하고 있다거나)
  • dependency가 얼마나 있는가? - 적을수록 좋음
반응형