본문 바로가기

Back-end & Server/Spring Boot with Kotlin

[Spring Boot] JPA(Java Persistence API)

728x90
반응형

JPA 개요

JPA(Java Persistence API)Java ORM(Object-Relational Mapping) 기술에 대한 표준 명세로, 자바에서 제공하는 API 중 하나이다.

ORM은 객체 지향 프로그래밍 언어를 사용하여 비즈니스 객체를 관계형 데이터베이스의 데이터로 매핑하는 프로그래밍 기법이다.

 

주요기능

 

1. ORM 지원

  • JPA는 RDBMS의 데이터를 객체 지향적으로 관리할 수 있게 해주는 ORM 기술을 제공한다.
  • 객체와 RDB 사이의 패러다임 불일치 문제를 해결하는 역할을 한다.

 

2. 쿼리 언어 제공

  • JPA는 JPQL(Java Persistence Query Language)이라는 쿼리 언어를 제공한다.
  • JPQL은 SQL과 유사하면서, 엔티티 객체를 대상으로 쿼리를 작성할 수 있게 해준다.

 

3. 데이터베이스 벤더 독립성

  • JPA는 DB 벤더에 독립적인 코딩을 가능하게 한다.
  • 한 종류의 DBMS에서 다른 종류의 DBMS로 변경해도 코드를 거의 수정하지 않고 그대로 사용할 수 있다.

 

4. CRUD 기능 제공

  • JPA가 제공하는 Repository interface에는 기본적인 CRUD 연산 메소드가 이미 정의되어 있다.
  • 개발자는 복잡한 SQL 쿼리 없이도 이런 기능들을 구현할 수 있다.

 

 

JPA 구현체

 

JPA는 인터페이스이고 실제 동작 부분은 JPA의 구현체에 달려있다.

  • JPA는 표준 스펙을 제공
  • 구현체는 실제 동작을 담당

 

JPA 구현체 종류

 

1. Hibernate 

  • 가장 널리 사용되는 구현체, 풍부한 기능과 성능 최적화 기능을 제공.
  • JPA 외에도 Criteria Query나 HQL(Hibernate Query Language)같은 자체 쿼리 언어 제공.

 

2. EclipseLink

  • JPA 2.0 스펙을 만든 회사에 만든 구현체, 다양한 DB와 잘 호환되며 부가적인 기능을 많이 제공.

 

3. OpenJPA

  • Apache에서 만든 오픈소스 JPA 구현체.
  • 엔터프라이즈 환경에 적합한 특성을 가지고 있다.

 

4. DataNucleus

  • JDO(Java Data Objects)와 JPA를 모두 지원하는 구현체.
  • 객체 저장소와 관계형 데이터베이스 둘 다 지원.

 

 

Hibernate 사용 예시

이전 포스트 DTO, DAO에서 사용한 코드들이 Hibernate를 사용한 코드이다.

 

코드별로 상세하게 설명해보겠다.

// Entity
package com.springboot.kotlinexample.entity

// spring boot 3에서는 Jakarta EE 9가 포함됨에 따라 javax 관련 패키지명이 jakarta로 변경
import jakarta.persistence.Entity
import jakarta.persistence.GeneratedValue
import jakarta.persistence.GenerationType
import jakarta.persistence.Id
import jakarta.persistence.Table
import jakarta.persistence.Column


@Entity
@Table(name="`user`")
data class User(
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    var id: Long? = null,
    @Column(nullable = false)
    var name: String="",
    @Column(nullable = false)
    var email: String=""
)

 

@Entity : 이 클래스가 JPA의 Entity임을 나타낸다. 이를 통해 Hibernate가 이 클래스와 DB 테이블을 매핑하게 됨.

@Table : 이 클래스가 엔티티가 매핑될 테이블을 지정.(name을 사용하여 테이블 이름지정, 지정안할 경우 클래스 이름이 이름으로 사용)

@Id : 이 필드가 테이블의 기본 키임을 나타냄.

@GeneratedValue : 키값이 자동으로 생성되어야 함을 나타냄.

@Column : 이 필드가 테이블의 컬럼임을 나타냄.(name으로 컬럼 이름지정, 지정안할 경우 필드 이름이 컬럼 이름으로 사용)

 

 

// DAO(JPA)
package com.springboot.kotlinexample.dao

import com.springboot.kotlinexample.entity.User
import org.springframework.data.jpa.repository.JpaRepository

