ABOUT ME

-

Today
-
Yesterday
-
Total
-
  • JPA로 JSON Column CRUD 해보기
    JPA 2024. 1. 3. 00:01
    728x90

    개요

    JPA를 활용하여 JSON Column을 만들어보고 CRUD 튜토리얼을 수행해보고자 합니다.

    실제 코드는 github을 참고해 주세요

     

    RDB에서 JSON을 사용하는 이유는?

    일부 비정형데이터가 예상하지 못하게 생긴 경우에는 NoSQL의 기술스택을 추가하여 사용하기보다는 RDB에 JSON 타입을 사용해 볼 수 있습니다.

     

    JSON을 사용했을 때 단점은?

    데이터를 조회 후 핸들링하기가 기존보다 복잡해진다.

     

     

    JSON 타입 vs TEXT 타입

    일반적으로 대용량 칼럼, 소용량 칼럼으로 비교해 보았을 때 JSON보다 TEXT 타입이 성능적으로 더 좋은 모습을 보여줍니다.

     

    하지만 일부분을 변경하고자 해도 TEXT는 통째로 UPDATE 해야 하지만 JSON 타입은 특정 필드만 UPDATE 할 수 있고, 인덱스를 활용할 수 있는 장점이 있습니다.

     

    보통 온라인 트랜잭션 처리 용도의 RDBMS는 가볍고 빠른 쿼리들이 매우 빈번하게 처리되는 경우가 대부분입니다.

    수십 KB이상의 데이터를 한두 개의 칼럼에 저장하고 빈번하게 접근하는 것은 큰 부하를 유발합니다.

    성능에 매우 민감한 DMBS 서버를 사용하고 있다면, 데이터는 최대한 콤팩트하게 유지하는 것이 좋습니다.

     

    결론적으로 아래의 요건이 필요한 경우에는 JSON 타입을 사용하는 것이 좋습니다.

    • 특정 필드만 접근이 가능해야 하는 경우
    • 특정 필드만 자주 업데이트되는 경우
    • 특정 필드로 인덱스 생성이 필요한 경우

     

     

    조금 더 자세하게 보고 싶으신 분들은 당근 테크 블로그를 참고해 보시면 좋을 것 같습니다.

     

    환경

    • Spring Boot 3.1
    • JDK 17
    • MySQL with Docker
    • Hibernate 6.2

     

    docker-compose.yml

    version: '3.8'
    services:
      mysql:
        image: mysql:8.0.22
        container_name: mysql-db
        ports:
          - "3306:3306"
        environment:
          MYSQL_ROOT_PASSWORD: root_password
          MYSQL_DATABASE: your_database_name
          MYSQL_USER: your_database_user
          MYSQL_PASSWORD: your_database_password
        networks:
          - spring-network
    
    networks:
      spring-network:
        driver: bridge

     

    docker-compose up -d 명령어로 백그라운드로 실행

     

    application.yml

    # Database Settings
    spring:
      datasource:
        url: jdbc:mysql://localhost:3306/your_database_name?useSSL=false&allowPublicKeyRetrieval=true
        username: your_database_user
        password: your_database_password
        driver-class-name: com.mysql.cj.jdbc.Driver
    
      jpa:
        hibernate:
          ddl-auto: create-drop
        properties:
          hibernate:
            format_sql: true
        show-sql: true

    docker 파일을 기반으로 datasource 연결 설정 구성!

     

    Hibernate-Type

    Hibernate ORM에서 지원하지 않는 JSON 타입 등을 지원하는 라이브러리입니다.

    해당 라이브러리를 통해 간단하게 RDB에서 JSON 타입을 JPA로 핸들링할 수 있습니다.

     

    https://github.com/vladmihalcea/hypersistence-utils

     

    GitHub - vladmihalcea/hypersistence-utils: The Hypersistence Utils library (previously known as Hibernate Types) gives you Sprin

    The Hypersistence Utils library (previously known as Hibernate Types) gives you Spring and Hibernate utilities that can help you get the most out of your data access layer. - GitHub - vladmihalcea/...

    github.com

     

    readme 파일의 install guide

    readme 파일을 살펴보면 Hibernate 버전별로 어떤 의존성이 적합한지 가이드가 주어집니다.

    나의 Hibernate가 몇 버전인지 확인할 필요가 있을 것 같습니다.

     

    나의 Hibernate 버전 확인하기

     

    External Libraries를 통해 확인하거나 Spring Boot 공식문서의 dependency-versions를 통해 확인할 수 있습니다.

    문서 & 라이브러리를 기반으로 확인해 본 결과 Spring Boot 3.1 버전의 hibernate의 버전은 6.2.2입니다.

     

     

    Hibernate-Type 필요한 Gradle 의존성 정리

    //json column 활용하기 위한 hibernate-type 의존성 - hibernate 6.2 기준
    implementation ("io.hypersistence:hypersistence-utils-hibernate-62:3.7.0")
    implementation ("com.fasterxml.jackson.module:jackson-module-jakarta-xmlbind-annotations")

    hibernate 버전 6.2에 대한 설치 의존성입니다.

    6.2 버전이 아니라면 Hibernate-Type의 Github readme를 참고해서 다른 의존성을 추가할 수 있습니다.

     

     

    Application 구동 시 Create Table Query

    Hibernate: 
        drop table if exists member
    Hibernate: 
        create table member (
            id bigint not null auto_increment,
            histories json,
            primary key (id)
        ) engine=InnoDB

    json 타입의 histories column이 생긴 것을 확인할 수 있습니다!

     

    Member Entity 만들기

    import io.hypersistence.utils.hibernate.type.json.JsonType
    import jakarta.persistence.Column
    import jakarta.persistence.Entity
    import jakarta.persistence.GeneratedValue
    import jakarta.persistence.GenerationType
    import jakarta.persistence.Id
    import org.hibernate.annotations.Type
    
    @Entity
    data class Member(
            @Id
            @GeneratedValue(strategy = GenerationType.IDENTITY)
            val id: Long = 0,
    
            @Type(JsonType::class)
            @Column(name = "histories", columnDefinition = "json")
            val histories: MutableMap<String, String>
    ) {
            fun addHistories(additionalHistories: List<String>) {
                    additionalHistories.forEach {
                            histories[it] = it
                    }
            }
    }

    @Type 어노테이션을 활용해 주고, Map으로 만들어주었습니다.

    List도 가능합니다.

     

    MemberRepository

    interface MemberRepository: JpaRepository<Member, Long> {}

     

     

    MemberService

    @Service
    @Transactional(readOnly = true)
    class MemberService(
            private val memberRepository: MemberRepository,
    ){
        fun findUserBy(id: Long): Member {
            return memberRepository.findByIdOrNull(id) ?: throw IllegalArgumentException("${id}로 회원을 조회할 수 없습니다")
        }
    
        @Transactional
        fun signUp(memberHistories: List<String>): Long{
            val histories = memberHistories.map { it to it }.toMap().toMutableMap()
            val member = Member(histories = histories)
            return memberRepository.save(member).id
        }
    
        @Transactional
        fun addMemberHistories(id: Long, additionalHistories: List<String>): Member{
            val member = memberRepository.findByIdOrNull(id) ?: throw IllegalArgumentException("${id}로 회원을 조회할 수 없습니다")
            member.addHistories(additionalHistories)
            return memberRepository.save(member)
        }
    
        @Transactional
        fun withdraw(id: Long){
            memberRepository.deleteById(id)
        }
    
    }

    memberRepository를 활용하여 CRUD를 수행하였습니다.

     

     

    MemberController

    @RestController
    @RequestMapping("/members")
    class MemberController(
            private val memberService: MemberService,
    ) {
    
        @GetMapping("/{id}")
        fun getMemberById(@PathVariable id: Long): ResponseEntity<Member> {
            val member = memberService.findUserBy(id)
            return ResponseEntity.ok(member)
        }
    
        @PostMapping
        fun signUp(@RequestBody signUpRequest: SignUpRequest): ResponseEntity<Long> {
            val id = memberService.signUp(signUpRequest.histories)
            return ResponseEntity.ok(id)
        }
    
        @PutMapping("/{id}/history")
        fun addMemberHistories(
                @PathVariable id: Long,
                @RequestBody addHistoryRequest: AddHistoryRequest,
        ): ResponseEntity<Member> {
            val member = memberService.addMemberHistories(id = id, additionalHistories = addHistoryRequest.histories)
            return ResponseEntity.ok(member)
        }
    
        @DeleteMapping("/{id}")
        fun withdraw(@PathVariable id: Long): ResponseEntity<Unit> {
            memberService.withdraw(id = id)
            return ResponseEntity.noContent().build()
        }
    }

     

    REST API를 통해 CRUD를 구현하였습니다.

     

    기타 DTO들

    data class AddHistoryRequest(
        val histories: List<String>,
    )
    
    data class SignUpRequest(
        val histories: List<String>,
    )

     

     

    실제 API 호출을 통한 테스트 - curl

    회원 가입

    curl --location 'http://localhost:8080/members' \
    --header 'Content-Type: application/json' \
    --data '{
        "histories": [
            "wakeup", "eat", "study"
        ]
    }'

     

     

    회원 조회

    curl --location 'http://localhost:8080/members/1'

     

     

     

    회원 이력 수정

    curl --location --request PUT 'http://localhost:8080/members/1/history' \
    --header 'Content-Type: application/json' \
    --data '{
        "histories": [
            "drink","sleep"
        ]
    }'

     

     

    회원 탈퇴

    curl --location --request DELETE 'http://localhost:8080/members/1'

     

    이외에도 활용할 수 있는 @Convert

    이외에도 @Convert라는 어노테이션을 통해서도 json을 직렬화하는 방법을 활용해 볼 수 있습니다.

    간단하게 소개하자면 엔티티의 데이터를 변환해서 데이터베이스에 저장하는 기법입니다.

     

     

     

    참고자료

    https://danawalab.github.io/spring/2022/08/05/Jpa-Json-Type.html

    https://medium.com/daangn/json-vs-text-c2c1448b8b1f

    https://github.com/vladmihalcea/hypersistence-utils

     

    댓글

Designed by Tistory.