스프링부트

[SpringBoot][DB] JPA (이용 배경, 개념, table과 객체 맵핑 실습)

민석삼 2025. 3. 9. 17:48

JPA 이용 배경

JdbcTemplate을 사용해서, 개발자가 문자열로 저장한 sql을 JdbcTemplate을 이용하여 데이터베이스에 던져주는 식으로 진행했을 때의 한계

1. 문자열을 개발자가 직접 작성: 실수할 수 있고, 실수 인지 시점이 느림

문자열 내부에서 나는 이 오류는 컴파일 시점이 아닌 런타임 시점에 잡히기 때문.

*런타임 시점 오류와 컴파일 시점 오류에 대해서는 아래 글을 참고하세요

https://mim-doremi.tistory.com/54

2. 특정 데이터베이스에 종속적이게 됨

데이터베이스를 변경하고자 할 때, 작성했던 레포지토리 클래스를 하나하나 찾아가서 바꿔줘야 함.

3. 반복 작업이 많아짐

테이블 하나 생성 시 기본적으로 CRUD가 항상 필요한데, 이 쿼리들을 항상 작성해줘야 한다.

4. 데이터베이스의 테이블과 객체는 패러다임이 다르다.

객체: 각 클래스들이 양방향으로 서로를 가리킬 수 있다

테이블: 하나의 테이블만 하나의 테이블을 가리킬 수 있고, 양방향은 불가능하다.

 

JPA (Java Persistence API)

자바 진영의 ORM(Object-Relational Mapping)

데이터를 영구적으로 보관하기 위해 Java 진영에서 정해진 규칙

*ORM: 테이블에 있는 정보와 객체에 있는 정보를 짝짓는 도구

객체와 관계형 DB의 테이블을 짝지어 데이터를 영구적으로 저장할 수 있도록 정해진 Java 진영의 규칙

 

JPA는 규칙이고, Hibernate가 규칙을 구현한 코드(구현체)

Hibernate는 내부적으로 jdbc를 사용한다.

 

JPA를 사용하기 전에 추가적인 설정을 해줘야 함.

DB를 최초에 연결할 때 했던 설정인 application.yml 파일에 추가 설정을 해줄 것

spring:
  jpa:
    hibernate:
      ddl_auto: none
    properties:
      hibernate:
        show_sql: true
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect
spring:
  jpa:
    hibernate:
      ddl_auto: none

Hibernate가 DDL(Data Definition Language)를 자동 실행하는 방식을 설정

스프링이 시작할 때 DB에 있는 테이블을 어떻게 처리해주는 지 결정.

스프링이 시작할 때 DB의 table과 객체의 매핑이 다르면(사용자의 코드 실수 등으로) 어떻게 할것인가?

create 기존 테이블이 있다면 삭제 후 다시 생성
create-drop 스프링이 종료될 때 테이블을 모두 제거
update 객체와 테이블이 다른 부분만 변경
validate 객체와 테이블이 동일한지 확인하여, 실패한 경우 서버 종료
none 별다른 조치를 하지 않는다

*create, create-drop의 경우, 우리가 서버를 시작할 때마다 테이블이 다 지워지고 테이블 내의 유저 정보도 다 사라짐.

더보기

나의 의문.. 그럼 메모리에 저장하는 거랑 마찬가진데, 그런 create와 create-drop을 왜 쓰지?

-실제 서비스에서는 사용하면 안되지만, 특정한 상황에서는 유용하게 사용된다.

개발 초반, 엔티티 설계를 자주 변경할 때

test code 작성할 때

실험용 프로젝트 개발 시

    properties:
      hibernate:
        show_sql: true
        format_sql: true
        dialect: org.hibernate.dialect.MySQL8Dialect

show_sql: JPA를 사용해 DB에 SQL을 날릴 때 SQL을 보여줄 지 말지 결정

format_sql: sql을 보여줄 때 사람이 보기좋게 예쁘게 포맷팅 할것인지 결정

