JPA, Spring Data JPA에 대해서는 추후에 자세히 정리하도록 하겠다.
Reference
순수 Jdbc 환경설정
build.gradle - jdbc, h2 DB 관련 라이브러리 추가
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-jdbc'
runtimeOnly 'com.h2database:h2'
..
..
}
Groovy
복사
resources/application.properties - 데이터베이스 연결 설정 추가
spring.datasource.url=jdbc:h2:tcp://localhost/~/test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
Groovy
복사
주의!!!
1.
spring.datasource.username=sa 는 boot 2.4버전 부터는 반드시 들어가야한다.
2.
spring.datasource.username=sa 뒤에 공백이 있을 경우 오류가 발생할 수 있으니, 공백도 모두 제가해야 한다.
Jdbc 구현
순수 Jdbc API로 구현하는 것은 거의 20년 전의 일이므로 우선 구현되는지만 확인해라. (굉장히 길다….)
JdbcMemberRepository 구현 코드
@Configuration
public class SpringConfig {
// 초기설정(application.properties)에서 등록해둔 Datasource를 가져와 사용하기 위한 코드
private final DataSource dataSource;
public SpringConfig(DataSource dataSource) {
this.dataSource = dataSource;
}
@Bean
public MemberService memverService(){
return new MemberService(memberRepository());
}
@Bean
public MemberRepository memberRepository(){
//return new MemoryMemberRepository(); 아래 코드로 변경
return new JdbcMemberRepository();
}
}
Java
복사
위와 같이 설정할 경우 코드 상에서 MemoryMember Repository와 JdbcMemoryRepository 둘 모두가 Member Repository라는 Interface를 Implements 하고 있다. 하지만 return new JdbcMemberRepository(); 로 SpringConfig를 해주었기 때문에 Interface에서의 DI는 JdbcMemberRepository로 이루어지게 된다.
이렇게 기능을 완전히 변경을 해도 코드 몇줄로 바꿀 수 있는 이러한 구조를 개방-폐쇄 원칙(OCP, Open Closed Principle)이라고 한다.
이는 확장에는 열려 있고, 수정, 변경에는 닫혀있다는 이론으로 JAVA의 다형성을 잘 이용하는 개념이다
TestCode의 작성
위 2번 과정인 Jdbc를 하지 않았다는 가정하에 TestCode를 작성해 TDD(Test-Driven-Develop)로써 구현해보자
통합 테스트
package com.boot.lecture1.service;
import com.boot.lecture1.domain.Member;
import com.boot.lecture1.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.*;
import org.springframework.transaction.annotation.Transactional;
import static org.assertj.core.api.Assertions.assertThat;
import static org.junit.jupiter.api.Assertions.assertThrows;
@SpringBootTest
@Transactional //test가 끝나면 롤백
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
}
}
Java
복사
•
@SpringBootTest 테스트 코드를 스프링 컨테이너와 함께 실행하도록 해주는 어노테이션
•
@Transactional 테스트 케이스에서 이 어노테이션이 있다면 테스트 종료 시에 테스트 시작 전과 똑같이 롤백해준다.
→ 그렇기에 @AfterEach 로 초기화를 해주지 않아도 된다.
단위 테스트
package com.boot.lecture1.service;
import com.boot.lecture1.domain.Member;
import com.boot.lecture1.repository.MemoryMemberRepository;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.assertj.core.api.Assertions.*;
import static org.junit.jupiter.api.Assertions.assertThrows;
class MemberServiceTest {
MemberService memberService;
MemoryMemberRepository memberRepository;
@BeforeEach
public void beforeEach(){
memberRepository = new MemoryMemberRepository();
memberService = new MemberService(memberRepository);
}
@AfterEach
public void afterEach(){
memberRepository.clearStore();
}
@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
}
}
Java
복사
통합 테스트 보다는 단위 테스트를 잘 할 줄 알아야하고, 순수한 단위 테스트가 훨신 좋은 테스트일 확률이 높다.
JdbcTemplate
•
•
스프링 JdbcTemplate과 MyBatis 같은 라이브러리는 JDBC API에서 본 반복 코드를 대부분
제거해준다. 하지만 SQL은 직접 작성해야 한다.
•
아래와같이 코드를 구현하고 설정을 바꾸어주면 바로 사용 가능하다.
구현코드 및 설정 코드
JPA
•
JPA는 기존의 반복 코드는 물론이고, 기본적인 SQL도 JPA가 직접 만들어서 실행해준다.
•
JPA를 사용하면, SQL과 데이터 중심의 설계에서 객체 중심의 설계로 패러다임을 전환을 할 수 있다.
•
JPA를 사용하면 개발 생산성을 크게 높일 수 있다.
build.gradle - JPA 관련 라이브러리 추가
•
starter-data-jpa 는 jdbc 관련 라이브러리도 포함하고 있기 때문에 제거해도 무방
dependencies {
//implementation 'org.springframework.boot:spring-boot-starter-jdbc'
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
runtimeOnly 'com.h2database:h2'
..
..
}
Groovy
복사
resources/application.properties - JPA 설정 추가
•
show-sql : JPA가 생성하는 SQL을 콘솔에서 보여준다.
•
ddl-auto : JPA는 자동으로 테이블을 생성해주는 기능을 제공하는데 이를 꺼준다.
→ create 라고 설정할 경우 엔티티 정보 바탕으로 테이블도 직접 생성해준다.
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
Groovy
복사
•
JPA에서는 엔티티 매핑, 서비스 계층에 트렌젝션 추가와 같은 절차가 필요하다.
Member.class
•
@Entity, @Id, @GenerateValue 어노테이션 추가
package com.boot.lecture1.domain;
import jakarta.persistence.*;
@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;
}
}
Java
복사
1)직접 할당 : 기본 키를 애플리케이션에서 직접 엔티티클래스의 @Id 필드에 set해준다.
2)자동 생성 : 대리 키 사용 방식
•
IDENTITY : 기본 키 생성을 데이터베이스에 위임한다.(ex MySQL - AUTO INCREMENT...)
•
SEQUENCE : 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다.(ex Oracle sequence...)
•
TABLE : 키 생성 테이블을 사용한다.(ex 시퀀스용 테이블을 생성해서 테이블의 기본키를 저장하고 관리한다.)
JpaMemberRepository
•
JPAQuery를 이용하여 코드 구현
package com.boot.lecture1.repository;
import com.boot.lecture1.domain.Member;
import jakarta.persistence.EntityManager;
import java.util.List;
import java.util.Optional;
public class JpaMemberRepository implements MemberRepository{
private final EntityManager em;
public JpaMemberRepository(EntityManager em) {
this.em = em;
}
@Override
public Member save(Member member) {
em.persist(member);
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) {
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() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
}
Java
복사
MemberService
•
@Transactional 어노테이션 추가
import org.springframework.transaction.annotation.Transactional
@Transactional
public class MemberService {}
Java
복사
@Transactional 사용 이유
스프링은 해당 클래스의 메서드를 실행할 때 트랜잭션을 시작하고, 메서드가 정상 종료되면 트랜잭션을
커밋한다. 만약 런타임 예외가 발생하면 롤백한다.
JPA를 통한 모든 데이터 변경은 트랜잭션 안에서 실행해야 한다.
JpaMemberRepository로 SpringConfig 변경
•
JPA에서는 DataSource가 아닌 EntityManager를 이용한다.
package hello.hellospring;
import hello.hellospring.repository.*;
import hello.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 EntityManager em;
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);
}
}
Java
복사
스프링 데이터 JPA
스프링 부트와 JPA만 사용해도 개발 생산성이 정말 많이 증가하고, 개발해야할 코드도 확연히 줄어듭니다.
여기에 스프링 데이터 JPA를 사용하면, 기존의 한계를 넘어 마치 마법처럼, 리포지토리에 구현 클래스 없이
인터페이스 만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 스프링 데이터
JPA가 모두 제공합니다.
스프링 부트와 JPA라는 기반 위에, 스프링 데이터 JPA라는 환상적인 프레임워크를 더하면 개발이 정말
즐거워집니다. 지금까지 조금이라도 단순하고 반복이라 생각했던 개발 코드들이 확연하게 줄어듭니다.
따라서 개발자는 핵심 비즈니스 로직을 개발하는데, 집중할 수 있습니다.
실무에서 관계형 데이터베이스를 사용한다면 스프링 데이터 JPA는 이제 선택이 아니라 필수 입니다.
(김영한 - 스프링 입문 - 코드로 배우는 스프링부트, 웹 MVC, DB접근기술)
SpringDataJpaMemberRepository.Interface
•
JpaRepository에서 자동으로 findById(), findAll()등 다양한 매서드를 지원해주기 때문에 따로 생성해줄 필요가 없다.
findByName() , findByEmail() 처럼 메서드 이름 만으로 조회 기능을 추가하면 사용할 수 있다.
package com.boot.lecture1.repository;
import com.boot.lecture1.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);
}
Java
복사
SpringData JPA 제공 클래스
(김영한 - 스프링 입문 - 코드로 배우는 스프링부트, 웹 MVC, DB접근기술)
(김영한 - 스프링 입문 - 코드로 배우는 스프링부트, 웹 MVC, DB접근기술)
참고: 실무에서는 JPA와 스프링 데이터 JPA를 기본으로 사용하고, 복잡한 동적 쿼리는 Querydsl이라는
라이브러리를 사용하면 된다. Querydsl을 사용하면 쿼리도 자바 코드로 안전하게 작성할 수 있고, 동적
쿼리도 편리하게 작성할 수 있다. 이 조합으로 해결하기 어려운 쿼리는 JPA가 제공하는 네이티브 쿼리를
사용하거나, 앞서 학습한 스프링 JdbcTemplate를 사용하면 된다.