이전 글에서는 H2 데이터베이스를 연결하고, JdbcTemplate을 사용해서 직접 SQL을 작성해 회원을 저장하고 조회했습니다.
예를 들면 이런 SQL을 사용했습니다.
INSERT INTO members(name) VALUES (?)
그리고 조회할 때도 이런 SQL을 작성했습니다.
SELECT id, name FROM members ORDER BY id
이 방식은 DB 흐름을 이해하기에 좋습니다.
하지만 기능이 많아지면 SQL을 계속 직접 작성해야 해서 코드가 길어질 수 있습니다.
이번 글에서는 JPA를 사용해서 SQL을 직접 많이 작성하지 않고, 객체 중심으로 데이터를 저장하고 조회하는 흐름을 정리해보겠습니다.
이번 글의 목표는 딱 하나입니다.
SQL을 직접 많이 쓰지 않아도 저장과 조회가 되는 흐름 이해하기
JPA는 무엇일까?
JPA는 Java 객체와 데이터베이스 테이블을 연결해주는 기술입니다.
처음에는 어렵게 생각하지 않아도 됩니다.
쉽게 말하면, JPA는 Java 객체를 DB에 저장하고 다시 꺼내올 수 있게 도와주는 도구입니다.
예를 들어 Java에서는 회원을 이렇게 객체로 다룹니다.
Member member = new Member("kim");
DB에서는 회원이 보통 테이블에 저장됩니다.
members 테이블
JPA는 이 둘을 연결해줍니다.
Java 객체 Member
↕
DB 테이블 members
즉, JPA를 사용하면 SQL을 매번 직접 작성하지 않아도 객체를 저장하고 조회할 수 있습니다.
이번 글에서 사용할 흐름
이번 글에서는 아주 단순한 회원 기능을 다시 만들어보겠습니다.
- 회원 객체 만들기
- JPA Entity로 표시하기
- Repository 만들기
- 회원 저장하기
- 회원 조회하기
전체 흐름은 이렇습니다.
브라우저 요청
↓
Controller
↓
Service
↓
JpaRepository
↓
Database
이전 글과 다른 점은 Repository입니다.
이전에는 Repository 안에서 SQL을 직접 작성했습니다.
이번에는 JpaRepository를 사용해서 기본 저장과 조회 기능을 가져옵니다.
1. 필요한 의존성 추가하기
JPA를 사용하려면 Spring Data JPA 의존성이 필요합니다.
build.gradle에 아래 의존성이 있는지 확인합니다.
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
}
각 의존성의 역할은 다음과 같습니다.
| 의존성 | 역할 |
|---|---|
spring-boot-starter-web | 웹 요청 처리 |
spring-boot-starter-data-jpa | JPA 사용 |
h2 | 학습용 데이터베이스 |
처음에는 이렇게 기억하면 됩니다.
web = 요청 받기
jpa = 객체를 DB와 연결하기
h2 = 사용할 DB
2. DB 설정하기
application.properties 파일에 DB와 JPA 설정을 추가합니다.
파일 위치는 아래와 같습니다.
src/main/resources/application.properties
spring.datasource.url=jdbc:h2:file:./data/jpa-demo-db
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
설정 하나씩 보기
1. DB 연결 주소
spring.datasource.url=jdbc:h2:file:./data/jpa-demo-db
H2 데이터베이스를 파일 형태로 사용하겠다는 뜻입니다.
이전 글과 비슷하게, 프로젝트 안의 data 폴더에 DB 파일이 만들어질 수 있습니다.
2. H2 콘솔 켜기
spring.h2.console.enabled=true
spring.h2.console.path=/h2-console
브라우저에서 H2 DB를 확인할 수 있게 해주는 설정입니다.
Spring Boot 실행 후 아래 주소로 접속할 수 있습니다.
http://localhost:8080/h2-console
3. 테이블 자동 생성 설정
spring.jpa.hibernate.ddl-auto=update
이 설정은 Entity 정보를 보고 DB 테이블을 자동으로 맞춰주도록 도와줍니다.
학습할 때는 편하지만, 실제 운영 서비스에서는 주의해서 사용해야 합니다.
처음에는 이렇게만 이해하면 됩니다.
Entity를 보고 테이블을 자동으로 준비해준다.
4. 실행되는 SQL 보기
spring.jpa.show-sql=true
JPA가 내부에서 실행하는 SQL을 콘솔에 보여주는 설정입니다.
JPA를 쓰면 SQL을 직접 많이 작성하지 않지만, 실제로 DB에는 SQL이 실행됩니다.
이 설정을 켜두면 JPA가 어떤 SQL을 실행하는지 확인할 수 있습니다.
3. Entity 만들기
이제 회원 객체를 JPA Entity로 만들어보겠습니다.
Member.java 파일을 만듭니다.
src/main/java/com/example/demo/Member.java
package com.example.demo;
import jakarta.persistence.Entity;
import jakarta.persistence.GeneratedValue;
import jakarta.persistence.GenerationType;
import jakarta.persistence.Id;
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
protected Member() {
}
public Member(String name) {
this.name = name;
}
public Long getId() {
return id;
}
public String getName() {
return name;
}
}
Entity란?
Entity는 DB 테이블과 연결될 Java 객체입니다.
아래 어노테이션이 붙으면 JPA가 이 클래스를 DB와 연결할 대상으로 인식합니다.
@Entity
즉, Member 클래스는 단순한 Java 클래스가 아니라 DB에 저장될 수 있는 객체가 됩니다.
처음에는 이렇게 이해하면 됩니다.
Entity = DB 테이블과 연결되는 Java 객체
@Id는 무엇일까?
@Id
private Long id;
@Id는 이 필드가 기본 키라는 뜻입니다.
기본 키는 데이터를 구분하는 고유한 값입니다.
회원 데이터가 여러 개 있을 때, 이름이 같을 수도 있습니다.
예를 들어 이름이 kim인 회원이 두 명 있을 수 있습니다.
그래서 각 회원을 확실히 구분할 값이 필요합니다.
그 역할을 하는 것이 id입니다.
id = 회원을 구분하는 번호
@GeneratedValue는 무엇일까?
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@GeneratedValue는 id 값을 자동으로 만들어달라는 뜻입니다.
즉, 회원을 저장할 때 우리가 직접 id를 넣지 않아도 DB가 번호를 만들어줍니다.
예를 들어 회원을 저장하면 이런 식으로 번호가 붙습니다.
| id | name |
|---|---|
| 1 | kim |
| 2 | park |
처음에는 이렇게 이해하면 됩니다.
@GeneratedValue = id 번호를 자동으로 만들어주는 설정
기본 생성자는 왜 필요할까?
Entity 안에는 아래 코드가 있습니다.
protected Member() {
}
처음 보면 이 생성자가 이상해 보일 수 있습니다.
우리가 직접 쓰는 생성자는 아래 코드입니다.
public Member(String name) {
this.name = name;
}
그런데 JPA는 객체를 만들 때 기본 생성자가 필요합니다.
그래서 아무 값도 받지 않는 생성자를 하나 만들어둡니다.
외부에서 마음대로 쓰지 않게 protected로 두었습니다.
처음에는 이렇게만 기억하면 됩니다.
JPA Entity에는 기본 생성자가 필요하다.
4. JpaRepository 만들기
이제 Repository를 만듭니다.
이전에는 직접 클래스를 만들고 SQL을 작성했습니다.
이번에는 JpaRepository를 사용합니다.
MemberRepository.java 파일을 만듭니다.
src/main/java/com/example/demo/MemberRepository.java
package com.example.demo;
import org.springframework.data.jpa.repository.JpaRepository;
public interface MemberRepository extends JpaRepository<Member, Long> {
}
코드가 아주 짧습니다.
하지만 이 한 줄로 기본적인 저장, 조회, 삭제 기능을 사용할 수 있습니다.
JpaRepository란?
JpaRepository는 Spring Data JPA가 제공하는 Repository입니다.
아래 코드를 보겠습니다.
public interface MemberRepository extends JpaRepository<Member, Long> {
}
이 뜻은 다음과 같습니다.
Member 객체를 DB에 저장하고 조회하는 Repository를 만들겠다.
그리고 Member의 id 타입은 Long이다.
JpaRepository<Member, Long>에서:
| 부분 | 의미 |
|---|---|
Member | 저장할 Entity 타입 |
Long | Entity의 id 타입 |
처음에는 이렇게 기억하면 됩니다.
JpaRepository<저장할 객체, id 타입>
JpaRepository가 기본으로 제공하는 기능
JpaRepository를 사용하면 기본 메서드를 바로 사용할 수 있습니다.
대표적으로 이런 것들이 있습니다.
| 메서드 | 역할 |
|---|---|
save() | 저장 또는 수정 |
findAll() | 전체 조회 |
findById() | id로 조회 |
delete() | 삭제 |
count() | 개수 조회 |
즉, 단순 저장과 조회를 위해 SQL을 직접 작성하지 않아도 됩니다.
5. Service 만들기
회원 등록과 조회를 처리할 Service를 만듭니다.
MemberService.java 파일을 만듭니다.
src/main/java/com/example/demo/MemberService.java
package com.example.demo;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
public Member join(String name) {
if (name == null || name.isBlank()) {
throw new IllegalArgumentException("이름은 비어 있을 수 없습니다.");
}
Member member = new Member(name);
return memberRepository.save(member);
}
public List<Member> findMembers() {
return memberRepository.findAll();
}
}
Service 코드 설명
1. Repository 사용하기
private final MemberRepository memberRepository;
Service는 Repository를 사용해서 데이터를 저장하고 조회합니다.
이번에는 직접 SQL을 작성한 Repository가 아니라 JpaRepository를 상속한 Repository를 사용합니다.
2. 회원 저장하기
Member member = new Member(name);
return memberRepository.save(member);
new Member(name)으로 회원 객체를 만들고, save()로 저장합니다.
여기서 중요한 점은 우리가 INSERT INTO ... SQL을 직접 작성하지 않았다는 것입니다.
JPA가 Entity 정보를 보고 필요한 SQL을 대신 만들어 실행합니다.
3. 회원 전체 조회하기
return memberRepository.findAll();
findAll()은 저장된 회원 전체를 조회합니다.
이때도 우리가 SELECT * FROM ... SQL을 직접 작성하지 않았습니다.
6. Controller 만들기
브라우저에서 요청을 받을 Controller를 만듭니다.
MemberController.java 파일을 만듭니다.
src/main/java/com/example/demo/MemberController.java
package com.example.demo;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
public class MemberController {
private final MemberService memberService;
public MemberController(MemberService memberService) {
this.memberService = memberService;
}
@GetMapping("/members/new")
public Member join(@RequestParam("name") String name) {
return memberService.join(name);
}
@GetMapping("/members")
public List<Member> members() {
return memberService.findMembers();
}
}
실행해보기
Spring Boot를 실행한 뒤 브라우저에서 아래 주소로 접속합니다.
http://localhost:8080/members/new?name=kim
결과는 대략 이렇게 나올 수 있습니다.
{
"id": 1,
"name": "kim"
}
이번에는 다른 회원도 등록해봅니다.
http://localhost:8080/members/new?name=park
결과는 대략 이렇게 나올 수 있습니다.
{
"id": 2,
"name": "park"
}
이제 회원 목록을 조회합니다.
http://localhost:8080/members
결과는 대략 이렇게 나옵니다.
[
{
"id": 1,
"name": "kim"
},
{
"id": 2,
"name": "park"
}
]
콘솔에서 SQL 확인하기
spring.jpa.show-sql=true를 설정했다면, 콘솔에 JPA가 실행한 SQL이 보일 수 있습니다.
예를 들어 저장할 때는 이런 SQL이 보일 수 있습니다.
insert into member (name, id) values (?, default)
조회할 때는 이런 SQL이 보일 수 있습니다.
select m1_0.id, m1_0.name from member m1_0
중요한 점은 우리가 SQL을 직접 작성하지 않았는데도, JPA가 내부에서 SQL을 만들어 실행한다는 것입니다.
즉, JPA가 SQL을 없애는 것이 아니라 대신 만들어주는 것입니다.
H2 콘솔에서 확인하기
브라우저에서 H2 콘솔에 접속합니다.
http://localhost:8080/h2-console
JDBC URL에는 설정 파일에 적은 값을 입력합니다.
jdbc:h2:file:./data/jpa-demo-db
접속 후 아래 SQL을 실행해봅니다.
SELECT * FROM MEMBER;
저장한 회원 데이터가 보이면 성공입니다.
테이블 이름은 환경이나 설정에 따라 MEMBER 또는 member처럼 보일 수 있습니다.
JPA를 쓰면 뭐가 좋아질까?
JPA를 쓰면 단순한 저장과 조회 코드를 줄일 수 있습니다.
이전에는 저장하기 위해 이런 SQL을 직접 작성했습니다.
INSERT INTO members(name) VALUES (?)
조회할 때도 SQL을 직접 작성했습니다.
SELECT id, name FROM members ORDER BY id
하지만 JPA를 사용하면 이런 메서드를 사용할 수 있습니다.
memberRepository.save(member);
memberRepository.findAll();
처음에는 이 차이만 이해해도 충분합니다.
직접 SQL 작성 → JdbcTemplate
객체 중심 저장/조회 → JPA
JPA를 쓴다고 SQL을 몰라도 될까?
아닙니다.
JPA를 쓰더라도 SQL과 DB 개념은 알아야 합니다.
JPA는 SQL을 대신 만들어주지만, 결국 DB에 실행되는 것은 SQL입니다.
그래서 JPA를 사용할수록 오히려 아래 개념도 중요해집니다.
- 테이블
- 기본 키
- 컬럼
- 관계
- 인덱스
- SQL 실행 결과
처음에는 JPA가 편해 보이지만, 나중에는 JPA가 어떤 SQL을 실행하는지도 볼 줄 알아야 합니다.
자주 헷갈리는 부분
1. Entity는 그냥 DTO와 같은 걸까?
같지 않습니다.
Entity는 DB 테이블과 연결되는 객체입니다.
DTO는 데이터를 주고받기 위한 객체로 쓰는 경우가 많습니다.
처음에는 이렇게 구분하면 됩니다.
Entity = DB와 연결되는 객체
DTO = 요청/응답에 쓰는 객체
이번 글에서는 단순하게 Entity를 바로 응답으로 반환했지만, 실제 프로젝트에서는 DTO를 따로 쓰는 경우가 많습니다.
2. @Id를 안 붙이면 어떻게 될까?
JPA는 Entity를 구분할 기본 키가 필요합니다.
그래서 @Id가 없으면 Entity로 제대로 사용할 수 없습니다.
즉, Entity에는 보통 @Id가 꼭 필요합니다.
3. JpaRepository는 구현 클래스를 안 만들어도 될까?
네, 기본적인 기능은 구현 클래스를 직접 만들지 않아도 됩니다.
Spring Data JPA가 실행 시점에 Repository 구현체를 만들어줍니다.
그래서 우리는 인터페이스만 작성해도 기본 기능을 사용할 수 있습니다.
public interface MemberRepository extends JpaRepository<Member, Long> {
}
처음에는 이 부분이 신기하지만, Spring Data JPA가 대신 만들어준다고 이해하면 됩니다.
4. GET으로 저장해도 괜찮을까?
이번 글에서는 브라우저 주소창에서 쉽게 테스트하기 위해 GET 요청으로 회원을 저장했습니다.
/members/new?name=kim
하지만 실제 서비스에서는 데이터를 저장할 때 보통 POST 요청을 사용합니다.
이번 방식은 학습용입니다.
나중에는 @PostMapping과 요청 body를 사용해서 저장하는 방식으로 넘어가는 것이 좋습니다.
오늘 배운 것
이번 글에서는 JPA를 처음 살펴봤습니다.
핵심은 아래와 같습니다.
@Entity는 DB 테이블과 연결될 객체를 표시함@Id는 Entity의 기본 키를 표시함@GeneratedValue는 id 값을 자동 생성하게 함JpaRepository를 사용하면 기본 저장과 조회 기능을 바로 사용할 수 있음- JPA를 쓰면 SQL을 직접 많이 작성하지 않아도 저장과 조회가 가능함
- 하지만 실제로는 JPA가 내부에서 SQL을 만들어 실행함
정리
JPA는 Java 객체와 DB 테이블을 연결해주는 기술입니다.
이번 글에서는 Member Entity를 만들고, JpaRepository를 사용해서 회원을 저장하고 조회해봤습니다.
흐름은 이렇게 바뀌었습니다.
이전: Repository에서 SQL 직접 작성
이번: JpaRepository가 기본 저장/조회 제공
처음에는 JPA가 마법처럼 보일 수 있습니다.
하지만 핵심은 단순합니다.
Entity로 DB에 저장할 객체를 만들고,
JpaRepository로 저장과 조회를 맡긴다.
이 흐름을 이해하면 다음 단계인 CRUD API를 만들 때 훨씬 수월해집니다.
다음 글에서는 지금까지 배운 내용을 바탕으로 간단한 CRUD API를 만들어보겠습니다.