내일배움캠프

[내일배움캠프 사전캠프] H2와 JPA

munsik22 2026. 3. 31. 16:48
 

GitHub - qkrwns1478/spring-order

Contribute to qkrwns1478/spring-order development by creating an account on GitHub.

github.com

🧩 H2 Database

H2 데이터베이스는 작고 가벼운 자바용 SQL 데이터베이스이다.

Mode H2 다운로드 실행 주체 DB 저장 위치 사용 용도
Server O 외부 로컬 배포
In-Memory X 스프링 메모리 테스트
Embedded X 스프링 로컬 개발

 

임베디드 모드에서 test라는 이름의 DB를 사용하기 위해서는 아래와 같이 설정해야 한다.

# application.properties
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.url=jdbc:h2:~/test
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console

 

이 때 DB는 사용자의 홈 디렉토리 아래에 test.mv.db 파일로 저장된다.

/h2-console이 404가 뜨는데요?

스프링부트 4.x 버전부터는 build.gradleh2console 의존성을 추가해야지 H2 콘솔을 사용할 수 있다.

runtimeOnly 'com.h2database:h2'
implementation 'org.springframework.boot:spring-boot-h2console' // ← ★이거★

굿


🧩 JPA

ORM(Object-Relational Mapping)은 객체 지향 프로그래밍 언어의 객체와 관계형 데이터베이스(RDBMS)의 테이블을 자동으로 연결하는 기술로, MyBatis로 대표되는 QueryMapper 방식의 DB 의존성 및 중복 쿼리 문제를 해결하기 위해 탄생했다.

 

ORM을 사용하는 가장 쉬운 방법은 JpaRepository를 사용하는 것이다. JPA(Java Persistence API)는 자바 객체와 데이터베이스 테이블을 연결해 주는 표준으로, 자바 클래스 하나를 만들고 @Entity 같은 표시를 붙이면 그걸 DB 테이블처럼 다룰 수 있게 해준다.

 

기존의 Repository는 Repository의 기본 기능만 가진 구현체를 생성하는 반면, JpaRepositorySpringDataJpa에 의해 엔티티의 CRUD, 페이징, 정렬 기능 메소드들(상위 인터페이스들의 기능)을 가진 빈이 등록된다.

상품 CRUD 예제

  • 상품 엔티티
package com.example.springorder.Product;

import jakarta.persistence.Entity;
import jakarta.persistence.Id;
import lombok.Getter;
import lombok.Setter;

@Setter
@Getter
@Entity
public class Product {
    @Id
    private Long id;
    private String name;
    private int price;
    private int quantity;
    
    public Product() {
    }
    
    public Product(Long id, String name, int price, int quantity) {
        this.id = id;
        this.name = name;
        this.price = price;
        this.quantity = quantity;
    }

}

products 테이블을 해결할 수 없다는 메세지가 떴길래 "DB만 만들고 테이블을 안 만들어서 그런가?"라는 생각이 들었다. 일단 H2 콘솔에서 아래 SQL문을 실행시켜 테이블을 만들었다.

CREATE TABLE products (
    id BIGINT PRIMARY KEY,
    name VARCHAR(255),
    price INTEGER,
    quantity INTEGER
);
  • 상품 Repository
package com.example.springorder.Product;

import org.springframework.data.jpa.repository.JpaRepository;

public interface ProductRepository extends JpaRepository<Product,Long> {
}

 

혹시나 클래스 'ProductRepository'은(는) abstract로 선언되거나 'JpaRepository'에서 추상 메서드 'flush()'을(를) 구현해야 합니다.라는 에러가 발생했다면 interface가 아니라 class로 정의하지 않았는지 확인해보자.

  • 상품 Controller
package com.example.springorder.Product;

import org.springframework.web.bind.annotation.*;
import java.util.List;

@RestController
@RequestMapping("/products")
public class ProductController {

    private final ProductRepository productRepository;

    public ProductController(ProductRepository productRepository) {
        this.productRepository = productRepository;
    }

    /* 상품 등록 */
    @PostMapping
    public Product create(@RequestBody Product product) {
        return productRepository.save(product);
    }

    /* 상품 목록 조회 */
    @GetMapping
    public List<Product> findAll() {
        return productRepository.findAll();
    }

    /* 상품 단건 조회 */
    @GetMapping("/{id}")
    public Product findById(@PathVariable Long id) {
        return productRepository.findById(id).get();
    }

