Spring boot X JPA

Spring boot에서 JPA 사용하기

1. JPA(Java Persistence API)란?

1-1. 개요

  • JavaSE, JavaEE를 위한 영속성(Persistence) 관리와 ORM(Object Relational Mapping [객체 - 관계형 데이터베이스 맵핑])을 위한 표준 기술
  • 종류로는 Hibernate, OpenJPA, EclipseLink, TopLink Esstientials 등과 같은 구현체가 있으며, 이에 대한 표준 인터페이스가 바로 JPA이다.

1-2. 장단점

  • 장점

    • 객체 지향적으로 데이터 관리가 가능하기에 비즈니스 로직에 집중할 수 있다. 당연하게도 객체지향 중심으로 개발이 가능해진다.
    • 테이블 생성, 변경, 관리가 쉽다.
    • 쿼리에 집중하기 보다 객체 자체 개발에 대해서 집중할 수 있다. (쿼리를 사용하지 않기에)
    • 빠른 개발이 가능하다.
  • 단점

    • 어렵고 알아야할것들이 많다.
    • 잘 이해하고 사용하지 못할경우 데이터 손실 가능성이 있다. (persistence context)
    • 성능상 문제가 있을 수 있다. (잘 안다면 해결이 가능하다.)

1-3. ORM(Object Relational Mapping)이란

  • Object(객체) Relational(관계) Mapping(맵핑)
    • 객체는 객체대로 설계하고, RDB는 RDB 대로 설계한다.
    • ORM 프레임워크가 중간에서 맵핑해준다.
      • MyBatis, iBatis는 ORM이 아닌, SQL Mapper이다. SQL Mapper는 쿼리를 매핑한다.

2. JPA 의존성 설정

  • gradle 설정
    • Dependencies 에 다음 추가

      implementation('org.springframework.boot:spring-boot-starter-data-jpa')
      implementation('com.h2database:h2')
      • spring-boot-stater-data-jpa: Spring boot용 Spring Data JPA 추상화 라이브러리. 스프링 부트 버전에 맞춰서 자동으로 JPA관련 라이브러리 버전을 관리해준다.
      • H2: 인메모리 관계형 데이터 베이스. 별도의 설치 필요없이 의존성만으로 관리가 가능하고, 메모리에서 실행되기에 어플리케이션 재시작시 초기화된다.

3. DB (Sql-light) 설치

3-1. sqllitebrower 설치 (SQLite 테스트용)

  • Linux

    snap install sqlitebrowser # 실행 sqlitebrowser
  • Windows

    • 여기 에서 다운로드 받을 수 잇습니다.
  • 설치 후

3-2. SQlite 데이터 베이스 생성

  • 새 데이터베이스 클릭 후 원하는 DB 저장 위치에 save

3-3. TEST 테이블 생성

  • 데이터베이스 구조 > 테이블 생성하기 클릭
  • 테이블 이름을 TEST라고 하고 필드에 각각 ID : INTEGER (PK) VALUE : TEXT NOT NULL DEFAULT "" 추가

