*김영한 | 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의를 참조한 글입니다.
이전 시간까지는 데이터를 메모리에 저장하는 방식으로 구현했다.
그러나 이렇게 할 경우 서버를 내렸다가 다시 켜면 모든 데이터가 사라진다는 문제점이 있다.
데이터베이스를 만들어 구현하면 이런 단점을 해결할 수 있다.
순수 JDBC -> 스프링 jdbcTemplate -> JPA -> 스프링 데이터 JPA 순으로 DB 접근기술이 발전되었다.
1. H2 데이터베이스 설치
H2 데이터베이스는 자바로 작성된 관계형 데이터 베이스 관리 시스템으로, 용량이 작고 가벼워서 교육용으로 적합하다.
실무에서는 mysql, oracle 등을 사용한다.
DB 접근 기술에 대해 배우려면 DB가 필요하므로 H2 데이터베이스를 먼저 설치한다.
1-1. H2 데이터베이스를 설치한다.
DB 접근 기술에 대해 배우려면 DB가 필요하므로 H2 데이터베이스를 먼저 설치한다.
(최근 버젼을 설치하면 일부 기능이 정상 작동하지 않을 수 있으므로, 1.4.200버젼 다운을 권장한다.)
https://www.h2database.com/html/download-archive.html
Archive Downloads
www.h2database.com
1-2. 압축을 풀고, 'h2 - bin - h2.bat'파일을 실행한다.
실행하면, 다음과 같은 화면이 뜬다.
1-3. 1-2번에서 실행이 안될시 H2_HOME 환경변수를 설정해준다.
환경변수 설정 방법은 이전 글에서 JAVA_HOME 환경변수 설정해주었던 방법과 동일하게 해주면 된다.
2023.01.18 - [spring] - 1. 프로젝트 설치 및 실행하기 [김영한 | 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술]
1. 프로젝트 설치 및 실행하기 [김영한 | 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근
*김영한 | 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술 강의를 참조한 글입니다. 1. 프로그램 설치 - JAVA 11, INtelliJ 프로젝트를 시작하기에 앞서, 두 개의 프로그램을 설치해주어
8w8u8.tistory.com
1-4. '연결' 버튼을 누른다.
그럼 다음의 화면이 뜨는데, 왼쪽 상단의 JDBC URL에 적혀있는 jdbc::h2:~/test 는 이 파일의 경로를 의미한다.
이후부터는 해당 부분에 jdbc:h2:tcp://localhost/~/test 라고 적어야한다.
1-5. 테이블을 만든다.
아래의 코드를 입력하고, 실행한다.
drop table if exists member CASCADE;
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
<코드 설명>
- id : java에서는 long 타입을 사용 -> sql에서는 bigint 타입을 사용
- generated by default as identity : id 값이 들어오지 않으면 db가 자동으로 id값을 생성하여 채움
이번에는 회원 데이터를 몇 개 만들어 넣어주자.
다음의 코드를 입력하고 실행한다.
insert into member (name) values ('spring');
잘 만들어졌는지 조회를 하기 위해 아래의 코드를 입력하고 실행한다.
또는, JDBC URL 밑의 'MEMBER'를 클릭하고 실행해도 된다.
select * from member;
실행 결과는 다음과 같다. spring 이라는 이름의 회원이 잘 생성되었음을 알 수 있다.
마찬가지 방식으로 spring1, spring2 라는 이름의 회원 데이터도 만들어준다.
이제 본격적으로 스프링 DB 접근 기술에 대해 배워보자.
2. 순수 JDBC
순수 JDBC로 개발하는 방식은 20년 전에 사용하던 매우 고전적인 방식으로, 지금은 거의 쓰이지 않는다.
고대 개발자들이 이렇게 고생하고 살았구나 생각하고, 정신건강을 위해 참고만 하고 넘어가자.
2-1. build.gradle 파일에 Jdbc와H2 데이터 베이스 관련 라이브러리 추가
dependencies 안에 다음의 코드를 작성한다.
implementation 'org.springgramework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
<코드 설명>
implementation 'org.springgramework.boot:spring-boot-starter-jdbc'
// jdbc : db와 연동하려면 꼭 필요
runtimeOnly 'com.h2database:h2'
// h2 클라이언트
2-2. application.properties에 H2 드라이버 추가
'src - main - resources - application.properties' 에 다음의 코드를 작성한다.
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
DB와 연동하기 위한 접속 경로를 넣은 것이다.
2-3. JdbcMemberRepository.java
이전 시간에는 인터페이스 구현을 메모리(MemberRepository.java)에서 했다면,
이번에는 Jdbc(JdbcMemberRepository.java)에서 구현해보자.
'src - main - java - hellospring.hellospring - repository'패키지에 JdbcMemberRepository.java를 생성하고 다음의 코드를 작성한다.
package hellospring.hellospring.repository;
import hellospring.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;
} // DB와 연동하기 위해 dataSource가 필요하다.
// 2-2번에서 DB와 스프링을 연동해뒀으니, 스프링이 알아서 접속 정보를 만들어놓는다.
// 이후에 dataSource.getConnection() 메서드를 통해 스프링으로부터 DB를 주입받는다.
@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);
}
}
2-4. 스프링(SpringConfig) 설정 변경
Jdbc로 구현하기 위해 SpringConfig 설정도 적절하게 변경시켜주자.
package hellospring.hellospring;
import hellospring.hellospring.repository.JdbcMemberRepository;
import hellospring.hellospring.repository.JdbcTemplateMemberRepository;
import hellospring.hellospring.repository.MemberRepository;
import hellospring.hellospring.repository.MemoryMemberRepository;
import hellospring.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
private final DataSource dataSource;
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
return new JdbcMemberRepository(dataSource);
}
3. 스프링 통합 테스트
이전에는 순수 java 코드를 가지고 테스트를 했지만
DB가 연결되는 순간 DB에 대한 정보를 스프링이 들고 있기 때문에 순수 java가 아닌 스프링 통합테스트를 해야한다.
통합테스트를 해야하는 경우가 있을 수 있지만 대부분은 단위테스트를 하며, 단위테스트를 하는 것이 가장 좋다.
package hellospring.hellospring.service;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertEquals;
import hellospring.hellospring.domain.Member;
import hellospring.hellospring.repository.MemberRepository;
import hellospring.hellospring.repository.MemoryMemberRepository;
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;
@SpringBootTest
@Transactional
public class MemberServiceIntegrationTest {
// test code에서는 편하게 필드 기반 injection
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository; // clear를 해주기 위해 가져옴
@Test
public void 회원가입() {
//Given
Member member = new Member();
member.setName("hello");
//When
Long saveId = memberService.join(member);
//Then
Member findMember = memberRepository.findById(saveId).get();
assertEquals(member.getName(), findMember.getName());
}
// 예외가 잘 처리되는지도 확인해야함
@Test
public void 중복_회원_예외() {
//Given
Member member1 = new Member();
member1.setName("spring5");
Member member2 = new Member();
member2.setName("spring5");
//When
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));//예외가 발생해야 한다.
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
}
}
4. 스프링 JdbcTemplate
JdbcTemplate은 순수 Jdbc와 동일한 환경 설정(build.gradle, application.properties)을 가지며,
순수 Jdbc에 있는 중복 코드를 대부분 제거해줘서 더 간결한 코드를 작성할 수 있어 실무에서도 사용한다.
단, sql은 직접 작성해야 한다.
4-1. JdbcTemplateMemberRepository.java
'src - main - java - hellospring.hellospring - repository'패키지에 JdbcTemplateMemberRepository.java를 생성하고,
다음의 코드를 작성한다.
package hellospring.hellospring.repository;
import hellospring.hellospring.domain.Member;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Optional;
import javax.sql.DataSource;
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;
public class JdbcTemplateMemberRepository implements MemberRepository{
private final JdbcTemplate jdbcTemplate;
@Autowired // 생성자가 하나이므로 생략 가능
public JdbcTemplateMemberRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public Member save(Member member) {
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
Map<String, Object> parameters = new HashMap<>();
parameters.put("name", member.getName());
Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
member.setId(key.longValue());
return member;
}
@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id=?", memberRowMapper());
return result.stream().findAny();
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(), name);
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
private RowMapper<Member> memberRowMapper() {
return new RowMapper<Member>() {
@Override
public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
}
};
}
}
<코드 설명>
// JdbcTemplate 객체 생성
public class JdbcTemplateMemberRepository implements MemberRepository {
private final JdbcTemplate jdbcTemplate;
// 만약 생성자 인자값이 하나일 경우 아래처럼 @Autowired 생략 가능(스프링이 자동으로 넣어줌)
// @Autowired
public JdbcTemplateMemberRepository(DataSource dataSource) {
this.jdbcTemplate = new JdbcTemplate(dataSource);
}
}
// sql 쿼리 메서드 작성
@Override
public Member save(Member member) {
SimpleJdbcInsert jdbcInsert = new SimpleJdbcInsert(jdbcTemplate);
// SimpleJdbcInsert : JdbcTemplate를 넣어서 만드는 객체
// 쿼리문을 직접 작성하지 않아도 되게 도와줌.
jdbcInsert.withTableName("member").usingGeneratedKeyColumns("id");
Map<String, Object> parameters = new HashMap<>();
parameters.put("name", member.getName());
parameters.put("age", member.getAge());
Number key = jdbcInsert.executeAndReturnKey(new MapSqlParameterSource(parameters));
// key를 전달받아서
member.setId(key.longValue()); // setId로 넣어줄게
return member;
}
@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper(),id);
// 리스트로 반환할겨 = jdbcTemplate.query(sql, 결과, 파라미터)
// 파라미터에는 sql의 ?에 대응하는 값을 넣어준다.
// memberRowMapper()는 아래에 작성함
return result.stream().findAny();
}
@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper(),name);
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
*쿼리문
query()는 DB에 sql문을 보내는 메서드이다. 리턴 타입은 List.
JdbcTemplate를 이용하여 2-3번과 비교해보면 엄청 코드가 간결해진 것을 확인할 수 있다.
// RowMApper 객체 생성
// sql 결과 값을 RowMapper 객체를 생성한 뒤에 ResultSet으로 받아온 뒤
// set이용하여 member에 값을 담아 return함
// 람다식 사용하여 줄이기
private RowMapper<Member> memberRowMapper() {
return (rs, rowNum) -> {
Member member = new Member();
member.setId(rs.getLong("id"));
member.setName(rs.getString("name"));
return member;
};
}
// 람다식 사용 X
private RowMapper<Member> memberRowMapper() {
return new RowMapper<Member>() {
@Override
public Member mapRow(ResultSet rs, int rowNum) throws SQLException {
Member member = new Member(); // Member 객체 생성
member.setId(rs.getLong("id")); // SetId에서 ResultSet 받아옴
member.setName(rs.getString("name")); // Setname에서 받아옴
return member;
}
}
}
4-2. SpringConfig 파일에서 아래와 같이 변경 후 테스트
package hellospring.hellospring;
import hellospring.hellospring.repository.JdbcMemberRepository;
import hellospring.hellospring.repository.JdbcTemplateMemberRepository;
import hellospring.hellospring.repository.MemberRepository;
import hellospring.hellospring.repository.MemoryMemberRepository;
import hellospring.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
private final DataSource dataSource;
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
// return new JdbcMemberRepository(dataSource);
return new JdbcTemplateMemberRepository(dataSource);
}
}
4. JPA
JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다.
JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환을 할 수 있다.
JPA를 사용하면 개발 생산성을 크게 높일 수 있다.
4-1. build.gradle에서 jdbc -> jpa 변경, JPA 설정 추가
// jpa, jdbc 모두 포함함
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
spring-boot-starter-data-jpa 는 내부에 jdbc 관련 라이브러리를 포함한다.
따라서 이전에 작성한 jdbc는 제거해도 된다.
testImplementation('org.springframework.boot:spring-boot-starter-test') {
exclude group: 'org.junit.vintage', module: 'junit-vintage-engine'
}
또한, 위의 코드를 추가해준다.
4-2. application.properties에 추가
spring.jpa.show-sql=true // JPA가 생성하는 query 보기
spring.jpa.hibernate.ddl-auto=none // 테이블 자동 생성 기능 끄기
// create로 적어주면, table 자동 생성
application.properties에 JPA관련 설정을 추가해준다.
4-3. JPA를 사용하기 위해 entity로 매핑해줘야함
package hellospring.hellospring.domain;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;
@Entity
public class Member {
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String name;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
<코드 설명>
@Entity
: JPA가 관리하는 entity(개체; object는 객체)라고 명시하는 애노테이션
JPA는 모든 것이 EntityManager로 동작한다. 4-1번에서와 같이 gradle에 JPA라이브러리를 추가하면 스프링이 자동으로 EntityManager를 빈으로 등록해 관리한다.
@Id @GeneratedValue(strategy = GenerationType.IDENTITY)
*기본 키 맵핑
- 직접 할당 : @Id만 사용
- 자동 생성 : @Id와 @GeneratedValue를 같이 사용
@Id
: 데이터베이스 테이블의 기본 키(PK)와 객체의 필드를 맵핑시켜주는 애노테이션
@GeneratedValue(strategy = GenerationType.IDENTITY)
GeneratedValue의 전략은 세 개가 있다. - Identity, Sequence, Table
그 중에서 Identity 전략 매핑을 선택했다.
*Identity 전략
@GeneratedValue(strategy = GenerationType.IDENTITY)
: 기본 키(pk)를 생성하는 작업을 전적으로 데이터베이스에 위임한다.
데이터베이스에 한 번 갔다 와야 pk를 알 수 있으므로....
Identity 전략에서는 entityManager.persist() 후에 엔티티의 pk를 곧바로 알 수 없다.
=> 문제점 :
영속성 컨텍스트에서 해당 객체가 관리되려면 pk값을 알아야 하는데,
Identity전략에서는 1차 캐시 안에 있는 @Id값은 DB에 넣기 전까지 세팅할 수 없게 된다.
=> 해결책 :
Identity 전략에서만 예외적으로 entityManager.persist()가 호출되는 시점에 바로 DB에 Insert쿼리를 날린다.
(다른 전략에서는 이미 id값을 알기에 commit하는 시점에 Insert쿼리를 날린다.)
*Sequence 전략
@GeneratedValue(strategy = GenerationType.SEQUNCE)
: persist하면 데이터베이스에서 Sequence Object의 pk값을 가져와서 바로 세팅해준다.
=> 문제점 :
그럼 매번 persist할 때마다 DB에 다녀와야 되는가?
=> 해결책 :
allocationSize속성값(기본값 : 50)을 조절해 원하는 사이즈만큼 pk를 가져온다.
*Table 전략
@GeneratedValue(strategy = GenerationType.TABLE)
: 키 생성 전용 테이블을 하나 만들어서 데이터베이스 시퀀스를 흉내내는 전략이다.
=> 최적화되어있지 않은 테이블을 직접 사용하기 때문에 성능상의 문제가 많다.
4-4. JpaMemberRepository.java 파일 생성
'src - main - java - hellospring.hellospring - repository'패키지에 JpaMemberRepository.java를 생성해 멤버 리포지토리를 구현해보자. 전체 코드는 다음과 같다.
package hellospring.hellospring.repository;
import hellospring.hellospring.domain.Member;
import java.util.List;
import java.util.Optional;
import javax.persistence.EntityManager;
public class JpaMemberRepository implements MemberRepository {
// jpa는 entitymanager로 모든게 동작
// build.gradle에서 jpa설정 해둠으로써 spring boot에서 entityManager를 자동 생성해줌
// 여기선 그렇게 만들어진 것을 injection 받음
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
@Override
public Member save(Member member) {
em.persist(member); // jpa가 insert query 다 만들어서 디비에 넣어줌
return member;
}
@Override
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id); // 조회할 타입과 식별자를 인자로
return Optional.ofNullable(member);
}
@Override
public Optional<Member> findByName(String name) {
// pk가 아닌 값으로 조회할 때는 jpql이라는 query언어를 써야함
List<Member> result = em.createQuery("select m from Member m where m.name = :name",
Member.class).setParameter("name", name).getResultList();
return result.stream().findAny();
}
@Override
public List<Member> findAll() {
List<Member> result = em.createQuery("select m from Member m", Member.class).getResultList();
return result;
}
}
4-5. JPA를 사용할 때 Service에 @Transactional 추가는 필수
JPA를 통한 모든 데이터 저장, 변경은 트랜잭션 안에서 실행해야 한다.
스프링이 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고,
메서드가 정상 종료되면 트랜잭션을 커밋한다.
@Transactional
public class MemberService {
...
*트랜잭션(transaction)
: 데이터베이스 관리 시스템 또는 유사한 시스템에서 여러 단위 작업들을 그룹화한 상호작용의 단위를 말한다.
트랜잭션이 성공하면 트랜잭션 동안 이루어진 모든 데이터 수정은 커밋(commit)되고 데이터베이스의 영구적인 부분이 되고, 트랜잭션에 오류가 발생하여 취소되거나 롤백(rollback)되면 모든 데이터 수정은 지워진다.
*스프링의 테스트 코드에서 @Transactional 애너테이션을 사용하면, 테스트 종료시 자동으로 롤백이된다.
4-6. SpringConfig 파일에서 변경
package hellospring.hellospring;
import hellospring.hellospring.repository.*;
import hellospring.hellospring.service.MemberService;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
@Configuration
public class SpringConfig {
private final DataSource dataSource;
private final EntityManager em;
public SpringConfig(DataSource dataSource, EntityManager em) {
this.dataSource = dataSource;
this.em = em;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
// return new JdbcMemberRepository(dataSource);
// return new JdbcTemplateMemberRepository(dataSource);
return new JpaMemberRepository(em);
}
}
4-7. import javax.persistence.Entity 관련 에러 해결
https://mingyum119.tistory.com/m/37
인프런 스프링 입문 강의 #JPA
JPA란? : (Java Persistence API) ORM(Object Relational Mapping)의 기술 표준, 인터페이스의 모음이다. ORM에 대한 자바 API의 규격이며 Hiberate, OpenJAP등이 JPA의 구현체이다. 데이터베이스를 객체지향적으로 관리
mingyum119.tistory.com
위의 글의 도움을 받아 해결!
javax.persistence-api-2.2.jar 파일을 다운로드 받아서 다음과 같이 Project Structure>Modules에 Export 파일에 추가.
5. 스프링 데이터 JPA
스프링 데이터 JPA는 위의 JPA에서 더 나아가 기본 crud도 제공하고,
리포지토리에 구현 클래스 없이 인터페이스 만으로도 개발 가능하며,
기본적인 findById, findAll, save 등은 제공이 된다.
*주의
: 스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 기술이다.
따라서 JPA를 먼저 학습한 후에 스프링 데이터 JPA를 학습해야 한다.
5-1. SpringDataJpaMemberRepository interface
*스프링 데이터 설정은 JPA설정과 동일하다. (4-1, 2, 3번)
'src - main - java - hellospring.hellospring - repository'패키지에 SpringDataJpaMemberRepository 인터페이스를 생성하고, 다음의 코드를 작성한다.
스프링 데이터 JPA를 사용하면, 리포지토리에 구현 클래스 없이 인터페이스만으로 개발이 끝난다!
package hellospring.hellospring.repository;
import hellospring.hellospring.domain.Member;
import java.util.Optional;
import org.springframework.data.jpa.repository.JpaRepository;
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
@Override
Optional<Member> findByName(String name);
}
<코드 설명>
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository
- SpringDataJpaMemberRepository는 JpaRepository, MemberRepository를 다중상속받는다.(extend)
위와 같이 인터페이스를 정의만 하면,
스프링 데이터 JPA가 자동으로 구현체를 만들고 빈에 등록한다.
- JpaRepository<T, ID> : T는 엔티티, ID는 엔티티의 PK값
@Override
Optional<Member> findByName(String name);
스프링 데이터 JPA가 기본적인 CRUD 메서드는 제공하지만 findByName과 같은 특수한 메서드는 어떻게 할까?
findBy(컬럼명)과 같이 추상 메서드를 선언하면 스프링 데이터 JPA가 JPQL
select m from Member as m where m.name = :name
을 자동으로 생성하고 SQL로 번역돼서 쿼리문이 실행된다.
5-2. SpringConfig 파일 변경
package hellospring.hellospring;
import hellospring.hellospring.repository.JdbcMemberRepository;
import hellospring.hellospring.repository.JdbcTemplateMemberRepository;
import hellospring.hellospring.repository.JpaMemberRepository;
import hellospring.hellospring.repository.MemberRepository;
import hellospring.hellospring.repository.MemoryMemberRepository;
import hellospring.hellospring.service.MemberService;
import javax.persistence.EntityManager;
import javax.sql.DataSource;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class SpringConfig {
private final MemberRepository memberRepository;
@Autowired
public SpringConfig(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
@Bean
public MemberService memberService() {
return new MemberService(memberRepository);
}
}
// @Bean
// public MemberRepository memberRepository() {
// return new MemoryMemberRepository();
// return new JpaMemberRepository(em);
// }
// 장점: Memory -> db로 변경하고 싶으면 return new DbMemberRepository()로만 바꿔주면 된다!
}
'back-end > 김영한 | 스프링 입문' 카테고리의 다른 글
Spring에서 MySQL 연결하기 (0) | 2023.08.04 |
---|---|
5. 회원 관리 예제(웹 MVC 개발) [김영한 | 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술] (1) | 2023.03.06 |
4. 스프링 빈과 의존관계 [김영한 | 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술] (0) | 2023.02.20 |
3. 회원 관리 예제(백엔드 개발) [김영한 | 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술] (3) | 2023.01.29 |
2. 스프링 웹 개발 기초 [김영한 | 스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술] (1) | 2023.01.28 |