별도로 repository 패키지를 만듭니다.
MemberService를 만듭니다.
package jpabook.jpashop.service;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service //...1
@Transactional(readOnly = true) //...5
public class MemberService {
private final MemberRepository memberRepository;
@Autowired //...2
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
//회원 가입 ...3
@Transactional //...5
public Long join(Member member) {
validateDuplicateMember(member); //중복 회원 검증
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
//EXCEPTION
List<Member> findMembers = memberRepository.findByName(member.getName());
if (!findMembers.isEmpty()) { //...4
throw new IllegalStateException("이미 존재하는 회원입니다.");
}
}
//회원 전체 조회
public List<Member> findMembers() {
return memberRepository.findAll();
}
//회원 단건 조회
public Member findOne(Long memberId) {
return memberRepository.findOne(memberId);
}
}
밑에서 조금 자세하게 설명하겠습니다.
회원 가입
validateDuplicateMember()
에서 예외를 터뜨릴 것입니다.JPA에서 em.persist를 하면 영속성 컨텍스트에 member객체를 올립니다. 이 때 id 값이 키가 됩니다.
데이터베이스마다 다르지만 항상 id값이 생성되어있다는게 보장이 됩니다.
→ 자세히
아직 DB에 들어갈 시점이 아니여도 값을 채워줍니다.
예를 들어, call next value ... 가 먼저 날라갑니다.
예외 처리
→ 자세히
@Transactional
JPA의 모든 데이터 변경이나 로직들은 가급적이면 트랜잭션 안에서 실행이 되어야합니다.
@Transactional이 있어야 LAZY 로딩 같은게 다 됩니다.
클래스 레벨에 @Transactional를 붙이면 public 메서드들은 기본적으로 다 @Transactional에 걸려들어갑니다.
@Transactional는 2개가 있는데 스프링이 제공하는 @Transactional를 쓰는게 좋습니다. 그래야지 쓸 수 있는 옵션들이 훨씬 많습니다.
→ 자세히
<aside> ❗ join은 등록입니다. 하지만 findMembers는 조회입니다.
@Transactional에 readOnly = true라는 옵션을 주면 JPA가 조회하는 곳에서 좀 더 성능을 최적화 시켜줍니다.
디테일하게 들어가면 영속성 컨텍스트를 플러시 안하고 더티 체킹을 안하는 것으로 인한 이점도 있고,
추가로 데이터베이스에 따라서 읽기 전용 트랜잭션이라면 DB한테 리소스를 너무 많이 쓰지 말라고 단순히 읽기 전용으로 읽으라고 하는 드라이버들도 있습니다. (물론 드라이버 같은 것은 확인을 해봐야합니다.)
결론적으로, 읽기에는 readOnly = true라는 옵션을 넣어주면 되고 읽기가 아닌 쓰기에는 무조건 readOnly = true를 넣으면 안됩니다. 만약 readOnly = true를 넣게 되면 쓰기가 안됩니다.
현재 MemberService에는 읽기 전용이 더 많습니다.
즉, 클래스 레벨에 @Transactional(readOnly = true)
를 넣어주고 쓰기 전용인 join에 @Transactional
만 넣어주면 됩니다.
기본적으로 @Transactional(readOnly = true)
가 public메서드에 다 먹히고, 따로 @Transactional
설정을 하면 우선권을 가져서 join은 @Transactional(readOnly = false)
가 되는 것입니다.
@Transactional
의 기본 값은 readOnly = false 입니다.
</aside>
<aside> ❗ 개발을 많이 했던 분들은 validateDuplicateMember 로직이 위 처럼 작성해도 문제가 될 것이라는 생각이 들겁니다.
왜냐면 WAS가 동시에 여러개가 뜹니다. 예를 들어, memberA라는 애가 동시에 DB에 insert를 하게 되면 validation로직을 둘 다 통과할 수 있습니다. 그래서 동시에 2명의 memberA가 회원 가입을 하는 것입니다.
실무에서는 한번 더 최후의 방어를 해야합니다. 멀티쓰레드 같은 상황을 고려해서 데이터베이스의 name을 유니크 제약조건으로 잡아주는 것을 권장합니다.
자세하게 봅시다.
보통 이름이 같은 사람도 많은데 name 에 unique를 주어도 되는건지 궁금합니다.
→ 이 예제에서는 이름이 같은 것을 허락하지 않는다는 비즈니스 정책을 잡았기 때문입니다. 게임 같은 경우 생각해보시면 같은 이름을 허용하지 않는 경우가 있습니다. 물론 비즈니스 상황에 따라 이름이 동일해도 되는 경우도 많습니다. 그러면 제약을 하면 안되겠지요.
validateDuplicateMember를 할 때에 동시에 회원가입이 이루어질 때 방지로 name에 유니크를 걸어 주신다고 했는데 이해가 잘 되지를 않습니다.
→ 서버가 한대만 있고, 자바(JVM)로 웹 애플리케이션을 단 하나만 구동하는 상황이면 자바 만으로 동시성 제어를 할 수 있습니다. 그런데 실무에서는 보통 서버 두 대 이상을 사용하기 때문에, 동시성 제어를 JVM안에서 해결하는게 어렵습니다. 관계형 데이터베이스는 이런 동시성 제어를 고려해서 개발되었기 때문에, 결국 관계형 데이터베이스에 동시성 제어를 위임해야 합니다. 그 중에 관계형 데이터베이스가 제공하는 유니크 제약조건을 사용하면, 같은 이름을 절대로 동시에 저장할 수 없습니다. 그래서 name에 유니크 제약조건을 실무에서는 걸어주어야 한다고 이야기 했습니다. 그런데 이런 유니크 제약조건은 정말 최악의 경우(진짜 초 단위로 같은 데이터가 저장되었을 때)가 발생했을 때 동작하는 것이고, 대부분은 validation에서 막힙니다.
</aside>