Super Kawaii Cute Cat Kaoani [스프링 부트와 JPA 활용 1] 3. 애플리케이션 구현 준비, 회원 도메인 개발

[스프링 부트와 JPA 활용 1] 3. 애플리케이션 구현 준비, 회원 도메인 개발

2024. 1. 23. 12:39
728x90
SMALL

📌 구현 요구사항

  • 회원 기능
    • 회원 등록
    • 회원 조회
  • 상품 기능
    • 상품 등록
    • 상품 수정
    • 상품 조회
  • 주문 기능
    -상품 주문
    -주문 내역 조회
    -주문 취소

✔ 예제를 단순화하기 위해 다음 기능은 구현하지 않음

  • 로그인과 권한 관리 X
  • 파라미터 검증과 예외처리 X
  • 상품은 도서만 사용
  • 카테고리 사용 X
  • 배송정보 사용 X

 

 

📌 애플리케이션 아키텍처

✔ 계층형 구조 사용

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

✔ 패키지 구조

  • jpabook.jpashop
    • domain
    • exception
    • repository
    • service
    • web
개발 순서 : 서비스, 리포지토리 계층을 개발하고, 테스트 케이스를 작성해서 검증, 마지막에 웹 계층 적용

 

 

📌 회원 도메인 개발

✔ 구현기능

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

✔ 순서

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

 

 

📌 회원 리포지토리 개발

MemberRepository.java

🔶 main/.../jpa.shop/repository/MemberRepository

package jpabook.jpashop.repository;

import jakarta.persistence.EntityManager;
import jakarta.persistence.PersistenceContext;
import jpabook.jpashop.domain.Member;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class MemberRepository {

    @PersistenceContext
    private 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).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

  • Spring Framework에서 사용되는 애노테이션
  • 컴포넌트스캔에 의해서 자동으로 스프링 빈으로 등록되어 스프링 컨테이너에서 관리가 됨
  • JPA 예외를 스프링 기반 예외로 예외 변환

@PersistenceContext

  • JPA에서 사용되는 애노테이션
  • 엔티티 매니저를 주입받는 용도로 쓰임
  • EntityManager : JPA를 사용하여 엔티티와 데이터베이스 간의 통신을 관리하는 중요한 역할

💡 @PersistenceUnit : 엔티티 매니저 팩토리 주입

 

persist()

  • 주어진 엔티티를 영속성으로 컨텍스트에 저장하는 메서드
  • 영속성 컨테스트에 저장된 엔티티는 데이터베이스에 반영됨

 find()

  • 주어진 엔티티 클래스와 해당 엔티티의 기본키 값을 기반으로 데이터베이스에서 엔티티 조회

createQuery()

  • JPQL(JPA Query Language)를 사용하여 데이터베이스에서 엔티티 조회하거나 조작하는 쿼리를 생성하는 역할
  • em.createQuery("JPQL 쿼리", 반환 타입)

getResultList()

  • JPQL 쿼리의 실행 결과를 리스트 형태로 반환하는 메서드
  • 조회된 엔티티들을 리스트로 반환함

setParameter()

  • JPQL 쿼리에서 사용되는 파라미터를 설정하는 메서드
  • 쿼리 내에서 :name과 같이 사용된 파라미터에 값을 바인딩함

 

 

📌 회원 서비스 개발

MemberService.java

🔶 main/.../jpa/shop/service/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 java.util.List;

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

    @Autowired
    public 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);
    }
}

@Service

  • 스프링 컨테이너에 해당 클레스의 인스턴스를 빈으로 등록 -> 의존성 주입을 통해 다른 컴포넌트들과 상호작용 가능

@Transactional

  • 스프링 프레임워크에서 트랜잭션을 관리하기 위해 사용
  • 트렌잭션 : 데이터베이스와 같은 영속성 저장소에서 여러 작업을 묶어서 원자적으로 실행하고, 일관성을 유지하고, 무결성을 보장하는 기술
  • 메서드가 끝날 떄 커밋 또는 롤백되며, 데이터베이스의 일관성과 무결성 유지
  • 데이터베이스 드라이버가 지원하면 DB에서 성능 향상
  • @Transactional(readOnly = true) 옵션
    • JPA가 조회하는 곳에서는 성능을 최적화
    • 읽기에는 가급적이면 사용 권장(쓰기에는 사용하면 안됨)
  • 여기서는 읽기가 많으므로 기본을 @Transactional(readOnly = true)사용하고 쓰기가 쓰이는 곳에 따로 @Transaction해줌

@Autowired

  • 스프링이 스프링 빈에 등록되어있는 MemberRepository를 주입해줌

 

💡 cf) 실무에서는 검증 로직이 있어도 멀티 쓰레드 상황을 고려해서 회원 테이블의 회원명 컬럼에 유니크 제 약 조건을 추가하는 것이 안전하다.

 

💡 cf) 스프링 필드 주입 대신에 생성자 주입을 사용하자.

 

필드 주입

public class MemberService{
    @Autowired
    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;
    ...
}

@RequiredArgsConstructor

  • Lombok 라이브러리에서 제공
  • 생성자를 자동으로 생성해주는 기능
  • 주로 의존성 주입을 받는 필드를 갖는 클래스에서 사용되며 생성자를 편리하게 생성
  • final키워드가 붙은 필드에 대한 생성자가 자동으로 생성
    • 의존성 주입을 통해 필드에 값을 주입
💡 cf) 스프링 데이터 JPA를 사용하면 EntityManager도 주입 가능

 

