짱아의 개발 기록장

Chapter 03. Spring Boot에서 JPA로 데이터베이스를 다뤄보자 본문

개발 프로젝트 정리/[스프링 부트와 AWS로 혼자 구현하는 웹 서비스] 책 정리

Chapter 03. Spring Boot에서 JPA로 데이터베이스를 다뤄보자

jungahshin 2021. 2. 23. 21:30
반응형

JPA를 사용하게 된 이유?

  • SQL 단순 반복 작업으로 인한 업무 효율성 악화
  • 패러다임 불일치

패러다임 불일치

관계형 데이터베이스는 어떻게 데이터를 저장할지에 초점이 맞춰진 기술, but, 객체지향 프로그래밍은 메시지를 기반으로 기능과 속성을 한 곳에서 관리하는 기술,,, 관계형 데이터베이스로 객체지향을 표현할 수 있을까??? => 패러다임 불일치....

이렇게 서로 지향하는 바가 다른 2객의 영역(객체지향 프로그래밍 언어 / 관계형 데이터베이스)을 중간에서 패러다임 일치를 시켜주기 위한 기술이  ====> "JPA"이다.

즉, 개발자는 객체지향적으로 프로그래밍을 하고, 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가 등장한 이유는 크게 2가지이다.

  • 구현체 교체의 용이성
  • 저장소 교체의 용이성

구현체 교체의 용이성

Hibernate 외에 다른 구현체로 쉽게 교체하기 위해서이다.

Hiberante가 언젠가 수명을 다해서 새로운 JPA의 구현체가 대세로 떠오르게 될때, Spring Data JPA를 사용하고 있으면 쉽게 구현체 교체가 가능하다.

 

저장소 교체의 용이성

 서비스 초기에는 관계형 데이터베이스로 모든 기능의 처리가 가능하지만, 서비스의 트래픽이 점점 많아지면 관계형 데이터베이스만으로는 감당이 힘들다. 따라서, MongoDB와 같은 데이터의 처리가 빠른 비관계형데이터베이스가 필요하다.

이때, 개발자는 Spring Data JPA에서 Spring Data MongoDB로 의존성만 교체하면 된다!!

이는 Spring Data의 하위 프로젝트들은 기본적인 CRUD 인터페이스가 같기 때문이다.

즉, Spring Data JPA, Spring Data MongoDB, Spring Data Redis 등의 Spring Data 하위 프로젝트들은 save(), findAll, findOne()  등을 인터페이스로 가지고 있다.

그러다 보니 저장소를 교체하더라도, 기본적인 기능은 바뀔 것이 없다.

그래서 Hibernate와 같은 구현체를 직접 쓰기보다는 한 겹 더 감싸놓은 Spring Data JPA를 사용하는 것이 좋다..! (실제 Spring 팀에서도 권장하고 있다.)

 

프로젝트에 Spring Data Jpa 적용하기

build.gradle에 dependencies 추가하기

 

spring-boot-starter-data-jpa

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

h2

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

Posts클래스 추가하기

 

domian패키지/post패키지/Posts클래스 생성

src/main/java/com/jungahshin/blog/springboot/domain/posts/Posts 파일에 다음과 같은 코드를 작성해줍니다.

 

@Entity

  • 테이블과 매핑될 클래스이다. (=> Entity 클래스라고 불린다.)
  • 기본값으로 클래스의 카멜케이스 이름을 언더스코어 네이밍(_)으로 테이블 이름을 매핑한다.
  • ex) HelloController.java -> hello_controller 라는 이름의 테이블과 매핑된다.

@Id

  • 해당 테이블의 PK가 되는 필드이다.

@GeneratedValue

  • PK의 생성 규칙을 나타낸다.
  • 스프링 부트 2.0에서는 GennerationType.IDENTITY 옵션을 추가해야지만 auto_increment가 된다.

