본문 바로가기
Study/spring

Spring Data JPA

by 유경호 2020. 6. 16.
반응형

 Spring으로 Web 개발을 할 때에 항상 나는 MyBatis를 활용해 개발해왔다. ORM에 대해서는 몇 번 듣기도 했으나 나는 익숙한 방법을 고집하며 개발을 해왔었던 기억이 난다. 현재 많은 기업들이 ORM을 적극적으로 활용하는 모습이 보여 나도 ORM을 수용해보고자 한다.

 

 먼저 JPA 실습에 들어가기전기본 개념을 간단하게 정리해보자.

 


# ORM이란?

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

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

# JPA란?

 - Java Persistence API의 약자로, 자바 어플리케이션에서 RDBMS를 사용하는 방식을 정의한 인터페이스이다.
 - 즉 특정 기능을 수행하는 라이브러리가 아니라 ORM을 사용하기 위한 인터페이스를 모아둔 것 이라고 볼 수 있다.
 - ORM에 대한 자바 API 규격이며 Hibernate, OpenJPA 등이 JPA를 구현한 구현체이다.

# Hibernate란?

 - JPA라는 명세를 구현한 라이브러리중 하나.

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

 - JPA의 구현체일 뿐이다. 즉 JPA를 사용하기 위해 반드시 하이버네이트만 사용할 필요는 없다. 하이버네이트의 동작 방식이 맘에 들지 않으면 다른 JPA 구현체를 찾아볼 수도 있고 본인이 구현해 사용할 수도 있다.

 

# 그럼 Spring Data JPA는?

 - Spring에서 제공하는 모듈 중 하나로, 개발자가 더 쉽고 편하게 JPA를 사용하도록 해줌.

 - 이는 JPA를 한 단계 추상화시킨 Repository 라는 인터페이스를 제공함으로써 이뤄짐

 - 사용자가 Repository 인터페이스에 정해진 규칙대로 메소드를 입력하면, Spring이 알아서 해당 메소드 이름에 적합한 쿼리를 날리는 구현체를 만들어서 Bean으로 등록해준다.

 - Spring Data JPA를 추상화했다는 말은 Spring Data JPA의 Rpository 구현에서 JPA를 사용하고 있다는 것.


왜 쓸까?

 기존에 MyBaits를 활용하여 개발 시 새로운 프로젝트를 시작할 때나 기존 프로젝트에 새로운 엔티티가 추가될 때마다 전에 사용한 CRUD SQL을 재활용하여 반복적으로 작업하는 경우가 빈번하다. 엔티티가 하나만 추가되어도  DTO, DAO 같은 비지니스 로직을 새로 개발하는 매우 반복적인 작업을 하게 된다. 하여 이러한 불필요한 작업을 최대한 줄이고자 데이터 베이스 중심 설계의 단점을 개선하여 효율적으로 개발할 수 있도록 객체와 테이블을 매핑시켜주는 ORM이 등장 하였고 자바에서는 JPA라는 표준 스펙을 정의하게 되었다.


어떤 장점과 어떤 단점이 있을까?

▶ 1. 장점

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

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

▶ 2. 단점

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

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


실습

▶ 1. Dependency 추가

 - 인메모리 DB(h2 등)를 통해 간단하게 테스트 할 수있다는데, 나는 h2와는 안 친하고 자주 써왔던 MySQL과 연결하여 CRUD 예제를 수행하겠다.

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
        <scope>runtime</scope>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
        <scope>test</scope>
        <exclusions>
            <exclusion>
                <groupId>org.junit.vintage</groupId>
                <artifactId>junit-vintage-engine</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-jpa</artifactId>
    </dependency>
</dependencies>
cs

 

▶ 2. Entity 클래스 생성 

 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
package com.ykh.study.jpatest.model.member;
 
import lombok.*;
import javax.persistence.*;
 
 
@Data
@AllArgsConstructor
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity(name = "member")
public class MemberVO {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private long memberNo;
    private String id;
    private String name;
    
