Post

웹 페이지의 성능과 CDN

웹 개발을 하다보면 항상 나오는 것들이 성능에 대한 이슈이다.
이 성능 이슈는 매우 다양한 양상으로 나타나는대 크게 나뉘면 두 가지다.

1. 클라이언트에서 렌더링이 늦다.

인터넷 브라우저를 통해 웹 페이지에 접속하게 되면 브라우저는 해당 서버에 html,javascript,css를 요청하여 응답받은 텍스트 데이터를 파싱(구문 분석)하여 웹 페이지를 그리게 된다.

html을 이용하여 DOM 트리를 형성하고, CSS를 이용하여 CSSOM을 형성하는데 이 두 개를 결합하여 화면에 표기되는 것들을 정리해둔 렌더 트리를 형성한다.

이 렌더 트리를 기반으로 레이아웃을 계산한 뒤 브라우저 화면에 HTML을 페인팅하는 과정을 거친다. 그렇다면 클라이언트에서 렌더링이 늦다는 것은 HTML,JAVASCRIPT,CSS를 파싱하여 브라우저에 그리는 것이 늦다는 것으로 그 이유로는 아래의 두가지를 뜻한다.

1-1 표기할 개체가 많다.

렌더 트리에 추가할 DOM 객체가 많다면 당연하게도 렌더 트리를 생성하는 시간이 길어지고, 렌더 트리가 커지면 브라우저에 레이아웃을 잡고 HTML을 시각화하는데 시간이 많이 걸리는 것은 당연한 일이다. 하지만 경험상 만 단위의 HTML을 생성해서 그려넣는게 아니라면 일반적인 HTML로 느려지는 경우는 잘 없었으며 CSS의 경우도 시각적으로 연산해야하는 효과가 많을 경우 느려지는 문제가 생겼었다.

1-2 렌더 트리 변경이 잦다.

화면을 구성하는데 HTML과 CSS만 사용한다면 초기에 DOM과 CSSOM을 만들고 렌더 트리를 만들어서 그리는데 까지만 기다리면 화면을 볼 수 있겠지만 Javascript를 이용해서 HTML을 조작하는 경우가 많기 때문에 실상 성능이 떨어진다는건 이런 경우가 대부분이다.

아래와 같은 경우를 보자.

1
2
3
4
5
6
let dataList // 표기해야할 데이터가 담겨있는 array
let parent = document.getElementById("example")

for (let i=0;i<2000;i++) {
    parent.innerHtml += "<div>"+dataList[i]+"</div>"
}

반복문을 통해서 렌더트리를 자주 변경하는 경우인데, 이런 경우 렌더 트리의 잦은 변경과 동시에 새로 레이아웃을 계산하고 다시 그려야하기 때문에 실질적으로 렌더링이 늦어지는 효과를 가져온다.

그렇다면 어떻게 하면 성능이 올라갈까? 한번에 만들어서 한번에 넣는 방식이 좋다. 그러면 렌더 트리 변경과 브라우저 리페인트를 최소로 줄일 수 있기 때문이다.

해결법이 나왔으니 그대로 해보자면 아래와 같다.

1
2
3
4
5
6
7
let tempHtmlStr = ""

for (let i=0;i<2000;i++) {
    tempHtmlStr += "<div>"+dataList[i]+"</div>"
}

document.getElementById("example").innerHTML = tempHtmlStr

HTML을 string으로 꾸며서 innerHtml로 변경하는 방법이 있는데, 사실 innerHtml의 경우 DB에서 갖고오거나 외부 데이터를 갖고와서 그리는 경우 보안상의 취약점이 발생할 수 있다. 물론 적절한 Filter를 프론트나 백엔드 단에서 처리해준다면 문제 없겠지만 그래도 아예 그럴 가능성을 원천 차단하는게 개인적으로는 좋은 방법이라고 생각한다.

그렇기 때문에 개인적으로 정말로 필요한 경우가 아니라면 아래와 같이 하는게 좋다고 생각한다.

1
2
3
4
5
6
7
8
9
let tempParent = document.createDocumentFragment()

for (let i=0;i<2000;i++) {
    let tempHtml = document.createElement("div")
    tempHtml.innerText += i
    tempParent.appendChild(tempHtml)
}

