20 Jan 2018

Toby's Spring Chap 09: 스프링 프로젝트 시작하기

스프링은 어떤 종류의 애플리케이션에 대해서도 잘 들어맞도록 매우 유연하게 설계된 범용 프레임워크이다. 그래서 아키텍처의 종류나 프로젝트를 구성하는 방법에 대해 자유도가 높지만, 그만큼 구성 방법이나 아키텍처를 선택할 때 주의해야 한다.


자바 엔터프라이즈 플랫폼과 스프링 애플리케이션

스프링으로 만들 수 있는 애플리케이션의 종류에는 제한이 없다. 자바 언어를 사용하는 모든 종류의 프로젝트라면 어디든 사용할 수 있다. 하지만 스프링은 주로 자바 엔터프라이즈 환경에서 동작하는 애플리케이션을 개발하는 목적으로 사용된다.

자바 엔터프라이즈 애플리케이션은 서버에서 동작하며, 클라이언트를 상대로 서비스를 제공하도록 되어 있다. 즉 클라이언트의 요청을 받아 그에 대한 작업을 수행하고, 그 결과를 돌려주는 것이 기본적인 동작 방식이다.


클라이언트와 백엔드 시스템

엔터프라이즈 애플리케이션은 자신이 클라이언트가 되어서 또 다른 엔터프라이즈 시스템에 서비스를 요청할 수도 있다. 가장 많이 사용되는 구조는 클라이언트가 웹 브라우저이고 백엔드 시스템이 DB인 구성이다.

스프링의 주요 기능은 웹 브라우저를 클라이언트로 하고 DB에 데이터를 저장, 조회하는데에 집중되어 있다.

그렇다고 해서 꼭 클라이언트가 웹 브라우저여야 하며, 백엔드 시스템은 DB를 이용해야 하는 것은 아니다. 자바가 받아들일 수 있는 요청이나 제공하는 접속 방식을 사용한다면 클라이언트나 백엔드 시스템은 무엇이든 사용할 수 있다.


00.png


애플리케이션 서버

스프링으로 만든 애플리케이션을 자바 서버환경에 배포하려면 JavaEE (또는 J2EE) 서버가 필요하다. JavaEE 표준을 따르는 애플리케이션 서버는 크게 두 가지로 구분할 수 있다.

  • 경량급 WAS / 서블릿 컨테이너
    • 웹 모듈 배포만 가능한 경량급 WAS 또는 서블릿/JSP 컨테이너이다. 스프링은 기본적으로 톰캣이나 제티와 같은 가벼운 서블릿 컨테이너만 있어도 충분하다. 이런 서블릿 컨테이너만으로도 엔터프라이즈 애플리케이션에 필요한 핵심기능을 모두 사용할 수 있다.
  • WAS
    • 미션 크리티컬한 시스템에서 요구되는 고도의 안정성과 고성능을 요구한다면 상용 또는 오픈소스 WAS를 사용할 수도 있다.

스프링은 JavaEE 표준 기술을 적극적으로 지원한다.


애플리케이션 아키텍처

아키텍처는 여러가지 방식으로 정의되고 이해될 수 있는 용어이다. 어떤 경계 안에 있는 내부 요소들이 어떤 책임을 갖고 있고, 어떤 방식으로 서로 관계를 맺고 동작하는지를 규정하는 것이라고 할 수 있다.

아키텍처는 단순히 정적인 구조를 나타내는 것으로 생각하기 쉽지만, 실제로는 그 구조에서 일어나는 동적인 행위와 깊은 관계가 있다.


계층형 아키텍처

관심, 책임, 성격, 변하는 이유와 방식이 서로 다른 것들을 분리함으로써 분리된 각 요소의 응집도는 높여주고 서로의 결합도를 낮추는 관심사의 분리는 아키텍처 단위로 크게 확장해볼 수도 있다.

애플리케이션을 구성하는 오브젝트들을 비슷한 성격과 책임을 가진 것들끼리 묶을 수 있다.

  • DAO: 데이터 엑세스 로직
  • 서비스: 특정 기술과 환경에 종속되지 않으며 도메인에는 밀접한 관련을 갖는 POJO 오브젝트
    • 주로 DAO를 이용해 정보 저장과 검색을 진행하고, DB의 엔티티 모델과 유사한 도메인 객체를 이용해 데이터를 주고 받는다.
  • 프레젠테이션: 서블릿 컨테이너로부터 받은 사용자의 요청을 해석하고, POJO 기반의 서비스 객체에게 전달하고, 결과를 받아 웹 사용자로 하여금 표시가능한 형태로 변환

