여기서 말하는 ‘서비스’와 웹 서비스는 서로 다른 개념임에 주의하자.
우리는 그동안 QuestionController에서 QuestionRepository를 직접 접근해 질문 목록 데이터를 조회했다. 하지만 대부분의 규모 있는 스프링 부트(Spring Boot) 프로젝트는 컨트롤러에서 리포지터리를 직접 호출하지 않고 중간에 서비스를 두어 데이터를 처리한다. 이러한 서비스를 사용하여 SBB 프로그램을 개선해 보자.
서비스가 필요한 이유
서비스란 무엇일까? 서비스(service)는 간단히 말해 스프링에서 데이터 처리를 위해 작성하는 클래스이다. 우리는 그동안 서비스 없이도 웹 프로그램을 동작시키는 데 문제가 없었다. 그런데 굳이 서비스를 사용해야 할까? 서비스가 필요한 이유를 좀 더 자세히 알아보자.
복잡한 코드를 모듈화할 수 있다
예를 들어 A라는 컨트롤러가 어떤 기능을 수행하기 위해 C라는 리포지터리의 메서드 a, b, c를 순서대로 실행해야 한다고 가정해 보자. 그리고 B라는 컨트롤러도 A 컨트롤러와 동일한 기능을 수행해야 한다면 A, B 컨트롤러가 C 리포지터리의 메서드 a, b, c를 호출해 사용하는 중복된 코드를 가지게 된다. 이런 경우 C 리포지터리의 a, b, c 메서드를 호출하는 기능을 서비스로 만들고 컨트롤러에서 이 서비스를 호출하여 사용할 수 있다. 즉, 서비스를 사용하면 이와 같은 모듈화가 가능하다.
엔티티 객체를 DTO 객체로 변환할 수 있다.
우리가 앞에서 작성한 Question, Answer 클래스는 모두 엔티티 클래스이다. 엔티티 클래스는 데이터베이스와 직접 맞닿아 있는 클래스이므로 컨트롤러 또는 타임리프와 같은 템플릿 엔진에 전달해 사용하는 것은 좋지 않다. 왜냐하면 엔티티 객체에는 민감한 데이터가 포함될 수 있는데, 타임리프에서 엔티티 객체를 직접 사용하면 민감한 데이터가 노출될 위험이 있기 때문이다.
이러한 이유로 Question, Answer 같은 엔티티 클래스는 컨트롤러에서 사용하지 않도록 설계하는 것이 좋다. 그래서 Question, Answer를 대신해 사용할 DTO (Data Transfer Object) 클래스가 필요하다. 그리고 Question, Answer 등의 엔티티 객체를 DTO 객체로 변환하는 작업도 필요하다. 그러면 엔티티 객체를 DTO 객체로 변환하는 일은 어디서 처리해야 할까? 이때도 서비스가 필요하다. 서비스는 컨트롤러와 리포지터리의 중간에서 엔티티 객체와 DTO 객체를 서로 변환하여 양방향에 전달하는 역할을 한다.
이 책은 간결한 설명을 위해 별도의 DTO를 만들지 않고 엔티티 객체를 컨트롤러와 타임리프에서 그대로 사용할 것이다. 하지만 실제 프로그램을 개발할 때는 엔티티 클래스를 대신할 DTO 클래스를 만들어 사용하기를 권장한다.
서비스 만들기
컨트롤러에서 리포지터리 대신 사용할 서비스를 만들어 보자. 먼저 src/main/java 디렉터리의 com.mysite.sbb.question 패키지에 QuestionService.java 파일을 만들어 다음과 같은 내용을 작성해 보자.
[파일명:/question/QuestionService.java]
package com.mysite.sbb.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();
}
}
생성한 클래스를 서비스로 만들기 위해서는 이와 같이 클래스명 위에 @Service 애너테이션을 붙이면 된다. @Controller, @Entity 등과 마찬가지로 스프링 부트는 @Service 애너테이션이 붙은 클래스를 서비스로 인식하므로 서비스를 쉽게 생성할 수 있다.
여기서 questionRepository 객체는 @RequiredArgsConstructor에 의해 생성자 방식으로 주입된다.
이 코드에서는 질문 목록 데이터를 조회하여 리턴하는 getList 메서드를 추가했다. getList 메서드의 내용을 살펴보면 컨트롤러(QuestionController)에서 리포지터리를 사용했던 부분을 그대로 옮긴 것을 알 수 있다.
컨트롤러에서 서비스 사용하기
QuestionController.java 파일로 돌아가 QuestionController가 리포지터리 대신 서비스를 사용하도록 다음과 같이 수정해 보자.
[파일명:/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";
}
}
여기서 questionService 객체도 @RequiredArgsConstructor에 의해 생성자 방식으로 주입된다.
브라우저로 http://localhost:8080/question/list 페이지에 접속하면 리포지터리를 사용했을 때와 동일한 화면을 볼 수 있다.

앞으로 작성할 다른 컨트롤러들도 이와 같이 리포지터리를 직접 사용하지 않고 컨트롤러 → 서비스 → 리포지터리 순서로 접근하는 과정을 거쳐 데이터를 처리할 것이다.