2 백엔드 개발

2.1 백엔드 개발 환경 설정

2.1.1 자바 버전

  • java 8

  • java 11

2.1.2 통합개발환경

  • eclipse

2.1.3 스프링 프레임워크와 의존성 주입

  • 스프링이란 메모리 혹은 CPU 자원이 적게 들어가는 경량(?) 프레임워크이다.

프레임워크 제공하는 클래스나 라이브러리를 사용하거나 이를 상속 및 구현해 발자들이 확장하여 사용 가능한 코드 .

  • 의존성 주입이란 클래스가 의존하는 다른 클래스들을 외부에서 주입시키는 것을 의미한다.

  • 의존성 주입을 구현하는 방법

    • 생성자를 이용하여 주입

    • Setter를 이용하여 주입

public class TodoService {
    private final ITodoPersistence persistence; 
    
    public TodoService(ITodoPersistence persistence) {
        this.persistence = persistence;
    }
    
    public void create(. . .) {
        . . .
        persistence.create(. . .);
    }
}

// Todo Service 오브젝트를 생성할 ITodoPersistence 구현부를 넘겨줌.
public static void main (String[] args) {
    ITodoPersistence persistence = new FileTodoPersistence();
    TodoService service = new TodoService(persistence);
}

  • 스프링의 의존성 주입 컨테이너로서의 기능

    • 스프링은 베이스 패키지와 그 하위 패키지에서 자바 빈을 찾아 ApplicationContext (스프링의 의존성 주입 컨테이너 오브젝트)에 등록

    • 애플리케이션 실행 중 어쩐 오브젝트가 필요한 경우 의존하는 다른 오브젝트를 찾아 연결해 줌

  • 스프링 프레임워크에서 의존성을 명시하는 방법 (때에 따라 적합한 방법 사용)

    • 어노테이션

    • XML

    • 자바 코드

  • 의존성 주입을 직접 구현하지 않고 스프링 프레임워크를 사용하는 이유 ?

    • 규모가 큰 프로그램의 경우 관리해야하는 오브젝트가 많아질 수록 비효율적

  • 스프링 부트

    • 스탠드얼론 프로덕션급의 스프링 기반 애플리케이션 구동을 가능하게 해줌

    • 임베디드 톰캣이나 제티(jetty)같은 웹 서버를 애플리케이션 실행 시 함께 실행 및 설정

    • 많은 부분을 자동으로 설정해줌

웹서버

2.1.4 스프링 프레임워크와 디스패처 서블릿

서블릿

  • 자바 서블릿 (Java Servlet)

    • http 요청이 서버로 전달되면 웹 서버는 받은 요청을 해석하여 해당되는 서블릿 클래스를 실행

    • 그러면 서블릿 컨테이너가 서블릿 서브 클래스를 실행

    • 개발자는 javax.servlet.http.HttpServlet을 상속받는 서브 클래스를 작성해야 함

package com.example.Demo;

import java.io.*;
import javax.servlet.*;
import javax.servlet.http.*;

public class Hello extends HttpServlet {
    @Override
    public void doGet(HttpServeltRequest request, HttpServletResponse response) throws ServletException, IOException {
    //parameter 해석
    String name = request.getParameter("name");
    
    //business logic 실행
    process(name);
    
    //response 구축
    response.setContentType("text/html");
    PrintWriter out = response.getWriter();
    out.print("<html>");
    //UI 부분
    out.print("</html>");
    }
    
    private void process(String name) {
        //business logic
    }
}
  • 코드 흐름

    • HttpServlet을 상속하는 서브 클래스를 만들고 doGet() 메서드 구현

    • 매개변수로 넘어오는 HttpServletRequest에서 원하는 정보를 추출

    • 비즈니스 로직인 process()를 실행

    • 반환할 정보를 HttpServletResponse에 담음

  • 문제

    • 매개변수 해석과 응답 부분을 항상 작성해 줘야함

    • API 하나 만들 때마다 위와같은 똑같은 작업을 반복

  • DispatcherServlet

    • 스프링 부트는 DispatcherServlet이라는 서블릿 서브 클래스를 이미 구현하고 있음

    • 개발자는 스프링 부트가 제공하는 어노테이션과 인터페이스를 이용하면 됨

@RestController //Json을 리턴하는 웹 서비스임을 명시
public class HelloController {
    @GetMapping("/test")
    public String process(@RequestParam String name) {
        // business logic
        return "test" + name;
    }
}

  • 스프링 사용했을 경우

    • HttpServlet을 상속받지 않아도 됨

    • doGet을 오버라이드하지 않아도 됨

    • HttpServletRequest를 직접 파싱하지 않아도 됨

    • HttpServletResponse를 작성하지 않아도 됨

2.1.5 스프링부트 프로젝트 설정

  • Project

  • Language

  • Spring Boot

  • Project Metadata

    • Group

    • Artifact

    • Packaging

    • Java version

  • Dependencies

