1. 데이터베이스란?
- 데이터베이스를 관리하기 위한 소프트웨어를 DBMS(database management system)라고 한다.
- DBMS는 관리 특징에 따라서 관계형, 객체-관계형, 도큐먼트형, 비관계형 등으로 구분한다.
관계형 DBMS
- relational DBMS를 줄여서 REBMS라고 부른다. 테이블 형태로 이루어진 데이터 저장소
H2, MySQL
- 개발 및 테스트시에는 H2, 실제 서비스를 올릴 때는 MySQL를 사용
- H2는 자바로 작성되어 있는 RDBMS이다. 스프링 부트가 지원하는 인메모리 관계형 데이터베이스이다. 데이터를 다른 공간에 따로 보관하는 것이 아니라 애플리케이션 자체 내부에 데이터를 저장하는 특징이 있다.
SQL문으로 데이터베이스 조작
● 데이터 조회
SELECT name
FROM customers
WHERE id=2
● 데이터 추가
INSERT INTO customers (name, phone_number, age)
VALUES ('박사번', '010-4444-4444', 40);
● 데이터 삭제
DELETE FROM customers WHERE id=5;
● 데이터 수정
UPDATE customers
SET age=11
WHERE name='김일번';
2. ORM이란?
- ORM(object-relational mapping)은 자바와 객체와 데이터베이스를 연결하는 프로그래밍 기법이다.
- ORM이 있다면 데이터베이스의 값을 마치 객체처럼 사용할 수 있다.
- 객체와 데이터베이스를 연결해 자바 언어로만 데이터베이스를 다룰 수 있게 하는 도구를 ORM이라 한다.
ORM 장점
- SQL을 직접 작성하지 않고 사용하는 언어로 데이터베이스에 접근할 수 있다.
- 객체지향적 코드로 작성할 수 있기 때문에 비즈니스 로직에만 집중할 수 있다.
- 데이터베이스 시스템이 추상화되어 있기 때문에 MySQL에서 PostgreSQL로 전환한다고 해도 추가로 드는 작업이 없다. 즉, 데이터베이스 시스템에 대한 종속성이 줄어든다.
- 매핑하는 정보가 명확하기 때문에 ERD에 대한 의존도를 낮출 수 있고 유지보수할 때 유리하다.
ORM 단점
- 프로젝트의 복잡성이 커질수록 난이도도 올라간다.
- 복잡하고 무거운 쿼리는 ORM으로 해결이 불가능한 경우가 있다.
3. JPA와 하이버네이트
- ORM에도 여러 종류가 있는데, 자바에서는 JPA(java persistence API)를 표준으로 사용한다.
- JPA는 자바에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스이다. 인터페이스이므로 실제 사용을 위해서는 ORM 프레임워크를 추가로 선택해야 한다. 대표적으로는 하이버네이트(hibernate)를 많이 사용한다.
- 하이버네이트는 JPA 인터페이스를 구현한 구현체이자 자바용 ORM 프레임워크이다. 내부적으로는 JBDC API를 사용한다.
1) 엔티티 매니저
엔티티(entity)
- 데이터베이스의 테이블과 매핑되는 객체
- 데이터베이스의 테이블과 직접 연결된다는 특징으로 일반 객체와 구분지어 부른다.
엔티티 매니저(entity manager)
- 엔티티를 관리해 데이터베이스와 애플리케이션 사이에서 객체를 생성, 수정, 삭제하는 등의 역할을 한다.
- 이런 엔티티 매니저를 만드는 곳이 엔티티 매니저 팩토리(entity manager factory)이다.
ex) 회원 2명이 동시에 회원 가입을 하려는 경우
- 회원 1의 요청에 대해서 가입 처리를 할 엔티티 매니저를 엔티티 매니저 팩토리가 생성하면 이를 통해 가입 처리해 데이터베이스에 회원 정보를 저장하는 것이다. 회원 2도 마찬가지이다. 그리고 회원 1, 회원 2를 위해 생성된 엔티티 매니저는 필요한 시점에 데이터베이스와 연결한 뒤에 쿼리한다
- 스프링 부트는 내부에서 엔티티 매니저 팩토리를 하나만 생성해서 관리하고 @Persistence Context 또는 @Autowired 애너테이션을 사용해서 엔티티 매니저를 사용한다.
ex) 스프링 부트가 엔티티 매니저를 사용하는 방법
@PersistenceContext
EntityManger em; //프록시 엔티티 매니저. 필요할 때 진짜 엔티티 매니저 호출
- 스프링 부트는 기본적으로 빈을 하나만 생성해서 공유하므로 동시성 문제가 발생할 수 있다. 그래서 실제로는 엔티티 매니저가 아닌 실제 엔티티 매니저와 연결하는 프록시 엔티티 매니저를 사용한다. 필요할 때 데이터베이스 트랜잭션과 관련된 실제 엔티티 매니저를 호출하는 것이다.
2) 영속성 컨텍스트
- 엔티티 매니저는 엔티티를 영속성 컨텍스트에 저장한다는 특징이 있다.
- 영속성 컨텍스트는 JPA의 중요한 특징 중 하나로, 엔티티를 관리하는 가상의 공간이다.
- 영속성 컨텍스트에는 1차 캐시, 쓰기 지연, 변경 감지, 지연 로딩이라는 특징이 있다.
1차캐시
- 영속성 컨텍스트는 내부에 1차 캐시를 가지고 있다. 이때 캐시의 키는 엔티티의 @Id 애너테이션이 달린 기본키 역할을 하는 식별자이며 값은 엔티티이다. 엔티티를 조회하면 1차 캐시에서 데이터를 조회하고 값이 있으면 반환한다. 값이 없으면 데이터베이스에서 조회해 1차캐시에 저장한 다음 반환한다. 이를 통해 캐시된 데이터를 조회할 때는 데이터베이스를 거치지 않아도 되므로 매우 빠르게 데이터를 조회할 수 있다.
쓰기 지연
- 트랜잭션을 커밋하기 전까지는 데이터베이스에 실제로 질의문을 보내지 않고 쿼리를 모았다가 트랜잭션을 커밋하면 모았던 쿼리를 한번에 실행하는 것을 의미한다. 이를 통해 적당한 묶음으로 쿼리를 요청할 수 있어 데이터베이스 시스템의 부담을 줄일 수 있다.
변경 감지
- 트랜잭션을 커밋하면 1차 캐시에 저장되어 있는 엔티티의 값과 현재 엔티티의 값을 비교해서 변경된 값이 있다면 변경 사항을 감지해 변경된 값을 데이터베이스에 자동으로 반환한다. 이를 통해 쓰기 지연과 마찬가지로 적당한 묶음으로 쿼리를 요청할 수 있고, 데이터베이스 시스템의 부담을 줄일 수 있다.
지연 로딩
- 쿼리로 요청한 데이터를 애플리케이션에 바로 로딩하는 것이 아니라 필요할 때 쿼리를 날려 데이터를 조회하는 것을 의미한다.
3) 엔티티의 상태
- 엔티티는 4가지 상태를 가진다.
분리(detached) 상태 | 영속성 컨텍스트가 관리하고 있지 않은 |
관리(managed) 상태 | 영속성 컨텍스트가 관리 |
비영속(transient) 상태 | 영속성 컨텍스트와 전혀 관계가 없음 |
삭제된(removed) 상태 | 삭제된 상태 |
ex)
public class EntityMangerTest {
@Autowired
EntityManger em;
public void example() {
//엔티티 매니저가 엔티티를 관리하지 않는 상태(비영속 상태)
Member member = new Member(1L, "홍길동");
//엔티티가 관리되는 상태
em.persist(member);
//엔티티 객체가 분리된 상태
em.detach(member);
//엔티티 객체가 삭제된 상태
em.remove(member);
}
}
4. 스프링 데이터와 스프링 데이터 JPA
- 스프링 데이터(spring data)는 비즈니스 로직에 더 집중할 수 있게 데이터베이스 사용 기능을 클래스 레벨에서 추상화했다. 스프링 데이터에서 제공하는 인터페이스를 통해서 스프링 데이터를 사용할 수 있다.
- 이 인터페이스는 CRUD를 포함한 여러 메서드가 포함되어 있으며, 알아서 쿼리를 만들어준다. 또한 이외에도 페이징 처리 기능과 메서드 이름으로 자동으로 쿼리를 빌딩하는 기능이 제공되는 등 많은 장점이 있다. 추가로 각 데이터베이스의 특성에 맞춰 기능을 확장해 제공하는 기술도 제공한다.
스프링 데이터 JPA
- 스프링 데이터의 공통적인 기능에서 JPA의 유용한 기술이 추가된 기술이다.
- 스프링 데이터 JPA에서는 스프링 데이터의 인터페이스인 PagingAndSortingRepository를 상속받아 JpaRepository 인터페이스를 만들었으며, JPA를 더 편리하게 사용하는 메서드들을 제공한다.
- 지금까지는 메서드 호출로 엔티티의 상태를 바꿨다.
@PersistenceContext
EntityManger em;
public void join() {
//기존에 엔티티 상태를 바꾸는 방법(메서드를 호출해서 상태 변경)
Member member = new Member(1L,"홍길동");
em.persist(member);
}
- 스프링 데이터 JPA를 사용하면 리포지터리 역할을 하는 인터페이스를 만들어 데이터베이스의 테이블 조회, 수정, 생성, 삭제 같은 작업을 간단히 할 수 있다.
public interface MemberRepository extends JpaRepository<Member,Long>{
}
- JPARepository 인터페이스를 우리가 만든 인터페이스에서 상속받고, 제네릭에는 관리할 <엔티티 이름, 엔티티 기본키 타입>을 입력하면 기본 CRUD 메서드를 사용할 수 있다.
5. 스프링 데이터 JPA에서 제공받는 메서드 사용해보기
- MemberRepositoryTest 파일을 생성해 학습 테스트 진행
- 학습 테스트는 기능 구현을 위한 테스트라기보다는 우리가 사용하는 라이브러리, 프레임워크에서 지원하는 기능을 검증하며 어떻게 동작하는지 파악하는 테스트
1) 조회 메서드
- JPA에서 데이터를 가져올 때는 쿼리를 작성하는 대신 findAll() 메서드를 사용
- 테스트용 데이터 추가
[test->resources]에 마우스 우클릭하여 insert-members.sql 파일 생성
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')
- [test->resources]에 마우스 우클릭하여 application.yml 파일 생성한다. 이 옵션은 src/main/resources 폴더 내에 있는 data.sql 파일을 자동으로 실행하지 않게 하는 옵션
test/application.yml
spring:
sql:
init:
mode:never
- 실제로 데이터를 잘 가져오는지 검증하는 테스트 코드 작성
MemberRepositoryTest.java
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.test.context.jdbc.Sql;
import static org.assertj.core.api.Assertions.assertThat;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
@DataJpaTest
class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@Sql("/insert-members.sql")
@Test
void getAllMembers() {
//when
List<Member> members = memberRepository.findAll();
//then
assertThat(members.size()).isEqualTo(3);
}
}
- @Sql 애너테이션을 사용하면 테스트를 실행하기 전에 SQL 스크립트를 실행시킬 수 있다.
- id로 멤버 찾기
- id가 2인 멤버 찾기
MemberRepository.java
...
@Sql("/insert-members.sql")
@Test
void getMemberById() {
//when
Member member = memberRepository.findById(2L).get();
//then
assertThat(member.getName()).isEqualTo("B");
}
...
쿼리 메서드 사용해보기
- JPA는 메서드 이름으로 쿼리를 작성하는 기능을 제공한다.
- name의 값이 'C'인 멤버를 찾아야 하는 경우
MemberRepository.java
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByName(String name);
}
MemberRepositoryTest.java
...
@Sql("/insert-members.sql")
@Test
void getMemberByName() {
//when
Member member = memberRepository.findByName("C").get();
//then
assertThat(member.getId()).isEqualTo(3);
}
...
- 쿼리 메서드는 JPA가 정해준 메서드 이름 규칙을 따르면 쿼리문을 구현하지 않아도 메서드처럼 사용할 수 있다.
전체 조회 | findAll() 메서드 사용 |
아이디로 조회 | findById() 메서드 사용 |
특정 컬럼으로 조회 | 쿼리 메서드 명명 규칙에 맞게 정의 후 사용 |
추가, 삭제 메서드 사용해보기
- JPA에서는 데이터를 삽입할 때 save() 메서드를 사용
MemberRepositoryTest.java
- given절에 새로운 A 멤버 객체를 준비하고, when 절에 실제로 저장한 뒤에 then 절에서는 1번 아이디에 해당하는 멤버의 이름을 가져온다.
@Test
void saveMember() {
//given
Member member = new Member(1L,"A");
//when
memberRepository.save(member);
//then
assertThat(memberRespository.findById(1L).get().getName()).isEqualTo("A");
}
- 여러 엔티티를 한꺼번에 저장하고 싶다면 saveAll() 메서드를 사용
MemberRepository.java
...
@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);
}
...
- JPA에서는 deleteById()를 사용하면 아이디로 레코드를 삭제할 수 있다.
MemberRepository.java
- 미리 스크립트로 작성한 insert-members.sql을 실행하여 3명의 멤버를 추가하고 deleteById() 메서드를 사용해 2번 멤버를 삭제한 뒤 2번 아이디를 가진 레코드가 있는지 조회, 삭제된 데이터이므로 isEmpty() 결괏값이 true인지 검증
...
@Sql("/insert-members.sql")
@Test
void deleteMemberById() {
//when
memberRepository.deleteById(2L);
//then
assertThat(memberRepository.findById(2L).isEmpty()).isTrue();
}
....
- 모든 데이터를 삭제하고 싶다면 deleteAll() 메서드를 사용
- 이 메서드는 정말 모든 데이터를 삭제하므로 실제 서비스 코드에서는 거의 사용하지 않는다. 대신 테스트간의 격리를 보장하기 위해 사용된다.
- 한 테스트의 실행으로 데이터베이스가 변경되었을 때 다른 테스트가 그 데이터베이스를 사용할 때 영향을 주지 않도록 하기 위함이다.
MemberRepository.java
@Sql("/insert-members.sql")
@Test
void deleteAll() {
//when
memberRepository.deleteAll();
//then
assertThat(memberRepository.findAll().size()).iszero();
}
- 그래서 보통은 @AfterEach 애너테이션을 붙여 cleanUp() 메서드와 같은 형태로 사용한다.
@DataJpaTest
class MemberRepositoryTest {
@Autowired
MemberRepository memberRepository;
@AfterEach
public void cleanUp() {
memberRepository.deleteAll();
}
}
save() | 레코드 추가 |
saveAll() | 한꺼번에 여러 레코드 추가 |
deleteById() | 아이디로 레코드 삭제 |
deleteAll() | 모든 레코드 삭제 |
수정 메서드 사용해보기
- JPA는 트랜잭션 내에서 데이터를 수정해야 한다.
- 따라서 데이터를 수정할 때는 그냥 메서드만 사용하면 안되고 @Transactional 애너테이션을 메서드에 추가해야 한다.
Member.java
- name의 필드값을 바꾸는 changeName 메서드 추가
public void changeName(String name) {
this.name = name;
}
- 만약 이 메서드가 @Transactional 애너테이션이 포함된 메서드에서 호출되면 JPA는 변경 감지(dirty checking) 기능을 통해 엔티티의 필드값이 변경될 때 그 변경사항을 데이터베이스에 자동으로 반영한다.
- 만약 엔티티가 영속 상태일 때 필드값을 변경하고 트랜잭션이 커밋되면 JPA는 변경사항을 데이터베이스에 자동으로 적용
5. 예제 코드 살펴보기
Member.java
@NoArgsConstructor(access= AccessLevel.PROTECTED) //기본 생성자
@AllArgsConstructor
@Getter
@Entity //엔티티로 지정
public class Member {
@Id //id 필드를 기본키로 지정
@GeneratedValue(strategy= GenerationType.IDENTITY)
//기본키를 자동으로 1씩 증가
@Column(name="id", updatable=false)
private Long id; //DB 테이블의 'id' 컬럼과 매칭
@Column(name="name", nullable=false)
private String name; //DB 테이블의 'name' 컬럼과 매칭, name이라는 not null 컬럼과 매핑
public void changeName(String name) {
this.name = name;
}
}
- @Entity 애너테이션은 Member 객체를 JPA가 관리하는 엔티티로 지정한다. 즉, Member 클래스와 실제 데이터베이스의 테이블을 매핑시킨다.
- @Entity의 속성 중에 name을 사용하면 name의 값을 가진 테이블 이름과 매핑되고, 테이블 이름을 지정하지 않으면 클래스 이름과 같은 이름의 테이블과 매핑된다.
- 엔티티는 반드시 기본 생성자가 있어야 하고, 접근 제어자는 public 또는 protected여야 한다.
- GeneratedValue는 기본키의 생성 방식을 결정한다.
- @Column 애너테이션은 데이터베이스의 컬럼과 필드를 매핑해준다.
MemberRepository.java
@Repository
public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByName(String name);
}
- 리포지터리는 엔티티에 있는 데이터들을 조회하거나 저장, 변경, 삭제를 할 때 사용하는 인터페이스로, 스프링 데이터 JPA에서 제공하는 인터페이스인 JpaRepository 클래스를 상속받아 간단히 구현할 수 있다.
- JpaRepository 클래스를 상속받을 때, 엔티티 Member와 엔티티의 기본키 타입 Long을 인수로 넣어준다.
'SpringBoot' 카테고리의 다른 글
[스프링 부트 3 백엔드 개발자 되기] ch 7. 블로그 화면 구성하기 (0) | 2024.07.17 |
---|---|
[스프링 부트 3 백엔드 개발자 되기] ch 6. 블로그 기획하고 API 만들기 (0) | 2024.07.17 |
[스프링 부트 3 백엔드 개발자 되기] ch 4. 스프링 부트 3과 테스트 (0) | 2024.07.11 |
[스프링 부트 3 백엔드 개발자 되기] ch 3. 스프링 부트 3 구조 이해하기 (0) | 2024.07.11 |
[스프링 부트 3 백엔드 개발자 되기] ch 2. 스프링 부트 3 시작하기 (0) | 2024.07.10 |