객체들의 단위도 유사한 성격을 가진 그룹별로 나누는 것이 좋다. 성격과 책임이 다른 것들이 얽혀 있으면 반드시 문제가 발생한다. 그래서 아키텍처 레벨에 대해서도 성격이 다른 것들은 분리하는 것이 좋다.

이렇게 책임과 성격이 다른 것을 크게 그룹으로 만들어 분리해두는 것을 아키텍처 차원에서는 계층형 아키텍처라고 부른다. 보통 웹 기반의 엔터프라이즈 애플리케이션은 3계층으로 분류된다.


3계층 아키텍처와 수직 계층

보통 다음과 같이 DB나 백엔드 시스템과 연동하는 인터페이스 역할을 하는 데이터 엑세스 계층, 비즈니스 로직을 담은 서비스 계층, 웹 기반의 UI를 만들고 흐름을 관리하는 프레젠테이션 계층으로 구분한다.


01.png

데이터 엑세스 계층

이 계층은 DAO 계층이라고도 하며, DAO 패턴을 보편적으로 사용한다. 또한 이 데이터 엑세스 계층은 DB 외에도 ERP, 레거시 시스템, 메인 프레임 등에 접근하는 역할을 하므로 EIS(Enterprise Information System) 계층이라고도 한다.

데이터 엑세스 계층은 다시 세분화된 계층으로 분류할 수도 있다. 이 계층에서 다시 세분화하는 것은 추상화 수준에 따른 구분이므로, 수직적인 계층이라고도 한다.


02.png

위 그림과 같이 JdbcTemplate가 추상화를 위한 계층으로 사용되어 로우레벨의 기술들을 간접적으로 이용하게 만들 수 있다. 이런 수직적인 계층에서 사용자 정의 계층을 추가하면 그 계층은 하위 계층의 변화에 대응해야 하는 책임도 가지게 된다.

만약 추상 계층을 새로 추가하는 것이 부담되고, 때에 따라서는 유연하게 하위 계층의 API를 활용할 필요가 있다면 공통적인 기능을 분리하여 유틸리티나 헬퍼 메소드 또는 객체로 제공하는 것도 좋은 방법이다.

서비스 계층

잘 만들어진 스프링 애플리케이션의 서비스 계층 클래스는 이상적인 POJO로 작성된다. POJO로 만든다면 객체지향적인 설계 기법이 적용된 코드를 통해 비즈니스 로직의 핵심을 담아내고 이를 쉽게 테스트하고 유연하게 확장할 수 있다.

보통 서비스 계층은 DAO 계층을 호출하고 이를 활용해서 만들어진다.

특별한 경우가 아니라면 추상화된 수직적인 계층구조를 가질 필요가 없다. 서비스 계층은 기술 API를 직접 다루지 않으므로, 기술에 일관된 방식으로 접근하거나 편하게 사용하는 추상화는 필요가 없기 때문이다.

시스템 레벨이나 다른 서버에서 제공하는 기반 서비스를 사용할 때도 DAO처럼 독립적인 계층의 서비스를 이용하는 것으로 봐야 한다.


03.png

원칙적으로는 서비스 계층 코드가 기반 서비스 계층의 구현에 종속되면 안된다. 추상화된 서비스 인터페이스를 통해서만 접근하도록 만들어 특정 구현과 기술에 대한 종속성을 제거해야 한다.

이상적인 서비스 계층은 데이터 엑세스 및 프레젠테이션 계층이 모두 바뀌어도 유지될 수 있어야 한다.

프레젠테이션 계층

이 계층은 가장 복잡한 계층으로 매우 다양한 기술과 프레임워크의 조합을 가질 수 있다. 웹과 프레젠테이션 기술은 끊임없이 발전하고 새로운 모델이 등장하기 때문이다.

보통 자바 엔터프라이즈 애플리케이션의 프레젠테이션 계층은 HTTP 프로토콜을 사용하는 서블릿이 바탕이 된다.

HTML과 자바스크립트만을 사용하는 브라우저이든, 플래시 애플리케이션이나 RESTful 스타일 클라이언트 또는 WS-* 방식의 웹 서비스 클라이언트이든 상관없이 대부분의 엔터프라이즈 애플리케이션을 사용하는 클라이언트들은 HTTP 프로토콜을 선호한다.

