0321 - 0327
0321
해당 분야의 용어로 타입 이름 짓기
컴퓨터 과학에서 어려운 일은 단 두 가지뿐이다. 캐시 무효화와 이름 짓기.
라는 말이 있을 정도로 이름 짓기는 타입 설계에서 중요하면서도 어려운 부분이다.
엄선된 타입, 속성, 변수의 이름은 의도를 명확히 하고 코드와 타입의 추상화 수준을 높여 준다.
반면, 잘못 선택한 타입 이름은 코드의 의도를 왜곡하고 잘못된 개념을 심어주게 된다.
interface Animal {
name: string;
endangered: boolean;
habitat: string;
}
const leopard = {
name: "Snow Leopard",
endangered: false,
habitat: "tundra",
};
위 코드에는 4가지 문제점이 있다.
- name은 매우 일반적인 용어이기에 정확히 무엇을 지칭하는지 알 수 없다.
- endangered 속성이 멸종 위기를 표현하기 위해 boolean 타입을 사용한 것이 이상하다. 해당 속성의 의도를 ‘멸종 위기 또는 멸종’으로 생각한 것일지도 모르고 정확하게 파악하기 힘들다.
- 서식지를 나타내는 habitat 속성은 너무 범위가 넓은 string 타입일 뿐만 아니라 서식지라는 뜻 자체도 불분명하기 때문에 다른 속성들보다 훨씬 모호하다.
- 객체의 변수명이 leopard이지만, name 속성의 값은 ‘Snow Leopard’이다. 객체의 이름과 속성의 name이 다른 의도로 사용된 것인지 불분명하다.
위의 문제를 해결하려면, 정보가 모호하기 때문에 해당 속성을 작성한 사람에게 의도를 물어봐야 한다. 이는 매우 비효율적이다.
interface Animal {
commonName: string;
genus: string;
species: string;
status: ConservationStatus;
climates: KoppenClimate[];
}
반면, 위의 코드 타입 선언은 의미가 분명하다.
- name은 commonName, genus, species 등 더 구체적인 용어로 대체했다.
- endangered는 동물 보호 등급에 대한 IUCN의 표준 분류 체계인 ConservationStatus 타입의 status로 변경되었다.
- habitat은 기후를 뜻하는 climates로 변경되었으며, 쾨펜 기후 분류를 사용한다.
위의 타입 선언은 데이터를 훨씬 명확하게 표현하여 정보를 찾기 위해 사람에 의존할 필요가 없다.
코드로 표현하고자 하는 모든 분야에는 주제를 설명하기 위한 전문 용어들이 있다. 자체적으로 용어를 만들어 내려 하지 말고, 해당 분야에 이미 존재하는 용어를 사용하면 타입의 명확성을 올릴 수 있다.
타입, 속성, 변수에 이름을 붙일 때 명심해야 할 규칙
- 동일한 의미를 나타낼 때는 같은 용어를 사용해야 한다. 정말로 의미적으로 구분이 되어야 하는 경우에만 다른 용어를 사용해야 한다.
- data, info, item, object 같은 모호하고 의미 없는 이름은 피해야 한다. 만약 entity라는 용어가 해당 분야에서 특별한 의미를 가진다면 괜찮지만 귀찮다고 의미 없는 이름을 붙여서는 안 된다.
- 이름을 지을 때는 포함된 내용이나 계산 방식이 아니라 데이터 자체가 무엇인지 고려해야 한다. 예를 들어, INodeList 보다 Directory가 더 의미있는 이름이다.
좋은 이름은 추상화의 수준을 높이고 의도치 않은 충돌의 위험성을 줄여 준다.
공식 명칭에는 상표를 붙이기
구조적 타이핑의 특성 때문에 가끔 코드가 이상한 결과를 낼 수 있다.
이를 해결하기 위해 공식 명칭을 사용할 수 있는데, 이는 타입이 아니라 값의 관점에서 말하는 것이다.
공식 명칭 개념을 타입스크립트에서 흉내 내려면 ‘상표’를 붙이면 된다.
interface Vector2D {
_brand: "2d";
x: number;
y: number;
}
function vec2D(x: number, y: number): Vector2D {
return { x, y, _brand: "2d" };
}
function calculateNorm(p: Vector2D) {
return Math.sqrt(p.x * p.x + p.y * p.y);
}
calculateNorm(vec2D(3, 4));
const vec3D = { x: 3, y: 4, z: 1 };
calculateNorm(vec3D);
// '_brand' 속성이 '{ x: number; y: number; z: number; }' 형식에 없지만 'Vector2D' 형식에서 필수입니다.
- vec3D 값에
_brand: '2d'라고 추가하는 악의적인 사용을 막을 수는 없지만 실수를 방지하기에는 충분하다. - 상표 기법은 타입 시스템에서 동작하지만 런타임에 상표를 검사하는 것과 동일한 효과를 얻을 수 있다.
- 타입 시스템이기 때문에 런타임 오버헤드를 없앨 수 있고 추가 속성을 붙일 수 없는 string이나 number 같은 내장 타입도 상표화할 수 있다.
절대 경로를 사용해 파일 시스템에 접근하는 함수를 만들 때, 런타임에는 절대 경로로 시작하는지 체크하기 쉽지만, 타입 시스템에서는 절대 경로를 판단하기 어렵기 때문에 상표 기법을 사용하다.
type AbsolutePath = string & { _brand: "abs" };
function listAbsolutePath(path: AbsolutePath) {
//
}
function isAbsolutePath(path: string): path is AbsolutePath {
return path.startsWith("/");
}
function f(path: string) {
if (isAbsolutePath(path)) {
listAbsolutePath(path);
}
}
- string이면서 _brand 속성을 가지는 객체를 만들 수는 없다. AbsolutePath는 온전히 타입 시스템의 영역이다.
- 만약 path 값이 절대 경로와 상대 경로 둘 다 될 수 있다면, 타입 가드와 함께 사용하면 이러한 오류를 방지할 수 있다.
any 타입은 가능한 한 좁은 범위에서만 사용하기
function processBar(b: Bar) {
// ...
}
function f() {
const x = expressionReturningFoo();
processBar(x);
// 'Foo' 형식의 인수는 'Bar' 형식의 매개변수에 할당될 수 없습니다.
}
위의 오류를 제거하는 방법은 두 가지이다.
function f1() {
const x: any = expressionReturningFoo();
processBar(x);
}
function f2() {
const x = expressionReturningFoo();
processBar(x as any);
}
- 두 가지 해결책 중 f2에 사용된
x as any형태가 권장된다. - 그 이유는 any 타입이 매개변수에서만 사용된 표현식이므로 다른 코드에는 영향을 미치지 않기 때문이다.
- f1에서는 함수의 마지막까지 x의 타입이 any이기에 만약 f1 함수가 x를 반환한다면 문제가 커진다.
- 이와 같이 의도치 않은 타입 안정성의 손실을 피하기 위해 any의 사용 범위를 최소한으로 좁혀야 한다.
@ts-ignore
강제로 타입 오류를 제거하려면 any 대신 @ts-ignore를 사용하는 것이 좋다.
function f1() {
const x: any = expressionReturningFoo();
// @ts-ignore
processBar(x);
}
그러나 이 방법은 근본적 해결 방법이 아니기 때문에 다른 곳에서 더 큰 문제가 발생할 수도 있다.
큰 객체 안에서 한 개 속성이 타입 오류를 가지는 경우
const config: Config = {
a: 1,
b: 2,
c: {
key: value as any,
},
};
- 객체 전채를
as any로 선언하면 오류를 제거할 수 있지만 다른 속성들 역시 타입 체크가 되지 않는다. - 그러므로 최소한의 범위에만 any를 사용하는 것이 좋다.
any를 구체적으로 변형해서 사용하기
any 타입의 값을 그대로 정규식이나 함수에 넣는 것은 권장되지 않는다.
function getLengthBad(array: any) {
return array.length;
}
function getLength(array: any[]) {
return array.length;
}
- getLengthBad보다 getLength는 세 가지 장점을 가지고 있다.
- 함수 내의 array.length 타입이 체크된다.
- 반환 타입이 any 대신 number로 추론된다.
- 함수 호출될 때 매개변수가 배열인지 체크된다.
함수의 매개변수가 객체이긴 하지만 값을 알 수 없다면 {[key: string]: any}처럼 선언하면 된다.
함수의 타입 구체화
함수의 타입에도 단순히 any를 사용해서는 안 된다. 최소한으로나마 구체화할 수 있는 세 가지 방법이 있다.
type Fn0 = () => any; // 매개변수 없이 호출 가능한 모든 함수
type Fn1 = (arg: any) => any; // 매개변수 1개
type FnN = (...args: any[]) => any; // 모든 개수의 매개 변수, Function 타입과 동일
함수 안으로 타입 단언문 감추기
함수를 작성하다 보면, 외부로 드러난 타입 정의는 간단하지만 내부 로직이 복잡해서 안전한 타입으로 구현하기 어려운 경우가 많다.
함수의 모든 부분을 안전한 타입으로 구현하는 것이 이상적이지만, 불필요한 예외 사항까지 고려해 가며 타입 정보를 힘들게 구성할 필요는 없다.
함수 내부에는 타입 단언을 사용하고 함수 외부로 드러나는 타입 정의를 정확히 명시하는 정도로 끝내는 게 낫다.
프로젝트 전반에 위험한 타입 단언문이 드러나 있는 것보다, 제대로 타입이 정의된 함수 안으로 타입 단언문을 감추는 것이 더 좋은 설계이다.
0322
프락시
프락시는 클라이언트와 서버 사이에 위치하여 그들 사이의 HTTP 메시지를 정리하는 중개인처럼 동작한다.
웹 중개자
웹 프락시 서버는 클라이언트의 입장에서 트랜잭션을 수행하는 중개인이다.
웹 프락시가 없다면, 클라이언트는 HTTP 서버와 직접 이야기하고, 있다면 자신의 입장에서 서버와 대화해주는 프락시와 이야기한다.
트랜잭션을 완료하는 것이 클라이언트라느 점은 변하지 않지만, 프락시 서버가 제공하는 좋은 서비스를 이용하게 된다.
HTTP 프락시 서버는 웹 서버이기도 하고 웹 클라이언트이기도 하다.
프락시는 HTTP 클라이언트의 요청을 받게 되므로, 반드시 웹 서버처럼 요청과 커넥션을 적절히 다루고 응답을 돌려줘야 한다.
또한 요청을 서버에 보내기도 하므로, 요청을 보내고 응답을 받는 올바른 HTTP 클라이언트처럼 동작해야 한다.
개인 프락시와 공유 프락시
하나의 클라이언트만을 위한 프락시를 개인 프락시라고 부르고, 여러 클라이언트가 함께 사용하는 프락시는 공용 프락시라 부른다.
공용 프락시
대부분의 프락시는 공용이며 공유된 프락시다.
중앙 집중형 프락시를 관리하는 게 더 비용효율이 높고 쉽다.
여러 사용자들의 공통된 요청에서 이득을 취할 수 있기 때문에 캐시 프락시 서버와 같은 몇몇 프락시 애플리케이션은 프락시를 이용하는 사용자가 많을수록 유리하다.
개인 프락시
어떤 브라우저 보조 제품들은 몇몇 ISP 서비스와 마찬가지로 브라우저의 기능을 확장하거나 성능을 개선하거나 무료 ISP 서비스를 위한 광고를 운영하기 위해 작은 프락시를 사용자의 컴퓨터에서 직접 실행한다.
프락시 vs 게이트웨이
- 엄밀하게 말하면, 프락시는 같은 프로토콜을 사용하는 둘 이상의 애플리케이션을 연결하고, 게이트웨이는 서로 다른 프로토콜을 사용하는 둘 이상을 연결한다.
- 게이트웨이는 클라이언트와 서버가 다른 프로토콜로 말하더라도 서로 간의 트랜잭션을 완료할 수 있도록 해주는 프로토콜 변환기처럼 동작한다.
- 예를 들어, 웹 프락시는 클라이언트와 서버 양쪽 모두에게 HTTP로 말한다.
- HTTP 프론트엔드에 매여서 POP 이메일 백엔드를 향하고 있는 경우의 중개 장치는 게이트웨이다. 이 게이트웨이는 웹 트랜잭션을 적절한 POP 트랜잭션으로 변환하고, 사용자가 이메일을 HTTP를 통해 읽을 수 있게 해준다.
- 브라우저와 서버가 다른 버전의 HTTP를 구현하는 경우 프락시는 약간의 프로토콜 변환을 하기도 한다.
프락시를 사용하는 이유
프락시 서버는 실용적이고 유용한 것이라면 무슨 일이든 한다. 보안 개선, 성능 향상, 비용 절약 등이 있다.
그리고 프락시 시버는 모든 HTTP 트래픽을 들여다보고 건드릴 수 있기 때문에, 프락시는 부가적인 가치를 주는 여러 유용한 웹 서비스를 구현하기 위해 트래픽을 감시하고 수정할 수 있다.
- 학교의 필터 프락시는 부적절한 콘텐츠를 차단하는 경우 사용할 수 있다.
- 문서 접근 제어 프락시는 대기업 환경이나 분산된 관료 조직에서 서버들과 리소스에 대한 접근 제어 전략을 구현하고 감사 추적을 하기 위해 사용될 수 있다.
- ex) 서버에 접근하기 전에 먼저 비밀번호를 요구한다.
- 방화벽 프락시 서버는 조직 안에 들어오거나 나가는 응용 레벨 프로토콜의 흐름을 네트워크의 한 지점에서 통제한다. 또한 바이러스를 제거하는 웹이나 이메일 프락시가 사용할 수 있는, 트래픽을 세심히 살펴볼 수 있는 후크(hook)를 제공한다.
- 프락시 캐시는 인기 있는 문서의 로컬 사본을 관리하고 해당 문서에 대한 요청이 오면 빠르게 제공하여, 느리고 비싼 인터넷 커뮤니케이션을 줄인다.
- 대리 혹은 리버스 프락시로 불리는 이들은 진짜 웹 서버 요청을 받지만 웹 서버와는 달리 요청 받은 콘텐츠의 위치를 찾아내기 위해 다른 서버와 커뮤니케이션을 시작한다.
- 공용 콘텐츠에 대한 느린 웹 서버의 성능을 개선하기 위해 사용될 수 있다.
- 이런 식으로 사용하는 대리 프락시를 서버 가속기라고 부른다.
- 또한 콘텐츠 라우팅 기능과 결합하여 주문형 복제 콘텐츠의 분산 네트워크를 만들기 위해 사용될 수 있다.
- 프락시 시버는 인터넥 트래픽 조건과 콘텐츠 종류에 따라 요청을 특정 웹 서버로 유동하는 콘텐츠 라우터로 동작할 수 있다.
- 또한 콘텐츠 라우터는 사용자들에게 제공할 여러 서비스를 구현하는데 사용할 수 있다.
- ex) 사용자나 콘텐츠 제공자가 높은 성능을 위해 돈을 지불했다면 요청을 가까운 복제 캐시로 전달하거나, 사용자가 필터링 서비스에 가입했다면 요청이 필터링 프락시를 통과하도록 할 수 있다.
- 프락시 서버는 콘텐츠를 클라이언트에게 전달하기 전에 본문 포맷을 수정할 수 있는데, 이와 같이 데이터의 표현 방식을 변환하는 것을 트랜스코딩이라고 부른다.
- 트랜스코딩 프락시는 크기를 줄이기 위해 GIF 이미지를 JPG 이미지로 변환할 수 있다.
- 또한 텍스트 파일은 압축될 수 있고, 문서를 외국어 문서로 변환하는 것 또한 가능하다.
- 익명화 프락시는 HTTP 메시지에서 신원을 식별할 수 있는 특성들을 적극적으로 제거함으로써 개인 정보 보호와 익명성 보장에 기여한다.
- 그러나 신원 정보를 제거하게 되면 브라우징 경험의 질이 떨어지게 될 수 있고, 몇몇 웹 사이트는 적절히 동작하지 않을 수 있다.
프락시는 어디에 있는가?
프락시 서버 배치
어떻게 사용할지에 따라 프락시는 어디에든 배치할 수 있다.
출구 프락시
- 로컬 네트워크와 더 큰 인터넷 사이를 오가는 트래픽을 제어하기 위해 프락시를 로컬 네트워크의 출구에 박아 넣을 수 있다.
- 회사 밖의 악의적인 해커들을 막는 방화벽을 제공하기 위해, 혹은 인터넷 요금을 절약하고 인터넷 트래픽의 성능을 개선하기 위해 회사에서 출구 프락시를 사용할 수 있다.
접근(입구) 프락시
- 고객으로부터의 모든 요청을 종합적으로 처리하기 위해 프락시는 ISP 접근 지점에 위치하기도 한다.
- ISP는 사용자들의 다운로드 속도를 개선하고 인터넷 대역폭 비용을 줄이기 위해 캐시 프락시를 사용해 많이 찾는 문서들의 사본을 저장한다.
대리(리버스) 프락시
- 리버스 프락시는 네트워크의 가장 끝에 있는 웹 서버들의 바로 앞에 위치하여 웹 서버로 향하는 모든 요청을 처리하고 필요할 때만 웹 서버에게 자원을 요청할 수 있다.
- 웹 서버에 보안 기능을 추가하거나 빠른 웹 서버 캐시를 느린 웹 서버의 앞에 놓음으로써 성능을 개선할 수도 있다.
네트워크 교환 프락시
- 캐시를 이용해 인터넷 교차로의 혼잡을 완화하고 트래픽 흐름을 감시하기 위해, 충분한 처리 능력을 갖춘 프락시가 네트워크 사이의 인터넷 피어링 교환 지점들에 놓일 수 있다.
프락시 계층
- 프락시들은 프락시 계층이라고 불리는 연쇄를 구성할 수 있다.
- 프락시 계층에서 프락시 서버들은 부모와 자식의 관계를 갖는데, 인바운드 프락시(서버에 가까운 쪽)를 부모라고 부르고 아웃바운드 프락시(클라이언트 가까운 쪽)는 자식이라고 부른다.
- 프락시 계층은 정적일수도 있고 동적일 수도 있다.
- 동적 부모 선택의 예시
- 부하 균형 - 자식 프락시는 부하를 분산하기 위해 현재 부모들의 작업량 수준에 근거하여 부모 프락시를 고른다.
- 지리적 인접성에 근거한 라우팅 - 자식 프락시는 원 서버의 지역을 담당하는 부모를 선택할 수도 있다.
- 프로토콜/타입 라우팅 - 자식 프락시는 URI에 근거하여 다른 부모나 원 서버로 라우팅 할 수 있다.
- 유료 서비스 가입자를 위한 라우팅
어떻게 프락시가 트래픽을 처리하는가
클라이언트가 트래픽이 프락시로 가도록 만드는 방법에는 4가지가 있다.
클라이언트를 수정한다
많은 웹 클라이언트들은 수동 혹은 자동 프락시 설정을 지원한다. 만약 클라이언트가 프락시를 사용하도록 설정되어 있다면, 클라이언트는 의도적으로 요청을 프락시로 보낸다.
네트워크를 수정한다
네트워크 인프라를 가로채서 웹 트래픽을 프락시로 가도록 조정하는 기법
이 가로챔은 일반적으로 HTTP 트래픽을 지켜보고 가로채어 클라이언트 모르게 트래픽을 프락시로 보내는 스위칭 장치와 라우팅 장치를 필요로 한다. 이것을 인터셉트(투명) 프락시라고 부른다.
DNS 이름공간을 수정한다
리버스 프락시에서는 모든 요청이 서버 대신 리버스 프락시로 간다.
이는 DNS 이름 테이블을 수동으로 편집하거나 사용할 적절한 프락시나 서버를 계산해주는 특별한 동적 DNS 서버를 이용해서 조정될 수 있다.
웹 서버를 수정한다
몇몇 웹 서버는 리다이렉션 명령을 클라이언트에게 돌려줌으로써 클라이언트의 요청을 프락시로 리다이렉트 하도록 설정할 수 있다.
리다이렉트를 받는 즉시 클라이언트는 프락시와의 트랜잭션을 시작한다.
클라이언트 프락시 설정
많은 브라우저가 프락시를 설정하는 여러 가지 방법을 제공한다.
- 수동 설정 - 프락시를 사용하겠다고 명시적으로 설정
- 브라우저 기본 설정 - 브라우저를 소비자에게 전달하기 전에 프락시를 미리 설정해 놓을 수 있다.
- 프락시 자동 설정(PAC) - JS 프락시 자동 설정 파일에 대한 URI를 제공할 수 있다. 자바스크립트 파일을 가져와서 실행한다.
- WPAD 프락시 발견 - 대부분의 브라우저는 자동설정 파일을 다운받을 수 있는 설정 서버를 자동으로 찾아주는, 웹 프락시 자동발견 프로토콜을 제공한다.
0323
프락시 요청의 미묘한 특징들
프락시 URI는 서버 URI와 다르다
클라이언트가 프락시 대신 서버로 요청을 보내면 요청의 URI가 달라진다.
클라이언트가 웹 서버로 요청을 보낼 때, 요청줄은 스킴, 호스트, 포트번호가 없는 부분 URI를 가지는 반면, 프락시로 요청을 보낼 때 요청줄은 완전한 URI를 갖는다.
가상 호스팅에서 일어나는 문제
가상으로 호스팅 되는 웹 서버는 여러 웹 사이트가 같은 물리적 웹 서버를 공유한다. 요청 하나가 부분 URI로 오면, 가상으로 호스팅 되는 웹 서버는 그 요청이 접근하고자 하는 웹 사이트의 호스트 명을 알 필요가 있다.
이는 프락시와 비슷한 문제이지만, 각각 다른 방법으로 해결되었다.
- 명시적인 프락시는 요청 메시지가 완전한 URI를 갖도록 함으로써 이 문제를 해결했다.
- 가상으로 호스팅 되는 웹 서버는 호스트와 포트에 대한 정보가 담겨 있는 Host 헤더를 요구한다.
인터셉트 프락시는 부분 URI를 받는다
클라이언트가 프락시를 사용한다고 설정되어 있지 않더라도, 클라이언트의 트래픽은 대리 프락시나 인터셉트 프락시를 지날 수 있다.
두 가지 겅우 모두, 클라이언트는 자신이 웹 서버와 대화하고 있다고 생각하고 부분 URI를 보내서 문제가 발생할 수 있다.
- 대리 프락시는 원 서버의 호스트 명과 아이피 주소를 사용해 원 서버를 대신하는 프락시 서버이다.
- 인터셉트 프락시는 네트워크 흐름에서 클라이언트에서 서버로 가는 트래픽을 가로채 캐시된 응답을 돌려주는 일을 하는 프락시 서버다.
프락시는 프락시 요청과 서버 요청을 모두 다룰 수 있다
트래픽이 프락시 서버로 리다이렉트 될 수 있는 여러 가지 방법이 존재하기 때문에, 다목적 프락시 서버는 요청 메시지의 완전한 URI와 부분 URI를 모두 지원해야 한다.
프락시는 명시적인 프락시 요청에 대해서는 완전한 URI를 사용하고 아니면 부분 URI를 사용해야 하며, 웹 서버 요청의 경우에는 가상 Host 헤더를 사용해야 한다.
- 완전한 URI가 주어졌다면, 프락시는 이를 사용한다.
- 부분 URI가 주어졌고, Host 헤더가 있다면, Host 헤더를 이용해 원 서버 이름과 포트 번호를 알아내야 한다.
- 부분 URI가 주어졌으나 Host 헤더가 없다면, 다음 방법으로 원 서버를 알아내야 한다.
- 프락시가 대리 프락시라면, 프락시에 실제 서버의 주소와 호트 번호가 설정되어 있을 수 있다.
- 이전에 어떤 인터셉트 프락시가 가로챘던 트래픽을 받았고, 그 인터셉트 프락시가 원 IP 주소와 포트번호를 사용할 수 있도록 해두었다면, 그 IP 주소와 포트번호를 사용할 수 있다.
- 모두 실패했다면, 반드시 에러 메시지를 반환해야 한다.
전송 중 URI 변경
프락시 서버는 요청 URI 변경에 매우 신경을 써야 한다. 사소한 URI 변경이라도 다운스트림 서버와 상호운용성 문제를 일으킬 수 있다.
프락시 서버는 가능한 한 관대하도록 해야 한다. 프로토콜을 엄격하게 준수하도록 강제한다면 기존에 잘 동작하던 기능들을 망가뜨리는 결과를 수반할 수 있기 때문이다.
특히 HTTP 명세는 일반적으로 인터셉트 프락시가 URI를 전달할 때 절대 경로를 고쳐 쓰는 것을 금지한다. 유일한 예외는 빈 경로를 ‘/’로 교체하는 것 뿐이다.
URI 클라이언트 자동확장과 호스트 명 분석
브라우저는 프락시의 존재 여부에 따라 요청 URI를 다르게 분석한다.
프락시가 없다면 타이핑한 URI를 가지고 그에 대응하는 IP 주소를 찾는다. 만약 호스트명이 발견되면 그에 대응하는 IP 주소들을 연결에 성공할 때까지 시도해본다.
그러나 호스트가 발견되지 않는다면, 많은 브라우저들은 사용자가 호스트 명의 짧은 약어를 타이핑한 것으로 보고 자동화된 호스트 명의 확장을 제공하고자 다음과 같이 몇 가지 시도를 한다.
www.접두사를 붙이고.com접미사를 붙인다.- 심지어 몇몇 브라우저는 해석할 수 없는 URI를 서드파티 사이트로 넘기기도 하는데, 이 사이트는 오타 교정을 시도하고 사용자가 의도했을 URI를 제시한다.
- 이뿐만 아니라, 대부분의 시스템에서 DNS는 사용자가 호스트 명의 앞부분만 입력하면 자동으로 도메인을 검색하도록 설정되어 있다.
프락시 없는 URI 분석
- 사용자는 oreilly를 브라우저의 URI 창에 입력했다. 브라우저는 이를 호스트 명으로 사용하고 기본 스킴을
http://로, 기본 포트를 80으로, 기본 경로를 ‘/’로 간주한다. - 브라우저는 호스트 oreilly를 찾아본다. 이것은 실패한다.
- 브라우저는 호스트 명을 자동으로 확장한 후 DNS에 ‘
www.oreilly.com‘의 주소 분해를 요청한다. 이것은 성공해서 IP 주소를 돌려받아 요청과 응답을 주고 받는다.
명시적인 프락시를 사용할 때의 URI 분석
명시적인 프락시를 사용한다면, 브라우저는 이와 같은 편리한 확장들 중 어느 것도 수행할 수 없다. 브라우저의 URI가 프락시를 그냥 지나쳐버리기 때문이다.
브라우저는 명시적인 프락시가 있는 경우 부분 호스트 명을 자동확장하지 않는다.
- 사용자는 oreilly를 브라우저의 URI 창에 입력했다.
- 프락시 명시적으로 설정되었으므로, 브라우저는 DNS를 사용해 프락시 서버를 찾고, IP 주소를 얻는다.
- 브라우저는 프락시에 접속하고자 시도하고, 커넥션을 맺는다.
- 브라우저가 HTTP 요청을 보낼 때 클라이언트가 자동확장을 하지 않았기 때문에, 프락시는 요청에서 부분 호스트 명을 얻는다.
이러한 이유로, 몇몇 프락시는 ‘www…com’ 자동확장이나 지역 도메인 접미사 추가와 같은 브라우저의 편리한 서비스를 할 수 있다면 최대한 흉내 내려고 시도한다.
인터셉트 프락시를 이용한 URI 분석
호스트 명 분석은 인터셉트 프락시와 함께일 때 약간 달라지는데, 클라이언트 입장에서 프락시는 존재하지 않는 것이기 때문이다.
DNS가 성공할 때까지 호스트 명을 자동확장하는 브라우저를 사용할 때의 동작은 프락시가 아닌 서버의 경우와 별 차이가 없다.
그러나 서버로의 커넥션이 만들어졌을 때는 차이가 발생한다.
- DNS가 성공할 때까지 호스트 명을 자동확장하는 브라우저를 사용할 때의 동작은 프락시가 아닌 서버의 경우와 동일
- 클라이언트는 성공적으로 호스트 명을 분석하였고, IP 주소의 목록을 갖고 있다. 일반적으로, 클라이언트는 성공할 때까지 모든 IP 주소에 대해 접속을 시도하지만, 어떤 IP 주소들은 죽은 것일 수 있다. 그러나 인터셉트 프락시와 함께라면, 첫 번째 접속 시도는 원 서버가 아닌 프락시 서버에 의해 종료된다. 클라이언트는 성공적으로 웹 서버와 대화했다고 믿지만, 웹 서버는 살아있지도 않을 것이다.
- 프락시가 최종적으로 원 서버와 상호작용할 준비가 되었을 때, 프락시는 그 IP 주소가 실제로 다운된 서버를 가리키고 있음을 알게 될 것이다. 브라우저에서 제공하는 것과 동등한 수준의 장애 허용을 제공하기 위해서, 프락시는 호스트 헤더에 들어 있는 호스트 명을 다시 분석하든 아니면 IP 주소에 대한 역방향 DNS 룩업을 해서든 다른 IP 주소를 시도해야 한다.
메시지 추적
프락시가 점점 더 흔해지면서, 서로 다른 스위치와 라우터를 넘나드는 IP 패킷의 흐름을 추적하는 것 못지않게 프락시를 넘나드는 메시지의 흐름을 추적하고 문제점을 찾아내는 것도 필요한 일이 되었다.
Via 헤더
Via 헤더 필드는 메시지가 지나는 각 중간 노드(프락시나 게이트웨이)의 정보를 나열한다. 메시지가 또 다른 노드를 지날 때마다, 중간 노드는 Via 목록의 끝에 반드시 추가되어야 한다.
Via 헤더 필드는 메시지의 전달을 추적하고, 메시지 루프를 진단하고, 요청을 보내고 그에 대한 응답을 돌려주는 과정에 관여하는 모든 메시지 발송자들의 프로토콜을 다루는 능력을 알아보기 위해 사용된다.
Via 요청과 응답 경로
요청 메시지와 응답 메시지 모두 프락시를 지나므로 둘 모두 Via 헤더를 가진다.
요청과 응답은 보통 같은 TCP 커넥션을 오가므로, 응답 메시지는 요청과 같은 경로를 되돌아간다.
즉, 응답의 Via 헤더는 거의 언제나 요청 Via 헤더와 반대다.
Via와 게이트웨이
Via 헤더는 게이트웨이의 프로토콜 변환을 기록하므로 HTTP 애플리케이션은 프락시 연쇄에서 프로토콜 능력과 변환이 있었는지를 알아챌 수 있다.
Server 헤더와 Via 헤더
Server 응답 헤더 필드는 원 서버에 의해 사용되는 소프트웨어를 알려준다.
응답 메시지가 프락시를 통과할 때, 프락시는 Server 헤더를 수정해서는 안 된다. 대신 프락시는 via 항목을 추가해야 한다.
Via가 개인정보 보호와 보안에 미치는 영향
Via 문자열 안에 정확한 호스트 명이 들어가기를 원하지 않는 몇 가지 경우가 있다.
프락시는 방화벽 뒤에 숨어있는 호스트의 이름과 포트를 전달홰서는 안된다. 방화벽 뒤의 네트워크 아키텍처에 대한 정보가 악의적인 집단에 의해 이용될 수 있기 때문이다.
만약 Via 노드 이름 전달이 가능하지 않다면, 보안 경계선의 일부분인 프락시는 호스트 명을 그 호스트에 대한 적당한 가명으로 교체해야 한다.
TRACE 메서드
프락시 서버는 메시지가 전달될 때 메시지를 바꿀 수 있다. 헤더가 추가되거나, 변경되거나, 삭제될 수 있으며, 본문이 다른 형식으로 변환될 수 있다.
TRACE 메서드는 요청 메시지를 프락시의 연쇄를 따라가면서 어떤 프락시를 지나가고 어떻게 각 프락시가 요청 메시지를 수정하는지 관찰/추적할 수있도록 해준다. 이는 프락시 흐름을 디버깅하는데 매우 유용하다.
TRACE 요청이 목적지 서버에 도착했을 때, 서버는 전체 요청 메시지를 HTTP 응답 메시지의 본문에 포함시켜 송신자에게 그대로 돌려보낸다.
Max-Forwards
TRACE와 OPTIONS 요청의 프락시 홉 개수를 제한하기 위해 Max-Forwards 헤더를 사용할 수 있는데, 이는 전달되는 메시지가 무한 루프에 빠지지 않는지 프락시 연쇄를 테스트하거나 연쇄 중간의 특정 프락시 서버들의 효과를 체크할 때 유용하다.
Max-Forwards 요청 헤더 필드는 이 요청 메시지가 몇 번 더 다음 홉으로 전달될 수 있는지 말해주는 정수 하나를 담고 있다.
만약 값이 0이라면, 수신자는 자신이 원 서버가 아니라 할지라도 TRACE 메시지를 더 이상 전달하지 말고 반드시 클라이언트에게 돌려줘야 한다.
프락시 인증
HTTP는 사용자가 유효한 접근 권한 자격을 프락시에 제출하지 않는 한 콘텐츠에 대한 요청을 차단하는 프락시 인증이라는 메커니즘을 정의하고 있다.
프락시 인증은 인증에 참여하는 프락시가 프락시 연쇄상에 여러 개 있을 때는 일반적으로 잘 동작하지 않는다.
프락시 상호운용성
프락시 서버는 서로 다른 프로토콜을 구현했을 수도 있고 골치 아프게 이상한 동작을 할 수도 있는 클라이언트와 서버 사이를 중개해야 한다.
지원하지 않는 헤더와 메서드 다루기
프락시 서버는 넘어오는 헤더 필드들을 모두 이해하지 못할 수도 있다.
프락시는 이해할 수 없는 헤더 필드는 반드시 그대로 전달해야 하며, 같은 이름의 헤더 필드가 여러 개 있는 경우에는 그들의 상대적인 순서도 반드시 유지해야 한다.
OPTIONS: 어떤 기능을 지원하는지 알아보기
OPTIONS 메서드는 서버에서 지원하거나 지정한 리소스에 대해 가능한 선택적인 기능들을 서술하는 여러 헤더 필드를 포함한 응답을 반환한다.
하지만 HTTP/1.1이 명시한 헤더는 Allow 헤더 뿐이다.
0325
any의 진화를 이해하기
타입스크립트에서 일반적으로 변수의 타입은 변수를 선언할 때 결정된다.
그 후에 정제될 수 있지만, 새로운 값이 추가되도록 확장할 수는 없다. 그러나 any 타입과 관련해서 예외인 경우가 존재한다.
function range(start: number, limit: number) {
const out = []; // 타입이 any[]
for (let i = start; i < limit; i++) {
out.push(i);
}
return out; // 타입이 number[]
}
- 처음에는 any 타입 배열인
[]로 초기화되었는데, 마지막에는number[]로 추론되고 있다. - out의 타입은
any[]로 선언되었지만 number 타입의 값을 넣는 순간부터 타입은number[]로 진화한다. - 타입의 진화는 타입 좁히기와 다른다. 배열에 다양한 타입의 요소를 넣으면 배열의 타입이 확장되며 진화한다.
- 또한 조건문에서는 분기에 따라 타입이 변할 수도 있다.
변수의 초깃값이 null인 경우도 any의 진화가 일어난다. 보통은 try/catch 블록 안에서 변수를 할당하는 경우에 나타난다.
const result = [];
result.push("a");
result.push(1);
let val = null; // 타입이 any
try {
val = 12; // 타입이 number
} catch (e) {
console.warn("alas");
}
val; // 타입이 number | null
- any 타입의 진화는 noImplicitAny가 설정된 상태에서 변수의 타입이 암시적 any인 경우에만 일어난다. 그러나 명시적으로 any를 선언하면 타입이 그대로 유지된다.
- 암시적 any 상태인 변수에 어떠한 할당도 하지 않고 사용하려고 하면 암시적 any 오류가 발생하게 된다.
- any 타입의 진화는 암시적 any 타입에 어떤 값을 할당할 때만 발생한다. 그리고 어떤 변수가 암시적 any 상태일 때 값을 읽으려고 하면 오류가 발생한다.
- 암시적 any 타입은 함수 호출을 거쳐도 진화하지 않는다.
- any를 진화시키는 방식보다 명시적인 타입 구문을 사용하는 것이 안전한 타입을 유지하는 방법이다.
모르는 타입의 값에는 any 대신 unknown을 사용하기
함수의 반환값과 관련된 unknown
YAML 파서인 parseYAML 함수를 작성한다고 가정해보자
function parseYAML(yaml: string): any {
// ...
}
interface Book {
name: string;
author: string;
}
const book = parseYAML(`
name: aksje
author: emeifmn
`);
alert(book.title);
book("read");
- 이 경우 함수의 반환값에 타입 선언을 강제할 수 없기 때문에, 호출한 곳에서 타입 선언을 생략하게 되면 book 변수는 암시적 any 타입이 된다.
- 그렇기에 book 변수의 잘못된 프로퍼티에 접근하거나 했을 때, 적절한 경고를 내주지 못하고 런타임에 에러를 발생시킨다.
- 대신 parseYAML이 unknown 타입을 반환하게 만드는 것이 더 안전하다.
타입 체커는 집합 기반이기 때문에 any를 사용하면 타입 체커가 무용지물이 된다는 것을 주의해야 한다.
unknown은 any 대신 쓸 수 있는 타입 시스템에 부합하는 타입이다.
any는 다음과 같은 두 가지 속성을 가지고 있다.
- 어떠한 타입이든 any 타입에 할당 가능하다.
- any 타입은 어떠한 타입으로도 할당 가능하다.
- unknown 타입은 any의 첫 번째 속성을 만족하지만, 두 번째 속성(unknown은 오직 unknown과 any에만 할당 가능)은 만족하지 않는다.
- 반면 never 타입은 첫 번째 속성은 만족하지 않지만, 두 번째 속성은 만족한다.
- unknown 타입인 채로 값을 사용하면 오류가 발생한다. unknown인 값에 함수 호출을 하거나 연산을 하려고 해도 마찬가지이다. unknown 상태로 사용하려고 하면 오류가 발생하기 때문에, 적절한 타입으로 변환하도록 강제할 수 있다.
변수 선언과 관련된 unknown
어떠한 값이 있지만 그 타입을 모르는 경우 unknown을 사용한다.
예를 들어, GeoJSON 사양에서 Feature의 properties 속성은 JSON 직렬화가 가능한 모든 것을 담는 잡동사니 주머니 같은 존재이다.
그래서 타입을 예상할 수 없기 때문에 unknown을 사용한다.
interface Feature {
id?: string | number;
geometry: Geometry;
properties: unknown;
}
- 타입 단언문이 unknown에서 원하는 타입으로 변환하는 유일한 방법은 아니다.
- instanceof를 체크한 후 unknown에서 원하는 타입으로 변환할 수 있다.
- 또한 사용자 정의 타입 가드도 unknown에서 원하는 타입으로 변환할 수 있다.
function isBook(val: unknown): val is Book {
return (
typeof val === "object" && val !== null && "name" in val && "author" in val
);
}
- unknown 타입의 범위를 좁히기 위해서는 많은 노력이 필요하다.
- in 연산자에서 오류를 피하기 위해 먼저 val이 객체임을 확인해야 하고, null이 아님을 확인해야 한다.
- 제너릭을 사용한 스타일은 타입 단언문과 달라 보이지만 기능적으로는 동일하다. 제너릭보다는 unknown을 반환하고 사용자가 직접 단언문을 사용하거나 원하는 대로 타입을 좁히도록 강제하는 것이 좋다.
단언문과 관련된 unknown
이중 단언문에서 any 대신 unknown을 사용할 수 있다.
declare const foo: Foo;
let barAny = foo as any as Bar;
let barUnk = foo as unknown as Bar;
- barAny와 barUnk는 기능적으로 동일하지만, 나중에 두 개의 단언문을 분리하는 리팩터링을 한다면 unknown 형태가 더 안전하다.
- any의 경우는 분리되는 순간 그 영향력이 퍼지게 되지만, unknown의 경우는 분리되는 즉시 오류를 발생하게 되므로 더 안전하다.
unknown, object, {}
unknown과 비슷한 방식으로 object 또는 {}를 사용하는 코드들이 존재한다.
object 또는 {}를 사용하는 방법 역시 unknown만큼 범위가 넓은 타입이지만, unknown보다는 범위가 약간 좁다.
- {}타입은 null과 undefined를 제외한 모든 값을 포함한다.
- object 타입은 non-primitive 타입으로 이루어진다.
- 정말로 null과 undefined가 불가능하다고 판단되는 경우만 unknown 대신 {}를 사용하면 된다.
몽키 패치보다는 안전한 타입 사용하기
자바스크립트 특징 중 하나는, 객체와 클래스에 임의의 속성을 추가할 수 있다는 것이다.
객체의 속성을 추가할 수 있는 기능은 종종 웹 페이지에서 window나 document에 값을 할당하여 전역 변수를 만드는 데 사용된다.
또는 DOM 엘리먼트에 데이터를 추가하기 위해서도 사용된다.
사실 객체에 임의의 속성을 추가하는 것은 좋은 설계가 아니다. 예를 들어 window에 데이터를 추가하면 그 데이터는 기본적으로 전역 변수가 된다.
전역 변수를 사용하면 의도치 않은 부분들 간에 의존성을 만들게 되어 사이드 이펙트를 고려해야 한다.
타입스크립트를 더하면 또 다른 문제가 발생하는데, 임의로 추가한 속성에 대해서 타입스크립트는 알지 못한다.
이 오류를 해결하는 가장 간단한 방법은 any 단언문을 사용하는 것인데, 이는 타입 안정성을 상실하고, 언어 서비스를 사용할 수 없게 된다는 단점이 존재한다.
최선의 해결책은 document 또는 DOM으로부터 데이터를 분리하는 것이다.
분리할 수 없는 경우(객체와 데이터가 붙어 있어야만 하는 라이브러리를 사용 중이거나 자바스크립트 애플리케이션을 마이그레이션 하는 과정 중이라면), 두 가지 차선책이 존재한다.
- interface의 특수 기능 중 하나인 보강을 사용한다.
- 더 구체적인 타입 단언문을 사용한다.
보강
보강을 사용한 방법이 any보다 나은 점은 다음과 같다.
- 타입이 더 안전하여 타입 체커는 오타나 잘못된 타입의 할당을 오류로 표시한다.
- 속성에 주석을 붙일 수 있다.
- 속성에 자동완성을 사용할 수 있다.
- 몽키 패치가 어떤 부분에 적용되었는지 정확한 기록이 남는다.
모듈 관점에서(타입스크립트 파일이 import/export를 사용하는 경우), 제대로 동작하게 하려면 global 선언을 추가해야 한다.
export {};
declare global {
interface Document {
monkey: string;
}
}
- 보강을 사용할 때 주의할 점은 모듈 영역과 관련이 있다.
- 보강은 전역적으로 적용되기 때문에, 코드의 다른 부분이나 라이브러리로부터 분리할 수 없다.
구체적인 타입 단언문
interface MonkeyDocument extends Document {
monkey: string;
}
(document as MonkeyDocument).monkey = "hi";
- MonkeyDocument는 Document를 확장하기 때문에 타입 단언문은 정상이며 할당문의 타입은 안전하다.
- 또한 Document 타입을 건드리지 않고 별도로 확장하는 새로운 타입을 도입했기 때문에 모듈 영역 문제도 해결할 수 있다.
- 따라서 몽키 패치된 속성을 참조하는 경우에만 단언문을 사용하거나 새로운 변수를 도입하면 된다.
타입 커버리지를 추적하여 타입 안정성 유지하기
noImplicitAny를 설정하고 모든 암시적 any 대신 명시적 타입 구문을 추가해도 any 타입과 관련된 문제들로부터 안전하다고 할 수 없다.
any 타입이 여전히 프로그램 내에 존재할 수 있는 두 가지 경우가 있다.
- 명시적 any 타입
any[]와{ [key: string]: any] }같은 타입은 인덱스를 생성하면 단순 any가 되고 코드 전반에 영향을 미친다.
- 서드파티 타입 선언
- @types 선언 파일로부터 전파되기 때문에 특별히 조심해야 한다.
any 타입은 타입 안정성과 생산성에 부정적 영향을 미칠 수 있으므로, 프로젝트에서 any의 개수를 추적하는 것이 좋다.
npm의 type-coverage 패키지를 활용하여 any를 추적할 수 있다.
npx type-coverage
npm type-coverage --detail
프로젝트에서 any가 아닌 타입의 백분율을 출력해준다. --detail 플래그를 붙이면, any 타입이 있는 곳을 모두 출력해 준다.
작성한 프로그램의 타입이 얼마나 잘 선언되었는지 추적해야 한다. 추적함으로써 any의 사용을 줄여 나갈 수 있고, 타입 안정성을 꾸준히 높일 수 있다.
devDependencies에 typescript와 @types 추가하기
모든 타입스크립트 프로젝트에서 공통적으로 고려해야 할 의존성 두 가지
타입 스크립트 자체 의존성
타입스크립트를 시스템 레벨로 설치할 수도 있지만, 다음 두 가지 이유 때문에 좋지 않다.
- 팀원들 모두가 항상 동일한 버전을 설치한다는 보장이 없다.
- 프로젝스틀 셋업할 때 별도의 단계가 추가된다.
따라서 타입스크립트를 시스템 레벨로 설치하기보다는 devDependencies에 넣는 것이 좋다.
타입 의존성(@types)
- 사용하려는 라이브러리에 타입 선언이 포함되어 있지 않더라도, DefinitelyTyped에서 타입 정보를 얻을 수 있다.
- DefinitelyTyped의 타입 정의들은 npm 레지스트리의 @types 스코프에 공개된다.