[백엔드 첫 걸음] Spring Boot에서의 Jpa (스프링)

khyojun·2022년 8월 9일
2
post-thumbnail

이전까지는 여러 테스트 코드들을 사용하고 Lombok을 사용하여 코드들을 리팩토링하여보는 시간을 가졌었는데 이번에는 스프링부트에서는 Jpa를 어떻게 사용하는지 실습들을 통해서 한 번 알아보자.


Jpa?

Java Persistence API의 약자
자바에서 제공 하는 인터페이스로 ORM 기술에 대한 명세서
관계형 데이터베이스를 사용하는 방식을 나타내고 있다.
ORM이므로 자바 클래스와 DB table을 Mapping 합니다.

이렇게 정의가 되어지는데 간단하게 말하자면은 관계형 DB를 다루는데 어떻게 하면 객체지향 프로그래밍으로 관계형 DB를 사용할 수 있을까?를 고민하다가 나온 해결책이 Jpa라는 것이다.

❗ MyBatis?

이렇게 Jpa를 공부하다보면 Mybatis라는 것을 보게 되는데

객체 지향 언어인 자바의 관계형 데이터베이스 프로그래밍을 좀 더 쉽게 할 수 있게 도와 주는 개발 프레임 워크로서 JDBC를 통해 데이터베이스에 엑세스하는 작업을 캡슐화하고 일반 SQL 쿼리, 저장 프로 시저 및 고급 매핑을 지원하며 모든 JDBC 코드 및 매개 변수의 중복작업을 제거 합니다. Mybatis에서는 프로그램에 있는 SQL쿼리들을 한 구성파일에 구성하여 프로그램 코드와 SQL을 분리할 수 있는 장점을 가지고 있습니다.

이런 설명을 보게 되면은 앞서 말했던 Jpa와 역할이 거의 똑같은것 아닌가?라는 생각이 들지만 엄연히 말해서는 Jpa= ORM(Object Relational Mapping)이고 Mybatis는 SQL Mapper이다. 그래서 어떻게 말하다보면 비교대상이 아닐 수도 있다. ORM은 객체를 매핑을 하고 SQL Mapper는 쿼리를 매핑을하게 된다.

최근 여러 SI회사들에서는 아직 Spring + Mybatis를 사용하는 곳이 많다고 하는데 여러 이름이 있는 자사 서비스를 운영하는 곳에서는 Spring + Jpa를 전사 표준으로 사용하면서 사용을 하고 있다고 한다.

객체를 관계형에서?

객체를 관계형 DB에서 다루는것이 상당히 중요하다고 하는 현대시대인데 왜 그렇게 중요할까?
알다시피 거의 모든 웹 애플리케이션에서는 관계형 DB가 들어가지 않을 수가 없다. 그렇기에 객체를 관계형 DB에서 관리하는게 너무나도 중요하다고 한다.

실상은...

여러가지 커뮤니티나 다른 곳에서의 글들을 보다보면은

"이게 생각보다 회사에 여러분들이 들어가시게 되면은 여러분들이 원하는 java 코드보다 실제로는 SQL 쿼리문만 진짜 한 100중 60-80정도는 수정하고 있을 수 있습니다."

라는 말들이 한 두개가 아니라 엄청나게 많다. 그만큼 쿼리문을 반복적으로 적어야 하는 문제가 생각보다 되게 많다는 것인데 그것을 하지 않기 위하여서 어찌보면 Jpa를 사용하면서 반복적인 쿼리문을 작성하는 것을 줄여나가는것이 많다.(이전 실습에서 JDBC사용한것만 봐도 ㅎㄷㄷ.....)

Spring Data Jpa

Jpa는 인터페이스로서 자바 표준 명세서이다. 인터페이스인 Jpa를 사용하기 위하여서 구현체가 필요한데 대표적인 구현체로는 Hibernate, Eclipse Link 등이 있다.

구현체들을 좀 더 쉽게 사용하기 위하여서 우리는 Spring Data Jpa모듈을 이용하여 Jpa 기술을 다루기로 하였다.

  • Jpa <- Hibernate <- Spring Data Jpa

