[스프링, 스프링부트]Entity, DTO, 그 사이의 ModelMapper 이야기
MVC기반에서 Model의 역할 - 영속성을 띈 Entity
MVC기반으로 개발할때, 특히 MVC기반의 REST기반의 API 서버를 만들때는
View영역과 Reopository영역에서 사용되는 DatoModel을 분리해서 사용하는게 일반적이다.
영속성 영역(Persistant Layer)의 데이타를 관리하는 클래스를 우리는 보통 Reopository 클래스로 지칭하고 @Repository
어노테이션을 붙여 그 역할을 부여한다.
이 Repository에 있는 Entity 클래스들은 대부분 DB의 테이블 스키마와 1:1로 매칭된 형태의 구조로 정의된다.
특히 JPA를 사용하는 경우는 Entity 클래스 = DB테이블
구조가 된다.
Student(학생)이라는 영속성을 띈 모델을 설계하고, Entity 클래스로 정의하고 DB의 테이블로 생성하면
학생 Entity는 모델의 설계가 바뀌지 않는 이상, 바뀔일이 없다.
MVC관점에서 Model의 역할을 하는 클래스들이다.
그럼 이 Entity클래스들이 MVC에서 View영역까지 넘어가서 사용(참조)되는게 맞을까?
위에서 얘기했듯이 API서버를 구축한다는 전제로 아래의 API를 설계해야 한다고 해보자.
- 학생의 이름과 성별, 생년월일, 연락처, 전공, 비밀번호등의 신상정보로 학생을 등록하는 API
- 학생 이메일정보를 변경하는 API
- 학번으로 학생명과 이메일를 조회하는 API
모두 학생이라는 Entity와 관련된 - 학생Entity의 데이타가 변경되는- API이다.
기능적인 측면에서 보면 3개의 API모두 학생테이블을 변경(등록)하거나 조회하는 기능이고 DB영역(Persistant Layer)에서는 학생 Entity 하나로 데이타 처리가 다 이루어질 것 이다.
즉, Repository에서는 데이타처리는 학생 Entity클래스 하나만 있으면 된다.
그럼 위의 3개의 API도 학생 Entity 하나로 input/output을 위한 데이타전달객체로로 사용하면 되지 않을까?
이 질문에 답하기 전에 우선 3개에 대한 API를 간단하게 요청방식과 In/Out으로 설계를 해보자.
우선 Model영역에서 DB와의 데이타 전달을 담당할 학생 Entity 클래스를 아래와 같이 정의할 수 있겠다.
API 설계를 요청과 응답항목으로만 간단히 명세를 만들어 보자.
- 학생등록 API - Post
- Input - 학생이름, 성별, 생년월일, 전화번호, 비밀번호, 이메일, 주소등….
- Output - 필요없음 (성공여부 정도)
- 학생 이메일정보변경 API - Put
- Input - 학번, 원래 이메일, 변경후이메일, 비밀번호
- Output - 필요없음 (성공여부 정도)
- 내 정보조회 API - Get
- Input - 학번
- Output - 학생명, 이메일
간단하게 설계를 했지만 사실 HTTP API의 설계라는게
- 요청(input)과 응답(output),
- 전달되는 문서의 양식
만 정해지면 기본적인 API의 명세는 완성된다.
3개의 API 모두 요청과 응답항목들을 보면 속성들 모두 Students Entity의 속성의 부분집합이다. 따라서 3개의 API의 데이타 전달객체로 Students클래스의 인스턴스를 사용해도 전혀 문제가 없을 거 같다.
따라서 아래와 같이 API의 RequestBody 객체유형을 모두 Students Class를 사용하더라도 요청을 처리하는데 전혀(?) 문제가 없다.
당연히 문제가 없고, 로직상 정상적으로 처리될 것이다.
하지만 MVC를 지향하는 Spring 프레임워크를 쓰는 개발환경에서는 좋은 구조라고 할 수는 없을 것이다. 아래의 이유에서이다.
-
View에서 표현하는 속성값들은 요청에 따라 계속 달라질 수 있는데, 그 때마다 Entity의 속성값들이 변하게 되면 학생이라는 영속성 모델을 표현한 Students 클래스의 순수성이 모호지게 된다.
- 예를 들어 학생 이메일을 변경하는 API의 경우, View에서 요청시 보내는 항목에는 원래의 이메일과 변경이메일이 필요한데, Entity클래스의 입장에서는 둘다 동일한 email 속성하나의 의미이다. View에서의 요청처리를 위해서
afterEmail
속성값을 추가해서 쓰게 되면 학생 Entity는 동일한 성격의 이메일에 대해서 두개의 속성값을 가지게 된다. - 이런 부분들은 로직을 처리함에 있어 문제가 되지는 않는다. 하지만 View의 요청에 따라 항목들이 계속 추가되어야 하는 경우, Entity는 그 순수성을 잃게 되고, 영속성 모델(테이블)과의 관계성도 모호해지게 되면서 MVC기반의 설계자체가 흔들리게 된다.
- 예를 들어 학생 이메일을 변경하는 API의 경우, View에서 요청시 보내는 항목에는 원래의 이메일과 변경이메일이 필요한데, Entity클래스의 입장에서는 둘다 동일한 email 속성하나의 의미이다. View에서의 요청처리를 위해서
-
영역간 불필요한 속성값들의 전달로 불필요한 방어 및 체크로직이 생겨날 수 있고 API서비스의 경우, 명세가 달라지는 큰 이슈가 발생한다.
-
위의 API에서 2번과 3번은 각각 요청과 응답객체에서 불필한 속성값들이 포함되게 된다.
-
학생 이메일 정보변경의 경우, 변경전, 변경후 이메일속성만 요청값으로 필요한데 학생 Entity 클래스를 전체를 쓰게 된다.
-
내 정보조회의 경우, 응답항목으로 학생명과 이메일주소만 필요한데 학생 Entity의 모든 속성정보가 응답항목으로 넘어오게 된다.
-
이 이슈는 어플리케이션의 View영역이 실제 화면인 경우에는 단순히 불필요한 속성값의 전달이 이슈-화면에서 사용하지 않으면 문제는 없다-이겠지만 어플리케이션이 API 서버라면 꽤 심각한 이슈가 생긴다.
API 어플리케이션에서는 응답전문자체가 View를 의미하기 때문에 내정보조회 API의 응답객체를 Students로 사용하게 되면 API명세자체가 달라지게 된다.
-
VIEW 와 Model 영역에서의 데이타전달객체는 반드시 분리한다.
DAO(Data Access Object)와 DTO(Data Transfer Object)라는 개념이 나온 이유일 것이다. DAO는 지금까지 얘기한 영속성 영역에서의 Model의 Entity클래스를 의미하고, DTO는 View와 Controller간의 영역에서 사용되는 객체라고 보면 된다.
실제, View의 요청별로 DTO클래스를 만들어 쓰게 되면 그대로 들어나는 속성의 중복성때문에 거부감이 들게 된다. 상속을 활용하던지 inner class로 동일 엔티티별로 DTO클래스들을 묶던지 하는 방법으로 중복을 최소화하는 것이 필요할 수 있다.
[ 학생정보 DTO의 예시 ]
REST API의 경우, 요청/응답전문이 모두 JSON의 형태일텐데 jackson라이브러리가 제공하는 어노테이션을 적극 활용해서 하나의 DTO클래스로 각 API의 명세에 맞도록 여러 속성값들을 컨트롤 하는것도 방법이다.
이렇게 까지 중복성의 리스크와 비용을 일정부분 감수하고 동일모델객체를 영역별로 클래스를 별도로 사용하는 이유는, MVC기반에서의 데이타전달객체가 영역간의 모호성을 많이 띄게 될때 감당해야 할 비용이 중복성으로 인한 비용보다 크기 때문일 것이다.
DB의 영속성 모델(테이블)을 기반으로 만들어진 Entity는 테이블이 변경될때 필연적으로 변경이 일어나는 클래스이고,
그에 따른 영향의 범위은 Model영역내에서만이어야 한다.
반대로, 상대적으로 더 변경의 가능성이 많은 View영역에서의 전달 객체의 속성들의 변경은 Model영역에 영향을 주지 않아야 한다.
이 것이 우리가 대부분 사용하고 있는 MVC의 특징이자 장점이다. 동일한 속성이라고 Model에서의 Entity클래스를 View영역까지 끌고가게 되면 위의 원칙을 깨어지고, MVC의 가장 큰 특징이자 장점은 사라지게 된다.
굳이 MVC를 사용할 필요가 없게 되는 것이다.
DTO와 Entity간의 데이타전달객체의 매핑은 ModelMappper를 활용하자.
ModelMapper를 적극적으로 활용하면 DTO와 Entity간의 객체전환 및 매핑시의 반복작업등을 상당부분 줄일 수 있게 된다.
DTO와 DAO(Entity)를 분리해서 쓰는게 사실 일정부분 코드의 중복을 발생시키게 되는데, 그 필요성을 얘기하면서 코드의 중복성을 줄이게 하는 ModelMapper가 필요하다는 소개는 좀 아이러니하기도 하다.
어쩔 수 없는 중복과 그 중복의 최소화를 위한 필수적인 라이브러이이기 때문에 안 써보신 분들은 꼭 사용해 보시길 권한다.
다음편에서는 ModelMapper의 사용에 대해서 자세히 소개해보도록 하겠다.
PS.
개인적으로는 본 내용이 MVC 기반의 어플리케이션 아키텍쳐 설계측면에서 보면
항상 고민이 되는 부분인데, 관점에 따라서는 단순한 주제일수도 있고 쓸데없는 고민인것 같다는 생각도 듭니다.
혹시라도 글을 읽는 다른 개발자분들은 이 주제에 대해서 어떻게 생각하고 있는지 궁금하네요..