소스 코드
Member
package jpabook.jpashop.domain;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.*;
import java.util.ArrayList;
import java.util.List;
@Entity
@Getter
@Setter
public class Member {
@Id
@GeneratedValue
@Column(name = "member_id")
private Long id;
@OneToMany(mappedBy = "member")
private List<Order> orders = new ArrayList<>();
private String name;
@Embedded
private Address address;
}
MemberRepository
package jpabook.jpashop.repository;
import jpabook.jpashop.domain.Member;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
import java.util.List;
@Repository
public class MemberRepository {
@PersistenceContext
private EntityManager em;
public Long save(Member member) {
em.persist(member);
return member.getId();
}
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();
}
}
MemberService
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)
public class MemberService {
@Autowired
private MemberRepository memberRepository;
/*
회원가입
*/
@Transactional
public Long join(Member member){
validateDuplicateException(member);
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateException(Member member) {
List<Member> findMembers = memberRepository.findByName(member.getName());
if(!findMembers.isEmpty()){
throw new IllegalStateException("이미 존재하는 회원입니다.");
}
}
// 회원 전체 조회
public List<Member> findMembers(){
return memberRepository.findAll();
}
// 단건 조회
public Member findOne(Long id){
return memberRepository.findOne(id);
}
}
MemberService를 확인해보자.
- @Transcational
- 클래스위에 붙는 @Transcational이 기본이 된다.
- 조회할 때만 @Transcational(readOnly = true)를 넣어주면 성능이 더 좋아질 수 있다.
- 예를들어) MemberService의 경우 조회 메소드3개 추가 메소드 1개 이므로 조회를 더 자주 하니까
class의 @Transcational(readOnly =true)로 하고 join메소드만 @Transcational를 달아주는게 좋다. (default=>false) - 멀티스레드 충돌방지
validateDuplicationExeption의 메소드의 검증가지고는 멀티스레드 충동 방지를 제대로 하지 못할 수 있다.
예를 들면, 동시에 두명의 회원이 똑같은 이름으로 가입할 수도 있기 때문에,,
실무에서는 이에 대한 최종방어선으로 name을 unique 제약조건을 걸어서 방지하는 것도 방법이 될 수 있다고 한다. - throw new IllegalStateExeption("message")
몰랐단 Exeption처리 방식이다. 알아두자. - 의존 주입의 생략 ***
이렇게 자동 의존 주입을해도 괜찮지만, 혹시나 test를 한다던가 했을 때, 원하는 memberRepository를 직접 주입하고 싶을 때 직접 주입을 하고 싶어도 하지 못하는 경우가 발생한다.public class MemberService { @Autowired MemberRepository memberRepository; // ...생략 }
따라서, 생성자를 통한 의존주입, setter를 통한 의존주입이 있는데 최근에는 생성자 의존 주입을 쓰는것이 트렌드 라고 하셨다. 보통 어플리케이션 로딩 시점에 주입과 조립이 다 끝이나고 setter로 따로 변경하는 경우가 없기 때문에, 로딩하는 시점과 일치하는 생성자 의존주입을 하는 것이 더 좋다는 말씀을 하셨다. 그리고 생성 시점에 의존주입을 하도록 설정되어 있어서 에러를 놓치지 않고 방지 할 수 있다. => 생성자 인젝션
따라서, 아래와 같이 수정할 수 있다.
(생성자가 하나밖에 없을 때에 @Autowired도 생략가능)public class MemberService { private MemberRepository memberRepository; @Autowired public MemberService(MemberRepository memberRepository) { this.memberRepository = memberRepository; } // ...생략 }
여기서 lombok을 통해서 더 생략이 가능하다.
생성자와 의존주입 어노테이션 없이, 의존주입할 필드를 final 예약어를 붙여주고
class에 @RequiredArgsContructor 을 달아주면 된다.
최종
@RequiredArgsContructor public class MemberService { private final MemberRepository memberRepository; // ...생략 }
- MemberRepository의
- 이 부분도 spring boot 라이브러리와 lombok을 사용하면 를 사용하면 위 방법을 영속성 컨텍스트의 의존 주입에도 사용 할 수 있다.@Repository public class MemberRepository { @PersistenceContext private EntityManager em; // ... }
- @PersistenceContext를 @Autowired로 대체할 수 있다. (springboot 라이브러리가 있기 때문)
그럼 이 케이스는 위의 의존주입처럼 정리 할 수 있으므로
최종
@Repository @RequiredArgsConstructor public class MemberRepository { private final EntityManager em; // ...생략 }
테스트
JUnit5를 통해 회원가입과 회원가입시 중복회원예외 테스트를 검사해보자.
JUnit5 설정
- JUnit5 완벽가이드 (출처:https://donghyeon.dev/junit/2021/04/11/JUnit5-%EC%99%84%EB%B2%BD-%EA%B0%80%EC%9D%B4%EB%93%9C/ )
- https://junit.org/junit5/docs/current/user-guide/ 참고
MemberServiceTest 소스코드
package jpabook.jpashop.service;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.annotation.Rollback;
import org.junit.rules.ExpectedException;
import javax.transaction.Transactional;
import static org.junit.jupiter.api.Assertions.*;
@SpringBootTest
class MemberServiceTest {
@Autowired
MemberService memberService;
@Autowired
MemberRepository memberRepository;
@Test
@Transactional
@Rollback(false)
public void 회원가입() throws Exception {
// given
Member member = new Member();
member.setName("memberA");
// when
Long saveId = memberService.join(member);
// then
assertEquals(member, memberRepository.findOne(saveId));
}
@Test
@Transactional
@Rollback(value = false)
public void 중복_회원_예외() throws Exception {
// given
Member member1 = new Member();
member1.setName("kim1");
Member member2 = new Member();
member2.setName("kim1");
// when
try {
memberService.join(member2);
} catch (IllegalStateException e) {
return;
}
// then
fail("예외가 발생해야한다.");
}
}
0. 강의에선 JUnit4를 사용했지만 나는 JUnit5로 테스트 해보았다.
import할 때
- import static org.junit.jupiter.api.Assertions.*
- import org.junit.jupiter.api.Test;
이렇게 jupiter를 거쳐서 import하는 것을 확인하자 ! 그럼 JUnit5를 사용하고 있는 것이다. JUnit5의 자세한 어노테이션과 설명은 더 디테일하게 파고 들어야 겠다.
1. 회원가입 테스트 (회원가입())
- 저번시간에 Test했던 코드와 비슷하다.
given에 데이터를 넣고, when에서 service를 통해 회원가입을 한 뒤에, then에서 repository로 접근하여 조회를 통해 회원가입한 데이터와 똑같은지 확인했다. - @Transactional이 있으면 기본적으로 rollback이 되기 때문에 insert Query가 나가지 않는다.
-> 우리가 확인하기가 쉽지 않다.
확인하기 위한 방법
방법 1. @Rollback(false)로 설정해서 DB를 직접 확인한다.
=> 단점: DB는 초기화 되지 않는다. Test용 반복 실행하기 때문에 DB 저장이 굳이 필요 없다.
방법 2.
@Autowired EntityManage em; 을 의존 주입을 통해 만들어주고
assert로 확인하는 코드 전에 em.flush()를 해주면 영속성컨텍스트의 변경내용을 DB에 반영하려 하기 때문에
쿼리를 날리는것 을 확인할 수 있다.
2. 중복회원예외 테스트
- member1, member2 모두 "kim1"이라는 같은 이름으로 회원가입을 시키려 한다. 우리는 MemberService에서 이름이 중복된 회원가입은 IllegalStateException을 발생시키도록 설정 시켜놨기 때문에
memberService.join(member1);을 실행하고
memberService.join(member2);을 실행하면 Exception이 발생해야 한다.
방법1.
위의 코드처럼 try.catch문으로 예외를 잡고 리턴하는 것이다. fail()까지 도달하지 않는다. @Rollback(false)해도 오류가 발생하지 않으며 member1의 데이터는 저장되고 member2 데이터는 저장되지 않는다.// when memberService.join(member1); try { memberService.join(member2); } catch (IllegalStateException e) { return; } // then fail("예외가 발생해야한다.");
방법2.
@Test의 expected 속성에 예상 가능한 Exception 클라스를 전달하여 위의 방법1과 같은 대처가 가능하도록 설정한다.@Test(expected = IllegalStateException.class) @Transactional @Rollback(value = false) public void 중복_회원_예외() throws Exception { // when memberService.join(member1); memberService.join(member2); // then fail("예외가 발생해야한다."); }
방법3
assertThrows를 이용하여 람다함수와 함께 예외처리한다.@Test @Transactional @Rollback(value = false) public void 중복_회원_예외() throws Exception { // given Member member1 = new Member(); member1.setName("kim1"); Member member2 = new Member(); member2.setName("kim1"); // when memberService.join(member1); assertThrows(IllegalStateException.class, () -> { memberService.join(member2); }); // then fail("예외가 발생해야한다."); }
하지만 이렇게 코드를 짜면 -> meber2는 걸러지지만 fail까지 도달한다.
fail을 지우고 실행해도 org.springframework.transaction.UnexpectedRollbackException: Transaction silently rolled back because it has been marked as rollback-only
오류가 발생한다. 해결방법(https://techblog.woowahan.com/2606/)
그리 좋지 않은 방법인 것 같다.. 그냥 try-catch문을 사용하는 것이 나을 듯 보인다.
- => JUnit4의 @Test와 JUnit5 @Test는 다르다. JUnit5의 @Test에는 expected 속성이 없다. 따라서
내부 DB로 전환해서 사용하기
우리가 지금까지 한 방법은 H2 외부 DB에 연결하여 Test를 실행했던 방법이다.
Test를 띄울 때 완전히 격리된 환경에서, 즉 Java 내부에서 살짝 내부 DB를 사용하여 테스트할 수 있는 방법이 있다.
메모리 기능 (SpringBoot는 이것을 쉽게 가능하게 만든다 !)
1. Test폴더에도 resources->application.yml 파일을 추가.
-> 만약 application.yml이 없을 경우에는 Main의 application.yml을 가져다 쓴다.
main의 것과 test의 것을 구분하는 것이 당연히 좋다. 운영에서의 설정과 테스트에서의 설정이 다른것으로 이해하면 될것같다.
2. 메모리 방식으로 전환
http://h2database.com/html/cheatSheet.html
↑ 위 사이트에서
빨간색 밑줄친 부분을 복사하여
application.yml의 url 부분을 변경하여 준다.
url을 jdbc:h2:mem:test 로 변경해준다.
그럼 이제 메모리 방식으로 돌아간다.
그러나 !!!
스프링부트는 위와같이 별다른 설정이 없으면 메모리에서 돌려주는 착한 친구이다. 알아서 memory 모드로 돌려준다.
알아둘 점은 별다른 설정이 없을 때 springboot가 알아서 해줄 때 ddl-auto의 default는 create-drop 이라는 것이다.
이 게시물은 '실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발' 강의를 수강하고 정리한 내용임을 밝힙니다.
실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의
실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., 본
www.inflearn.com