멤버 서비스
package com.jpabook.jpashop.domain.service;
import com.jpabook.jpashop.domain.entity.Member;
import com.jpabook.jpashop.domain.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.util.List;
@Service
@Transactional(readOnly = true) // 얘가 있어야 레이지로딩 이런 거 다 됨. 데이터 변경하는 것은 트랜잭션이 꼭 있어야함.
@RequiredArgsConstructor // 얘를 해주면 final이 있는 필드만 가지고 생성자를 만들어줌
public class MemberService {
//@Autowired // 필드 injection
private final MemberRepository memberRepository; // 테스트 하거나할 때 얘를 바꿔아햐는데 못바꿈.그래서 보통 세터 인젝션을 씀.
/*
@Autowired // 그래서 바로 주입하는 것이 아니라, 이렇게 들어와서 주입을 해줌. -> 세터 인젝션.
public void setMemberRepository(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
} // 세터 인젝션의 장점은 테스트 코드 같은거 작성할 때 '목' 같은 걸 내가 여기에 직접 주입할 수가 있다. 이게 큰 장점이다.(필드 인젝션은 어려움)
// 세터인젝션의 단점은 이게 치명적인 건데, 어떤 것이냐면 이게 한 번 뭔가 정말 런타임에 실제 애플리케이션 돌아가는 시점에 누군가가 이걸 바꿀 수 있다.
// 이게 큰 단점이다. 그래도 잘 생각해보면 이걸 중간에 바꿀일이 있을까?
// -> 보통은 없다. 보통 애플리케이션 로딩 시점에 조립이 다 끝나버려서 멤버 서비스는 이 피로지토리를 쓰고, 이게 다 끝나버리게 된다.
// -> 애플리케이션 실행해서 딱 스프링 올라오는 타이밍에 그 세팅 조립이 다 끝나버리게된다.
// -> 이거를 조립한 이후에 내가 뭔가 실제 애플리케이션 동작을 잘 하고 있는데 바꿀 일이 없단 말이다.
// -> 즉, 셋터 인젝션이 안 좋다는 것이다.
*/
/**
* 그래서 요즘 권장하는 방식은 __생성자 인젝션__ 쓰는 것이다.
* 아래와 같이 쓰면 된다. 근데 요즘은, 롬복의 @RequiredArgsConstructor 를 쓰는 추세.
*/
// @Autowired
// public MemberService(MemberRepository memberRepository) { // 스프링이 뜰 때 생성자에서 이거를 인젝션 해준다,
// this.memberRepository = memberRepository;
// }
/**
* 회원가입
*/
@Transactional // 중요한 write에는 트랜잭션을 해야함. 절대 읽기전용으로하면 안됨,
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()); // db에 멤버의 네임(member.getName)을 유니크제약 조건으로 잡아주는 것을 권장.
if (!findMembers.isEmpty()) {
throw new IllegalStateException("이미 존재하는 회원입니다");
}
}
// 회원 전체 조회
public List<Member> findMembers() {
return memberRepository.findAll();
}
public Member findOne(Long memberId) {
return memberRepository.findOne(memberId);
}
}
생성자 injection을 권장 → 변경 불가능한 안전한 객체 생성이 가능하기 때문이다.
→ 생성자가 하나라면, @Autowired
어노테이션을 생략할 수 있다.
→ final
키워드를 추가하면 컴파일 시점에 memberRepository
를 설정하지 않는 오류를 체크할 수 있다.
참고: 스프링 데이터 JPA를 사용하면
EntityManager
도 주입 가능 → 어떻게 가능하다는 것이지 ? 스프링 데이터 JPA를 사용하면EntityManager
를 주입받을 수 있다는 말은 스프링 프레임워크가 JPA의EntityManager
를 자동으로 관리하고, 개발자가 필요할 때 이를 쉽게 사용할 수 있게 해준다는 뜻이며,EntityManager
는 엔티티를 저장, 조회, 수정, 삭제 등을 관리하는 역할을 하며, JPA 작업의 시작점이다.import javax.persistence.EntityManager; import javax.persistence.PersistenceContext; @Service public class Service { /** EntityManager 주입 */ @PersistenceContext private EntityManager entityManager; // ... 클래스의 다른 내용 ... }
그렇지만 내가 권장하는 방식은 아래와 같다.
@Service @Transactional(readOnly = true) // 얘가 있어야 레이지로딩 이런 거 다 됨. 데이터 변경하는 것은 트랜잭션이 꼭 있어야함. @RequiredArgsConstructor // 얘를 해주면 final이 있는 필드만 가지고 생성자를 만들어줌 public class MemberService { private final MemberRepository memberRepository; // 테스트 하거나할 때 얘를 바꿔아햐는데 못바꿈.그래서 보통 세터 인젝션을 씀.
멤버 리포지토리
package com.jpabook.jpashop.domain.repository;
import com.jpabook.jpashop.domain.entity.Member;
import jakarta.persistence.EntityManager;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import java.util.List;
@Repository
@RequiredArgsConstructor // #3,
public class MemberRepository {
//@PersistenceContext // #1, 이렇게 해주면 이제 스프링이 이 엔티티manager를 만들어서 걔를 여기다 주입해준다.(익젝션 해줌)
// jpa를 쓰면 @PersistenceContext 대신
// @Autowired // #2, 얘를 쓸 수 있게된다. 근데 얘보다 더 좋은게 있다.
private final EntityManager em;
// 얘도 이렇게 생성자 주입이 가능함.
/* #3, 주석해줘야함.
public MemberRepository(EntityManager em) {
this.em = em;
}*/
//매니저 팩토리를 직접 주입받고 싶을때엔 이렇게 해주는데, 거의 쓸 일이 없음. 어차피 엔티티 매니저를 사용할 수 있으니까.
/*@PersistenceUnit
private EntityManagerFactory entityManagerFactory;*/
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();
/*
sql은 테이블을 대상으로 쿼리를 하는데, 얘는 엔티티 객체를 대상으로 쿼리를 한다고 보면 된다.
멤버에 대한 엔티티 객체에 대한 alias를 m으로 주고 엔티티 멤버를 조회해 라는 문법이다.
*/
}
public List<Member> findByName(String name) {
return em.createQuery("select m from Member m where m.name = :name", Member.class)
.setParameter("name", name)
.getResultList();
}
}
package com.jpabook.jpashop.domain.service;
import com.jpabook.jpashop.domain.entity.Member;
import com.jpabook.jpashop.domain.repository.MemberRepository;
import jakarta.persistence.EntityManager;
import org.junit.jupiter.api.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
import static org.junit.jupiter.api.Assertions.*;
@RunWith(SpringRunner.class) // 1
@SpringBootTest // 2 1,2 두 개가 있어야 스프링이랑 인티게이션해서 스프링부트를 실제 딱 올려서 테스트할 수 있다.
@Transactional // 데이터를 변경해야하기 때문에 이게 있어야 롤백이 가능
class MemberServiceTest {
@Autowired
MemberService memberService;
@Autowired
MemberRepository memberRepository;
@Autowired
EntityManager em; // db에 쿼리 날리는 것을 보고 싶을때
@Test
@Rollback(value = false)
// db에 들어가는게 맞는지 db에서 눈으로 확인할 수 있음.
void 회원가입() throws Exception {
// 테스트는 항상 given when then
// given
Member member = new Member();
member.setName("kim");
//when
Long savedId = memberService.join(member);
//then
em.flush(); // 이렇게하면 쿼리에 있던게 db에 반영해줌.
assertEquals(member, memberRepository.findOne(savedId));
}
@Test // (expected = IllegalStateException.class) => junit4 방식
void 중복_회원_예외() throws Exception {
//given
Member member1 = new Member();
member1.setName("kim");
Member member2 = new Member();
member2.setName("kim");
//when
memberService.join(member1);
// memberService.join(member2); // 예외가 발생해야 한다 !!!
/*
이건 junit 4방식.
try {
memberService.join(member2); // 예외가 발생해야 한다 !!!
} catch (IllegalStateException e) {
return;
}
*/
/**
* junit5 테스트 방식.
*/
assertThrows(IllegalStateException.class, () -> {
memberService.join(member2); // 여기서 예외가 발생해야 합니다.
});
//then
// fail("예외가 발생해야 한다."); -> junit 5로 할 시 지워줌.
/*
memberService.join(member2) 호출 시 IllegalStateException이 발생하면 테스트는 성공하고,
예외가 발생하지 않으면 자동으로 실패합니다. 따라서, fail 메서드는 필요 없습니다.
*/
}
}