Notice
Recent Posts
Recent Comments
Link
일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | |||
5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 | 20 | 21 | 22 | 23 | 24 | 25 |
26 | 27 | 28 | 29 | 30 | 31 |
Tags
- 자바의 정석 기초편 ch9
- 2024 정보처리기사 시나공 필기
- 자바의 정석 기초편 ch1
- 코드로 시작하는 자바 첫걸음
- 스프링 mvc2 - 타임리프
- 스프링 mvc2 - 로그인 처리
- jpa 활용2 - api 개발 고급
- 자바의 정석 기초편 ch11
- 자바의 정석 기초편 ch8
- 자바의 정석 기초편 ch13
- 스프링 mvc2 - 검증
- 자바의 정석 기초편 ch6
- 2024 정보처리기사 수제비 실기
- 스프링 입문(무료)
- 자바의 정석 기초편 ch12
- 스프링 db2 - 데이터 접근 기술
- 자바의 정석 기초편 ch3
- 타임리프 - 기본기능
- 자바의 정석 기초편 ch7
- 자바의 정석 기초편 ch2
- 스프링 mvc1 - 서블릿
- jpa - 객체지향 쿼리 언어
- 자바의 정석 기초편 ch5
- 스프링 mvc1 - 스프링 mvc
- @Aspect
- 게시글 목록 api
- 스프링 고급 - 스프링 aop
- 자바의 정석 기초편 ch4
- 자바의 정석 기초편 ch14
- 스프링 db1 - 스프링과 문제 해결
Archives
- Today
- Total
나구리의 개발공부기록
프로젝트 환경설정, 예제 도메인 모델 및 동작확인, 공통 인터페이스 기능, 순수 JPA 기반 리포지토리 만들기, 공통 인터페이스 설정/적용/분석 본문
인프런 - 스프링부트와 JPA실무 로드맵/실전! 스프링 데이터 JPA
프로젝트 환경설정, 예제 도메인 모델 및 동작확인, 공통 인터페이스 기능, 순수 JPA 기반 리포지토리 만들기, 공통 인터페이스 설정/적용/분석
소소한나구리 2024. 10. 25. 23:00출처 : 인프런 - 실전! 스프링 데이터 JPA (유료) / 김영한님
유료 강의이므로 정리에 초점을 두고 코드는 일부만 인용
1. 프로젝트 환경설정
1) 프로젝트 생성
(1) Project
- Gradle - Groovy
- Java 17
- Spring Boot 3.3.5
(2) Metadata
- Group - study
- Artifact - data-jpa
- Packaging - jar
(3) Dependencies
- Spring Web
- spring Data JPA
- H2 database
- Lombok
2) H2 데이터베이스 설정
- 스프링부트 3.x 이상 버전이기때문에 H2데이터베이스 버전이 2.2.224버전이므로 해당 버전과 맞춰서 설치를 진행
- 최초 db파일을 생성해야하므로 jdbc:h2:~/datajpa로 홈경로에 db파일을 생성
- 그 이후에는jdbc:h2:tcp://localhost/~/datajpa 경로로 접속
** 참고
- db파일 생성시 원하는 경로에 생성할 경우 ~(home)문법이아닌 ./경로/파일명으로 적어주어야 파일이 생성되며 그이후에 tcp로 접속할때에는 ~/경로/파일명으로 접속해도됨
- 그리고 db파일을 생성하지 않고 tcp로 접속하면 들어가지긴 하는데, 이후 종료했을때 제대로 종료가 안되기 때문에 db파일을 생성한 후 해당파일의 경로로 tcp 접속을 하는 것을 권장함
3) 스프링 데이터 JPA와 DB설정 및 동작확인
(1) application.yml로 변경
- 기존 생성된 application.properties를 삭제하고 설정파일을 yml로 작성
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/h2db/datajpa
username: sa
password:
driver-class-name: org.h2.Driver
jpa:
hibernate:
ddl-auto: create
properties:
hibernate:
format_sql: true
highlight_sql: true # 콘솔에 sql구문의 가독성을 높임, p6spy 라이브러리를 사용하는것을 더 권장함
logging.level:
org.hibernate.SQL: debug
org.hibernate.orm.jdbc.bind: trace # SQL 파라미터 바인딩 로그
(2) Test를위한 클래스들 생성
- entity 패키지에 Member 클래스를, repository 클래스에 MemberJpaRepository클래스를 생성
- setter를 사용하지 않고 생성자를 통해 회원을 생성
- 실무에서 권장되는 방법이며, 필드의 값을 변경할 때도 setter가 아니라 수정할 수 있는 메서드를 생성하여 하는 것이 권장됨(계속 강의에서 강조되는 내용)
- JPA 동작 메커니즘 때문에 기본생성자를 생성, 자세한 내용은 JPA 강의 참조
- 여기서 생성한 repository는 일반 JPA로 동작하는 repository임
// Member클래스
@Entity
@Getter
public class Member {
@Id @GeneratedValue
private Long id;
private String username;
protected Member() {}
public Member(String username) {
this.username = username;
}
}
// MemberJpaRepository 클래스
@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {
private final EntityManager em;
public Member save(Member member) {
em.persist(member);
return member;
}
public Member find(Long id) {
return em.find(Member.class, id);
}
}
(3) Test 수행 - MemberJpaRepositoryTest생성
- JUnit5로 생성한 Member와 MemberJpaRepository의 메소드들을 테스트 진행
- @Transactional을 테스트에서 적용하면 테스트가 종료하고 Rollback을 하기 때문에 안전하게 테스트를 수행할 수 있음
@SpringBootTest
@Transactional
class MemberJpaRepositoryTest {
@Autowired MemberJpaRepository memberJpaRepository;
@Test
public void testMember() {
Member member = new Member("memberA");
Member savedMember = memberJpaRepository.save(member);
Member findMember = memberJpaRepository.find(savedMember.getId());
assertThat(findMember.getId()).isEqualTo(member.getId());
assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
assertThat(findMember).isEqualTo(member);
}
}
(3) Spring Data JPA로 동작하는 Repository 생성
- 인터페이스로 repository를 생성하고 JpaRepository를 상속받으면 끝이며 JPA강의에서 직접 생성했었던 기능보다 훨씬 수많은 기능을 공짜로 매우 간편하게 사용할 수 있음
public interface MemberRepository extends JpaRepository<Member, Long> {
}
(4) Test 수행
- Repository에 메서드들을 만들지 않았는데, 기본 메서드들이 모두 제공되고있음
- 테스트를 수행해보면 위에서 JPA로 동작하는 Repository와 완전 동일하게 동작하는 로그를 확인할 수 있음
@SpringBootTest
@Transactional
//@Rollback(false) // 적용하면 롤백되지않고 DB에 데이터가 반영됨
class MemberRepositoryTest {
@Autowired MemberRepository memberRepository;
@Test
public void testMember() {
Member member = new Member("memberA");
// 스프링 데이터 JPA가 제공하는 save와 findById 메서드, findById는 기본 반환타입이 Optional임
Member saveMember = memberRepository.save(member);
Member findMember = memberRepository.findById(saveMember.getId()).get();
assertThat(findMember).isEqualTo(saveMember);
assertThat(findMember.getId()).isEqualTo(saveMember.getId());
assertThat(findMember.getUsername()).isEqualTo(saveMember.getUsername());
}
}
** 참고
- 쿼리 파라미터 로그를 남기기 위해 기본 hibernate설정이 아닌 아래의 외부 라이브러리도 많이 사용함
- 다만 시스템 자원을 사용하기 때문에 개발 단계에서는 편하게 사용하되 운영 시스템에 적용하려면 꼭 성능 테스트를 하고 사용해야함
- 스프링 부트 버전마다 적용해야하는 버전이 다르기 때문에 사이트에서 내용을 참고해서 적용하면됨
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.9.0'
2. 예제 도메인 모델 - 예제 도메인 모델과 동작 확인
1) 도메인 모델의 다이어그램
2) Entity 생성
- 실무와 비슷하게 하기위해 가급적 Setter를 사용하지 않도록 생성
- 예제이기 때문에 다시 @Setter를 사용하도록 변경하며 실무에서는 Setter를 권장하지 않음
(1) Member 수정 및 Team 생성
- @NoArgsConstructor 애노테이션을 적용하여 기본생성자를 롬복으로 적용
- @ToString 애노테이션으로 객체를 출력해도 해당 필드를 쉽게 출력하도록 설정
- 해당 매핑에 대해서 설명은 JPA 기본강의 내용을 참고해야함
package study.data_jpa.entity;
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED) // 기본생성자를 생성하는 롬복 애노테이션, 제어자를 PROTECTED로 설정
@ToString(of = {"id", "username", "age"}) // toString을 적용하는 롬복 애노테이션, 연관관계 필드는 toString적용 제외
public class Member {
@Id @GeneratedValue
@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);
}
}
// 연관관계 편의 메서드 - team을 변경할 때 해당 메서드를 호출하면 Member와 Team의 연관필드를 모두 수정
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
package study.data_jpa.entity;
@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;
}
}
(2) 생성한 Entity Test
- 순수 JPA로 동작을 확인하고 이후 강의에서 Spring Data JPA를 적용
package study.data_jpa.entity;
@SpringBootTest
@Transactional
@Rollback(false)
class MemberTest {
// JPA로 동작 확인
@PersistenceContext
private 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();
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());
}
}
}
3. 순수 JPA 기반 리포지토리 생성
** 순수 JPA 리포지들을 Spring Data JPA로 변경하면서 순수 JPA의 한계를 어떻게 극복하는지 알 수 있음
1) MemberJpaRepository 수정
- 기본적인 JPA를 사용한 메서드들을 작성
- 스프링데이터 JPA와 비교하기위해 Optional로 반환하는 단건조회 메서드를 추가
- update는 JPA의 변경감지기능을 활용하기 때문에 메서드를 만들 필요가 없음
package study.data_jpa.repository;
@Repository
@RequiredArgsConstructor
public class MemberJpaRepository {
private final EntityManager em;
// 저장
public Member save(Member member) {
em.persist(member);
return member;
}
// 삭제
public void delete(Member member) {
em.remove(member);
}
// 전체조회
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class).getResultList();
}
// 단건조회 - 멤버를반환
public Member find(Long id) {
return em.find(Member.class, id);
}
// 단건조회 - 옵셔널로반환(스프링데이터JPA의 반환방식)
public Optional<Member> findById(Long id) {
Member member = em.find(Member.class, id);
return Optional.ofNullable(member);
}
// 개수 반환
public long count() {
return em.createQuery("select count(m) from Member m", Long.class).getSingleResult();
}
}
2) TeamJpaRepository 생성
- MemberJpaRepository와 타입만 다르고 완전 똑같기 때문에 코드 생략
- 디테일한 쿼리가 달라질뿐이지 이런 기본적인기능들은 코드 구성이 비슷함
3) 생성한 리포지토리의 CURD 테스트
- Team은 완전히 동일하기 때문에 MemberJpaRepository만 테스트
- 기존에 생성했던 MemberJpaRepositoryTest에 basicCRUD 테스트케이스를 추가하여 진행
class MemberJpaRepositoryTest {
// ... 기존 코드 생략
@Test
public void basicCRUD() { // repository의 CRUD 테스트
Member memberA = new Member("memberA");
Member memberB = new Member("memberB");
memberJpaRepository.save(memberA);
memberJpaRepository.save(memberB);
// 단건조회 테스트
Member findMember1 = memberJpaRepository.findById(memberA.getId()).get();
Member findMember2 = memberJpaRepository.findById(memberB.getId()).get();
assertThat(findMember1).isEqualTo(memberA);
assertThat(findMember2).isEqualTo(memberB);
findMember1.setUsername("변경하자이름을"); // 수정 테스트
assertThat(findMember1.getUsername()).isEqualTo("변경하자이름을");
List<Member> all = memberJpaRepository.findAll(); // 다건 조회 테스트
assertThat(all.size()).isEqualTo(2);
long count = memberJpaRepository.count(); // count 테스트
assertThat(count).isEqualTo(2);
memberJpaRepository.delete(memberA); // 삭제 테스트
memberJpaRepository.delete(memberB);
long deleteCount = memberJpaRepository.count(); // 다시 count 테스트
assertThat(deleteCount).isZero(); // 0개가 되어야함
}
}
4. 공통 인터페이스 기능 - 설정 / 적용 / 분석
1) 스프링 데이터 JPA설정
- 스프링 데이터 JPA를 설정하려면 main메서드가있는 애플리케이션 클래스에 적용할 리포지토리를 @EnableJpaRepositories애노테이션으로 패키지를 포함하여 지정해줘야함
** 참고
- 해당 설정은 스프링 부트 사용시 생략 가능하기때문에 대부분 수동 설정을 하지않음
@SpringBootApplication
@EnableJpaRepositories(basePackages = "study.data_jpa.repository")
public class DataJpaApplication {
// main 메서드
}
2) 스프링 데이터 JPA 구조
- 스프링 데이터 JPA는 생성한 Repository 인터페이스를 스프링 데이터 JPA와 관련된 인터페이스를 상속하기만하면 기본적인 메서드들을 구현하지 않아도 사용할 수 있는데, 그 이유는 자동으로 구현체를 만들기 때문임
- 실제로 스프링 데이터 JPA관련된 인터페이스를 상속받은 Repository 인터페이스를 getClass로 출력해보면 Proxy로 동작하고있는 것을 확인할 수 있음
- 순수 JPA 리포지토리에서는 @Repository 애노테이션을 붙혀야했지만 스프링 데이터 JPA로 사용할 경우에는 알아서 찾아서 동작하기 때문에 컴포넌트 스캔기능과 스프링을 예외로 변환하는 과정도 모두 자동으로 처리해줌
3) 공통 인터페이스 적용
- MemberRepository이미 만들어봤기 때문에 적용이 끝났음
- TeamRepository도 스프링 데이터 JPA로 만들어서 적용
public interface TeamRepository extends JpaRepository<Team, Long> {
}
- 스프링 데이터 JPA로 만든 MemberRepository의 CRUD를 테스트하기위해 기존에 순수 JPA로 만든 Repository를 테스트했던 basicCRUD 테스트 메서드를 그대로 의존관계만 변경하여 적용해보면 테스트가 정상적으로 동작함
- 그이유는 JPA의 메서드를 구현했을 때 스프링 데이터 JPA의 메서드이름과 동일하게 구현했기 때문임
- 즉, 이러한 기본적인 메서드들이 전부 상속받은 스프링 데이터 JPA 관련 인터페이스에 모두 정의 되어있다는 뜻
4) 공통 인터페이스 분석
(1) 상속 관계도
- JpaRepository를 들어가서 확인해보면 다양한 각각의 Crud, Paging, 상세 Query관련된 스프링 데이터 인터페이스들을 또 상속받고 있음
- 패키지 구조를 보면 JpaRepository는 jpa.repository에 있지만, 그 상위 인터페이스들은 data.repository에 바로 정의되어있음
- 그렇기 때문에 JpaRepository를 사용하면 모든 스프링 데이터 JPA의 기능들을 전부 사용할 수 있음
- JpaRepository인터페이스 -> 스프링 데이터 관련된 Repository 인터페이스들 -> 최상단에 Repository 인터페이스의 구조로 되어있다고 생각하면 되며 스프링 데이터 관련된 Repository의 인터페이스들은 어떤 DB에서 사용하더라도 공통으로 기능들이 적용할 수 있으며, 각 DB마다 상세로 구현되는 인터페이스도 별도로 제공됨
public interface JpaRepository<T, ID> extends ListCrudRepository<T, ID>
, ListPagingAndSortingRepository<T, ID>
, QueryByExampleExecutor<T> {
(2) 주요 메서드
- save, delete, findById, getOne(엔터티를 프록시로 조회), findAll .... 등등 수많은 생각하는 기능들이 기본으로 제공되고 있어 메서드의 이름만 알면됨(잘 모르면 엔터페이스에 들어가서 확인해보면됨)
- findAll 메서드는 정렬이나 페이징조건을 파라미터로 제공할 수 있음
** 참고
- findOne(id) -> 반환타입이 Optional로 되고 findById(id)로 변경됨
- exists(id) -> existsById(id)로 변경됨
- 공통 인터페이스가 아닌 다른 기능을 구현하고자할 때는 쿼리 메소드의 기능이나, 쿼리를 직접 구현하는 애노테이션을 활용하여 커스텀 할 수 있음