본문 바로가기
공부/프로그래밍

[SpringBoot] JPA 설정 및 테스트

by demonic_ 2018. 7. 4.
반응형

JPA 란?

JPA는 자바진영의 ORM 기술표준입니다. ORM이란 object-relational-mapping 객체와 관계형데이터베이스 매핑을 의미하는 것인데 하이버네이트(Hibernate) 오픈소스 ORM프레임워크를 기반으로 기술표준이 만들어진것이 JPA입니다. 


JPA의 장점

JPA를 사용하는 이유는 생산성, 유지보수, 성능 등을 꼽는데 우선 SQL문을 작성하지 않아도 된다는 점과 데이터베이스 중심 설계에서 객체 중심 설계로 변경됩니다. 그리고 DB컬럼이 추가될 때마다 테이블 수정이나 SQL 수정하는 과정이 없습니다. 그리고 같은 트랜잭션에 select가 여러번 호출된다면, 한번만 데이터베이스와 통신하고, 두번째부터는 조회한 객체를 재사용 합니다. 

그래서 가장 큰 장점 2가지를 꼽자면 생산성 향상(코드단위로만 알고 있어도 개발이 가능하고), DBMS가 변경된다 하더라도 소스, 특히 쿼리를 변경할 필요가 없습니다.(한번 설정한 DBMS를 바꿀일이 왠만하면 일어나진 않겠지만요.) 


JPA의 단점

반대로 단점은 다양한 쿼리작성이 힘들다는 점(오라클로 말하면 View 나 union all 등의 쿼리들), 그리고 데이터가 많이 쌓였을 경우 튜닝의 난해하기 때문에 성능상 문제가 발생할 수 있다는 것입니다. 한국에서는 잘 보이지 않지만 외국에서는 많이 보편화된 프레임워크입니다. 


SpringBoot 를 통해 JPA 설정 및 테스트를 시작해보도록 하겠습니다. 

우선 이전 버전과 최신버전 사이에 명령어가 변경된 것이 있는데 아래와 같습니다.


findOne(…) -> findById(…) 

save(Iterable) -> saveAll(Iterable) 

findAll(Iterable<ID>) -> findAllById(…) 

delete(ID) -> deleteById(ID) 

delete(Iterable<T>) -> deleteAll(Iterable<T>) 

exists() -> existsById(…)




0. DBMS에 접속해서 스키마를 생성. 

스키마 생성은 JPA로 불가능하기 때문에 DBMS에서 직접 생성하여 접속정보를 DataSource 생성시에 쓸 수 있도록 설정해 주어야 합니다. DataSource는 아래 2번에서 설정하는 방법에 대해 작성되어 있습니다.



1. gradle 에 JPA 관련 항목들 등록 

gradle 파일에 다음을 추가로 넣습니다.

compile('org.springframework.boot:spring-boot-starter-data-jpa') 

compile('org.mariadb.jdbc:mariadb-java-client') // 여기선 MariaDB를 사용했고, Mysql의 경우 는 compile('mysql:mysql-connector-java') 을 사용합니다. 

testCompile('org.springframework.boot:spring-boot-starter-test')



2. DataSource 등록 

application.yml 파일에 다음을 등록합니다.

spring:
  datasource:
    username: test
    password: test
    driver-class-name: net.sf.log4jdbc.DriverSpy
    url: jdbc:log4jdbc:mariadb://localhost:3306/test?useSSL=false



3. JPA 설정 

application.yml 파일에 등록

spring:
  jpa:
    hibernate:
      ddl-auto: update 		# create, update, create-drop, none 등의 옵션이 있습니다.
      										# create: 기존테이블 삭제 후 다시 생성
      										# update: 변경된 부분만 반영
      										# create-drop: create와 같으나 종료 시점에 테이블 DROP
      										# none: 사용하지 않음
    generate-ddl: false 	# DDl 생성 시 데이터베이스 고유의 기능 사용여부
    show-sql: true 				# 실행되는 쿼리문 보여주기 여부
    database: mysql 			# 사용되는 데이터베이스(MariaDB는 없기 때문에 mysql로 지정)
    # 테이블 생성시 엔진을 InnoDB 로 생성. 설정하지 않으면 myisam로 등록
    database-platform: org.hibernate.dialect.MySQL5InnoDBDialect



4. 엔터티 클래스 설계 

JPA는 자동으로 테이블을 생성할 수 있는 기능을 가지므로 1) SQL을 이용해서 테이블을 생성하고 엔터티 클래스를 만드는 방법과 2) JPA를 이용해서 클래스만 설계하고 자동으로 테이블을 생성하는 방법을 선택할 수 있습니다. 여기서는 2번 방법을 사용하도록 하겠습니다.


Board 엔터티를 생성합니다.

// 아래 어노테이션은 Lombok 의 기능을 사용한 것입니다.
// Lombok 을 사용하지 않는다면 getter/setter 를 직접 생성해줘야 합니다.
@Getter @Setter @ToString
@Entity
@Table(name="tb_boards")
public class Board{
	// 해당 칼럼이 식별키라는 것을 지정하는 것입니다.
	@Id
	// Mysql에서 Auto Increment, Oracle 에서는 Sequence와 동일한 일을 합니다.
	@GeneratedValue(strategy = GenerationType.AUTO) 
	private Long id;
	private String title;
	private String writer;
	private String content;

	// 엔티티가 생성되는 시점의 날짜 데이터를 기록하는 설정
	@CreationTimestamp
	private Timestamp reg_date;
	// 엔티티가 업데이트되는 시점의 날짜 데이터를 기록하는 설정
	@UpdateTimestamp
	private Timestamp mod_date;
}



