본문 바로가기
WIKI 기술 지식 베이스

아키텍처

원문 보기 위키 갱신

아키텍처 (Architecture)

Caddy는 Go로 작성되어 외부 의존성이 전혀 없는 단일 자립 정적 바이너리예요. 이런 가치는 배포를 단순화하고 프로덕션 환경에서 지루한 트러블슈팅을 줄여주기 때문에 프로젝트 비전의 중요한 부분이에요.

출처: Caddy 공식 문서

본문

Caddy는 Go로 작성됐기 때문에 외부 의존성이 전혀 없는 단일 자립 정적 바이너리예요. 이런 가치는 배포를 단순화하고 프로덕션 환경에서 지루한 트러블슈팅을 줄여주므로 프로젝트 비전의 중요한 부분이에요.

동적 링크가 없다면 어떻게 확장할 수 있을까요? Caddy는 외부(동적 링크) 의존성이 있는 다른 웹 서버를 훨씬 넘어서는 능력을 확장하는 독특한 플러그인 아키텍처를 갖추고 있어요.

"움직이는 부품을 줄이자"는 우리의 철학은 궁극적으로 더 신뢰할 수 있고, 관리하기 쉬우며, 저렴한 사이트로 이어져요. 특히 규모가 커질 때요. 이 반기술적 문서는 소프트웨어 엔지니어링을 통해 어떻게 그 목표를 달성하는지 설명해요.

개요 (Overview)

Caddy는 명령(command), 코어 라이브러리, 모듈로 구성돼요.

명령은 여러분이 익숙할 명령줄 인터페이스를 제공해요. 운영체제에서 프로세스를 실행하는 방법이에요. 여기의 코드와 로직은 상당히 적으며, 사용자가 원하는 방식으로 코어를 부트스트랩하는 데 필요한 것만 있어요. 우리는 설정용 플래그와 환경 변수 사용을, config 부트스트래핑에 관련된 것 외에는 의도적으로 피해요.

모듈은 명령줄 인터페이스에 하위 명령을 추가할 수 있어요! 예를 들어 caddy file-server 명령이 바로 거기서 나온 거예요. 이 추가된 명령은 핵심 Caddy 명령이 사용을 최소화하더라도 원하는 어떤 플래그나 환경 변수든 사용할 수 있어요.

코어 라이브러리(또는 Caddy의 "코어")는 주로 설정을 관리해요. 새 설정을 Run()할 수도, 실행 중인 설정을 Stop()할 수도 있어요. 모듈이 사용할 다양한 유틸리티, 타입, 값을 제공하기도 해요.

모듈은 그 외의 모든 것을 해요. 많은 모듈이 Caddy에 내장돼 있으며 이를 표준 모듈이라고 불러요. 대부분의 사용자에게 가장 유용하다고 판단된 것들이에요.

때때로 모듈, 플러그인, *확장(extension)*이라는 용어가 서로 바꿔 쓰이는데, 보통 그건 괜찮아요. 기술적으로 모든 모듈은 플러그인이지만, 모든 플러그인이 모듈은 아니에요. 모듈은 특히 Caddy의 설정 구조를 확장하는 일종의 플러그인이에요.

Caddy 코어

핵심적으로 Caddy는 단지 초기 설정("config")을 로드하거나, 없다면 나중에 새 설정을 받아들이기 위해 소켓을 여는 일을 해요.

Caddy 설정은 일부 최상위 필드가 있는 JSON 문서예요:

{
	"admin": {},
	"logging": {},
	"apps": {•••},
	...
}

Caddy의 코어는 이 필드 중 일부를 네이티브로 다루는 방법을 알아요:

  • admin — admin API를 설정하고 프로세스를 관리할 수 있게 해줌

  • logging — 로그를 발행할 수 있게 해줌

하지만 다른 최상위 필드(예: apps)는 Caddy 코어에게 불투명해요. 실제로 Caddy가 apps의 바이트로 아는 전부는 두 메서드를 호출할 수 있는 인터페이스 타입으로 역직렬화하는 거예요:

  1. Start()

  2. Stop()

...그게 전부예요. config가 로드되면 각 앱에 Start()를 호출하고, config가 언로드되면 각 앱에 Stop()을 호출해요.

앱 모듈이 시작되면 앱의 모듈 수명주기가 시작돼요.

Caddy 모듈을 만드는 프로그래머라면 Extending Caddy 가이드에서 코드에 더 초점을 둔 유사한 정보를 찾을 수 있어요.

모듈 수명주기 (Module lifecycle)

두 종류의 모듈이 있어요: 호스트 모듈과 게스트 모듈이에요.

호스트 모듈(또는 "부모" 모듈)은 다른 모듈을 로드하는 것들이에요.

게스트 모듈(또는 "자식" 모듈)은 로드되는 것들이에요. 모든 모듈은 게스트 모듈이에요. 앱 모듈조차도요.

모듈은 로드되고, 프로비저닝·검증되며, 사용된 다음, 정리되는 순서로 진행돼요:

  1. 로드(Loaded)

  2. 프로비저닝·검증(Provisioned and validated)

  3. 사용(Used)

  4. 정리(Cleaned up)

Caddy는 설정이 로드될 때 먼저 설정된 모든 앱 모듈을 초기화해 모듈 수명주기를 시작해요. 거기서부터 각 앱 모듈이 나머지를 처리하면서 아래로 끝없이 이어져요(turtles all the way down).

로드 단계 (Load phase)

모듈 로드는 JSON 바이트를 메모리의 타입 값으로 역직렬화하는 작업이에요. ...그게 기본적으로 전부예요. 그냥 JSON을 값으로 디코딩하는 거예요.

