DTO를 사용한다면 어느 Layer까지 사용하는 것이 좋을까요?

2012-11-08 08:59

혹시 DTO(VO) 작성하시나요?(http://www.slipp.net/questions/22)) 글에서 DTO의 사용 여부에 대해서 논의가 있었습니다. 그렇다면 DTO를 사용한다면 어느 Layer까지 사용하는 것이 적합할까요?

Browser => Controller(Presentation Layer) => Service(Businss Layer) => Repository or Dao(Persistence Layer)와 같이 구현한다면 어느 시점에 DTO를 생성하고, 사용하는 것이 좋을까요?

제가 생각했을 때 두가지로 나누어 생각할 수 있지 않을까 생각합니다. 글쓰기나 수정과 같이 사용자의 입력 데이터를 전달하는 경우와 목록 보기나 상세 보기와 같이 조회를 하는 경우에 따라 달라지지 않을까 생각되네요. HTTP로 본다면 POST, PUT과 GET으로 나누어 생각할 수 있지 않을까 생각되네요.

먼저 글쓰기와 수정과 같이 사용자 입력 데이터를 전달하는 경우는 다음과 같지 않을까요? slipp.net의 질문 답변 게시판을 예로 들어보죠.

  • 사용자가 글쓰기를 하면 Controller에서 QuestionDto가 생성된다.
  • QuestionDto는 Service 클래스에 전달되어 데이터베이스와 매핑하고 있는 Question 도메인 모델로 변환된다. 이 변환 작업은 Service 클래스 내에서 이루어진다.

목록 보기나 상세 조회와 같은 경우는 화면을 위한 DTO를 사용할 수도 있고 사용하지 않을 수도 있다. Question 도메인 모델 클래스를 Controller를 통해 JSP까지 전달해 View를 구성하는데 직접 사용할 수도 있고, 화면에 대응하는 DTO를 만들어 데이터를 전달할 수도 있다. 각 화면에 대응하는 DTO를 만드는 것은 상당한 부담이 될 수 있기 때문에 대부분의 경우에는 Question 도메인 모델 클래스를 View에 직접 전달해 구현하는 것이 일반적이라 생각한다.

DTO를 사용한다면 위와 같은 방식으로 구현할 수 있을 것이라 생각하는데 여러분의 생각은 어떠한가?

4개의 의견 from SLiPP

2012-11-09 11:22

예를 들어주신 사용자 글쓰기는 거의 모든 서비스에서 http post 메소드를 사용한다는 가정을 내려봅니다. (즉, 클라이언트에서 http post 메소드를 써서 server에 request를 하는 경우로 정할 수 있을 것 같네요.) 이때에는 개발자 편의에 따라서 DTO를 controller에 생성하는 경우가 잦을 것 같아요. (mvc 패턴을 강제해주는(?) 프레임웤인 스트러츠나 스프링MVC 의 validation 체크 지원을 받기 위해서죠.)

controller에서 validation 체크가 끝나고 DTO가 생성되었다면 이제 service layer로 해당 DTO를 넘겨주겠죠. 그럼 service layer에서는 경우에 따라 이 DTO를 가공하게 되겠고 이후 model layer로 DTO를 넘길 준비를 할텐데 이때엔 @자바지기 님께서 들어주신 예의 두번째 단락으로 경우의 수가 2개가 생길 것 같네요.

일반적인 jdbc사용을 함으로 DTO의 value가 그대로 query의 parameter로 전달되는 경우엔 model layer까지 넘어가겠습니다. 그럼 model layer에서 DB와 통신후에 return이 되어 model layer의 method에서 return이 되는 순간 DTO는 GC대상이 되겠죠. (이 DTO를 다시 또 return하는 경우는 논외로 하겠습니다.)

하지만 ORM을 사용하는 경우 등으로는 경험이 없다보니(ㅠㅠㅠㅠ 좌절...) 이 DTO를 다른 Object로 만든다거나 하는 경우가 있을 수도 있겠네요. (헌데 이럴 때에는 갑자기 든 생각인데 데코레이션 패턴같은 걸로 변환작업을 할 수는 없을까요? 어찌되었거나 service layer에서 가공이 끝난 DTO에 담겨진 data는 model layer로 전달되어야 하는 data이니까요. 그럼 ORM에 필요한 Object에 data를 맵핑하기 위해서 변환작업을 할 것이 아니라 DTO에 ORM에 필요한 기능들을 덧붙여주는게 더 효율적일 수도 있겠다라는 생각이 드네요.) 결국 괄호안의 갑자기 든 생각으로 ^^;; DTO를 만약 생성하였다면 전 layer에서 모두 사용하는게 맞겠다 싶습니다.

get 메소드같은 경우로 parameter가 적은 경우에는(글보기를 위해 글의 index만 parameter로 받는 등) jdbc를 사용할 경우 model layer에서 DTO가 생성되겠군요. 그럼 이 것은 controller layer에서 model 객체로 담아 view로 넘기게 되는데 이건 논란의 여지가 있을 수도 있겠네요 ;;; 저와 마찬가지로 모든 layer에 DTO를 쓰고 있으며 velocity같은 template엔진은 안쓰고 있고 jsp만 쓰는 친구가 있는데 이 친구는 DTO의 get메소드 수정시에 jsp파일까지 수정이 안되는 경우가 있다고 하더군요. (이건 이클립스 버그일까요? 그럼 논외가 되야할까요?)

2012-11-09 13:46

@김문수 글이 수정이 안되네욬ㅋㅋㅋㅋㅋ 템플릿메소드패턴이라고 수정하려고 했는데 ㅎㅎ;;; 그리고 @자바지기 님께서 올리셨던 블로그의 내용(http://www.slipp.net/wiki/pages/viewpage.action?pageId=2031636)) 을 보니 갑자기 들었던 제 생각은 이미 사용되고 있다 느껴져 지우려고 했는데 ㅋㅋ;;; 부끄럽네요 헤헤헤;;; 담주 벙개하나요?! 모두 어서 친해졌음 좋겠어요! ㅋ (어제 과하게 마신 술이 덜깼나봐요...)

2012-11-09 21:11

@김문수 글 잘 읽었다. 네가 이야기한대로 ORM을 사용하건 사용하지 않건 간에 DTO와 데이터베이스와 매핑이 되는 클래스(네가 말한 Model Layer 클래스, 이후에는 Model 클래스라고 지징함) 사이에 중복이 되는 부분이 많은 것이 사실이다. 이 같은 중복 때문에 Model 클래스와 DTO 클래스를 분리하지 않고 사용하는 것이 최근의 흐름이다. 예를 들어 현재 slipp.net의 질문을 담당하고 있는 Question 클래스를 한번 살펴보자.

public class Question implements HasCreatedAndUpdatedDate {
	@Id
	@GeneratedValue(strategy = GenerationType.AUTO)
	private Long questionId;
	
	@ManyToOne
	@org.hibernate.annotations.ForeignKey(name = "fk_question_writer")
	private SocialUser writer;
	
	@Column(name = "title", length=100, nullable = false)
	private String title;


	@ElementCollection(fetch = FetchType.LAZY)
	@CollectionTable(name = "question_content_holder", joinColumns = @JoinColumn(name = "question_id", unique = true))
	@org.hibernate.annotations.ForeignKey(name = "fk_question_content_holder_question_id")
	@Lob
	@Column(name = "contents", nullable = false)
	private Collection<String> contentsHolder;


	@Temporal(TemporalType.TIMESTAMP)
	@Column(name = "created_date", nullable = false, updatable = false)
	private Date createdDate;


	@Temporal(TemporalType.TIMESTAMP)
	@Column(name = "updated_date", nullable = false)
	private Date updatedDate;


	@Column(name = "answer_count", nullable = false)
	private int answerCount = 0;


	@Column(name = "show_count", nullable = false)
	private int showCount = 0;


	@ManyToMany(fetch = FetchType.LAZY)
	@JoinTable(name = "question_tag", joinColumns = @JoinColumn(name = "question_id"), inverseJoinColumns = @JoinColumn(name = "tag_id"))
	@org.hibernate.annotations.ForeignKey(name = "fk_question_tag_question_id", inverseName = "fk_question_tag_tag_id")
	private Set<Tag> tags = Sets.newHashSet();
	
	@Column(name = "denormalized_tags", length=100)
	private String denormalizedTags; // 역정규화한 태그를 저장
	
	@Transient
	private Set<NewTag> newTags = Sets.newHashSet();
	
	@Transient
	private String plainTags;


	@OneToMany(mappedBy = "question", fetch = FetchType.LAZY)
	@OrderBy("answerId ASC")
	private List<Answer> answers;
	
	@Column(name = "deleted", nullable = false)
	private boolean deleted = false;
	
	@Transient
	private boolean connected = false;


	...
}

slipp.net 게시판을 보면 기능이 단순하기 때문에 속성이 몇 가지 없겠다는 생각을 할 수 있다. 하지만 현재 기능만으로도 상당히 많은 속성이 추가되어 있는 것을 확인할 수 있다. 예상보다 더 많은 속성이 있는 이유는 다른 Model 클래스와의 관계도 포함하고 있기 때문에 더 그렇다.

Question 클래스를 보면 ORM을 활용해 DB와 매핑이 되어 있기 때문에 매핑을 위한 Annotation도 포함하고 있다. 그런데 앞에서 이야기했듯이 이 Question 클래스는 Model 클래스와 DTO 역할을 같이하고 있다. 즉, Controller에서 사용자가 입력한 데이터를 저장하기 위해서도 사용되고 있다. 처음에는 작게 시작했는데 지금은 상당히 커져있는 상태가 된 것이지..

이 클래스에서 DTO로서 필요한 속성은 @Transient 속성이 붙어 있는 속성들과 title, contents 속성에 이에 속한다. 나머지는 데이터베이스와의 매핑을 담당하는 Model 클래스로서의 속성이다. 현 시점에 내가 리팩토링해야겠다고 생각하는 부분은 일부 중복이 있더라도 DTO와 Model의 역할을 클래스로 분리해야겠다는 생각이다. 물론 처음부터 이런 부분이 보이면 좋겠지만 처음에 잘 보이지 않는 경향이 많기 때문에 처음에는 DTO와 Model 클래스를 하나의 클래스로 유지하다가 일정 시간이 지난 복잡도가 증가하고 분리하는 것이 좋겠다고 판단하는 시점에 분리할 필요가 있다고 생각한다.

물론 처음부터 분리할 것인지 말 것인지를 결정하면 그것만큼 좋은 것이 없겠지만 우리의 현실이 그렇지 못한 경우가 많기 때문에 필요한 시점에 적절히 분리하는 것이 맞다고 생각한다. 이런 리팩토링 이슈 때문에 처음부터 분리하는 것을 원칙으로 가져갈 경우 중복 코드가 많이 발생하고 개발 및 유지보수에도 좋지 못한 경우가 많이 발생한다. 따라서 이에 대한 정답은 없다고 생각하고 최초 설계할 때 가장 적절하다고 판단되는 방향으로 개발하고 이후 변경이 발생할 경우 그에 따라 발빠르게 대응하는 연습을 하는 것이 좋겠다는 생각이 든다. 이를 위해 필요한 것이 단위 테스트일 것이다. 단위 테스트가 있다면 하나의 Question 클래스를 QuestionDto와 Question 클래스로 분리해도 기존 기능이 정상적으로 동작하는지 검증하는데 유용하리라 생각한다.

프로그래밍이라는 것이 참 힘든 것이 정답이 없다는 것... 그래서 누군가는 프로그래밍을 Art라고 이야기하는 사람도 있겠지. 나는 프로그래밍이 경우에 따라 A가 정답이 될 수도 있고, B가 정답이 될 수 있는 것이 사람과 사람간의 관계와 같고, 사람 간의 문제를 해결하는 다양한 방법을 찾는 것과 같다고 생각한다. 사람 간의 문제를 해결하기 위해 다양한 고민을 하고 해결책을 찾으려고 노력하듯이 프로그래밍의 결과물은 하나이지만 다양한 구현 방법과 설계 방법을 찾기 위한 고민은 똑같다는 생각이 든다.

2012-11-11 23:06

@자바지기 후아;;; 그렇네요;;; 코드를 직접 보게되니 말씀하신 부분에 대해 알 것 같습니다. 저 위에 실제 database에 입력해야하는 data가 분리되야겠네요;;; 필요한 속성들은 따로이 어떠한 형태로든 객체로 Model 객체의 메소드에 넘겨주어야할 것 같네요. 위 코드와 말씀해주신 다른 Model클래스와의 관계도 포함되어있다보니 상당히 무거운 객체가 될 것 같네요. 어떻게보면 예로 보여주신 코드는 상당히 간소화된 경우일텐데, 정말 복잡한 회원테이블에 맵핑되야하는 Model객체에 회원가입이라도 발생하다가는 정말 많은 속성이 필요해지겠군요. 물론 이런 경우엔 validation외에는 data가공에 대해 이슈가 많이 없어서 자료구조객체를 넘김으로 DTO를 만들지 않겠지만, 혹여나 만들게 될 때에 제가 생각했던 DTO 기능을 하는 Model객체가 될 경우엔 ㄷㄷㄷ 이네요;;; 어서 경험을 해봐야겠네요. 전 JDBC만 사용하다보니 (ㅠㅠ) Model객체의 속성에 대한 고민을 별로 해보지 않아 편하게 생각했었는데 다른 Model클래스와의 관계가 속성에 들어가다니;; 거기에 많은 어노테이션까지 붙으니 코드가 점점 거대해질 것 같네요;; 그렇다고 처음부터 분리하면 말씀하신 중복 코드와 유지보수에 문제가 생길것이고... 그래서인지 Map객체와 같은 자료구조를 사용하는 경우가 우선시 되는 것을 이해할 수 있는 것 같아요. 하지만 DTO를 사용해야한다면 분리해야하는 시점이 중요한데 ㅎㅎ;;; 말씀해주신대로 정말 답이 없네요. 그럼 본문에서와 같은 상황이라면 말씀하신것처럼 DTO가 컨트롤러에서 생성되고 서비스에서 데이타가공 후 Model클래스로 변환작업이 필요할 것 같은데 아 경험을 못해봐서 어떻게 표현을 못하겠네요... 같은 고민을 하고 계신분들께서는 어떻게 생각하시는지 알고 싶어요. ㅎㅎ

의견 추가하기

연관태그

← 목록으로