트랜잭션
트랜잭션에 커밋
rollback
persist
영속성컨텍스트
jpql
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);
}
}
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
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
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 를 해줌
@AllArgsConstructor
@AllArgsConstructor
롬복에서 제공해주는 @AllArgsConstructor 사용하면,
@Autowired
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
안써줘도 됨
@RequiredArgsConstructor
@RequiredArgsConstructor
final 있는 필드만 가지고 생성자를 만들어줌. @AllArgsConstructor 보다 이거 권
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);
}
}
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 사용해서 확인가능)

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

+) 실제 운영에서 필요한 설정과 test 에서 필요한 설정이 다를 수 있으므로,
이렇게 test 를 위한 application.yml 파일 (설정) 을 따로 두는게 좋음
+)
spring boot는 ddl-auto: 가 create 가 아닌 create-drop 으로 기본적으로 돌아감
create? 내가 가지고 있는 entity 를 다 drop 한 다음에 create 를 하고, application 을 실행시킴
create-drop? 내가 가지고 있는 entity 를 다 drop 한 다음에 create 를 하고, application 을 실행시킴. 까지는 똑같은데, 마지막에 application 종료 시점에 drop 쿼리를 다 날려줌 -> 완전히 깨끗하게 초기화
: 깔끔하게 자원 정리까지 해준다
| 7. 웹 계층 개발 (0) | 2024.03.10 |
|---|---|
| 5. 상품 도메인 개발 (0) | 2024.03.06 |
| 2. 도메인 분석 설계 (0) | 2024.03.03 |
| 1. 환경 설정 (0) | 2024.03.03 |