어찌보면 Hibernate를 그냥 사용하면 안되나요? 할 수도 있는데 Spring Data Jpa를 사용하는 이유는 다음과 같은 큰 두 가지의 이유가 있다고 한다.

  • 구현체 교체의 용이성
  • 저장체 교체의 용이성
    쉽게 말하면은 Hibernate 외에 다른 구현체로 쉽게 교체하기 위함이다. 어떤 경우일까?

예를 들면 실제로 Java의 Redis 클라이언트가 Jedis에서 Lettuce로 대세가 넘어가게 된 경우가 있었다고 한다. 그럴때 Spring Data Redis를 사용한 분들은 아주 쉽게 교체를 하게 되고...

저장소의 경우도 마찬가지이다. Spring Data Jpa에서 MongoDB로 교체가 필요하게 된다면 Spring Data Jpa에서 Spring Data MongoDB로 의존성만 교체를 하여주면 된다는 편의성이 있다.

근데 이게 왜 가능?

Spring Data의 하위 프로젝트들은 기본적인 CRUD 인터페이스들이 같기 때문에 이것이 가능하다고 볼 수 있다.
save(), findAll(), findOne() 등을 인터페이스로 갖고 있기 때문에 기본적인 기능은 딱히 변경할 것이 별로 없다고 할 수 있다.

실무에서 JPA

근데 왜 아까 말했던 SI에서는 많이 사용을 하지 않을까? 많은 이유로는 높은 러닝 커브를 이야기를 한다고 한다. JPA를 잘 사용하기 위하여서는 객체지향 프로그래밍과 관계형 데이터베이스를 둘 다 이해를 해야 한다.

속도는?

속도는 의외로 이미 해결책들이 준비되어있는 상태여서 잘 활용만 하면 네이티브 쿼리만큼의 퍼포먼스를 낼 수 있다.


실습(게시판 기능들 구현)

이제 게시판을 하나씩 실습을 통하여서 만들어보자.

그러기 위하여선 뭐부터 해야할까?

Spring Data Jpa 적용하기

dependencies {
    testImplementation 'org.junit.jupiter:junit-jupiter-api:5.8.1'
    testRuntimeOnly 'org.junit.jupiter:junit-jupiter-engine:5.8.1'
    implementation('org.springframework.boot:spring-boot-starter-web')
    implementation('org.projectlombok:lombok')
    implementation('org.springframework.boot:spring-boot-starter-data-jpa')
    implementation('com.h2database:h2')
    testImplementation('org.projectlombok:lombok')
    testImplementation('org.springframework.boot:spring-boot-starter-test')
    annotationProcessor('org.projectlombok:lombok')
    testAnnotationProcessor('org.projectlombok:lombok')
}

implementation에 보게 되면은 org.springframework.boot:spring-boot-starter-data-jpa, com.h2database:h2를 추가하였다.

spring-boot-starter-data-jpa

  • 스프링 부트용 Spring Data Jpa 추상화 라이브러리입니다.
  • 스프링 부트 버전에 맞춰 자동으로 JPA관련 라이브러리들의 버전을 관리해 줍니다.
    h2
  • 인메모리 관계형 데이터베이스입니다.
  • 별도의 설치가 필요 없이 프로젝트 의존성만으로 관리할 수 있습니다.
  • 메모리에서 실행되기 때문에 애플리케이션을 재시작할 때마다 초기화된다는 점을 이용하여 테스트 용도로 많이 사용이 되어진다.

이후 Domain 패키지에 Post 클래스 구현

📂java/com/khyojun/admin/springboot/domain/posts/Posts.java

package com.khyojun.admin.springboot.domain.posts;

import lombok.Builder;
import lombok.Generated;
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;
    }
}

