Spring

토비의 Spring_chapter09_스프링 프로젝트 시작하기

강용민 2022. 11. 29. 22:23

스프링을 이용해 애플리케이션 프로젝트를 처음 구성할 때 알아야 할 기본적인 내용과 스프링 개발에 도움이 되는 개발 툴과 빌드 방법도 살펴볼 것이다.

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

 

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

스프링으로 만들 수 있는 애플리케이션의 종류에는 제한이 없지만, 주로 자바 엔터프라이즈 환경에서 동작하는 애플리케이션을 개발하는 목적으로 사용된다. 자바 엔터프라이즈 애플리케이션은 서버에서 동작하며 클라이언트의 요청을 받아서 그에 대한 작업을 수행하고 그 결과를 돌려주는 것이 기본적인 동작 방식이다.

 

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

엔터프라이즈 애플리케이션에서 가장 많이 사용되는 구조는 클라이언트가 웹  브라우저이고 백엔드 시스템이 DB인 구성인 'DB를 사용하는 웹 애플리케이션'이다. 하지만 그외에도 많은 구조가 있다.다음 그림은 엔터프라이즈 애플리케이션으로 할 수 있는 다양한 구조이다.

 

애플리케이션 서버

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

  • 경량급 WAS 또는 서블릿/JSP
    • 스프링은 기본적으로 톰캣이나 제티 같은 가벼운 서블릿 컨테이너만 있어도 충분하다. 기존에 EJB와 WAS를 사용해야 가능했던 선언적인 트랜잭션이나 선언적 보안, DB 연결 풀링, 리모팅이나 웹 서비스는 물론이고 도움을 받으면 분산/글로번 트랜잭션까지도 가능하다.
  • WAS
    • 미션 크리티컬한 시스템에서 요구하는 고도의 안정성이나 안정적인 리소스 관리, 레거시 시스템의 연동 등의 필요가 있다면 WAS를 이용하면 좋다. 또 관리 기능이나 모니터링 기능이 뛰어나서 여러 대의 서버를 동시겡 운영할 때 유리한 점이 많다.

 

스프링소스 tcServer

톰캣을 기반으로 엔터프라이즈 스프링 애플리케이션에 최적화된 경량급 애플리케이션 서버인 tcServeer를 개발했다. tcServer를 이용하면 기존 톰캣에서는 아쉬웠던 고급 서버 관리 기능, 배포 기능과 진단 긴응을 포함해서 톰캣 전문가에게 받는 기술지원도 함께 제공받을 수 있다.

 

스프링 애플리케이션의 배포 단위

스프링으로 만든 애플리케이션은 다음의 세 가지 단위로 배포할 수 있다.

  • 독립 웹 모듈
    • 스프링은 보통 war로 패키징된 독립 웹 모듈로 배포된다. EJB 모듈을 함께 사용한다거나 여러 개의 웹 모듈을 묶어서 하나의 웹 애플리케이션 모듈로 만들지 않는 한 독립 웹 모듈이 가장 단순하고 편리한 배포 단위이다.
  • 엔터프라이즈 애플리케이션
    • 경우에 따라선 확장자가 ear인 엔터프라이즈 애플리케이션으로 패키징해서 배포할 수도 있다. 하나 이상의 웹 모듈과 별도로 분리된 공유 가능한 스프링 컨텍스트를 엔터프라이즈 애플리케이션으로 묶어주는 방법이다.
  • 백그라운드 서비스 모듈
    • rar는 리소스 커넥터를 만들어 배포할 때 사용하는 방식인데, 만약 스프링으로 만든 애플리케이션이 UI를 가질 필요는 없고 서버 내에서 백그라운드 서비스처럼 동작할 필요가 있다면 rar 모듈로 만들어서 배포할 수 있다. 이때는 J2EE 1.4 이상의 WAS가 필요하다.

서블릿 컨테이너나 웹 모듈 모드 JavaEE 표준의 일부일 뿐이기 때문에 설정만 바꾸면 어렵지 않게 이전이 가능하다.

 