    @Builder
    public MemberVO(String id, String name) {
        super();
        this.id = id;
        this.name = name;
    }
}
 
cs

 

 

- MySQL Sample 테이블 생성 script 예시

 

1
2
3
4
5
6
CREATE TABLE `spring-boot-jpa-test`.`member` (
  `MEMBER_NO` INT NOT NULL AUTO_INCREMENT,
  `ID` VARCHAR(200),
  `NAME` VARCHAR(200),
  PRIMARY KEY (`MEMBER_NO`));
 
cs

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

 - memberNo필드는 @id 어노테이션을 사용하여 기본키(PK)로 지정한다.
 - Table 생성시 해당 필드를 PK, AUTO_INCREMENT로 설정하였기때문에 직접할당 방식이 아닌, 자동으로 생성되도록 하기위해 @GeneratedValue를 사용한다.
 - GenerationType.IDENTITY는 기본 키 생성을 데이터베이스에 위임하는 방식이다.
 - @GeneratedValue는 여러 strategy가 있다.

 

▶ 3. Repository 클래스 생성 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
package com.ykh.study.jpatest.repository;
 
import java.util.List;
 
import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.stereotype.Repository;
 
import com.ykh.study.jpatest.model.member.MemberVO;
 
@Repository
public interface MemberRepository extends JpaRepository<MemberVO, Long> {
    
    public List<MemberVO> findById(String id);
    
    public List<MemberVO> findByName(String name);
    
    public List<MemberVO> findByNameLike(String id);
}
 
cs

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

 - 메소드 작성 시 findBy 뒤에 칼럼명을 붙여주면 이를 이용한 검색이 가능하다.

 - findByNameLike 메소드처럼 만들면 like 검색도 가능함.

 - 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>

▶ 4. Service 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
package com.ykh.study.jpatest.model.member;
 
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;
 
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
 
import com.ykh.study.jpatest.repository.MemberRepository;
 
@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 memberNo) {
        Optional<MemberVO> member = memberRepository.findById(memberNo);
        return member;
    }
    
    public void deleteById(Long memberNo) {
        memberRepository.deleteById(memberNo);
    }
    
    public MemberVO save(MemberVO member) {
        memberRepository.save(member);
        return member;
    }
    
    // save 시에 기존의 Id를 가진 개체가 있다면 대체함
    public void updateById(Long memberNo, MemberVO member) {
        Optional<MemberVO> e = memberRepository.findById(memberNo);
        
        if (e.isPresent()) {
            e.get().setMemberNo(member.getMemberNo());
            e.get().setId(member.getId());
            e.get().setName(member.getName());
            memberRepository.save(e.get());
        }
    }
}
 
cs

 - Repository를 활용하여 데이터를 검색하게 되면 복수의 데이터 검색은 List 단수의 데이터 검색은 Optional로 return한다.

- updateById 메소드가 동작하는 방식은 Repository를 통해 save할 때, 해당 Id를 가진 기존의 개체가 있다면 기존 개체에 데이터를 Update해준다.

 

▶ 5. Controller 생성 

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
package com.ykh.study.jpatest.controller;
 
import java.util.List;
import java.util.Optional;
 
import javax.servlet.http.HttpServletRequest;
 
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.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
 
import com.ykh.study.jpatest.model.member.MemberService;
import com.ykh.study.jpatest.model.member.MemberVO;
 
