Spring Boot (JPA를 이용한 DB 생성 및 접근)

최병현·2026년 2월 20일

spring boot

목록 보기
3/34
post-thumbnail

이 내용은 Spring Boot 백엔드에서 Persistence Layer(DB 접근 계층)에 해당한다. 프론트(HTML/JS/React)가 API로 요청하면, 백엔드(Spring Boot)가 Service/Repository를 통해 DB에 접근하고, JPA가 SQL을 직접 작성하지 않아도 Entity 기반으로 테이블/쿼리 생성과 매핑을 수행한다.


1. JPA란?

JPA(Jakarta Persistence API)는 Java에서 데이터 영속성(Persistence)을 표준 방식으로 다루기 위한 API다. 핵심은 “엔티티(Entity) 클래스”를 기준으로 테이블 구조를 정의하고, 객체를 저장/조회하는 방식으로 DB를 다룬다는 점이다.

즉, HeidiSQL에서 직접 DB/TABLE/COLUMN 만들고 SQL로 CRUD하던 방식과 달리, Spring Boot 프로젝트 내부 코드(Entity/Repository)로 DB 구조와 접근을 처리한다.


2. ORM이란?

ORM(Object Relational Mapping)은 객체(Object)와 관계형 DB(Relational)를 매핑(Mapping)해서 객체 조작이 SQL 실행으로 이어지도록 연결하는 기술이다.

  • Java 개발자는 테이블 중심이 아니라 객체 중심으로 개발 가능
  • SQL을 매번 직접 작성하는 양이 줄어 개발 속도/코드량이 감소
  • DBMS마다 다른 dialect 부담이 줄어 이식성이 좋아짐

3. JPA 구현체: Hibernate

JPA는 “표준”이고, 실제 동작은 구현체가 수행한다. Spring Boot에서 가장 널리 쓰이는 구현체가 Hibernate이며, 기본 구현체로 많이 사용된다.


4. Entity 클래스 만들기

@Entity를 붙인 클래스가 “테이블”에 대응된다. 클래스 이름이 테이블 이름이 되고, field 이름이 column 이름으로 매핑된다.

애플리케이션 초기화 시점에 JPA 설정에 따라 Entity 기반으로 테이블 생성이 가능하다. 테이블 이름을 바꾸고 싶으면 @Table(name="...")를 사용한다. 특히 User 같은 예약어 충돌 가능성이 있어 @Table(name="users")처럼 명시하는 패턴이 자주 쓰인다.

4-1. 기본 Entity 예시

package com.korit12.cardatabase.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Car {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String brand, model, color, registrationNumber;

    private int modelYear, price;
}

엔티티에는 기본 키(PK)가 필요하다. DB는 어떤 field가 PK인지 자동으로 알 수 없으므로 @Id로 명시한다.

  • @GeneratedValue: PK 자동 생성 전략 지정 (AUTO_INCREMENT 성격)
  • strategy = AUTO: JPA가 DB에 맞는 전략을 자동 선택하는 기본값 성격
  • 복합키도 가능하지만 보통은 단일 PK를 권장하고, 복합키는 별도 전략(@EmbeddedId 등)을 사용

4-2. 컬럼 설정이 필요할 때

field명 그대로 column이 만들어지는 것이 기본이지만, 컬럼명/길이/nullable 같은 제약을 명시하고 싶으면 @Column을 쓴다.

import jakarta.persistence.Column;

@Column(name = "explanation", nullable = false, length = 512)
private String description;

5. Lombok 적용 시 주의점

id는 자동 생성이므로 생성자에 id까지 포함되는 AllArgsConstructor를 무조건 쓰면 설계가 꼬일 수 있다. 보통은 기본 생성자 + 필요한 필드 생성자 형태를 가져가거나, Lombok의 @NoArgsConstructor와 @RequiredArgsConstructor를 조합한다. JPA는 기본 생성자(no-args constructor)를 요구한다.

5-1. Lombok 미사용 버전

package com.korit12.cardatabase.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;

@Entity
public class Car {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    private String brand, model, color, registrationNumber;

    private int modelYear, price;

    public Car() {}

    public Car(String brand, String model, String color, String registrationNumber, int modelYear, int price) {
        this.brand = brand;
        this.model = model;
        this.color = color;
        this.registrationNumber = registrationNumber;
        this.modelYear = modelYear;
        this.price = price;
    }

    public Long getId() { return id; }

    public String getBrand() { return brand; }
    public void setBrand(String brand) { this.brand = brand; }

    public String getModel() { return model; }
    public void setModel(String model) { this.model = model; }

    public String getColor() { return color; }
    public void setColor(String color) { this.color = color; }

