본문 바로가기
Server/Server-Project

Spring을 이용해 DB와 연동된 ToDoList iOS 앱 제작(2) - 서버제작

by print_soo 2024. 8. 29.
[전체 목차]

1. 스프링 부트 프로젝트 생성
2. 프로젝트 열기, 파일 구조 형성
3. 모델이 되는 객체, 객체 생성자 정의
4. Repository 개발
5. Sevice 개발
6. 서버 단위 테스트
7. Controller 개발
8. DB(H2) 연결
9. 통합테스트
10. JPA에서 Spring Data JPA로 전환

 

 

1. 스프링 부트 프로젝트 생성

 

 

2. 프로젝트 열기, 파일 구조 형성

이 단계에서는 Domain, Controller, Repository, Service의 패키지를 만들어준다.

 

 

3. 모델이 되는 객체, 객체 생성자 정의 - Domain

//Domain - 해당 패키지는 객체를 생성하는 곳입니다.
package todolist.todolist_spring.domain;

public class Todo {
    private Long id;           // 할 일의 고유 ID
    private String title;      // 할 일의 제목
    private String description; // 할 일의 설명 (옵션)
    private boolean completed;  // 완료 여부

    public Todo(Long id, String title, String description) {
        this.id = id;
        this.title = title;
        this.description = description;
        this.completed = false; // 처음 생성할 때는 미완료 상태로 설정
    }

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getTitle() {
        return title;
    }

    public void setTitle(String title) {
        this.title = title;
    }

    public String getDescription() {
        return description;
    }

    public void setDescription(String description) {
        this.description = description;
    }

    public boolean isCompleted() {
        return completed;
    }

    public void setCompleted(boolean completed) {
        this.completed = completed;
    }
}

 

 

4. Repository 개발

respository는 최종적으로 DB와 소통하는 부분이다. 

package todolist.todolist_spring.repository;

import todolist.todolist_spring.domain.Todo;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

public class MemoryTodoRepository implements TodoRepository {

    private List<Todo> todoList = new ArrayList<>(); //
    private Long nextId = 0L; // 할 일 ID를 생성할 때 사용

    @Override
    public Todo save(Todo todo) {
        todo.setId(++nextId); //ID 생성해서 객체에 저장
        todoList.add(todo); //todo 객체를 리스트에 저장
        return todo;
    }

    @Override
    public Optional<Todo> findById(Long id) {
        return todoList.stream()
                .filter(todo -> todo.getId().equals(id))
                .findFirst();
    }

    @Override
    public List<Todo> findAll() {
        return new ArrayList<>(todoList);
        //todoList 리스트를 복사하여 반환
    }

    @Override
    public void deleteById(Long id) {
        todoList.removeIf(todo -> todo.getId().equals(id));
        //removeIf 괄호 내부의 값이 true라면 해당 값을 삭제하는 것이다.
        //따라서 todo값을 넣고 그 값에서 id가 입력된 id와 같다면 삭제를 하는 구문이다.
    }
}

 

5. Sevice 개발

Controller와 Repository의 중간 다리 역할을 한다.

package todolist.todolist_spring.service;

import todolist.todolist_spring.domain.Todo;
import todolist.todolist_spring.repository.TodoRepository;

import java.util.List;
import java.util.Optional;

public class TodoService {
    private final TodoRepository todoRepository;

    public TodoService(TodoRepository todoRepository) {
        this.todoRepository = todoRepository;
    }

    public Todo addTodo(String title, String description) {
        Todo todo = new Todo(null, title, description);
        return todoRepository.save(todo);
    }

    public List<Todo> getAllTodos() {
        return todoRepository.findAll();
    }

    public Todo completeTodo(Long id) {
        Optional<Todo> optionalTodo = todoRepository.findById(id);

        if (optionalTodo.isPresent()) { //해당 아이디를 가진 투두가 있다면 완료 조치
            Todo todo = optionalTodo.get();
            todo.setCompleted(true);
            return todoRepository.save(todo);
        } else {
            return null;
        }
    }

    public void deleteTodoById(Long id) {
        todoRepository.deleteById(id);
    }
}

 

6. 서버 단위 테스트

package todolist.todolist_spring.service;

import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import todolist.todolist_spring.domain.Todo;
import todolist.todolist_spring.repository.MemoryTodoRepository;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.*;

class TodoServiceTest {

    TodoService todoService;
    MemoryTodoRepository memoryTodoRepository;

    @BeforeEach
    public void beforeEach() {
        memoryTodoRepository = new MemoryTodoRepository();
        todoService = new TodoService(memoryTodoRepository);
    }

    @AfterEach
    public void afterEach() {
        memoryTodoRepository.clearToDoList();
    }

    @Test
    void addTodo_Test() {
        //given - 무엇인가 주어졌고

        //when - 그걸 받아서 실행했을 때
        Todo savedTodo = todoService.addTodo("스프링 프로젝트", "스프링이용해서 프론트와 연동하기");

        //then - 어떤 결과가 나와야한다.
        assertEquals("스프링 프로젝트", savedTodo.getTitle());
    }