프로비저닝 단계 (Provision phase)

이 단계에서 대부분의 설정 작업이 일어나요. 모든 모듈은 로드된 후 스스로 프로비저닝할 기회를 얻어요.

JSON 인코딩의 속성은 이미 디코딩됐으므로, 여기에는 추가 설정만 하면 돼요. 프로비저닝 중 가장 흔한 작업은 게스트 모듈을 설정하는 거예요. 즉 호스트 모듈을 프로비저닝하면 그 게스트 모듈도 끝까지 전부 프로비저닝돼요.

이를 감각으로 이해하려면 문서에서 Caddy의 JSON 구조를 탐색해보면 돼요. {•••}가 보이는 곳마다 게스트 모듈을 사용할 수 있고, 클릭해 들어가면 더 이상 게스트 모듈이 없을 때까지 계속 탐색할 수 있어요.

다른 흔한 프로비저닝 작업은 모듈 수명 동안 사용할 내부 값을 설정하거나, 입력을 표준화하는 거예요. 예를 들어 http.matchers.remote_ip 모듈은 프로비저닝 단계를 사용해 JSON에서 받은 문자열 입력에서 CIDR 값을 파싱해요. 그래서 매 HTTP 요청마다 그걸 하지 않아도 되고, 결과적으로 더 효율적이에요.

검증도 프로비저닝 단계에서 일어날 수 있어요. 모듈의 결과 설정이 유효하지 않으면 여기서 에러가 반환될 수 있고, 그러면 전체 config 로드 과정이 중단돼요.

사용 단계 (Use phase)

게스트 모듈이 프로비저닝·검증되면 호스트 모듈이 사용할 수 있어요. 정확히 무엇을 의미하는지는 각 호스트 모듈에 달려 있어요.

각 모듈은 ID를 가지며, 이는 네임스페이스와 그 네임스페이스 안의 이름으로 구성돼요. 예를 들어 http.handlers.reverse_proxy는 http.handlers 네임스페이스에 있으면서 이름이 reverse_proxy라서 HTTP 핸들러예요. http.handlers 네임스페이스의 모든 모듈은 호스트 모듈이 아는 동일한 인터페이스를 만족해요. 그래서 http 앱은 이런 종류의 모듈을 로드하고 사용하는 방법을 알아요.

정리 단계 (Cleanup phase)

config를 중지할 때가 되면 모든 모듈이 언로드돼요. 모듈이 해제해야 할 리소스를 할당했다면 정리 단계에서 그렇게 할 기회가 있어요.

플러그인 연결 (Plugging in)

모듈 — 또는 어떤 Caddy 플러그인 — 은 모듈 패키지에 대한 import를 추가해 Caddy에 "연결"돼요. 패키지를 import하면 모듈이 스스로 등록해 Caddy 코어에 알려져, Caddy 프로세스가 시작될 때 각 모듈을 이름으로 알아요. 모듈 값과 이름을 서로 연관시킬 수도 있어요.

플러그인은 Caddy 코드 베이스를 전혀 수정하지 않고 추가할 수 있어요. readme에 있는 지침이 이 방법을 설명해요!

설정 관리 (Managing configuration)

실행 중인 서버의 활성 설정을 바꾸는 일(흔히 "reload"라고 함)은 높은 동시성과 서버가 요구하는 수천 개의 매개변수 때문에 까다로울 수 있어요. Caddy는 많은 이점이 있는 설계로 이 문제를 우아하게 해결해요:

  • 실행 중인 서비스에 중단 없음

  • 세분화된 config 변경 가능

  • (백그라운드에서) 하나의 락만 필요

  • 모든 reload는 원자적이고, 일관되고, 고립되며, 대개 지속적("ACID")

  • 최소한의 전역 상태

Caddy 2 설계에 대한 비디오를 여기서 볼 수 있어요.

config reload는 새 모듈을 프로비저닝하고, 모두 성공하면 이전 것을 정리하는 방식으로 동작해요. 잠시 동안 두 config가 동시에 운영돼요.

각 설정은 모든 모듈 상태를 보유하는 context와 연결돼서, 대부분의 상태가 config의 범위를 벗어나지 않아요. 이는 정확성, 성능, 단순성에 좋은 소식이에요!

하지만 때로는 진정한 전역 상태가 필요해요. 예를 들어 리버스 프록시는 업스트림의 상태를 추적할 수 있어요. 각 업스트림은 전역에 하나뿐이므로, 사소한 config 변경 때마다 그것을 잊는다면 좋지 않을 거예요. 다행히 Caddy는 언어 런타임의 가비지 컬렉터 같은 시설을 제공해 전역 상태를 깔끔하게 유지해요.

온라인 config 업데이트의 한 명백한 접근법은 핫 패스까지 포함해 모든 config 매개변수에 대한 접근을 동기화하는 거예요. 이는 성능과 복잡성에서 엄청나게 나쁘고, 특히 규모가 커질 때 그렇기 때문에 Caddy는 이 접근법을 사용하지 않아요.

대신 config는 불변의 원자적 단위로 취급돼요. 전체가 교체되거나, 아무것도 바뀌지 않거나. admin API 엔드포인트(구조를 탐색해 세분화된 변경을 허용)는 config의 메모리 내 표현만 변경하고, 거기서 완전히 새로운 config 문서가 생성·로드돼요. 이 접근법은 단순성, 성능, 일관성에서 큰 이점이 있어요. 락이 하나뿐이라 Caddy가 빠른 reload를 처리하기 쉬워요.

더 알아보기 (Learn more)