웹어플리케이션 아키텍처는 시스템의 구축과정에서, 웹어플리케이션 구성요소들을 어떤 원칙 하에 분할하고, 상호 작용하게 할 것인가를 정의한다. 시스템이 대형화 할수록 그 구성요소들의 특성과 종류는 다양하게 구분되고, 구성요소 간의 상호 작용은 복잡해 진다. 이러한 구성요소의 분할 및 구분과 구성요소간 상호작용은 결과적으로 시스템의 기능적, 성능적 안정성과 유지보수성을 지배하게 된다. 웹어플리케이션의 특성에 맞는 아키텍처를 정의하고 이 원칙에 부합하는 분석, 설계 모델을 작성하여 시스템을 구축하여야 한다. 이러한 아키텍처 이슈는 다른 어떠한 시스템 구축에 관한 이슈보다도 중요하다.
웹어플리케이션 아키텍처에 대한 개념을 정립하고 일반적인 개발 구조와 패턴들에 대해 알아보고 개발에 실제 적용할 아키텍처를 설계한다. 목차는 다음과 같다.
다음은 웹 어플리케이션 아키텍처를 정의하기 위해 적용해야 할 기본적인 사항이다.
-
유지보수와 재사용을 향상시킬 수 있는 구조로 정의되어야 한다.
-
이해하기 쉽게 설계되어야 한다.
-
이를 위해서 층에 의한 분할구조(Layered Architecture)를 기본으로 한다. 이것은 아래 설명할 아키텍처의 확장과도 관계된다.
-
각 층은 관심의 분리(Separation Of Concern) 원칙을 통해 구성요소간의 의존성을 줄이고, 독립성을 보장하는 방향으로 설계되어야 한다.
어플리케이션 개발에 있어서 가장 중요한 것은 최종 사용자에게 신뢰성 있는, 유용한, 그러면서도 정확한 소프트웨어를 제공하는 것이다. 소프트웨어는 기능적인 요구사항(functional requirement)을 완벽하게 충족시켜야 하며 또한 성능(Performance), 모듈화(Modularity), 유연성(Flexibility), 유지보수성(Maintainability), 확장성(Extensibility) 등과 같은 비기능적인 요구사항(non-functional requirement) 역시 충족해야 한다.
아키텍처의 설계에 있어서 중요한 요소 중 하나는 바로 아키텍처의 확장성을 고려한 설계다. 대부분의 비즈니스 서비스 환경은 기존의 기본적인 고유 업무에 머무는 것이 아니라 다양한 형태의 정보 및 데이터 분석을 통해 그 결과를 업무에 적극 활용함으로써 최상의 생산성을 추구하는 방향으로 급격히 변화하고 있다. 또한 이들의 정보욕구는 갈수록 다양화, 양질화되어 가는 추세에 있으므로 처리돼야 할 서비스의 종류가 증가되며 따라서 시스템 처리용량 자체도 증가될 수밖에 없다.
따라서 확장에 용이한 구조로 설계되지 않는다면 필연적으로 한계에 다다를 수밖에 없고 재구축이라는 막대한 비용이 추가로 발생할 수밖에 없어 이런 요소를 감안할 때 시스템 아키텍처는 반드시 확장에 유연한 구조로 설계돼야 한다.
확장의 유연성이라는 측면에서 볼 때 물리적(하드웨어적) 아키텍처의 구성은 수평적, 그리고 수직적 확장이 가능한 형태로 구성될 수 있다. 수평적 확장은 노드수를 증가시키는 형태를 의미하며 수직적 확장은 CPU, 메모리 증가 또는 tpmC가 높은 다른 CPU로 업그레이드를 통해 전체적인 용량을 확장하는 케이스를 의미한다. 일반적으로 웹 서버의 경우, 특성상 다수의 커넥션(Connection)을 동시에 처리하는 형태이며 Connectionless의 구조를 가지고 있으므로 이에 대한 처리는 수직적인 확장보다는 수평적인 확장이 더욱 유리한 경우에 해당할 수 있다.
따라서 노드의 특성상 확장구조가 다르기 때문에 티어(Tier)의 분리는 반드시 필요하며 또한 분리된 노드에서 각기 서로 다른 애플리케이션 로직 처리를 담당하게 되면(예를 들어 Web Server 노드에서는 UI로직을, Application Server에서는 비즈니스 로직만을 담당해 처리함) 각 모듈간의 독립성을 높일 수 있고, 비즈니스의 변경에 따른 변경사항도 최소화될 수 있어서 그에 따른 관리 및 운영의 효율성이 증대될 수 있다.
(Improving availability by clustering the WebSphere Application Server 참고)
수직 스케일링 토폴로지는 보통 클러스터 구성원을 작성하여 한 시스템의 여러 Application Server 설정을 참조한다.
다음 그림에서는 수직 스케일링 토폴로지의 예를 보여준다(수직 스케일링 토폴로지 참고).
수평 스케일링은 Application Server 클러스터의 구성원이 여러 실제 시스템에 있을 때 존재한다. 하나의 응용프로그램을 여러 시스템의 클러스터 구성원에 걸치면서도 단일 시스템 이미지를 나타내도록 할 수 있다.
다음 그림에서는 수평 스케일링 예를 보여준다(수평 스케일링 토폴로지 참고).
언급한 바와 같이 시스템의 규모가 큰 경우 가장 일반적인 아키텍처는 하드웨어를 기능별로 계층(layer) 또는 티어(tier)로 구성하는 방식이다. 따라서 계층화된 소프트웨어 아키텍처에 채택할 경우 다음과 같은 잇점이 있다.
-
차후 티어에 서버를 추가함으로서 처리효율(throughput)을 높이는 가장 좋은 방식으로 알려져 있는 수평적 확장(horizontal scaling, 위 그림 참고)을 고려할 수 있다.
-
주로 방문 수가 증가하면 부하를 분산하기 위해 jsp, servlet이 포함된 웹티어(프리젠테이션 티어)에 서버를 추가한다.
-
주로 더 많은 처리를 해야 하는 경우 비즈니스 티어에 서버를 증설한다.
계층화된 아키텍처(Layered Architecture)는 다음 특성을 보장해야 한다
-
시스템을 대상으로 층을 형성하는 컴포넌트의 그룹으로 구조화 시킨다.
-
상위의 층은 단지 바로 하위 층의 서비스를 사용하도록 한다. 반대로 되는 상황이 있어서는 안된다.
-
바로 아랫층의 서비스를 사용하는 것이외의 다른 것을 금한다. 층을 건너 다른 층의 서비스를 사용하지 말아야한다. 이경우 단지 연계만을 목적으로 하는 중간 층을 정의하여 사용한다.
전형적인 J2EE 분산 환경의 멀티티어 어플리케이션은 아래 그림과 같다(J2EE Platform참고).
Client Tier는 UI를 제공하고, 하나 또는 그 이상의 Middle Tier 모듈은 응용 프로그램의 비즈니스 로직을 포함하며 서비스를 제공한다. back-end EIS Tier는 데이터 관리를 제공한다.
다양한 기술적 결정 사항들을 고려하여 어플리케이션 아키텍처를 정의한다. 사용자 화면(UI)에서 데이터베이스 접근을 포함하는 백엔드 구현부까지 필요한 역할을 판단하고 Layer를 분할하도록 한다. 또한 이러한 각 Layer를 기준으로 하여 유형화된 어플리케이션의 패턴을 정의하도록 한다. 이러한 패턴은 실제 개발에 있어 어떤 코드가 어디에 위치해야 하는지에 대한 가이드라인을 제시하고 개발자들로 하여금 중복 코드 작성을 자연스럽게 피할 수 있도록 하며 어플리케이션 로직의 구현에 집중할 수 있도록 하여야 한다.
J2EE에서 이러한 것을 충족시키기 위한 소프트웨어 디자인 원칙은 다음과 같다.
-
인터페이스를 사용하라 인터페이스를 통하여 객체 간의 계약 관계를 표현하는 것이 좋다. 인터페이스는 다형성(polymorphism)을 갖는다.
-
관심 영역의 분리 및 응집성 소프트웨어의 특정 기능만을 특화시켜 컴포넌트화 한다면 개발 효율이 증대되고 유지 보수가 쉬워진다. 또한 재사용성이 증대된다. 관심 영역을 분리(Separation Of Concern)하게 되면 자연스럽게 응집도(Cohesion, 하나의 클래스가 하나의 단위작업 또는 목적에 얼마나 충실한가 하는 정도)도 높아진다.
-
복잡성을 숨겨라 예를 들어, 시스템이 검색 서비스를 사용해야 하는 경우, 실제 구현 부분을 하나의 컴포넌트로 구현하여 다른 모든 컴포넌트들이 이를 사용하게 하는 방식이 최선일 것이다. 이러한 접근법은 시스템 컴포넌트를 단순화하는 효과를 가져온다. 사실, 복잡성을 숨기라는 것과 관심 영역의 분리는 유사한 주제이다.
-
느슨한 결합도(Loose Coupling) 객체지향 시스템은 그 본성상 객체 간의 대화로 이루어진다. 이 대화에 인터페이스를 이용하면, 두 개의 클래스가 서로 대화하기 위하여 서로 간에 알아야할 것들을 줄여준다. 두 클래스가 서로에 대하여 알아야 할 것이 적을 수록 서로 간의 결합도는 느슨해진다.
-
원격 프록시(Remote Proxy) 시스템의 규모가 커지게되면 기존 서버의 사이즈를 늘려 하나의 서버를 거대화하기 보다는 새로운 서버를 도입하여 문제를 푸는 것이 현실이다. 이것이 가능하려면 서로 다른 서버 위에서(서로 다른 heap 영역에서) 돌아가는 자바 객체들이 통신해야 한다. 이러한 환경에서 지역 객체인 프록시는 실제 서비스 객체와 통신하는데 필요한 모든 복잡한 내용을 내부에 숨긴다.
-
선언적인 제어를 많이 사용하라 J2EE 컨테이너의 능력을 십분 활용하여 배포 서술자(Depoy Descriptor)를 사용한 선언적 제어를 가능하게 한다. 이것은 코드 수정 없이 DD를 수정하는 것만으로 시스템의 행동을 변경할 수 있도록 한다.
사용자 화면(UI)의 로직과 비즈니스 로직은 어떻게 구현하고 어디에 위치시킬것인가, 그리고 어플리케이션에 필요한 데이터 및 어플리케이션의 상태는 어떻게 유지하고 공유할 것인가 등에 대해서 정의하고 각 계층으로 분할한다. 계층의 분할에 있어서, 각 계층의 역할과 각 계층을 연결하기 위해 사용할 기술 요소들과 특정 계층을 교체하거나 변경할 경우 다른 계층들에 영향을 주지 않을 정도의 유연성에 대해서 반드시 고려하여야 한다.
JSP 명세에서는 jsp 페이지를 사용한 웹 어플리케이션을 개발하기 위한 두가지 접근법을 제시하고 있다. JSP Model 1과 Model 2 아키텍처가 그것들인데, 작업 처리의 위치에 있어서 차이를 보이고 있다(Servlets and JSP Pages Best Practices 참고).
Model 1 아키텍처는 아래 그림에서와 같이 Jsp 페이지가 요청 처리와 클라이언트에 응답을 보내는 책임을 갖고 있다.
Model 2 아키텍처는 아래 그림에서와 같이 Servlet 및 Jsp 페이지들을 통합하며, jsp 페이지들은 Presentation layer를 구성하며 servlet들은 작업 처리를 책임지고 있다. servlet은 요청 처리 및 jsp에서 필요한 bean을 생성하고 또한 요청을 포워드하기 위해 어떤 jsp를 보여줄지를 결정하는 controller로서 행동한다. jsp 페이지는 servlet에서 생성된 객체를 받아서 동적인 컨텐츠를 구성하게 된다.
Model 2 아키텍처는 Model-View-Controller(MVC) 패턴으로도 잘 알려져 있다. 아래 그림은 MVC 응용프로그램에서의 모델, 뷰, 컨트롤러 등의 계층들의 관계및 기능을 묘사하고 있다(J2EE Blueprints 참고).
MVC의 특징은 다음과 같다.
-
컨트롤러와 모델과는 독립적으로 뷰를 수정할 수 있다.
-
모델 컴포넌트는 뷰와 컨트롤러 컴포넌트로부터 데이터 구조와 같은 내부적인 상세한 사항을 숨긴다.
-
모델에 인터페이스를 가능한 한 사용하면,
GUI 또는 J2ME와 같은 영역에서도 재사용이 가능하다.
-
컨트롤러에서 모델 코드 부분을 분리하면 원격 비즈니스 컴포넌트 사용으로 옮겨가는 것이 수월하다.
다음은 MVC의 구성 요소(Model, View, Controller)들이 갖는 기능에 대한 설명이다.
-
외부로부터의 어플리케이션에 대한 요구를 받아들이는 부분인 동시에 처리 결과를 사용자에게 보여 주는 부분이다.
-
Event-driven 방식의 GUI에 기반한 프리젠테이션을 포함하고 있으며, 이벤트 처리 및 데이터 포맷팅을 책임진다.
-
뷰 층의 구성요소는 Controller의 구성 요소와 상호 작용한다.
-
뷰 층으로부터의 이벤트를 메시지로 전환하여 Model에게 전달하는 사용자 입력에 대한 응답 메카니즘을 포함한다.
-
사용자 요청에 대한 응답을 위해 내부에서 처리하는 부분이다.
-
Model 층이 담당하는 Data의 직접적 조작은 제외되며 구성 요소간 처리흐름의 제어, 데이터 조작 의뢰, 데이터의 변환 및 연산 등을 포함한다.
-
어플리케이션에 필요한 지속성 있는 데이터를 조작한다.
-
데이터베이스/파일의 데이터를 조작하는 기능을 수행한다.
-
인터페이스를 통해 데이터의 직접적 조작(데이터의 입력, 수정, 삭제, 조회)을 담당한다.
Model-View-Controller(MVC) 패턴은 계층화된 아키텍처(Layered Architecture)의 전형을 보여주는 좋은 사례이며, 어플리케이션 아키텍쳐의 기본 모델로 적용 가능한 디자인 패턴이다. Model, View, Controller로 구성되며 이러한 구성은 각 계층을 분리 개발함으로서 UI에서 로직을 분리할 수 있으며, 또한 로직을 재사용할 수 있다. 나아가 개발 용이성과 유지보수에 있어서의 잇점 또한 얻을 수 있다.
프론트 컨트롤러(Front Controller)는 일반적인 MVC 패턴을 확장한 것이다. 일반화되고 반복적인 요청 처리 부분을 하나의 컴포넌트로 만들고자 할 때프론트 컨트롤러를 사용한다. 이 패턴을 사용하면 어플리케이션 컨트롤러는 훨씬 응집력이 있으며, 덜 복잡한 모습을 보인다(Front Controller 참고). 일반 MVC의 경우 동일한 패턴의 컨트롤러가 다수 만들어지는데, 이것은 작업 처리 부분의 복잡도를 증가시킬 수 있다. 프론트 컨트롤러는 웹 어플리케이션의 웹 티어를 하나의 컴포넌트로 제어하는 것으로, 어플리케이션의 모든 요청은 이 하나의 컴포넌트를 통해 어디로 갈지 제어가 가능해진다.(Scriptless JSP Pages: The Front Man 참고)

