회원 리포지토리 개발
@Repository
public class MemberRepository {
@PersistenceContext
private EntityManager em;
public void save(Member member) {
em.persist(member);
}
public Member findOne(Long id) {
return em.find(Member.class, id);
}
public List<Member> findAll() {
return em.createQuery("select m from Member m", Member.class)
.getResultList();
}
public List<Member> findByName(String name) {
return em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
}
}
기술 설명
- @Repository: 스프링 빈으로 등록, JPA 예외를 스프링 기반 예외로 예외 변환
- @PersistenceContext: 엔티티 메니저( EntityManager ) 주입
- @PersistenceUnit: 엔티티 메니터 팩토리( EntityManagerFactory ) 주입
회원 서비스 개발
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {
private final 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> findMember() {
return memberRepository.findAll();
}
public Member findOne(Long memberId) {
return memberRepository.findOne(memberId);
}
}
@Transactional
- 트랜잭션, 영속성 컨텍스트
- readOnly=true 옵션
- 데이터의 변경이 없는 읽기 전용 메서드에 사용
- 영속성 컨텍스트를 플러시 하지 않으므로 약간의 성능 향상(읽기 전용에는 다 적용)
- 데이터베이스 드라이버가 지원하면 DB에서 성능 향상
비즈니스 로직에서 조회가 많으면 위 코드처럼 클래스 레벨에 @Transactional(readOnly = true) 옵션을 적용할 수 있다. 트랜잭션이 필요한 메서드에는 @Transactional을 사용하면 된다.
참고
실무에서는 WAS가 동시에 여러 개가 뜬다.
이런 상황에서 사용자 두 명이 동시에 MemberA라는 이름으로 동시에 회원가입을 한다고 하자.
그러면 join() 메서드를 동시에 호출하게 되고 동시에 validateDuplicateMember() 검증을 통과하게 된다.
따라서 memberRepository.save(member); 로직을 동시에 호출할 수 있는 문제가 발생한다.
그래서 실무에서는 검증 로직이 있어도 멀티 쓰레드 상황을 고려해서 회원 테이블의 회원명 컬럼에 유니크 제약 조건을 추가하는 것이 안전하다.
회원 기능 테스트
@SpringBootTest
@Transactional
class MemberServiceTest {
@Autowired
MemberService memberService;
@Autowired
MemberRepository memberRepository;
@Test
public void 회원가입() throws Exception {
Member member = new Member();
member.setName("memberA");
Long findMemberId = memberService.join(member);
assertEquals(member, memberRepository.findOne(findMemberId));
}
@Test
public void 중복_회원_예외() throws Exception {
//Given
Member member1 = new Member();
member1.setName("kim");
Member member2 = new Member();
member2.setName("kim");
//When
memberService.join(member1);
//Then
assertThrows(IllegalStateException.class, () -> { memberService.join(member2); });
}
}
@SpringBootTest
- 스프링 부트 띄우고 테스트(이게 없으면 `@Autowired` 다 실패)
@Transactional
- 반복 가능한 테스트 지원한다.
- 각각의 테스트를 실행할 때마다 트랜잭션을 시작하고 테스트가 끝나면 트랜잭션을 강제로 롤백한다.
- (이 어노테이션이 테스트 케이스에서 사용될 때만 롤백한다)
JPA에서 같은 트랜잭션에서는 ID(PK)값이 같으면 영속성 컨텍스트에서 같은 엔티티로 관리된다.
그래서 위 테스트 코드에서 같은 ID로 조회 했을때 같은 엔티티가 조회되는 이유이다.
이게 가능한 이유는 @Transactional을 사용했기 때문이다.
첫 번째 테스트 코드 쿼리 실행 결과
Hibernate:
select
m1_0.member_id,
m1_0.city,
m1_0.street,
m1_0.zipcode,
m1_0.name
from
member m1_0
where
m1_0.name=?
Hibernate:
insert
into
member
(city, street, zipcode, name, member_id)
values
(?, ?, ?, ?, default)
첫 번째 테스트 코드를 실행하면 위와 같이 쿼리가 출력된다.
@Rollback(value = false)을 설정해야 @Transactional 사용시 자동으로 롤백을 실행하지 않아 insert 쿼리가 발생한다. 하지만 위 첫 번째 테스트 코드에서는 @Rollback(value = false) 사용하지 않았지만 Insert 쿼리가 발생한다.
그 이유가 뭘까?
그 이유는 GenerationType.IDENTITY을 설정했기 때문이다.
이 옵션 사용 시 em.persist()를 하는 순간 insert 쿼리가 실행되고 해당 엔티티의 id값을 받아온다.
@Entity
@Getter
@Setter
public class Member {
@Id
@Column(name = "member_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
}
'JPA > JPA 활용 1' 카테고리의 다른 글
7. 웹 계층 개발 (0) | 2024.08.22 |
---|---|
6. 주문 도메인 개발 (0) | 2024.08.21 |
5. 상품 도메인 개발 (0) | 2024.08.20 |
2. 도메인 분석 설계 (0) | 2024.08.20 |
1. 프로젝트 환경설정 (0) | 2024.08.20 |