9.3 애플리케이션 아키텍처

클라이언트와 백엔드 시스템의 종류와 사용 기술, 연동 방법을 결정했다면 시스템 레벨의 아키텍처는 대략 구성된 셈이다. 다음으로 결정할 사항은 스프링 웹 애플리케이션의 아키텍처다.

성격이 다른 모듈이 강하게 결합되어 한데 모여 있으면 한 가지 이유로 변경이 일어날 때 그와 상관없는 요소도 함께 영향을 받게 된다. 따라서 인터페이스와 같은 유연한 경계를 만들어두고 분리하거나 모아주는 작업이 필요하다.

 

계층형 아키텍처

아키텍처와 SoC

지금까지는 주로 오브젝트 레벨에서 결합도를 낮추는 방법(분리)의 문제에 대해 생각해봤다. 이 원리는 아키텍처 레벨에서 좀 더 큰단위에 대해서도 동일하게 적용할 수 있다. 성격이 다른 것은 아키텍처 레벨에서 분리해주는 게 좋다. 이렇게 분리된 각 오브젝트는 독자적으로 개발과 테스트가 가능해서 개발과 변경 작업이 모두 빨라질 수 있다. 또 구현 방법이나 세부 로직은 서로 영향을 주지 않고 변경될 수 있을 만큼 유연하다. 

이렇게 책임과 성격이 다른 것을 크게 그룹으로 만들어 분리해두는 것을 아키텍처 차원에서는 '계층형 아키텍처' 또는 멀티 티어 아키텍처'/ '3계층 애플리케이션' 라 부른다.

 

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

3계층 아키텍처의 각 계층은 다음과 같은 역할을 한다.

3계층 아키텍처

  • 데이터 액세스 계층
    • 백엔드의 DB나 레거시 시스템과 연동하는 인터페이스 역할
    • 데이터 액세스 계층은 사용 기술에 따라서 다시 세분화된 계층으로 구분될 수 있다. 이 경우는 추상화 수준에 따른 구분이기 때문에 수직적인 계층이라 부른다. 스프링의 JdbcTemplate을 사용하는 DAO 계층이라면 다음 그림과 같이 나타낼 수 있다.
    • JdbcTemplate을 사용하는데 데이터 액세스 계층의 특징은 JdbcTemplate이 추상화를 위한 계층을 사용돼서 로우레벨의 기반 계층에 존재하는 JDBC와 드라이버, 스프링의 트랜잭션 추상화 서비스의 동기화 기능을 간접적으로 이용하게 만든다는 것이다.
DAO 코드
JdbcTemplate
JDBC 트랜잭션 동기화
DataSource

 

  • 서비스 계층
    • 비즈니스 로직을 담는 역할을 하며, DAO 계층을 호출하고 이를 활용해서 만들어진다.
    • 잘 만들어진 스프링 애플리케이션의 서비스 계층 클래스는 이상적인 POJO로 작성된다. POJO로 만든다면 객체지향적인 설계 기법이 적용된 코드를 통해서 비즈니스 로직의 핵심을 잘 담아내고, 이를 쉽게 테스트하고 유연하게 확장할 수 있다.
    • 서비스 계층은 DAO 계층을 호출하고 이를 활용해서 만들어진다. 때론 데이터 액세스를 위한 기능 외에 서버나 시스템 레벨에서 제공하는 기반 서비스를 활용할 필요도 있다. 메시징 서비스를 이용하는 것이 대표적인 예다. 이런 기반 서비스는 3계층 어디에서나 접근이 가능하도록 만들 수도 있고, 아키텍처를 설계하기에 따라서 반드시 서비스 계층을 통해 사용되도록 제한할 수도 있다.
  • 프레젠테이션 계층
    • 웹 기반의 UI를 만들어내고 그 흐름을 관리하는 역할
    • 엔터프라이즈 애플리케이션의 프레젠테이션 계층은 클라이언트의 종류와 상관없이 HTTP 프로토콜을 사용하는 서블릿이 바탕이된다.

 

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

