스트링부트에서 JPA로 데이터베이스 다루기(1)
JPA란?
JPA란 java persistence API 로 자바 진영에서 ORM(Object-Relational Mapping) 기술 표준으로 사용되는 인터페이스의 모음입니다. 실제적으로 구현된것이 아니라 구현된 클래스와 매핑을 해주기 위해 사용되는 프레임워크 입니다.
그러면 ORM은 무엇인가? ORM은 우리가 일반 적으로 알고 있는 애플리케이션 Class와 RDB(Relational DataBase)의 테이블을 매핑(연결)한다는 뜻이며, 기술적으로는 어플리케이션의 객체를 RDB 테이블에 자동으로 영속화 해주는 것이라고 생각 하면 됩니다.
과거 스프링에서는 MyBatis와 같은 SQL 매퍼를 사용해서 데이터베이스의 쿼리를 작성했습니다. 그러다 보니 실제로 개발 하는 시간보다 SQL을 다루는 시간이 더 많았습니다. 그렇기 때문에 해결책으로 객체지향 프로그래밍을 관계형 데이터베이스에서 사용하는 JPA를 사용하게 됩니다.
즉, 개발자는 객체지향적으로 프로그래밍을 하고 JPA가 이를 관계형 데이터베이스에 맞게 SQL을 대신 생성해서 사용할수 있도록 해줍니다.
프로젝트에 Spring Data Jpa를 추가해보겠습니다.
build.gradle에 다음과 같이 의존성을 추가 합니다.
build.gradle
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'com.h2database:h2'
'org.springframework.boot:spring-boot-starter-data-jpa'
스프링 부트 버전에 맞춰 자동으로 JPA관련 라이브러리들의 버전을 관리해줍니다.
'com.h2database:h2'
인메모리 관계형 데이터베이스 입니다. 별도의 설치가 필요 없이 프로젝트 의존성만으로 관리할 수 있습니다.
메모리에서 실행되기 때문에 애플리케이션을 재시작할 때마다 초기화된다는 점을 이용하여 테스트 용도로 많이 사용됩니다. Jpa의 테스트, 로컬환경에서의 구동에서 사용할 예정입니다.
JPA 를 알아보기 위해 아래와 같은 클래스를 만들어 줍니다.
Posts.class
import jakarta.persistence.*;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
@Getter // lombok
@NoArgsConstructor // lombok
@Entity // JPA
public class Posts { // 실제 DB의 테이블과 매칭될 클래스. (Entity 클래스). DB 데이터에 작업할 경우 실제 쿼리를 날리기보다 이 Entity 클래스의 수정을 통해 작업합니다.
@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_manager table
@Id
해당 테이블의 PK (Prime key) 필드를 나타냅니다.
@GeneratedValue
PK의 생성 규칙을 나타냅니다.
스프링 부트 2.0에서는 Generation Type.IDENTITY 옵션을 추가해야만 auto_increment가 됩니다.
@Column
테이블의 칼럼을 나타내며 굳이 선언 하지 않아도 해당 클래스의 필드는 모두 칼럼이 됩니다.
사용하는 이유는 기본값 외에 추가로 변경이 필요한 옵션이 있으면 사용합니다.
@NoArgsConstructor
기본 생성자 자동 추가
public Posts() {} 와 같은 효과
@Getter
클래스 내 모든 필드의 Getter 메소드를 자동 생성
@Builder
해당 클래스의 빌더 패턴 클래스를 생성
생성자 상단에 선언시 생성자에 포함된 필드만 빌더에 포함
서비스 초기 구축 단계 에선 테이블 설계 (여기선 Entity 설계)가 빈번하게 변경되는데, 이때 롬복의 어노테이션들은 코드변경량을 최소화시켜 주기 때문에 적극적으로 사용합니다.
Setter 메소드 사용 안하는 이유
이 Posts 클래스에는 한가지 특이점이 있습니다. 바로 Setter 메소드가 없다는 점입니다. 자바빈 규약을 생각하면서 getter/setter를 무작정 생성하는 경우가 있습니다. 이렇게 되면 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확하게 구분할 수가 없어, 차후 기능 변경시 정말 복잡해집니다.
그래서 Entity 클래스에서는 절대 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에 삽입 하는것 이며, 값 변경이 필요한 경우 해당 이벤트에 맞는 public 메소드를 호출하여 변경하는 것을 전제로 합니다.
여기서는 생성자 대신에 @Builder를 통해 제공되는 빌더 클래스를 사용합니다. 생성자나 빌더나 생성 시점에 값을 채워주는 역할은 똑같습니다. 다만 생성자의 경우 지금 채워야 할 필드가 무엇인지 명확히 지정할 수 없습니다.
다음으로는 Posts 클래스로 Database를 접근하게 해줄 JpaRepository 를 생성합니다.
PostsRepository.class
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostsRepository extends JpaRepository<Posts, Long> {}
보통 ibatis나 MyBatis 등에서 Dao라고 불리는 DB Layer 접근자 입니다. JPA에선 Repository라고 부르며 인터페이스로 생성합니다. 단순히 인터페이스를 생성 후, JpaRepository<Entity 클래스, PK 타입> 를 상속하면 기본적인 CRUD 메소드가 자동으로 생성됩니다.
@Repository를 추가할 필요도 없습니다. 여기서 주의할 점은 Entity 클래스와 기본 Entity Repository 는 저장 위치가 같아야 한다는 점입니다. 둘은 아주 밀접한 관계이고 Entity 클래스는 기본 Repository 없이는 제대로 역할을 할 수가 없습니다.
나중에 프로젝트가 규모가 커져 도메인별로 프로젝트를 분리해야 한다면 이때 Entity클래스와 기본 Repository는 함께 움직여야 하므로 아래와 같이 도메인 패키지에서 함께 관리합니다.
이제 테스트 코드로 기능을 검증해 보겠습니다.
test 디렉토리에 domain.posts 패키지를 생성하고, 테스트 클래스는 PostsRepositoryTest 란 이름으로 생성합니다.
PostsRepositoryTest.class
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;
import static org.assertj.core.api.Assertions.assertThat;
@RunWith(SpringRunner.class)
@SpringBootTest
public class PostsRepositoryTest {
@Autowired
PostsRepository postsRepository;
@After
public void cleanup() {
postsRepository.deleteAll(); // JPA 인터페이스가 deleteAll()를 자동 구현
}
@Test
public void 게시글저장_불러오기() {
//given
String title = "테스트 게시글";
String content = "테스트 본문";
postsRepository.save(Posts.builder() // JPA 인터페이스가 save()를 자동 구현. 테이블 posts에 insert/update 쿼리를 실행
.title(title)
.content(content)
.author("jdh@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);
}
}
@After
Junit에서 단위 테스트가 끝날때 마다 수행되는 메소드를 지정
보통은 배포전 전체 테스트를 수행할 때 테스트간 데이터 침범을 막기 위해 사용합니다.
여러 테스트가 동시에 수행되면 테스트용 데이터베이스인 H2에 데이터가 그대로 남아 있어 다음 테스트 실행시 테스트가 실패 할수도 있습니다.
postsRepository.save
테이블 posts에 insert/update 쿼리를 실행합니다.
id 값이 있다면 update, 없다면 insert 쿼리가 실행됩니다.
postsRepository.findAll
테이블 posts에 있는 모든 데이터를 조회해오는 메소드입니다.
별다른 설정 없이 @SpringBootTest 를 사용할 경우 H2 데이터베이스를 자동으로 실행해 줍니다. 이 테스트 역시 실행할 경우 H2가 자동으로 실행됩니다. 테스트 코드를 실행하면 테스트가 통과된 것을 확인할수 있습니다.
여기서 실제로 실행된 쿼리를 확인하는 방법은 없을까요? 스프링부트에서는 application.properties, application.yml 등의 파일로 한줄의 코드로 설정할 수 있도록 지원하고 권장하니 이를 사용합니다.
resources 디렉토리 아래에 application.properties 파일에 아래의 코드를 한줄 추가해줍니다.
spring.jpa.show_sql=true
그러면 다음과 같이 콘솔에서 쿼리 로그를 확인 할수 있습니다.
참고자료 : 이동욱님의 '스프링 부트와 AWS로 혼자 구현하는 웹서비스' 서적