[Spring Boot] 5. 데이터베이스 조작이 편해지는 ORM

김민경·2024년 7월 5일
post-thumbnail

'스프링 부트3 백엔드 개발자 되기' 책을 참고하며 작성 중 입니다.


데이터베이스?

데이터를 매우 효율적으로 보관하고 꺼내볼 수 있는 곳
많은 사람이 안전하게 데이터를 사용하고, 관리할 수 있다는 이점

데이터베이스 관리자, DBMS

데이터베이스를 관리하기 위한 소프트웨어
DBMS ( DataBase Management System)

관계형 DBMS, RDBMS

관계형 모델을 기반으로 한 DBMS, 테이블 형태로 이루어진 데이터 저장소

H2

자바로 작성되어 있는 RDBMS, 스프링 부트가 지원하는 인메모리 관계형 데이터베이스
애플리케이션 자체 내부에 데이터를 저장하기 때문에 휘발성

개발 및 테스트 시에는 H2, 서비스 배포 시에는 MySQL

데이터베이스 용어

테이블 : 데이터를 구성하기 위한 가장 기본적인 단위, 행과 열로 구성
: 테이블 구성 요소 중 하나, 테이블의 가로로 배열된 데이터의 집합
반드시 고유한 식별자인 기본키를 가져야 하며,
레코드 라고 부르기도 한다.
: 테이블의 구성 요소 중 하나, 행에 저장되는 유형의 데이터
각 요소에 대한 속성을 나타내며 무결성 보장
기본키 : 행을 구분할 수 있는 식별자
테이블에서 유일 해야 하며, 수정 불가, NULL값 불가
쿼리 : 데이터베이스에서 데이터 처리를 위해 사용하는 명령문
SQL 이라는 데이터베이스 전용 언어를 사용하여 작성


SQL 문

SELECT

	SELECT <무엇을> FROM <어디에서> WHERE <무슨>
  • 조건이 없으면 WHERE 절은 생략 가능
  • 컬럼을 모두 가져오고 싶다면 * 을 사용
	SELECT * FROM customers;

WHERE의 조건

명령어설명예시
=동일한 값을 가진 행 조회
!= 또는 <>동일하지 않은 행 조회
<,>,<=,>=대소 비교하여 조회
BETWEEN지정된 값의 사이 값을 조회age BETWEEN 10 AND 20
LIKE패턴 매칭을 위해 사용, %를 사용하면 와일드 카드로 사용 가능name LIKE ' 김%'
AND두 조건 모두 참이면 조회
OR두 조건 중 하나라도 참이면 조회
IS NULL, IS NOT NULLNULL 값의 존재 여부 검사name IS NULL

INSERT

INSERT INTO <어디에> VALUES <어떤 값을>

INSERT INTO customers(name, age) VALUES ('A', 30);
  • 보통 테이블을 만들 때 id 값은 AUTO_INCREMENT 속성을 추가한다
    • 새 레코드를 추가할 때 자동으로 1씩 증가

DELETE

DELETE FROM <어디에서> WHERE <어떤 조건으로>

DELETE FROM customers WHERE id=5;

UPDATE

UPDATE <어디에> SET <무슨 칼럼을 어떤 값으로> WHERE <어떤 조건으로>

UPDATE customers SET age=11 WHERE name='A';

주의 WHERE 절을 생략하면 테이블의 모든 레코드가 수정된다


ORM?

Object-Relational Mapping
자바의 객체와 데이터베이스를 연결하는 프로그래밍 기법

객체와 데이터베이스를 연결해 자바 언어로만 데이터베이스를 다룰 수 있게 하는 도구

장점

  1. SQL을 직접 작성하지 않고 사용하는 단어로 데이터베이스에 접근
  2. 객체지향적으로 코드를 작성할 수 있어, 비즈니스 로직에만 집중가능
  3. 데이터베이스 시스템이 추상화되어 있기 때문에 종속성이 줄어듬
  4. 매핑하는 정보가 명확하기 때문에 ERD 의존도를 낮출 수 있고, 유지보수에 유리

단점

  1. 프로젝트의 복잡성이 커질수록 사용 난이도 UP
  2. 복잡하고 무거운 쿼리는 ORM으로 해결이 불가능할 수 있음

JPA와 Hibernate

DBMS도 종류가 있듯이 ORM에도 여러 종류가 존재

자바가 표준으로 사용하는 것은

JPA (Java Persistance API)

