스프링부트를 공부하고, 처음으로 초소형 프로젝트를 진행하고 있다.
백엔드 개발자의 기초이자 근-본 첫 프로젝트인 CRUD를 지원하는 게시판 만들기다. 타임리프와 JPA를 써서 MVC패턴을 통해 구현하는게 목표였다.
가장먼저 "C", 게시글 작성 기능을 구현하고 있었다.
html form 태그에서, input의 name 태그를 엔티티 필드 이름에 알맞게 써주었다.
@Entity
@Getter
public class Question {
@Id @GeneratedValue
private Long id;
private String title;
@Column(columnDefinition = "TEXT")
private String context;
@Column(name = "A_WRITER")
private String writer;
@Column(name = "A_PASSWORD")
private String password;
public Question() {
}
@Builder
public Question(String title, String context, String writer, String password) {
this.title = title;
this.context = context;
this.writer = writer;
this.password = password;
}
}
(엔티티)
<form th:action method="post">
<div class="form-group">
<label for="writer">작성자명:</label>
<input type="text" name="writer" class="form-control" id="writer" placeholder="작성자명을 입력하세요">
</div>
...
(form 태그의 일부)
위와 같이 엔티티와 폼을 구성하고, 컨트롤러에서 엔티티에 바로 바인딩 시킬 생각으로 다음과 같이 메서드를 구현하였다.
@PostMapping("/question/create")
public String create(@ModelAttribute Question question, RedirectAttributes redirectAttributes){
Long questionId = questionService.createQuestion(question);
redirectAttributes.addAttribute("questionId", questionId);
return "redirect:/question/{questionId}";
}
이제 다음과 같이 작성하고..
완료를 누르면..!
차있어야 할 제목, 작성자, 내용 부분이 없어졌다.
DB에 저장이 제대로 안된건가? 싶어서 db를 확인해보았으나
모든 데이터가 null로 저장되었다. @ModelAttribute가 제대로 작동하지 않았다는 결론이 도출되어 정보를 찾아보았다.
답은 @ModelAttribute의 작동 원리에 숨어있었다.
@ModelAtribute의 작동 과정은 다음과 같다.
- @ModelAttribute가 붙어있다면, 해당 객체를 생성한다. (이 때, 해당 클래스는 스프링 Bean이여야 한다.)
ex)public String create(@ModelAttribute Question question) //-> Question 객체 생성
- 1번 과정에서, 객체엔 적절한 규칙에 맞게 getter와 setter가 구현되어 있어야 한다.
- 만든 객체에, form으로 넘어온 데이터를 binding한다. 이때 binding 기준은, "name"태그를 보고 필드 이름과 맞춰 바인딩한다.
- 바인딩된 데이터들은 자동으로 Model에도 추가(addAttribute)된다 (파라미터로 주로 Model model 꼴로 받는 그 Model이다.)
이중 내가 놓친 부분은, 2번 과정 이였다.
Getter와 Setter가 설정되어 있어야 하는데, 나는 Getter만 만들어둔 상태였다.
그렇다면 엔티티에 Setter를 만들면 되겠다! 싶겠지만, 엔티티를 Mutable하게 만드는 접근 방식은 프로젝트가 복잡해질수록 위험한 접근이란 말을 많이 들었다. 문제가 생겼을때 유지 보수 하는 과정도 굉장히 힘들다.
그래서, DTO를 도입하기로 했다.
DTO 도입 방법에 대해선 모르는 지식이 대다수 입니다. 내용 중 잘못된 접근 방법이 있다면 댓글로 알려주신다면 정말 감사하겠습니다.
DTO가 뭐지?
(설명 하이퍼링크 추가 예정)
먼저, Question 엔티티에 대한 DTO 클래스를 생성해준다.
@Getter @Setter
@Component
public class QuestionDTO {
private String title;
private String context;
private String writer;
private String password;
}
그 다음으로, DTO와 Entity를 변환해주는 Mapper 클래스를 작성하였다.
DTO와 Entity간 변환을 어디서 해야하는가에 대한 고민이 생겨 정보를 찾아보았지만 마땅히 답을 얻지 못했다. 따라서 나는 별도의 패키지로 분리해서 스프링 빈으로 등록한뒤(싱글톤으로 사용하는게 가장 나은 방법같기에) 사용할 커스텀 Mapper를 만들어서 써보기로 했다.
@Component
public class QuestionMapper {
//TODO: 이 접근법이 옳은지 확인해보기
public QuestionDTO toDTO(Question question){
QuestionDTO qd = new QuestionDTO();
qd.setTitle(question.getTitle());
qd.setContext(question.getContext());
qd.setWriter(question.getWriter());
qd.setPassword(question.getPassword());
return qd;
}
public Question toEntity(QuestionDTO qd){
return Question.builder()
.title(qd.getTitle())
.context(qd.getContext())
.writer(qd.getWriter())
.password(qd.getPassword()).build();
}
}
아직 이 방식에 대해 옳은 접근법인지, 그리고 이렇게 냅다 Component로 등록해버려도 되는지 확실치는 않다.
마지막으로, 컨트롤러를 수정해준다. 컨트롤러에서 방금 구현한 QuestionMapper는 DI를 통해 전달받는다.
@PostMapping("/question/create")
public String create(@ModelAttribute QuestionDTO questionDTO, RedirectAttributes redirectAttributes){
Long questionId = questionService.createQuestion(questionMapper.toEntity(questionDTO));
redirectAttributes.addAttribute("questionId", questionId);
return "redirect:/question/{questionId}";
}
이로서 문제 상황이 해결되었다.
각종 어노테이션이나, 스프링 작동 원리에 대해 더 깊게 알아봐야할 필요성을 느끼며 글을 마무리한다.