본문으로 바로가기

[스프링부트 (7)] Spring Boot JPA(1) - 시작 및 기본 설정

category 3. 웹개발/3_1_3 스프링부트 2020. 2. 23. 15:49
반응형

[스프링부트 (7)] Spring Boot JPA(1) - 시작 및 기본 설정

 

안녕하세요. 갓대희 입니다. 이번 포스팅은 [ 스프링 부트  JPA 설정 방법입니다. : ) 

 

 

0. 기본개념

JPA를 들어가기 전에 ORM부터 간단하게 알아보자.

실제 예제부터 먼저 돌려보고 나중에 이해해도 무방 할 것 같다.

# ORM이란?

 - Object-Relational Mapping (객체와 관계형데이터베이스 매핑, 객체와 DB의 테이블이 매핑을 이루는 것)

 - 객체가 테이블이 되도록 매핑 시켜주는 프레임워크 이다.
 - 프로그램의 복잡도를 줄이고 자바 객체와 쿼리를 분리할 수 있으며 트랜잭션 처리나 기타 데이터베이스 관련 작업들을 좀 더 편리하게 처리할 수 있는 방법
 - SQL Query가 아닌 직관적인 코드(메서드)로서 데이터를 조작할 수 있다.
ex) 기존쿼리 : SELECT * FROM MEMBER; 이를 ORM을 사용하면 Member테이블과 매핑된 객체가 member라고 할 때, member.findAll()이라는 메서드 호출로 데이터 조회가 가능하다.

# JPA란?

 - Java Persistence API (자바 ORM 기술에 대한 API 표준 명세)
 - 한마디로 ORM을 사용하기 위한 인터페이스를 모아둔 것 이라고 볼 수 있다.
 - 자바 어플리케이션에서 관계형 데이터베이스를 사용하는 방식을 정의한 인터페이스이다.
 - ORM에 대한 자바 API 규격이며 Hibernate, OpenJPA 등이 JPA를 구현한 구현체 이다. (ORM을 사용하기 위한 인터페이스를 모아둔 것)
 - Hibernate 이외에도 EcipseLink, DataNucleus, OpenJPA, TopLink 등이 있습니다.
※결국 인터페이스이기 때문에 JPA를 사용하기 위해서는 JPA를 구현한 Hibernate, EclipseLink, DataNucleus 같은 ORM 프레임워크를 사용해야 한다.

# Hibernate?

 - JPA를 사용하기 위해서 JPA를 구현한 ORM 프레임워크중 하나.
   (자바를 위한 오픈소스 ORM(Object-relational mapping) 프레임워크를 제공한다.)
 - Hibernate는 JPA 명세의 구현체이다. javax.persistence.EntityManager와 같은 JPA의 인터페이스를 직접 구현한 라이브러리이다.

 

 

우리가 알고 있는 Hibernate가 JPA의 구현체 인 것이다.

JPA Interface : 인터페이스
↓ 상속
Hibernate, EcipseLink, DataNucleus 등 : 구현체

 

 

1. JPA를 왜 사용할까?

Mybatis에 익숙해져서 잊어버렸을 수 있지만, 신규 개발시 또는 신규 컬럼이 추가되어 작업을 할때 등 CRUD SQL을 계속 반복적으로 사용한다.


신규 컬럼 하나만 추가되더라도 DTO(Vo, Domain 등), DAO 개발, 수정 작업이 매우 반복되는 작업임을 느꼈을 것이다.
(이러한 이유 때문에 쿼리를 자동생성해주는 툴들도 많이 생겼고, DB Query문으로도 자동 SELECT, INSERT 문을 생성하여 사용하기도 하였다.)


내가 근무하고 있는 쇼핑몰이나, 여러 SI시장의 경우 매우 복잡한 비즈니스 모델을 갖추고 있고 이에 따라 데이터 베이스 중심 설계(모델링, DB 테이블 설계)이 우선시되었고,
객체지향의 장점을 살리지 않고 단순히 객체 (DTO(VO, Domain 등))를 데이터 전달 목적으로 사용하기에 급급했다.

차츰 데이터 베이스 중심 설계의 단점을 개선하여 효율적으로 개발할수 있는 방법론에 관심이 많아지기 시작하였다. 이에 객체와 테이블을 매핑시켜주는 ORM이 주목 받기 시작했고, 자바에서는 JPA라는 표준 스펙을 정의하게 되었다.