각 계층은 응집도가 높으면서 다른 계층과는 낮은 결합도를 유지할 수 있어야 한다.  즉, 각 계층은 자신의 계층의 책임에만 충실해야 한다.

 

 

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

엔터프라이즈 시스템은 본질적으로 동시에 많은 작업이 빠르게 수행돼야 하는 시스템이라 stateless상태를 가진다. 애플리케이션을 사이에 두고 흘러다니는 정보를 어떤 식으로 다룰지를 결정하는 일도 아키텍처를 결정할 때 매우 중요한 기준이 된다. 엔터프라이즈 애플리케이션에 존재하는 정보를 단순히 데이터로 다루는 경우와 오브젝트로 다루는 경우, 두 가지 기준으로 구분해볼 수 있다.

 

데이터 중심 아키텍처

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

해당 구현방식은 본인이 가장 많이 사용했던 아키텍처 방식이다. 물론 알고 사용했다기 보다는 3계층 아키텍처만을 신경을 쓰고 만들다 보니 DB/SQL 중심의 로직이 짜여져 있었다.

데이터 중심 구조의 특징은 하나의 업무 트랜잭션에 모든 계층의 코드가 종속되는 경향이 있다는 점이다. 예를 들어 사용자의 이름으로 사용자 정보를 검색해서 일치하는 사용자의 정보를 보여주는 작업이 있다고 하자. 이것이 하나의 업무 단위가 되면 모든 계층의 코드가 이 기준에 맞춰서 만들어진다. 사용자 조회라는 단위 업무를 위해서만 존재하는 각 계층의 코드가 만들어진다는 뜻이다. 유사한 방법의 사용자 조회용 DAO 메소드라도 화면에 나타날 정보가 다르면 SQL이 달라지기 때문에 새로 만들어야 한다.

이런 방식은 개발하기 쉽다는 장점이 있지만 자바 코드를 단지 DB와 웹 화면을 연결해주는 단순한 인터페이스 도구로 전락시키는 것이다. 자바의 오브젝트는 단지 HTTP 서비스 채널을 만들어주고 JDBC를 이용해 DB기능을 사용하게 하는 스크립트 정도로 역할이 축소된다. 그외 다음과 같은 단점들이 존재한다.

  • 이런 개발 방식은 변화에 취약하다. 객체지향의 장점이 별로 활용되지 못하는데다 각 계층의 코드가 긴밀하게 연결되어 있기 때문이다.
  • 중복을 제거하기도 쉽지 않다.
  • 업무 트랜잭션에 따라 필드 하나가 달라도 거의 비슷한 DAO 메소드를 새로 만들기도 한다.
  • 로직을 DB와 SQL에 많이 담으면 담을수록 점점 확장성이 떨어진다. 확장한다 하더라도 매우 큰비용이 든다.
  • SQL이나 저장 프로시저에 담긴 로직은 테스트하기 힘들다.

 

거대한 서비스 계층 방식

데이터 중심 아키텍처이긴 하지만 DFB에 많은 로직을 두는 개발 방법의 단점을 피하면서 애플리케이션 코드의 비중을 높이는 방법이 있다. DB에는 부하가 걸리지 않도록 저장 프로시저의 사용을 자제하고 복잡한 SQL을 피하면서, 주요 로직은 서비스 계층의 코드에서 처리하도록 만드는 것이다. 이는 많은 비즈니스 로직을 DB의 저장 프로시저나 SQL에서 서비스 계층의 오브젝트로 옮겨왔기 때문에 애플리케이션 코드의 비중이 커진다. 그만큼 구조는 단순해지고 객체지향 개발의 장점을 살릴 기회가 많아진다.

상대적으로 단순한 DAO 로직을 사용하고, 비즈니스 로직의 대부분을 서비스 계층에 집중하는 이런 접근 방법은 결국 거대한 서비스 계층을 만들게 된다. 서비스 계층의 코드는 여전히 업무 트랜잭션 단위로 집중돼서 만들어지기 때문에 DAO를 공유할 수 있는 것을 제외하면 코드의 중복도 적지 않게 발생한다.또한 데이터 액세스 계층의 SQL은 서비스 계층의 비즈니스 로직의 필요에 따라 만들어지기 쉽다. 그래서 계층 간의 결합도가 여전히 크다.

 