    @Test
    void getAllTodos_Test() {
        //given - 무엇인가 주어졌고
        Todo todo1 = new Todo(null, "스프링 프로젝트", "스프링으로 투두리스트 만들기!");
        Todo todo2 = new Todo(null, "ios 프로젝트", "ios로 투두리스트 만들기!");
        memoryTodoRepository.save(todo1);
        memoryTodoRepository.save(todo2);

        //when - 그걸 받아서 실행했을 때

        //then - 어떤 결과가 나와야한다.
        assertThat(todoService.getAllTodos().size()).isEqualTo(2);
    }

    @Test
    void completeTodo_Test() {
        //given - 무엇인가 주어졌고
        Todo todo = new Todo(null, "스프링 프로젝트", "스프링으로 투두리스트 만들기!");
        memoryTodoRepository.save(todo);


        //when - 그걸 받아서 실행했을 때
        todoService.completeTodo(todo.getId());

        //then - 어떤 결과가 나와야한다.
//        System.out.println(todo.isCompleted());
        assertThat(todo.isCompleted()).isTrue();
    }

    @Test
    void deleteTodoById_Test() {
        //given - 무엇인가 주어졌고
        Todo todo = new Todo(null, "스프링 프로젝트", "스프링으로 투두리스트 만들기!");
        memoryTodoRepository.save(todo);

        System.out.println("삭제 전:" + todoService.getAllTodos());

        //when - 그걸 받아서 실행했을 때
        todoService.deleteTodoById(todo.getId());

        System.out.println("삭제 후:" + todoService.getAllTodos());

        //then - 어떤 결과가 나와야한다.
        assertThat(todoService.getAllTodos().size()).isEqualTo(0);
    }
}

 

7. Controller 개발

iOS앱에서 특정한 요청을 보내면 그 요청의 형태에 따라 데이터를 처리한다.

package todolist.todolist_spring.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import todolist.todolist_spring.domain.Todo;
import todolist.todolist_spring.repository.TodoRepository;
import todolist.todolist_spring.service.TodoService;

import java.util.List;

//@RequestBody: HTTP 요청의 **본문(body)**에 포함된 JSON 데이터를 자바 객체로 변환하여 메서드 파라미터로 전달합니다.
//@PathVariable: URL 경로에 있는 값을 메서드 파라미터로 전달합니다. 예를 들어, /{id}와 같이 경로에 포함된 변수 값을 추출합니다.
//@RequestParam: URL의 쿼리 파라미터를 메서드 파라미터로 전달합니다. 예를 들어, ?key=value와 같은 쿼리 파라미터 값을 가져옵니다.


@RestController //앱과 연동하기에 RESTAPI
@RequestMapping("/todo")
public class TodoController {
    private final TodoService todoService;

    @Autowired
    public TodoController(TodoService todoService) {
        this.todoService = todoService;
    }

    @PostMapping
    public Todo crateTodo(@RequestBody Todo todo){
        return todoService.addTodo(todo.getTitle(), todo.getDescription());
    }

    @GetMapping
    public List<Todo> getAllTodos(){
        return todoService.getAllTodos();
    }

    @PutMapping("/{id}/complete") //아이디도 변경 됨.
    public  Todo completeTodo(@PathVariable Long id) {
        return todoService.completeTodo(id);
    }

    @DeleteMapping("/{id}")
    public  void deleteTodo(@PathVariable Long id) {
        todoService.deleteTodoById(id);
    }


}

 

8. DB(H2) 연결

(1) h2 터미널로 연결하고 테이블 생성

CREATE TABLE TODO (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY,
    title VARCHAR(255),
    description VARCHAR(255),
    completed BOOLEAN,
    PRIMARY KEY(id)
);

(2) 스프링 부트에 JPA설정 추가하기

spring.application.name=todolist-spring
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none

(3) 엔티티 매핑 - 데이터베이스의 정보를 Spring 객체와 연결해주는 것

@Entity
public class Todo {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    // id 값은 데이터 베이스가 할당해준다고 JPA에게 전달하는 코드
    
    private Long id;           // 할 일의 고유 ID
    private String title;      // 할 일의 제목
    private String description; // 할 일의 설명 (옵션)
    private boolean completed;  // 완료 여부

    ...
}

(4) JPA 레포지토리 만들기

package todolist.todolist_spring.repository;

import jakarta.persistence.EntityManager;
import todolist.todolist_spring.domain.Todo;

import java.util.List;
import java.util.Optional;

public class JpaTodoRepository implements TodoRepository{

    private final EntityManager entityManager;
    public JpaTodoRepository(EntityManager entityManager){
        this.entityManager = entityManager;
    }

    @Override
    public Todo save(Todo todo) {
        entityManager.persist(todo);
        return todo;
    }

    @Override
    public Optional<Todo> findById(Long id) {
        Todo todo = entityManager.find(Todo.class, id);
        return Optional.ofNullable(todo);
    }

