본 게시글은 김영한님의 [스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술]을 수강하며 작성한 글입니다.
1. H2 데이터베이스 설치
1) 실행
- H2 Database Engine 설치
- 다운받은 파일의 압축을 풀고 /h2/bin/으로 이동
- h2.sh를 실행시키기 위해 권한 부여
chmod 755 h2.sh - h2.sh 실행
./h2.sh
2) DB 접속
- 최초 DB 접속
JDBC URL: jdbc:h2:~/test - ~/test.mv.db 파일 생성 확인
- 이후 DB 접속
JDBC URL: jdbc:h2:tcp://localhost/~/test
3) Member 테이블 생성
1. SQL문을 사용하여, 이전의 회원관리 예제에서 사용되던 Member 테이블을 생성한다.
create table member
(
id bigint generated by default as identity,
name varchar(255),
primary key (id)
);
2. member 추가
insert into member(name) values('spring');
insert into member(name) values('spring2');
💡 사용한 sql 문은 hello-spring/sql/ 하위에 ddl.sql 파일로 저장하면 깃허브를 통해 버전관리를 할 수 있다.
2. 순수 JDBC
1) JDBC란
JDBC는 자바에서 DB에 접속할 수 있도록 하는 자바 API이다.
JDBC로 Repository 코드를 작성하면 각 메소드마다 대략 다음의 과정으로 작업을 수행하게 된다.
- 쿼리 설정
- 커넥션 받아오기
- 쿼리 실행 준비
- 쿼리 실행
- 커넥션 닫기
그리고 이것들을 개발자가 직접 다 구현해야한다. 그래서 코드가 길고, 로직을 파악하기가 어렵다.
따라서 사실상 최근에는 쓰지 않는 기술이라고 보면 된다.
2) 메모리 방식 → JDBC 방식
기존 메모리 방식이 아닌 JDBC 방식을 사용하도록 바꾼다. 단순히 JDBC API로만 코딩한다.
1. build.gradle 파일에 jdbc, h2 데이터베이스 관련 라이브러리 추가
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
2. resources/application.properties에 데이터베이스 연결 설정 추가
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
3. JdbcMemberRepository 파일을 생성하고 JDBC API를 사용하여 MemberRepository 구현
4. 자바 설정참고파일(SpringConfig)에 새로 사용할 리포지토리(JdbcMemberRepository)를 스프링 빈으로 받을 수 있도록 변경
package hello.hellospring;
import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Configurable;
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);
}
}
DataSource는 데이터베이스 커넥션을 획득할 때 사용하는 객체다. 스프링 부트는 데이터베이스 커넥션 정보를 바탕으로 DataSource를 생성하고 스프링 빈으로 만들어 둔다. 따라서 DI를 받을 수 있다.
- 객체 지향 설계의 SOLID 원칙 중 개방-폐쇄 원칙(OCP)를 만족한다.
- 스프링의 DI를 사용하여 기존 코드를 전혀 손대지 않고, 설정(SpringConfig)만으로 구현 클래스를 변경할 수 있다.
3. 스프링 통합 테스트
스프링 컨테이너와 DB까지 연결한 통합 테스트를 진행한다.
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
import hello.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;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest
@Transactional
public class MemberServiceIntegrationTest {
@Autowired MemberService memberService;
@Autowired MemberRepository memberRepository;
@Test
void 회원가입() {
// given
Member member = new Member();
member.setName("spring");
// when
Long saveId = memberService.join(member);
// then
Member findMember = memberService.findOne(saveId).get();
assertThat(member.getName()).isEqualTo(findMember.getName());
}
@Test
public void 중복_회원_예외() {
// given
Member member1 = new Member();
member1.setName("spring");
Member member2 = new Member();
member2.setName("spring");
// when
memberService.join(member1);
IllegalStateException e = assertThrows(IllegalStateException.class, () -> memberService.join(member2));
assertThat(e.getMessage()).isEqualTo("이미 존재하는 회원입니다.");
// then
}
}
- @SpringBootTest 어노테이션
스프링 컨테이너와 테스트를 함께 실행한다. 설정 파일을 참고하여 런타임 의존성, DB 연결 등을 모두 사용한다. - @Transactional 어노테이션
테스트 케이스에 해당 어노테이션이 있으면 테스트 시작 전에 트랜잭션을 시작하고, 테스트 완료 후에 항상 롤백한다. 즉, DB에 데이터가 남지 않으므로 데이터를 지우는 코드(beforeEach(), afterEach())가 필요하지 않으며, 다음 테스트에 영향을 주지 않는다. - @Autowired 어노테이션
테스트에서는 주로 필드 주입 방법을 통해 DI를 한다.
💡 통합 테스트가 있으니 순수 Jdbc 단위 테스트는 더이상 필요 없지 않나요?
통합 테스트는 스프링 부트 컨테이너를 통해 모든 설정과 빈을 로드하기 때문에 시간이 오래 걸리고 무겁다. 순수한 단위 테스트가 더 좋은 테스트일 경우가 많기 때문에 단위로 쪼개서 테스트하는 습관이 좋다.
4. 스프링 JdbcTemplate
1) JdbcTemplate이란
- JDBC Template은 개발자가 JDBC 기술을 쉽게 사용할 수 있도록 도와주는 클래스이다.
- JDBC API를 이용할 경우 아래와 같은 작업이 반복된다.
- Connection으로 연결하기
- 쿼리를 작성 후 PreparedStatement로 실행하기
- resultSet을 이용하여 결과 처리하기
- Connection 닫아주기
- 환경 설정은 순수 Jdbc의 환경 설정과 동일하다.
2) JdbcTemplate 사용하기
1. JdbcTemplateMemberRepository 파일 생성
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import javax.sql.DataSource;
import java.util.List;
import java.util.Optional;
public class JdbcTemplateMemberRepository implements MemberRepository{
private final JdbcTemplate jdbcTemplate;
@Autowired
public JdbcTemplateMemberRepository(DataSource dataSource) {
jdbcTemplate = new JdbcTemplate(dataSource);
}
@Override
public Member save(Member member) {
return null;
}
@Override
public Optional<Member> findById(Long id) {
return Optional.empty();
}
@Override
public Optional<Member> findByName(String name) {
return Optional.empty();
}
@Override
public List<Member> findAll() {
return null;
}
}
💡 생성자가 1개일 경우 @Autowired를 생략할 수 있다.
2. 각각의 메서드 구현
- save()
@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;
}
- findById()
@Override
public Optional<Member> findById(Long id) {
List<Member> result = jdbcTemplate.query("select * from member where id = ?", memberRowMapper());
return result.stream().findAny();
}
- findByName()
@Override
public Optional<Member> findByName(String name) {
List<Member> result = jdbcTemplate.query("select * from member where name = ?", memberRowMapper());
return result.stream().findAny();
}
- findAll()
@Override
public List<Member> findAll() {
return jdbcTemplate.query("select * from member", memberRowMapper());
}
3. JdbcTemplateMemberRepository를 사용하도록 스프링 설정 변경
package hello.hellospring;
import hello.hellospring.repository.JdbcMemberRepository;
import hello.hellospring.repository.JdbcTemplateMemberRepository;
import hello.hellospring.repository.MemberRepository;
import hello.hellospring.repository.MemoryMemberRepository;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Configurable;
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. 통합 테스트 수행
5. JPA
1) JPA란
- 자바에서 사용하고 있는 ORM의 표준이다.
- 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다.
- SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환할 수 있다.
- 개발 생산성을 크게 높일 수 있다.
2) JPA 사용하기
1. build.gradle에 jpa 관련 라이브러리 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-devtools'
// 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'
}
spring-boot-starter-data-jpa는 내부에 Jdbc 관련 라이브러리를 포함하고 있으므로 기존의 Jdbc 라이브러리는 제거해도 된다.
2. application.properties에 sql 관련 설정을 추가한다.
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
- show-sql=true:JPA가 생성하는 SQL을 확인할 수 있다.
- hibernate.ddl-auto=none: 테이블 자동 생성을 막는다.
3. Entity 매핑
package hello.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에서 관리할 객체로 선언한다.
- @Id : PK(primary key)
- @GeneratedValue: 기본 키 생성을 데이터베이스에 위임한다.
(ex MySQL - AUTO INCREMENT)
4. JpaMemberRepository 파일 생성
EntityManager
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
PK 관련 메서드
- save()
@Override
public Member save(Member member) {
em.persist(member);
return member;
}
- findByName()
@Override
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
PK 외의 나머지 관련 메서드
- findByName()
@Override
public Optional<Member> findByName(String name) {
List<Member> result = em.createQuery("select m from Member m where m.name= :name", Member.class)
.setParameter("name", name)
.getResultList();
return result.stream().findAny();
}
- findAll()
@Override
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
💡 entity를 대상으로 한 쿼리로, entity 자체를 조회한다. 이후 JPA에 의해 SQL로 변환된다.
5. @Transactional
package hello.hellospring.service;
import hello.hellospring.domain.Member;
import hello.hellospring.repository.MemberRepository;
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
public class MemberService {
}
jpa 사용 시에는 모든 데이터 변경이 transaction 안에서 실행되어야 하므로 service 파일의 클래스 상단에@Transactional을 붙여준다.
6. JpaMemberRepository를 사용하도록 스프링 설정 변경
package hello.hellospring;
import hello.hellospring.repository.*;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
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 EntityManager em;
@Autowired
public SpringConfig(EntityManager em) {
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);
}
}
7. 통합 테스트 수행
6. 스프링 데이터 JPA
1) 스프링 데이터 JPA란
- 리포지토리에 구현 클래스 없이 인터페이스 만으로 개발이 가능하다.
- 반복되던 기존의 CURD 기능도 모두 제공한다.
💡 스프링 데이터 JPA는 JPA를 편리하게 사용하도록 도와주는 기술이다. 따라서 JPA를 먼저 학습한 후에 스프링 데이터 JPA를 학습해야 한다.
2) 스프링 데이터 JPA 사용하기
1. SpringDataJpaMemberRepository 파일 생성
package hello.hellospring.repository;
import hello.hellospring.domain.Member;
import org.springframework.data.jpa.repository.JpaRepository;
import java.util.Optional;
public interface SpringDataJpaMemberRepository extends JpaRepository<Member, Long>, MemberRepository {
@Override
Optional<Member> findByName(String name);
}
- JpaRepository<Member, Long> 인터페이스와 기존에 만들었던 MemberRepository를 extends한다. 또한 Long은 엔티티 pk의 타입을 뜻한다.
- 위와 같이 JpaRepository를 상속하면 스프링컨테이너가 MemberRepository 구현체를 자동으로 스프링 빈으로 만들어 관리한다.
- 인터페이스가 인터페이스를 상속할 때는 implements가 아닌 extends를 사용하며 다중상속이 가능하다.
2. SpringDataJpaMemberRepository를 사용하도록 스프링 설정 변경
package hello.hellospring;
import hello.hellospring.repository.*;
import hello.hellospring.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Configurable;
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 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 JdbcMemberRepository(dataSource);
// return new JdbcTemplateMemberRepository(dataSource);
// return new JpaMemberRepository(em);
// }
//
}
생성자가 1개이기 때문에 굳이 @Autowired를 붙일 필요가 없다.
스프링데이터 JPA의 기본 인터페이스인 JpaRepository 인터페이스는 Paging관련 인터페이스, CRUD관련인터페이스를 다중상속하고 있다. 그렇기 때문에 기본적인 CRUD 등을 쿼리나 구현메소드 없이 바로 동작하게 해준다.
복잡한 동적 쿼리는 QueryDsl라는 라이브러리를 사용한다. QueryDsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적 쿼리도 편하게 작성할 수 있다. 해당 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를 사용하거나, 앞서 학습한 스프링 JdbcTemplate을 사용하면 된다.
또한 기본적으로 제공하는 메소드 이외에 비지니스 로직 별로 다른 메소드가 있는 경우 표기룰만 맞춰준다면 리플렉션 기술로 자동으로 SQL을 만들어준다. 위 코드의 findByName은 사실 기본적인 스프링데이터JPA 제공 메소드가 아니다. 리플렉션 기술로 findByName은 다음처럼 해석된다.
select m from member m where m.name = ?
참고자료
[인프런 김영한 로드맵1]스프링 입문 강의 정리(3)
-인프런 김영한 강사님의 스프링 입문 강의 핵심 위주로 정리한다. -모든 소스는 깃허브에서 관리한다.(https://github.com/coderahn/Spring-Lecture1) 7.스프링 DB 접근 기술 메모리DB가 아닌 H2 DB를 사용하여
roadofdevelopment.tistory.com
[Spring] 입문-7. Jpa / Spring Data Jpa / 통합 테스트
스프링의 DB 접근 기술 중, JPA를 알아보자.
velog.io
'Courses > Spring' 카테고리의 다른 글
[스프링 핵심 원리 - 기본편] 1. 객체 지향 설계와 스프링 (0) | 2023.10.06 |
---|---|
[스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술] 7. AOP (0) | 2023.09.24 |
[스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술] 5. 회원 관리 예제 MVC 개발 (0) | 2023.09.24 |
[스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술] 4. 스프링 빈과 의존관계 (0) | 2023.09.21 |
[스프링 입문 - 코드로 배우는 스프링 부트, 웹 MVC, DB 접근 기술] 3. 회원 관리 예제 - 백엔드 개발 (0) | 2023.09.21 |