Chapter 03 스프링 부트에서 JPA로 데이터베이스 다뤄보자

LeeKyoungChang·2022년 5월 16일
0
post-thumbnail

스프링 부트와 AWS로 혼자 구현하는 웹 서비스 를 공부하고 정리한 내용입니다.

 

📚 1. JPA

JPA : 자바 표준 ORM
ORM : 객체를 매핑하는 것
SQL Mapper : 쿼리를 매핑하는 것

📖 A. JPA 도입 전

현업 프로젝트 대부분이 애플리케이션 코드보다 SQL로 가득하게 된다.
SQL로만 가능하니 각 테이블마다 기본적인 CRUD SQL을 매번 생성해야 한다.

패러다임 불일치 문제

  • 관계형 데이터베이스는 어떻게 데이터를 저장할지 초점에 맞춰진 기술
  • 객체지향 프로그래밍 언어는 메시지를 기반으로 기능과 속성을 한 곳에서 관리하는 기술

➡️ 관계형 데이터베이스와 객체지향 프로그래밍 언어의 패러다임이 서로 다른데, 객체를 데이터베이스에 저장하려고 하니 여러 문제가 발생한다. 이를 패러다임 불일치라고 한다.

 

User, Group 부모-자식 관계

User user = findUser();
Group group = user.getGroup();

여기에 데이터베이스가 추가

User user = userDao.findUser();
Group group = groupDao.findGroup(user.getGroupId);
  • User 따로, Group 따로 조회
  • User와 Group이 어떤 관계인지 알 수 없다.
  • 상속, 1:N 등 다양한 객체 모델링을 데이터베이스로 구현할 수 없다.

➡️ 그러다보니, 웹 애플리케이션 개발은 점점 데이터베이스 모델링에만 집중하게 된다.
➡️ 이로 인해, JPA는 이런 문제점들을 해결하기 위해 등장하게 된다.

 

📖 B. JPA 도입, Spring Data JPA

✔️ JPA란?

  • 서로 지향하는 바가 다른 2개 영역(객체지향 프로그래밍 언어와 관계형 데이터베이스)을 중간에 패러다임 일치를 시켜주기 위한 기술이다.
  • 개발자는 객체지향적으로 프로그래밍을 하고, JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 실행한다.
  • 개발자는 항상 객체지향적으로 코드를 표현할 수 있으니 더는 SQL에 종속적인 개발을 하지 않아도 된다.

객체 중심으로 개발을 하게 되니 생산성 향상은 물론 유지 보수하기가 정멸 편하다!

 

✔️ Spring Data JPA
JPA는 인터페이스로서 자바 표준명세서이다.
인터페이스인 JPA를 사용하기 위해서는 구현체가 필요하다.
대표적으로 Hibernate, Eclipse Link 등이 있다. 하지만 Spring에서 JPA를 사용할 때는 이 구현체들을 직접 다루지는 않는다.

구현체들을 좀 더 쉽게 사용하고자 추상화시킨 Spring Data JPA라는 모듈을 이용하여 JPA 기술을 다룬다.

JPA ← Hibernate ← Spring Data JPA

Hibernate를 쓰는 것과 Spring Data JPA를 쓰는 것 사이에는 큰 차이가 없다.
그럼에도 스프링 진영에서는 Spring Data JPA를 개발했고, 이를 권장하고 있다.

 

✏️ Spring Data JPA가 등장한 이유

  • 구현체 교체의 용이성 : Hibernate 외에 다른 구현체로 쉽게 교체하기 위함
    • Hibernate가 언젠간 수명을 다해서 새로운 JPA 구현체가 대세로 떠오를 때, Spring Data JPA를 쓰는 중이라면 아주 쉽게 교체할 수 있다.
  • 저장소 교체의 용이성 : 관계형 데이터베이스 외에 다른 저장소로 쉽게 교체하기 위함
    • Spring Data의 하위 프로젝트들은 기본적인 CRUD의 인터페이스가 같다.
    • Spring Data JPA, Spring Data Redis, Spring Data MongoDB 등등 Spring Data의 하위 프로젝트들은 save(), findAll(), findOne()등을 인터페이스로 갖고 있다.
    • 그러다 보니, 저장소가 교체되어도 기본적인 기능은 변경할 것이 없다.

