본문 바로가기

spring boot

실전! 스프링 부트와 JPA 활용1 - 3,4주차

3주차

1&2. 구현 요구사항& 애플리케이션 아키텍쳐

 

기능

 

패키지 구조 jpabook.jpashop 안에

  • domain : 엔티티가 모여있는 계층, 모든 계층에서 사용
  • exception :예외 처리
  • repository : jpa를 직접 사용하는 계층, 엔티티 매니저 사용
  • service : 비즈니스 로직, 트랜잭션 처리
  • web : 웹 계층

으로 나누어져 있다.

 

개발 순서는 service, repository -> 테스트 케이스를 작성하여 검증, 마지막에 web 계층을 적용한다.


4주차

1. 회원 리포지토리 개발

 

package jpabook.jpashop.repository;
import jpabook.jpashop.domain.Member;
import org.springframework.stereotype.Repository;
import lombok.RequiredArgsConstructor;
import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import java.util.List;
@Repository // 스프링 빈으로 등록, JPA 예외를 스프링 기반 예외로 예외 변환
@RequiredArgsConstructor
public class MemberRepository {
    @PersistenceContext //  엔티티 메니저( EntityManager ) 주입
    private EntityManager em;
    public void save(Member member) { // JPA 저장 로직(DB 저장)
        em.persist(member);
    }
    public Member findOne(Long id) { // member를 찾아서 반환
        return em.find(Member.class, id); // pk값 넣기
    }
    public List<Member> findAll() { // list 반환, from <엔티티>(테이블 x)
        return em.createQuery("select m from Member m", Member.class)
                .getResultList();
    }
    public List<Member> findByName(String name) { // name 파라미터 넣어주기
        return em.createQuery("select m from Member m where m.name = :name",
                        Member.class)
                .setParameter("name", name)
                .getResultList();
    }
}

save(), findOne(), findAll(), findByName()의 기능이 있다.


2. 회원 서비스 개발

 

package jpabook.jpashop.service;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
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) // 트랜잭션, 영속성 컨텍스트
// 데이터의 변경이 없는 읽기 전용 메서드에 사용, 영속성 컨텍스트를 플러시 하지 않으므로 약간의 성능 향상(읽기 전용에는 다 적용), 데이터베이스 드라이버가 지원하면 DB에서 성능 향상
@RequiredArgsConstructor // annotation을 이용한 간단한 생성자 주입 방법
public class MemberService {
    private final MemberRepository memberRepository;
    /**
     * 회원가입
     */
    @Transactional //변경
    public Long join(Member member) {
        validateDuplicateMember(member); //중복 회원 검증
        memberRepository.save(member);
        return member.getId();
    }
    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);
    }
}

join(), findMembers(), findOne()의 기능이 있다.

실무에서는 생성자 주입 방식으로 변경 불가능한 안전한 객체 생성 방식을 권장한다.

@RequiredArgsConstructor로 간단하게 생성자를 주입할 수 있다. 

final 키워드 추가로 컴파일 시점에 memberRepository 설정


3. 회원 기능 테스트

 

테스트는 회원가입 성공과 회원가입 할 때 같은 이름이 있으면 예외가 발생하도록 했다.

반드시 ./h2.bat을 실행중인 상태에서 테스트 돌리기!! 안그러면 오류가 난다.

package jpabook.jpashop.service;
import jpabook.jpashop.JpashopApplication;
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.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;import static org.junit.Assert.assertEquals;
import static org.junit.Assert.fail;
@RunWith(SpringRunner.class) // spring과 test 통합
@SpringBootTest // 스프링 부트 띄우고 테스트 -> 없으면 @Autowired 다 실패
@Transactional // 반복가능한 테스트 지원, 각 테스트가 끝나면 트랜잭션 강제 롤백
// @ContextConfiguration(classes = JpashopApplication.class)
public class MemberServiceTest {
    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;
    @Test
    public void 회원가입() throws Exception {
        //Given
        Member member = new Member();
        member.setName("kim");
        //When
        Long saveId = memberService.join(member);
        //Then
        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); //예외가 발생해야 한다.
        //Then fail("예외가 발생해야 한다.");
    }
}

Given, When, Then으로 테스트 케이스를 작성하는 것이 좋다.

 

테스트 케이스는 실제 실행 환경과 다른 설정 파일을 사용하는 것이 좋다.

test package에 resources 폴더를 추가하고 application.yml을 복사해온다.

여기에 테스트용 db(메모리 db)를 h2.database에서 가져온다. 다만, datasource와 jpa 설정은 없어도 test코드는 알아서 잘 돌아간다~

In-Memory 저걸 가져온다.
test 성공

test코드가 메모리 DB에서 잘 실행된 것을 확인 가능하다. 

만약 이 위치에 설정파일이 없으면 원래 위치인 src/resources/application.yml 파일을 읽어온다.


참고한 사이트

[Spring] @RequiredArgsConstructor 어노테이션을 사용한 "생성자 주입" (velog.io)

 

[Spring] @RequiredArgsConstructor 어노테이션을 사용한 "생성자 주입"

의존성주입의 종류로는 Constructor(생성자),Setter,Field 타입이 있다.Constructor(생성자)Setter3.Field생성자주입의 단점은 위의 Constructor(생성자) 코드처럼 생성자를 만들기 번거롭다는 것이다. 하지만 이

velog.io