1. 회원 Repository 개발
@Repository
public class MemberRepository {
// EntityManager를 주입할 수 있다.
@PersistenceContext
private EntityManager em;
private void save(Member member) {
em.persist(member);
}
// 조회 - id
private Member findOne(Long id) {
return em.find(Member.class, id);
}
// 전체 조회
private List<Member> findAll() {
// JPQL(Entity를 대상으로 Query)
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
// 조회 - name
private List<Member> findByName(String name) {
return em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
}
}
EntityManager를 통해 Persistence Context(영속성 콘텍스트)에서 persist, find, createQuery를 통해 저장 및 찾을 수 있다. 이 EntityManager를 주입하기 위해 EntityManager에 @PersistenceContext 어노테이션을 붙인다.
다음은 EntityManager의 여러 메서드를 정의한다.
# persist
Persistence Context에 Entity를 저장
# find
public key(id)를 통해 persistence context에서 찾기
# createQuery
JPQL을 사용하여 persistence context에서 찾기. JPQL이란 일반적인 쿼리문과 유사하지만 테이블이 아닌 Entity를 조회하는 문법이다.
이제 Service에서 JPA의 모든 데이터의 변경이나 로직은 트랜잭션 안에서 실행되어야 하기 때문에 @Transactional 어노테이션을 넣을텐데, Persistence Context의 1차 캐시와 변경 감지(Dirty Checking) 기능 덕분에 repository에서 가져온 Entity의 데이터를 수정하면, 트랜잭션이 종료될 때(해당 메서드가 종료될 때) 변경된 Entity의 값이 그대로 데이터베이스에 반영되게 된다.
2. 회원 Service 개발
...
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(readOnly = true) // JPA의 모든 데이터의 변경이나 로직은 트랜잭션 안에서 실행되어야 한다.
public class MemberService {
// 방법 1
@Autowired
private MemberRepository memberRepository;
// 회원 가입
@Transactional
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
List<Member> findMembers = memberRepository.findByName(member.getName());
if (!findMembers.isEmpty()) {
throw new IllegalStateException("이미 존재하는 회원입니다.");
}
}
// 회원 전체 조회
public List<Member> findMembers() {
return memberRepository.findAll();
}
// 회원 조회 - Id
public Member findOne(Long memberId) {
return memberRepository.findOne(memberId);
}
}
회원 가입 - 중복 회원 검증
// 회원 가입
@Transactional
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
List<Member> findMembers = memberRepository.findByName(member.getName());
if (!findMembers.isEmpty()) {
throw new IllegalStateException("이미 존재하는 회원입니다.");
}
}
MemberRepostiory에 구현한 save 함수를 호출한다.
회원 전체 조회, Id 조회
// 회원 전체 조회
public List<Member> findMembers() {
return memberRepository.findAll();
}
// 회원 조회 - Id
public Member findOne(Long memberId) {
return memberRepository.findOne(memberId);
}
@Transactional(readOnly = true)
@Service
@Transactional(readOnly = true) // JPA의 모든 데이터의 변경이나 로직은 트랜잭션 안에서 실행되어야 한다.
public class MemberService {
// 방법 1
@Autowired
private MemberRepository memberRepository;
// 회원 가입
@Transactional
public Long join(Member member) {
validateDuplicateMember(member); // 중복 회원 검증
memberRepository.save(member);
return member.getId();
}
...
}
최상위 Class에도 @Transactional(readOnly = true)가 설정되어 있고, join 메서드에도 @Transactional가 설정되어 있다. 그런데 나머지 메서드에는 @Transactional가 설정되어 있지 않다. 무슨 이유일까?
JPA의 모든 데이터의 변경이나 로직은 트랜잭션 안에서 실행되어야 하기 때문에 @Transactional 어노테이션을 반드시 선언해줘야 한다. 이때 데이터를 Write(쓰기)하는 것이 아닌 Read(조회)하는 경우 readOnly = true 옵션을 주면 더 높은 퍼포먼스를 가져올 수 있다.
하지만 default는 readOnly가 false이다. 현재 Write 하는 메서드는 join 메서드 하나이기 때문에, 최상단 class인 MemberService에 @Transactional(readOnly = true)를 설정하고, 따로 Write 하는 join 메서드에만 @Transactional을 설정한 것이다.
3. 필드에서 정의하는 repository 개선
지금까지는 @Autowired를 통해 외부에서 MemberRepository를 주입하여 사용하였다. 하지만 이렇게 선언되면 매우 편리하게 주입한다는 장점이 있지만 그것 말고는 없어서 스프링 4.3부터는 사용하지 않는 것을 권장한다.
@Autowired
private MemberRepository memberRepository;
스프링에서는 다음과 같은 이유로 생성자 주입을 사용하는 것을 권장한다.
① 순환 참조 방지
② 테스트 코드 작성 용이
③ 객체 변이 방지 (final 가능)
따라서 본 장에서는 @Autowired가 아닌 생성자 주입을 통해 memberRepository를 가져온다.
1차 수정
먼저 Setter Injection 방식을 사용하여 MemberRepository를 정의한다.
private final MemberRepository memberRepository;
@Autowired
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
가장 기본적인 생성자를 사용하여 정의한 방식이다. 이로써 테스트 코드 작성에는 용이하지만, 어플리케이션 구동 중간에 set 메서드를 통해 repository를 변형할 수도 있어 안전하지는 않다.
final을 선언한 이유는 compile 시점에 값이 들어와있는지 체크할 수 있어 개발에 용이하다.
최종 수정(@RequiredArgsConstructor)
생성자 Injection 방식을 통해 MemberRepository를 정의하는 방식이다. 중간에 set 함수를 통해 repository를 변형할 수 없을 뿐더러 TestCase 작성에도 용이하다. 최종 코드이다.
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
...
}
코드가 상당히 많이 정리되었다. 그 과정을 알아보자.
생성자가 하나만 있는 경우에는 @Autowired 없이도 Spring이 자동으로 Injection 해준다.
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
}
여기서 @RequiredArgsConstructor가 나온다. @RequiredArgsConstructor은 final이 있는 필드만 가지고 생성자를 만들어준다. 즉 생성자 부분이 생략 가능하다.
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
// public MemberService(MemberRepository memberRepository) {
// this.memberRepository = memberRepository;
// }
..
}
'🌱 Spring > JPA ①' 카테고리의 다른 글
[Spring] 주문 Entity, Repository, Service 개발 (0) | 2023.08.09 |
---|---|
[Spring] 상품 Entity, Repository, Service 개발 (0) | 2023.08.05 |
[Spring] 회원 Service Unit Test 작성하기 (0) | 2023.08.05 |
[Spring] Entity 설계 및 구현 (0) | 2023.08.03 |
[Spring] 프로젝트 기본 구성 (0) | 2023.08.02 |