➡️ 이와 같은 장점들로 인해 Hibernate를 직접 쓰기보다는 Spring Data 프로젝트를 사용하는 것을 권장한다.

 

📚 2. 실무에서 JPA

🎁 실무에서 JPA를 사용하지 못하는 이유

  • 실무에서 JPA를 사용하지 못하는 가장 큰 이유는 높은 러닝 커브를 이야기한다.
  • JPA를 잘 쓰려면 객체지향 프로그래밍과 관계형 데이터베이스를 둘 다 이해해야 한다.

 

🔑 JPA의 보상

  • CRUD 쿼리를 직접 작성할 필요가 없다.
  • 부모-자식 관계 표현, 1:N 관계 표현, 상태와 행위를 한 곳에서 관리하는 등 객체지향 프로그래밍을 쉽게 할 수 있다.

 

📖 A. JPA, Java 프로젝트

✔ 요구사항 분석
게시판 기능

  • 게시글 조회
  • 게시글 등록
  • 게시글 수정
  • 게시글 삭제

회원 기능

  • 구글/네이버 로그인
  • 로그인한 사용자 글 작성 권한
  • 본인 작성 글에 대한 권한 관리

 

(1) spring-boot-starter-data-jpa

  • 스프링 부트용 Spring Data Jpa 추상화 라이브러리
  • 스프링 부트 버전에 맞춰 자동으로 JPA관련 라이브러리들의 버전을 관리해준다.

(2) H2

  • 인메모리 관계형 데이테베이스이다.
  • 별도의 설치가 필요 없이 프로젝트 의존성만으로 관리할 수 있다.
  • 메모리에서 실행되기 때문에 애플리케이션을 재시작할 때마다 초기화된다는 점을 이용하여 테스트 용도로 많이 사용된다.

 

🔔 domain 패키지

  • 도메인을 담을 패키지
  • 시글, 댓글, 회원, 정산, 결제 등 소프트웨어에 대한 요구사항 혹은 문제 영역
  • dao 패키지와는 조금 결이 다르다.
  • xml에 쿼리를 담고, 클래스는 오로지 쿼리의 결과만 담던 일들이 모두 도메인 클래스에서 해결된다.

 

Posts

package springbootawsbook.springawsbook.domain.posts;  
  
import lombok.Builder;  
import lombok.Getter;  
import lombok.NoArgsConstructor;  
  
import javax.persistence.*;  
  
@Getter  
@NoArgsConstructor  
@Entity  
public class Posts {  
    @Id  
    @GeneratedValue(strategy = GenerationType.IDENTITY)  
    private Long id;  
  
    @Column(length = 500, nullable = false)  
    private String title;  
  
    @Column(columnDefinition = "TEXT", nullable = false)  
    private String content;  
  
    private String author;  
  
    @Builder  
    public Posts(String title, String content, String author) {  
        this.title = title;  
        this.content = content;  
        this.author = author;  
    }  
}
  • 주요 어노테이션을 클래스에 가깝게 둔다.
  • 어노테이션을 정렬하는 기준
    • @Entity : JPA의 어노테이션
    • @Getter, @NoArgsConstructor : 롬복의 어노테이션
      • 롬복 : 코드를 단순화시켜 주지만 필수 어노테이션은 아니다.
    • 주요 어노테이션인 @Entity를 클래스에 가깝게 두고, 롬복 어노테이션을 그 위로 두었다.
    • 이렇게 하면, 이후에 코틀린 등의 새 언어 전환으로 롬복이 더 이상 필요 없을 경우 쉽게 삭제할 수 있다.
  • Posts 클래스 : 실제 DB의 테이블과 매칭된 클래스, 보통 Entity 클래스라고도 한다.

