학습일지/Language
Writing Beautiful Package in Go
inspirit941
2022. 5. 29. 20:30
반응형
Golang UK Conference 2017 발표.
- go로 개발한 오픈소스 패키지가 여러 사람들에게 유용하고 쉽게 쓰이려면 어떻게 해야 하는지를 설명한 강연
- package는 go파일 (_test.go 포함) 로 구성된 하나의 디렉토리.
- 다른 프로젝트에서 import해서 사용할 수 있음
- exported / internal 두 종류가 있음
- main 패키지 말고. main 패키지는 command를 의미함
user-centred Design
- 결국 사람이 쓰는 거니까, 개발하려는 프로덕트의 최종 사용자의 요구사항, 제한조건을 고려해서 설계해야 한다.
- 따라서 고민해야 할 점
- 누가 쓸 건지
- 하려는 건 무엇인지
- 왜 하려는 건지
- 굳이 내 패키지를 쓰려는 이유는 뭔지
- PoC 수준일지? Bigger System일지?
Single Method Interface
- 인터페이스의 목적은, 사람들이 그걸 구현하길 바라는 것.
- 구현하기 쉬우려면, 인터페이스에서 요구하는 메소드의 개수는 최소한이어야 한다.
- 예컨대 function Adaptor 형태로 사용자가 인터페이스를 구현할 수 있음.
- 이런 형태는 인터페이스에 정의된 메소드가 단 하나만 있을 때 가능하다.
- type struct를 정의하는 대신 function으로 정의. More Simpler / versatile한 활용이 가능함.
- 위 예시는, http 요청을 받아서 응답으로 status code를 리턴하는 구조.
- NotFoundHandler를 int로 casting해서 사용할 수 있게 됨.
이렇게 single method interface를 정의해두면 보다 직관적이고 편리하게 사용할 수 있다.
Structuring Code Properly
패키지를 사용할지 말지 판단할 때, 보통 패키지의 구조를 먼저 보기 마련이다.
- 로컬에서 테스트해봤고 잘 돌아간다는 자신이 있다고 해도, 사용자는 패키지에서 제일 먼저 테스트코드를 보고 작동방식을 파악한다.
Subfolder convention
- cmd: for command
- pkg: for package code
- testdata: for test
- internal: for internal stuff, only your code will import
- docs
- multiple go files: 해당 디렉토리에서 가장 연관성이 높은 것을 디렉토리 상단에, 관련성이 높을수록 가까운 거리에.
Leave Concurrency to the user.
- awesome하고 cool한, go친화적이고 성능을 높이기 위해 concurrency를 사용하고 싶을 수 있지만
- 하지만, 이건 사용자에게 함수를 blocking하게 사용할 수 있는 권리가 없는 형태. 활용 방법이 제한된다.
- 사용자 스스로가 concurreny control을 하고 싶다거나... 선택지를 줘야 함.
- 물론 이건 절대적인 규칙이나 convention이 아님. 써야만 하는 경우가 당연히 있을 수 있음.
- 일반적인 경우엔 그렇다는 뜻.
TDD 방식
패키지 구조를 설계하기 전에 TDD 형태로 개발하는 것도 좋은 방법이다.
- 물론, 기존에 만들어진 코드의 unit 테스트 코드를 짜는 작업은 세상에서 제일 지루한 방법임.
- TDD 방식으로 코드를 만든다는 건
- coverage.
- use the package even before it exists. -> 설계에 도움이 됨.
- API FootPrint를 시작 단계에서부터 생각할 수 있음.
Convention을 존중한다
- standard Library나 다른 패키지와 가능한 비슷하게 하자.
- Be Obvious, not Clever.
Naming Things
- 패키지의 이름 자체가 part of API인 거 감안하고 명명한다.
- 패키지 + 함수 간 Redundancy를 최대한 줄인다.
Expose yourself to the API footprint, from the beginning
TDD 방식을 사용한다면 쉽게 충족할 수 있는 목표 중 하나.
- test code는 별도의 패키지로
로그는 가급적이면 넣지 말자. 넣는다면, 패키지 사용자가 로그 설정을 off할 수 있는 선택지를 줘야 한다.
default Value를 패키지 함수에서 설정한다.
- type struct로 정의해둘 경우, default value를 사용자가 수정해서 사용할 수도 있다.
Constructor는 되도록이면 사용하지 않는다.
go는 struct를 정의해서 사용할 수 있다. 위 두 개의 예시를 비교하면
- NewBrewer 메소드 내에서 무엇이 어떻게 이루어지는지를 알 수 없다. 생성자 메소드가 무엇을 어떻게 처리하는지를 사용자가 추가로 확인해야 하는 번거로움이 있음.
- struct를 생성하고 필드를 할당하는 로직이 사용자에게 더 직관적이다.
- api footprint 범위를 줄일 수 있다. 불필요한 메소드를 쓰지 않아도 되기 때문
기계적으로 interface를 생성하는 것도 지양한다.
- struct인 FormatGreeter와 interface인 Greeter. 둘 다 필요할 이유가 없고, 사용자는 둘 중 뭘 써야 하는지 헷갈린다.
- 만약 interface를 사용할 거라면, 내부적으로 동작하는 struct는 숨기면 된다.
Go-like Name : short and obvious.
- 자바나 루비 계열 언어는 메소드명이 길다. 메소드명으로 많은 정보를 알려주려고 함.
- go의 경우 smaller / succinct (간결한) 한 방식을 사용한다.
context는 first argument로
http request를 사용한다면, 사용자에게 http.Client 를 입력받는다.
- 사용자가 http.Client를 생성하고, proxy나 timeout, redirect 등을 알아서 세팅하도록 유도한다.
- 사용자가 값을 입력하지 않아도 http.DefaultClient 사용할 수 있음.
Global Variable 남용하지 말자.
- flag나 init도 가급적 사용하지 않는 걸 추천한다. misused / not clear what's going to happen.
- standard library를 호출해놓고 커스텀하게 수정하지 않는다.
- http.DefaultClient를 호출해놓고 timeout을 별도로 설정하는 경우라던가
- 내가 개발한 패키지를 Import했을 때 side effect가 발생해서는 안 된다.
Subpackage도 그저 패키지로 취급되므로, 헷갈리지 않게 명명한다.
- 위 예시를 보면 "github.com/matryer/vice/test" 패키지를 import하고 있다. 이름에는 아무런 문제가 없음
- 그런데 이 패키지를 코드에서 호출하려면 test.Transport() 이 된다. test 패키지가 무엇에 대한 test 패키지인지 코드에서 명확하게 드러니지 않음
- 따라서 "github.com/matryer/vice/vicetest" 처럼. subpackage라고 해도 코드에서는 독립된 패키지처럼 사용된다.
물론 package import 경로에서는 중복이 발생한다. 하지만 코드에서의 가독성을 위해서는 수긍할 만한 중복이라고 본다.
Logo 넣자
- 입증핢 만한 근거는 딱히 없지만, 로고 넣으면 사용자에게 기억되기도 / 더 많은 기여자가 찾아올 수 있다.
Quality Package 확인할 수 있는 기준
- go fmt 적용했는가
- _test.go 파일이 많이 있는가?
- 코드 가독성이 좋은가?
- 다른 언어의 프로젝트를 그대로 포팅한 느낌인가? (자바식 이름을 고집하고 있다거나)
- dependency가 얼마나 있는가? - 적을수록 좋음
반응형