@Column

  • 테이블의 칼럼을 나타내며 굳이 선언하지 않아도 해당 클래스의 필드는 모두 칼럼이 된다.
  • 사용하는 이유는, 기본값 이외에 추가로 옵션을 적용하기 위해서이다.
  • 문자열의 경우 VARCHAR(255)가 기본인데, 사이즈를 500으로 늘리고 싶거나, 타입을 "TEXT"로 변경하고 싶을 때 사용한다.

@Builder

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

Entity 클래스에서는 Setter메소드가 존재하지 않는다.

위 Posts 클래스에는 한 가지 특이점이 있다. 바로, setter메소드가 존재하지 않는다는 것!!

getter/setter 메소드를 무작정 생성하다 보면, 해당 클래스의 인스턴스 값들이 언제 어디서 변해야 하는지 코드상으로 명확히 구분할 수가 없어서 차후 기능 변경 시 매우 복잡해진다,,,,

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

대신, 해당 필드의 값 변경이 필요하면 명확히 그 목적과 의도를 나타낼 수 있는 메소드를 추가해야 한다.

 

Ex) 주문 취소 메소드

예를 들어, 주문 취소 메소드를 만든다고 가정하고 다음 코드를 비교해보자.

 

잘못된 사용 예(Setter메소드를 사용했다... -> 목적과 의도가 명확하지 않다.)

1
2
3
4
5
6
7
8
9
public class Order {
    public void setStatus(boolean status) { 
        this.status = status;
    }
}
 
public void 주문서비스의_취소이벤트 () {
    order.setStatus(false);
}
cs

 

올바른 사용 예(목적과 의도가 명확한 메소드를 사용했다.)

1
2
3
4
5
6
7
8
9
public class Order {
    public void cancelOrder(boolean status) { 
        this.status = false;
    }
}
 
public void 주문서비스의_취소이벤트 () {
    order.cancelOrder();
}
cs

 

@Builder의 역할 ( 생성자와의 비교 )

여기서 드는 의문은? 그럼 과연 Setter가 없는 상황에서 어떻게 값을 채워 DB에 삽입(insert)해야 할까?

기본적인 구조는 생성자를 통해 최종값을 채우고 이후에 값의 변경이 필요할 때마다 해당 이벤트에 맞는 public 메소드를 호출하여 변경하는 것을 전제로 한다.

 

하지만, 이 책에서는 생성자 대신에 @Builder를 통해 제공되는 빌더 클래스를 사용한다.

생성자나 빌더나 생성 시점에 값을 채워주는 역할을 하는 것은 똑같다.

다만, 생성자의 경우 지금 채워야하는 필드가 무엇인지를 명확히 지정할 수가 없다.

 

Ex) Example 메소드

예를 들어, 아래와 같은 생성자가 있다면 개발자가 new Example(b, a)처럼 a와 b의 위치를 변경해도 코드를 실행하기 전까지는 문제점을 찾을 수가 없다.

 

1
2
3
4
public Example(String a, String b) {
    this.a = a;
    this.b = b;
}
cs

 

하지만, 빌더를 사용하게 되면 다음과 같이 어느 필드에 어떤 값을 채워야 할지 명확하게 인지할 수 있다.

 

1
2
3
4
Example.builder()
    .a(a)
    .b(b)
    .build();
cs

 

JpaRepository 생성하기

PostsRepsitory생성

src/main/java/com/jungahshin/blog/springboot/domain/posts/PostsRepository 파일에 다음과 같이 코드를 작성해줍니다.

 

보통 ibatis, Mybatis 등에서 Dao라고 불리는 DB Layer 접근자이다.

JPA에서는 보통 Repository라고 부르며 인터페이스로 생성한다. 단순히 인터페이스를 생성 후, JpaRepository<Entity클래스, PK타입>상속하면 기본적인 CRUD 메소드가 자동으로 생성된다.

여기서 주의할 점은 Entity클래스와 기본 Entity Repository는 함께 위치해야 한다는 것이다!

둘은 아주 밀접한 관계이고, Entity클래스는 기본 Repository없이는 제대로 역할을 할 수가 없다.

Spring Data JPA 테스트 코드 작성하기

PostsRepositoryTest 생성

