▼ What ?
커리큘럼 3주차에 배웠던 내용들 중 하나인 서비스(Service)에 관해서 배우고 다뤄본 내용들을 이어서 정리해보려고 한다.
▼ 서비스 (Service)
서비스(Service)가 왜 필요할까 ?
- 모듈화
- 이전처럼 컨트롤러에서 리포지터리를 직접 호출해서 데이터를 조회했을 경우에 발생하는 문제
➜ 예를 들어, 여러개의 리포지터리를 사용하여 데이터를 조회한 후 가공하여 리턴하고자 하는 기능을 필요로 하는 모든 컨트롤러에 중복으로 구현해줘야 하는 문제가 발생한다 !
➜ 따라서, 리포지터리를 직접 호출하지 않고 중간에 해당 기능을 구현시켜놓은 서비스(Service)를 통해 데이터를 처리한다 !
- 이전처럼 컨트롤러에서 리포지터리를 직접 호출해서 데이터를 조회했을 경우에 발생하는 문제
- 보안상 안정적이다.
➜ 해킹을 통해 컨트롤러를 제어할 수 있게 되더라도 리포지터리에 직접 접근할 수 없게 된다.
- 엔티티 객체와 DTO 객체의 변환 처리
앞서 작성한 Question, Answer와 같은 엔티티(Entity) 클래스는 DB와 직접적으로 연결돼 있는 클래스이다.
➜ 전달받은 데이터 객체의 속성을 변경하여 비즈니스적인 요구를 처리하는 컨트롤러나 템플릿 엔진에 엔티티를 전달하여 사용하는 것은 테이블 컬럼(Column)이 변경돼 엉망이 될 수 있기 때문에 좋은 방법이 아니다 !
➜ 그래서 엔티티 클래스 대신 사용할 DTO 클래스가 필요하고, 엔티티 객체를 DTO 객체로 변환하는 작업도 필요하다 !
( 하지만, 커리큘럼에선 별도의 DTO를 만들지 않고 엔티티 객체를 컨트롤러와 타임리프에서 그대로 사용한다. )
➜ 이때 엔티티 객체를 DTO 객체로 변환하는 작업을 하는 클래스가 바로 서비스(Service) 클래스 !
서비스(Service) 클래스
- 서비스 클래스 작성
[ .../webboard/question/QuestionService.java ]
➜ 스프링부트에서 '@Service'가 붙은 클래스는 서비스로 인식한다.
package com.gdsc.webboard.question;
import java.util.List;
import org.springframework.stereotype.Service;
import lombok.RequiredArgsConstructor;
@RequiredArgsConstructor
@Service
public class QuestionService {
private final QuestionRepository questionRepository;
// 질문 목록을 조회하여 리턴하는 메서드
public List<Question> getList() {
return this.questionRepository.findAll();
}
}
🔻 'QuestionController' 컨트롤러 클래스에서 리포지터리를 사용했던 부분을 그대로 옮긴 것이다.
- 'QuestionController' 컨트롤러 클래스에서 리포지터리 대신 서비스를 사용하도록 수정하자.
[ .../webboard/question/QuestionController.java ]
...
public class QuestionController {
private final QuestionService questionService;
@GetMapping("/question/list")
public String list(Model model) {
List<Question> questionList = this.questionService.getList();
model.addAttribute("questionList", questionList);
return "question_list";
}
}
- 컨트롤러가 리포지터리를 직접 사용하던 구조에서 "Controller ➜ Service ➜ Repository" 구조로 데이터를 처리하는 구조로 변경되었다 !
( 브라우저로 "http://localhost:8080/question/list" 페이지에 접속하면 이전과 동일한 화면을 볼 수 있다. )
▼ 질문 상세 페이지 생성
질문 상세 링크 추가
- 질문 목록 템플릿인 "question_list.html" 을 수정하자.
<table>
<thead>
<tr>
<th>제목</th>
<th>작성일시</th>
</tr>
</thead>
<tbody>
<tr th:each="question, index : ${questionList}">
<td>
<a th:href="@{|/question/detail/${question.id}|}" th:text="${question.subject}"></a>
</td>
<td th:text="${question.createDate}"></td>
</tr>
</tbody>
</table>
<a th:href="@{/question/detail/${question.id}}" th:text="${question.subject}"></a>
🔻 타임리프에서 링크의 주소는 'th:href' 속성을 사용
➜ 타임리프에서 URL 주소를 나타낼 때는 반드시 '@{' 문자와 '}' 사이에 입력해야 한다.
➜ URL 주소는 '/question/detail/' + '${question.id}' 형태로 만들어졌는데 타임리프에서 문자열이나 자바 객체의 값을 연결 할 때엔 '|' 문자를 사용한다.
( '|' 가 없다면 오류 발생 )
질문 상세 컨트롤러 생성
- 이제 "http://localhost:8080/question/list" 에서 질문 상세 링크를 추가한 제목을 클릭해보면 404(Page not found) 에러가 발생할 것이다.
그 이유는 ?
➜ 앞서 봤던 오류들처럼 해당 " http://localhost:8080/question/detail/1" URL 에 대한 매핑 처리가 되어있지 않기 때문이다 !
➜ 질문 상세 페이지에 대한 URL 매핑을 위해 "QuestionController.java" 를 수정하자.
...
import org.springframework.web.bind.annotation.PathVariable;
...
public class QuestionController {
...
@GetMapping(value = "/question/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id) {
return "question_detail";
}
}
🔻 '{id}'처럼 변하는 값을 얻을 때엔 '@PathVaraible' 애너테이션을 사용 !
( 단, 매핑하는 값의 이름 '{id}' 과 매핑된 값을 받아온 매개변수 값의 이름 '{id}' 처럼 이름은 동일해야 한다. )
질문 상세 템플릿 생성
- 위와 같이 컨트롤러까지 수정해주고 해당 URL을 다시 호출하면 404가 아닌 500 에러가 발생한다.
그 이유는 ?
➜ 응답으로 리턴할 템플릿이 없다.
➜ 다음과 같은 " question_detail.html" 파일을 생성해주면, 아래와 같은 페이지를 볼 수 있다.
[ /web-board/src/main/resources/templates/question_details.html ]
<h1>제목</h1>
<div>내용</div>
질문 상세 서비스 생성 & 컨트롤러 수정
- "제목", "내용" 이 아닌 데이터의 실제 제목과 내용을 출력하기 위해선, 질문 데이터를 처리하는 "QuestionService" 클래스를 수정 !
[ /webboard/question/QuestionService.java ]
➜ getQuestion 메서드(id 값으로 Question 데이터를 조회) 추가한다.
...
import java.util.Optional;
import com.gdsc.webbaord.DataNotFoundException;
...
public class QuestionService {
...
public Question getQuestion(Integer id) {
Optional<Question> question = this.questionRepository.findById(id);
if (question.isPresent()) {
return question.get();
} else {
throw new DataNotFoundException("question not found");
}
}
}
🔻 리포지터리로 얻은 Question 객체는 Optional 객체이기 때문에, isPresent 메서드로 해당 데이터가 존재하는지 검사하는 로직이 필요하다 !
➜ 따라서, 만약 id 값에 해당하는 Question 데이터가 없을 경우에는 DataNotFoundException가 발생한다.
- DataNotFoundException 클래스
➜ DataNotFoundException이 발생하면 @ResponseStatus 애너테이션에 의해 404(HttpStatus.NOT_FOUND) 에러 발생한다.
package com.gdsc.webboard;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ResponseStatus;
@ResponseStatus(value = HttpStatus.NOT_FOUND, reason = "entity not found")
public class DataNotFoundException extends RuntimeException {
private static final long serialVersionUID = 1L;
public DataNotFoundException(String message) {
super(message);
}
}
- 서비스 클래스에 질문 데이터를 조회하는 메서드를 추가했다면 ?
➜ 해당 메서드를 컨트롤러 클래스에서 호출하여 Question 객체를 템플릿에 전달하도록 수정해줘야 한다 !
...
public class QuestionController {
...
@GetMapping(value = "/question/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id) {
Question question = this.questionService.getQuestion(id); // 서비스 클래스의 메서드 호출
model.addAttribute("question", question); // 템플릿에 전달
return "question_detail";
}
}
템플릿 수정
- QuestionController의 detail 메서드에서 Question 객체를 "question" 이라는 이름으로 Model 객체에 저장했으면?
➜ 해당 Model 객체를 전달받고 있는 템플릿을 수정해줘야 한다 !
[ /web-board/src/main/resources/templates/question_details.html ]
<h1 th:text="${question.subject}"></h1>
<div th:text="${question.content}"></div>
▼ URL Prefix
URL Prefix ?
- "QuestionController" 에 있는 2가지 매핑.
➜ @GetMapping("/question/list"), @GetMapping(value = "/question/detail/{id}")
( URL 매핑 시 value는 생략할 수 있다. )
➜ 두 URL의 Prefix가 "/question"으로 시작함을 알 수 있고, 이 경우엔 클래스명 위에 "@RequestMapping("/question")" 애너테이션을 추가하고 메서드 단위에선 "/question"을 생략할 수 있다 !
...
import org.springframework.web.bind.annotation.RequestMapping;
@RequestMapping("/question")
@RequiredArgsConstructor
@Controller
public class QuestionController {
private final QuestionService questionService;
@GetMapping("/list")
public String list(Model model) {
...
}
@GetMapping(value = "/detail/{id}")
public String detail(Model model, @PathVariable("id") Integer id) {
...
}
}
🔻 컨트롤러의 클래스 단위의 URL 매핑은 필수사항은 아니다.