5. JPA 처리를 담당하는 Repository 인터페이스 설계

JPA는 별도의 클래스 파일을 작성하지 않고 원하는 인터페이스를 구현하는 것만으로 JPA와 관련된 모든 처리가 끝납니다. 과거 DAO 역할을 했던 것을 여기서는 Repository 가 대신합니다. 


CRUD(Create, Read, Update, Delete) 작업을 주로 할때에는 CrudRepository<Table, ID> 를 

페이징 처리, 검색 처리등을 주로 사용할 경우 PagingAndSortingRepository<Table, ID>를 상속받아 구현합니다.


BoardRepository 인터페이스 만들기

/**
 * BoardRepository 클래스를 동적으로 생성
 * 		엔터티가 Board라는 것과 식별자가 Long형 임을 추가한다.
 */
public interface BoardRepository extends CrudRepository {
}



6. 테스트로 적용여부 확인하기. 

아래 테스트클래스를 작성한 후 메소드별로 차례대로 실행하면서 확인하면 좋다.

/**
 * JPA 기능 테스트 클래스 생성
 */
@RunWith(SpringRunner.class)
@SpringBootTest
public class BoardRepositoryTests {

    // 생성한 Repository를 주입
    @Autowired
    private BoardRepository boardRepository;

    // 테이블 생성 테스트
    @Test
    public void inspect(){
        // 실제 객체의 클래스 이름
        Class clz = boardRepository.getClass();

        System.out.println(clz.getName());

        // 클래스가 구현하고 있는 인터페이스 목록
        Class[] interfaces = clz.getInterfaces();

        Stream.of(interfaces).forEach(inter -> System.out.println(inter.getName()));

        // 클래스의 부모 클래스
        Class superClasses = clz.getSuperclass();

        System.out.println(superClasses.getName());
    }
/**
= 실행결과    
2018-07-02 15:31:06.062  INFO 11228 --- [           main] jdbc.sqlonly                             : create table tb_boards (id bigint not null, content varchar(255), regdate datetime, title varchar(255),
updatedate datetime, writer varchar(255), primary key (id)) engine=InnoDB
*/    


    // 삽입
    @Test
    public void testInsert(){
        Board board = new Board();
        board.setTitle("제목");
        board.setContent("내용");
        board.setWriter("user01");

        boardRepository.save(board);
    }
/**
= 실행결과
2018-07-02 15:32:03.808  INFO 3472 --- [           main] jdbc.sqlonly                             : insert into tb_boards (content, regdate, title, updatedate, writer, id) values ('내용', '07/02/2018
15:32:03.794', '제목', '07/02/2018 15:32:03.794', 'user01', 1)
 */


    // 조회
    @Test
    public void testRead(){
        Optional board = boardRepository.findById(1L);

        System.out.println(board);
    }
/**
= 실행결과
2018-07-02 15:32:45.075  INFO 10308 --- [           main] jdbc.sqlonly                             : select board0_.id as id1_0_0_, board0_.content as content2_0_0_, board0_.regdate as regdate3_0_0_,
board0_.title as title4_0_0_, board0_.updatedate as updateda5_0_0_, board0_.writer as writer6_0_0_
from tb_boards board0_ where board0_.id=1
 */


    // 수정
    @Test
    public void testUpdate(){
        Optional boardOpt = boardRepository.findById(1L);

        boardOpt.get().setTitle("제목3 로 수정");

        boardRepository.save(boardOpt.get());

    }
/**
= 실행결과
조회를 하고 난 후 수정하는 것을 확인할 수 있다.
2018-07-02 15:34:53.788  INFO 10764 --- [           main] jdbc.sqlonly                             : select board0_.id as id1_0_0_, board0_.content as content2_0_0_, board0_.regdate as regdate3_0_0_,
board0_.title as title4_0_0_, board0_.updatedate as updateda5_0_0_, board0_.writer as writer6_0_0_
from tb_boards board0_ where board0_.id=1
2018-07-02 15:34:53.818  INFO 10764 --- [           main] jdbc.sqlonly                             : update tb_boards set content='내용', regdate='07/02/2018 15:34:36.000', title='제목3 로 수정', updatedate='07/02/2018
15:34:53.811', writer='user01' where id=1

JPA는 데이터베이스에서 바로 작업하는 JDBC와는 달리 엔터티 객체들을 메모리상에서 관리하고, 필요한 경우 데이터베이스에 작업을 하게 됩니다.
수정과 삭제 작업을 직접 데이터베이스에서 SQL을 실행하는 것이 아니라, 엔티티 객체가 우선적으로 메모리상에 존재하고 있어야 하기 때문에 select가 동작하게 됩니다.
*/


    // 삭제
    // 해당 식별키를 가진 엔터티가 있으면 삭제합니다.
    @Test
    public void testDelete(){
        boardRepository.deleteById(1L);
    }
/**
= 실행결과
2018-07-02 15:35:41.575  INFO 9576 --- [           main] jdbc.sqlonly                             : select board0_.id as id1_0_0_, board0_.content as content2_0_0_, board0_.regdate as regdate3_0_0_,
board0_.title as title4_0_0_, board0_.updatedate as updateda5_0_0_, board0_.writer as writer6_0_0_
from tb_boards board0_ where board0_.id=1
2018-07-02 15:35:41.635  INFO 9576 --- [           main] jdbc.sqlonly                             : delete from tb_boards where id=1

수정부분과 마찬가지로 우선 select를 통해 조회한 후에 엔터티 객체를 보관하고 이후 delete로 실행하여 삭제합니다.
 */    
}


반응형

댓글