이번 강의는 새로 학습한 내용이나 실무에서 중요하다고 하신 내용만 정리하려고 한다.
1. 예제 도메인 모델
도메인 모델 동작 확인
아래 코드에 적용된 롬복(Lombok)에 대해 간단하게 설명한다.
- @Setter
- 실무에서는 가급적 Setter는 사용하지 않기
- @NoArgsConstructor(AccessLevel.PROTECTED)
- 기본 생성자를 막고 싶어도 JPA 스펙상 PROTECTED로 열어 둬야 함
- @ToString
- 가급적 연관관계가 없는 내부 필드에만 사용하기
@Entity
@Getter @Setter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@ToString(of = {"id", "username", "age"})
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, 0);
}
public Member(String username, int age) {
this(username, age, null);
}
public Member(String username, int age, Team team) {
this.username = username;
this.age = age;
if (team != null) {
changeTeam(team);
}
}
public void changeTeam(Team team) {
this.team = team;
team.getMembers().add(this);
}
}
- changeTeam() 메서드로 양방향 연관관계를 한 번에 처리할 수 있다. 이런 메서드를 연관관계 편의 메서드라고 부른다.
참고
가급적이면 순수 JPA로 동작하는 걸 확인해야 한다.
- DB 테이블 결과 확인
- 지연 로딩(Lazy Loading) 동작 확인
2. 공통 인터페이스 기능
순수 JPA 기반 리포지토리
예시로 사용할 순수 JPA 기반 리포지토리인 MemberJpaRepository만 작성해 둔다.
@Repository
public class MemberJpaRepository {
@PersistenceContext
private EntityManager em;
public Member save(Member member) {
em.persist(member);
return member;
}
public void delete(Member member) {
em.remove(member);
}
...
}
참고
JPA에서 수정은 변경 감지(Dirty Checking) 기능을 사용하면 된다. 트랜잭션 안에서 엔티티를 조회한 다음에 데이터를 변경하면, 트랜잭션 종료 시점에 변경 감지 기능이 작동해서 변경된 엔티티를 감지하고 UPDATE SQL을 실행한다.
공통 인터페이스 설정
아래 코드처럼 JavaConfig 설정을 만들어야 한다. 스프링 부트를 사용한다면 생략해도 된다.
- 스프링 부트 사용 시 @SpringBootApplication 애노테이션 위치를 지정하면 해당 패키지와 하위 패키지를 인식할 수 있다.
- 만약 위치가 달라지면, 아래 코드처럼 @EnableJpaRepositories 애노테이션에 위치를 따로 설정해야 한다.
@Configuration
@EnableJpaRepositories(basePackages = "jpabook.jpashop.repository")
public class AppConfig {
...
}
인터페이스만 만들어 놔도 아래 그림처럼 스프링 데이터 JPA가 구현 클래스를 대신 생성해 넣어준다.
- org.springframework.data.repository.Repository를 구현한 클래스가 스캔 대상이다.
- MemberRepository 인터페이스가 동작한 이유가 바로 이것 때문이다.
- 실제 출력해 보면 프록시 객체가 출력된다.
- memberRepository.getClass() → class com.sun.proxy.$ProxyXXX
- 따라서 @Repository 애노테이션을 생략해도 된다.
- 컴포넌트 스캔을 스프링 데이터 JPA가 자동으로 처리한다.
- JPA 예외를 스프링 예외로 변환하는 과정도 자동으로 처리한다.
공통 인터페이스 적용 및 분석
순수 JPA로 구현한 MemberJpaRepository 대신 스프링 데이터 JPA가 제공하는 공통 인터페이스를 사용해 보자. JpaRepository 인터페이스를 사용하면 대부분의 공통 CRUD 메서드를 제공한다. 제네릭은 <엔티티 타입, 식별자 타입>으로 설정하면 된다.
- 다른 DB를 쓰더라도 정렬(Sort)과 페이징(Pageable)은 RDB든 NoSQL이든 다 비슷하기 때문에 공통 인터페이스가 제공된다.
// JpaRepository 공통 기능 인터페이스
public interface JpaRepository<T, ID extends Serializable>
extends PagingAndSortingRepository<T, ID> {
...
}
///
// JpaRepository를 사용하는 인터페이스
public interface MemberRepository extends JpaRepository<Member, Long> {
}
참고
인터페이스를 상속받을 땐 extends를 사용한다.
JpaRepository는 대부분의 공통 메서드를 제공한다. 주요 메서드를 알아보자. 제네릭 타입은 T(엔티티), ID(엔티티의 식별자 타입), S(엔티티와 그 자식 타입)으로 보면 된다.
- save(S)
- 새로운 엔티티는 저장하고, 이미 있는 엔티티는 병합한다.
- 내부에서 EntityManger.persist() 또는 EntityManager.merge() 메서드를 호출한다.
- delete(T)
- 엔티티 하나를 삭제한다.
- 내부에서 EntityManager.remove() 메서드를 호출한다.
- findById(ID)
- 엔티티 하나를 조회한다.
- 내부에서 EntityManager.find() 메서드를 호출한다.
- getOne(ID)
- 엔티티를 프록시로 조회한다.
- 내부에서 EntityManager.getReference() 메서드를 호출한다.
- findAll(...)
- 모든 엔티티를 조회한다.
- 정렬(Sort)이나 페이징(Pageable) 조건을 파라미터로 제공할 수 있다.
주의
위 그림의 CRUD 인터페이스에서 몇 가지 수정 사항이 있다.
T findOne(ID) → Optional<T> findById(ID)
boolean exists(ID) → boolean existsById(ID)