    public String getRegistrationNumber() { return registrationNumber; }
    public void setRegistrationNumber(String registrationNumber) { this.registrationNumber = registrationNumber; }

    public int getModelYear() { return modelYear; }
    public void setModelYear(int modelYear) { this.modelYear = modelYear; }

    public int getPrice() { return price; }
    public void setPrice(int price) { this.price = price; }
}

5-2. Lombok 적용 버전

package com.korit12.cardatabase.domain;

import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

@Entity
@Data
@NoArgsConstructor
@RequiredArgsConstructor
public class Car {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @NonNull
    private String brand, model, color, registrationNumber;

    @NonNull
    private int modelYear, price;
}

6. Repository로 CRUD 만들기

Spring Data JPA는 Repository 인터페이스만 정의하면 기본 CRUD 구현을 자동으로 제공한다. 이 부분이 Persistence Layer에서 “SQL 대신 메서드 호출”로 DB를 다룰 수 있게 해주는 핵심이다.

6-1. CrudRepository / JpaRepository

CrudRepository는 기본 CRUD 메서드를 제공한다. JpaRepository는 CrudRepository + Paging/Sorting 등을 포함하는 상위 종합 패키지로 실무에서 많이 선택한다.

package com.korit12.cardatabase.domain;

import org.springframework.data.repository.CrudRepository;

public interface CarRepository extends CrudRepository<Car, Long> {
}

여기서 <Car, Long>은 Generic으로, Repository가 다룰 Entity 타입과 PK 타입을 명확히 해준다. 그래서 save()에 Car를 넣고, findById()에 Long을 넣는 흐름이 타입 안정적으로 굳어진다.

6-2. CrudRepository 대표 메서드 감각

  • count(): COUNT()
  • findAll(): SELECT *
  • findById(id): SELECT ... WHERE id = ? (Optional 반환)
  • delete(entity): DELETE
  • save(entity): INSERT or UPDATE

Optional은 “없을 수도 있음”을 타입으로 강제해서 NullPointerException을 줄이려는 목적이다.

Optional<Car> opt = repository.findById(1L);
if (opt.isPresent()) {
    Car car = opt.get();
}

7. 더미 데이터 자동 삽입 (CommandLineRunner)

H2 in-memory DB를 쓰면 실행할 때마다 초기화된다. 매번 수동 INSERT가 귀찮으니, 앱 시작 시점에 더미 데이터를 저장하도록 CommandLineRunner를 활용할 수 있다.

이 코드는 Spring Boot 백엔드의 “애플리케이션 부팅 단계”에서 Repository를 통해 데이터를 주입하는 구조다. DI(Dependency Injection)로 CarRepository를 주입받아 run()에서 save()를 호출한다.

package com.korit12.cardatabase;

import com.korit12.cardatabase.domain.Car;
import com.korit12.cardatabase.domain.CarRepository;
import lombok.RequiredArgsConstructor;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.slf4j.Logger;

@SpringBootApplication
@RequiredArgsConstructor
public class CardatabaseApplication implements CommandLineRunner {
    private static final Logger logger = LoggerFactory.getLogger(CardatabaseApplication.class);

    private final CarRepository repository;

    public static void main(String[] args) {
        SpringApplication.run(CardatabaseApplication.class, args);
        logger.info("Application started.");
    }

    @Override
    public void run(String... args) {
        repository.save(new Car("현대", "소나타", "검정", "123가4567", 2026, 30000000));
        repository.save(new Car("기아", "K9", "흰색", "987나5432", 2025, 20000000));
        repository.save(new Car("람보르기니", "쿤타치", "빨강", "159다7532", 2010, 130000000));
    }
}

8. Repository 응용: Query Method / @Query

Spring Data JPA는 메서드 이름 규칙(findBy...)만으로도 WHERE 조건 쿼리를 자동 생성한다. 복잡한 경우에는 @Query로 JPQL을 직접 작성할 수도 있다.

package com.korit12.cardatabase.domain;

import org.springframework.data.jpa.repository.JpaRepository;
import org.springframework.data.jpa.repository.Query;
import java.util.List;

public interface CarRepository extends JpaRepository<Car, Long> {

    List<Car> findByBrand(String brand);

    List<Car> findByColor(String color);

    List<Car> findByModelYear(int modelYear);

    List<Car> findByBrandAndModel(String brand, String model);

    List<Car> findByBrandOrColor(String brand, String color);

    List<Car> findByBrandOrderByModelYearAsc(String brand);

    @Query("select c from Car c where c.model = ?1")
    List<Car> findByModel(String model);

    @Query("select c from Car c where c.brand like %?1")
    List<Car> findByBrandEndWith(String brand);
}