그럼 어떤 장점과 단점이 있는지 정리 해보자.

 

▶ 1. 장점

1) 생산성이 뛰어나고 유지보수가 용이하다.(데이터베이스 중심 설계에서 객체 중심 설계로 변경됨에 따른)
 - 객체 지향적인 코드로 인해 더 직관적이고 비즈니스 로직에 더 집중할 수 있게 도와준다.
 - 객체지향적으로 데이터를 관리할 수 있기 때문에 전체 프로그램 구조를 일관되게 유지할 수 있다.
 - SQL을 직접적으로 작성하지 않고 객체를 사용하여 동작하기 때문에 유지보수가 더욱 간결하고, 재사용성도 증가하여 유지보수가 편리해진다.
 - DB컬럼이 추가될 때마다 테이블 수정이나 SQL 수정하는 과정이 많이 줄어들고, 값을 할당하거나, 변수 선언등의 부수적인 코드 또한 급격히 줄어든다.
 - 각각의 객체에 대한 코드를 별도로 작성하여 코드의 가독성도 올라간다.

2) DBMS에 대한 종속성이 줄어든다.
 - DBMS가 변경된다 하더라도 소스, 쿼리, 구현 방법, 자료형 타입 등을 변경할 필요가 없다.
 - 즉 프로그래머는 Object에만 집중하면 되고, DBMS를 교체하는 작업에도 비교적 적은 리스크와 시간이 소요된다.
   특히 요즘은 탈Oracle을 하여 MariaDB 등의 무료, 오픈소스 기반의 DMBS로 변경하는 기업이 늘고 있는데 이럴때 개발자들이 신경쓸 부분이 현저히 줄어든다.

 

▶ 2. 단점

1) 어렵다.
 - JPA의 장점을 살려 잘 사용하려면 학습 비용이 높은 편이다.
 - 복잡한 쿼리를 사용해야 할 때에 불리하다.
   업무 비즈니스가 매우 복잡한 경우 JPA로 처리하기 어렵고, 통계처리와 같은 복잡한 쿼리 자체를 ORM으로 표현하는데 한계가 있다.
   (실시간 처리용 쿼리에 더 최적화되어 있다고 봐도 무방할 것이다.)
 - 결국 기존 데이터베이스 중심으로 되어 있는 환경에서는 JPA를 사용하기도 어렵고, 힘을 발휘하기 어렵다.
 - 잘못사용할 경우 실제 SQL문을 직접 작성하는 것보다는 성능이 비교적 떨어질 수 있다.
 - 대용량 데이터 기반의 환경에서도 튜닝이 어려워 상대적으로 기존 방식보다 성능이 떨어질 수 있다.

결국 업무 환경, 이러한 장단점을 고려하여 Mybatis를 사용할지 JPA를 사용할지 의사결정에 참고하면 좋을 것 같다.

 

 

2. JPA 설정 방법(기본 CRUD)

▶ 1. Dependency 추가

 - 인메모리 DB(h2 등)를 통해 간단하게 테스트 할 수도 있지만, 나와 같은 경우는 MariaDB와 연결하여 CRUD를 할 수 있는 예제를 소개하려 한다.

 - 이미 추가되어있는 Dependency는 제외하고 설정 하지 않은 Dependency를 추가 하자.

 - h2, test, lombok은 이번 포스팅에서는 필수는 아니다.

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-test</artifactId>
    <scope>test</scope>
</dependency>
<dependency>
    <groupId>org.projectlombok</groupId>
    <artifactId>lombok</artifactId>
    <optional>true</optional>
</dependency>

 

▶ 2. Entity 클래스 생성 

package com.god.bo.jpaTest.vo;

import lombok.*;
import javax.persistence.*;

@Data
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity(name="member")
public class MemberVo {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long mbrNo;

    private String id;

    private String name;

    @Builder
    public MemberVo(String id, String name) {
        this.id = id;
        this.name = name;
    }
}

 

ex) MariaDB Sample 테이블 생성 script

CREATE TABLE IF NOT EXISTS TEST.MEMBER (
	MBR_NO BIGINT NOT NULL AUTO_INCREMENT,
	ID VARCHAR(200),
	NAME VARCHAR(200),
	PRIMARY KEY(MBR_NO) /*AUTO_INCREMENT 컬럼 단일 PK */
);