    @Override
    public List<Todo> findAll() {
        return entityManager.createQuery("select t from Todo t", Todo.class).getResultList();
    }

    @Override
    public void deleteById(Long id) {
        Todo todo = entityManager.find(Todo.class, id);

        if (todo != null) {
            entityManager.remove(todo);
        } else {
            throw new IllegalArgumentException("Todo with id " + id + " not found");
        }
    }
}

(5) 트랜잭션 추가 - 서비스가 데이터베이스와의 작업을 신뢰할 수 있게 하고, 모든 작업이 올바르게 처리되도록 도와주는 과정

@Transactional
public class TodoService {
    private final TodoRepository todoRepository;

    ...
}

(6) 빈 자동 연결 없애고 빈 수동 연결

config 파일 만들어서 코드 추가

package todolist.todolist_spring;

import jakarta.persistence.EntityManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import todolist.todolist_spring.repository.JpaTodoRepository;
import todolist.todolist_spring.repository.TodoRepository;
import todolist.todolist_spring.service.TodoService;

@Configuration
public class SpringConfig {
    private EntityManager entityManager;

    public SpringConfig(EntityManager entityManager) {
        this.entityManager = entityManager;
    }

    @Bean
    public TodoService todoService(){
        return new TodoService(todoRepository());
    }

    @Bean
    public TodoRepository todoRepository(){
        return new JpaTodoRepository(entityManager);
    }
}

 

9. 통합테스트

package todolist.todolist_spring.service;

import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Commit;
import org.springframework.transaction.annotation.Transactional;
import todolist.todolist_spring.domain.Todo;
import todolist.todolist_spring.repository.TodoRepository;

import java.util.Optional;

import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertEquals;

@SpringBootTest //통합 테스트를 위한 어노테이션 (해당 어노테이션을 붙이면 실제 DB와 연결한다.)
@Transactional
public class TodoServiceIntegrationTest {
    @Autowired
    TodoService todoService;
    @Autowired
    TodoRepository todoRepository;

    @Test
    @Commit //실제로 DB에 저장하고 싶을 때 사용
    void addTodo_Test() {
        //given - 무엇인가 주어졌고

        //when - 그걸 받아서 실행했을 때
        Todo savedTodo = todoService.addTodo("스프링 프로젝트", "스프링이용해서 프론트와 연동하기");

        //then - 어떤 결과가 나와야한다.
        assertEquals("스프링 프로젝트", savedTodo.getTitle());
    }

    @Test
    @Commit
    void getAllTodos_Test() {
        //given - 무엇인가 주어졌고
        Todo todo1 = new Todo(null, "스프링 프로젝트", "스프링으로 투두리스트 만들기!");
        Todo todo2 = new Todo(null, "ios 프로젝트", "ios로 투두리스트 만들기!");
        todoRepository.save(todo1);
        todoRepository.save(todo2);

        //when - 그걸 받아서 실행했을 때

        //then - 어떤 결과가 나와야한다.
        assertThat(todoService.getAllTodos().size()).isEqualTo(2);
    }

    @Test
    @Commit
    void completeTodo_Test() {
        //given - 무엇인가 주어졌고
        Todo todo = new Todo(null, "스프링 프로젝트1", "스프링으로 투두리스트 만들기!1");
        todoRepository.save(todo);


        //when - 그걸 받아서 실행했을 때
        todoService.completeTodo(todo.getId());

        //then - 어떤 결과가 나와야한다.
//        System.out.println(todo.isCompleted());
        assertThat(todo.isCompleted()).isTrue();
    }

    @Test
    @Commit
    void deleteTodoById_Test() {
        //given - 무엇인가 주어졌고
        Todo todo = new Todo(null, "스프링 프로젝트", "스프링으로 투두리스트 만들기!");
        todoRepository.save(todo);

        //when - 그걸 받아서 실행했을 때

        todoService.deleteTodoById(todo.getId());

        //then - 어떤 결과가 나와야한다.
        assertThat(todoService.getAllTodos().size()).isEqualTo(0);
    }
}

 

10. JPA에서 Spring Data JPA로 전환

 

(1) 스프링 데이터 JPA 레포지토리 만들기

package todolist.todolist_spring.repository;

import org.springframework.data.jpa.repository.JpaRepository;
import todolist.todolist_spring.domain.Todo;

public interface SpringDataJpaTodoRepository extends JpaRepository<Todo, Long>, TodoRepository{
    
}

(2) 스프링 설정 변경하기

package todolist.todolist_spring;

import jakarta.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import todolist.todolist_spring.repository.JpaTodoRepository;
import todolist.todolist_spring.repository.TodoRepository;
import todolist.todolist_spring.service.TodoService;

@Configuration
public class SpringConfig {

    private final TodoRepository todoRepository;

    @Autowired
    public SpringConfig(TodoRepository todoRepository){
        this.todoRepository = todoRepository;
    }

    @Bean
    public TodoService todoService(){
        return new TodoService(todoRepository);
    }

}