화면 흐름을 결정하고 입력 값에 대한 검증, 서비스 계층의 호출과 전달되는 값의 포맷의 변화, 뷰라고 불리는 화면을 어떻게 그릴지에 대한 처리를 담당한다.

스프링은 웹 기반의 프레젠테이션 계층을 개발할 수 있는 전용 웹 프레임워크를 제공할 뿐만 아니라 다양한 서드파티 웹 기술을 지원하므로 아예 프레젠테이션 계층을 통째로 스프링이 아닌 다른 웹 기술을 가져다 쓸 수도 있다.



계층형 아키텍처 설계의 원칙

객체와 그 관계에 적용했던 대부분의 객체지향 설계의 원칙은 아키텍처 레벨의 계층과 그 관계에도 동일하게 적용할 수 있다. 각 계층은 응집도가 높으면서 다른 계층과 낮은 결합도를 유지할 수 있어야 한다.

각 계층은 자신의 역할에만 충실해야하고 자신과 관련된 기술이 아닌 다른 기술의 API 사용을 삼가야 한다. 자신의 역할과 기술에만 충실한 계층을 만들면 각 계층간의 결합도는 자연스럽게 낮아질 것이다.

특정 계층에서 어느 필요한 작업을 다른 계층에게 부탁할 때, 계층 레벨에서 정의한 인터페이스를 통해서 요청하고 다음과 같이 그 인터페이스 메소드에서는 특정 계층의 기술이 드러나면 안된다.

public ResultSet findUsersByName(String name) throws SQLException;

위 메소드의 문제점은 데이터 엑세스 계층의 기술과 그 역할을 다른 계층에 노출한다는 점이다. 결국 이 것을 사용하는 계층에서는 ResultSet이라는 데이터 엑세스 계층에서 만들어진 객체를 직접 다루어야 한다. 또한 SQLException 이라는 예외를 해석하여 분석하고 처리하는 코드도 들어간다.

계층 간에 강한 결합이 생기면 유연성이 떨어지기 때문에 각 계층의 내부 구현이 변화하면 다른 계층의 코드도 함께 수정해야 한다.

위의 메소드는 다음과 같이 특정 계층의 기술이나 구현에 종속되지 않도록 바꾸어야 한다. 또한 예외도 스프링의 DataAccessException 처럼 특정 기술에 종속되지 않는 런타임 예외로 만들어야 한다.

public List<User> findUsersByName(String name) throws DataAccessException;

또한 프레젠테이션 계층에서 주로 사용하는 오브젝트 (ex: HttpServletRequest or HttpServletResponse)들을 서비스 계층의 파라미터 타입으로 사용해서는 안된다.

어떤 경우라도 계층 사이의 낮은 결합도를 깨뜨리지 않도록 설계해야 한다. 이를 위해 계층 사이의 호출은 인터페이스를 통해 이루어져야 한다.. 인터페이스를 사용하더라도 메소드 추가 / 변경시 매우 신중하게 결정하고 계층 내부의 예상되는 변화에도 쉽게 바뀌지 않도록 해야 한다. (다른 계층에서 꼭 필요한 메소드만 노출)

한 계층의 내부에서만 사용되도록 만든 빈 객체가 있는데, DI를 통해 다른 계층에서 함부로 가져다 쓰는 것도 피해야 한다.


애플리케이션 정보 아키텍처

엔터프라이즈 시스템은 동시에 많은 작업이 빠르게 수행되어야 하는 시스템이다. 사용자의 작업 상태를 오래 유지하는 독립형 애플리케이션과는 다르게 엔터프라이즈 애플리케이션은 사용자의 요청을 처리하는 동안만 간단한 상태를 유지한다.

주요 상태 정보는 주로 DB나 벡엔드 시스템에 분산되어서 저장된다. 하나의 작업이 여러 번의 요청과 페이지에 걸쳐 일어나는 경우에 유지되는 임시 상태정보는 클라이언트에 일시적으로 보관되기도 하고 서버의 사용자별 세션 메모리에 저장되기도 한다.

애플리케이션을 사이에 두고 흘러다니는 정보를 어떤 식으로 다룰지를 결정하는 일도 아키텍처를 결정할 때 매우 중요한 기준이 된다. 단순히 데이터로 다루는 경우오브젝트로 다루는 경우, 두 가지 기준으로 구분해볼 수 있다.

  • 데이터 중심 아키텍처: 애플리케이션에 흘러다니는 정보를 단순히 값이나 값을 담기 위한 목적의 객체 형태로 취급하는 구조이다. DB나 백엔드 시스템에서 가져온 정보를 값으로 다루고, 그 값을 취급하는 로직을 구현하고 그대로 프레젠테이션 계층의 뷰와 연결해주는 것이다. 보통 DB에서 돌려주는 내용을 그대로 맵이나 단순 저장용 객체를 사용한다.