데이터 중심 아키텍처의 특징은 계층 사이의 결합도가 높은 편이고 응집도는 떨어진다는 점이다. 화면을 중심으로 하는 업무 트랜잭션 단위 코드가 모이기 때문에 처음엔 개발하기 편하지만 중복이 많아지기 쉽고 장기적으로 코드를 관리하고 발전시키기 힘들다는 단점이 있다.

 

오브젝트 중심 아키텍처

오브젝트 중심 아키텍처가 데이터 중심 아키텍처와 다른 가장 큰 특징은 도메인 모델을 반영하는 오브젝트 구조를 만들어두고 그것을 각 계층 사이에서 정보를 전송하는 데 사용한다는 것이다. 그래서 오브젝트 중심 아키텍처는 객체지향 분석과 모델링의 결과로 나오는 도메인 모델을 오브젝트 모델로 활용한다.

 

데이터와 오브젝트

간단한 예를 가지고 데이터와 오브젝트 방식을 비교해보자.

어떤 업무를 분석해보니 카테고리와 상품이라는 두 가지 엔티티가 나온다고 해보자. 이는 전형적인 1:N의 관계로 나오며 DB테이블로 만들면 다음과 같이 나온다.

 

Category 테이블

필드명 타입 설정
CategoryId int Primary Key
Description varchar(100)  

Product 테이블

필드명 타입 설정
ProductId int Primary Key
Name varchar(100)  
Price int  
CategoryId int Foreign Key(Category)

 

조건에 맞는 모든 카테고리와 상품 정보를 가져와서 화면에 출력하는 기능을 만든다고 해보자. 이때 데이터 중심 아키텍처에서는 SQL과 DB 관점에서 생각한다. 따라서 DAO에서 다음과 같은 SQL을 사용하게 만들 것이다.

select c.categoryid, c.description, p.productid, p.name, p.price 
from product p join category c 
on p.categoryid = c.categoryid;

DAO에서는 JDBC로 SQL을 실행하고 받은 결과를 다음과 같이 담아서 서비스 계층으로 넘겨줄 것이다.

while(rs.next()){
    Map<String, Object> resMap = new HashMap<String, Object>();
    resMap.put("categoryid",rs.getString(1));
    resMap.put("description", rs.getgetString(2));
    ...
    list.add(resMap);
}

이렇게 가져온 정보를 웹 페이지 내에서 수정해서 DB에 다시 반영해야 한다면 어떻게 될까? 사용자가 수정한 정보는 다시 맵이나 배열 등에 담겨서 전달될 것이다.

이렇게 데이터 중심의 아키텍처에서는 DAO가 만드는 SQL의 결과에 모든 계층의 코드가 의존하게 된다. 도메인 분석을 통해 작성된 모델정보는 DB에 대한 SQL을 작성할 때 외에는 코드에 반영되는 일이 없다.

반면에 오브젝트 방식에서는 애플리케이션에서 사용되는 정보가 도메인 모델의 구조를 반영해서 만들어진 오브젝트 안에 담긴다. 도메인 모델은 애플리케이션 전 계층에서 동일한 의미를 가지기에 도메인 모델이 반영된 도메인 오브젝트도 전 계층에서 일관된 구조를 유지한 채로 사용될 수 있다.

먼저 도메인 모델의 구조를 따라서 다음과 같이 의미 있는 타입과 정보를 가진 클래스를 정의한다.

 

Category.class

public class Category{
    int categoryid;
    String description;
    Set<Product> products;	//0~N개의 Product를 참조하고 있는 컬렉션을 가질 수 있다.
    
    //getter, setter
}

Product.class

public class Product{
    int productid;
    String name;
    int price;
    Category category;	//1개의 Category를 가리키는 레퍼런스를 직접 갖고 있다.
    