이 코드를 보게 되면 상당히 어노테이션들이 많은 것을 알 수가 있는데 하나씩 보도록 하자.

  1. @Entity
    • 테이블과 링크가 될 클래스임을 나타낸다. - DB와 직접적으로 연결되있는 클래스라고 생각
    • 기본값으로 클래스의 카멜케이서 이름을 언더스코어 네이밍(_)으로 테이블 이름을 매칭한다.
    • ex) SalesManager.java -> sales_manger table
  2. @Id
    • 해당 테이블의 PK 필드를 나타낸다.
  3. @GeneratedValue
    • PK의 생성 규칙을 나타낸다.
    • 스프링 부트 2.0부터는 GeneratedType.IDENTITY 옵션을 추가해야만 auto_increment가 된다.
  4. @Column
    • 테이블의 칼럼을 나타내는데 실질적으로 작성하지 않아도 된다.
    • 사용이유로는 기본값 이외로 추가로 변경이 필요한 옵션이 있으면 사용
    • 위의 경우에서 length=500의 경우에는 사이즈를 500으로 늘리는 경우 (기본적으로는 VARCHAR(255)가 Default), 그리고 타입을 TEXT로 바꾸는 경우
  5. @NoArgsConstructor:
    • 기본 생성자 자동 추가
    • public Posts(){}와 같은 효과
  6. @Getter
    • 클래스 내 모든 필드의 Getter 메소드를 자동생성
  7. @Builder
    • 해당 클래스의 빌더 패턴 클래스를 생성
    • 생성자 상단에 선언 시 생성자에 포함된 필드만 빌더에 포함

Setter는 어디갔나요?

이것의 경우는 생각을 해보게 된다면은 getter/setter는 거의 짝과 같이 항상 같이 있게 되는데 이번과 같은 경우에서는 같이 있게 되면은 인스턴스 값들이 언제 어디서 변해야 하는지를 명확하게 구분하기가 어렵다고 한다. 차후에 이제 기능들을 변경할때 참말로 복잡해진다고 한다.

그래서 Entitiy클래스에서는 절대로 절대로!!!! Setter 메소드를 만들지 않는다고 한다.

🏸 잘못된 예시

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에 삽입을 하게 되지만 위 코드에서는 @Builder를 사용하여 빌더 클래스를 사용한다. 생성자의 경우에는 지금 채워야 할 필드가 무엇인지 명확히 지정할 수 없지만 @Builder는 그것을 가능하게 한다.

PostRepository

📂java/com/khyojun/admin/springboot/domain/posts/PostsRepository.java

package com.khyojun.admin.springboot.domain.posts;

import org.springframework.data.jpa.repository.JpaRepository;

public interface PostRepository extends JpaRepository<Posts, Long> {

}

Jpa에서는 Repository라고 부르며 인터페이스로 생성을하게 되는데 JpaRepository<엔티티 클래스, PK타입>을 상속시키면 CRUD 메소드가 자동으로 생성이 된다. @Repository를 추가할 필요도 없다. 주의할 점이 Entity클래스와 Entity Repository는 함께 위치해야한다. Entity 클래스와 기본 Repository는 함께 움직여야해서 도메인 패키지에서 함께 관리를 해야한다.

그럼 이제 테스트로...

📂test/java/com/khyojun/admin/springboot/domain/posts/PostsRepositoryTest.java

package com.khyojun.admin.springboot.domain.posts;

import org.assertj.core.api.Assertions;
import org.junit.After;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;

import java.util.List;

@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {

    @Autowired
    PostRepository postRepository;

    @After
    public void cleanup(){
        postRepository.deleteAll();

    }

    @Test
    public void 게시글저장_불러오기(){
        //given 테스트준비과정
        String title="테스트 게시글";
        String content="테스트 본문"; postRepository.save(Posts.builder().title(title).content(content).author("jojoldu@gmail.com").
                build());
        //when 구체화하고자 하는 행동
        List<Posts> postsList = postRepository.findAll();
        //then 행동에 대한 기댓값이 성립하는지?
        Posts posts= postsList.get(0);
        Assertions.assertThat(posts.getTitle()).isEqualTo(title);
        Assertions.assertThat(posts.getContent()).isEqualTo(content);
    }
}
  1. @After
    • AfterEach와 같이 테스트가 끝날때마다 이 어노테이션이 붙어있는 메소드로 향하게 된다.
    • 이 메소드 안의 기능은 h2데이터베이스에 테스트가 끝난 후 데이터가 남아있지 않도록 비워주는 기능을 수행하게 된다.
  2. save
    • 테이블 posts에 insert/update 쿼리를 실행한다.
    • id 값이 있다면 update, 없으면 insert를 실행하게 된다.
  3. postRepository.findAll
    • 테이블 posts에 있는 모든 데이터를 조회하게 된다.

결론:

Jpa 관련 익숙해지기

profile
코드를 씹고 뜯고 맛보고 즐기는 것을 지향하는 개발자가 되고 싶습니다

0개의 댓글