1) @Entity가 붙은 클래스는 JPA가 관리하는 클래스이고, 테이블과 매핑할 테이블은 해당 어노테이션을 붙인다.

 

2) mbrNo 필드는 @id 를 사용하여 기본키(PK)로 지정한다.
 - Table 생성시 해당 필드를 PK, AUTO_INCREMENT로 설정하였기때문에 직접할당 방식이 아닌, 자동으로 생성되도록 하기위해 @GeneratedValue를 사용한다.

 - GenerationType.IDENTITY는 기본 키 생성을 데이터베이스에 위임하는 방식이다.
 - @GeneratedValue는 여러 strategy가 있다. 이부분은 추후 자세히 포스팅 하도록 할 예정이니 이번엔 IDENTITY로 설정하자. (IDENTITY, SEQUENCE, TABLE, AUTO 등이 있다.)

 

이외 JPA 엔티티 매핑방법도 추후 자세히 알아보고 이번엔 간단히 이정도만 하고 넘어 가도록 하자.

 

▶ 3. Repository 클래스 생성 

 - JPA에서는 단순히 Repository 인터페이스를 생성한후 JpaRepository<Entity, 기본키 타입> 을 상속받으면(extends하면) 기본적인 Create, Read, Update, Delete가 자동으로 생성된다.
그렇기 떄문에 단순히 인터페이스를 만들고, 상속만 잘해주면 기본적인 동작 테스트가 가능하다.

 

 - JPA 처리를 담당하는 Repository는 기본적으로 4가지가 있다. (T : Entity의 타입클래스, ID : P.K 값의 Type)
1) Repository<T, ID>
2) CrudRepository<T, ID>
3) PagingAndSortingRepository<T, ID>
4) JpaRepository<T, ID>

이번엔 제일 기본적인 Repository만 사용해볼 것이다. 나머지 Repository는 추후 자세히 알아보자.

package com.god.bo.jpaTest.repository;

import com.god.bo.jpaTest.vo.MemberVo;
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public interface MemberRepository extends JpaRepository<MemberVo, Long> {
    //비워있어도 잘 작동함.
    // long 이 아니라 Long으로 작성. ex) int => Integer 같이 primitive형식 사용못함

    // findBy뒤에 컬럼명을 붙여주면 이를 이용한 검색이 가능하다
    public List<MemberVo> findById(String id);

    public List<MemberVo> findByName(String name);

    //like검색도 가능
    public List<MemberVo> findByNameLike(String keyword);
}

위에서 작성한 것 처럼 메소드 이름을 잘 조합하여 쿼리 처리를 할 수 있는데, 지원되는 키워드들은 예를 들어 다음과 같다.
이부분도 추후 자세히 포스팅 해야하고, 이번엔 기본 CRUD에만 집중 하자. 

EX)
And => findByLastnameAndFirstname (EX. where x.lastname = ?1 and x.firstname = ?2)
Or => findByLastnameOrFirstname (EX. where x.lastname = ?1 or x.firstname = ?2)
Is, Equals => findByName,findByNameIs,findByNameEquals (EX. where x.name = 1?)
Between => findBySalBetween(EX. where x.sal between 1? and ?2)
기타 등등

▶ 4. Service 생성

package com.god.bo.jpaTest.service;

import com.god.bo.jpaTest.repository.MemberRepository;
import com.god.bo.jpaTest.vo.MemberVo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Optional;

@Service
public class MemberService {
    @Autowired
    private MemberRepository memberRepository;

    public List<MemberVo> findAll() {
        List<MemberVo> members = new ArrayList<>();
        memberRepository.findAll().forEach(e -> members.add(e));
        return members;
    }

    public Optional<MemberVo> findById(Long mbrNo) {
        Optional<MemberVo> member = memberRepository.findById(mbrNo);
        return member;
    }

    public void deleteById(Long mbrNo) {
        memberRepository.deleteById(mbrNo);
    }

    public MemberVo save(MemberVo member) {
        memberRepository.save(member);
        return member;
    }

