회원 도메인 개발

LeeKyoungChang·2022년 2월 25일
0
post-thumbnail

스프링 부트와 JPA 활용 1 - 웹 애플리케이션 개발 수업을 듣고 정리한 내용입니다.

 

📚 1. 회원 리포지토리 개발

구현 기능

  • 회원 등록
  • 회원 목록 조회

순서

  • 회원 엔티티 코드 다시 보기
  • 회원 리포지토리 개발
  • 회원 서비스 개발
  • 회원 기능 테스트

 

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 {

    // spring이 엔티티 매니저를 만들어서 엔티티를 주입해준다.
    @PersistenceContext
    private EntityManager em;


    public void save(Member member) { em.persist(member); }

    // 단일
    // jpa가 제공하는 find(id)를 통해 member를 찾아 반환
    public Member findOne(Long id) {
        return em.find(Member.class, id);
    }

    // 여러가지
    // 여러가지 데이터 조회할 때는 List!
    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 : 스프링 빈으로 등록, JPA 예외를 스프링 기반 예외로 예외 변환
  • @PersistenceContext : 엔티티 매니저( EntityManager ) 주입
  • @PersistenceUnit : 엔티티 매니저 팩토리( EntityManagerFactory ) 주입

 

✔️ 기능 설명

  • save() : 저장
  • findOne() : 단건 조회
  • findAll() : 리스트 조회
  • findByName() : name을 이용해서 조회

 

📚 2. 회원 서비스 개발

MemberService

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)  // 이럴경우, jpa가 조회하는 경우에서는 성능이 최적화된다. (읽기 : readOnly = true, 쓰긴 경우 : x)
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;  // 컴파일시에 생성자 체크할 수 있으므로 final을 추가해야 한다.

    // 회원 가입
    @Transactional  // readOnly = false가 우선권을 먹는다. (클래스 전체는 readOnly = true이지만 메소드 위에는 readOnly = false 이므로)
    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());
        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=true : 데이터의 변경이 없는 읽기 전용 메서드에 사용한다. 영속성 컨텍스트를 플러시 하지 않으므로 약간의 성능 향상(읽기 전용에는 다 적용)
    • 데이터베이스 드라이버가 지원하는 DB에서 성능 향상
  • @Autowired
    • 생성자 Injection 많이 사용한다.
    • 생성자가 하나면 생략 가능

 

✔️ 기능 설명

  • join() : 회원 가입
  • findMembers() : 회원 전체 조회
  • findOne() : id로 회원 조회 (id : key)

 

💡 참고

  • 실무에서는 검증 로직이 있어도 멀티 쓰레드 상황을 고려해서 회원 테이블의 회원명 컬럼에 유니크 제약 조건을 추가하는 것이 안전하다.
  • 스프링 필드 주입 대신에 생성자 주입을 사용하자!

 

✔️ 필드 주입

public class MemberService {

    @Autowired
    private MemberRepository memberRepository;
    ...
}

 

✔️ 생성자 주입

public class MemberService {

    private final MemberRepository memberRepository;
    
    public MemberService(MemberRepository memberRepository) {
        this.memberRepository = memberRepository;
    }
    ...
}
  • 생성자 주입 방식을 권장
  • 변경 불가능한 안전한 객체 생성 가능
  • 생성자가 하나면, @Autowired 를 생략할 수 있다.
  • final 키워드를 추가하면 컴파일 시점에 memberRepository 를 설정하지 않는 오류를 체크할 수 있다. (보통 기본 생성자를 추가할 때 발견한다.)

 

✔️ lombok

@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;
    ...

}

(스프링 데이터 JPA를 사용하면 EntityManager 도 주입 가능하다.)

	@Repository
    @RequiredArgsConstructor
    public class MemberRepository {
		
        private final EntityManager em;
		... 
		
	}

  • @RequiredArgsConstructor : 초기화되지 않은 final 필드나, @NonNull이 붙은 필드에 대해 생성자를 생성해준다.
    • 소스를 삽입 하면서 세팅이 끝난 필드는 (수정이 필요 없는 것들은) final 선언하기

 

✔️ MemberService 최종 코드

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)  // 이럴경우, jpa가 조회하는 경우에서는 성능이 최적화된다. (읽기 : readOnly = true, 쓰긴 경우 : x)
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;  // 컴파일시에 생성자 체크할 수 있으므로 final을 추가해야 한다.

    // 회원 가입
    @Transactional  // readOnly = false가 우선권을 먹는다. (클래스 전체는 readOnly = true이지만 메소드 위에는 readOnly = false 이므로, 그리고 Transactional default는 false이다.)
    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());
        if (!findMembers.isEmpty()) {
            throw new IllegalStateException("아마 존재하는 회원입니다.");
        }
    }

    // 회원 전체 조회

    public List<Member> findMembers() {
        return memberRepository.findAll();
    }

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

 

📚 3. 회원 기능 테스트

🔔 테스트 요구사항

  • 회원가입을 성공해야 한다.
  • 회원가입 할 때 같은 이름이 있으면 예외가 발생해야 한다. (중복회원 체크)

 

✔️ MemberServiceTest - 회원가입 테스트 코드

package jpabook.jpashop.service;

import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
import org.junit.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 javax.persistence.EntityManager;

import static org.junit.Assert.*;

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class MemberServiceTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;
    @Autowired EntityManager em;

    @Test
    @Rollback(false)
    public void 회원가입() throws Exception{
        // given
        Member member = new Member();
        member.setName("kim");


        // when
        Long saveId = memberService.join(member);

        // then
//        em.flush();
        assertEquals(member, memberRepository.findOne(saveId));
    }

    @Test(expected = IllegalStateException.class)
    public 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);    // 예외가 발생해야 한다.