3-4. Spring-boot Sqlite JDBC 연동

  1. gradle dependencies 에 다음 추가
    implementation('org.xerial:sqlite-jdbc') // 밑에 버전은 보안 이슈가 있었던거 같다.
  1. SQLDialect.java 파일 생성 출처

    • Hibernate에는 SQLite를 위한 Dialect가 없다.

    • Dialect란 JPA에서는 개발자가 직접 SQL문을 작성하는게 아니라 JPA가 대신해주는데, 이때 사용하는게 Dialect이고 SQLDialect는 데이터베이스간의 SQL 문법 차이를 보정해주기위해 JPA가 제공하는 클래스이다.

    • SQLDialect.java

     package com.test.blog.config;
    
    import org.hibernate.dialect.Dialect;
    import org.hibernate.dialect.function.SQLFunctionTemplate;
    import org.hibernate.dialect.function.StandardSQLFunction;
    import org.hibernate.dialect.function.VarArgsSQLFunction;
    import org.hibernate.type.StringType;
    
    import java.sql.Types;
    
    public class SQLDialect extends Dialect {
        public SQLDialect() {
            registerColumnType(Types.BIT, "integer");
            registerColumnType(Types.TINYINT, "tinyint");
            registerColumnType(Types.SMALLINT, "smallint");
            registerColumnType(Types.INTEGER, "integer");
            registerColumnType(Types.BIGINT, "bigint");
            registerColumnType(Types.FLOAT, "float");
            registerColumnType(Types.REAL, "real");
            registerColumnType(Types.DOUBLE, "double");
            registerColumnType(Types.NUMERIC, "numeric");
            registerColumnType(Types.DECIMAL, "decimal");
            registerColumnType(Types.CHAR, "char");
            registerColumnType(Types.VARCHAR, "varchar");
            registerColumnType(Types.LONGVARCHAR, "longvarchar");
            registerColumnType(Types.DATE, "date");
            registerColumnType(Types.TIME, "time");
            registerColumnType(Types.TIMESTAMP, "timestamp");
            registerColumnType(Types.BINARY, "blob");
            registerColumnType(Types.VARBINARY, "blob");
            registerColumnType(Types.LONGVARBINARY, "blob");
            // registerColumnType(Types.NULL, "null");
            registerColumnType(Types.BLOB, "blob");
            registerColumnType(Types.CLOB, "clob");
            registerColumnType(Types.BOOLEAN, "integer");
            registerFunction("concat", new VarArgsSQLFunction(StringType.INSTANCE, "", "||", ""));
            registerFunction("mod", new SQLFunctionTemplate(StringType.INSTANCE, "?1 % ?2"));
            registerFunction("substr", new StandardSQLFunction("substr", StringType.INSTANCE));
            registerFunction("substring", new StandardSQLFunction("substr", StringType.INSTANCE));
        }
        public boolean supportsIdentityColumns() { return true; }
        public boolean hasDataTypeInIdentityColumn() { return false;  }
        public String getIdentityColumnString() { return "integer"; }
        public String getIdentitySelectString() { return "select last_insert_rowid()"; }
        public boolean supportsLimit() {
            return true;
        }
        protected String getLimitString(String query, boolean hasOffset) {
            return new StringBuffer(query.length() + 20).append(query).append(hasOffset ? " limit ? offset ?" : " limit ?") .toString();
        }
        public boolean supportsTemporaryTables() { return true; }
        public String getCreateTemporaryTableString() { return "create temporary table if not exists"; }
        public boolean dropTemporaryTableAfterUse() { return false; }
        public boolean supportsCurrentTimestampSelection() { return true; }
        public boolean isCurrentTimestampSelectStringCallable() { return false; }
        public String getCurrentTimestampSelectString() { return "select current_timestamp"; }
        public boolean supportsUnionAll() { return true; }
        public boolean hasAlterTable() { return false;  }
        public boolean dropConstraints() { return false; }
        public String getAddColumnString() { return "add column"; }
        public String getForUpdateString() { return ""; }
        public boolean supportsOuterJoinForUpdate() { return false; }
        public String getDropForeignKeyString() { throw new UnsupportedOperationException("No drop foreign key syntax supported by SQLiteDialect"); }
        public String getAddForeignKeyConstraintString(String constraintName, String[] foreignKey, String referencedTable, String[] primaryKey, boolean referencesPrimaryKey) { throw new UnsupportedOperationException("No add foreign key syntax supported by SQLiteDialect"); }
        public String getAddPrimaryKeyConstraintString(String constraintName) { throw new UnsupportedOperationException("No add primary key syntax supported by SQLiteDialect"); }
        public boolean supportsIfExistsBeforeTableName() { return true; }
        public boolean supportsCascadeDelete() { return false; }
    
    }
    
  2. application.properties 에 다음 추가 (없으면 src > main > resources > application.properties 에 생성)

    spring.jpa.database-platform=com.test.blog.config.SQLDialect
    spring.jpa.hibernate.ddl-auto=update
    
    spring.datasource.url=jdbc:sqlite:TBlog.db
    spring.datasource.driver-class-name=org.sqlite.JDBC
    
    spring.datasource.username=admin
    spring.datasource.password=admin
    
  3. Test Entity 클래스 작성

    package com.test.blog.test.entity;
    
    import lombok.Getter;
    import lombok.RequiredArgsConstructor;
    
    import javax.persistence.Entity;
    import javax.persistence.GeneratedValue;
    import javax.persistence.Id;
    
    @Getter
    @RequiredArgsConstructor
    @Entity
    public class Test {
        @Id
        @GeneratedValue
        private int id;
        private String value;
    }
  4. 서버 구동 시 TBlog.db 파일이 생성된다.

    • 해당 파일을 sqlitebrowser로 열어서 보면 다음과 같이 테이블과 컬럼들이 생성되어있다.

4. Spring boot에 JPA 적용하기