프론트 컨트롤러의 원칙과 특징은 다음과 같이 정리할 수 있다.
-
프론트 컨트롤러의 원칙
-
프론트 컨트롤러의 특징
-
웹 어플리케이션의 초기 요청 처리 작업을 하나의 컴포넌트로 집중화한다.
-
다른 패턴과 프론트 컨트롤러를 함께 사용하면 프리젠테이션 티어 디스패칭 작업을 선언적으로 할 수 있으므로 컴포넌트 결합도를 낮추는 역할을 한다.
-
프론트 컨트롤러의 대표적인 프레임워크는 Struts 또는 Spring MVC 이다.
컨트롤러가 원격 객체를 직접 호출하는 것이 아니라 중개자(Go-between)를 통해 호출한다. 이런 중개자를 비즈니스 델리게이트라고 한다. 비즈니스 델리게이트는 요청을 받은 다음 JNDI를 검색하고 스텁을 리턴 받은 후 비즈니스 메소드를 호출하는 기능을 수행한다. 또한 메소드 호출 시 발생한 원격 예외 사항을 처리 또는 제거하며 결과를 컨트롤러에 리턴한다. 웹 어플리케이션 모델 컴포넌트가 원격인 환경에서 웹 티어 컨트롤러를 보호하려면 비즈니스 델리게이트를 사용한다(Business Delegate 참고).
-
비즈니스 델리게이트의 원칙
-
비즈니스 티어 변경으로 인하여 웹 티어가 영향을 받는 것을 최소화한다.
-
두 티어 간의 결합 정도를 낮춘다.
-
어플리케이션에 새로운 계층을 하나 추가함으로써 복잡도를 높이는 결과를 가져온다.
-
비즈니스 델리게이트 메소드 호출은 네트워크 트래픽을 최소화하기 위해 큰 단위(coarse-grained) 호출이 바람직하다.
-
비즈니스 델리게이트의 특징
-
프록시로 행동한다. 원격 서비스의 인터페이스 구현
-
원격 서비스와 통신을 초기화한다.
-
통신 시 상세 사항 및 예외 사항을 처리한다.
-
요청을 해석하여, 이를 비즈니스 서비스로 넘겨준다(스텁을 통하여).
-
상세한 원격 컴포넌트 검색 및 통신을 처리함으로써 컨트롤러를 더욱 응집성 있는 컴포넌트로 만들어 준다.
비즈니스 델리게이트에서 JNDI 검색(lookup)하는 코드의 중복을 피할 수 있으며, 해당 부분을 서비스 로케이터로 데어냄으로서 비즈니스 델리게이트의 응집성을 높일 수 있다. 레지스트리 검색(lookup)을 위해 서비스 로케이터 패턴을 사용한다. 이를 사용하면 JNDI(또는 다른 레지스트리) 검색을 해야 하는 모든 컴포넌트(예를 들면 비즈니스 델리게이트)를 아주 단순하게 유지할 수 있다(Service Locator 참고).
-
서비스 로케이터의 원칙
-
서비스 로케이터의 특징
-
InitialContext 객체를 얻는다.
-
레지스트리 검색을 수행한다.
-
한번 읽은 객체 참조는 캐시(Cache)해 두어 성능을 향상기킴
-
다음과 같은 다양한 레지스트리와 작업함: JNDI, RMI, UDDI, COS 네이밍
트랜스퍼 오브젝트는 주로 직렬화된 전송 객체를 의미한다. 잘게 나누어진(fine-grained) 원격 컴포넌트(일반적으로 엔티티)를 대신하는 지역(로컬) 클래스를 제공하여 네트워크 트래픽을 감소하는데 트랜스퍼 오브젝트를 사용한다. 트랜스퍼 오브젝트를 사용하기 전에, 데이터의 일관성/동기화와 성능 둘 중 어느 것이 중요한지를 결정하는 것이 좋다(Transfer Object 참고).
-
트랜스퍼 오브젝트의 원칙
-
원격 컴포넌트의 데이터를 네트워크를 통하여 빈번하게 호출함으로써 발생하는 성능상의 문제로부터 웹티어를 보호한다.
-
티어 간의 결합 정도를 낮춘다.
-
트랜스퍼 오브젝트에 들어 있는 데이터가 최신 데이터가 아닌 시간이 지난 데이터라는 단점이 있다. 이는 데이터베이스 등 다른 곳에 저장된 특정 시점의 정보기 때문에 그렇다.
-
동시성이 보장된(concurrency-safe) 수정 가능한 트랜스퍼 오브젝트는 만들기가 꽤 복잡하다.
-
트랜스퍼 오브젝트의 특징
-
원격 엔티티에 대한 지역 대표 객체를 제공함(즉, 여러 데이터 정보를 통째로 가지고 있는 객체)
-
네트워크 트래픽을 최소화
-
자바 빈 명명법을 따르기 때문에 기타 객체들이 쉽게 접근할 수 있다.
-
네트워크를 가로 질러 전송될 수 있도록 직렬화된 객체를 구현한다.
-
일반적으로 뷰 컴포넌트에서 쉽게 접근할 수 있도록 만든다.
서블릿으로 보내지는 요청(Request)을 수정하기 위하여 또는 사용자에게 보낼 응답(Response)을 수정하기 위하여 인터셉팅 필터를 사용한다(Intercepting Filter 참고).
-
인터셉팅 필터의 원칙
-
인터셉팅 필터의 특징
-
서블릿에 도착하기 전 요청을 낚아채서, 수정할 수 있다.
-
응답이 클라이언트로 넘어가기 전 이를 낚아채서 수정할 수 있다.
-
필터는 DD를 사용하여 선언적으로 배포할 수 있다.
-
필터는 모듈화되어 있어, 연결하여 실행할 수 있다.
-
컨테이너가 필터 생명주기를 관리한다.
-
필터는 컨테이너 콜백 메소드를 구현해야 한다.
아래 그림은 비즈니스 델리게이트, 서비스 로케이터, 트랜스퍼 오브젝트 패턴을 적용한 예제를 비지오로 작성해본 것이다.
프로젝트의 개발 기간 단축과 비용 절감의 가장 중요한 요소는 한번 작성된 코드의 재사용 여부이다. 따라서 개발 지원을 위해 재사용 가능한 코드를 체계적인 framework로 관리할 필요가 있다. 프레임워크는 개발 시점에 프로젝트를 관리하고 빠르고 편리한 시스템 구성을 위하여 일반적이고 유용한 기능을 제공해야 하며, N-Tier 환경에서의 분산 응용프로그램 구조로 디자인된다. 또한 MVC 모델와 같은 계층화된 아키텍처를 적용하여 코드와 뷰를 분리한다.
여기서는 프로젝트에 향상된 기능 구현을 제공하면서도 보다 안정적이고 효율적인 개발을 수행을 위하여 프레임워크를 구현하기 위한 일반적인 목표와 구성 요건에 대해서 정리한다.
프레임워크의 설계를 위한 기본 명제와 목표는 다음과 같다.
-
framework는 가능한한 간단하게 디자인한다. framework는 최소한의 개체수를 유지하며 간단한 메소드 구현과 깊이 들어가지 않는 상속 계층 구조를 갖도록 디자인하는 것을 원칙으로 한다. KCF(Kico Core Framework, 가칭)는 lightweight framework(경량 프레임웍)이다. 중,소 규모의 프로젝트를 대상으로 설계하기 때문에 되도록 기본 기능 구현에 충실하도록 가볍게 유지되어야 한다. framework이 무거워질수록 부가적인 기능은 많아지겠지만 이것은 그만큼 개발자들이 알아야할 것이 많아진다는 것이다. 디자인의 복잡성으로 인해서 학습 기간이 길어지고 코드의 복잡도가 증가하여 이해하기 힘들어지는 단점이 있다. 또한 지금까지의 경험으로 볼때 있으면 좋지만, 반드시 필요한 필수 기능이 아닌 항목이 너무 많아지는 경향이 있다. 결국 개발 효율이 떨어지게 된다. 반면에 간단한 디자인일 수록 코드가 단순해지면서 그만큼 에러도 적어진다. 또한 이러한 것은 프로젝트의 구현 코드 자체도 단순화할 수 있게된다. 우리나라의 개발 환경은 인력 투입 즉시 개발 할 수 있는 환경이 되어야 한다. ^^
framework는 logging, configuration, database access, exception handling 등 몇개의 기본 핵심 컴포넌트들로 구성된다. 이들 핵심 컴포넌트는 시스템 기동시에 각 서비스를 띄우고 다른 컴포넌트들에서 이들 서비스를 사용할 수 있도록 하여 비즈니스 고유 구현부의 효율을 극대화할 수 있는 주요한 서비스를 제공한다.
-
사용하기 쉬워야한다. 그렇지 않으면 개발자들은 컴포넌트를 사용하는 대신 system.out.println을 남발하며 performance를 손상시킬 것이다. 로깅 서비스를 사용하기 위해 단지 하나의 import 문과 one line 코드만이 필요하도록 해야 한다.
-
config 가능한 방법으로 Logging Service의 출력 포맷을 동적 구성할 수 있어야한다.
-
다양한 시스템 메세지를 위한 다양한 레벨을 정의할 수 있어야 한다.
-
출력 대상을 파일이나 데이타베이스 메일 등으로 다양화 할 수 있어야 한다.
(Wiring Your Web Application with Open Source Java 참고)
Presentation Layer, Service Layer, Business Layer, Persistense Layer, Domain Model Layer 등 각 계층에 대한 오픈 소스를 활용할 수 있다. 몇가지 검증된 대세 프레임워크들이 존재하며, 오픈소스의 활용은 선택이 아닌 필수 사항이다. 오픈소스와 그 활용에 대해서는 차후에 정리한다.
이 문서는 이전 프로젝트에서 참고용으로 작성했던 자료를 외부에 공개할 수 있도록 첨삭하여 정리한 것이다.
아래는 참고 사이트들이다.