document.getElementById("example").appendChild(tempParent)

document.createDocumentFragment()를 통하여 임시로 html을 담은 뒤 해당 parent의 child로 귀속시켜서 한번에 처리하는 방식인데 이 경우 innerHtml을 사용하지 않아 좀 더 안전하고, 각 dom에 대해 설정값을 변경할때 좀 더 가시성 있게 변경 할 수 있어 좋다고 생각한다.

2. 서버에서 답변이 늦다.

백엔드의 경우 답변이 늦는 경우를 3가지로 나뉠 수 있다.

2-1. DB에서 서버로 데이터를 보내는 게 늦다.

이 경우라면 DB 성능에 대한 문제인데, 대부분 쿼리가 적절하지 않게 짜여있을 가능성이 높다 필요 없는 데이터까지 요청해서 데이터 크기가 크다거나, 효율적이지 않은 쿼리를 요청해서 해당 데이터를 DB에서 만드는데 오래 걸린다던가 하는 경우이다. 아니면 서비스 특성상 이러한 느린 쿼리를 써야하고 write 성능 보다 read 성능을 우선시 해야하는 경우에는 DB에 index를 걸어 Search 속도를 올려야 하는 경우가 있는데 이러한 Indexing이 제대로 되어있지 않아서 그런 경우가 있다.

2-2. 서버에서 연산량이 많아서 오래 걸린다.

이 경우 서버에서 연산량이 많은 것으로 이건 원래 그런 형태가 아니라면 알고리즘 개선을 하는 수밖에 없다. 이건 해당 케이스가 어떤 케이스냐에 따라 다른 것이므로 천편일률적으로 논할순 없다. 하지만 아래와 같이 대략적인 솔루션은 있다.

2-2-1. 미리 연산을 해두고 정말 필요한 부분만 다시 연산하여 반환한다.

이 연산을 미리 해두는 방식은 사실 실제로도 많이들 사용하는 방법이다. DB의 특정 테이블에 특정 칼럼을 다 더한 값이 필요한데, 전체를 다 더해서 갖고 오기에 시간이 너무 걸릴 경우 아예 해당 테이블에 행을 추가할때 어느 한 구석에 SUM 값에 추가할 값을 더하고 추가하는 경우가 있다. SQL DB는 어떤지 모르겠지만 No SQL DB에선 성능 향상을 위해 권장되는 구조이기도 하다.

2-2-2. 연산을 작은 단위로 쪼갠 뒤 합친다.

빅데이터를 이런식으로 처리한다. 사실 이런건 작은 단위로 쪼갤 수 있는 형태의 계산만 가능하다. ex) 연속 덧셈 연산, 연속 곱셈 연산, 한번에 데이터 가져와서 추가적인 연산이 필요한 경우 등 그리고 작은 단위지만 서버에서 해야하는 연산이 있는 경우 따로 추가적인 솔루션을 도입하는데 그건 아래에서 계속 이야기하겠다.

2-3. 서버에서 클라이언트로 보내는게 늦다.

이 경우가 사실 진짜 제대로 말하고 싶었던 케이스인데 이 세번째의 경우 역시 두 가지로 나뉜다.

2-3-1. 데이터가 크다

2-3-2. 데이터 리소스가 멀리 있다.

2-3-1, 2-3-2번의 경우도 결국에는 둘다 캐싱으로 해결 할 수 있는데 이러한 캐싱에는 브라우저 캐싱도 있지만 이 브라우저 캐싱은 보관할 수 있는 콘텐츠가 한정적이고 캐싱되기 전 최초 한번은 느리다는 단점이 있다. 이럴때 필요한 게 캐시 서버인데, 이 캐시 서버라는건 콘텐츠를 원 서버가 아닌 서비스 지역에서 가까운 곳에서 받아 올 수 있도록 별도로 개설된 서버로 아주 큰 회사라면 아예 캐시 서버 자체를 자체 인프라와 함께 서비스 지역에 놔두겠지만 (ex - 유튜브, 넷플릭스), 이럴 경우 서버 비용과 인프라 유지비용이 너무 커서 어지간히 큰 회사 아니라면 불가하다. 그럴때 현실적으로 생각해볼 수 있는게 바로 CDN이다.