interface UserRepository : JpaRepository<User, Long>

 

JpaRepository interface를 상속받아 UserRepository interface를 선언하였음.

JpaRepository는 CRUD 작업을 위한 여러 메소드를 제공하며, 이를 통해 Hibernate를 사용하여 쉽게 DB 작업을 수행할 수 있음.

 

 

package com.springboot.kotlinexample.service

import com.springboot.kotlinexample.dto.UserDto
import com.springboot.kotlinexample.dao.UserRepository
import com.springboot.kotlinexample.entity.User
import org.springframework.stereotype.Service
import org.springframework.transaction.annotation.Transactional

interface UserService {
    fun findAllUsers(): List<UserDto>
    fun addUser(userDto: UserDto): UserDto
    fun updateUser(id: Long, userDto: UserDto): UserDto
    fun deleteUser(id: Long)
}

@Service
class UserServiceImpl(private val userRepository: UserRepository) : UserService {
    @Transactional(readOnly = true)
    override fun findAllUsers(): List<UserDto> = userRepository.findAll().map { UserDto(it.name, it.email) }

    @Transactional
    override fun addUser(userDto: UserDto): UserDto {
        val user = userRepository.save(User(name = userDto.name, email = userDto.email))
        return UserDto(user.name, user.email)
    }

    @Transactional
    override fun updateUser(id: Long, userDto: UserDto): UserDto {
        val user = userRepository.findById(id).orElseThrow { RuntimeException("User not found") }
        user.name = userDto.name
        user.email = userDto.email
        val updatedUser = userRepository.save(user)
        return UserDto(updatedUser.name, updatedUser.email)
    }

    @Transactional
    override fun deleteUser(id: Long) {
        userRepository.deleteById(id)
    }
}

 

@Service : 이 클래스가 Spring의 서비스 반임을 나타냄.

@Transactinal : 이 메소드가 DB의 트랜잭션에서 실행되어야 함을 나타냄.(readOnly=true ->  read-only 트랜잭션에서 실행)

.findAll() : 모든 엔티티를 조회(DTO를 Mapping하여 DTO 형식으로 조회)

.save(저장할 엔티티 인스턴스) : 엔티티를 저장.

.findById(id 값) : id 값을 가진 엔티티를 조회.

.orElseThrow{ RuntimeException("User not found") } : 없을 시 에러 발생.

.deleteById() : id에 해당하는 엔티티 삭제.

 

package com.springboot.kotlinexample.controller

import org.springframework.web.bind.annotation.GetMapping
import org.springframework.web.bind.annotation.RestController
import org.springframework.web.bind.annotation.RequestMapping
import org.springframework.web.bind.annotation.PostMapping
import org.springframework.web.bind.annotation.PutMapping
import org.springframework.web.bind.annotation.DeleteMapping
import org.springframework.web.bind.annotation.RequestBody
import org.springframework.web.bind.annotation.PathVariable



// dto
import com.springboot.kotlinexample.dto.UserDto
// service
import com.springboot.kotlinexample.service.UserService

@RestController
@RequestMapping("/users")
class UserController(private val userService: UserService) {

    @GetMapping
    fun getAllUsers(): List<UserDto> = userService.findAllUsers()

    @PostMapping
    fun addUser(@RequestBody userDto: UserDto): UserDto = userService.addUser(userDto)

    @PutMapping("/{id}")
    fun updateUser(@PathVariable id: Long, @RequestBody userDto: UserDto): UserDto = userService.updateUser(id, userDto)

    @DeleteMapping("/{id}")
    fun deleteUser(@PathVariable id: Long) = userService.deleteUser(id)
}

 

@PostMapping : HTTP POST 요청을 해당 메소드에 매핑한다.(새로운 리소스 생성, 서버 상태 변경)

@PutMapping : HTTP PUT 요청을 해당 메소드에 매핑한다.(기존 리소스 갱신, 수정)

@DeleteMapping : HTTP DELETE 요청을 해당 메소드에 매핑한다.(특정 리소스 삭제)

@PathVariable : URI 경로의 일부를 변수로 캡처하고 이를 메소드 파라미터에 바인딩한다.(/users/{id} -> id를 캡쳐하여 전달)

@RequestBody : HTTP 요청 본문의 내용을 Java 객체로 변환하고 이를 메소드 파라미터에 바인딩한다.(클라이언트가 전송한 JSON 데이터 등을 객체로 변환하여 처리할 수 있다.)

 