    public void updateById(Long mbrNo, MemberVo member) {
        Optional<MemberVo> e = memberRepository.findById(mbrNo);

        if (e.isPresent()) {
            e.get().setMbrNo(member.getMbrNo());
            e.get().setId(member.getId());
            e.get().setName(member.getName());
            memberRepository.save(member);
        }
    }
}

▶ 5. Controller 생성 

package com.god.bo.jpaTest.controller;

import com.god.bo.jpaTest.service.MemberService;
import com.god.bo.jpaTest.vo.MemberVo;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletRequest;
import java.util.List;
import java.util.Optional;

@RestController
@RequestMapping("memberTest")
public class TestJpaRestController {
    // 기본형
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    MemberService memberService;

    // 모든 회원 조회
    @GetMapping(produces = { MediaType.APPLICATION_JSON_VALUE })
    public ResponseEntity<List<MemberVo>> getAllmembers() {
        List<MemberVo> member = memberService.findAll();
        return new ResponseEntity<List<MemberVo>>(member, HttpStatus.OK);
    }

    // 회원번호로 한명의 회원 조회
    @GetMapping(value = "/{mbrNo}", produces = { MediaType.APPLICATION_JSON_VALUE })
    public ResponseEntity<MemberVo> getMember(@PathVariable("mbrNo") Long mbrNo) {
        Optional<MemberVo> member = memberService.findById(mbrNo);
        return new ResponseEntity<MemberVo>(member.get(), HttpStatus.OK);
    }

    // 회원번호로 회원 삭제
    @DeleteMapping(value = "/{mbrNo}", produces = { MediaType.APPLICATION_JSON_VALUE })
    public ResponseEntity<Void> deleteMember(@PathVariable("mbrNo") Long mbrNo) {
        memberService.deleteById(mbrNo);
        return new ResponseEntity<Void>(HttpStatus.NO_CONTENT);
    }

    // 회원번호로 회원 수정(mbrNo로 회원을 찾아 Member 객체의 id, name로 수정함)
    @PutMapping(value = "/{mbrNo}", produces = { MediaType.APPLICATION_JSON_VALUE })
    public ResponseEntity<MemberVo> updateMember(@PathVariable("mbrNo") Long mbrNo, MemberVo member) {
        memberService.updateById(mbrNo, member);
        return new ResponseEntity<MemberVo>(member, HttpStatus.OK);
    }

    // 회원 입력
    @PostMapping
    public ResponseEntity<MemberVo> save(MemberVo member) {
        return new ResponseEntity<MemberVo>(memberService.save(member), HttpStatus.OK);
    }

    // 회원 입력
    @RequestMapping(value="/saveMember", method = RequestMethod.GET)
    public ResponseEntity<MemberVo> save(HttpServletRequest req, MemberVo member){
        return new ResponseEntity<MemberVo>(memberService.save(member), HttpStatus.OK);
    }

}

▶ 6. Properties 설정 

 - 일단 test 할 때에는 다음 프로퍼티를 다 주석처리하고 시작할 예정이다.

#JPA 설정
#Dialect 설정
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
#하이버네이트가 실행하는 모든 SQL문을 콘솔로 출력해 준다.
spring.jpa.properties.hibernate.show_sql=true
#콘솔에 출력되는 JPA 실행 쿼리를 가독성있게 표현한다.
spring.jpa.properties.hibernate.format_sql=true
#디버깅이 용이하도록 SQL문 이외에 추가적인 정보를 출력해 준다.
spring.jpa.properties.hibernate.use_sql_comments=true

 

 

3. 테스트 및 실행

▶ 1. 회원 저장

 - POST로 id, name 데이터를 전송하여 저장한다. (goddaehee, goddaehee2 2명 저장 하였다.)

 - Hibernate 쿼리는 따로 찍히지 않고, 내가 설정해 두었던 JDBC 로그만 찍히는 것을 볼 수 있다.

▶ 2. 전체 회원 조회 

 - 전에 저장한 회원 2명을 전체 조회 한다.

 - application.properties에 다음 내용을 추가 한다. (하이버네이트가 실행하는 모든 SQL문을 콘솔로 출력해 준다.)

   spring.jpa.properties.hibernate.show_sql=true

 - http://localhost:8080/memberTest/

 - 기존 JDBC 쿼리 이외에 하이버 네이트 쿼리가 찍힌 것을 볼 수 있다.