@RestController
@RequestMapping("memberTest")
public class TestJpaRestController {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());
    
    @Autowired
    MemberService memberService;
    
    /**
     * 모든 회원 조회 Controller
     */
    @GetMapping(produces = { MediaType.APPLICATION_JSON_VALUE })
    public ResponseEntity<List<MemberVO>> getAllMember() {
        List<MemberVO> member = memberService.findAll();
        return new ResponseEntity<List<MemberVO>>(member, HttpStatus.OK);
    }
    
    /**
     * 회원번호로 한 명의 회원 조회 Controller
     */
    @GetMapping(value = "/{memberNo}", produces = { MediaType.APPLICATION_JSON_VALUE })
    public ResponseEntity<MemberVO> getMember(@PathVariable("memberNo") Long memberNo){
        Optional<MemberVO> member = memberService.findById(memberNo);
        return new ResponseEntity<MemberVO>(member.get(), HttpStatus.OK);
    }
    
    /**
     * 회원번호로 회원 삭제 Controller
     */
    @DeleteMapping(value = "/{memberNo}", produces = { MediaType.APPLICATION_JSON_VALUE })
    public ResponseEntity<Void> deleteMember(@PathVariable("memberNo") Long memberNo){
        memberService.deleteById(memberNo);
        return new ResponseEntity<Void>(HttpStatus.NO_CONTENT);
    }
    
    /**
     * 회원번호로 한 명의 회원 조회 Controller
     * (memberNo로 회원을 찾아 member 객체의 id, name로 수정함)
     */
    @PutMapping(value = "/{memberNo}", produces = { MediaType.APPLICATION_JSON_VALUE })
    public ResponseEntity<MemberVO> updateMember(@PathVariable("memberNo") Long memberNo, MemberVO member){
        memberService.updateById(memberNo, member);
        return new ResponseEntity<MemberVO>(member, HttpStatus.OK);
    }
    
    /**
     * 회원 입력 Controller
     */
    @PostMapping
    public ResponseEntity<MemberVO> save(MemberVO member){
        return new ResponseEntity<MemberVO>(memberService.save(member), HttpStatus.OK);
    }
    
    /**
     * 회원 입력 Controller
     */
    @RequestMapping(value = "/saveMember", method = RequestMethod.GET)
    public ResponseEntity<MemberVO> save(HttpServletRequest req, MemberVO member){
        return new ResponseEntity<MemberVO>(memberService.save(member), HttpStatus.OK);
    }
}
 
cs

▶ 6. Properties 설정

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
#JPA 설정
#Dialect 설정
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQLDialect
#하이버네이트가 실행하는 모든 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
 
#Datasource 설정
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/schema?serverTimezone=UTC&characterEncoding=UTF-8
spring.datasource.username=
spring.datasource.password=
cs

 - 나는 MySQL을 DB로 사용했기 때문에 위와 같은 Datasource 설정을 해줬다. DB가 다르다면 다르게 설정해야한다.

 - 또한 DB 마다 Dialect도 다른데 해당 내용은 https://jaeman1990.tistory.com/11를 참고할 것


테스트

▶ 1. 회원 저장

요청 예시

 

요청 결과

 

 - POST로 id, name 각각 값을 담아 요청한다. (kyeongho, kyeongho2 2개의 아이디로 요청함)

 

▶ 2. 전체 조회

전체 조회 예시

 - 전체 조회 요청 시 앞서 요청한 두 개의 회원이 조회됨.

 

▶ 3. 특정 회원 조회 

특정 회원 조회

 - 전체 조회 요청 url 뒤에 memberNo을 붙여 특정 회원을 조회할 수 있다.

   http://localhost:8080/memberTest/{memberNo}

 

▶ 4. 특정 회원 삭제 

회원 삭제
삭제 결과

 - memberNo을 붙여 Delete 요청을 하면 해당 회원을 삭제할 수 있다.

 - 2번 회원을 삭제한 뒤 2번 회원을 조회하면 에러가 뜨면서 삭제된 것을 확인함.

 

▶ 5. 특정 회원 수정

특정 회원 수정 요청 예시
수정된 회원 조회

 - 수정하고자 하는 회원의 memberNo와 수정할 내용을 id, name에 담아 요청함

 - 3번 회원을 수정한 후 조회하니 수정된 것을 확인함.

저장소

https://github.com/KyeonghoYoo/spring-boot-jpa

참고

[스프링부트 (7)] Spring Boot JPA(1) - 시작 및 기본 설정
https://goddaehee.tistory.com/209 [갓대희의 작은공간]
JPA, Hibernate, 그리고 Spring Data JPA의 차이점
https://suhwan.dev/2019/02/24/jpa-vs-hibernate-vs-spring-data-jpa

 

반응형