DB / SQL 중심의 로직 구현 방식

데이터 중심 구조의 특징은 하나의 업무 트랜잭션에 모든 계층의 코드가 종속되는 경향이 있다는 점이다.

이름으로 사용자 정보를 검색하여 보여주는 작업이 있다고 했을 때, 이 아키텍처에서 검색 조건과 같은 비즈니스 로직은 SQL로 만들어진다. 만약 클라이언트에서 사용자 정보 중 가입일자 중에서 연도만을 보여준다고 하면, 해당 필드에서 연도를 추출하는 것은 SQL의 날짜처리 function을 사용해야 한다. 즉, SQL은 이미 화면에 어떤 식으로 출력할지를 알고 있다.

SQL 결과를 맵이나 객체로 프레젠테이션 계층에 전달된다고 했을 때, 서비스 계층은 별로 할 것이 없고, 프레젠테이션 계층의 해당 뷰는 SQL에서 정확히 어떤 필드값을 리턴할지, 어떤 포맷으로 전달하는지 알고 있다. 프레젠테이션 계층에서는 DB에서 돌려준 값을 그대로 사용한다.

따라서 이런 구조는 하나의 특정 업무에 모든 계층의 코드가 종속되며 업무의 내용이 바뀌면 모든 계층의 코드가 함께 변경된다. 따라서 다른 업무에 재사용하기에도 힘들다. 대부분의 코드는 대응되는 작업 단위에 1:1로 매핑되고, 여러 작업에서 반복되는 기능이 있다면 그에 대한 코드는 중복되기 쉽다.

보통 서비스 계층이 프레젠테이션 계층에 전달하는 결과의 포맷은 DAO의 SQL 결과와 같고, 웹 페이지의 출력 내용과도 1:1로 대응된다. 따라서 대부분의 코드는 하나의 작업 단위에 1:1로 매핑된다. 따라서 코드의 중복이 일어나기 쉽다.


04.png

이러한 방식은 자바 코드를 단지 DB와 웹 화면을 연결해주는 단순한 인터페이스로 전락시킨다. 또한 SQL의 변화가 일어나면 모든 계층이 다 변경되어야 한다. 겉보기에는 각 계층이 독립적으로 보이긴 하지만, 그 사이를 이동하는 데이터가 일종의 접착제 역할을 해서 계층간의 강한 결합을 만들게 된다. 따라서 변화에 매우 취약하다.

로직을 DB와 SQL에 많이 담으면 확장성이 떨어지며, 복잡한 SQL을 처리하기 위해 제한된 자원인 DB에 큰 부담을 주게된다.

상대적으로 애플리케이션 서버와 그 안에 담긴 오브젝트는 비용이 적게 든다. 서버를 늘려 쉽게 확장할 수도 있다.

또한 SQL과 프로시저에 담긴 로직은 테스트하기도 힘들다. 따라서 DB에는 부하를 가능한 주지 않는 간단한 작업만 하고, 복잡한 로직은 객체에 담아 애플리케이션 내에서 처리하는 것이 낫다.


거대한 서비스 계층의 방식

DB에서 가져온 데이터가 애플리케이션에 흘러다니는 정보의 중심이 되는 아키텍처이기는 하지만, DB보다는 애플리케이션 코드의 비중을 높인 방식이다. DB에는 부하가 걸리지 않도록 하면서, 주요 로직은 서비스 계층의 로직에서 처리하도록 하는 것이다. 따라서 DB / SQL 중심 구현 방식보다는 객체지향 개발의 장점을 살릴 기회가 많아진다.

하지만 여전히 SQL의 결과를 그대로 담고 있는 단순한 객체나 맵을 통해 데이터를 계층간에 주고 받는다.


05.png

서비스 계층의 코드는 여전히 업무 트랜잭션 단위로 만들어지므로 DAO를 공유할 수 있다는 것을 제외하고 코드 중복이 많이 발생하게 된다.

데이터 엑세스 계층의 SQL은 서비스 계층의 비즈니스 로직의 필요에 따라 만들어지기 쉽고, 따라서 계층간의 결합도는 여전히 크다.


오브젝트 중심 아키텍처