Hibernate: select member0_.mbr_no as mbr_no1_0_, member0_.id as id2_0_, member0_.name as name3_0_ from member member0_

 - application.properties에 다음 내용을 추가 하고 다시 호출해 보자.

   spring.jpa.properties.hibernate.format_sql=true

Hibernate: 
    select
        member0_.mbr_no as mbr_no1_0_,
        member0_.id as id2_0_,
        member0_.name as name3_0_ 
    from
        member member0_

 - 쿼리가 이쁘게 찍히는 것을 볼 수 있다.

 

 - application.properties에 다음 내용을 추가 하고 또 다시 호출해 보자.

   spring.jpa.properties.hibernate.use_sql_comments=true

Hibernate: 
    /* select
        generatedAlias0 
    from
        Member as generatedAlias0 */ select
            member0_.mbr_no as mbr_no1_0_,
            member0_.id as id2_0_,
            member0_.name as name3_0_ 
        from
            member member0_

 - 추가정보(주석)가 찍힌 것도 볼 수 있다.

▶ 3. 특정 회원 조회 

 - mbrNo로 특정 회원을 조회할 수 있다. (ex mbrNo = 3)

Hibernate: 
    select
        member0_.mbr_no as mbr_no1_0_0_,
        member0_.id as id2_0_0_,
        member0_.name as name3_0_0_ 
    from
        member member0_ 
    where
        member0_.mbr_no=?
DEBUG 20-02-23 15:44:35[http-nio-8080-exec-3] [sqltiming:368] -  com.zaxxer.hikari.pool.ProxyPreparedStatement.executeQuery(ProxyPreparedStatement.java:52)
1. select member0_.mbr_no as mbr_no1_0_0_, member0_.id as id2_0_0_, member0_.name as name3_0_0_ from member member0_ where member0_.mbr_no=3
 {executed in 18 msec}

 

▶ 4. 특정 회원 삭제 

 - mbrNo로 특정 회원을 삭제 한다.

Hibernate: 
    /* delete com.god.bo.jpaTest.vo.Member */ delete 
        from
            member 
        where
            mbr_no=?
DEBUG 20-02-23 15:46:43[http-nio-8080-exec-5] [sqltiming:368] -  com.zaxxer.hikari.pool.ProxyPreparedStatement.executeUpdate(ProxyPreparedStatement.java:61)
1. /* delete com.god.bo.jpaTest.vo.Member */ delete from member where mbr_no=4
 {executed in 5 msec}

 

이로써 기본적인 JPA설정 및 기본 CRUD 예제를 실습해 보았다.

반응형

댓글을 달아 주세요

  1. 2020.05.31 19:03

    비밀댓글입니다

  2. 2020.09.04 13:03

    비밀댓글입니다

  3. Favicon of https://snepbnt.tistory.com BlogIcon 쟈누이 2020.12.26 20:31 신고

    항상 잘 읽고 있습니다!
    이번 포스팅 덕분에 긴가 민가 했던 부분이 이해가 잘되었네요!

  4. ChanHHOO 2021.04.01 22:43

    며칠 동안 갓대희님 블로그만 보면서 공부하고 있는데, 예제가 너무 좋은 것 같습니다. 좋은 자료 감사합니다.
    자바를 거의 모르는 상황이라 가끔 잘 이해 안 되는 부분도 있는데 혼자 문제 찾아가면서 해보는 것도 좋네요 ^&^

  5. 음냐옹 2021.04.07 10:15

    안녕하세요. 블로그 잘 보고 있습니다. 혹시 테스트 하실때 쓰신 툴이 뭔지 알수 있을까요? POST, GET 이렇게 바꿔서 테스트 해보려는데 잘 안되서요.

  6. 호잇 2021.06.07 12:16

    JPA에 대한 설명이 잘되어있어서 이해가 쉽게되었습니다.
    혹시 출처를 남기고 네이버 블로그 포스팅을 하여도괜찮을까요?

  7. ㅇㅇ 2021.06.21 19:02

    한글로는 spring data jpa 블로그 치고 최신기법이 드물었는데, 덕분에 큰 도움이 되었습니다. 감사합니다~

  8. Favicon of https://jihogrammer.tistory.com BlogIcon Jihogrammer 2021.07.15 11:19 신고

    감사합니다😊