멤버 서비스

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 메서드는 필요 없습니다.
         */
    }
}