백엔드 서버에서 가장 중요한 것은 데이터의 CRUD와 데이터를 프론트엔드 서버로 전송하는 것이다. Spring은 Java 프레임워크이므로 이러한 기능 역시 특정 객체의 메소드로 구현하는데, 이러한 기능을 가진 객체를 각각 Repository, Service, Controller라고 부른다. 이 객체들은 모두 Spring Container의 한 종류이므로 Annotation을 붙여 Spring에게 알려야 제대로 사용할 수 있다.
- Repository
Repository 객체는 백엔드 서버와 DB의 연결을 담당하는, 즉 데이터의 CRUD를 담당하는 객체이다. DB에 따라 데이터를 가져오는 절차가 다를 수 있으므로 Interface를 이용해 구현하는 것이 바람직하며, DB와 데이터를 주고받는 기능만을 진행해야 한다.
// src/main/java/{package}/repository/PersonRepositoryMemory.js
package com.example.demo.repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Optional;
import com.example.demo.domain.Person;
import org.springframework.stereotype.Repository;
// 먼저 interface를 만들어 기능을 명확히 한 뒤
// DB에 따라 interface를 상속해 실질적인 기능을 만드는 것이 좋다
@Repository
public class PersonRepositoryMemory implements PersonRepositoryInterface {
private HashMap<Long, Person> hashmap = new HashMap<>();
private static Long idSetter = 0L;
@Override
public void create(Person person) {
person.setId(++idSetter);
hashmap.put(idSetter, person);
}
@Override
public List<Person> read() {
return new ArrayList<Person>(hashmap.values());
}
@Override
public Optional<Person> read(Long id) {
return Optional.ofNullable(hashmap.get(id));
}
@Override
public void update(Long id, Person person) {
person.setId(id);
hashmap.put(id, person);
}
@Override
public void delete(Long id) {
hashmap.remove(id);
}
}
- Service
Service 객체는 Repository에서 데이터를 받아온 뒤 데이터를 활용한 기능을 제공하는 객체이다. Repository 객체를 내부 변수로 갖고, 데이터가 필요할 때마다 내부 변수로 지정한 Repository에서 CRUD를 실행하는데, 이 Repository는 일반적으로 Singleton 기법을 사용하므로 이미 존재하는 Repository를 생성자의 인자로 건네주어 한 Repository를 사용하게 하는 것이 좋다.
// src/main/java/{package}/service/PersonService.js
package com.example.demo.service;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
import com.example.demo.domain.Person;
import com.example.demo.repository.PersonRepositoryInterface;
import com.example.demo.repository.PersonRepositoryMemory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class PersonService implements PersonServiceInterface{
private final PersonRepositoryInterface repo;
@Autowired
PersonService(PersonRepositoryMemory repo_) {
this.repo = repo_;
}
@Override
public Optional<Long> getId(Person person) {
List<Person> list = repo.read();
for (Person p : list)
if (Objects.equals(p.getFirst(), person.getFirst()) &&
Objects.equals(p.getLast(), person.getLast()))
return Optional.of(p.getId());
return Optional.empty();
}
@Override
public void join(Person person) {
if (this.getId(person).isEmpty()) repo.create(person);
}
@Override
public List<Person> search(Optional<String> first, Optional<String> last) {
List<Person> list = repo.read();
if (first.isPresent()) {
list = list
.stream()
.filter(p -> p.getFirst().equals(first.get()))
.collect(Collectors.toList());
}
if (last.isPresent()) {
list = list
.stream()
.filter(p -> p.getLast().equals(last.get()))
.collect(Collectors.toList());
}
return list;
}
@Override
public void change(Person before, Person after) {
this.getId(before).ifPresent(id -> repo.update(id, after));
}
@Override
public void leave(Person person) {
this.getId(person).ifPresent(id -> repo.delete(id));
}
}
- Controller
Controller 객체는 프론트엔드 서버와 직접적으로 연결되는 객체이다. 백엔드 서버에서 프론트엔드 서버로 제공하는 기능은 모두 이 Controller 객체에 자리하고, 프론트엔드 서버에 제공하고자 하는 모든 기능은 Annotation을 이용해 REST의 특정 Method와 요청 URI의 경로를 명시해 주어야 한다.
Spring은 REST의 get / post / put / delete 방식을 모두 지원하지만, 이 예제에선 html의 form을 이용해 테스트할 것이므로 put과 delete는 post 방식으로 대체하겠다.
// src/main/java/{package}/controller/PersonController.js
package com.example.demo.controller;
import java.util.List;
import java.util.Optional;
import com.example.demo.domain.Person;
import com.example.demo.service.PersonService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
@Controller
public class PersonController {
private final PersonService service;
@Autowired
PersonController(PersonService service_) {
service = service_;
}
@GetMapping("/")
public String home(Model model) {
List<Person> list = service.search(Optional.empty(), Optional.empty());
model.addAttribute("list", list);
return "person";
}
@PostMapping("/")
public String join(Person person) {
service.join(person);
return "redirect:/";
}
// @PutMapping을 사용할 수도 있지만 html의 form을 사용하기 위해 post로 대체
@PostMapping("/update")
public String change(Long id, Person person) {
service.get(id).ifPresent(p -> service.change(p, person));
return "redirect:/";
}
// @DeleteMapping을 사용할 수도 있지만 html의 form을 사용하기 위해 post로 대체
@PostMapping("/delete")
public String leave(Long id) {
service.get(id).ifPresent(person -> service.leave(person));
return "redirect:/";
}
}
// src/main/resources/templates/person.html
<html xmlns:th="http://www.thymeleaf.org">
<body>
<div>
<form id="form" action="/" method="post">
<input type="text" id="first" name="first" placeholder="first">
<input type="text" id="last" name="last" placeholder="last">
<button type="submit">등록</button>
</form>
</div>
<div>
<table>
<thead>
<tr>
<th>#</th>
<th>이름</th>
</tr>
</thead>
<tbody>
<tr th:each="person : ${list}">
<td th:text="${person.id}"></td>
<td>
<form th:action="'/update?id=' + ${person.id}" method="post">
<input type="text" name="first" th:value="${person.first}">
<input type="text" name="last" th:value="${person.last}">
<input type="hidden" name="_method" value="put"/>
<button type="submit">수정</button>
</form>
<form th:action="'/delete?id=' + ${person.id}" method="post">
<button type="submit">제거</button>
</form>
</td>
</tr>
</tbody>
</table>
</div>
</body>
</html>
만일 위 코드처럼 각 클래스에 Annotation을 붙이지 않고 설정 클래스 방식으로 관리한다면, 각 클래스에 붙은 Annotation을 제거한 뒤 아래 설정 클래스 파일처럼 관리할 수 있다.
// /src/main/{package}/Config.java
package com.example.demo;
// import com.example.demo.controller.PersonController;
import com.example.demo.repository.PersonRepositoryInterface;
import com.example.demo.repository.PersonRepositoryMemory;
import com.example.demo.service.PersonService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class Config {
// 이유는 모르겠으나 작동하지 않음
/* @Bean
public PersonController personController() {
return new PersonController(personService());
} */
@Bean
public PersonService personService() {
return new PersonService(personRepository());
}
@Bean
public PersonRepositoryInterface personRepository() {
return new PersonRepositoryMemory();
}
}
'웹 > Spring' 카테고리의 다른 글
DB와 연결하기 (0) | 2022.03.22 |
---|---|
JUnit / AssertJ를 이용하여 테스트하기 (0) | 2022.03.17 |
Container와 Bean (0) | 2022.03.15 |
간단한 MVC 패턴과 API 만들기 (0) | 2022.03.10 |
프로젝트 build하기 (0) | 2022.03.10 |