Spring boot에서 DBMS를 연동하는 방법 JPA 고급편

안녕하세요. Spring 관련 글을 정말 안쓴지가 너무 오래되었네요…ㅠㅠ 요즘 데이터 분야에서 일하다보니 R을 다루면서 백엔드에 대한 포스팅이 많이 밀려있네요.. 앞으로는 Spring을 이용한 오픈 프로젝트에 참여 중이니 다시 재건을 위해서라도 천천히 하나씩 글을 작성해보도록 하겠습니다.

이번에는 지난 글에 이어서 JPA 고급편에 대해 작성해보도록 하겠습니다. 워낙 오래됐음에도 불구하고 고급편에 무엇을 적어야할지 명시가 되어 있네요. 오늘은 그 부분을 다뤄보겠습니다.

SQL과 HQL

SQL은 Structed Query Language의 약자로 DBMS의 데이터를 조작/제어하는 쿼리 언어 중에 하나입니다. SQL Server에서는 T-SQL을 사용하는 것처럼 말이죠.

HQL은 그것과 유사하게 Hibernate Query Language의 약자로 객체 지향적으로 데이터베이스를 다루도록하는 쿼리 언어입니다. Spring boot에서 주로 데이터베이스를 다룰 때 사용하는 hibernate 라이브러리를 쿼리로 질의할 때 사용하는 언어 중 하나이며 무엇보다 장점은 SQL에서는 여러 테이블을 작업할 때 명시적인 JOIN 쿼리를 요구하지만 HQL은 그러한 쿼리를 요구하지 않는다는 것입니다.

HQL의 장단점

그럼 간단히 HQL이 SQL에 비하여 어떠한 점이 좋고 나쁜지를 정리해보겠습니다.

  • 장점
    1. 객체지향적으로 데이터를 관리할 수 있습니다.
    2. 앞서 기본편에서 JPA를 다뤘듯이 테이블 생성, 변경, 관리가 쉽습니다.
    3. 정적인 쿼리를 정의하지 않고 객체에 집중하기 때문에 빠른 개발이 가능합니다.
  • 단점
    1. 일반적인 SQL Query를 사용할 때 보다 성능적인 이슈가 존재합니다.
    2. 데이터 손실에 대해 민감합니다.

확실히 편한만큼 단점이 존재합니다. 정적인 쿼리를 사용하면 그만큼 빠르다는 장점이 있는 반면 개발은 조금 어려워지고 쉽게 개발하려고 한다면 그만큼 성능의 감소는 조금은 감안해야 합니다.

HQL을 사용해야 하는 경우

기본편에서 간단한 코드 작성으로 CRUD를 작성했습니다. 이를 통하여 Create, Read, Update, Delete를 별도의 쿼리 없이 쉽게 진행할 수 있었는데요. 그렇다면 어떨 때 쿼리 언어를 사용해야 할까요?

코드를 살펴보도록 하겠습니다. (이번 포스트에서는 Kotlin 코드만을 다룰 것입니다.)

1
2
3
4
5
6
7
8
9
10
11
12
package xyz.neonkid.jpaexample.Model

import java.io.Serializable
import javax.persistence.*

@Entity
@Table(name = "items")
data class Item(@Column(name = "name", updatable = false, nullable = false) val name: String, @Column(name = "price", updatable = false, nullable = false) val price: Int) : Serializable {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false, nullable = false)
val Id: Long? = null
}
1
2
3
4
5
6
7
package xyz.neonkid.jpaexample.Model

import org.springframework.data.repository.CrudRepository
import org.springframework.stereotype.Repository

@Repository
interface ItemRepository : CrudRepository<Item, Long>

기본편에서 이 두가지 코드만을 가지고 REST API를 호출하여 Item 테이블에 대한 정보를 가져오고, 값을 추가하는 것이 가능했습니다. 그런데, 여기서 기본적으로 제공하는 간단한 쿼리 말고 나만의 조건을 걸어줘서 나만의 메소드를 정의해야할 때가 있습니다. 어떠한 경우가 있을까요?

예를 들자면, 지금은 GET 메소드를 이용해서 Items 테이블의 모든 요소를 가져오게끔 기본적으로 작성되어 있습니다. 하지만 특정 아이템 한 가지만을 고르고 싶다면? 물론 GET 메소드를 통해서 모든 아이템 요소를 가져온 다음 클라이언트 단에서 색인하는 방법도 있을 것입니다. 하지만 이 방법은 테이블 내에 item이 10,000개, 100,000개 있는 무수한 경우가 생길 수 있기 때문에 정말 좋은 방법이 아닙니다.

이럴 경우 HQL을 사용해서 코드를 작성해주시면 됩니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
package xyz.neonkid.jpaexample.Model

import org.springframework.data.jpa.repository.Query
import org.springframework.data.repository.CrudRepository
import org.springframework.data.repository.query.Param
import org.springframework.stereotype.Repository
import org.springframework.transaction.annotation.Transactional

@Repository
@Transactional
interface ItemRepository : CrudRepository<Item, Long> {
@Query(value = "select i from items i where i.name = :name")
fun selectItem(@Param(value = "name") itemName: String): Item
}

간단하게 지금 원하는 Item의 정보만을 반환하는 코드를 작성하였습니다. 그런데, 위에 Transactional은 무엇이냐구요?

@Transactional

데이터베이스에서 말하는 Transaction은 DB의 상태를 변환시키는 작업의 단위라고 합니다. Spring boot에서 Transactional은 이러한 작업을 수행함을 정의하는 어노테이션으로 이를테면 DELETE, INSERT 작업이 이루어졌을 때 에러가 발생하면 자동으로 Rollback이 수행할 수 있도록 해주는 녀석입니다.

