상세 컨텐츠

본문 제목

4. 회원 도메인 개발

본문

# 추가로 찾아볼 키워드

트랜잭션

트랜잭션에 커밋

rollback

persist

영속성컨텍스트

jpql

 

# JpashopApplication

package jpabook.jpashop;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

// @SpringBootApplication 이 있는 패키지와 하위 패키지에 있는걸 다 컴포넌트 스캔해서 스프링 빈에 자동 등록
@SpringBootApplication
public class JpashopApplication {

   public static void main(String[] args) {
      
      SpringApplication.run(JpashopApplication.class, args);
   }

}

 

# MemberRepository

package jpabook.jpashop.repository;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jpabook.jpashop.domain.Member;
import org.springframework.stereotype.Repository;

import java.util.List;

// @Repository 사용하면 컴포넌트 스캔에 의해 자동으로 스프링 빈에 등록돼서 관리 됨
@Repository
public class MemberRepository {

    // JPA 를 사용하므로, JPA 가 제공하는 표준 어노테이션인 @PersistenceContext 사용
    // -> 스프링이 EntityManger 를 만들어서 여기에 injection 해줌
    @PersistenceContext
    private EntityManager em;

    // JPA 가 저장해줌
    public void save(Member member) {
        // persist 하면 영속성 컨텍스트에 member 객체를 넣음
        // 트랜잭션이 commit 되는 시점에 db에 반영 (-> db에 insert 쿼리 날리는 것)
        em.persist(member);
    }

    public Member findOne(Long id) {
        // find(타입, pk)
        return em.find(Member.class, id);
    }


    // <둘의 차이> from 의 대상
    // SQL : 테이블을 대상으로 쿼리
    // JPQL : 엔티티 객체를 대상으로 쿼리
    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

@PersistenceContext

@PersistenceUnit

 

# Service

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
// 데이터 변경, 데이터 관련 로직들은 기본적으로 꼭 트랜잭션이 있어야 함 -> 트랜잭션 안에서 실행되어야 함
@Transactional(readOnly = true)
// (1)
// -> public 메서드들은 transaction 에 걸려 들어감
// javax 가 제공하는, spring 이 제공하는 transaction 이 있는데, 이미 spring 사용하는 중이니까 spring 이 제공하는거 사용 추천
// (+ 사용할 수 있는 기능 더 많음)
// (2)
// 읽기 전용 (조회) 인 곳에서 (readOnly = true) 사용하면
// jpa 가 조회하는 곳에서는 트랜잭션이 성능 더 최적화
// (읽기 전용 아닌 곳에서 사용 x)
// 여기에서는 조회가 더 많으니까 (readOnly = true) 를 디폴트로 두기
// -> public 인 곳에 자동으로 @Transactional(readOnly = true) 적용되고, 따로 @Transactional 해둔 곳엔 그렇게 적용
// 원래 (readOnly = False) 이게 디폴트
public class MemberService {

    // 스프링 빈에 등록되어있는 MemberRepository 를 injection 해줌 -> 필드 injection
    @Autowired
    private MemberRepository memberRepository;

    @Transactional
    // 회원 가입
    public Long join(Member member) {
        // 중복 회원 검증
        validateDuplicateMember(member);

        memberRepository.save(member);
        return member.getId();
    }

    // 실무에서는 멀티 스레드 상황을 고려해서 (ex) 동시에 같은 이름을 가진 member 저장하는 요청 들어오는
    // db에 member 의 name 을 unique 제약 조건으로 두는 걸 권장
    private void validateDuplicateMember(Member member) {
        // Exception
        List<Member> findMembers = memberRepository.findByName(member.getName());
        if (!findMembers.isEmpty()) {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        }
    }

    // 회원 전체 조회
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    public Member findOne(Long memberId) {
        return memberRepository.findOne(memberId);
    }
}

@Service

@Transactional (readonly 조건)

@Autowired

이름 중복 확인

 

 

@Autowired 추가 설명

테스트를 하거나 할 때

@Autowired
    private MemberRepository memberRepository;

이걸 바꿔야 할 때가 있는데, 위에 처럼 작성하면 접근할 수 있는 방법이 없음

(필드이고 private 으로 되어있으니까)

 

Setter Injection 방식

// 스프링 빈에 등록되어있는 MemberRepository 를 injection 해줌 -> 필드 injection
    private MemberRepository memberRepository;
    
    // (방법1) setter injection
    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

그래서 이렇게 setter injection 방법이 있음

스프링이 아까처럼 바로 주입하는 방식이 아닌, this.memberRepository로 들어와서

이걸로 위에 private MemberRepository memberRepository에 주입해줌

 

장점) 테스트 코드 작성하는 상황에서 setMemberRepository(MemberRepository memberRepository) 를 통해 바로 주입해 줄 수 있다는 장점이 있음 (테스트 코드 작성을 위한 가짜 repository 주입 가능)