    //getter, setter
}

이 구조는 애플리케이션 어디에서도 사용될 수 있는 일관된 형식의 도메인 정보를 담고 있다. DB에서 SQL 결과로 가져온 값을 그대로 사용하는 경우와는 다르게, 도메인 모델을 반영하는 오브젝트를 사용하면 자바 언어의 특성으 ㄹ최대한 활용할 수 있도록 가공할 수 있다. 대표적으로 오브젝트 사이의 관계를 나타내는 방법을 들 수 있다. 키의 조합을 통해 조인해서 의미 있는 관계를 만들어내는 대신 래퍼런스 변수를 이용해서 다른 오브젝트를 참조하는 것이다. 그래서 Product 크랠스에는 Product 테이블처럼 categoryid라는 외래키가 없다. 대신 Category 오브젝트를 가리키는 레퍼런스 변수를 갖고 있다. 때문에 원한다면 Category 오브젝트에서 다음과 같은 코드로 Category에 속한 Product를 간단히 가져올 수도 있다.

Set<Product> products = myCategory.getProducts();

데이터 중심 방식에서는 Category와 그에 대응되는 Product를 찾아 SQL을 이용해 조인한 다음 하나의 맵에 뭉뚱그려서 가져왔다면 오브젝트 중심 방식에서는 테이블의 정보와 그 관계를 유지한 채로 정확한 개수의 Category 오브젝트와 그에 대응되는 Product 오브젝트로 만들어 사용한다.

이렇게 도메인 모델을 따르는 오브젝트 구조를 만들려면 DB에서 가져온 데이터를 도메인 오브젝트 구조에 맞게 변환해준다면 그 이후의 작업은 수월해진다. DAO는 자신이 DB에서 가져와서 도메인 모델 오브젝트에 담아주는 정보가 어떤 업무 트랜잭션에서 어떻게 사용될지는 신경 쓰지 않아도 된다.

 

도메인 오브젝트를 사용하는 코드

오브젝트 중심 방식에서 비즈니스 로직의 구현이 얼마나 간단하고 명확한지 살펴보자. 어떤 카테고리에 포함된 상품의 모든 가격을 계산해야 하는 로직이 필요하다면 서비스 계층의 오브젝트 안에 다음과 같은 메소드를 만들어 사용하면 된다.

public int calTotalOfProductPrice(Category cate){
    int sum = 0;
    for(Product prd : cate.getProducts()){
        sum += prd.getPrice();
    }
    return sum;
}

Category 자체가 독립된 오브젝트이므로 서비스 계층 어디에서든지 Category의 상품 각격을 계산해야 할 때는 이 메소드를 사용하면 된다.

 

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

도메인 오브젝트를 사용하면 코드는 이해하기 쉽고 로직을 작성하기도 수월하다는 장점이 있다.

하지만 단점도 있다. 최적화된 SQL을 매번 만들어 사용하는 경우에 비해 성능 면에서 조금은 손해를 감수해야 할 수 도 있다.

오브젝트 관계에도 문제가 있다. 만약 단순히 Product 정보만 필요한 비즈니스 로직이 있다고 해보자. 그런데 DAO가 돌려준 Product 오브젝트에는 관계를 갖고 있는 Category 오브젝트도 함께 담겨 있을 것이다. 이는 상당한 낭비다. Category가 필요한 경우와 필요치 않은 경우를 구분해서 DAO를 만든다 해도 Product의 category 필드에는 null 값이 들어간다.

이런 문제를 해결하는 접근 방법은 여러 가지가 있다.

지연된 로딩기법을 이용하면 일단 최소한의 오브젝트 정보만 읽어두고 관계하고 있는 오브젝트가 필요한 경우에만 다이내믹하게 DB에서 다시 읽어올 수 있다.

