개발

[React.js, 스프링부트, AWS로 배우는 웹 개발] 2장 - 퍼시스턴스 레이어

jih0ssang 2024. 7. 22. 16:59

관계형 데이터베이스에 쿼리를 날리기 위해 어떻게 하는가?

  1. 데이터베이스 클라이언트 설치 (MySQL일 경우, MySQL Workbench 설치)
  2. 클라이언트를 이용해 데이터베이스에 연결되면 쿼리를 작성

[Create Table 쿼리]

CREATE TABLE IF NOT EXISTS Todo(
 id VARCHAR(100) NOT NULL PRIMARY KEY,
 userId VARCHAR(100) NOT NULL,
 title VARCHAR(100) NOT NULL,
 done boolean DEFAULT false
);

 

[SELECT 쿼리]

SELECT id, title, done
FROM Todo
WHERE id = "ff80808177";

 

테이블 생성, 테이블에 엔트리 추가, 수정, 삭제도 모두 웹 서비스의 일부로 동작해야 한다.

우선 JDBC 드라이버가 있어야 한다.

 

JDBC 드라이버는 자바에서 데이터베이스에 연결할 수 있도록 도와주는 라이브러리이다.

쉽게 말하자면 MySQL 클라이언트 같은 것이다.

 

 

ORM(Object-Relation Mapping)

[데이터베이스 콜 스니펫 (데이터베이스와 상호작용하는 코드의 짧은 예제) ]

String sqlSelectAllTodos = "SELECT * FROM Todo where USER_ID = " + request.getUserId();
String connectionUrl = "jdbc:mysql://mydb:3306/todo";

try {
/* 데이터베이스 연결*/
Connection conn = DriverManager.getConnection(connectionUrl, "username", "password");
/* SQL 쿼리 준비 */
PrepareStatement ps = conn.prepareStatement(sqlSelectAllTodos);
/* 쿼리 실행 */
ResultSet rs = ps.executeQuery();
dfdf
/* 결과를 오브젝트로 파싱 */
while (rs.next()) {
 long id = rs.getString("id");
 String title = rs.getString("title");
 Boolean isDone = rs.getBoolean("done");
 
 tods.add(new Todo(id, title, isDone));
 }
} catch (SQLException e) {
}
  1. JDBC 커넥션인 Connection을 통해 데이터베이스에 연결
  2. PrepareStatement에  sqlSelectAllTodos에 작성된 SQL을 담아 SQL 실행
  3. ResultSet을 통해 SQL 실행 결과를 담아온다.
  4. while문 내부에서 ResultSet을 Todo의 오브젝트로 바꿔준다.

이 일련의 작업을 ORM(Object-Relation Mapping)이라고 한다.

  • SQL 실행
  • SQL 쿼리로 얻어온 결과를 Object로 매핑

 

데이터베이스 테이블을 자바 내에서 사용하려면 엔티티마다 해줘야 한다.

데이터베이스 테이블 하나마다 그에 상응하는, 예를 들어 MySQL의 Todo 테이블과 TodoEntity.java 같은 엔티티 클래스가 존재한다. 또 이런 ORM 작업을 집중적으로 해주는 DAO(Data Access Object) 클래스를 작성해야 한다.

 

 

 

JPA

보통 생성, 검색, 수정, 삭제 같은 기본적인 오퍼레이션들을 엔티티마다 작성해주는데, 이런 반복 작업을 줄이기 위해 Hibernate 같은 ORM 프레임워크가 등장했고 더 나아가 JPA나 스프링 데이터 JPA 같은 도구들이 개발됐다.

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

 

JPA는 반복해서 데이터베이스 쿼리를 보내 ResultSet을 파싱해야 하는 개발자들의 노고를 덜어준다.

JPA는 스펙이다. 스펙은 'JPA의 구현을 위해 이러한 기능을 작성해라' 라는 지침이 되는 문서이다.

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

이 스펙을 구현하는 구현자를 JPA Provider 라고 부르는데, 그중 대표적인 JPA Provider가 Hibernate이다.

 

 

 

 

데이터베이스와 스프링 데이터 JPA 설정

 

[build.gradle에 h2 dependencies 추가]

runtimeOnly 'com.h2database:h2'
  • H2는 In-Memory 데이터베이스로 로컬 환경에서 메모리상에 데이터베이스를 구축해 준다.
  • H2를 사용하면 데이터베이스 서버를 구축할 필요 없어, 초기 개발 시 많이 사용한다.
  • build.gradle에 h2를 depedencies로 설정하면 @SpringBootApplication 어노테이션의 일부로 스프링이 알아서 애플리케이션을 H2 데이터베이스에 연결해 준다.
  • In-Memory 특성상, 애플리케이션 실행 시 테이블이 존재하고 애플리케이션 종료 시 테이블이 함께 소멸된다.

 