@Query는 강력하지만, 특정 DB에 종속적인 SQL(native query)까지 쓰기 시작하면 이식성이 떨어질 수 있다. 가능하면 Query Method로 해결하고, 정말 필요한 경우에만 @Query를 쓰는 게 안전하다.


9. 테이블 관계 매핑 (1:N)

요구사항: Owner 1명이 Car 여러 대를 가질 수 있고, Car는 Owner 1명만 가진다. 이게 1:N(One-to-Many) 관계다.

  • Car 쪽: @ManyToOne (FK를 가진 쪽)
  • Owner 쪽: @OneToMany (mappedBy로 연관관계 주인 지정)

중요 포인트는 “ownerId 같은 FK 필드를 직접 추가”하는 방식이 아니라, Car 엔티티 안에 Owner 타입을 field로 둬서 객체로 연결한다는 점이다.

9-1. Car.java

package com.korit12.cardatabase.domain;

import jakarta.persistence.*;
import lombok.Data;
import lombok.NoArgsConstructor;
import lombok.NonNull;
import lombok.RequiredArgsConstructor;

@Entity
@Data
@NoArgsConstructor
@RequiredArgsConstructor
public class Car {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    private Long id;

    @NonNull
    private String brand;
    @NonNull
    private String model;
    @NonNull
    private String color;
    @NonNull
    private String registrationNumber;
    @NonNull
    private int modelYear;
    @NonNull
    private int price;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "owner")
    private Owner owner;
}

9-2. Owner.java

package com.korit12.cardatabase.domain;

import jakarta.persistence.*;
import lombok.*;
import java.util.List;

@Entity
@NoArgsConstructor
@RequiredArgsConstructor
public class Owner {
    @Id
    @GeneratedValue(strategy = GenerationType.AUTO)
    @Getter
    private Long ownerId;

    @Getter @Setter @NonNull
    private String firstName;

    @Getter @Setter @NonNull
    private String lastName;

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "owner")
    @Getter @Setter
    private List<Car> cars;
}

10. FetchType과 Cascade 이해

10-1. FetchType

FetchType은 연관 데이터 로딩 전략이다.

  • EAGER: 즉시 로딩 (조회 시점에 연관 데이터까지 바로 가져옴)
  • LAZY: 지연 로딩 (연관 데이터가 실제로 필요할 때 가져옴)

toMany는 기본이 LAZY인 경우가 많고, toOne은 명시하지 않으면 EAGER로 동작하는 케이스가 있어서 예제처럼 toOne 쪽에 fetch=FetchType.LAZY를 습관처럼 명시하는 편이 안전하다.

10-2. CascadeType.ALL

cascade는 “부모 작업이 자식에게 전파되는 범위”다. CascadeType.ALL이면 Owner를 삭제할 때 Owner가 가진 Car들도 함께 삭제될 수 있다. 실무에서는 요구사항에 따라 ALL을 쓰기도 하지만, 삭제 전파는 특히 신중하게 선택한다.


11. 실제 DB에서는 어떤 컬럼이 생기나?

관계 매핑 결과로 Car 테이블에 owner라는 컬럼(FK)이 생기고, 그 값으로 Owner의 PK(owner_id)가 들어간다. 즉, 물리적으로는 PK-FK 관계가 만들어지고, 필요하면 SQL JOIN으로 확인할 수 있다.

// example SQL 
select *
from CAR c
inner join OWNER o
on c.OWNER = o.OWNER_ID;

12. 프론트 ↔ 백엔드 ↔ DB 연결 흐름

최종적으로 웹 앱에서 데이터가 오가는 흐름은 보통 아래처럼 간다.

  • Frontend(React/JS)가 HTTP로 API 요청
  • Controller가 요청을 받고 Service 호출
  • Service가 Repository(JPA) 호출
  • Repository가 Hibernate를 통해 SQL 실행
  • DB 결과가 Entity로 매핑되어 다시 Response로 내려감

이번 글은 그 중에서 “Entity/Repository로 DB 구조를 만들고 접근하는 부분”을 정리한 것이다.


13. 핵심 정리

  • JPA는 Entity 기반으로 테이블을 정의하고 객체 중심으로 CRUD를 수행한다.
  • Spring Data JPA는 Repository 인터페이스만 만들어도 CRUD 구현을 자동 제공한다.
  • Query Method(findBy...)로 WHERE 조건을 자동 생성할 수 있다.
  • 관계 매핑은 FK 필드가 아니라 “객체 참조”로 설계한다.
  • FetchType, Cascade는 성능/삭제 전파에 큰 영향을 주므로 기준을 정하고 적용한다.
profile
Develop

0개의 댓글