2.1.6 메인 메서드와 @SpringBootApplication

  • @SpringBootApplication

    • 해당 클래스가 스프링 부트를 설정하는 클래스임을 의미

    • 이 어노테이션이 달린 클래스가 있는 패키지를 베이스 패키지로 간주

    • 컴포넌트 스캐닝을 할 수 있는 @ComponentScan 을 포함

    // . . . 다른 어노테이션들
    @ComponentScan //매개변수 생략
    public @interface SpringBootAllication {
    	// . . .
    }

  • @Autowired

    • 자동으로 다른 오브젝트를 찾아 연결

  • @Component

    • 스프링에게 이 클래스를 자바 빈으로 등록시키라고 알려주는 어노테이션

    • 앞선 예제에서 사용하였던 @Service 내부에도 @Component 어노테이션을 달고있음 (이후 나올 @Controller 와 @Repository 등 다양한 스테레오 타입 어노테이션 내부에도 @Component이 달려있음)

      @Component
      public @interface Service {
      	. . .
      }

  • 중간 정리

    → 관리하고 싶은 빈의 클래스 상단에 @Component 를 추가

    → 자동으로 이 오브젝트를 스프링에 빈으로 등록

    → @Autowired와 함께 스프링이 필요할 때 알아서 오브젝트를 생성

🙌🏻 What if 스프링이 자동으로 오브젝트를 찾아 생성하게 하고싶지 않다면 ? (= @Component를 추가하지 않고 스프링을 통해 빈을 관리하고 싶다면 ? )

  • 엔지니어가 오브젝트를 어떻게 생성하고

  • 어느 클래스에서 사용하는지 정확히 알아야 하는 경우

  • 로컬 환경에서 애플리케이션을 실행할 때, 자동으로 연결되는 빈이 아닌 다른 빈을 사용하고 싶을 경우

  • @Bean

    • 스프링에게 이 오브젝트를 어떻게 생성해야 하는지 알려줌

    • 매개변수를 어떻게 넣어줘야 하는지 알려줌

  • 정리

    • 스프링부트 애플리케이션 시작

    • @ComponentScan 어노테이션 있는 경우 베이스 패키지와 하위 패키지에서 @Component가 달린 클래스를 찾음

    • 필요한 경우 @Component가 달린 클래스의 오브젝트를 생성

      • 이때 생성하려는 오브젝트가 다른 오브젝트에 의존할 경우, 그 멤버 변수 오브젝트를 찾아 넣어주어야 함

      • @Autowired를 사용하는 경우 스프링이 오브젝트를 찾아 생성하여 넣어줌

        1. @Autowired에 연결된 변수의 클래스가 @Component가 달린 클래스 → 스프링이 오브젝트를 생성해 넘겨줌

        2. 만약 @Bean 어노테이션으로 생성하는 오브젝트 → @Bean이 달린 메서드를 불러 생성하여 넘겨줌

2.1.7 빌드 자동화 툴 : Gradle과 라이브러리 추가

  • Gradle

    • 빌드 자동화 툴

    • 컴파일, 라이브러리 다운로드, 패키징, 테스팅 등 자동화

  • 빌드 자동화를 사용하는 이유 ?

    • 빌드 자동화 툴이 없다면

      • 라이브러리 사이트에서 jar 파일 다운, Project Build Path에 추가

      • 오퍼레이터 또는 개발자가 모든 라이브러리를 컴파일해 빌드를 하고 유닛 테스트를 실행시켜야 함 (빌드 순서 고려)

    • 빌드 자동화 툴 사용

      • 라이브러리를 다운받는 대신 원하는 라이브러리와 버전을 코드로 작성

      • 오퍼레이터가 직접 컴파일, 빌드, 유닛테스트를 실행하는 대신 이 과정을 일련의 코드로 적어, gradle이 이 코드를 해석해 프로젝트 빌드에 필요한 작업 실행

plugins {
	id 'org.springframework.boot' version '2.6.4'
	id 'io.spring.dependency-management' version '1.0.11.RELEASE'
	id 'java'
}

group = 'com.soobin'
version = '0.0.1-SNAPSHOT'
sourceCompatibility = '11'

configurations {
	compileOnly {
		extendsFrom annotationProcessor
	}
}

repositories {
	mavenCentral()
}

dependencies {
	implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
	implementation 'org.springframework.boot:spring-boot-starter-web'
	compileOnly 'org.projectlombok:lombok'
	developmentOnly 'org.springframework.boot:spring-boot-devtools'
	runtimeOnly 'com.h2database:h2'
	annotationProcessor 'org.projectlombok:lombok'
	testImplementation 'org.springframework.boot:spring-boot-starter-test'
}

tasks.named('test') {
	useJUnitPlatform()
}

  • plugins

  • group

  • version

  • sourceCompatibility

  • configurations

  • repositories

  • dependencies

  • test

