백엔드/JPA

[JPA 활용1] [3] 앱 구현 준비 및 회원 도메인 개발

RE-Heat 2023. 8. 24. 23:44

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

인프런 김영한 님의 강의를 듣고 작성한 글입니다.

 

[1] 애플리케이션 구현 준비

1. 구현 요구 사항

 

  • 회원 기능 
    • 회원 등록
    • 회원 조회
  • 상품 기능
    • 상품 등록
    • 상품 수정
    • 상품 조회
  • 주문 기능
    • 상품 주문
    • 주문 내역 조회
    • 주문 취소
  • 예제를 단순화하기 위해 다음 기능은 구현하지 않는다.
    • 로그인과 권한 관리
    • 파라미터 검증과 예외 처리
    • 상품은 도서만 사용
    • 카테고리는 사용
    • 배송 정보는 사용

 

2. 애플리케이션 아키텍처

계층형 구조를 사용한다.

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

개발순서 : 서비스·리포지토리 계층 개발 → 테스트 케이스 작성 검증 → 웹 계층에 적용

 

[2] 회원 리포지토리 개발

MemberRepository

@Repository
@RequiredArgsConstructor
public class MemberRepository {

    private final EntityManager em;

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

    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) //JPQL from의 대상이 엔티티라는 게 좀 다름
                .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();
    }
}

① @PersistenceContext로 엔티티를 주입하는 대신 생성자 주입으로 대체

② @RequiredArgsConstructor는 final이 붙거나 @NotNull이 붙은 필드의 생성자를 자동으로 생성해 준다.

③ createQuery로 JPQL 사용. :name을 넣고 setParameter로 값을 넣었다.

 

 

[3] 회원 서비스 개발

@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {

    private final MemberRepository memberRepository;

    //회원 가입
    @Transactional(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);
    }

}

① @Transactional : 트랜잭션이 관리하도록 함.

  • readOnly = true로 읽기 전용 메서드라는 것을 명시. findMembers(), findOne()에 적용.
  • 대신 쓰기인 join()엔 @Transactional(readOnly = false)를 명시 

② 필드 주입, 세터 주입 등이 있으나, 최근엔 생성자 주입이 대세다.

필드 주입(좌), 세터 주입(우)

③ validateDuplicateMember()로 존재하는 회원인지 확인한다고 하더라도 같은 이름이 동시에 가입하는 상황에 처할 수 있음. 그래서 실무에선 memberName에 유니크 제약 조건을 잡아주는 게 안전함.

 

 

[4] 회원 기능 테스트

@RunWith(SpringRunner.class)
@SpringBootTest
@Transactional
public class MemberServiceTest {
    @Autowired
    MemberService memberService;
    @Autowired
    MemberRepository memberRepository;


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

        //When
        Long savedId = memberService.join(member);

        //Then
        assertEquals(member, memberRepository.findOne(savedId));
        //Assertions.assertThat(member).isEqualTo(memberRepository.findOne(savedId));
    }

    @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("예외가 발생해야 한다.");
    }
}

① @SpringBootTest : 스프링부트 띄우고 테스트(이게 없으면 @Autowired 다 실패)

② @Transactional을 쓰면 실제 DB와 관계없이 테스트 가능하다.

    하지만, DB에서 어떻게 동작하는지 확인하고 싶으면 @Rollback(false)를 쓰면 된다.

 

 

■ 테스트 케이스를 위한 설정

test/resources/application.yml

spring:
#  datasource:
#    url: jdbc:h2:mem:testdb
#    username: sa
#    password:
#    driver-class-name: org.h2.Driver
#
#  jpa:
#    hibernate:
#      ddl-auto: create
#    properties:
#      hibernate:
#        #        show_sql: true
#        format_sql: true

logging.level:
  org.hibernate.SQL: debug
  org.hibernate.type: trace #스프링 부트 2.x, hibernate5
#  org.hibernate.orm.jdbc.bind: trace #스프링 부트 3.x, hibernate6

① 테스트에서 스프링을 실행하면 이 설정 파일을 읽는다.

② 스프링부트는 datasource 설정이 없으면 기본적으로 메모리 DB를 사용하고 create-drop 모드로 동작한다. 다시 말해 따로 설정하지 않아도 자동으로 메모리 모드로 돌려버린다는 뜻이다.

③ test 설정파일을 따로 해주면 create-drop 모드로 해도 부담이 없다.

 

참고] H2 DataBase Engine Cheat Sheet