👉 JPA를 사용하면 DB데이터에 작업할 경우 실제 쿼리를 날리기보다는, 이와 같은 Entity 클래스의 수정을 통해 작업을 한다.

 

📝 코드 설명, 어노테이션
@Entity

  • 테이블과 링크될 클래스임을 나타낸다.
  • 기본 값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍(_)으로 테이블 이름을 매칭한다.
  • ex) SalesManager.java ➡ sales_managertable

@Id

  • 해당 테이블의 PK 필드를 나타낸다.

@GeneratedValue

  • PK의 생성 규칙을 나타낸다.
  • 스프링 부트 2.0에서는 GenerationType.IDENTITY 옵션을 추가해야만 auto_increment가 된다.
  • 스프링 부트 2.0 버전과 1.5 버전의 차이는 http://jojoldu.tistory.com/295를 참고하기

@Column

  • 테이블의 칼럼을 나타내며 굳이 선언하지 않더라도 해당 클래스의 필드는 모두 칼럼이 된다.
  • 사용하는 이유 : 기본 값 외에 추가로 변경이 필요한 옵션이 있으면 사용한다.
  • 문자열의 경우 VARCHAR(255)가 기본 값이지만, 사이즈를 500으로 늘리고 싶거나 (ex: title), 타입을 TEXT로 변경하고 싶거나(ex: content) 등의 경우에 사용된다.

 

💡 참고
@Entity의 PK는 Long 타입의 Auto_increment를 추천한다. (MySQL 기준으로 이렇게 하면 bigint 타입이 된다.)
주민등록번호와 같이 비즈니상 유니크 키나, 여러 키를 조합한 복합키로 PK를 잡을 경우 난감한 상황이 발생한다.

  • FK를 맺을 때 다른 테이블에서 복합키 전부를 갖고 있거나, 중간 테이블을 하나 더 둬야 하는 상황이 발생한다.
  • 인덱스에 좋은 영향을 끼치지 못한다.
  • 유니크한 조건이 변경될 경우 PK 전체를 수정해야 하는 일이 발생한다.

➡ 주민등록번호, 복합키 등은 유니크 키로 별도로 추가하는 것을 추천한다!

 

📝 코드 설명, 어노테이션
@NoArgsConstructor

  • 기본 생성자 자동 추가
  • public Posts() {}와 같은 효과

@Getter

  • 클래스 내 모든 필드의 Getter 메소드를 자동생성

@Builder

  • 해당 클래스의 빌더 패턴 클래스를 생성
  • 생성자 상단에 선언시 생성자에 포함된 필드만 빌더에 포함

 

✔ 그런데, Posts 클래스에는 Setter 메서드가 없다?

  • getter/setter를 무작정 생성할 시, 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수가 없다.
  • Entity 클래스에서는 절대 Setter 메소드를 만들지 않는다!!
  • 대신, 해당 필드의 값 변경이 필요하면 명확히 그 목적과 의도를 나타낼 수 있는 메소드를 추가해야한다.

ex) 주문 취소 메소드를 만든다고 가정할 때

public class Order{
	public void setStatus(boolean status){
		this.status = status
	}
}

public void 주문서비스의_취소이벤트(){
	order.setStatus(false);
}

public class Order{
	public void cancelOrder(){
		this.status = false;
	}
}

public void 주문서비스의_취소이벤트(){
	order.cancelOrder();
}

 

✔ Setter가 없는 상황에서는 어떻게 값을 채워 DB에 삽입할까?

  • 생성자를 통해 최종값을 채운 후 DB에 삽입한다.
  • 값 변경이 필요한 경우 해당 이벤트에 맞는 public 메소드를 호출하여 변경하는 것을 전제로 한다.

 

💡 참고
책에서는 생성자 대신에 @Builder를 통해 제공되는 빌더 클래스를 사용한다.
생성자나 빌더나 생성 시점에 값을 채워주는 역할은 똑같다.

 

✔ 생성자 vs 빌더