사실 가장 이상적인 방법은 JPA나 JDO, 하이버네이트와 같은 오브젝트/RDB 매핑(ORM)기술을 사용하는 것이다. 이런 데이터 액세스 기술은 기본적으로 지연된 로딩 기법 등을 제공해주기 때문에 번거로운 코드를 만들지 않고도 도메인 오브젝트의 생성을 최적화 할 수 있다. 또한 SQL 결과를 가지고 도메인 오브젝트를 만들고 값을 채우는 등의 복잡한 DAO 코드를 만들지 않아도 된다.

그래서 도메인 오브젝트를 사용하는 오브젝트 중심 아키텍처에서는 가능하다면 ORM과 같은 오브젝트 중심 데이터 엑세스 기술을 사용하는 것을 권장한다.

 

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

도메인 오브젝트에 정보만 담겨 있고, 정보를 활용하는 아무런 기능도 갖고 있지 않다면 이는 온전한 오브젝트가 아니기에 빈약한(anemic)오브젝트라고 부른다. 다음 그림은 빈약한 도메인 오브젝트 방식의 구조를 보여준다. 도메인 오브젝트는 3개의 계층에는 독립적으로 존재하면서 일관된 구조의 정보를 담아서 계층 간에 전달하는 데 사용된다.

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

 

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

풍성한 도메인 오브젝트 방식은 빈약한 도메인 오브젝트의 단점을 극복하고 도메인 오브젝트의 객체지향적인 특징을 잘 사용할 수 있도록 개선한 것이다. 어떤 비즈니스 로직은 특정 도메인 오브젝트나 그 관련 오브젝트가 가진 정보와 깊은 관계가 있다. 이런 로직을 서비스 계층의 코드가 아니라 도메인 오브젝트에 넣어주고, 서비스 계층의 비즈니스 로직에서 재사용하게 만드는 것이다.

앞에서 서비스 계층의 코드로 만들었던 calcTotalOfProductPrice()는 Category라는 오브젝트와 그 관련 Product의 정보만을 사용하는 간단한 로직이기에 Category 클래스의 메소드에 넣을 수도 있다.

 

Category.java

public class Category{
    int categoryid;
    String description;
    Set<Product> products;	//0~N개의 Product를 참조하고 있는 컬렉션을 가질 수 있다.
    
    //getter, setter
    public int calTotalOfProductPrice(Category cate){
        int sum = 0;
        for(Product prd : cate.getProducts()){
            sum += prd.getPrice();
        }
        return sum;
    }
}

이렇게 도메인 오브젝트 안에 로직을 담아두면 이 로직을 서비스 계층의 메소드에 따로 만드는 경우보다 응집도가 높다. 만약 Category에 대해 상품 가격을 계산하는 작업이 CategoryService외의 서비스 계층 오브젝트에서 필요하다고 해보자. 이때 CategoryService의 메소드 호출을 하지 않고 그냥 Category 오브젝트에게 직접 필요한 계산 작업을 요청하면 되는 것이다.

물론 도메인 오브젝트에 비즈니스 로직을 넣는다고 해서 비즈니스 로직을 담고 있던 서비스 계층 오브젝트가 필요 없어지는 건 아니다. 여러 종류의 도메인 오브젝트의 기능을 조합해서 복잡한 비즈니스 로직을 만들었다면 특정 도메인 오브젝트에 넣기는 힘들다. 이런 비즈니스 로직은 서비스 계층의 오브젝트에 두는 것이 적당하다. 또한 도메인 오브젝트는 직접 데이터 액세스 계층이나 기반 계층 또는 다른 서비스 계층의 오브젝트에 접근할 수 없기 때문에 서비스 계층이 필요하기도 하다.

다음 그림은 풍성한 도메인 오브젝트 방식의 아키텍처를 나타낸다. 스프링의 빈으로 관리되는 3계층의 오브젝트들은 도메인 오브젝트를 자유롭게 이용할 수 있지만 그 반대는 안된다는 사실을 주의해야 한다.

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

도메인 계층 방식