엔드포인트 테스트

 

1. 모든 사용자 조회

curl -X GET http://localhost:8080/users

 

2. 새로운 사용자 추가

curl -X POST -H "Content-Type: application/json" -d '{"name":"newUser", "email":"newUser@email.com"}' http://localhost:8080/users

 

3. 기존 사용자 갱신

curl -X PUT -H "Content-Type: application/json" -d '{"name":"updatedUser", "email":"updatedUser@email.com"}' http://localhost:8080/users/{2}

 

4. 기존 사용자 삭제

curl -X DELETE http://localhost:8080/users/{4}

 

 

이렇게 JPA Hibernate를 사용한 CRUD Spring Boot API Server를 만들고 테스트 해보았다.

 

 

관계 매핑

엔티티 간의 관계를 매핑하기 위한 여러가지 어노테이션을 제공한다.

DB의 외래 키 제약 조건과 같은 관계를 객체 모델에 매핑하도록 돕는다.

 

@OneToOne : 1:1 관계를 매핑한다. 한 엔티티가 다른 엔티티를 하나만 가질 수 있음을 나타낸다.

@OneToMany : 1:N 관계를 매핑한다. 한 엔티티가 다른 엔티티 여러 개를 가질 수 있음을 나타낸다.

@ManyToOne : N:1 관계를 매핑한다. 여러 엔티티가 같은 하나의 엔티티를 가질 수 있음을 나타낸다.

@ManyToMany : M:N 관계를 매핑한다. 한 엔티티가 다른 여러 엔티티를 가지고, 그 엔티티도 다시 여러 엔티티를 가질 수 있음을 나타낸다.

@JoinColumn : 외래 키 칼럼을 지정, name 속성으로 칼럼 이름을 지정하고, referencedColumnName 속성으로 참조하는 칼럼 이름을 지정할 수 있다.

 

예시

@Entity
@Table(name = "company")
public class Company {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @OneToMany(mappedBy = "company")
    private List<Employee> employees;

    // getters and setters ...
}

@Entity
@Table(name = "employee")
public class Employee {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "company_id")
    private Company company;

    // getters and setters ...
}

 

Company 엔티티는 여러 Employee 엔티티를 가질 수 있고, Employee 엔티티는 하나의 Company 엔티티를 가질 수 있다.

@JoinColumn을 사용하여 Employee 테이블의 company_id 컬럼을 외래 키로 지정하였다.

 

 

쿼리 메소드

메서드 이름을 분석하여 쿼리를 생성하는 기능을 제공하는데 이를 "쿼리 메소드"라고 부른다.

 

예를 들어 다음과 같은 UserRepository 인터페이스가 있다고 가정할 때.

public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByName(String name);
    User findByEmail(String email);
    List<User> findByNameAndEmail(String name, String email);
}

 

findByName(String name) : 이름을 기준으로 사용자를 찾는 쿼리 생성, 반환 타입이 리스트이므로 해당하는 모든 사용자 반환.

findByEmail(String email) : 이메일을 기준으로 사용자를 찾는 쿼리 생성, 반환 타입이 단일 객체이므로 일치하는 사용자 한명 반환.

findByNameAndEmail(String name,String email) : 이름과 이메일을 모두 기준으로 사용자를 찾는 쿼리 생성, 이름과 이메일이 모두 일치하는 사용자를 반환.

 

이렇게 쿼리 메소드는 간단한 작업에는 편리하지만 복잡한 작업의 경우 @Query 어노테이션을 사용하여 직접 쿼리를 작성할 수 도 있다.

// JPQL
public interface UserRepository extends JpaRepository<User, Long> {
    @Query("SELECT u FROM User u WHERE u.email LIKE %?1%")
    List<User> findByEmailContains(String email);
}

// Native SQL Query
public interface UserRepository extends JpaRepository<User, Long> {
    @Query(value = "SELECT * FROM users WHERE email LIKE %?1%", nativeQuery = true)
    List<User> findByEmailContains(String email);
}

 

이렇게 @Query를 사용하면 메소드 이름으로 쿼리를 유추하는 것이 아니라, 직접 SQL 또는 JPQL(JPA Query Language)로 쿼리를 작성할 수 있다.

또 Native SQL Query를 사용하려면 nativeQuery에 true로 설정하면 된다.

728x90
반응형