들어가기 전에
하기 포스팅은 스프링 부트와 AWS로 혼자 구현하는 웹 서비스(이동욱 저)를 공부하여 작성한 내용입니다.
SQL 매퍼를 이용해 데이터베이스의 쿼리를 작성하는 경우가 많습니다. 이 경우, 실제 개발하는 시간보다 SQL을 다루는 시간이 많았습니다. 이러한 문제점을 해결하기 위해 JPA라는 자바 표준 ORM 기술이 생겨났습니다. 이번 포스팅에서는 JPA를 프로젝트에 적용해보도록 하겠습니다.
- Mybatis는 ORM이 아닌, SQL Mapper입니다. ORM은 객체를 매핑하는 것이고, SQL Mapper는 쿼리를 매핑합니다.
JPA
현대의 웹 어플리케이션에서는 관계형 DB는 빠질 수 없는 요소입니다. 그로 인해 객체를 관계형 데이터베이스에서 관리하는 것이 중요합니다. 관계형 데이터베이스가 웹 서비스의 중심이 되면서 모든 코드는 SQL 중심이 되어 어플리케이션 코드보다 SQL 양이 많아지게 되었습니다.
- 이는 관계형 데이터베이스가 SQL만 인식할 수 있어 생겨난 문제로, 기본적인 CRUD SQL을 매번 생성해야 했습니다(단순 반복 작업↑).
SQL 양이 많아진다는 문제도 있지만, 관계형 데이터베이스와 객체지향 프로그래밍 간 지향하는 바가 다른 것도 문제가 되었습니다.
- 관계형 데이터베이스는 어떻게 데이터를 저장할지에 초점 맞춤
- 객체지향 프로그래밍은 메시지를 기반으로 기능과 속성을 한 곳에서 관리하는 기술
예를 들어 객체지향 프로그래밍에서 부모가 되는 객체를 가져오려면 아래와 같이 진행합니다.
User user = findUser();
Group group = user.getGroup();
- 부모 - 자식 간의 관계임을 알 수 있습니다.
반면, 데이터베이스를 통해 가져와야 한다면 아래와 같이 User 따로, Group 따로 데이터베이스에 조회를 해야 합니다.
User user = userDao.findUser();
Group group = groupDao.findGroup(user.getGroupId());
- 이렇게 되면 User와 Group이 어떤 관계인지 알 수 없습니다. 이로 인해 웹 어플리케이션 개발은 점점 데이터베이스 모델링에만 집중하게 됩니다.
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를 쓰는 것을 권장하는 이유는 아래와 같습니다.
- 구현체 교체의 용이성
- Hibernate 외의 다른 구현체로 쉽게 교체하기 위함입니다.
- Spring Data JPA 내부에서 구현체 매핑을 지원해주어 다른 구현체를 사용하려고 할 때에 교체가 용이합니다.
- Hibernate 외의 다른 구현체로 쉽게 교체하기 위함입니다.
- 저장소 교체의 용이성
- 관계형 데이터베이스 외 다른 저장소로 쉽게 교체하기 위함입니다.
- 초기에는 관계형 데이터베이스만을 이용했지만, 점점 트래픽이 많아지면서 MongoDB로 교체가 필요할 때 개발자는 Spring Data JPA에서 Spring Data MongoDB로 의존성만 교체하면 됩니다.
- 이는, Spring Data의 하위 프로젝트들은 기본적인 CRUD의 인터페이스가 같기 때문입니다.
- 즉, Spring Data JPA, Spring Data Redis, Spring Data MongoDB 등의 Spring Data의 하위 프로젝트들은 save(), findAll(), findOne() 등을 인터페이스로 갖고 있습니다.
- 관계형 데이터베이스 외 다른 저장소로 쉽게 교체하기 위함입니다.
실무에서 JPA
실무에서 JPA를 사용하지 못하는 가장 큰 이유는 높은 러닝 커브 때문입니다. JPA를 잘 쓰기 위해서는 객체지향 프로그래밍과 관계형 데이터베이스를 잘 이해해야 합니다. 하지만, JPA를 사용해 얻는 보상이 크기 때문에 제대로 사용할 수 아는 것이 좋습니다.
JPA의 이점
- CRUD 쿼리를 직접 작성할 필요가 없습니다.
- 부모-자식 관계 표현, 1:N 관계 표현, 상태와 행위를 한 곳에서 관리하는 등 객체지향 프로그래밍이 가능합니다.
- JPA에서는 여러 성능 이슈 해결책을 준비해두었기 때문에 네이티브 쿼리만큼의 퍼포먼스를 낼 수 있습니다.
프로젝트에 Spring Data JPA 적용하기
먼저 build.gradle에 아래와 같이 org.springframework.boot:spring-boot-starter-data-jpa와 com.h2database:h2 의존성을 등록합니다.
...
dependencies {
compile('org.springframework.boot:spring-boot-starter-web')
compile('org.projectlombok:lombok')
compile('org.springframework.boot:spring-boot-starter-data-jpa')
compile('com.h2database:h2')
testCompile('org.springframework.boot:spring-boot-starter-test')
}
- spring-boot-starter-data-jpa
- springboot용 Spring Data JPA 추상화 라이브러리입니다.
- springboot 버전에 맞추어 자동으로 JPA 관련 라이브러리들의 버전을 관리해줍니다.
- com.h2database:h2
- 인메모리 관계형 데이터베이스입니다.
- 별도의 설치 없이 프로젝트 의존성만으로 관리할 수 있습니다.
- 메모리에서 실행되기 때문에 어플리케이션 재시작시마다 초기화됩니다.
- 테스트 용도로 많이 사용
의존성 등록이 완료되었다면, JPA 기능을 사용해보도록 하겠습니다. 먼저 domain 패키지를 만들어보겠습니다.
Entity 클래스 생성하기
domain 패키지란?
- 도메인을 담을 패키지입니다.
- 도메인이란, 게시글/댓글/회원/정산/결제 등 소프트웨어에 대한 요구사항 혹은 문제 영역이라고 생각하면 됩니다.
- Mybatis와 같은 쿼리 매퍼의 dao 패키지와는 조금 다릅니다.
- xml에 쿼리를 담고 클래스에서는 쿼리 결과를 담던 일을 모두 도메인 클래스에서 해결합니다.
domain 패키지 안에는 posts 패키지와 Posts 클래스를 생성합니다.
Posts 클래스 코드는 다음과 같습니다.
package com.earth.study.springboot.domain.posts;
import lombok.Builder;
import lombok.Getter;
import lombok.NoArgsConstructor;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@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,@Getter,@NoArgsConstructor
- @Entity는 JPA의 어노테이션이며, @Getter과 @NoArgsConstructor는 lombok 어노테이션입니다.
- 어노테이션 순서를 위 코드와 같이 둔 이유는, 롬복 어노테이션은 필수 어노테이션은 아니기 때문에 쉽게 삭제를 할 수 있도록 위쪽에 배치해둔 것입니다.
- @Entity는 JPA의 어노테이션이며, @Getter과 @NoArgsConstructor는 lombok 어노테이션입니다.
- Posts 클래스는 실제 DB 테이블과 매칭될 클래스이며 보통 Entity 클래스라고도 합니다. JPA를 사용하면 DB 데이터에 작업할 경우 실제 쿼리를 날리기보다는, Entity 클래스의 수정을 통해 작업합니다.
- @Id
- 해당 테이블의 PK 필드를 나타냅니다.
- Entity의 PK는 Long 타입의 Auto_increment를 추천합니다. 여러 키로 조합한 복합키나 유니크 키로 PK를 잡을 경우 불편할 수 있습니다.
- 인덱스에 좋은 영향을 주지 못합니다.
- 유니크한 조건이 변경될 경우, PK 전체를 수정해야 할 수 있습니다.
- @GeneratedValue
- PK 생성 규칙을 나타냅니다.
- 스프링 부트 2.0에서는 GenerationType.IDENTITY를 추가해야만 auto_increment가 됩니다.
- @Column
- 테이블의 컬럼을 나타내며 따로 선언하지 않아도 해당 클래스의 필드는 모두 컬럼이 됩니다.
- 해당 어노테이션은 기본값 외에 추가로 변경이 필요한 옵션이 있을 때 사용합니다.
- 예: 문자열의 경우 VARCHAR(255)가 기본값인데, 사이즈를 500으로 늘리고 싶거나 타입을 TEXT로 변경하고 싶을 때 사용합니다.
- @Entity
- 테이블과 링크될 클래스임을 나타냅니다.
- 기본값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍(_)으로 테이블 이름을 매칭합니다.
- 예: TestCase.java → test_case table
- @NoArgsConstructor
- 기본 생성자를 자동으로 생성합니다.
- 예: public Posts() {} 생성
- 기본 생성자를 자동으로 생성합니다.
- @Getter
- 클래스 내 모든 필드의 Getter 메소드를 자동 생성합니다.
- @Builder
- 해당 클래스의 빌더 패턴 클래스를 생성합니다.
- 생성자 상단에 선언할 경우 생성자에 포함된 빌드만 빌더에 포함됩니다.
서비스 초기 구축 단계에서는 테이블 설계(Entity 설계)가 빈번하게 이루어집니다. 이 때 롬복 어노테이션을 사용하면, 코드 변경량을 최소화시켜 주기 때문에 롬복 활용도가 높습니다.
Posts 클래스의 특이점은, Setter 메소드가 없다는 점입니다.
자바빈 규약을 생각하며 getter/setter를 무작정 생성하는 경우가 있지만, 이렇게 되면 해당 클래스의 인스턴스 값들이 언제 어디서 변해야하는지 코드상 명확히 구분하기 어려워 기능 변경시 복잡합니다.
그래서, Entity 클래스에서는 절대 Setter 메소드를 만들지 않습니다. 대신, 해당 필드의 값 변경이 필요하면 명확히 그 목적과 의도를 나타낼 수 있는 메소드를 추가 합니다.
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();
}
Entity 클래스에서 Setter가 없다면 어떻게 값을 채워 DB에 삽입할까?
기본적인 구조는 생성자를 통해 최종값을 채운 후 DB에 삽입하며, 값 변경이 필요한 경우 해당 이벤트에 맞는 public 메소드를 호출하여 변경하는 것을 전제로 합니다.
위에서는 생성자 대신, @Builder를 통해 제공되는 빌더 클래스를 사용합니다.
- 생성자나 빌더나 생성 시점에 값을 채워주는 역할은 똑같습니다.
- 다만, 생성자는 지금 채워야 할 필드가 무엇인지 명확하게 지정할 수 없습니다.
생성자와 Builder 예시
// 생성자의 경우, Example(b,a)로 잘못 넣어도 코드 실행 전에 오류를 찾을 수 없습니다.
public Example(String a, String b) {
this.a = a;
this.b = b;
}
// builder의 경우 어느 필드에 어떤 값을 채워야 할지 명확히 인지할 수 있습니다.
Example.builder()
.a(a)
.b(b)
.build();
- 생성자의 경우, 동일한 타입의 파라미터라면 값을 잘못 넣어도 코드 실행 전까지는 오류를 찾을 수 없습니다.
- builder의 경우 어느 필드에 어떤 값을 채워야 할지 명확하기 때문에 좀 더 직관적입니다.
JpaRepository 생성하기
Posts 클래스 생성이 완료되었으면, Posts 클래스로 Database를 접근하게 해줄 JpaRepository를 생성합니다.
JpaRepository는 보통 myBatis에서 dao로 불리는 DB Layer 접근자로, 인터페이스로 생성합니다. 여기서는 domain.posts 패키지 내에 PostsRepository 인터페이스를 생성하면 됩니다.
package com.earth.study.springboot.domain.posts;
import org.springframework.data.jpa.repository.JpaRepository;
public interface PostsRepository extends JpaRepository<Posts, Long> {
}
- JpaRepository<Entity 클래스, PK 타입>를 상속하면, 기본적인 CRUD 메소드가 자동으로 생성됩니다.
- @Repository 어노테이션을 추가할 필요 없습니다.
- 주의할 부분은 Entity 클래스와 기본 Entity Repository는 함께 위치해야 한다는 점입니다.
- Entity 클래스는 기본 Repository 없이는 제대로 역할을 할 수 없습니다.
Spring Data JPA 테스트 코드 작성하기
test 디렉토리에 domain.posts 패키지를 생성하고, 테스트 클래스는 PostsRepositoryTest란 이름으로 생성합니다.
PostsRepositoryTest에서는 아래와 같이 save, findAll 기능을 테스트합니다.
package com.earth.study.springboot.domain.posts;
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();
}
@Test
public void 게시글저장_불러오기() {
// given
String title = "테스트 게시글";
String content = "테스트 본문";
postsRepository.save(Posts.builder()
.title(title)
.content(content)
.author("author")
.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 데이터베이스를 자동으로 실행해줍니다.
위 테스트가 제대로 된다면, 아래와 같은 테스트 결과를 확인할 수 있습니다.
현재 테스트 로그에는 쿼리가 제대로 수행되었는지는 따로 확인이 어렵습니다. 만약 실행된 쿼리를 로그를 보고자 한다면 아래 과정을 통해 쿼리 로그를 ON으로 변경해주면 됩니다. 먼저, src/main/resources 디렉토리 아래에 application.properties 파일을 생성합니다.
spring.jpa.show_sql=true
application.properties에 위 코드를 작성한 후 PostsRepositoryTest를 재실행하면 콘솔에서 쿼리 로그를 확인할 수 있습니다.
이때, 로그 중 create table 쿼리 내 id bigint generated by default as identity라는 옵션으로 생성됩니다. 이는 H2의 쿼리 문법이 적용되어서 나타난 현상입니다. 이후 디버깅을 위해 출력되는 쿼리 로그를 MySQL 버전으로 변경해보겠습니다.
application.properties에 하기와 같은 코드를 추가해줍니다. 추가한 후, 다시 테스트를 실행하면 mySQL 버전으로 로그가 출력되는 것을 확인할 수 있습니다.
- 아래 설정은 spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL5InnoDBDialect으로 한 줄로 설정이 가능하나, 해당 설정이 deprecated되었기 때문에 아래와 같이 MySQL57Dialect을 사용하고, 직접 jdbc-url을 설정하여 사용합니다.
하기와 같이 설정을 변경한 부분은 아래 주소를 참고하여 진행하였습니다.
github.com/jojoldu/freelec-springboot2-webservice/issues/67#issuecomment-566523952
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL57Dialect
spring.jpa.properties.hibernate.dialect.storage_engine=innodb
spring.datasource.hikari.jdbc-url=jdbc:h2:mem:testdb;MODE=MYSQL
'FRAMEWORK > Spring' 카테고리의 다른 글
[SpringBoot] 게시판 만들기 v2.2 (머스테치로 화면 구성하기) (1) | 2021.04.25 |
---|---|
[SpringBoot] 게시판 만들기 v2.1 (등록/수정/조회 API 만들기) (0) | 2021.04.25 |
[SpringBoot] springboot에서 테스트 코드 사용하기 (0) | 2021.04.20 |
[SpringBoot, PostgreSQL] myBatis ?(물음표) 문자 사용하기 (0) | 2021.02.02 |
[SpringBoot] 게시판 구현하기 21 (스프링 Cloud Config 사용하기) - 미완 (1) | 2021.01.28 |