[Springboot] JPA(Hibernate) 공부 편
Development/Java

[Springboot] JPA(Hibernate) 공부 편

반응형

일단.. JPA랑 Hibernate는 같은 놈이라기보단 인터페이스와 구현체로 볼 수 있다


Springboot 패키지에 springboot-starter-jpa 라는 놈이 있는데, 이를 사용하면 JPA를 쉽게 사용할 수 있다.


이 JPA 구현체로 Hibernate를 사용하는 것이다.


JPA의 구현체에는 여러가지 기법이 있는데 그 중 가장 유명한 방식을 사용 하는 것

(하도 JPA Hibernate라고 하니, 두개가 같은 건지, 같이 꼭 붙어다녀야 하는건지 항상 의문이었다)


Hibernate는 ORM 이라 불린다.


ORM(Object Relationship Mapper)는 말 그대로 무언가를 매핑해주는 매퍼를 말하는데,

DB상에 존재하는 데이터를 JAVA의 POJO로 인식을 하여 접근하게 해주는 역할을 한다.


즉,

Person이라는 테이블에

{id=2, name=gompang, job=developer}


이라는 값이 존재한다고 했을 때



class Person{
 private int id;
 private String name;
 private String job;
}

와 같이 표현을 할 수 있는 것이다.


Mybatis와 같이 SQL을 지원하는 라이브러리에서도 물론 저렇게 매핑이 되긴 한다.


다만, Hibernate를 사용하는 궁극적인 이유는 그러한 SQL문들이 바뀌게 되고 수정이 필요하다면 SQL전체에 수정이 필요한 경우가 오게 된다.


만약 기존의 Column이름을 바꾼다던지, type을 바꾼다고 했을 때 Mybatis의 경우는 해당하는 부분을 전부 수정해줘야 한다(SQL)


현재 게시판을 직접 구성해보면서 느끼는건데, 짜잘한 쿼리가 상당히 많이 생길 수 밖에 없는데

(조회수 업데이트, 댓글 업데이트, 글 업데이트 등 update 및 각종 조건을 건 select문)


hibernate를 사용했더니 한결 편리해졌다고 느끼고 있다.


사용해보려면 해당 기능을 추가하면 된다(말이여 방구여)


#1. Spring-boot를 사용한 프로젝트를 진행하고 있으니, 해당 dependency를 추가해본다


https://spring.io/guides/gs/accessing-data-jpa/ 여기를 참고해서 dependency를 추가하자


maven은 아래와 같다


<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>


#2. DB Connector의 dependency를 추가해준다(중요, JPA만 추가해준다고 hibernate가 어떤 DB를 사용할지 알 수 없는 부분이므로, 반드시 추가한다)

mysql, mssql, h2 ... 등 여러 종류의 db에 대한 connector를 추가한다.


mysql의 경우

<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
</dependency>


만약 해당 db에 대한 커넥터가 없다면 아래와 같은 에러 메세지를 접할 수 있다


#3. application.properties에 사용할 DB와 커넥션 정보를 기입한다.


spring.datasource.url=jdbc:mysql://DB주소/DB명
spring.datasource.username=DB유저명
spring.datasource.password=패스워드

spring.jpa.database=mysql

위의 설정을 해주지 않으면 Hibernate가 어떤 DB에 매핑될 지 모르니 또 아래와 같은 에러 메세지를 접할 수 있다.


Caused by: org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Cannot determine embedded database driver class for database type NONE. If you want an embedded database please put a supported one on the classpath. If you have database settings to be loaded from a particular profile you may need to active it (the profiles "alpha" are currently active).


Caused by: org.springframework.beans.BeanInstantiationException: Failed to instantiate [org.apache.tomcat.jdbc.pool.DataSource]: Factory method 'dataSource' threw exception; nested exception is org.springframework.boot.autoconfigure.jdbc.DataSourceProperties$DataSourceBeanCreationException: Cannot determine embedded database driver class for database type NONE. If you want an embedded database please put a supported one on the classpath. If you have database settings to be loaded from a particular profile you may need to active it (the profiles "alpha" are currently active).


datasource 및 사용할 database를 정하면 위 문제가 해결된다.


#4. Entity class를 생성하고 DB와 동일하게 매핑해준다.



@Data
@Entity
@Table(name = "hibTest")
public class HibTest {

    @Id
    @GeneratedValue
    private int seq;

    private String name;
    private String password;
}


본인의 경우 Lombok 이라는 플러그인을 사용하고 있어서 @Data 라는 어노테이션을 사용했는데, 이는 그냥 각 멤버객체에 대한 getter/setter를 직접 만들어줘도 된다.