[build.gradle에 spring-boot-starter-jpa dependencies 추가]

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
  • 스프링 데이터 JPA를 사용하려면 spring-boot-starter-jpa 라이브러리가 필요하다.

[애플리케이션 실행 로그]

Bootstrapping Spring Data JPA repositories in DEFAULT mode.
Finished Spring Data repository scanning in 74 ms. Found 1 JPA repository interfaces.
...
HHH000412: Hibernate ORM core version 5.4.28.Final
HHH000400: Using dialect: org.hibernate.dialect.H2Dialect
  • JPA가 생성된 사실과 JPA Provider로 Hibernate ORM을 사용한다는 것, 애플리케이션이 H2 데이터베이스를 사용한다는 사실을 로그에서 확인할 수 있다.

 

TodoEntity.java

package com.example.demo.model;

import javax.persistence.Entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
public class TodoEntity {
  private String id;
  private String userId;
  private String title;
  private boolean done;
}
  • 자바 클래스를 엔티티로 지정하려면 @Entity를 추가해야 한다.
  • 엔티티에 이름을 부여하고 싶다면 @Entity("TodoEntity")처럼 매개변수를 넣을 수도 있다.

[테이블 이름 지정]

package com.example.demo.model;

import javax.persistence.Entity;
import javax.persistence.Table;


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

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@Table(name = "Todo")
public class TodoEntity {
  private String id;
  private String userId;
  private String title;
  private boolean done;
}
  • 테이블 이름을 지정하려면 @Table(name = "Todo") 어노테이션을 추가한다.
  • 이 엔티티는 데이터베이스의 Todo 테이블에 매핑된다는 뜻이다.
  • 만약 @Table을 추가하지 않거나 추가해도 name을 명시하지 않는다면 @Entity의 이름을 테이블 이름으로 간주한다.
  • @Entity의 이름을 지정하지 않는 경우 클래스의 이름을 테이블의 이름으로 간주한다.

 

[테이블의 기본 키 지정]

package com.example.demo.model;

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


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

@Builder
@NoArgsConstructor
@AllArgsConstructor
@Data
@Entity
@Table(name = "Todo")
public class TodoEntity {
  @Id
  private String id;
  private String userId;
  private String title;
  private boolean done;
}
  • @Id는 기본 키가 될 필드에 지정한다. 우리는 id가 기본 키이므로 id 필드 위에 @Id를 추가하였다.
  • id 필드는 오브젝트를 데이터베이스에 저장할 때마다 생성할 수도 있지만 @GeneratedValue 어노테이션을 이용해 자동으로 생성할 수 있다.

 

[필드 자동 생성 - id ]

package com.example.demo.model;

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


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

@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;
}
  • @GeneratedValue
    • ID 자동 생성
    • 매개변수 generator: @GenericGenerator을 통해 지정. 어떤 방식으로 ID 생성할지 지정 
  • @GenericGenerator
    • Hibernate가 제공하는 기본 generator가 아닌 나만의 generator를 사용하고 싶을 경우 이용
    • 이름과 생성 방식 지정 

 

[TodoRepository.java]

package com.example.demo.persistence;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Respository;
import com.example.demo.model.TodoEntity;

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

}
  • JpaRepository는 인터페이스다. 이 인터페이스를 사용하려면 새 인터페이스를 작성해 JpaRepository를 확장해야 한다.
  • JpaRepository<T, ID>
    • T: 테이블에 매핑될 엔티티 클래스
    • ID: 엔티티의 기본 키 타입
    • 우리의 경우 <TodoEntity, String> 을 넣어주었다.

 

@Repository

package com.example.demo.model;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import com.example.demo.model.TodoEntity;
import com.example.demo.persistence.TodoRepository;

@Service
public class TodoService {

 @Autowired
 private TodoRepository repository;

 public String testService() {
 // TodoEntity 생성
 TodoEntity entity = TodoEntity.builder().title("My first todo item").build();
 // TodoEntity 저장
 repository.save(entity);
 // TodoEntity 검색
 TodoEntity saveEntity = repository.findById(entity.getId()).get();
 return savedEntity.getTitle();
 }
}
  1. repository.save(entity)
    • TodoRepository는 `JPARepository`를 상속받음
    • `JPARepository`는 기본적인 CRUD 메서드 제공  (ex_ save() )
    • 실제 구현은 Spring Data JPA 가 제공
  2. Spring Data JPA(Hibernate)는 엔티티를 영속성 컨텍스트에 추가
    • 영속성 컨텍스트는 메모리 내 엔티티 객체들을 관리하며 데이터베이스와 동기화할 준비 함
  3. 데이터베이스에 해당 엔티티 저장
  4. 저장된 엔티티의 ID 값 설정 및 상태 업데이트

 

[리포지토리 테스트 결과]

{
   "error": null,
   "data": [
       "My first todo item"
   ]
}