이 아키텍처는 객체지향 분석과 모델링의 결과로 나오는 도메인 모델을 오브젝트 모델로 활용한다. 도메인 모델을 반영하는 객체 구조를 만들고, 그 것을 각 계층 사이에서 정보를 전송하는데 사용하고 이를 이용해 비즈니스 로직이나 프레젠테이션 로직을 작성한다.

대개 도메인 모델은 DB 엔티티 설계에도 반영되기 때문에 관계형 DB의 엔티티 구조와도 유사한 형태일 가능성이 높다.

이 방식에서는 애플리케이션에서 사용하는 정보가 도메인 모델의 구조를 반영해서 만들어진 객체 안에 담긴다. 도메인 모델은 애플리케이션 전 계층에서 동일한 의미를 갖기 때문에 이 객체는 전 계층에서 일관된 구조를 유지한 채로 사용할 수 있다.

도메인 모델을 따르는 객체 구조를 만들려면 DB에서 가져온 데이터를 도메인 객체 구조에 맞게 변환해줄 필요가 있다. 단, 한 번 변환되면 그 이후의 작업은 수월해진다. 따라서 서비스 / 프레젠테이션 계층에서는 DAO에서 어떤 SQL을 사용했는지, 어떤 방식으로 DB에서 조회를 했는지 알 필요가 없다.

따라서 계층 간 결합도는 낮아지고, 일관된 정보 모델을 사용하므로, 개발 생산성과 코드의 품질, 테스트 편의성도 향상시킬 수 있다.


도메인 오브젝트 사용의 문제점

데이터 중심 아키텍처에서 쓸 수 있는 최적화된 SQL을 사용할 때에 비해 성능 면에서 조금 손해를 볼 수 있다. 비즈니스 로직에서 도메인 객체의 모든 필드가 필요할 때도 있고, 아닐 때도 있기 때문이다.

또한 DB 엔티티 구조를 반영하는 도메인 객체를, 특정 비즈니스 로직은 이 객체와 연관된 다른 도메인 객체를 사용할 때도 있고 다른 비즈니스 로직에서는 필요없을 때가 있다.

이를 위해 지연된 로딩 기법을 활용하면, 일단 최소한의 객체 정보만 읽어두고 관계된 다른 객체가 필요한 경우에만 다이내믹하게 DB에서 다시 읽어오게 할 수 있다. 보통 지연된 로딩 기법은 하이버네이트나 JPA와 같은 데이터 엑세스 기술에서 제공해준다.


빈약한 도메인 오브젝트 방식

도메인 객체에 정보만 담겨 있고, 정보를 활용하는 아무런 로직은 담겨져 있지 않다. 이 방식에서 도메인 객체를 실제로 다루는 비즈니스 로직은 서비스 계층에 있다.


06.png

SQL에 의존적인 데이터 중심의 아키텍처에 비해서는, 도메인 객체를 사용함으로써 유연하고 간결하긴 하지만 여전히 서비스 계층의 메소드에는 대부분의 비즈니스 로직이 들어가 있어 재사용성이 떨어지고 코드의 중복이 발생하기 쉽다.


풍성한 도메인 오브젝트 방식

도메인 객체나 그 객체가 가진 정보와 깊은 관계가 있는 비즈니스 로직을 도메인 객체에 넣고, 서비스 계층에서는 이를 활용하도록 하는 것이다.

이렇게 설계하면 도메인 객체를 다루는 로직을 서비스 계층에 둘 때보다 응집도가 높다. 데이터와 그것을 사용하는 기능이 한 곳에 모여있기 때문이다. 특정 도메인 객체에 종속되는 비즈니스 로직은 각 서비스 계층의 객체가 아니라 도메인 객체에 넣으면 된다.

이 방식에서 서비스 계층에서는 여러 도메인 객체의 기능들을 조합해서 복잡한 처리를 수행하거나, 데이터 엑세스 계층, 다른 기반 계층을 호출할 필요가 있는 로직을 구현한다.

도메인 객체는 스프링에 의해 관리되는 빈이 아니므로, 직접 데이터 엑세스 계층이나 다른 서비스 계층을 호출할 수 없기 때문이다. 도메인 객체는 필요에 따라 새롭게 만들어지는 오브젝트이다.


07.png

풍성한 도메인 오브젝트 방식에서는 빈약한 도메인 오브젝트 방식보다 서비스 계층의 코드가 간결하여 로직을 이해하기도 쉽다.


Tags:
Stats:
0 comments