2.1.8 디펜던시 라이브러리 추가

  • 구글 구아바 라이브러리 추가

    • 메이븐 센트럴(https://mvnrepository.com/)을 사용하므로 메이븐 리포지터리 이용해 라이브러리를 추가

    • 메이븐 센트럴에서 google guava를 검색

    • 원하는 버전 선택 (주로 Usage가 많은 버전)

    // <https://mvnrepository.com/artifact/com.google.guava/guava>
    implementation 'com.google.guava:guava:31.0.1-jre'
    • 위 코드를 build.gradlw의 dependency 부분에 추가

2.1.9 롬복

롬복이 제공하는 어노테이션 프로세서가 getter, setter, builder, constructor 프로젝트 컴파일 시 관련 코드를 자동으로 작성해 줌

2.1.10 포스트맨 API 테스트

포스트맨은 REST API 테스팅 툴

  • REST API는 크게 URI, HTTP 메서드, 요청 매개변수 또는 요청 바디로 구분되는데 이를 브라우저에서 테스팅하는 데는 한계

  • 커맨드라인 툴인 cURL도 존재(하지만)

  • 사용이 간편하고 직관적인 GUI를 제공하는 포스트맨 사용

2.2 백엔드 서비스 아키텍처

2.2.1 레이어드 아키텍

  • 레이어드 아키텍처 → 프로젝트 내부에서 어떻게 코드를 관리할 것인지

  • REST 아키텍처 → 클라이언트가 서비스를 이용할 때 어떤 형식으로 요청을 보내고 응답을 받을 것인지

// Some code

2.2.2 모델, 엔티티, DTO

  • 자바로 된 비즈니스 애플리케이션 클래스 종류

    1. 기능을 하는 클래스 : 로직을 구현

      • 컨트롤러

      • 서비스

      • 퍼시스턴스

    2. 데이터를 담는 클래스

      • 엔티티

      • 모델

      • DTO

  • 모델과 엔티티 (이 프로젝트에서는 모델과 엔티티를 한 클래스에 구현)

    • 비즈니스 데이터를 담는 역할

    • 데이터베이스의 테이블과 스키마를 표현

//TodoEntity.java
package com.soobin.todoWeb.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class TodoEntity {
    private String id;
    private String userId;
    private String title;
    private boolean done;
}

  • @Builder

    • 오브젝트 생성을 위한 디자인 패턴 중 하나

    • 롬복이 제공

    • Builder 클래스를 따로 개발하지 않고도 오브젝트 생성

    TodoEntity todo = TodoEntity.builder()
            .id("t-10328373")
            .userId("developer")
            .title("Implement Model")
            .build();
    • 생성자를 이용하는 방법과 비교하자면, 매개변수의 순서를 기억할 필요가 없음

  • @NoArgsConstructor

    • 매개변수가 없는 생성자를 구현

    public TodoEntity() {
    
    }

  • @AllArgsConstructure

    • 클래스의 모든 멤버 변수를 매개변수로 받는 생성자를 구현

    public TodoEntity(String id, String userId, String title, boolean done) {
    	super();
    	this.id = id;
    	this.userId = userId;
    	this.title = title;
    	this.done = done;
    }

  • @Data

    • 클래스 멤버 변수의 Getter/Setter 메서드를 구현

    public String getId() {
    	return id;
    }
    public void setId() {
    	this.id = id;
    }

  • DTO (Data Transfer Object)

    • 서비스가 요청을 처리하고 클라이언트로 반환하는 경우 사용

    • 데이터는 전달하는 경우 사용

  • 모델을 그래도 리턴하지 않고 DTO를 사용하는 이유 ?

    • 비즈니스 로직 캡슐화

      • 자사 DB schema 숨김

    • 클라이언트가 필요로하는 정보를 커스터마이징해서 전달

      • 에러메시지 필드 추가

//TodoDTO.java 
package com.soobin.todoWeb.dto;

import com.soobin.todoWeb.model.TodoEntity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class TodoDTO {
    private String id;
    private String title;
    private boolean done;

    public TodoDTO(final TodoEntity entity) {
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.done = entity.isDone();
    }
//ResponseDTO.java
package com.soobin.todoWeb.dto;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
public class ResponseDTO<T> {
    private String error;
    private List<T> data;
}

2.2.3 REST API

  • REST(Representational State Transfer) API

    • 아키텍처 스타일

아키텍처 패턴 : 어떤 반복되는 문제 상황을 해결하는 도구

아키텍처 스타일 : 반복되는 아키텍처 디자인

  • REST 제약조건

    • 클라이언트-서버 (Client-Server)

    • 상태가 없는 (Stateless)

    • 캐시되는 (Cacheable) 데이터

    • 일관적인 인터페이스 (Uniform Interface)

    • 레이어 시스템 (Layered System)

    • 코드-온-디맨드 (Code-On-Demand)

  • 클라이언트-서버

    • 리소스를 관리하는 서버가 존재

    • 다수의 클라이언트가 리소스를 소비하려고 네트워크를 통해 접근

리소스란 REST API가 리턴할 수 있는 모든 것. (HTML, JSON, Image 등)

  • 상태가 없는 (Stateless)

    • 클라이언트가 서버에 요청을 보낼 때 이전 요청의 영향을 받지 않음

  • 캐시되는 (Cacheable) 데이터

    • 서버에서 리소스를 리턴할 때 캐시가 가능한지 아닌지 명시할 수 있어야 함

    • HTTP에서는 cache-control이라는 헤더에 리소스의 캐시 여부 명시

  • 일관적인 인터페이스 (Uniform Interface)

    • 시스템 또는 애플리케이션의 리소스에 접근할 때 인터페이스가 일관적이어야 함

      • URI의 일관성

      • 요청 형식의 일관성

      • 응답 형식의 일관성

  • 레이어 시스템 (Layered System)

    • 클라이언트가 서버에 요청할 때 여러 개의 레이어로 된 서비를 거칠 수 있음

      (ex. 인증 서버, 캐싱 서버, 로드 밸런서)

    • 레이어들은 요청과 응답에 어떤 영향을 미치지 않음

    • 클라이언트는 서버의 레이어 존재 유무를 알지 못함

  • 코드-온-디맨드 (Code-On-Demand) (선택사항)

    • 클라이언트는 서버에 코드를 요청할 수 있고 서버가 리턴한 코드를 실행 할 수 있음

REST는 HTTP와 다르다. REST는 HTTP를 이용해 구현하기 쉽고 대부분 그렇게 구현하지만, 엄밀히 말하면 REST는 아키텍처이고, HTTP는 REST 아키텍처를 구현할 때 사용하면 쉬운 프로토콜이다.

2.2.4 컨트롤러 레이어 : 스프링 REST API 컨트롤러

  • HTTP 요청 처리

    • HTTP는 GET/POST/DELETE/OPTIONS 등과 같은 메서드와 URI를 이용해 서버에 요청을 보냄

    GET /test HTTP/1.1
    Host: localhost:8080
    Content-Type: application/json
    Content-Length: 17
    {
    	"id": 123
    }

    • localhost:8080에 http GET 메서드를 이용해 test라는 리소스를 요청

    • 서버는 자기 주소를 제외한 /{리소스} 부분을 이해하고 또 이 요청이 어떤 HTTP 메서드를 이용했는지 알아야 함

    • 그 후 해당 리소스의 HTTP 메서드에 연결된 메서드 실행

  • 스프링 부트 스타터 웹 (spring-boot-starter-web)

    • 위와 같은 연결 도와주는 라이브러리

  • @RestController

    • 이 컨트롤러가 RestController임을 명시

    • http와 관려된 코드 및 요청/응답 매핑을 스프링이 알아서 해줌

  • @RequestMapping

  • @GetMapping

    • @GetMapping을 통해 이 메서드의 리소스와 HTTP 메서드를 지정

    !!@RequestMapping은 URI 경로에, @GetMapping은 HTTP 메서드에 매핑

    (하지만, @GetMapping에서도 URI 경로를 지정할 수 있음)

    • @PostMapping, @PutMapping, @DeleteMapping 등도 있음

@GetMapping, @PostMapping, @PutMapping, @DeleteMapping dms tmvmfld 4.3부터 지원되기 시작했고, 그 이전에는 아해와 같이 하나의 어노테이션에 HTTP 메서드를 매개변수로 주는 형태로 컨트롤러 메서드를 연결했다.

@RequestMapping(value=”/경로”, method=RequestMethod.GET)

@RequestMapping(value=”/경로”, method=RequestMethod.POST)

@RequestMapping(value=”/경로”, method=RequestMethod.PUT)

@RequestMapping(value=”/경로”, method=RequestMethod.DELETE)

매개변수를 넘겨받는 방법

/test가 아닌 /test/{id}로 PathVariable이나 /test?id=123처럼 요청 매개변수를 받아야 하는 경우

  • @PathVariable

    • /{id}와 같이 URI의 경로로 넘어오는 값을 변수로 받을 수 있음

  • @RequestParam

    • ?id={id}와 같이 요청 매개변수로 넘어오는 값을 변수로 받을 수 있음

  • @RequestBody

    • 보통 반환하고자 하는 리소스가 복잡할 때 사용

    {
    	"id" : 123, 
    	"message" : "Hello ?"
    }

문자열보다 복잡한 오브젝트를 리턴하려면 ?

  • @RestController의 내부에는 두 어노테이션 있음

    @Controller
    @ResponseBody
    public @interface RestController {
    . . .
    }

    • @Controller는 @Component로 스프링이 이 클래스의 오브젝트를 생성하고 다른 오브젝트들과의 의존성을 연결한다는 뜻

    • @ResponseBody는 이 클래스의 메서드가 리턴하는 것은 웹 서비스의 ResponseBody라는 뜻

      • 메서드가 리턴할 때 스프링은 리턴된 오브젝트를 JSON의 형태로 바꾸고 HttpResponse에 담아 반환한다는 뜻

스프링이 오브젝트를 JSON으로 바꾸는 것처럼, 오브젝트를 저장하거나 네트워크를 통해 전달할 수 있도록 변환하는 것을 직렬화라고 하고, 반대의 작업을 역직렬화라고 한다.

  • ResponseEntity

    • 앞으로 우리가 작성할 컨트롤러는 모두 ResponseEntity를 반환할 예정

    • HTTP 응답의 바디뿐만 아니라 여러 다른 매개변수들, 이를테면 status나 header를 조작하고 싶을 때 사용

package com.soobin.todoWeb.controller;

import com.soobin.todoWeb.dto.ResponseDTO;
import com.soobin.todoWeb.dto.TestRequestBodyDTO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

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

@RestController
@RequestMapping("test")
public class TestController {


    @GetMapping
    public String testController() {
        return "Hello World !";
    }

    @GetMapping("/testGetMapping")
    public String testControllerWithPath() {
        return "Hello World! testGetMapping !!";
    }

    @GetMapping("/{id}")
    public String testControllerWithPathVariables(@PathVariable(required = false) int id) {
        return "Hello world ! ID :" + id;
    }

    @GetMapping("/testRequestParam")
    public String testControllerRequestParam(@RequestParam(required = false) int id) {
        return "Hello world ! ID " + id;
    }

    @GetMapping("/testRequestBody")
    public String testControllerRequestBody (@RequestBody TestRequestBodyDTO testRequestBodyDTO) {
        return "Hello World ! ID : " + testRequestBodyDTO.getId() + " Message : " + testRequestBodyDTO.getMessage();
    }

    @GetMapping("/testResponseBody")
    public ResponseDTO<String> testControllerResponseBody() {
        List<String> list = new ArrayList<>();
        list.add("Hello World! I'm ResponseDTO");
        list.add("hello hello");
        ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
        return response;
    }

    @GetMapping("/testResponseEntity")
    public ResponseEntity<?> testControllerResponseEntity() {
        List<String> list = new ArrayList<>();
        list.add("Hello World ! I'm ResponseEntity.");
        ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
        return ResponseEntity.ok().body(response);
    }

    @GetMapping("/todo")
    public ResponseEntity<?> testTodo() {
        String str = service.testService();
        List<String> list = new ArrayList<>();
        list.add(str);
        ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
        return ResponseEntity.ok().body(response);
    };

2.2.5 서비스 레이어 : 비즈니스 로직

서비스레이어

  • 컨트롤러와 퍼시스턴스 사이에서 비즈니스 로직을 수행

  • HTTP와 심닐히 연관된 컨트롤러에서 분리돼 있고 또 데이터베이스와 긴밀히 연관된 퍼시스턴스와도 분리돼 있어서, 개발하고자하는 로직에 집중할 수 있음

package com.soobin.todoWeb.service;

import org.springframework.stereotype.Service;

@Service
public class TodoService {
    public String testService() {
        return "Test Service";
    }
}
package com.soobin.todoWeb.controller;

import org.springframework.beans.factory.annotation.Autowired;
// 여러 다른 패키지들

@RestController
@RequestMapping("test")
public class TestController {

    @Autowired
    private TodoService service;

    @GetMapping("/test")
    public ResponseEntity<?> testTodo() {
        String str = service.testService();
        List<String> list = new ArrayList<>();
        list.add(str);
        ResponseDTO<String> response = ResponseDTO.<String>builder().data(list).build();
        return ResponseEntity.ok().body(response);
    }
}

  • @Service

    • 내부에 @Component 어노테이션을 갖고 있음

    • 이 클래스가 스프링 컴포넌트임을 알려줌

    • 기능적으로 비즈니스 로직을 수행하는 서비스 레이어임을 알려줌

@RestController도 내부에 @Component 어노테이션을 갖고있고, @Service, @RestController 모두 자바 빈이고 스프링이 관리한다.

스프링은 TodoController 오브젝트를 생성할 때 TodoController 내부에 선언된 TodoService에 @Autowired 어노테이션이 붙어 있다는 것을 확인하고, 알아서 빈을 찾은 다음 그 빈을 인스턴스 멤버 변수에 연결한다.

2.2.6 퍼시스턴스 레이어 : 스프링 데이터 JPA

시스턴스 레이어 : 스프링 데이터 JPA

  • MySQL

  • MySQL Workbench

  • JDBC 드라이버

    • 자바에서 데이터베이스에 연결할 수 있도록 도와주는 라이브러리

    • MySQL 클라이언트 같은 것

MySQL 클라이언트도 내부에서 JDBC/ODBC 등의 드라이버를 사용한다

//데이터베이스 콜 스니팻
String sqlSlectAlltodo = "SELECT * FROM Todo where USER_ID = " + request.getUserId();
String connectionUrl = "jdbc:mysql://mydb:3306/todo";

try {
	/* (1) 데이터베이스에 연결  */
	Connection conn = DriverManager.getConnection(connectionUrl, "username", "password");
	/* (2) SQL 쿼리 준비 */
	PreparedStatement ps = conn.prepareStatement(sqlSelectAllTodos);
	/* (3) 쿼리 실행 */
	RequltSet rs = ps.excuteQuery();

	/* (4) 결과를 오브젝트로 파싱 */
	while (rs.next()) {
		long id = rs.getString("id");
		String title = rs.getString("title");
		Boolean isDone = rs.getBeoolean("done");

		todos.add(new Todo(id, title , idsdDone));
	}
} catch (SQLException e) {
	// handle the exception
}
  • JDBC 커넥션인 Connectio을 이용해 데이터베이스에 연결

  • sqlSelectAllTodos에 작성된 SQL을 실행

  • ResultSet이라는 클래스에 결과를 담아옴

  • while문 내부에서 ResultSet을 Todo 오브젝트로 바꿔줌

이런 작업을 ORM이라고 함

  • 데이터베이스 테이블을 자바 내에서 사용하려면 위와같은 작업을 엔티티마다 해줘야 함

  • 이런 ORM 작업을 집중적으로 해주는 DAO(Data Access Object) 클래스를 작성

  • 보통 생성, 검색, 수정, 삭제 같은 기본적인 오퍼레이션들을 엔티티마다 작성해줌

시간이 흐르며 이런 반복 작업을 줄일 수 있는 Hibernate같은 ORM 프레임워크가 등장했고

더 나아가 JPA나 스프링 데이터 JPA 같은 도구들이 개발됐다.

  • JPA

    • 반복해서 데이터베이스 쿼리를 보내 ResultSet을 파싱해야 하는 작업의 노고를 덜어줌

    • JPA는 스펙 (스펙은 ‘JPA의 구현을 위해서 이런 이런 기능을 작성해라’라고 말해주는 지침 문서)

    • JPA란 자바에서 데이터베이스 접근, 저장, 관리에 필요한 스펙

    • 이 스펙을 구현하는 구현자를 JPA Provider라고 하는데, 그 중 대표적인 것이 Hibernate

  • 스프링 데이터 JPA vs JPA

    • 스프링 데이터 JPA는 JPA +a

    • JPA를 더 사용하기 쉽게 더와주는 스프링의 프로젝트

    • 기술적으로는 추상화 했다고 하며, 이는 사용하기 쉬운 인터페이스를 제공한다는 뜻

    • JpaRepository는 그런 인텉페이스 중 하나

    • 스프링 데이터 JPA를 사용하려면 spring-boot-starter-jpa 라이브러리가 필요

  • H2

    • In-Memory 데이터베이스

    • 로컬 환경에서 메모리상에 데이터베이스를 구축해 줌

    • build.gradle에 h2를 디펜던시로 설정하면 @SpringBootApplication 어노테이션의 일부로 스프링이 알아서 애플리케이션을 H2 데이터베이스에 연결

  • 엔티티 클래스

    • 보통 데이터베이스 테이블마다 그에 상응하는 엔티티 클래스가 존재

    • 그 자체가 테이블을 정의해야 함

      • ORM이 엔티티를 보고 어떤 테이블의 어떤 필드에 매핑해야 하는지 알 수 있어야 함

    • 어떤 필드가 기본 키인지, 외래 키인지도 구분할 수 있어야 함

      • 이런 데이터베이스 테이블 스키마에 관한 정보는 javax.persistence가 제공하는 JPA 관련 어노테이션을 이용해 정의함

  • 자바 클래스를 엔티티로 정의할 때 주의할 점

    1. 클래스에는 매개변수가 없는 생성자, NoArgsConstructor가 필요

    2. Getter/Setter가 필요

    3. 기본키를 지정해 주어야 함

package com.soobin.todoWeb.model;

import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.hibernate.annotations.GenericGenerator;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@Table(name = "Todo")
public class TodoEntity {
    @Id
    @GeneratedValue(generator = "system-uuid")
    @GenericGenerator(name="system-uuid", strategy="uuid")
    private String id;
    private String userId;
    private String title;
    private boolean done;
}

  • @Entity

    • 자바 클래스를 엔티티로 지정

    • @Entity(”TodoEntity”)처럼 매개변수를 넣어 엔티티에 이름을 부여할 수 있음

  • @Table

    • 테이블 이름 지정

    • 해당 엔티티가 데이터베이스의 Todo 테이블에 매핑된다는 뜻

    • @Table을 추가하지 않거나 추가해도name을 명시하지 않는다면 @Entity의 이름을 테이블 이름으로 간주

    • @Entity에 이름을 지정하지 않은 경우 클래스의 이름을 테이블 이름으로 간주

  • @Id

    • 기본 키가 될 필드에 지정

  • @GeneraredValue

    • ID를 자동으로 생성

    • 매개변수인 generator로 어떤 발식으로 ID를 생성할지 지정 가능

    • 위에 사용한 system-uuid는 @GenericGenerator에 정의된 generator의 이름

  • @GenericGenerator

    • Hibernate가 제공하는 기본 Generator가 아니라 나만의 Generator를 사용하고 싶은 경우 이용

    • 기본 Generator로는 INCREMENTAL, SEQUENCE, IDENTITY 등이 있음

이렇게 uuid를 사용하는 “system-uuid”라는 이름의 GenericGenerator를 만들어, 이 Generator는 @GeneratedValue가 참조해 사용한다.

UUID

package com.soobin.todoWeb.persistence;

import com.soobin.todoWeb.model.TodoEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

@Repository
public class TodoRepository extends JpaRepository<TodoEntity, String> {
}

  • JpaRepository

    • 인터페이스임

    • 이 인터페이스를 사용하려면 새 인터페이스를 작성해 JpaRepository를 확장해야 함

    • JpaRepository<T, ID>가 Genetic Type을 받음

      • T는 테이블에 매핑될 엔티티 클래스

      • ID는 이 엔티티의 기본 키 타입

  • @Repository

    • @Component 어노테이션 포함

//JpaRepositoryBean
public interface JpaRepository<T, ID> extends PagingAndSortingRepository<T, ID>, QueryByExampleExcutor<T> {
	List<T> findAll();
	
	List<T> findAll(Sort var1);

	List<T> findAllById(Iteravle<ID> var1);

	void flush();

	<S extends T> S savedAndFlush(S var1);

	void deleteInBatch(Iterabel<T> Vvar1);
	
	void deleteAllBatch();
	
	T getOne(ID var1);
	
	<S extends T> List<S> findAll(Example<S> var1);

	<S extends T> List<S> findAll(Example<S> var1, Sort var2);
}

AOP

기본적인 쿼리가 아닌 경우 ?

@Repository 
public interface TodoRepository extends JpaRepository<TodoEntity, Long> {
	List<TodoEntity> findByUserId(String userId);
} 
  • 스프링 데이터 JPA가 메서드 이름을 파싱해서 SELECT * FROM TodoRepository WHERE userId = {userId}와 같은 쿼리를 작성해 실행

  • 메서드 이름은 쿼리, 매개변수는 쿼리의 where문에 들어갈 값을 의미

더 복잡한 쿼리의 경우

@Repository 
public interface TodoRepository extends JpaRepository<TodoEntity, Long> {

	// ?1은 메서드의 매개변수의 순서 위치
	@Query("select * from Todo t where t.userId = ?1")
	List<TodoEntity> findByUserId(String userId);
} 
 

메서드 이름 작성 방법과 예제 공식 사이트 레퍼런스 :https://docs.spring.io/spring-data/jpa/docs/current/reference/html/#jpa-query-methods.query-creation

2.3 서비스 개발 및 실습

2.3.1 Create Todo

  • 구현 과정 : 퍼시스턴스 → 서비스 → 컨트롤러

  • @Slf4j

    • 로그 어노테이션

  • Create Todo

퍼시스턴스 구현

package com.soobin.todoWeb.persistence;

import com.soobin.todoWeb.model.TodoEntity;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface TodoRepository extends JpaRepository<TodoEntity, String> {
    List<TodoEntity> findByUserId(String userId);
}

서비스 구현

package com.soobin.todoWeb.service;

import com.soobin.todoWeb.dto.TodoDTO;
import com.soobin.todoWeb.model.TodoEntity;
import com.soobin.todoWeb.persistence.TodoRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

@Slf4j
@Service
public class TodoService {

    @Autowired
    private TodoRepository repository;

    public List<TodoEntity> create(final TodoEntity entity) {
        validate(entity);
        repository.save(entity);
        return repository.findByUserId(entity.getUserId());
    }

		private void validate(final TodoEntity entity) {
        if(entity == null) {
            log.warn("Entity cannot be null");
            throw new RuntimeException("Entity cannot be null");
        }

        if(entity.getUserId() == null) {
            log.warn("Unknown user.");
            throw new RuntimeException("Unknown user.");
        }
    }
}

컨트롤러 구현

  • HTTP 응답을 반환할 때 DTO를 사용하는 이유

    • 비즈니스 로직 캡슐화

    • 추가적인 정보 함께 반환

  • 컨트롤러는

    • 사용자에게서 TodoDTO를 요청 바디로 넘겨받고

    • 이를 TodoEntity로 변환해 저장

    • TodoService의 create()가 리턴하는 TodoEntity를

    • TodoDTO로 변환해 리턴

package com.soobin.todoWeb.controller;

import com.soobin.todoWeb.dto.ResponseDTO;
import com.soobin.todoWeb.dto.TodoDTO;
import com.soobin.todoWeb.model.TodoEntity;
import com.soobin.todoWeb.service.TodoService;
import org.apache.coyote.Response;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("todo")
public class TodoController {

    @Autowired
    private TodoService service;

    @PostMapping
    public ResponseEntity<?> createTodo(@RequestBody TodoDTO dto) {
        try {
            String temporaryUserId = "temp-user";

            TodoEntity entity = TodoDTO.toEntity(dto);

            entity.setId(null);
						
						//임시 사용자 아이디 설정
            entity.setUserId(temporaryUserId);

            List<TodoEntity> entities = service.create(entity);

            List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
            
						ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();
            
						return ResponseEntity.ok().body(response);
        } catch (Exception e) {
            String error = e.getMessage();
            ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().error(error).build();
            return ResponseEntity.badRequest().body(response);
        }
    }
}

package com.soobin.todoWeb.dto;

import com.soobin.todoWeb.model.TodoEntity;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@NoArgsConstructor
@AllArgsConstructor
@Data
public class TodoDTO {
    private String id;
    private String title;
    private boolean done;
    
    public TodoDTO(final TodoEntity entity) {
        this.id = entity.getId();
        this.title = entity.getTitle();
        this.done = entity.isDone();
    }

    public static TodoEntity toEntity(final TodoDTO dto) {
        return TodoEntity.builder()
                .id(dto.getId())
                .title(dto.getTitle())
                .done(dto.isDone())
                .build();
    }
}

2.3.2 Retrieve Todo

서비스 구현

package com.soobin.todoWeb.service;

import com.soobin.todoWeb.dto.TodoDTO;
import com.soobin.todoWeb.model.TodoEntity;
import com.soobin.todoWeb.persistence.TodoRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

@Slf4j
@Service
public class TodoService {

    @Autowired
    private TodoRepository repository;

    public List<TodoEntity> findByUserId(final String userId) {
        return repository.findByUserId(userId);
    }
}

컨트롤러 구현

package com.soobin.todoWeb.controller;

import com.soobin.todoWeb.dto.ResponseDTO;
import com.soobin.todoWeb.dto.TodoDTO;
import com.soobin.todoWeb.model.TodoEntity;
import com.soobin.todoWeb.service.TodoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("todo")
public class TodoController {

    @Autowired
    private TodoService service;

    @GetMapping("/findById")
    public ResponseEntity<?> findById() {
        String temporaryUserId = "temp-user";

        List<TodoEntity> entities = service.findByUserId(temporaryUserId);

        List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
        
				ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();
        
				return ResponseEntity.ok().body(response);
    }
}

2.3.3 Update Todo

서비스 구현

package com.soobin.todoWeb.service;

import com.soobin.todoWeb.dto.TodoDTO;
import com.soobin.todoWeb.model.TodoEntity;
import com.soobin.todoWeb.persistence.TodoRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

@Slf4j
@Service
public class TodoService {

    @Autowired
    private TodoRepository repository;

    public List<TodoEntity> update(final TodoEntity entity) {
        validate(entity);

        final Optional<TodoEntity> original = repository.findById(entity.getId());
 
        original.ifPresent(todo -> {
            todo.setTitle(entity.getTitle());
            todo.setDone(entity.isDone());

            repository.save(todo);
        });

        return findByUserId(entity.getUserId());
    }

    private void validate(final TodoEntity entity) {
        if(entity == null) {
            log.warn("Entity cannot be null");
            throw new RuntimeException("Entity cannot be null");
        }

        if(entity.getUserId() == null) {
            log.warn("Unknown user.");
            throw new RuntimeException("Unknown user.");
        }
    }
}

컨트롤러 구현

package com.soobin.todoWeb.controller;

import com.soobin.todoWeb.dto.ResponseDTO;
import com.soobin.todoWeb.dto.TodoDTO;
import com.soobin.todoWeb.model.TodoEntity;
import com.soobin.todoWeb.service.TodoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("todo")
public class TodoController {

    @Autowired
    private TodoService service;

    @PutMapping
    public ResponseEntity<?> updateTodo(@RequestBody TodoDTO dto) {
        String temporaryUserId = "temp-user";

        TodoEntity entity = TodoDTO.toEntity(dto);
        entity.setUserId(temporaryUserId);

        List<TodoEntity> entities = service.update(entity);
        List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
        ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();

        return ResponseEntity.ok().body(response);
    }
}

2.3.4 Delete Todo

서비스 구현

package com.soobin.todoWeb.service;

import com.soobin.todoWeb.dto.TodoDTO;
import com.soobin.todoWeb.model.TodoEntity;
import com.soobin.todoWeb.persistence.TodoRepository;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

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

@Slf4j
@Service
public class TodoService {

    @Autowired
    private TodoRepository repository;

    public List<TodoEntity> deleteById(final TodoEntity entity) {
        validate(entity);

        try {
            repository.delete(entity);
        } catch (Exception e) {
            log.error("error deleting entity ", entity.getId(), e);
            throw new RuntimeException("error deleting entity " + entity.getId());
        }
        return findByUserId(entity.getUserId());
    }

    private void validate(final TodoEntity entity) {
        if(entity == null) {
            log.warn("Entity cannot be null");
            throw new RuntimeException("Entity cannot be null");
        }

        if(entity.getUserId() == null) {
            log.warn("Unknown user.");
            throw new RuntimeException("Unknown user.");
        }
    }
}

컨트롤러

package com.soobin.todoWeb.controller;

import com.soobin.todoWeb.dto.ResponseDTO;
import com.soobin.todoWeb.dto.TodoDTO;
import com.soobin.todoWeb.model.TodoEntity;
import com.soobin.todoWeb.service.TodoService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
import java.util.stream.Collectors;

@RestController
@RequestMapping("todo")
public class TodoController {

    @Autowired
    private TodoService service;

    @DeleteMapping
    public ResponseEntity<?> deleteTodo(@RequestBody TodoDTO dto) {
        try {
            String temporaryId = "temp-user";

            TodoEntity entity = TodoDTO.toEntity(dto);
            entity.setUserId(temporaryId);

            List<TodoEntity> entities = service.deleteById(entity);
            List<TodoDTO> dtos = entities.stream().map(TodoDTO::new).collect(Collectors.toList());
            ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().data(dtos).build();

            return ResponseEntity.ok().body(response);
        } catch (Exception e) {
            String error = e.getMessage();
            ResponseDTO<TodoDTO> response = ResponseDTO.<TodoDTO>builder().error(error).build();
            return ResponseEntity.badRequest().body(response);
        }
    }
}

Last updated