단점) 실제로 돌아가는 runtime 시점에 누군가 setMemberRepository를 통해 repository를 바꿀 수 있긴함. 하지만 application 로딩 시점하고 스프링이 올라오면, 그땐 이미 모든 셋팅이 끝난 이후이므로, 그 이후로는 실제로 repository를 바꿀 일이 없음

 

생성자 Injection 쓰는 방식 (요즘 권장)

// (방법2) 생성자 Injection <- 권장 방식은 이거
    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

생성자 injection을 하게 되면,

한번 생성될 때 repository 셋팅이 완성이 되기 때문에 중간에 set 해서 바꿀 수 없음

 

테스트 케이스 작성할 때,

MemberService memberService = new MemberService(Mock()) 이런 식으로 직접 주입 (예시에서는 Mock()에 해당) 해주어야 하므로 누락될 위험이 없음. 어떤 것에 의존하는지도 명확하게 확인 가능

 

최신 버전의 스프링에서는,

@Autowired 써주지 않아도, 생성자가 하나만 있는 경우엔 스프링에서 알아서 생성자에 autowired 를 해줌

 

lombok 에서 제공해주는 기능

@AllArgsConstructor

@AllArgsConstructor

롬복에서 제공해주는 @AllArgsConstructor 사용하면,

 

@Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }

안써줘도 됨

 

@RequiredArgsConstructor

@RequiredArgsConstructor

final 있는 필드만 가지고 생성자를 만들어줌. @AllArgsConstructor 보다 이거 권

 

# 최종 service 코드

package jpabook.jpashop.service;

import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
// import lombok.AllArgsConstructor;
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)
// (1)
// -> public 메서드들은 transaction 에 걸려 들어감
// javax 가 제공하는, spring 이 제공하는 transaction 이 있는데, 이미 spring 사용하는 중이니까 spring 이 제공하는거 사용 추천
// (+ 사용할 수 있는 기능 더 많음)
// (2)
// 읽기 전용 (조회) 인 곳에서 (readOnly = true) 사용하면
// jpa 가 조회하는 곳에서는 트랜잭션이 성능 더 최적화
// (읽기 전용 아닌 곳에서 사용 x)
// 여기에서는 조회가 더 많으니까 (readOnly = true) 를 디폴트로 두기
// -> public 인 곳에 자동으로 @Transactional(readOnly = true) 적용되고, 따로 @Transactional 해둔 곳엔 그렇게 적용
// 원래 (readOnly = False) 이게 디폴트
// @AllArgsConstructor -> 이거 쓰면 생성자 주입도 안써줘도 됨
@RequiredArgsConstructor    // final 있는 필드만 가지고 생성자를 만들어줌. @AllArgsConstructor 보다 이거 권장
public class MemberService {

    // 스프링 빈에 등록되어있는 MemberRepository 를 injection 해줌 -> 필드 injection
    // 더 이상 변경할 일 없으니까 final 써주는거 권장
    private final MemberRepository memberRepository;

    // (방법1) setter Injection
    /*
    @Autowired
    public void setMemberRepository(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
     */

    // (방법2) 생성자 Injection <- 권장 방식은 이거
    // 최신 버전의 스프링에서는,
    // @Autowired 써주지 않아도, 생성자가 하나만 있는 경우엔 스프링에서 알아서 생성자에 autowired 를 해줌
    /*
    @Autowired
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
     */


    @Transactional
    // 회원 가입
    public Long join(Member member) {
        // 중복 회원 검증
        validateDuplicateMember(member);

        memberRepository.save(member);
        return member.getId();
    }

    // 실무에서는 멀티 스레드 상황을 고려해서 (ex) 동시에 같은 이름을 가진 member 저장하는 요청 들어오는
    // db에 member 의 name 을 unique 제약 조건으로 두는 걸 권장
    private void validateDuplicateMember(Member member) {
        // Exception
        List<Member> findMembers = memberRepository.findByName(member.getName());
        if (!findMembers.isEmpty()) {
            throw new IllegalStateException("이미 존재하는 회원입니다.");
        }
    }

    // 회원 전체 조회
    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

    public Member findOne(Long memberId) {
        return memberRepository.findOne(memberId);
    }
}

 

# 최종 Repository 코드

package jpabook.jpashop.repository;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jpabook.jpashop.domain.Member;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;

import java.util.List;

// @Repository 사용하면 컴포넌트 스캔에 의해 자동으로 스프링 빈에 등록돼서 관리 됨
@Repository
@RequiredArgsConstructor
public class MemberRepository {

    // JPA 를 사용하므로, JPA 가 제공하는 표준 어노테이션인 @PersistenceContext 사용
    // -> 스프링이 EntityManger 를 만들어서 여기에 injection 해줌
    // @PersistenceContext
    private final EntityManager em;