//        try {
//            memberService.join(member2);    // 예외가 발생해야 한다.
//        } catch (IllegalStateException e) {
//            return;
//        }

        // then
        fail("예외가 발생해야 합니다.");  // 코드가 돌다가 fail()메소드 줄에 도착하면 안된다. 발생시 노란불 출력됨
    }

}

 

✔️ 기술 설명(애노테이션)
@RunWith(SpringRunner.class)

  • 스프링과 테스트 통합

@SpringBootTest

  • 스프링 부트 띄우고 테스트(없으면 @Autowired 다 실패)

@Transactional

  • 반복 가능한 테스트 지원
  • 각각의 테스트를 실행할 때마다 트랜잭션을 시작하고 테스트가 끝나면 트랜잭션을 강제로 롤백 (이 어노테이션이 테스트 케이스에서 사용될 때만 롤백)

@Rollback(false)

  • Rollback을 할시 DB에 저장된 데이터를 날린다는 의미이다.
  • 그런데 false를 하면? DB에 저장된 데이터가 유지된다.
@Autowired EntityManager em;
em.flush();
  • 영속성 쿼리에 있는 데이터를 DB에 반영
  • test 디렉터리에서 실행했음에도, DB에 반영이 된다.
  • 그런데 test 파일에서는 마지막에 Rollback이 auto로 실행되므로, 데이터베이스에 실제 반영되지는 않는다. (Rollback(false)를 할 시 데이터베이스에 반영된다.)

insert문 실행됨
스크린샷 2022-04-05 오후 5 57 25

 

✔️ 중복 회원 예외

@Test(expected = IllegalStateException.class)
class{
		...
		        // when
        memberService.join(member1);
        memberService.join(member2);    // 예외가 발생해야 한다.
}

  • @Test(expected = IllegalStateException.class) : IllegalStateException 예외가 터졌을 때, 정상적으로 실행된다. (예외 예방 같은 것)

 

✔️ 기능 설명

  • 회원가입 테스트
  • 중복 회원 예외처리 테스트

 

💡 참고

  • 테스트 케이스를 작성할 때는 Given, When, Then 사용한다.
  • 이 방법을 기본으로 해서 다양하게 응용하는 것이 좋다!

 

실행 결과

스크린샷 2022-02-25 오후 6 08 39

 

📖 A. 테스트 케이스를 위한 설정

  • 테스트는 케이스 격리된 환경에서 실행하고, 끝나면 데이터를 초기화하는 것이 좋다.
  • 그런 면에서 메모리 DB를 사용하는 것이 가장 이상적이다.
  • 추가로 테스트 케이스를 위한 스프링 환경과, 일반적으로 애플리케이션을 실행하는 환경은 보통 다르므로 설정 파일을 다르게 사용하자.
  • 다음과 같이 간단하게 테스트용 설정 파일을 추가하면 된다!

 

✔️ test에 resources/yml 만들기

스크린샷 2022-02-25 오후 6 01 06

maintestapplicaton.yml 두개가 있다. 이럴 경우 test 우선순위를 더 높게한다.
그러므로, test/resources/application.yml이 실행된다.

test/resources/application.yml

spring: #띄어쓰기 없음
#  datasource: #띄어쓰기 2칸
#    url: jdbc:h2:mem:test
#    username: sa
#    password:
#    driver-class-name: org.h2.Driver
#  jpa: #띄어쓰기 2칸
#    hibernate: #띄어쓰기 4칸
#      ddl-auto: create #띄어쓰기 6칸
#    properties:
#      hibernate:
#        #        show_sql: true
#        format_sql: true #띄어쓰기 8칸

logging:
  level:
    org.hibernate.SQL: debug
    org.hibernate.type: trace

 

이렇게 testresources을 만들었을 경우,

./h2.sh를 실행하지 않은 상태에서도 test를 실행할 수 있다.

스크린샷 2022-02-25 오후 6 04 50 스크린샷 2022-02-25 오후 6 05 11
  • test 실행 성공!

 

💡 참고
스프링부트는 yml에 관련 소스가 없어도 잘 실행된다.

스크린샷 2022-02-25 오후 6 07 31 스크린샷 2022-02-25 오후 6 08 39

resources관련 파일들은 maintest를 기반으로 분리해야 한다. (테스트는(test) 돌리기만 한다. 운영은(main) 여러가지 추가할 것이 있다.)

 

💡 참고

  • 스프링 부트는 datasource 설정이 없으면, 기본적으로 메모리 DB를 사용하고, driver-class도 현재 등록된 라이브러리를 보고 찾아준다.
  • 추가로 ddl-autocreate-drop 모드로 동작한다. (drop -> create -> drop)
  • 따라서 데이터소스나, JPA 관련된 별도의 추가 설정을 하지 않아도 된다!

 

💡 참고
트랜잭션이란?

  • 트랜잭션(transaction) : 데이터베이스에서 논리적 상태 변화, 즉 Insert, Update, Delete로 데이터베이스의 데이터가 변화가 있는 것을 트랜잭션(transaction)이라고 한다.
    • 데이터의 변화가 많은 테이블 : 트랜잭션(Transaction)테이블
    • 데이터의 변화가 적은 테이블 : 마스터(Master)테이블
  • 이런 트랜잭션은 범위를 설정할 수 있는데 범위 설정을 통해서 원자성(Atomicity), 일관성(Consistency), 독립성(Isolation), 영구성(Durability)를 유지할 수 있다.

트랜잭션 참고하기 좋은 사이트

 

profile
"야, (오류 만났어?) 너두 (해결) 할 수 있어"

0개의 댓글