자바에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스
이를 실제 사용하기 위해서는 ORM 프레임워크를 추가로 선택해야 한다

그것이 바로

Hibernate

JPA 인터페이스를 구현한 구현체이자 ORM 프레임워크
내부적으로는 JDBC API를 사용

여기서 JDBC API?

Java DataBase Connectivity
DBMS에 상관없이 하나의 문법으로 처리해주는 API

정리

JPA : 자바 객체와 데이터베이스를 연결해 데이터 관리, 
객체 지향 도메인 모델과 데이터베이스의 다리 역할
Hibernate : JPA의 인터페이스 구현, 내부적으로 JDBC API 사용

Entity Manager?

Entity

데이터베이스의 테이블과 매팽되는 객체
일반 객체와 다르지 않지만 데이터베이스의 테이블과 직접 연결된다는 특징에서 구분

Entity Manager

Entity를 관리해 데이터베이스와 애플리케이션 사이에서 객체를 생성, 수정, 삭제하는 역할
이런 Entity Manager을 만드는 곳이 Entity Manager Factory

요청에 대해서 Entity Manager Factory가 Entity Manager을 생성하면 이를 통해 데이터베이스에 연결한 뒤 쿼리

스프링 부트 내에서는 Entity Manager Factory를 하나만 생성해서 관리
@PersistenceContext, @Autowired 애너테이션을 사용하여 Entity Manager를 사용

스프링 부트는 기본적으로 Bean을 하나만 생성하여 공유 -> 동시성 문제
실제로는 Entity Manager가 아닌 Proxy(가짜) Entity Manager을 사용
필요할 때 데이터베이스 트랜잭션과 관련된 실제 Entity Manager을 호출

@PersistenceContext
EntityManager em; //Proxy Entity Manager, 필요할 때 실제 EntityManger 호출

결론적으로 Entity Manager은 Spring Data JPA에서 관리하므로 직접 생성하거나 관리할 필요 X


Persistence Context (영속성 컨텍스트)?

Entity Manager은 Entity를 영속성 컨텍스트에 저장한다는 특징
Persistence Context는 Entity를 관리하는 가상의 공간, JPA의 중요한 특징 중 하나

특징

1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩
  1. 1차 캐시
    영속성 컨텍스트는 내부에 1차 캐시를 가지고 있음
    Key = Entity의 @Id 애너테이션이 달린 기본키 역할을 하는 식별자
    Value = Entity
    캐시된 데이터를 조회할 때는 데이터베이스를 거치치 않아도 되어 매우 빠르게 조회 가능
  1. 쓰기 지연
    트랜잭션을 커밋하기 전까지는 데이터베이스에 실제로 질의문을 보내지 않고 쿼리를 모았다가
    트랜잭션을 커밋하면 모았던 쿼리를 한번에 실행하는 것을 의미
    적당한 묶음으로 쿼리를 요청할 수 있어 데이터베이스 시스템의 부담을 줄일 수 있음
  1. 변경 감지
    트랜잭션을 커밋하면 1차 캐시에 저장되어 있는 Entity의 값과 현재 Entity의 값을 비교하여
    변경된 값이 있으면 감지하여 변경된 값을 데이터베이스에 자동으로 반영
    쓰기 지연과 마찬가지로 적당한 묶음으로 쿼리를 요청할 수 있어 데이터베이스 시스템의 부담을 줄일 수 있음
  1. 지연 로딩 (lazy loading)
    쿼리로 요청한 데이터를 애플리케이션에 바로 로딩하는 것이 아니라
    필요할 때 쿼리를 날려 데이터를 조회하는 것

상태

영속성 컨테스트와의 관계에 따라 Entity가 4가지 상태를 가짐

관리하지 않는 분리(detached), 관리(managed), 전혀 관계가 없는 비영속(transient), 삭제(removed)
@Autowired
EntityManager em;
    
public void example(){
	Member m = new Member(1L, 'A');
    //비영속 상태
    em.persist(m);
    //관리
    em.detach(m);
    //분리
    em.remove(m);
    //삭제, Entity를 영속성 컨텍스트와 데이터베이스에서 삭제
}

해당 Entity를 영속성 컨텍스트와 관계를 지어 관리되게 할 것인지를 선택


Spring Data JPA

Spring Data

Entity의 상태를 직접 관리하고 필요한 시점에 커밋을 해야하는 불편함 ..
영속성 컨텍스트에 관계를 짓고 커밋하고 삭제하고 ..