dialect: (직역: 방언 혹은 사투리) 이 옵션으로 DB를 특정하면, 데이터베이스마다 조금씩 다른 SQL 문법 등을 수정해준다.

JPA를 사용해서 테이블을 객체와 맵핑하기

Java 객체와 MySQL Table을 맵핑해보자

 

사용자의 이름, 나이 정보를 담은 User 객체를, id, 이름, 나이 필드를 가진 MySQL의 user 테이블에 맵핑해보자

public class User {

    private String name;
    private Integer age;
    
    public User(String name, Integer age) {
        if (name == null || name.isBlank()) {
            throw new IllegalArgumentException(String.format("잘못된 name(%s)이 들어왔습니다", name));
        }
        this.name = name;
        this.age = age;
    }

    public Long getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public Integer getAge() {
        return age;
    }

    public void updateName(String name) {
        this.name = name;
    }
}

User 객체

user table

 

@Entity

User 객체 위에 @Entity 어노테이션을 붙이면, 스프링이 User 객체와 user 테이블을 같은 것으로 바라보게 함

Entity란 저장되고 관리되어야 하는 데이터를 의미함 

@Entity
public class User {

 

클래스에 빠져있는 id를 추가해주자

User 객체의 필드: name, age

user table의 필드: id, name, age

table의 Id는, bigint로 선언하고 부가 조건에 auto_increment를 걸어준 primary key

 @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id = null;

bigint는 자바에서 long이니까 long으로 선언

Long으로 선언한 이유: long으로 선언하면, long은 null을 가질 수 없는 값이기에 기본값이 null이 아닌 0으로 설정됨

-> JPA가 id가 0이라고 인식해서, 새 엔티티인지 기존 엔티티인지 헷갈릴 수도 있음. 

-> 반면 Long id는 초기값이 null이라서, JPA가 새 엔티티라는 걸 확실히 알 수 있다.

@Id

이 필드를 primary key로 간주하는 어노테이션

선언하고, @Id 어노테이션을 붙여 이 필드는 id야 라고 알려주기

javax를 사용해야 함.

@GeneratedValue(strategy = GenerationType.IDENTITY)

테이블의 id가 auto_increment인 걸 구현해준 것

@GeneratedValue: primary key를 자동 생성하는 값

IDENTITY: MySQL의 auto_increment

 

JPA 객체 (Entity 객체)에는, 매개변수가 하나도 없는 기본 생성자가 꼭 필요함

이 기본 생성자는 protected여도 상관 없음: 엔티티 객체는 JPA가 관리하는 게 원칙이기 때문에, protected로 설정하여 jPA로는 접근 가능하지만 외부에서 기본생성자로 생성하는 것은 방지.

    protected User() {}

 

다른 필드들을 맵핑해보자

@Column: 객체의 필드와 Table의 필드를 맵핑

 

user table에서 name은 varchar(20)으로 선언되고 null이 될 수 없는 값

    @Column(nullable = false, length = 20, name = "name") //name varchar(20)
    private String name;

nullable: 이름은 널이 될 수 없으니까, false

length = 20: 스트링의 길이 제한 (varchar(20))

DB에서의 column 이름을 객체의 필드 이름과 맵핑해줌

name = "name": 이 필드 이름이 테이블의 "name"필드와 같은 것임을 보여줌

클래스의 필드 이름과 테이블의 필드 이름이 같다면, name = "name"을 생략할 수 있다. (다르다면 꼭 써줘야 한다)

 

사실 @Column은 생략 가능

age의 경우, user 객체의 age와 user table의 age와 속성이 완전히 동일. 이런 경우 Column은 생략 가능!

    private Integer age;

age는 table에서 null이 들어와도 상관 없는 int로 선언해준 값이고, DB의 int와 객체의 Integer는 완전히 동일

이렇게 완전히 동일한 경우에는 @Column 어노테이션을 사용하지 않아도 괜찮다.