서비스가 필요한 이유 | 질문 상세 페이지와 답변 등록 페이지 구현

Ryu·2023년 4월 3일
0

[ 서비스 ]


서비스는 왜 필요한걸까 ?

  1. 모듈화
  • 모듈화는 결국 ‘중복의 제거’가 핵심이다. 만약 서비스 단 없이 컨트롤러에서 여러 리포지터리에 접근해 데이터를 가공하는 작업을 한다면, 중복되는 코드가 엄청나게 증가할 것이다.
    리포지터리에 접근해서 데이터를 가공하는 작업을 서비스 단에서 ‘모듈화’ 시키면, 중복이 최소화되고 변경 지점도 감소한다.
  1. 보안
  • 컨트롤러는 리포지터리 없이 서비스를 통해서만 데이터베이스에 접근하게 해두어야, 보안상 안전하다.
    이렇게 하면 해커가 해킹을 통해 컨트롤러를 제어하더라도 리포지터리에 직접 접근할 수 없다.
  1. 엔티티 객체와 DTO 객체의 변환
  • 엔티티 클래스는 DB와 직접 맞닿아 있는 클래스이기 때문에, 이를 컨트롤러나 템플릿 엔진에 전달하여 사용하는 것은 좋지 않다.
  • 컨트롤러나 타임리프에서 사용하는 데이터 객체는 속성을 변경해 비즈니스 요구 처리를 해야하는 경우가 많다.
    엔티티 객체를 그대로 사용하면 연결된 테이블 컬럼 정보가 변경될 수 있다.
  • 따라서, Data Transfer Object (DTO) 가 필요하다.
    엔티티 객체를 DTO 객체로 변환하는 작업은 ‘서비스’단에서 수행한다. 이처럼 서비스는 컨트롤러, 리포지터리의 중간자적인 입장에서 엔티티 객체와 DTO객체를 서로 변환해 양방향에 전달하는 역할을 수행한다.

[ 질문 상세 ]


🍊 미션 :

질문 목록의 제목을 클릭 시, 상세 페이지가 출력되도록 구현해주세요.
-그럴 일 없겠지만, 백엔드 단에서는 예외 처리까지 모두 구현해줘야 합니다.(질문 제목 클릭했는데, 질문이 없는 경우)

// QuestionController
@GetMapping("/question/detail/{questionId}")
public String showDetail(@PathVariable("questionId") Long id, Model model) {
    Question question = questionService.findById(id);
    model.addAttribute("question", question);
    return "/question/question_detail";
}
// QuestionService
public Question findById(Long id) {
    Optional<Question> oQuestion = questionRepository.findById(id);
    if(oQuestion.isPresent()) {
        return oQuestion.get();
    } else {
        throw new QuestionNotFoundException("찾는 질문이 없습니다!");
    }
}
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "entity not found")
public class QuestionNotFoundException extends RuntimeException {
    public QuestionNotFoundException(String message) {
        super(message);
    }
}
  • @ResponseStatus로 상태 코드를 리턴할 수 있다.
// question_list.html 에 링크 추가
<tbody>
	<tr th:each="question : ${questions}">
	    <td>
	        <a th:href="@{|/question/detail/${question.id}|}" th:text="${question.subject}"></a>
	    </td>
	    <td th:text="${question.create_date}"></td>
	</tr>
</tbody>

// question_detail.html
<body>
    <h1>[[${question.subject}]]</h1>
    <div th:text = "${question.content}"></div>
</body>

참고). URL 프리픽스, @RequestMapping

@RequestMapping

  • 공통된 프리픽스 경로를 묶어주는 역할을 수행한다. 필수는 아니니까 상황에 맞게 사용하면 된다.

[ 답변 등록 ]


🍏 미션 :

질문 상세 템플릿에 form, textarea, input 을 추가해 답변을 등록하는 페이지를 만들어보자.
-답변 등록 버튼을 누르면, 해당 답변이 등록된 후 질문 상세 페이지로 다시 돌아와야 한다.
-질문 상세 페이지에 답변을 표시
해보자. (N개의 답변이 있습니다. → 이후 답변들이 보여야 한다.)

@PostMapping("/answer/create/{questionId}")
public String createAnswer(@PathVariable Long questionId, @ModelAttribute("form") AnswerDtoForm form, @RequestParam String content) {
    Question question = questionService.findById(questionId);
    form.setQuestion(question);
    answerServicee.saveAnswer(form);
    return "redirect:/question/detail/{questionId}";
}
  • @RequestParam String content
    • HTML form 도 결국 뒤에 쿼리 스트링처럼 붙어서 나가는 것이다.
      그렇기 때문에, 그냥 @RequestParam만 써도 인식하는 것.
    • 사실, 이거 빼줘도 정상 작동한다.
  • return “redirect:/question/detail/{questionId}”;
    return String.format("redirect:/question/detail/%s", id);
    return "redirect:/question/detail/%s".formatted(id)”;
// AnswerService
public Long saveAnswer(AnswerDtoForm form) {
    Answer answer = Answer.builder()
            .content(form.getContent())
            .question(form.getQuestion())
            .build();
    answerRepository.save(answer);
    return answer.getId();
}
// question_detail.html
<h1>[[${question.subject}]]</h1>
    <div th:text = "${question.content}"></div>

    <h5 th:text="|${#lists.size(question.answerList)}개의 답변이 있습니다.|"></h5>
    <div>
        <ul>
            <li th:each="answer : ${question.answerList}" th:text="${answer.content}"></li>
        </ul>
    </div>
    <form th:action="@{|/answer/create/${question.id}|}" method="post">
        <textarea name="content" id="content" cols="30" rows="15"></textarea>
        <input type="submit" value="등록하기">
    </form>
  • question_detail에서 answer 에 대한 정보를 사용하고 있으므로, 당연히 question에는 answer 가 연결되어 있어야 한다.
  • th:action 의 의미
    • form을 제출했을 때, 전달되는 경로를 말한다.
  • input 또는 textarea 태그의 id 속성과 name 속성
    • id : id 속성을 통해 해당 태그를 식별할 수 있다.
    • name : name 속성이 쿼리 스트링으로 서버에 전달된다.
      예를 들어, input 태그의 name = “content” 라면, 경로에 ?content=[입력한값] 이렇게 들어간다.
      이는 @RequestParam으로 받을 수 있다.

@SuperBuilder

  • @SuperBuilder 는 Builder 패턴으로 객체를 생성할 때, ‘부모 클래스’의 필드를 편하게 가져다 쓰기 위해 사용한다.
  • 이를 사용하기 위해서는, BaseEntity의 @NoArgsconstructor가 필요하다.
    이를 사용하지 않으면, 자식 클래스에서 인스턴스화할 때 에러가 발생할 수 있다. (부모 클래스의 기본 생성자를 호출해야 하는데, 이를 못하기 때문)
    또한, JPA에서는 기본적으로 엔티티 객체를 만들 때 기본 생성자를 사용한다.
profile
Strengthen the core.

0개의 댓글