비즈니스 로직에 더 집중할 수 있게 데이터베이스 사용 기능을 Class 레벨에서 추상화
Spring Data에서 제공하는 인터페이스를 통해서 사용 가능

CRUD를 포함한 여러 메서드가 포함되어 있으며, 알아서 쿼리를 만들어 주고
이외에도 페이징 처리 기능, 메서드 이름과 자동으로 쿼리를 빌딩하는 기능
각 데이터베이스의 특성에 맞춰 기능을 확장해 제공하는 기술도 제공

표준 스펙인 JPA는 스프링에서 구현한 Spring Data JPA를,
몽고디비는 Spring Data MongoDB를 사용

Spring Data JPA?

Spring Data의 공통적인 기능에서 JPA의 유용한 기술이 추가된 기술

Spring Data의 PagingAndSortingRepository Interface를 상속받아 
Spring Data JPA의 JpaRepository Interface 제작

기존

@Autowired
EntityManager em;
    
public void join(){
    //기존에 Entity 상태를 바꾸는 방법
    //메소드를 호출하여 상태 변경
	Member m = new Member(1L, 'A');
    em.persist(m);
}

Spring Data JPA 사용

Repository 역할을 하는 인터페이스를 만들어 데이터베이스의 테이블 조회, 수정, 생성, 삭제 같은 작업을 간단하게 가능

public interface MemberRepository extends JpaRepository<Member,Long>{
}

JpaRepository 인터페이스를 상속받아,
제네릭에는 관리할 <엔티티 이름, 엔티티 기본키의 타입>을 입력하면 기본 CRUD 메서드 사용 가능

Spring Data JPA에서 제공하는 메서드 Test

우리가 사용하는 라이브러리, 프레임워크에서 지원하는 기능을 검증하며 어떻게 동작하는지 파악하는 테스트

조회 메서드

테스트용 데이터 추가

//insert-members.sql
INSERT INTO member (id,name) VALUES (1,'A')
INSERT INTO member (id,name) VALUES (2,'B')
INSERT INTO member (id,name) VALUES (3,'C')

기존에 있던 data.sql 실행 X

spring:
  sql:
    init:
      mode: never
      #data.sql 실행 X    

MemberRepositoryTest

@DataJpaTest
class MemberRepositoryTest {
    @Autowired
    MemberRepository memberRepository;

    /* Member 가져오기 */

    @Sql("/insert-members.sql")
    //테스트 실행 전 sql 스크립트 실행
    @Test
    void getAllMembers(){
        //SELECT * FROM member;
        //when
        List<Member> members = memberRepository.findAll();

        //then
        assertThat(members.size()).isEqualTo(3);
    }

    @Sql("/insert-members.sql")
    @Test
    void getMemberById(){
        //SELECT * FROM member WHERE id = 2;
        //when
        Member member = memberRepository.findById(2L).get();

        //then
        assertThat(member.getName()).isEqualTo("B");
    }

쿼리 메서드 사용

MemberRepository

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
    Optional<Member> findByName(String name);
    //쿼리 메소드

    @Query("select m from Member m where m.name = ?1")
    Optional<Member> findByNameQuery(String name);
}

MemberRepositoryTest

@Sql("/insert-members.sql")
@Test
void getMemberByName(){
    //SELECT * FROM member WHERE name = 'C';
    //when
    Member member = memberRepository.findByName("C").get();

    //then
    assertThat(member.getId()).isEqualTo(3);
}

추가, 삭제 메서드

MemberRepositoryTest

@Test
void saveMember(){
	//INSERT INTO member (id, name) VALUES (1, 'A');
	//given
    Member member = new Member(1L, "A");

    //when
    memberRepository.save(member);

	//then
	assertThat(memberRepository.findById(1L).get().getName()).isEqualTo("A");
  }

@Test
void saveMembers(){
    //given
    List<Member> members = List.of(new Member(2L, "B"),
    new Member(3L, "C"));

    //when
    memberRepository.saveAll(members);

    //then
    assertThat(memberRepository.findAll().size()).isEqualTo(2);
}

    /* Member 삭제하기 */
@Sql("/insert-members.sql")
@Test
void deleteMemberById(){
    //DELETE FROM member WHERE id = 2;
    //when
    memberRepository.deleteById(2L);

    //then
    assertThat(memberRepository.findById(2L)).isEmpty();
    assertThat(memberRepository.findById(2L).isEmpty()).isTrue();
    //둘이 같은 표현
}