도메인 오브젝트에 담을 수 있는 비즈니스 로직은 데이터 액세스 계층에서 가져온 내부 데이터를 분석하거나, 조건에 따라 오브젝트 정보를 변경, 생성 하는 정도에 그칠 수 밖에 없다. 이렇게 변경된 정보가 다시 DB 등에 반영되려면 서비스 계층 오브젝트의 부가적인 작업이 필요하다.

도메인 계층의 역할과 비중을 극대화하려면 도메인 오브젝트가 기존 3계층과 같은 레벨로 격상되어 하나의 계층을 이루게 하는 도메인 계층 방식을 도입해야 한다. 개념은 간단하다. 도메인 오브젝트들이 하나의 독립적인 계층을 위뤄서 서비스 계층과 데이터 액세스 계층 사이에 존재하게 하는 것이다.

도메인 모브젝트가 독립된 계층을 이뤘기 때문에 기존 방식과는 다른 두 가지 특징을 갖게 된다.

  • 도메인에 종속적인 비즈니스 로직의 처리는 서비스 계층이 아니라 도메인 계층의 오브젝트 안에서 진행된다.
  • 도메인 오브젝트가 기존 데이터 액세스 계층이나 기반 계층의 기능을 직접 활용할 수 있다.

물론 도메인 오브젝트는 스프링에 등록돼서 싱글톤으로 관리되는 빈이 아니기 때문에 다른 빈을 DI 받을 수 없기에 그에 따른 간단한 설정이 추가돼야 한다. 스프링이 관리하지 않는 도메인 오브젝트에 DI를 적용하기 위해서는 AOP가 필요하다. 스프링 AOP는 메소드 호출 과정에서만 부가기능을 추가할 수 있기에 Aspect AOP를 사용해야한다. Aspect AOP를 사용하면 클래스의 생성자가 호출되면서 오브젝트가 만들어지는 시점을 조인 포인트로 사용할 수 있고 스프링 빈이 아닌 일반 오브젝트에도 AOP부가기능을 적용할 수 있다. 이를 이용해서 도메인 오브젝트가 생성되는 시점에 특별한 부가기능을 추가하게 만들어 줄 수있다. 이 부가기능은 오브젝트의 수정자 메소드나 DI용 애노테이션을 참고해서 DI 가능한 대상을 스프링 컨테이너에서 찾아 DI 해주는 기능이다.

이 덕분에 도메인 오브젝트 기능의 제약이 사라진다. 물론 도메인 오브젝트에 담긴 기능은 자신과 관련 오브젝트에 대한 작업으로 한정돼야 한다. 이 경우에도 비즈니스 로직은 여러 도메인 오브젝트의 기능을 조합해서 복잡한 작업을 진행하기 위해 필요하다.

도메인 오브젝트를 독립적인 계층으로 만들려고 할 때 고려해야 할 중요한 사항이 있다. 도메인 오브젝트가 도메인 계층을 벗어나서도 사용되게 할지 말지 결정해야 한다. 선택할 수 있는 방법은 두 가지가 있다.

  • 여전히 모든 계층에서 도메인 오브젝트를 사용한다.
    • 가장 손쉽고 편한 방법이며 도메인 모델을 따르는 오브젝트 구조를 활용하는 면에서 오브젝트 중심 아키텍처의 장점을 그대로 누릴 수 있다.
    • 도메인 오브젝트에게 DB나 백엔드 시스템에 작업 결과를 반영할 수 있는 막강한 권한을 부여했기에 주의를 기해야 한다.
    • 코딩 정책의 적용을 분석할 수 있는 툴을 이용해 검증하거나 AspectJ의 정책/ 표준 강제화 기능을 사용하면 실수를 방지할 수 있다.
  • 도메인 오브젝트는 도메인 계층을 벗어나지 못하게 하는 것이다.
    • 도메인 계층 밖으로 전달될 때는 별도로 준비된 정보 전달용 오브젝트에 도메인 오브젝트의 내용을 복사해서 넘겨줘야 한다. 이런 오브젝트는 데이터 전달을 위해 사용된다고해서 DTO라 한다. DTO는 상태의 변화를 허용하지 않고 읽기전용으로 만들어지기도 한다.