Example

그럼 Query를 적용한 예시를 몇가지 더 들어보도록 하겠습니다.

  1. 상품 이름을 바꾸는 메소드
  2. 해당 상품 정보를 보는 메소드
  3. 상품 정보를 제거하는 메소드

먼저 상품 이름을 바꾸는 메소드를 정의해봅시다.

1
2
3
@Modifying
@Query(value = "update items i set i.name = :newName where i.name = :prevName")
fun modItemName(@Param(value = "prevName") prevName: String, @Param(value = "newName") newName: String)

UPDATE 쿼리를 사용할 때는 Modifying Annotation을 사용합니다.

@Modifying

JPA에서 DML 작업을 진행할 경우 사용하는 Annotation으로 DML 쿼리를 사용할 때 이 어노테이션이 적용되지 않으면 Not supported for DML operations 에러가 발생한다.

다음 쿼리는 원하는 상품 정보를 가져오는 메소드입니다.

1
2
@Query(value = "select i from Items i where i.name = :name")
fun selectItem(@Param(value = "name") itemName: String): Item

이 쿼리는 생각보다 쉽게 구현할 수 있습니다.

상품 정보를 제거하는 메소드를 구현하기 전에, 음 기본적으로 CrudRepository에 DELETE 쿼리가 구현되어 있을텐데 별도로 구현해야 하나요?

3번 예시의 경우 Insert, Update, Delete 등 CRUD 쿼리를 직접 지정하는 방법에 대한 예시를 사용할 경우입니다. 이를테면 CRUDRepository에서 기본적으로 제공하는 DELETE 메소드에는 아래의 3가지가 있습니다.

  1. deleteById (ID 값을 파라미터로 받고 해당 데이터 제거)
  2. deleteAll (테이블에 해당하는 모든 데이터를 제거)
  3. delete (모델을 파라미터로 받고 해당 데이터 제거)

하지만 이 외에 내 데이터베이스의 삭제는 직접 질의하고 싶을 경우에는 아래의 코드를 작성하시면 됩니다.

1
@SQLDelete(sql = "DELETE Items WHERE id = ?")

예시는 매우 간단합니다. id의 값을 주면 그에 해당하는 데이터를 삭제하는 것이죠.

자 이제 쿼리를 작성했는데, CRUDRepository에서는 GET, POST 등 자동으로 REST API가 정해져 있었지만 우리가 개별적으로 쿼리를 지정한 경우에는 별도로 REST API를 정의해야 합니다.

CRUD 역할을 커스텀 하는 방법

다른 이벤트에 대한 쿼리에 대해서도 아래의 코드를 예시로 직접 쿼리를 정의할 수 있습니다.

1
2
3
4
@SQLInsert(sql = "")
@SQLUpdate(sql = "")
@SQLDelete(sql = "")
@SQLDeleteAll(sql = "")

그러면 제가 왜 삭제 쿼리에 대해서 따로 쿼리를 정의하는 경우에는 어떤 케이스가 있을까요? 예를 들자면 이런 경우가 있을 것 같네요. 예를 들어 현재 Items 테이블에 대한 데이터를 삭제하고자 하는데, 제약 조건(CONSTRAINT) 등이 존재한다면 해당 제약 조건의 제거를 먼저 선행한 다음 진행해야 합니다. 하지만 CRUDRepository에서 기본적으로 제공하는 쿼리에서는 이러한 부분들까지 신경써주지 않기 때문에 개발자가 직접 커스텀할 필요가 있게 됩니다.

CRUD 역할을 커스텀할 때는 Repository에 어노테이션을 지정하지 않고 반드시 객체 클래스에 지정하셔야 하며 SQL 파라미터 순서를 지켜야 합니다.

SQL 파라미터 순서는 Hibernate가 정하게 됩니다. 그렇다면 우리가 Hibernate의 파라미터 순서를 알아야 하는데, 이럴 때는 Hibernate의 처리를 Debug 모드로 변경하시고 그 순서를 보신 다음 어노테이션을 지정하시면 됩니다.

1
org.hibernate.persister.entity=DEBUG

추가로 SELECT에 대한 쿼리를 지정할 때에는 NamedNativeQuery 어노테이션을 사용합니다.

1
2
3
@NamedNativeQuery(name = "items", 
query = "select * from items where id= ?",
resultClass = Item.class)

items 테이블에서 원하는 id값으로 상품을 찾고자할 때 쓰는 쿼리를 질의하였습니다. 그리고 그의 대한 결과값은 해당 모델 객체로 반환합니다.

CRUD의 역할은 CRUDRepository에서 REST API를 제공해주기 때문에 개별적으로 쿼리를 별도로 설정해줬어도 기본으로 제공되는 REST API를 사용하시면 됩니다.

마치며…

여기까지 JPA를 사용한 Spring boot에서 DBMS와 연동하는 고급편에 대해 다뤄봤습니다. 아직은 어려운 부분이 많이 있을 것입니다. DBMS를 먼저 선행한 후에 진행하시는 사항이라면 어렵지 않게 통과하실 수 있는 부분도 있었겠지만 Spring boot가 Spring을 계승해서 나온 것도 있고 갈수록 Configuration보단 Annotation 적인 개발에 집중이 되는 프레임워크이기 때문에 많이 달라진 부분에 대해서 혼동이 있을 거라 생각합니다.

다음 포스트에서는 Spring boot Data, Spring boot Rest를 이용해서 이 포스트를 이용하여 정의한 여러분들의 쿼리를 REST API로 구현하는 방법에 대해 자세히 다뤄보도록 하겠습니다.

0%