스프링 프로젝트 시작하기
자바, 스프링 개발자들의 종착역이자, 주기적으로 다시 돌아오는 그곳, <토비의 스프링 3.1>을 드디어 읽는다.
한창 개발 걸음마를 막 떼고 스프링으로 아장아장 기어다니며 CRUD를 할 때즈음, 토비의 스프링을 찬양하고 있는 많은 개발자들을 보았고, 역시 지식이 깊어지려면, 책을 통해서 지식을 정리하고 깊이를 다지는 시간이 필요하겠구나, 생각했다. 하지만, 개발을 이제 막 배운 그때의 나에게 토비 책은 너무나도 무시무시했고, 토비책을 읽다가 개발을 포기한 사람들의 증언도 여럿 읽고 나니, 이 책은 개발자로서 레베루업이 필요할 때즈음, 다시 꺼내봐야겠다고 생각했다.
그리고 지금이다. 퇴사 후, 이직을 준비하는 동안, 지식의 깊이를 좀 더 다져놓고 싶다는 생각이 들었고, 집앞 도서관에서 (아무도 빌려가지 않는) 토비님의 책을 빌려서 읽기 시작했다. 처음에는 가볍게 읽기 시작했지만, 읽을 수록 인사이트가 쌓여, 기록하면서 제대로 읽고 싶어졌다. 그래서 나의 세컨 브레인인 이곳 블로그에 짧은 글들로 그 내용을 적어보며, 토비님으로부터 얻은 인사이트를 내것으로 만들어보고자 한다.
스프링 프레임워크는 자바 언어를 사용하는 모든 종류의 프로젝트라면 어디든 사용할 수 있다.
웹을 이용하는 자바 엔터프라이즈 시스템에서 가장 많이 사용된다.
그러나 스윙이나 이클립스 RCP 로 만드는 독립형 프로그램도 가능하고
모바일 어플리케이션 개발이나 엔터프라이즈 미들웨어 제품을 만들 때에도 사용 가능하다.
아키텍처
스프링은 계층형 아키텍처를 주로 사용한다. 멀티 티어 아키텍처나 3계층 애플리케이션이라고도 한다.
책임과 성격이 다른 것을 크게 그룹으로 만들어 분리해두는 아키텍처를 말한다.
3계층 아키텍처와 수직 계층
3계층 아키텍처는 백엔드의 DB나 레거시 시스템과 연동하는 데이터 엑세스 계층, 비즈니스 로직을 담고 있는 서비스 계층, 웹 기반의 UI 를 만들어내고 그 흐름을 관리하는 프레젠테이션 계층으로 구분한다.
데이터 계층은 아래와 같이 추상화 레벨에 따라 수직적으로 나뉘기도 한다. 아래로 갈 수록 추상화의 레벨이 낮다.
DAO 코드
JDBC Template
JDBC, 트랜잭션 동기화
DataSource
물론 새로운 추상계층을 도입해서 아래와 같이 만들어볼수도 있다.
DAO 코드
SimpleJdbcLayer
JDBC Template, SqlService
JDBC, 트랜잭션 동기화
DataSource
서비스 계층은 이상적인 POJO 로 작성되며, 가장 단순하다.
서비스 계층은 일반적으로 기술 API 를 직접 다루는 코드가 아니기 때문에 기술에 일관된 방식으로 접근하기 위해 사용하는 추상화 수직 계층구조를 가질 필요가 없다.
원칙적으로 서비스 계층의 코드가 기반 서비스 계층의 구현에 종속되면 안된다. 서비스 계층의 코드는 추상화된 기반 서비스 인터페이스를 통해서만 접근하도록 만들어서 특정 구현과 기술에 대한 종속성을 제거해야한다. AOP 를 사용하여 서비스 계층의 코드를 침범하지 않고 부가 기능을 추가하는 방법을 활용해야 한다.
이상적인 서비스 계층은 앞단의 클라이언트와 연결되는 프레젠테이션 계층이 바뀌고, 뒷단의 백엔드 데이터베이스 시스템과 연결과는 데이터 엑세스 계층이 바뀌어도, 그대로 코드 수정없이 유지될 수 있어야 한다.
어플리케이션에서 가장 중요한 자산은 바로 핵심 비즈니스 로직이 들어있는 이 서비스 계층!
프레젠테이션 계층
웹의 발전과 함께 계속 변화한다. 그래서 가장 복잡하기도 하다.
계층과 계층 사이를 넘어갈 때에는 반드시 특정 계층에 종속되지 않는 오브젝트 형태로 변환해주어야 한다.
중간 계층을 건너 뛰어서 관계를 갖지 않는 계층의 빈을 직접 DI 해서는 안된다.
데이터 중심의 아키텍처
DB/SQL 중심의 로직 구현 방식
하나의 업무 트랜잭션에 모든 계층의 코드가 종속되는 경향이 있다.
이렇게 데이터베이스 단을 중심으로 로직이 구현될 경우, 비즈니스 로직을 다루어야 할 서비스 계층은 단순히 데이터 베이스 조회 후, 프레젠테이션 단으로 전달하는 역할밖에 하지 못한다.
또한 거대하고 복잡한 SQL 이나 프로시저를 이용하는 등의 방법으로 핵심적인 비즈니스 로직이 모두 SQL 로 옮겨가게 된다. 이는 변화와 유지보수에 굉장히 취약하다.
중복되는 코드도 제거하기가 힘들며, 객체지향의 장점이 별로 활용될 수 없다.
거대한 서비스 계층 방식
DB에 부하가 걸리지 않도록 저장 프로시저의 사용을 자제하고, 복잡한 SQL 을 피하면서, 주요 로직은 서비스 계층의 코드에서 처리하도록 만들 수도 있다.
하지만 많은 비즈니스 로직을 DB의 저장 프로시저나 SQL 에서 서비스 계층으로 옮겨왔기 때문에 어플리케이션 코드의 비중이 커지게 된다. 거대한 서비스 계층이 탄생하는 것이다.
이 방식의 경우 DAO 에서 조회되는 결과값이 바교적 단순하다. 서비스 계층에서는 DAO 가 돌려준 정보를 분석, 가공하면서 비즈니스 로직을 적용한다.
DAO 의 결과값이 비교적 단순하기 때문에 이를 여러 비즈니스 로직에서 공유해서 사용해볼 수도 있다.
서비스 계층이 거대해지면서 매소드로 나눠볼수는 있겠지만, 여전히 클래스 내의 코드양은 그대로이다.
중복을 제거하기도 쉽지 않다.
계층 사이의 결합도는 높고 응집도는 떨어진다.
오브젝트 중심의 아키텍처
오브젝트 중심 아키텍처가 데이터 중심 아키텍처와 다른 가장 큰 특징은 도메인 모델을 반영하는 오브젝트 구조를 만들어두고, 그것을 각 계층 사이에서 정보를 전송하는데 사용한다는 것이다.
객체지향 분석과 모델링의 결과로 나오는 도메인 모델을 오브젝트 모델로 활용한다.
예를 들어보자.
category - product 로 모델링을 했다고 하자. 하나의 카테고리에 여러개의 물건들이 묶일 수 있는 전형적인 1대 다 관계이다.
DB 테이블에는 외래키를 기준으로 관계를 맺을 것이다.
여기서 만약 데이터 중심적인 아키텍처를 이용한다면, SQL 에서 조인을 해서 맵 형태로 반환값을 받게 된다.
이 반환값을 받은 서비스 및 프레젠테이션 단에서는 맵에 어떤 정보가 담겼는지 전혀 모른다. 때문에 데이터 단의 SQL 을 통해서 가공된 데이터의 구조를 알고 활용해야만 한다. 종속적인 것이다.
또한 기껏 공들인 모델링은 SQL 을 짤 때를 제외하고는 전혀 쓰일일이 없다.
반면 오브젝트 중심의 아키텍처를 보자. 도메인 모델의 구조 그대로 오브젝트에 담기게 된다. 각 오브젝트들은 서로를 참조할 수 있는 레퍼런스 변수를 지정하여 다른 오브젝트를 참고할 수 있다.
서비스나 프레젠테이션 계층은 SQL 내용을 전혀 몰라도 된다. 어떤 방식으로든지 모델에 담기게 되면, 그냥 조회해서 사용하면 그만이다.
중복도 손쉽게 제거할 수 있다. 자바 코드로 이루어졌고 DAO의 반환값이 특정 기능에 종속되지 않기 때문에 다른 서비스 단에서 손쉽게 재사용가능하다.
테스트하는 것도 SQL 에 비해서 훨씬 간단하고 쉽다. 심지어는 미리 다 만들어두지 않아도 테스트가 가능할 지경이다.
오브젝트 중심 아키텍처의 문제점
도메인 클래스 내부에서 이미 연관된 다른 도메인과 연결된 레퍼런스 변수가 있기 때문에, 해당 레퍼런스 변수가 필요없는 경우에도, 데이터가 들어가게 된다. 결국 사용하지 않는 불필요한 정보를 계속 받아야 하는 것이다.
물론 DAO 를 별도로 두어 product 없이 category 만 받도록 할 수 있지만, 그렇게 되면, product 에는 Null 값이 들어가게 되어 의도치 않게 NPE 를 만날 수 있다.
이런 문제점은 지연된 로딩 방식을 통해서 해결할 수 있다. 해당 변수가 사용되기 전까지는 조회하고 있지 않다가, 호출이 되면 그제서야 조회해서 가져오는 방식이다.
대부분의 ORM, Object RDB Mapping 기술은 이러한 지연 로딩을 지원해주고 있다. 대표적으로 JPA, JDO, 하이버네이트, TopLinK 등이 있다.
또한 자주 사용되는 참조성, 코드성 테이블이 있다면, 오브젝트 캐시로 만들어 메모리 캐시에 보관하고 있다면, 매번 DB 를 조회하지 않고 빠르게 가져올 수 있다. DB 부하도 줄어들게 된다.
결국 오브젝트 중심의 아키텍처에서는 가능하다면 ORM 과 같이 오브젝트 중심의 데이터 엑세스 기술을 사용하는 것을 권장한다.
빈약한 도메인 오브젝트 방식 vs. 풍부한 도메인 오브젝트 방식
도메인 오브젝트 방식도 구현하는 방법에 따라 천차만별이다. 항상 좋은 것은 아니다.
빈약한 도메인 오브젝트의 경우, 도메인 오브젝트가 데이터를 다믄 그릇 그 이상도 그 이하도 역할하지 않는다. 데이터 중심 아키텍처에 익숙한 스프링 개발자들이 자주 사용하는 방식인데, 이렇게 될 경우에는 객체지향의 이점을 전혀 활용할 수 없다.
결국 빈약한 도메인 오브젝트 방식을 사용했다면, 거대한 서비스 계층 방식과 유사하다고 볼 수 있다. 도메인 내부에 아무런 속성이나 행위의 정보도 없이 단순이 필드와 접근자, 수정자만 가지고 있다면 결국 도메인과 관련된 로직이 모두 서비스 계층에 몰빵 되기 때문이다.
반면 풍부한 도메인 오브젝트 방식에서는 도메인 내부에 도메인과 긴밀하게 연결된 비즈니스 로직이 담기게 된다. 아까의 예와 같이 category의 경우, 특정 category 를 가진 product의 개수를 모두 조회하는 calcTotalOfProductPrice() 로직의 경우, 서비스 계층이 아니라 category 내부에 담기게 되어 여러 서비스 계층에서 재사용가능할 수 있게 된다.
만약 도메인에 이 로직이 담기지 않고 CategoryService 에 매소드로 남아있었다면, InventoryService 와 같은 다른 서비스에서 이 로직을 재사용하고자 할때, CategoryService 를 DI 하는 귀찮은 과정이 필요하다. 하지만 도메인에 존재하면 그럴 필요 없이 그냥 매소드를 호출만 하면 된다.
도메인에 비즈니스 로직이 옮겨가게 되면 서비스 계층은 무쓸모해지는가?
그렇지 않다. 도메인 계층은 스프링에서 관리하는 빈이 아니기 때문에 스프링에서 생성하거나 관리하는 오브젝트의 DI를 받을 수 없다. 따라서 도메인 오브젝트는 DAO 나 서비스 오브젝트 같은 스프링 빈의 기능을 사용할 수 없는 것이다.
그렇기 때문에 수식계산이나 조건에 따른 데이터 변경과 같이 도메인과 밀접하게 연관되는 비즈니스 로직 이외에 DB에 결과를 저장하거나, 메일로 발송하거나, DB를 검색해서 원하는 정보를 가져와 활용하는 작업과 같은 것들은 도메인 오브젝트에서 불가능하다. 그래서 DAO 와 기반계층 오브젝트를 DI 받아서 사용할 수 있는 서비스 계층이 필요한 것이다.
도메인 계층 방식
위에서 언급했던 것과 같이, 도메인은 스프링이 관리하는 빈이 아니기 때문에 서비스 계층이나 다른 계층을 거쳐서 다른 작업을 진행했어야 했다. 그렇다면, 도메인 계층의 역할을 극대화하여 기존 3계층과 같은 레벨로 격상시켜 하나의 도메인 계층을 만들어보는 것은 어떤가?
즉, 도메인 오브젝트들이 하나의 독립적인 계층을 이루어서 서비스 계층과 데이터 엑세스 계층 사이에 존재하게 되는 것이다.
두 가지의 특징을 갖게 된다.
첫째는 도메인에 종속적인 비즈니스 로직의 처리는 서비스 계층이 아니라 도메인 계층의 오브젝트 안에서 진행된다는 점이다.
두번째는 도메인 오브젝트가 기존 데이터 엑세스 계층이나 기반 계층의 기능을 직접 활용할 수 있다는 것이다.
스프링에 의해 관리되는 빈은 아니지만, 그래도 간단한 설정을 통해 DI 를 적용할 수 있다.
AspectJ AOP를 사용하면, 클래스의 생성자가 호출되면서 오브젝트가 만들어지는 시점을 조인 포인트로 사용할 수 있고, 스프링 빈이 아닌 일반 오브젝트에도 AOP 부가기능을 적용할 수 있다. 이를 이용해서 도메인 오브젝트가 생성되는 시점에 특별한 부가기능을 추가하도록 만들 수 있다.
이 경우, 작은 규모의 서비스에서는 아예 서비스 계층을 없에고 도메인 오브젝트에서만 비즈니스 로직이 다루어질 수 있다.
또 하나 고려해야할 점은 도메인 오브젝트가 도메인 계층을 벗어나더라도 사용되게 해야할지 고민이 필요하다. 두 가지 방법이 있을 것 같은데,
하나는 여전히 모든 계층에서 도메인 오브젝트를 사용할 수 있게끔 하는 것이다. 하지만 이 경우, 도메인 오브젝트 내부에 DB나 백엔드 시스템에 접근하여 작업 결과를 반영할 수도 있기 때문에 자칫하면 심각한 혼란을 초래할 수 있다.
이런 문제는 개발팀 내부에서 철저한 가이드 라인을 통해 해결할 수 있는데, 문제는 이 가이드라인도 반드시 지키지 않는 사람이 존재한다는 것이다. 코딩 정책의 적용을 분석할 수 있는 툴을 이용해서 검증하거나, AspectJ의 정책/표준 강제화 기능을 사용하도록 하면 된다.
둘째 방법은 도메인 오브젝트를 도메인 계층에서 벗어나지 못하게 하는 것이다. 도메인 계층 밖으로 전달될 때에는 별도로 준비된 정보 전달용 오브젝트인 DTO, Data Transfer Object 로 내용을 복사해서 넘겨주도록 하는 방법이다.
DTO 는 기능을 갖지 않기 때문에 사용하기에 안전하다. 또 도메인 오브젝트를 외부 계층의 코드로부터 보호해준다. 반면에 도메인 오브젝트와 비슷한 구조를 가진 오브젝트를 따로 만들어야 하고, 이를 매번 변환해주어야 한다는 번거로움이 있다. 따라서 AOP 와 같은 방법을 활용해서 자동변환되도록 만들 필요가 있다.
커스텀 DI 를 별도로 만들어야 하는 이 방법을 사용하는 경우는 복잡하고 변경이 잦은 도메인을 가졌을 경우이다. 복잡한 도메인의 구조와 로직을 도메인 계층의 오브젝트에 반영하고, 도메인 모델과 설계에 변경이 발생했을 때, 도메인 계층의 오브젝트도 빠르게 대응해서 변경해주기 위해서이다.
이 방법의 경우, 도메인 계층의 응집도가 매우 높기 때문에 단위테스트를 작성하는 것이 편리하다. 도메인이 가진 복잡함을 객체지향적인 설계의 모든 장점을 동원해서 가장 유연한 방법으로 대응할 수 있다.
그러나 복잡하지 않은 어플리케이션의 경우, 이런 방식은 귀찮은 점이 훨씬 많다.
상태관리와 빈 스코프
서버 기반의 어플리케이션은 원래 지속적으로 유지되는 상태를 갖지 않는 restless 의 특징이 있다. 때문에 클라이언트와 서버 간에는 많은 양의 상태정보를 포함한 정보가 오고 가는데, 이를 위해 중요한 상태정보는 파일 시스템, 데이터그리드, DB 등에 저장되기도 한다.
스프링은 기본적으로 상태정보를 갖지 않는 빈과 오브젝트를 사용하는 것을 권장한다.
물론 스프링에서도 상태유지 어플리케이션을 만들 수 있고, 싱글톤 외에도 다른 스코프를 갖는 빈을 만들수도 있다.
스프링이 지원하는 기술의 의미 (서드파티 프레임워크, 라이브러리 등)
해당 기술을 스프링의 DI 패턴에 따라 사용할 수 있다.
스프링에서 지원하는 기술들은 핵심 클래스를 빈으로 등록할 수 있게 지원해주는 것이라고 생각하면 된다.
스프링의 서비스 추상화가 적용되었다.
스프링이 지지하는 프로그래밍 모델을 적용했다.
데이터 엑세스 모델에서 발생하는 예외에 대해서 일관된 예외 계층을 다라서 예외가 던져지도록 구성하였다.
불필요하게 예외를 처리하는 코드를 피하도록 런타임 위주의 예외를 사용한다는 스프링의 철학이 적용된 것!
템플릿/콜백이 지원된다.
스프링 프로그래밍 모델과 개발철학을 따라서 개발하는 방법
스프링의 빈으로 해당 기술의 핵심 오브젝트가 등록되도록 만든다.
필요에 따라서 팩토리 빈을 사용해서 오브젝트 생성과 초기화 작업, 프로퍼티 노출 등을 해줘야 한다.
어플리케이션 내의 빈들이 새로운 기술에 대해 빈을 DI 하는 방법으로 접근해줄 필요도 있다.
어떤 경우든 코드에 대한 초기화 작업이 필요하므로, 팩토리 빈을 만들어서 사용하면 편리하다.
서비스 추상화를 시도한다.
반복적으로 try, catch, finally 블록이 필요한 기술이라면, 템플릿/콜백 패턴을 이용한다.
AOP 나 예외전환을 적용한다.
정리
스프링은 유연하고 범용적이어서 어떤 어플리케이션에도 사용될 수 있지만, 기본적으로는 HTTP 를 통해 접근하는 웹 클라이언트와 백엔드 DB 를 사용하는 어플리케이션에 적합하다. 자바 엔터프라이즈 플랫폼에 최적화되어있다.
스프링에 가장 잘 어울리는 아키텍처는 오브젝트 중심의 아키텍처이다.
스프링이 직접 지원하지 않는 서드파티 기술 역시 스프링 스타일의 접근방식을 따라서 사용할 줄 알아야 한다.
Last updated