@Sql("/insert-members.sql")
@Test
void deleteMembers(){
    //DELETE FROM member;
    //when
    memberRepository.deleteAll();

    //then
    assertThat(memberRepository.findAll()).isEmpty();
    assertThat(memberRepository.findAll().size()).isZero();
    //둘이 같은 표현
}
    //모두 삭제하는 메소드는 테스트 간의 격리를 보장하기 위해 사용
    //즉, 한 테스트의 실행으로 데이터베이스가 변경 된 것이 다른 테스트에 영향을 주지 않도록 하기 위함
    //따라서 보통 아래처럼 @AfterEach로 사용

@AfterEach
public void cleanUp(){
    memberRepository.deleteAll();
}

수정 메서드

@Sql("/insert-members.sql")
@Test
void update(){
    //UPDATE member SET name = 'BC' WHERE id = 2;
    //given
    Member member = memberRepository.findById(2L).get();

    //when
    member.changeName("BC");

    //then
    assertThat(memberRepository.findById(2L).get().getName()).isEqualTo("BC");
}

@Transactional 을 사용 하지 않았는데 수정이 적용된 이유 ?
-> 맨 위에 @DataJpaTest에 포함되어 있음


예제 코드 살펴보기

Member

@NoArgsConstructor(access = AccessLevel.PROTECTED) //접근 제어자가 protected인 기본 생성자 생성
@AllArgsConstructor
@Getter
@Entity(name="member_list") //엔티티 지정
public class Member {
  @Id // id 필드를 기본키로 지정
  @GeneratedValue(strategy = GenerationType.IDENTITY)
  //기본키 자동으로 1씩 증가
  @Column( name = "id", updatable = false)
  private Long id;

  @Column(name = "name", nullable = false)
  //name 이라는 notnull 컬럼과 매핑
  private String name;

  public void changeName(String name){
      this.name = name;
  }
}

@Entity : Member 객체를 JPA가 관리하는 Entity로 지정, 데이터베이스 테이블을 매핑
name - 테이블 이름을 지정(member_list), 없으면 클래스 이름으로 테이블 이름 매핑 (member)
@NoArgsConstructor : 기본 생성자 생성
access - 기본 생성자의 접근 제어자 (protected)
@Id : Long 타입의 id 필드를 테이블의 기본키로 지정
@GeneratedValue : 기본키의 생성 방식 결정
strategy - 자동키 생성 설정 방식 (아래 참고)
@Column : 데이터베이스의 칼럼과 필드 매핑
name - 필드와 매핑할 칼럼 이름 (default : 필드 이름)
nullable - 칼럼의 null 허용 여부 (default : true)
unique - 칼럼의 유일한 값 여부 (default : false)
columnDefinition - 칼럼 정보 설정, default를 줄 수 있음

자동키 생성 설정 방식
AUTO : 선택한 데이터베이스 방언에 따라 방식을 자동으로 선택 (Default)
IDENTITY : 기본키 생성을 데이터베이스에 위임 (AUTO_INCREMENT)
SEQUENCE : 데이터베이스 시퀀스를 사용해서 기본키를 할당하는 방법. 오라클에서 주로 사용
TABLE : 키 생성 테이블 사용

MemberRepository

@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
   Optional<Member> findByName(String name);
   //쿼리 메소드

   @Query("select m from Member m where m.name = ?1")
   Optional<Member> findByNameQuery(String name);
}

Repository는 Entity에 있는 데이터들을 조회, 저장, 변경, 삭제를 할 때 사용하는 Interface로,
Spring Data JPA에서 제공하는 JpaRepository Interface를 상속받아 간단하게 구현
Entity(Member)와 Entity의 기본키 타입(Long)을 인수로 사용

총 정리

  1. ORM은 객체와 데이터베이스를 연결하는 프로그래밍 기법

  2. JPA는 자바에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스

    Entity : Persistence을 가진 객체로, 분리, 관리, 비영속, 삭제 상태를 가짐

    Entity Manager : Entity를 Persistence Context에 저장하여 Entity를 관리하며 조회, 삭제, 수정, 생성

    Entity Manager Factory: Entity Manager을 만드는 곳

    Persistence Context : 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩의 특징

  3. Hibernate는 JPA의 대표적인 구현체, 자바 언어를 위한 ORM 프레임워크

  4. Spring Data JPA는 JPA를 쓰기 편하게 만들어놓은 모듈

profile
뭐든 기록할 수 있도록

0개의 댓글