참고 자료
인프런_김영한님의 스프링 입문 강의를 참고하였습니다.
Summary
순수 JDBC | 스프링 JdbcTemplate | JPA | 스프링 데이터 JPA | |
코드 길이 | 매우 많음 (try-catch 예외처리 복잡) | 보통 | 보통 | 적음 |
SQL 사용 | O | O | 일부 기능이 인터페이스에 내장 (INSERT, PK을 조건으로 조회 등) | 대부분 기능이 인터페이스에 내장 (CRUD, 전체 조회, 페이징 처리, 갯수(count) 등 대부분 기능) |
실무 사용 | 거의 X | O | O | O |
데이터베이스 연동을 위한 기본 설정 방법
처음 연동 시 기본 설정 방법은 순수 JDBC, 스프링 통합 테스트, 스프링 JdbcTemplate, JPA, 스프링 데이터 JPA 방법 모두 공통되므로 알아두자.
1️⃣ build.gradle 설정
- build.gradle은 이 프로젝트는 어떤 라이브러리를 쓸지 정리해놓은 곳이다.
- 보통 새로운 도구들을 쓰려면 dependencies 에 추가한다.
plugins {
id 'java'
id 'org.springframework.boot' version '3.4.4'
id 'io.spring.dependency-management' version '1.1.7'
}
group = 'hello'
version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(17)
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
// jdbc, jpa, h2 데이터베이스 관련 라이브러리 추가
// implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
// ------------------------------------------------------------------------------
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
}
tasks.named('test') {
useJUnitPlatform()
}
2️⃣application.properties 설정
- application.properties 에서는 Spring Boot 프로젝트의 설정값을 저장하는 곳이다.
- build.gradle이 어떤 라이브러리를 쓸지 설정한다면, application.properties는 그 라이브러리들이 어떻게 동작할 지를 나타낸다.
- 아래 코드에서 DB와 연결하는 엔드포인트는 jdbc:h2:tcp://localhost/~/test 이다. DB 사용자 계정 이름은 sa 이다. 나머지 자세한 부분은 추후 다루자!
spring.application.name=hello-spring
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.jpa.show-sql=true
spring.jpa.hibernate.ddl-auto=none
순수 JDBC
옛날에 사용된 방식으로 암기하지 않아도 된다.
아~ 이런게 있구나 이런 흐름이구나~ 정도로만 이해하자!
[특징]
- 예외 처리가 많고 복잡한 것이 특징이다.
- 쿼리를 직접 짜야한다.
[알아두어야 할 것]
DataSource는 DB의 연결 정보를 가지고 있는 객체이다. (주입 받을 수 있다.)
dataSource.getConnection() 으로 DB와 Spring을 연결할 수 있다.
JDBC는 아래와 같은 흐름을 따른다.
// DataSource는 DB의 연결 정보를 가지고 있는 객체. 객체 주입
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) { // 주입 받는 구문
this.dataSource = dataSource;
}
String sql = "INSERT INTO member(name) VALUES (?)"; // SQL 구문
// 1) DB 연결
Connection conn = dataSource.getConnection();
// 2) SQL문 실행 준비
PreparedStatement pstmt = conn.preparedStatement(sql);
// 3) 파라미터 설정
pstmt.setString(1, member.getName()); // 첫 번째 ?에 member.getName() 값을 넣는다.
// 4) SQL문 실행
// pstmt.executeUpdate(); // 저장, 수정, 삭제용 (insert, update, delete)
ResultSet rs = pstmt.executeQuery(); // 조회용 (select)
// 5) SELECT인 경우 SQL 쿼리 결과가 담긴 rs 객체를 바탕으로 결과 처리
if (rs.next()) {
member.setId(rs.getLong(1));
} else {
throw new SQLException("id 조회 실패");
}
// 6) 리소스 할당 해제
pstmt.close();
conn.close();
rs.close();
전체 코드는 아래와 같다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.jdbc.datasource.DataSourceUtils;
import javax.sql.DataSource;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Optional;public class JdbcMemberRepository implements MemberRepository {
private final DataSource dataSource;
public JdbcMemberRepository(DataSource dataSource) {
this.dataSource = dataSource;
}
@Override
public Member save(Member member) {
String sql = "insert into member(name) values(?)";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql, Statement.RETURN_GENERATED_KEYS);
pstmt.setString(1, member.getName());
pstmt.executeUpdate();
rs = pstmt.getGeneratedKeys();
if (rs.next()) {
member.setId(rs.getLong(1));
} else {
throw new SQLException("id 조회 실패");
}
return member;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public Optional<Member> findById(Long id) {
String sql = "select * from member where id = ?";
Connection conn = null; PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setLong(1, id);
rs = pstmt.executeQuery();
if(rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return Optional.of(member);
} else {
return Optional.empty();
}
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public List<Member> findAll() {
String sql = "select * from member";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
rs = pstmt.executeQuery();
List<Member> members = new ArrayList<>();
while(rs.next()) { Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
members.add(member);
}
return members;
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
}
@Override
public Optional<Member> findByName(String name) {
String sql = "select * from member where name = ?";
Connection conn = null;
PreparedStatement pstmt = null;
ResultSet rs = null;
try {
conn = getConnection();
pstmt = conn.prepareStatement(sql);
pstmt.setString(1, name);
rs = pstmt.executeQuery();
if(rs.next()) {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return Optional.of(member);
}
return Optional.empty();
} catch (Exception e) {
throw new IllegalStateException(e);
} finally {
close(conn, pstmt, rs);
}
} private Connection getConnection() {
return DataSourceUtils.getConnection(dataSource);
}
private void close(Connection conn, PreparedStatement pstmt, ResultSet rs) {
try {
if (rs != null) {
rs.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (pstmt != null) {
pstmt.close();
}
} catch (SQLException e) {
e.printStackTrace();
}
try {
if (conn != null) {
close(conn);
}
} catch (SQLException e) {
e.printStackTrace();
}
}
private void close(Connection conn) throws SQLException {
DataSourceUtils.releaseConnection(conn, dataSource);
}
}
스프링 테스트
스프링 컨테이너에 가지고 있는 스프링 코드를 대상으로 테스트를 진행할 때 사용한다.
순수 자바 코드로 진행했던 테스트와 잠깐 비교해보자.
속도 | Bean 사용 가능 | |
순수한 자바 코드(JUnit만 사용, @Test) (단위 테스트) |
JVM 안에서 끝나서 빠르다. | 불가능 |
스프링 테스트 코드 (@SpringBootTest) (통합 테스트) |
Spring의 전체 컨텍스트를 로딩하기 때문에 느리다. | 가능 |
참고로 단일 테스트는 하나의 클래스나 메서드를 테스트한다. 의존성이 있는 경우 목(Mock, 가짜) 으로 테스트한다.
통합 테스트는 여러 클래스가 잘 작동하는지 테스트하고 DB와 스프링 빈들을 사용하여 실제 환경과 유사하게 테스트를 한다.
테스트 설계를 할 때 순수한 자바 코드의 단위 테스트로 설계하는 것이 일반적으로 좋을 가능성이 높다.
스프링 컨테이너까지 올려야 할 정도이면 테스트 설계가 잘못되었을 가능성이 높다.
스프링 통합 테스트를 위해 @SpringBootTest 을 넣는다. (모든 Bean들을 모아두고 주입 준비를 시켜준다.)
- Spring 전체 컨텍스트를 로딩해서 테스트를 진행한다. (Spring이 모든 Bean들을 만들고 연결해준다. 실제 서비스처럼 테스트 환경을 만드는 것이 가능하다.)
- * ApplicationContext란? : Spring에서는 모든 객체를 빈(Bean)으로 관리한다. 이렇게 빈(Bean)을 관리하는 공간을 ApplicationContext 라고 한다. (Spring의 빈(객체) 저장소)
- * 전체 컨텍스트를 로딩한다는 말은 우리가 만든 @Service, @Repository, @Controller, @Component 과 같은 클래스들을 Spring이 전부 찾아서 객체로 만들고 필요한 곳에 주입을 해준다는 의미
- 컨트롤러, 서비스, 레포지토리 등 모든 Bean들을 이용할 수 있다.
DB 테스트를 위해 @Transactional 어노테이션을 넣는다. (테스트 후 roolback을 해주는 역할)
- 테스트 클래스나 테스트 메서드에 붙는 경우, 테스트가 끝나면 자동으로 roolback 해준다.
- 테스트를 하더라도 DB에 값이 추가되지 않아 테스트에 유용하다.
** 참고로 @Commit 을 하면 테스트 후 자동으로 Commit 된다.
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest // 스프링 컨테이너와 테스트를 함께 실행한다.
@Transactional // 테스트 케이스에 붙었을 때 테스트 시작 전에 트랜잭션을 시작하고, test가 끝나면 rollback을 해준다.
class MemberServiceIntegrationTest {
@Autowired MemberRepository memberRepository;
@Autowired MemberService memberService;
// 회원가입을 시키고 나서 잘 해당 유저가 추가가 되었는지 체크
@Test
void 회원가입() throws Exception { // 테스트 코드는 한글로 작성 가능하다!
// given : 이 데이터를 기반으로~
Member member1 = new Member();
member1.setName("hello");
// when : ~ 메서드를 실행했을 때 잘 실행되는지 본다.
Long result = memberService.join(member1);
// then : 다시 조회하여 잘 저장이 되었는지 체크한다.
Member findMember = memberRepository.findById(result).get();
Assertions.assertThat(findMember.getName()).isEqualTo(member1.getName()); // 저장된 멤버에 "hello" 라는 사람이 있는지 찾는다.
}
// 중복 회원을 잘 걸러내는지 체크 (같은 회원 만들고 오류 터지는지 체크)
@Test
public void 중복_회원_예외() {
// given - member1은 기존회원 -> member1의 중복 가입을 잘 잡는지 체크
Member member1 = new Member();
member1.setName("spring");
Long result = memberService.join(member1);
Member member2 = new Member();
member1.setName("spring");
// when
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member1));// memberService.join(member2) 구문을 실행할 때 IllegalStateException 오류가 터지면 e에 메시지가 담긴다.
Assertions.assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다."); // 오류 메시지 검증
}
// 전체 회원 조회 체크 (2개 정도 넣어보고 2개가 나오는지 테스트)
@Test
void findMembers() {
// given
Member member1 = new Member();
member1.setName("spring1");
memberRepository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
memberRepository.save(member2);
// when
List<Member> result = memberService.findMembers();
// then
Assertions.assertThat(result.size()).isEqualTo(2);
}
// ID에 맞는 1명 조회 (2개 정도 넣어보고 그 특정 ID에 맞는 사람이 조회되는지 검증)
@Test
void findOne() {
// given
Member member1 = new Member();
member1.setName("spring1");
memberRepository.save(member1);
Member member2 = new Member();
member2.setName("spring2");
memberRepository.save(member2);
// when
Member result = memberService.findOne(memberRepository.findByName("spring1").get().getId()).get();
// then
Assertions.assertThat(result.getName()).isEqualTo(member1.getName());
}
}
스프링 JdbcTemplate
JDBC API에서 반복되는 코드를 대부분 제거해준다. 하지만 여전히 SQL문은 직접 작성해야 한다.
MyBatis와 유사한 느낌이 든다.
실무에서 사용된다.
JdbcTemplate 라이브러리라고 불리는 이유? -> Jdbc 코드를 줄이는 과정에서 디자인 패턴 중 Template method 을 많이 사용했기 때문이다.
사용법은 JdbcTemplate 메뉴얼 구글링을 통해 사용하는 것을 추천!
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.namedparam.MapSqlParameterSource;
import org.springframework.jdbc.core.simple.SimpleJdbcInsert;
import javax.sql.DataSource;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
public class JdbcTemplateMemberRepository implements MemberRepository {
// 스프링에서 SQL을 실행할 수 있도록 도와주는 도구 -> JdbcTemplate
private final JdbcTemplate jdbcTemplate;
@Autowired
public JdbcTemplateMemberRepository(DataSource dataSource) { // DB와 연결할 수 잇도록 하는 것들이 DataSource 객체에 담겨져 있다.
jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public Member save(Member member) {
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate); // SimpleJdbcInsert는 INSERT SQL문을 쉽게 실행할 수 있도록 도와준다.
jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id"); // member 테이블에 데이터를 넣을 거고, id라는 컬럼은 auto_increment가 되는 키라고 알려준다.
Map<String, Object> parameters = new HashMap<>(); // DB에 넣을 데이터를 Map 형태로 만든다.
parameters.put("name", member.getName()); // name 이라는 컬럼에 member.getName() 값을 넣는다.
Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters)); // 데이터를 실제로 DB에 INSERT하고 생성된 ID값을 받아온다.
member.setId(key.longValue()); // 받아온 id 값을 member 객체에 넣어준다.
return member; // DB에 잘 저장된 member 객체를 리턴한다.
}
// ID로 회원 조회
@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(), id); // ? 자리에는 3번째 파라미터인 id 값이 들어간다. 조회 결과는 memberRowMapper을 이용하여 Member 객체로 바꾼다.
return result.stream().findAny(); // 1개를 꺼내서 Optional로 감싸서 리턴
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
return result.stream().findAny(); // 리스트나 배열같은 자료를 stream() 형으로 변환하고 스트림에서 요소 하나를 찾아서 Optional로 감싸서 리턴한다.
}
@Override
public List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
// DB에서 조회한 결과를 Member 객체로 바꿔주는 역할
private RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
// DB에서 가져온 id 값과 name 값을 Member에 넣는다.
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member; // 만들어진 객체 리턴
};
}
}
JPA 인터페이스
기존의 반복 코드도 없애고, SQL문도 JPA가 직접 만들어서 실행해준다.
SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환할 수 있다.
-> 개발 생산성을 크게 높일 수 있다.
JPA는 인터페이스고, Hibernate가 구현체이다. Hibernate 외에 여러 구현체가 있는데, 보통 Hibernate 구현체를 자주 사용한다.
JPA는 ORM(Object Relational Mapping) 이라는 특징이 있다. ORM은 객체와 관계형 데이터베이스의 데이터를 자동으로 매핑(연결)해주는 것이다.
그런데, 은행에서 JPA를 쓸 수 있을까? -> 해외의 은행은 JPA를 쓰고 있다. 국내 결제, 주문 시스템도 JPA로 잘 사용하고 있다.
[사전 지식]
Entity(엔티티) : DB 테이블과 1:1로 매핑된 자바 클래스 (데이터베이스의 한 테이블을 자바 객체처럼 표현한 클래스)
예를 들어, DB에서 Member 테이블이 아래와 같이 있다고 가정해보자.
id | name | age |
1 | 철수 | 21 |
2 | 영 | 24 |
이 것을 JAVA에서는 아래와 같이 표현할 수 있다.
@Entity
public class Member {
@Id
private Long id;
private String name;
private int age;
// getter, setter, constructor 등...
}
이때 Member 클래스가 바로 엔티티이다.
[엔티티 사전 작업]
1️⃣객체와 관계형 데이터베이스의 데이터를 자동으로 매핑하기 위해 @Entity 어노테이션을 DB와 매핑하고자 하는 클래스에 추가해준다.
그 다음으로 각 변수의 제약 조건을 걸고, 자바에서의 변수와 DB에서의 컬럼 이름을 매핑한다.
package hello.hellospring.domain;
import jakarta.persistence.*;
@Entity // JPA가 관리하는 entity
public class Member {
// @Id = PK라는 의미
// @GeneratedValue = 값을 직접 넣지 않아도 됨(DB가 자동으로 생성해줌 - default 느낌)
// strategy = GenerationType.IDENTITY : DB가 자동으로 생성해주는 방식 중에 auto_increment을 선택
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// @Column(name = "username") // 자바에서 필드(변수명) 이름이 name 이고, DB에서 컬럼 이름은 username 이면 이를 매핑해주는 작업을 해주어야 한다.
// 자바의 변수명도 name 이고, DB member 테이블의 변수명도 name 인 경우 자동으로 매핑되므로 생략 가능
private String name;
public Long getId() {
return id;
}
public String getName() {
return name;
}
public void setId(Long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
@Override
public String toString() {
final StringBuilder sb = new StringBuilder("Member{");
sb.append("id=").append(id);
sb.append(", name='").append(name).append('\'');
sb.append('}');
return sb.toString();
}
}
2️⃣사용할 Service 코드에 @Transactional 을 추가한다.
JPA는 행의 변화가 있을 때(INSERT, UPDATE, DELETE) 트랜잭션 안에서 실행이 되어야 한다.
예를 들어 JpaMemberRepository.save(member); 코드가 실행이 되어도 실제 DB에 INSERT 쿼리가 바로 날아가지 않는다.
JPA는 영속성 컨텍스트라는 곳에 저장해두고, 트랜잭션이 커밋(commit) 되는 순간에 한 번에 쿼리를 보낸다.
* 영속성 컨텍스트 : JPA가 엔티티 객체를 저장해두는 임시 저장소 ( 아직 DB에 반영되진 않았지만, JPA가 관리하고 있는 상태 )
JPA는 @Transactional 이 없으면 언제 데이터를 반영해야 할지 모른다. 이렇게 되면 DB에 변경 사항이 적용되지 않거나 오류가 발생할 수 있다. 그래서 @Transactional이 필요하다.
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
import java.util.Optional;
@Transactional // JPA는 행의 변화가 있을 때 (INSERT, UPDATE, DELETE) 트랜잭션 안에서 실행이 되어야 한다.
public class MemberService {
private final MemberRepository memberRepository; // MemberRepository 중에서 MemoryMemberRepository
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
// 회원 가입
public Long join(Member member) {
// 같은 이름이 있는 중복 회원 X
Optional<Member> result = memberRepository.findByName(member.getName());
validateDuplicateMember(result);
memberRepository.save(member);
return member.getId();
}
// 중복 회원 검증
private static void validateDuplicateMember(Optional<Member> result) {
result.ifPresent(m -> {
throw new IllegalStateException("이미 존재하는 회원입니다.");
});
}
// 전체 회원 조회
public List<Member> findMembers() {
return memberRepository.findAll();
}
// ID에 맞는 1명 조회
public Optional<Member> findOne(Long memberId) {
return memberRepository.findById(memberId);
}
}
3️⃣스프링 빈 설정
1) EntityManager을 스프링 컨테이너에 넣어두고 Repository 클래스에서 생성자를 통해 넘겨 받는다. (아래 코드 참조)
2) Repository 클래스에서 바로 주입을 받기 위해 @Repository 어노테이션을 사용하여 EntityManager을 주입 받는다.
이 글에서는 연습을 위해 1) 방법으로 진행해본다.
스프링 빈을 관리하는 SpringConfig 클래스에서 스프링 컨테이너에 EntityManager 객체를 넣어둔다. (참고로 EntityManager 객체는 Spring이 미리 스프링 컨테이너에 넣어두고 관리하는 객체로 두었으므로 우리는 여기서 DI(주입)만 하면 된다.)
package hello.hellospring;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.*;
import hello.hellospring.service.MemberService;
import jakarta.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
// DI
private EntityManager em;
@Autowired
public SpringConfig(EntityManager em) {
this.em = em;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
// @Bean
// MemberRepository memberRepository() {
// return new MemoryMemberRepository();
// return new JdbcMemberRepository(dataSource);
// return new JdbcTemplateMemberRepository(dataSource);
return new JpaMemberRepository(em); // 생성자를 호출하고 EntityManager 객체를 넘겨준다.
}
}
repository 클래스에서는 EntityManager 클래스를 받는 매개변수로 받는 생성자를 하나 만들어 주입을 받을 수 있도록 한다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import jakarta.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
public class JpaMemberRepository implements MemberRepository {
// EntityManager 주입 받기 (EntityManager은 JPA를 사용할 때 DB의 연결 정보와 데이터를 가지고 있는 객체)
private final EntityManager em;
// DI
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
}
[코드 작성]
EntityManager을 주입 받고 조건에 맞게 구현한다. (아래 표 예시에서는 EntityManager 객체를 em 이라고 가정)
em.persist(member) | member 객체를 DB에 INSERT |
Member member = em.find(Member.class, 1) | 기본 키(PK)로 데이터 하나를 조회해온다. // Member 엔터티에서 기본 키인 ID 값이 1인 값 하나를 조회해서 반환한다. |
JPQL (JPA Query Language) em.createQuery 사용 List<Member> result = em.createQuery( "select m from Member as m where m.name = :name", Member.class) .setParameter("name", name) .getResultList(); |
원하는 조건을 가진 데이터를 조회 - 기본 키로 조회하는 경우가 아닌 경우 - 특정 조건이 필요한 경우 |
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import jakarta.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
public class JpaMemberRepository implements MemberRepository {
// EntityManager 주입 받기 (EntityManager은 JPA를 사용할 때 DB의 연결 정보와 데이터를 가지고 있는 객체)
private final EntityManager em;
// DI
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
@Override
public Member save(Member member) {
em.persist(member); // member 객체를 DB에 저장
return member;
}
@Override
public Optional<Member> findById(Long id) {
// SELECT * FROM Member WHERE id={id} // {id}는 findById의 매개변수에서 받아온 값
Member member = em.find(Member.class, id); // 내가 찾고 싶은 엔티티의 종류(자바)는 Member 이다. 거기서 id 값이 일치하는 DB 회원을 조회한다.
return Optional.ofNullable(member);
}
@Override
public Optional<Member> findByName(String name) {
// JPQL 이라는 JPA용 쿼리 작성
// Member.class -> 행에 리턴되는 데이터 타입은 Member 이다. (결과타입.class)
// Member 클래스에서 모든 값을 조회하는데, Member 객체의 name 값이(m.name) name인(:name) 것만 조회한다.
// Member 엔티티(테이블)에서 name이 같은 사람 찾기
List<Member> result = em.createQuery("select m from Member as m where m.name = :name", Member.class)
.setParameter("name", name) // :name 에 들어갈 것은 name 값이다.
.getResultList(); // 조건에 맞는 회원들을 리스트(List) 형태로 리턴한다.
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
// select m from Member as m : Member 객체 자체를 모두 select 한다.
// Member.class : 행에 리턴되는 데이터의 타입은 Member 이다.
List<Member> result = em.createQuery("select m from Member as m", Member.class)
.getResultList();
return result;
}
}
스프링 데이터 JPA
CRUD, 전체 조회, 페이징 처리, 갯수(count) 등 단순한 것들은 인터페이스에서 다 제공이 된다. (모두에게 통용될 수 있는 작업)
어떤 조건을 넣거나 복잡한 것은 비즈니스가 다르기 때문에 공통화할 수가 없으므로 제공이 되지 않는다.
그런 경우 findByName, findById, findByNameOrId 등으로 찾을 수 있다.
실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl 라이브러리를 사용한다.
이 조합으로도 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용한다.
[만드는 방법]
1️⃣Spring Data JPA 인터페이스를 만든다.
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
// SpringDataJpaMemberRepository(Spring Data JPA 인터페이스)를 만들어 놓으면 스프링에서 구현체를 자동으로 만들어서 스프링 빈에 자동으로 등록을 해놓는다.
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository { // JpaRepository<엔티티,PK의 자료형>
// JPQL | select m from Member m where m.name = ?
@Override
Optional<Member> findByName(String name);
// JPQL | select m from Member m where m.name = ? and m.id = ?
// Optional<Member> findByNameAndId(String name, Long id);
}
2️⃣스프링 빈을 관리하는 클래스에서 주입 받아서 사용한다.
package hello.hellospring;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.*;
import hello.hellospring.service.MemberService;
import jakarta.persistence.EntityManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration // @Bean에 등록된 메서드들을 스프링 컨테이너에 올려준다.
public class SpringConfig {
// DI
private final MemberRepository memberRepository;
@Autowired
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
}