src/test/java/com/jungahshin/blog/springboot/domain/posts/PostsRepositoryTest 파일에 다음과 같은 코드를 작성해줍니다.

@After

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

postsRepository.save()

  • 테이블 posts(Entity클래스가 Posts => 'posts' table에 매핑된다.)에 insert/update 쿼리를 실행한다.
  • id 값이 이미 테이블에 존재하면 update 쿼리가 실행되고 id값이 테이블에 없으면 insert 쿼리가 실행된다.

postsRepository.findAll()

  • 테이블 posts에 있는 모든 데이터들을 조회해오는 메소드이다.

실제로 실행된 쿼리 확인하기

실제로 어떤 형태로 쿼리가 실행되었는지 확인하고 싶다면 아래와 같이

src>resources>application.properties 파일을 생성하고 

다음 코드를 추가하면 된다.

1
2
spring.jpa.show_sql = true
spring.jpa.properties.hibernate.dialect = org.hibernate.dialect.MySQL5InnoDBDialect
cs

 

그럼 다음과 같이 실질적인 쿼리 형태를 콘솔창에서 확인할 수 있다.

등록/수정/조회 API 만들기

Spring 웹 계층

 

Web Layer, Service Layer, Repository Layer, Dto, Domain Model 총 5개의 계층으로 이루어져 있다.

각각의 기능과 역할을 다음과 같다.

1) Web Layer

 - Controller와 View의 역역이다.

 

2) Service Layer

 - Controller와 Dao사이의 중간자 역할을 한다.

 - @Service에 사용되는 서비스 영역이다.

 - @Transactional이 사용되어야 하는 영역이기도 하다.

 - 트랜잭션, 도메인 기능 간의 순서를 보장하는 역할만!! 수행한다.

 

3) Repository Layer

 - Database와 같이 데이터 저장소에 접근하는 영역이다.

 - Dao의 영역으로 이해하면 쉽다.

 

4) Dto

 - Contoller, View와 같은 각 계층들 간의 데이터 교환을 하게 해주는 역할을 한다.

 

5) Domain Model

 - 실질적인 비즈니스 로직을 처리한다.

 

 

API 만들기(등록/수정/조회)

 

PostsService, PostsApiController, PostsSaveRequestDto 파일 생성

1) PostsApiController

2) PostsSaveReqeustDto

여기서 Entity 클래스와 거의 유사한 형태임에도 불구하고 Dto클래스를 추가로 만들었다.

하지만, 절대로 Entity클래스를 Request/Response 클래스로 사용해서는 안된다.

Entity클래스는 database와 맞닿아있는 핵심 클래스이며 Entitiy클래스를 기분으로 테이블이 생성되고 스키마가 변경된다.

화면 변경은 아주 사소한 기능의 변경인데, 이를 위해서 테이블과 연결된 Entity클래스를 변경하는 것은 너무 큰 변경이다.

 

View Layer와 DB Layer의 역할 분리를 철저히 하는 것이 좋다.

실제로 Controller에서 결과값으로 여러 테이블을 조인해서 줘야 하는 경우가 빈번하기 때문에 Entity클래스만으로는 표현하기가 어려운 경우가 많다.

따라서, Entity클래스 외에도 Controller, View에서 쓸 수 있는 dto클래스가 따로 필요하다.

 

3) PostsService

4) PostsApiControllerTest

여기에서는 HelloController와는 달리 @WebMvcTest를 사용하지 않았다.

왜??그럴까??

@WebMvcTest의 경우에는 JPA의 기능이 작동하기 않는다. 따라서, JPA 기능까지 한꺼번에 테스트를 할 때에는 @SpringBootTest와 TestRestTemplate을 사용하면 된다.

 

등록 API

아래와 같이 'PostsApiControllerTest.Posts_등록된다'가 잘 실행되는 것을 알 수 있다.

 

수정, 조회 API

수정, 조회에 관련된 코드를 PostsApiController, PostsResponseDto, PostsUpdateRequestDto, Posts, PostsService 파일에 추가해준다.

