Per ardua ad astra !
I'm On My Way
Per ardua ad astra !
전체 방문자
오늘
어제
  • 분류 전체보기 (126)
    • Algorithm (50)
      • 백준 (30)
      • SWEA (3)
      • JUNGOL (3)
      • Programmers (5)
      • LeetCode (2)
    • 안드로이드 개발 (6)
      • Java로 개발 (0)
      • Kotlin으로 개발 (3)
    • Spring (41)
      • Spring기본 (17)
      • JPA기본 (15)
      • JPA활용 SpringBoot 기본 (9)
      • API 개발 기본 (0)
    • 네트워크 (3)
    • 운영체제 (0)
    • Life (3)
      • 책 (0)
      • 자기계발 (1)
      • 일상 (2)
    • 노마드코더 (3)
      • python으로 웹 스크래퍼 만들기 (3)
    • 프로그래밍 언어 (17)
      • Java 기본 (2)
      • 코틀린 기본 (15)

블로그 메뉴

  • 홈
  • 방명록

인기 글

hELLO · Designed By 정상우.
Per ardua ad astra !

I'm On My Way

회원 도메인 개발(레포지터리, 서비스, 테스트) && 의존주입 생략 과정, JUnit5를 이용한 테스트, 내부 DB로 전환
Spring/JPA활용 SpringBoot 기본

회원 도메인 개발(레포지터리, 서비스, 테스트) && 의존주입 생략 과정, JUnit5를 이용한 테스트, 내부 DB로 전환

2021. 8. 17. 14:47

소스 코드

 

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처리 방식이다. 알아두자.
  • 의존 주입의 생략 ***
    public class MemberService {
        @Autowired
        MemberRepository memberRepository;
        // ...생략
    }​
    이렇게 자동 의존 주입을해도 괜찮지만, 혹시나 test를 한다던가 했을 때, 원하는 memberRepository를 직접 주입하고 싶을 때 직접 주입을 하고 싶어도 하지 못하는 경우가 발생한다. 
    따라서, 생성자를 통한 의존주입, setter를 통한 의존주입이 있는데 최근에는 생성자 의존 주입을 쓰는것이 트렌드 라고 하셨다. 보통 어플리케이션 로딩 시점에 주입과 조립이 다 끝이나고 setter로 따로 변경하는 경우가 없기 때문에, 로딩하는 시점과 일치하는 생성자 의존주입을 하는 것이 더 좋다는 말씀을 하셨다. 그리고 생성 시점에 의존주입을 하도록 설정되어 있어서 에러를 놓치지 않고 방지 할 수 있다. => 생성자 인젝션

    따라서, 아래와 같이 수정할 수 있다.
    public class MemberService {
    
        private MemberRepository memberRepository;
        @Autowired
        public MemberService(MemberRepository memberRepository) {
            this.memberRepository = memberRepository;
        }
        // ...생략
    }​
     (생성자가 하나밖에 없을 때에 @Autowired도 생략가능)
    여기서 lombok을 통해서 더 생략이 가능하다.

    생성자와 의존주입 어노테이션 없이, 의존주입할 필드를 final 예약어를 붙여주고
    class에 @RequiredArgsContructor 을 달아주면 된다. 

    최종
    @RequiredArgsContructor
    public class MemberService {
        private final MemberRepository memberRepository;
        
        // ...생략
    }​
  • MemberRepository의
    @Repository
    public class MemberRepository {
        @PersistenceContext
        private EntityManager em;
        // ...   
    }
    - 이 부분도 spring boot 라이브러리와 lombok을 사용하면 를 사용하면 위 방법을 영속성 컨텍스트의 의존 주입에도 사용 할 수 있다.
    - @PersistenceContext를 @Autowired로 대체할 수 있다. (springboot 라이브러리가 있기 때문)

    그럼 이 케이스는 위의 의존주입처럼 정리 할 수 있으므로 

    최종
    @Repository
    @RequiredArgsConstructor
    public class MemberRepository {
    
        private final EntityManager em;
        // ...생략
    }​

테스트


JUnit5를 통해 회원가입과 회원가입시 중복회원예외 테스트를 검사해보자.


JUnit5 설정

 

build.gradle 설정


- 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.
            // when
            memberService.join(member1);
            try {
                memberService.join(member2);
            } catch (IllegalStateException e) {
                return;
            }
            // then
            fail("예외가 발생해야한다.");
    위의 코드처럼 try.catch문으로 예외를 잡고 리턴하는 것이다. fail()까지 도달하지 않는다. @Rollback(false)해도 오류가 발생하지 않으며 member1의 데이터는 저장되고 member2 데이터는 저장되지 않는다.

    방법2.
        @Test(expected = IllegalStateException.class)
        @Transactional
        @Rollback(value = false)
        public void 중복_회원_예외() throws Exception {
            // when
            memberService.join(member1);
            memberService.join(member2);
    
            // then
            fail("예외가 발생해야한다.");
        }​
    @Test의 expected 속성에 예상 가능한 Exception 클라스를 전달하여 위의 방법1과 같은 대처가 가능하도록 설정한다. 
    방법3
        @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("예외가 발생해야한다.");
        }
     assertThrows를 이용하여 람다함수와 함께 예외처리한다. 

    하지만 이렇게 코드를 짜면 -> 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 부분을 변경하여 준다.

application.yml

url을 jdbc:h2:mem:test 로 변경해준다.
그럼 이제 메모리 방식으로 돌아간다. 


그러나 !!!

스프링부트는 위와같이 별다른 설정이 없으면 메모리에서 돌려주는 착한 친구이다. 알아서 memory 모드로 돌려준다. 

알아둘 점은 별다른 설정이 없을 때 springboot가 알아서 해줄 때 ddl-auto의 default는 create-drop 이라는 것이다.

 

SpringBoot-Basic-main.zip
0.07MB

 

 

이 게시물은 '실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발' 강의를 수강하고 정리한 내용임을 밝힙니다.

출처: https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81%EB%B6%80%ED%8A%B8-JPA-%ED%99%9C%EC%9A%A9-1/

 

실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발 - 인프런 | 강의

실무에 가까운 예제로, 스프링 부트와 JPA를 활용해서 웹 애플리케이션을 설계하고 개발합니다. 이 과정을 통해 스프링 부트와 JPA를 실무에서 어떻게 활용해야 하는지 이해할 수 있습니다., 본

www.inflearn.com

 

 

    'Spring/JPA활용 SpringBoot 기본' 카테고리의 다른 글
    • 주문 도메인 개발
    • 상품 도메인 개발, @Transactional에 대한 궁금증 해소
    • 도메인 분석 설계 (JPA를 활용해서 엔티티 클래스 개발)
    • JPA와 DB 설정, 동작 확인
    Per ardua ad astra !
    Per ardua ad astra !
    개발자 지망생이며 열심히 공부하고 기억하기 위한 블로그입니다.

    티스토리툴바