(Lombok이 상당히 편리하므로 사용해보자. 단, 중요한 문제는 Lombok은 플러그인이 깔려있지 않은 곳에서는 Compile에러가 발생하게 된다 ㅎ;)


위의 Entity를 설명해보자면


@Entity 를 명시해서 해당 클래스가 Hibernate <-> DB간 사용될 도메인 객체라고 명시를 하고


@Table(name = "hibTest") 라고 명시하여 어떤 테이블을 사용할 지 지정해준다.

(이는 연결된 DB에 클래스 명과 틀릴 경우 사용하는 방법. Class명으로 일단 매핑을 시도해보기 때문에 적지 않아도 된다)


내부 객체로 들어가서


@Id

@GeneratedValue

가 두개가 붙어있는데,


@Id는 해당 테이블의 row를 식별할 Column을 의미한다.(long, int, String 등등) 즉 "Primary Key"가 걸린 column을 의미함.

@GeneratedValue는 int형의 경우 "auto increment"를 사용할 수 있는데 이런 경우에 명시를 해준다(자동증가 행이라는걸 의미), 다른 경우의 Generated가 되는 경우가 있는지는 모르겠다.


여기에 명시되지 않은 어노테이션들이 많은데


@Column(name ="test") 와 같이 내부 객체변수에 붙여주면 해당 Column을 뜻하는 데이터로 받아들인다


Hibernate에서 사용하는 name="" 와 같은 형식은 실제 변수명과 테이블의 column명이 동일하지 않을 때 사용하면 된다


그리고 매우 중요한거..

JPA(Hibernate)에서 사용하는 이름이 자동변환되서 적용된다는 점인데 이는 진짜 매우 왜 그렇게 만들었는지 의문인데,


예를 들어


위의 @Table(name ="hibTest") 라고 명시를 하면


이를 변환하여


실제 DB에 "hib_test" 라는 테이블에 매핑이 되어버린다.


??


Camel case를 적용해서 자동 이름변환이 되어 버리는데..


아니 뭐 새로 프로젝트 진행하면 모르겠는데.. 만약 기존 DB 매퍼를 변경한다고 치면 이러한 고려없이 테이블명 및 객체를 설계했다면 일일히 매핑해줘야 하는 수고로움이 들 것 같다.


분명 존재하는 Column인데 찾을수가 없다고 나오는 오류를 보자니.. 참.. 왜 이렇게 직관적이지 않게 했는지 의문인데..


여튼 그렇게 동작한다


문제를 겪은건 


private Date creationDate; // 라는 변수가 있었는데, 이를 db의 creation_date Column을 계속 찾아서 not found를 떨구는 것이다..; 누가 바꾸랬니;


뭐.. 적응되면 설계할때부터 그렇게 하겠지만 혼동이 좀 있는 부분이다.


#5. CRUD를 담당할 Repository를 생성한다.


위의 예시처럼 각 Row를 매핑할 클래스를 만들었다면 이제, 그 CRUD 작업을 하기 위한 레파지토리를 만들어야할 시간이다.




@Repository
public interface HibernateRepository extends CrudRepository {

    @Query("select hib from HibTest hib")
    List getHibTests();
}


간단하게 위와 같이 볼 수 있는데,


@Repository로 명시를 해서 spring-boot에서 사용할 bean으로 등록하고(이는 spring-boot관련된 어노테이션이다)


interface로 선언을 한 뒤, "CrudRepository" 를 상속받는다.


그럼 해당 abstract 함수들이 자동생성이 되는데, 이를 가지고도 CRUD를 진행할 수 있지만, custom한 작업을 위해서 직접 쿼리를 날릴수 있다


@Query("쿼리 내용")

와 같이 사용하게 되는데, 이는 객 db에서 사용되는 sql문과 정확히 동일하다.


다만, 아래와 같은 형태는 사용할 수 없다


'SELECT * FROM HibTest"


*와 같은 전체선택 연산자는 사용할 수 없고, 반드시 해당 테이블을 조회한 변수를 사용해서 sql문을 처리해야 한다.


LIMIT도 사용 불가하다(직접 sqlSession을 사용하는 Hibernate에선 가능하겠지만, Springboot starter 에선 안되는 걸로 알고있다)


이럴땐, Pageable 이라는 클래스를 사용해서 페이징 처리를 해서 내려줘야 한다.


기본적으로 제공되는 메서드는 아래와 같다(CrudRepository)


<S extends T> S save(S var1);

<S extends T> Iterable<S> save(Iterable<S> var1);

T findOne(ID var1);

boolean exists(ID var1);

Iterable<T> findAll();

Iterable<T> findAll(Iterable<ID> var1);

long count();

void delete(ID var1);

void delete(T var1);

void delete(Iterable<? extends T> var1);

void deleteAll();


편하긴 하다


굿

반응형