    /* 상품 수정 */
    @PutMapping("/{id}")
    public Product update(@PathVariable Long id, @RequestBody Product product) {
        Product new_product = productRepository.findById(id).get();
        new_product.setName(product.getName());
        new_product.setPrice(product.getPrice());
        new_product.setQuantity(product.getQuantity());
        return productRepository.save(new_product);
    }

    /* 상품 삭제 */
    @DeleteMapping("/{id}")
    public void deleteById(@PathVariable Long id) {
        productRepository.deleteById(id);
    }
}
  • API 테스트

Postman으로 POST 요청을 보냈는데 이럴수가 500 에러가 발생했다.

[spring-order] [nio-8080-exec-2] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.orm.jpa.JpaSystemException: Identifier of entity 'com.example.springorder.Product.Product' must be manually assigned before calling 'persist()'] with root cause

 

Stack Overflow에 이미 같은 문제를 다루는 글이 있었다. #

The error "Identifier of entity '...' must be manually assigned before calling 'persist()'" occurs in Spring Data JPA when the primary key field in your entity class is not being automatically generated or assigned before the save operation is attempted.

 

대충 해석하자면 엔티티 클래스의 기본 키 필드가 자동으로 생성되거나 할당되지 않을 때 Spring Data JPA에서 entity '...' 식별자를 수동으로 할당해야한다고 한다.

 

Product 엔티티 파일에 아래 코드를 추가했다.

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY) // ← ★이거★
    private Long id;

 

그랬더니 이번에는 또 다른 에러가 발생했다.

[spring-order] [nio-8080-exec-1] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Request processing failed: org.springframework.dao.DataIntegrityViolationException: could not execute statement [NULL not allowed for column "ID"; SQL statement: insert into products (name,price,quantity,id) values (?,?,?,default) [23502-240]] [insert into products (name,price,quantity,id) values (?,?,?,default)]; SQL [insert into products (name,price,quantity,id) values (?,?,?,default)]; constraint [ID]] with root cause

 

products.id를 자동 생성해 주는 설정이 실제 DB 컬럼에 없는데, JPA/Hibernate는 id를 DB 기본값으로 넣으려고 default를 사용해서 INSERT를 날리고 있다. 그런데 아까 H2 쪽 ID 컬럼은 identity/auto-generated 컬럼으로 설정하지 않아서 기본값이 없고, 결국 NULL not allowed for column "ID"가 발생한 것이다.

 

결국 아까 만든 테이블을 지우고 products.ididentity(자동 증가) 컬럼으로 다시 맞춰서 다시 만들었다.

DROP TABLE products;

CREATE TABLE products (
    id BIGINT GENERATED BY DEFAULT AS IDENTITY PRIMARY KEY,
    name VARCHAR(255) NOT NULL,
    price INT NOT NULL,
    quantity INT NOT NULL
);

드디어 POST 요청이 제대로 응답되었다.

 

저번에 스프링 CRUD를 구현할 때는 이런 에러가 없었던 것 같았는데 왜 이번에는 테이블 생성부터 이런 에러가 발생했는지 찾아보니, 저번에는 테이블을 직접 SQL로 만들지 않고, Hibernate가 엔티티를 보고 스키마까지 만들게 했기 때문이었다. 이런...

  • 예외 처리

예외 처리를 하지 않았기 때문에 GET이나 PUT 등의 요청에서 파라미터로 존재하지 않는 id 값을 입력할 경우 500 에러가 발생했다. 프로그램의 비정상 종료 문제를 방지하기 위해 예외 처리 코드를 추가했다.

수정 전

    /* 상품 단건 조회 */
    @GetMapping("/{id}")
    public Product findById(@PathVariable Long id) {
        return productRepository.findById(id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
    }

    /* 상품 수정 */
    @PutMapping("/{id}")
    public Product update(@PathVariable Long id, @RequestBody Product product) {
        Product new_product = productRepository.findById(id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
        new_product.setName(product.getName());
        new_product.setPrice(product.getPrice());
        new_product.setQuantity(product.getQuantity());
        return productRepository.save(new_product);
    }

    /* 상품 삭제 */
    @DeleteMapping("/{id}")
    public void deleteById(@PathVariable Long id) {
        Product target_product = productRepository.findById(id)
                .orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND));
        productRepository.delete(target_product);
    }

수정 후

 

다만 더 확실한 예외 처리를 위해 전역 예외 핸들러를 추가하는 편이 더 확실할 것으로 보인다.


TO-DO List

  • 테이블 객체끼리 관계 만들기
  • 테이블 객체로 자동 쿼리 생성