중급자를 위해 준비한
[웹 개발, 백엔드] 강의입니다.
스프링 데이터 JPA는 기존의 한계를 넘어 마치 마법처럼 리포지토리에 구현 클래스 없이 인터페이스만으로 개발을 완료할 수 있습니다. 그리고 반복 개발해온 기본 CRUD 기능도 모두 제공합니다. 스프링 데이터 JPA 실무 노하우를 전해드립니다.
✍️
이런 걸
배워요!
스프링 데이터 JPA를 기초부터 실무 활용까지 한번에 배울 수 있습니다.
실무에서 실제 사용하는 기능 위주로 학습합니다.
단순한 기능 설명을 넘어 실무 활용 노하우를 배울 수 있습니다.
JPA와 스프링 데이터 JPA의 차이를 명확하게 이해할 수 있습니다.
강좌 : https://www.inflearn.com/course/스프링-데이터-JPA-실전#
수업자료 :
https://github.com/braverokmc79/jpa-basic-lecture-file2
소스 : https://github.com/braverokmc79/data-jpa
[1] 프로젝트 환경설정
1. 프로젝트 생성
강의 :
https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-JPA-실전&unitId=27997&tab=curriculum
build.gradle
plugins { id 'java' id 'org.springframework.boot' version '2.7.9' id 'io.spring.dependency-management' version '1.0.15.RELEASE' } group = 'study' version = '0.0.1-SNAPSHOT' sourceCompatibility = '17' configurations { compileOnly { extendsFrom annotationProcessor } } repositories { mavenCentral() } dependencies { implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' compileOnly 'org.projectlombok:lombok' developmentOnly 'org.springframework.boot:spring-boot-devtools' runtimeOnly 'com.h2database:h2' runtimeOnly 'com.mysql:mysql-connector-j' annotationProcessor 'org.projectlombok:lombok' testImplementation 'org.springframework.boot:spring-boot-starter-test' } tasks.named('test') { useJUnitPlatform() }
프로젝트 생성
스프링 부트 스타터(https://start.spring.io/)
Project: Gradle - Groovy Project
사용 기능: web, jpa, h2, lombok
SpringBootVersion: 2.7.9
groupId: study
artifactId: data-jpa
주의! - 스프링 부트 3.0
스프링 부트 3.0을 선택하게 되면 다음 부분을 꼭 확인해주세요.
1. Java 17 이상을 사용해야 합니다.
2. jakarta 패키지 이름을 jakarta 로 변경해야 합니다.
오라클과 자바 라이센스 문제로 모든 jakarta 패키지를 jakarta로 변경하기로 했습니다.
3. H2 데이터베이스를 2.1.214 버전 이상 사용해주세요.
패키지 이름 변경 예)
JPA 애노테이션
javax.persistence.Entity jakarta.persistence.Entity
스프링에서 자주 사용하는 @PostConstruct 애노테이션
javax.annotation.PostConstruct jakarta.annotation.PostConstruct
스프링에서 자주 사용하는 검증 애노테이션
javax.validation jakarta.validation
셋팅 -> build 검색 후 --> 그래들 build -> 설정
=> 롬복 적용
동작 확인
기본 테스트 케이스 실행
스프링 부트 메인 실행 후 에러페이지로 간단하게 동작 확인(`http://localhost:8080')
테스트 컨트롤러를 만들어서 spring web 동작 확인(http://localhost:8080/hello)
테스트 컨트롤러
package study.datajpa.controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @GetMapping("/hello") public String hello(){ return "hello"; } }
참고: 최근 IntelliJ 버전은 Gradle로 실행을 하는 것이 기본 설정이다. 이렇게 하면 실행속도가 느리다.
다음과 같이 변경하면 자바로 바로 실행하므로 좀 더 빨라진다.
>
> Preferences Build, Execution, Deployment Build Tools Gradle
> Build and run using: Gradle IntelliJ IDEA
> Run tests using: Gradle IntelliJ IDEA
롬복 적용
1. Preferences plugin lombok 검색 실행 (재시작)
2. Preferences Annotation Processors 검색 Enable annotation processing 체크 (재시작)
3. 임의의 테스트 클래스를 만들고 @Getter, @Setter 확인
2. 프로젝트 생성
강의 :
https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-JPA-실전&unitId=27998&tab=curriculum
3. H2 데이터베이스 설치
강의 :
https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-JPA-실전&unitId=27999&tab=curriculum
H2 데이터베이스 설치
개발이나 테스트 용도로 가볍고 편리한 DB, 웹 화면 제공
https://www.h2database.com
다운로드 및 설치
h2 데이터베이스 버전은 스프링 부트 버전에 맞춘다.
권한 주기: chmod 755 h2.sh
데이터베이스 파일 생성 방법
jdbc:h2:~/datajpa (최소 한번)
~/datajpa.mv.db 파일 생성 확인
이후 부터는 jdbc:h2:tcp://localhost/~/datajpa 이렇게 접속
> 참고: H2 데이터베이스의 MVCC 옵션은 H2 1.4.198 버전부터 제거되었습니다. 사용 버전이 1.4.199
이므로 옵션 없이 사용하면 됩니다.
4. 스프링 데이터 JPA와 DB 설정, 동작확인
강의 :
https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-JPA-실전&unitId=28000&tab=curriculum
application.properties
#변환 사이트 https://mageddo.com/tools/yaml-converter #코드 색깔 표시 spring.output.ansi.enabled=always #Springboot auto build spring.devtools.livereload.enabled=true spring.devtools.restart.enabled=true spring.driver-class-name=org.h2.Driver spring.datasource.url= jdbc:h2:tcp://localhost/~/datajpa spring.datasource.username=sa spring.datasource.password= #spring.jpa.properties.hibernate.show_sql=true spring.jpa.hibernate.ddl-auto= create spring.jpa.properties.hibernate.format_sql=true spring.jpa.properties.hibernate.use_sql_comments=true spring.jpa.properties.hibernate.default_batch_fetch_size=100 logging.level.org.hibernate.SQL=debug
application.yml
spring: datasource: url: jdbc:h2:tcp://localhost/~/datajpa username: sa password: driver-class-name: org.h2.Driver jpa: hibernate: ddl-auto: create properties: hibernate: #show_sql: true format_sql: true logging.level: org.hibernate.SQL: debug # org.hibernate.type: trace
spring.jpa.hibernate.ddl-auto: create
이 옵션은 애플리케이션 실행 시점에 테이블을 drop 하고, 다시 생성한다.
> 참고: 모든 로그 출력은 가급적 로거를 통해 남겨야 한다.
> show_sql : 옵션은 System.out 에 하이버네이트 실행 SQL을 남긴다.
> org.hibernate.SQL : 옵션은 logger를 통해 하이버네이트 실행 SQL을 남긴다
실제 동작하는지 확인하기
회원 엔티티
package study.datajpa.entity; import lombok.Getter; import lombok.NoArgsConstructor; import lombok.Setter; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Entity @Getter @Setter public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String username; protected Member() { } public Member(String username){ this.username=username; } public void changeUsername(String username){ this.username=username; } }
1) 회원 JPA 리포지토리
package study.datajpa.repository; import org.springframework.stereotype.Repository; import study.datajpa.entity.Member; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; @Repository public class MemberJpaRepository { @PersistenceContext private EntityManager em; public Member save(Member member){ em.persist(member); return member; } public Member find(Long id){ return em.find(Member.class, id); } }
JPA 기반 테스트
package study.datajpa.repository; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import study.datajpa.entity.Member; @SpringBootTest @Transactional @Rollback(value = false) class MemberJpaRepositoryTest { @Autowired MemberJpaRepository memberJpaRepository; @Test public void testMember(){ Member member =new Member("memberA"); Member saveMember = memberJpaRepository.save(member); Member findMember = memberJpaRepository.find(saveMember.getId()); Assertions.assertThat(findMember.getId()).isEqualTo(member.getId()); Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername()); Assertions.assertThat(findMember).isEqualTo(member); } }
2) 스프링 데이터 JPA 리포지토리
package study.datajpa.repository; import org.springframework.data.jpa.repository.JpaRepository; import study.datajpa.entity.Member; public interface MemberRepository extends JpaRepository<Member, Long> { }
스프링 데이터 JPA 기반 테스트
package study.datajpa.repository; import org.assertj.core.api.Assertions; import org.junit.jupiter.api.Test; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import study.datajpa.entity.Member; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @Transactional @Rollback(value = false) class MemberRepositoryTest { @Autowired MemberRepository memberRepository; @Test public void testMember(){ Member member =new Member("memberA"); Member saveMember = memberRepository.save(member); Member findMember = memberRepository.findById(saveMember.getId()).get(); Assertions.assertThat(findMember.getId()).isEqualTo(member.getId()); Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername()); Assertions.assertThat(findMember).isEqualTo(member); } }
Entity, Repository 동작 확인
jar 빌드해서 동작 확인
참고: 스프링 부트를 통해 복잡한 설정이 다 자동화 되었다. persistence.xml 도 없고, LocalContainerEntityManagerFactoryBean 도 없다.
스프링 부트를 통한 추가 설정은 스프링 부트 메뉴얼을 참고하고, 스프링 부트를 사용하지 않고 순수 스프링과 JPA 설정 방법은
자바 ORM 표준 JPA 프로그래밍 책을 참고하자.
쿼리 파라미터 로그 남기기
로그에 다음을 추가하기 org.hibernate.type : SQL 실행 파라미터를 로그로 남긴다.
외부 라이브러리 사용
https://github.com/gavlyukovskiy/spring-boot-data-source-decorator
스프링 부트를 사용하면 이 라이브러리만 추가하면 된다
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.7'
> 참고: 쿼리 파라미터를 로그로 남기는 외부 라이브러리는 시스템 자원을 사용하므로,
개발 단계에서는 편하게 사용해도 된다. 하지만 운영시스템에 적용하려면 꼭 성능테스트를 하고 사용하는 것이 좋다.
★쿼리 파라미터 로그 남기기 - 스프링 부트 3.0
p6spy-spring-boot-starter 라이브러리는 현재 스프링 부트 3.0을 정상 지원하지 않는다.
스프링 부트 3.0에서 사용하려면 다음과 같은 추가 설정이 필요하다.
1. org.springframework.boot.autoconfigure.AutoConfiguration.imports 파일 추가
src/resources/META-INF/spring/ org.springframework.boot.autoconfigure.AutoConfiguration.imports
com.github.gavlyukovskiy.boot.jdbc.decorator.DataSourceDecoratorAutoConfiguration
폴더명: src/resources/META-INF/spring
파일명: org.springframework.boot.autoconfigure.AutoConfiguration.imports
2. spy.properties 파일 추가
src/resources/spy.properties
appender=com.p6spy.engine.spy.appender.Slf4JLogger
이렇게 2개의 파일을 추가하면 정상 동작한다
메이븐 설정
<dependency> <groupId>com.github.gavlyukovskiy</groupId> <artifactId>p6spy-spring-boot-starter</artifactId> <version>1.9.0</version> </dependency>
[2] 예제 도메인 모델
5. 예제 도메인 모델과 동작확인
강의 :
https://www.inflearn.com/course/lecture?courseSlug=스프링-데이터-JPA-실전&unitId=28002&tab=curriculum
Member 엔티티
package study.datajpa.entity; import lombok.*; import javax.persistence.*; @Entity @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @ToString(of = {"id", "username" , "age"}) public class Member { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name="member_id") private Long id; private String username; private int age; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name = "team_id") private Team team; public Member(String username){ this.username=username; } public Member(String username, int age, Team team) { this.username = username; this.age = age; if(team!=null){ changeTeam(team); } } public void changeUsername(String username){ this.username=username; } public void changeTeam(Team team){ this.team =team; team.getMembers().add(this); } }
롬복 설명
@Setter: 실무에서 가급적 Setter는 사용하지 않기
@NoArgsConstructor AccessLevel.PROTECTED: 기본 생성자 막고 싶은데, JPA 스팩상 PROTECTED로 열어두어야 함
@ToString은 가급적 내부 필드만(연관관계 없는 필드만)
changeTeam() 으로 양방향 연관관계 한번에 처리(연관관계 편의 메소드)
Team 엔티
package study.datajpa.entity; import lombok.*; import javax.persistence.*; import java.util.ArrayList; import java.util.List; @Entity @Getter @Setter @NoArgsConstructor(access = AccessLevel.PROTECTED) @ToString(of = {"id", "name"}) public class Team { @Id @GeneratedValue @Column(name="team_id") private Long id; private String name; @OneToMany(mappedBy = "team") private List<Member> members =new ArrayList<>(); public Team(String name) { this.name = name; } }
Member와 Team은 양방향 연관관계, Member.team 이 연관관계의 주인, Team.members 는 연관관계의
주인이 아님, 따라서 Member.team 이 데이터베이스 외래키 값을 변경, 반대편은 읽기만 가능
데이터 확인 테스트
package study.datajpa.entity; import org.junit.jupiter.api.Test; import org.springframework.boot.test.context.SpringBootTest; import org.springframework.test.annotation.Rollback; import org.springframework.transaction.annotation.Transactional; import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; import java.util.List; import static org.junit.jupiter.api.Assertions.*; @SpringBootTest @Transactional @Rollback(value = false) class MemberTest { @PersistenceContext EntityManager em; @Test public void testEntity(){ Team teamA =new Team("teamA"); Team teamB =new Team("teamB"); em.persist(teamA); em.persist(teamB); Member member1 =new Member("member1", 10,teamA); Member member2 =new Member("member2", 20,teamA); Member member3 =new Member("member3", 30,teamB); Member member4 =new Member("member4", 40,teamB); em.persist(member1); em.persist(member2); em.persist(member3); em.persist(member4); //초기화 em.flush(); em.clear(); List<Member> members=em.createQuery("select m from Member m ", Member.class).getResultList(); for(Member member: members){ System.out.println("member = " + member); System.out.println("member.getTeam() = " + member.getTeam()); } } }
가급적 순수 JPA로 동작 확인 (뒤에서 변경)
db 테이블 결과 확인
지연 로딩 동작 확인
select team0_.team_id as team_id1_1_0_, team0_.name as name2_1_0_ from team team0_ where team0_.team_id in (1, 2); member.getTeam() = Team(id=1, name=teamA) member = Member(id=2, username=member2, age=20) member.getTeam() = Team(id=1, name=teamA) member = Member(id=3, username=member3, age=30) member.getTeam() = Team(id=2, name=teamB) member = Member(id=4, username=member4, age=40) member.getTeam() = Team(id=2, name=teamB
댓글 ( 4)
댓글 남기기