@Repository
@RequiredArgsConstructor
public class MemberRepository {
    private final EntityManager em;
... 
}
  • 원래 엔티티매니저는 @PersistenceContext가 있어야 되는데 스프링 부트에서는 @Autowired로 해도됨
    • @RequiredArgsConstructor이 사용 가능해짐

 

MemberService.java 최종 코드

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)
@RequiredArgsConstructor
public class MemberService {

//    @Autowired
//    public MemberRepository memberRepository;
    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);
    }
}
반응형

 

 

📌 회원 기능 테스트

⚡ 테스트 요구사항

  • 회원가입을 성공해야 함
  • 회원가입할 떄 같은 이름이 있으면 에러 발생

 

MemberServiceTest.java

🔶 test/.../jpashop/service/MemberServiceTest

package jpabook.jpashop.service;

import jakarta.persistence.EntityManager;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.repository.MemberRepository;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
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.junit.jupiter.SpringExtension;
import org.springframework.transaction.annotation.Transactional;

import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(SpringExtension.class)
@SpringBootTest
@Transactional
public class MemberServiceTest {

    @Autowired MemberService memberService;
    @Autowired MemberRepository memberRepository;
    @Autowired EntityManager em; //insert문
    @Test
    //@Rollback(false)
    public void 회원가입() throws Exception {
        //given
        Member member = new Member();
        member.setName("kim");

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

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

    @Test
    public void 중복_회원_예외() throws Exception {
        //given
        Member member1 = new Member();
        member1.setName("kim");

        Member member2 = new Member();
        member2.setName("kim");


        //when
        memberService.join(member1);

        //then
        assertThrows(IllegalStateException.class, () -> {memberService.join(member2);});
        //fail("예외가 발생해야 한다.");
    }

}

@Test(expected = IllegalStateException.class)

  • juni4 사용 시에 쓰는 방법으로 Junit5에는 지원하지 않음
  • 예외처리 할 떄 쓰는 방법
  • 밑에 try-catch 코드 대신 쓰는 간결한 방법
  • Junit5에는 assertThrows(IllegalStateException.class, () -> {memberService.join(member2);});사용해야 함
memberService.join(member1);
try{
    memberService.join(member2);//예외 발생
}catch(IllegalStateException e){
    return;
}

@ExtendWith(SpringExtension.class)

  • Junit5의 확장 기능
  • 스프링 테스트 컨텍스트를 사용할 수 있게 해줌

@SpringBootTest

  • 스프링 부트 기반 테스트에서 사용
  • 테스트 시 스프링 컨텍스트를 로드하여 테스트 환경 구축
  • 이게 없으면 @Autowired 다 실패

@Transactional

  • 테스트 메서드 내에서 모든 데이터 변경 작업을 트랜잭션 내에서 실행하고, 테스트 종료 시 롤백하여 데이터베이스 상태를 원래대로 되돌림
  • 반복 가능한 테스트 지원

🔷 @Test

  • JUnit 테스트 메서드를 정의하는 어노테이션
  • 해당 메서드가 테스트 코드임을 나타냄
  • 해당 메서드 내에 테스트 로직 작성

🔷 em.flush();

  • 영속성 컨텍스트의 변경 내용을 데이터베이스에 동기화하는 작업 수행
  • 트랜잭션 내에서 변경 사항을 데이터베이스에 반영

🔷 assertThrows

  • 예외가 발생하는지 확인하는 메서드
  • 예외가 발생하지 않으면 테스트 실패

🔷 fail()

  • 테스트를 강제로 실패하게 만듦
💡 cf) 테스트 케이스 작성 고수 되는 마법: Given, When, Then (http://martinfowler.com/bliki/GivenWhenThen.html)

 

 

 

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

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

🔶 test/resources/application.yml

  • test 파일 밑에 resources 디렉토리 생성
spring:
  datasource:
    url: jdbc:h2:mem:test
    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
  • url을 메모리모드로 변경
  • 테스트에서 스프링을 실행하면 이 위치에 있는 설정파일을 읽음
    • 만약 이 위치에 없으면 src/resources/application.yml 읽음
  • 스프링 부트는 datasource 설정이 없으면, 기본적으로 메모리 DB를 사용하고, driver-class도 현재 등록된 라이브러리를 보고 찾아줌
  • 추가로 ddl-autocreate-drop모드로 동작함
    • 데이터소스나, JPA 관련된 별도의 추가 설정을 하지 않아도 됨

 

 

 

<실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발_김영한>을 수강하고 작성한 글입니다

 

 

 

 


PREV

 

[스프링 부트와 JPA 활용1] 2. 도메인 분석 설계

📌 요구사항 분석 📌 기능 목록 회원 기능 회원 등록 회원 조회 상품 기능 상품 등록 상품 수정 상품 조회 주문 기능 상품 주문 주문 내역 조회 주문 취소 기타 요구사항 상품은 재고 관리가 필

nyeroni.tistory.com

NEXT

 

[스프링 부트와 JPA 활용 1] 4. 상품 도메인 개발

📌 상품 도메인 개발 ✔ 구현 기능 상품 등록 상품 목록 조회 상품 수정 ✔ 순서 상품 엔티티 개발(비즈니스 로직 추가) 상품 리포지토리 개발 상품 서비스 개발 상품 기능 테스트 📌 상품 엔티

nyeroni.tistory.com

 

728x90
LIST

BELATED ARTICLES

more