CDN

CDN은 Content Delivery Network의 약자로 간단히 말해서 세계 각지에 서버를 두고 해당 요청이 들어왔을 때 요청한 클라이언트와 가장 가까운 서버에서 해당 데이터를 보내주는 방식이다. 이러한 CDN 서비스는 AWS나 AKAMAI등 큰 회사에서 제공을 하고 있다.

이러한 서비스를 AWS에서는 Amazon CloudFront라는 이름으로 CDN을 지원하고, AKAMAI의 경우 CDN이라고 지원한다.
나는 AKAMAI쪽의 CDN만 써봤기에 AKAMAI CDN을 기준으로 설명을 하자면 먼저 인증서를 업데이트하고, CDN 서버쪽에 원래 서버 정보를 등록해둔다. ORIGIN 정보가 달라지면 CORS 이슈로 인해 CDN 서버에서 보낸 데이터를 사용하지 못하기 때문이다. 그리고 CDN 서버에 내가 캐싱하고 싶은 콘텐츠를 올린다. 이렇게 콘텐츠를 올리는 것도 스테이지에 올리고 테스트 한 이후에 별도로 활성화를 시킬 수 있다. 이렇게 활성화를 시키는데는 시간이 걸리는데 이는 각지에 있는 AKAMAI CDN서버에 해당 내용을 배포하기 때문에 시간이 걸리면 평균적으로 20분 정도가 소요된다.

이렇게 활성화된 서버는 차후 요청이 올때 원 서버가 아닌 CDN서버가 요청에 응답하게 되며 그 요청을 받는 것은 물리적으로 가까운 위치에 있는 CDN서버에서 응답하기 때문에 실질적으로 성능이 올라가게 된다.

그런데 단순히 콘텐츠 요청만 여기서 처리하지 않고 연산이 필요한 요청 자체를 각지에서 받으면 안될까? 이러한 생각이 앞서 언급한 2-2-2에 대한 부분이고, 그러한 아이디어를 구현할때 필요한게 Edge computing이다

Edge Computing

이전에 글로벌 서비스의 성능을 향상 시키기 위해서 도입을 고민해보았지만 비용이 너무커서 사용하지 않은 서비스이다.
AWS에서는 Lambda@edge라는 이름으로 AKAMAI에서는 Edge Computing이라는 이름으로 서비스를 제공하고 있다.
요청을 받을 때 작업할 코드를 세계 각지에 있는 CDN 서버에 올려서 구동할 수 있다. 이를 에지 컴퓨팅이라고하며 기본적으로 CDN 설정이 되어있어야만 사용할 수 있다. 자체적으로 요청 간에 API 함수를 지원하며 이에 따라 다른 작업을 할 수 있는데 이렇게 작업한 뒤 서버에 코드를 올리고 20분은 있어야 활성화 된다.

처음에 해당 서비스 도입을 고려할 때 사용자가 접속하는 위치에 따라 다른 서비스를 제공할 생각으로 고민했던 건데 가격 문제와 서비스 적합성을 이유로 도입하지 않고 GTM을 도입했었다.

GTM

Global Traffic Management의 약자로 간단히 말해서 접속하는 위치마다 다른 서버로 연결 할 수 있게 해주는 서비스이다. AWS에서도 비슷하게 ROUTE 53에서 지리기반 설정을 하여 동일하게 구현이 가능하다. 하지만 이 GTM의 경우 사용자가 어떤 DNS 서버로 접속하느냐를 기준으로 라우팅을 하는 것이기 때문에 사용자 컴퓨터의 DNS 서버 설정에 따라 원하는대로 라우팅이 제대로 될 수도 안될 수도 있다. 가령 한국의 경우 KT, SKT, LG 3사의 DNS서버가 아닌 클라우드 플레어나 구글 DNS 서버를 사용하게 된다면 해당 클라이언트의 위치를 제대로 찾아낼 수 가 없다. 만약 해당 지리 위치를 완벽하게 처리해야하는 경우라면 IP기반의 지리적 설정을 할 수 있는 AWS의 서비스를 사용하는게 낫다.

This post is licensed under CC BY 4.0 by the author.