다음 그림은 도메인 계층 방식의 구조다.

도메인 계층 방식

도메인 계층은 기존 3계층과 비슷한 수준에서 독립적인 역할을 담당하고 있긴 하지만 그 특성은 확연히 다르다. 각 사용자의 요청별로 독립적으로 도메인 계층을 이루는 오브젝트들이 생성됐다가 해당 요청을 처리하고 나면 버려진다. 도메인 오브젝트는 사용자별 요청에 대해 독립적인 상태를 유지하고 있어야 하기 때문이다. 그렇기 때문에 특별한 방법으로 DI를 해줘야지만 다른 3계층의 빈들과 협력해서 일을 처리할 수 있다.

이런 여러 가지 제약과 불편을 감수하면서라도 이 방식을 택해야 하는 경우는 매우 복잡하고 변경이 잦은 도메인을 가졌을 때다. 복잡한 도메인의 구조와 로직을 최대한 도메인 계층의 오브젝트에 반영하고, 도메인 모델과 설계에 변경이 발생했을 때 도메인 계층의 오브젝트도 빠를게 변경해주기 위해서다.

 

스프링 애플리케이션을 위한 아키텍처 설계

지금까지 3단계로 역할을 분리하는 계층형 아키텍처와 정보를 다루는 방법에 따른 아키텍처의 종류를 알아봤다. 계층구조를 어떻게 나눌 것인가와 애플리케이션 정보를 어떻게 다룰지를 결정하는 것이 기본이 된다. 그리고 그 위에 각 계층에 사용될 구체적인 기술의 종류와 수직 추상화 계층의 도입, 세세한 기술적인 조건을 결정하는 일이 남았다.

 

계층형 아키텍처

3계층 구조는 스프링을 사용하는 엔터프라이즈 애플리케이션에서 가장 많이 사용되는 구조다. 단, 3계층이라는 것은 논리적이고 개념적인 구분이지 꼭 오븝젝트 단위로 딱 끊어져서 만들어지는 게 아님을 염두에 둬야 한다. 때로는 하나의 계층이 다시 수평으로 세분화될 수도 있다. 반대로 3계층에서 두 개의 계층이 통합돼서 하나의 오브젝트에 담기는 일도 얼마든지 가능하다. 단, 3계층을 단순화해서 2계층으로 만든다면 스프링 AOP를 이용한 트랜잭션의 경게를 설정하기 어렵기에 서비스 계층과 데이터 액세스 계층을 통합하는 편이 낫다. 물론 이때도 논리적으로는 서비스 계층과 데이터 액세스 계층의 경계를 분명하게 하는 게 좋다.

 

정보 전송 아키텍처

스프링의 기본 기술에 가장 잘 들어맞고 쉽게 적용해볼 수 있는 것은 오브젝트 중심 아키텍처의 도메인 오브젝트 방식이다.일단은 빈약한 도메인 오브젝트 방식으로 시작해 가능하다면 도메인 오브젝트에 단순한 기능이라도 추가하도록 노력해보는 것이다.

 

상태 관리와 빈 스코프

아키텍처 설계에서 한 가지 더 신경 써야 할 사항은 상태관리다. 크게는 사용자 로그인 세션 관리부터, 작게는 하나의 단위 작업이지만 여러 페이지에 걸쳐 진행되는 위저드 기능까지 애플리케이션은 하나의 HTTP 요청의 범위를 넘어서 유지해야 하는 상태정보가 있다.

엔터프라이즈 애플리케이션은 동시에 다수의 사용자가 요청하기에 서버의 자원이 특정 사용자에게 일정하게 할당되지 않는다. 그래서 서버 기반의 애플리케이션은 statelss특성을 갖는다.

하지만 어떤 식으로든 애플리케이션의 상태와 장시간 진행되는 작업정보는 유지 돼야 하기에 URL, 파라미터, 쿠키 등을 이용해 상태정보 또는 서버에 저장된 상태정보에 키 값 등을 전달해야한다.