    // repository 도, service 처럼, 이렇게 생성자 injection 할 수 있음
    // But, spring boot 의 springDataJpa 라이브러리를 사용하면,
    // 위에 EntityManager 에서 @PersistenceContext 를 @AutoWired 로 바꿀 수 있음
    // 따라서, service 로직과 마찬가지로 @RequiredArgsConstructor 사용 가능 -> 생성자 안써줘도 됨
    /*
    public MemberRepository() {
        this.em = em;
    }
     */

    // JPA 가 저장해줌
    public void save(Member member) {
        // (주의) persist 가 작동된다고 바로 db에 반영되는거 아님
        // persist 하면 영속성 컨텍스트에 member 객체를 넣음
        // db 트랜잭션이 commit 되는 시점에 플러쉬 flush 가 되면서 db에 반영 (-> db에 insert 쿼리 날리는 것)
        em.persist(member);
    }

    public Member findOne(Long id) {
        // find(타입, pk)
        return em.find(Member.class, id);
    }


    // <둘의 차이> from 의 대상
    // SQL : 테이블을 대상으로 쿼리
    // JPQL : 엔티티 객체를 대상으로 쿼리
    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 도, service 처럼, 이렇게 생성자 injection 할 수 있음


But, spring boot 의 springDataJpa 라이브러리를 사용하면,
위에 EntityManager 에서 @PersistenceContext 를 @AutoWired 로 바꿀 수 있음


따라서, service 로직과 마찬가지로 @RequiredArgsConstructor 사용 가능 -> 생성자 안써줘도 됨

 

 

# 테스트 케이스 작성

@Runwith(SpringRunner.class)

Junit 실행할 때, 스프링이랑 같이 엮어서 실행하겠다

 

@SpringBootTest

springboot를 띄운 상태로 테스트를 진행하기 위해서

이거 사용하지 않으면 @Autowired 다 실패

스프링 컨테이너 안에서 테스트를 돌리기 위해서 적어줌

 

@Transactional

(테스트에서 이 어노테이션을 사용하는 경우) 트랜잭션을 걸고 테스트를 진행시킨 후, 롤백시킴

(테스트가 아닌, 서비스 같은 클래스에 적어주는 경우 롤백시키지 않음)

 

테스트 케이스 작성할 때,

given / when / then 구조로 작성하는거 추천

 

@Test(expecte = IllegalStateException.class)

테스트 케이스 작성 시, try - catch 로 예외상황 처리 따로 작성해주지 않아도 됨

 

자바 안에서 db를 새로 만들어서 띄우는 방법 (테스트를 위한 db)

현재, 외부 db 사용해서 테스트 진행

: 이런 테스트를 병렬로 3-4개 돌리거나 db를 외부에 설치 해야함

: 번거롭고, 테스트가 끝나면 데이터가 다 초기화되는 게 좋음

-> 자바 띄울 때, 자바 안에서 db를 새로 만들어서 띄우는 방법 있음

-> 메모리 db 사용

-> main에 있던 application.yml을 test.resources.application.yml를 새로 만들어서 복붙

-> 테스트를 돌릴 땐, 이 yml 파일이 우선권을 가져서, 이걸 사용

-> 테스트용 yml 파일에서 datasource.url을 메모리로 변경

: (h2 사이트에서 Cheat Sheet -> In-Memory 참고) jdbc:h2:mem:test 로 변경

-> localhost:8082 작동안하지만, 테스트 케이스 통과하고 내부 db로 잘 작동 됨 (ps6 사용해서 확인가능)

jdbc:h2:mem:test 가 쓰이는거 확인 가능

 

 

-> BUT 스프링 부트에서 제공 해주는 기능 있음

test.resources.application.yml에서 위에 처럼 datasource.url 직접 변경해주지 않아도, 그리고 다른 코드 작성해주지 않아도 스프링부트에서 알아서 처리해 줌 (아예 다 지워도 잘 동작)

jdbc:h2:mem:testdb 로 connection 받아온다고 강의에선 그랬음..

 

+) 실제 운영에서 필요한 설정과 test 에서 필요한 설정이 다를 수 있으므로,

이렇게 test 를 위한 application.yml 파일 (설정) 을 따로 두는게 좋음

 

+)

spring boot는 ddl-auto: 가 create 가 아닌 create-drop 으로 기본적으로 돌아감

create? 내가 가지고 있는 entity 를 다 drop 한 다음에 create 를 하고, application 을 실행시킴

create-drop? 내가 가지고 있는 entity 를 다 drop 한 다음에 create 를 하고, application 을 실행시킴. 까지는 똑같은데, 마지막에 application 종료 시점에 drop 쿼리를 다 날려줌 -> 완전히 깨끗하게 초기화

 

: 깔끔하게 자원 정리까지 해준다

'스프링 > [인프런] 실전! 스프링 부트와 JPA 활용1' 카테고리의 다른 글

7. 웹 계층 개발  (0) 2024.03.10
5. 상품 도메인 개발  (0) 2024.03.06
2. 도메인 분석 설계  (0) 2024.03.03
1. 환경 설정  (0) 2024.03.03

관련글 더보기