public Example(String a, String b){
	this.a = a;
	this.b = b;
}
  • 생성자는 개발자가 a와 b의 위치를 변경해도 코드를 실행하기 전까지는 문제를 찾을 수 없다.

 

Example.builder()
	.a(a)
	.b(b)
	.build();
  • 빌더를 사용하게 되면 어느 필드에 어떤 값을 채워야 할지 명확하게 인지할 수 있다.

➡ 앞으로는 모든 예제에서 빌터 패턴을 적극적으로 사용할 것이다.

 

✔ DB 접근, JpaRepository
PostRepository

package springbootawsbook.springawsbook.domain.posts;  
  
import org.springframework.data.jpa.repository.JpaRepository;  
  
public interface PostsRepository extends JpaRepository<Posts, Long> {  
}
  • JPA에서는 Repository를 인터페이스로 생성한다.
  • 인터페이스를 생성 후, JpaRepository<Entity 클래스, PK 타입>를 상속하면 기본적인 CRUD 메소드가 자동으로 생성된다.
  • @Repository를 추가할 필요가 없다.
  • Entity 클래스와 기본 Entity Repository는 함께 위치해야한다.
    • Entity 클래스는 기본 Repository 없이는 제대로 역할을 할 수가 없다.
    • 나중에 규모가 커졌을 경우, Entity 클래스와 기본 Repository는 함께 움직여야 하므로, 도메인 패키지에서 함께 관리한다.

 

📖 B. Spring Data JPA 테스트 코드

PostRepositoryTest

package springbootawsbook.springawsbook.domain.posts;  
  
import org.junit.jupiter.api.AfterEach;  
import org.junit.jupiter.api.Test;  
import org.springframework.beans.factory.annotation.Autowired;  
import org.springframework.boot.test.context.SpringBootTest;  
  
import java.util.List;  
  
import static org.assertj.core.api.Assertions.*;  
  
@SpringBootTest  
class PostsRepositoryTest {  
  
    @Autowired  
    PostsRepository postsRepository;  
  
    @AfterEach  
    public void cleanup() {  
        postsRepository.deleteAll();  
    }  
  
    @Test  
    public void 게시글저장_불러오기() {  
        // given  
        String title = "테스트 게시글";  
        String content = "테스트 본문";  
  
        postsRepository.save(Posts.builder()  
                .title(title)  
                .content(content)  
                .author("lkc263@gmail.com")  
                .build());  
  
        // when  
        List<Posts> postsList = postsRepository.findAll();  
  
        // then  
        Posts posts = postsList.get(0);  
        assertThat(posts.getTitle()).isEqualTo(title);  
        assertThat(posts.getContent()).isEqualTo(content);  
    }  
  
}

📝 코드 설명
@AfterEach

  • Junit에서 단위 테스트가 끝날 때마다 수행되는 메소드를 지정한다.
  • 보통은 배포 전 전체 테스트를 수행할 때 테스트간 데이터 침법을 막기 위해 사용한다.
  • 여러 테스트가 동시에 수행되면 테스트용 데이터베이스인 H2에 데이터가 그대로 남아 있어 다음 테스트 실행 시 테스트가 실패할 수 있다.

postsRepository.save

  • 테이블 posts에 insert/update쿼리를 실행한다.
  • id 값이 있다면 update, 없다면 insert 쿼리가 실행된다.

postRepository.findAll

  • 테이블 posts에 있는 모든 데이터를 조회해오는 메소드

 

💡 참고
JUnit4 ➡ JUnit5

  • @After@AfterEach
  • AfterClass@AfterAll

 

실행 결과

사진1

 

🔑 실제로 실행된 쿼리는 어떤 형태일까?
실행된 쿼리를 로그로 보려면, application.properties, application.yml 등의 파일에서 한 줄의 코드로 설정할 수 있다.
이전에 공부한 내용을 참고하자!

이제 JPA와 H2에 대한 기본적인 기능과 설정을 진행했으니, 본격적으로 API를 만들어보자!

 

profile
"야, (오류 만났어?) 너두 (해결) 할 수 있어"

0개의 댓글