4-1. 요구사항 분석

  • 사용자 (User) 관련 로직 추가
    • 사용자 회원가입 (Insert)
    • 사용자 탈퇴 (Delete)
    • 사용자 조회 (Selete)
    • 사용자 정보 변경 (Update)
    • 사용자는 ID는 중복되지 않는다.
    • 사용자 이름, 전화번호, 주민등록번호, 비밀번호를 가진다.
    • 이름, 전화번호, 주민등록번호, 비밀 번호는 필수 입력 사항이다.

4-2. User Entity 클래스 작성

  • User.java
     package com.test.blog.user.entity;
     
     
     import lombok.Builder;
     import lombok.Getter;
     import lombok.NoArgsConstructor;
     import lombok.RequiredArgsConstructor;
     
     import javax.persistence.Column;
     import javax.persistence.Entity;
     import javax.persistence.Id;
     
     @Getter
     @Entity
     @NoArgsConstructor
     public class User {
     
         @Id
         private String id;
     
         @Column(length = 100, nullable = false)
         private String phoneNumber;
     
         @Column(length = 100, nullable = false)
         private String socialSecurityNumber;
     
         @Column(length = 100, nullable = false)
         private String password;
     
         @Builder
         public User(String id, String phoneNumber, String socialSecurityNumber, String password){
             this.id                     = id;
             this.phoneNumber            = phoneNumber;
             this.socialSecurityNumber   = socialSecurityNumber;
             this.password               = password;
         }
     }   
    • @Entity: 테이블과 링크될 클래스임을 명시. 기본적으로 카멜케이스 이름을 언더 스코어 이름으로 자동 변환하여 컬럼이 삽입된다.
    • @Column: 굳이 컬럼을 추가하지 않아도 Entity에 있는 필드는 전부 컬럼이 된다. 하지만 붙이는 이유는 length 와 같은 옵션들을 추가해야할때 붙인다.
    • @Builder : 해당 클래스의 빌더 메소드를 생성. 생성자 상단에 선언시 해당 생성자의 파라미터에 포함된 필드들만 빌더에 포함됨.
    • 위와 같이 작성 후 서버를 실행하면 다음과 같은 DB 테이블이 만들어진다.

Entity 클래스에는 Setter 메소드를 만들지 않는다. (언제 값이 변했는지 명확하게 파악하기 힘들기 때문)

4-3. JpaRepository 클래스 생성

  • Repository 작성

    package com.test.blog.user.entity;
    
    import com.test.blog.user.entity.User;
    import org.springframework.data.jpa.repository.JpaRepository;
    
    public interface UserRepository extends JpaRepository<User, String> { // 제네릭의 첫번째 타입은 Entity, 두번째 타입은 PK 타입
    
    }
    
    • Mybatis나 ibatis에서 DAO라고 불리는 DB 레이어 접근자.
    • Repositroy는 인터페이스로 생성
    • @Repository 어노테이션을 안붙여도 됨.
    • JpaRepository<Entity class, PK type>를 상속받음. 상속시 기본적인 CRUD 메소드가 생성됨.

4-4. User Test

  • Test code 작성

    package com.test.blog.user;
    
    import com.test.blog.user.entity.User;
    import com.test.blog.user.entity.UserRepository;
    import org.assertj.core.api.Assertions;
    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;
    
    @RunWith(SpringRunner.class)
    @SpringBootTest
    public class UserTest {
    
        @Autowired
        UserRepository userRepository;
    
        @After
        public void cleanUp(){
            userRepository.deleteAll();
        }
    
        @Test
        public void insertAndSelectUser(){
            final String adminId = "ADMIN";
            final String adminPassword = "ADMIN1234";
            User user = User.builder()
                    .id(adminId)
                    .password(adminPassword)
                    .phoneNumber("000-0000-0000")
                    .socialSecurityNumber("123456-1234567")
                    .build();
    
            userRepository.save(user);
    
            List<User> users = userRepository.findAll();
    
            User user1 = users.get(0);
    
            Assertions.assertThat(user1.getId()).isEqualTo(adminId);
            Assertions.assertThat(user1.getPassword()).isEqualTo(adminPassword);
        }
    }
    
    • @SpringBootTest : 별다른 설정이 없으면 H2 데이터베이스를 자동으로 실행해준다.

    • @After: 단위테스트가 끝날때 마다 실행되는 메소드. 보통 단위 테스트간의 데이터 침범을 막기위해 사용한다.

    • userRepository.save : Insert/update 쿼리를 실행한다. ID에 해당하는 Row값이 있다면 Update, 없다면 Create를 한다.

    • userRepository.findAll : 해당 테이블의 모든 Row를 가져오는 메소드

      application.properties에 spring.jpa.show-sql=true를 추가하면 쿼리 로그를 확인 할 수 있다.

좋은 웹페이지 즐겨찾기