PostsService 파일

PostsService에서 위와 같이 수정, 조회 코드를 작성했다.

여기서 신기한 점은, update기능에서 데이터베이스에 쿼리를 날리는 부분이 없다는 것이다!

이게 가능한 이유는 JPA의 영속성 컨텍스트 때문이다.

영속성 컨텍스트란, 엔티티를 영구 저장하는 환경이다. JPA의 핵심 내용은 엔티티가 영속성 컨텍스트에 포함되어 있냐 아니냐로 나뉜다.

 

JPA 엔티티 매니저가 활성화된 상태로 트랜잭션 안에서 데이터베이스의 데이터를 가져오면 이 데이터는 영속성 컨텍스트가 유지된 상태이다. 이 상태에서 해당 데이터의 값을 변경하면, 트랜잭션이 끝나는 시점에 해당 테이블에 변경분을 반영한다. 즉, Entity 객체의 값만 변경하면 별도로 Update 쿼리를 날릴 필요가 없다는 것!! -> 이 개념을 'Dirty Checking'이라고 한다.

 

PostsApiControllerTest 코드 이해하기

코드가 살짝 이해가 안된다...😂

 

조회 기능 확인해보기 (H2 데이터베이스 사용하기)

로컬 환경에서는 데이터베이스로 H2를 사용한다. 메모리에서 실행하기 때문에 직접 접근하려면 웹 콘솔을 사용해야한다.

먼저, 웹 콘솔 옵션을 활성화한다.

application.properties 파일에 "spring.h2.console.enabled = true" 옵션을 추가한다.

 

insert Query 실행

select Query 실행

브라우저로 API 조회

JPA Auditing으로 생성시간/수정시간 자동화하기

보통 Entity에는 해당 데이터의 생성시간과 수정시간을 포함한다. 언제 만들어졌고 언제 수정되었는지는 차후 유지보수에 있어서 굉장히 중요한 정보이기 때문이다.

그렇다 보니 매번 DB에 삽입하기 전, 갱신 하기 전에 날짜 데이터를 등록/수정하는 코드가 여기저기 들어가게 된다.

 

이런 단순하고 반복적인 코드가 모든 테이블과 서비스 메소드에 포함되어야 한다고 생각하면 매우 귀찮고 코드가 지저분해진다...ㅎ

그래서 이러한 문제를 해결하고자 JPA Auditing을 사용한다.

 

LocalDate 사용

날짜 타입을 사용한다. Java8부터 LocalDate, LocalDateTime 이 등장했다. 그간 Java의 날짜 타입인 Date의 문제점을 제대로 고친 타입이라 Java8일 경우 무조건 사용해야 한다.

 

src/test/java/com/jungahshin/blog/springboot/domain/BaseTimeEntity 파일에 다음과 같은 코드를 작성해줍니다.

 

@MappedSuperclass

JPA Entity 클래스들이 BaseTimeEntity를 상속할 경우, 필드들(createDate, modifiedDate)도 자동적으로 칼럼으로 인식하도록 한다.

 

@EntityListeners(AuditingEntityListener.class)

BaseTimeEntity 클래스에 Auditing기능을 포함시킨다.

 

@CreatedDate

Entity가 생성되어 저장될때의 시간이 자동 저장된다.

 

@LastModifiedDate

조회한 Entity의 값을 변경할 때의 시간이 자동 저장된다.

 

JPA Auditing 테스트 코드 작성하기

src/test/java/com/jungahshin/blog/springboot/domain/posts/PostsRepositoryTest 파일에 다음과 같은 코드를 이어서 작성해줍니다.

실행 결과

LocalDate.of()

인자로 전달한 값에 따른 시간 데이터를 생성해준다.

 

isAfter()

검증 대상의 시간이 인자로 전달된 시간 이후인지를 검증하는 메서드이다.

ex) 인자로 전달된 시간 -> 2021년 4월 5일 / 현재 시간 -> 2021년 4월 4일 (아래 사진과 같이 "Test Failed"로 나온다.)

 

 

반응형
Comments