📌 구현 요구사항
- 회원 기능
- 회원 등록
- 회원 조회
- 상품 기능
- 상품 등록
- 상품 수정
- 상품 조회
- 주문 기능
-상품 주문
-주문 내역 조회
-주문 취소
✔ 예제를 단순화하기 위해 다음 기능은 구현하지 않음
- 로그인과 권한 관리 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-auto
도create-drop
모드로 동작함 - 데이터소스나, JPA 관련된 별도의 추가 설정을 하지 않아도 됨
<실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발_김영한>을 수강하고 작성한 글입니다

PREV
[스프링 부트와 JPA 활용1] 2. 도메인 분석 설계
📌 요구사항 분석 📌 기능 목록 회원 기능 회원 등록 회원 조회 상품 기능 상품 등록 상품 수정 상품 조회 주문 기능 상품 주문 주문 내역 조회 주문 취소 기타 요구사항 상품은 재고 관리가 필
nyeroni.tistory.com
NEXT
[스프링 부트와 JPA 활용 1] 4. 상품 도메인 개발
📌 상품 도메인 개발 ✔ 구현 기능 상품 등록 상품 목록 조회 상품 수정 ✔ 순서 상품 엔티티 개발(비즈니스 로직 추가) 상품 리포지토리 개발 상품 서비스 개발 상품 기능 테스트 📌 상품 엔티
nyeroni.tistory.com
'인프런 Spring 강의 정리 > 실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발' 카테고리의 다른 글
[스프링 부트와 JPA 활용 1] 6. 웹 계층 개발 (1) | 2024.01.24 |
---|---|
[스프링 부트와 JPA 활용 1] 5. 주문 도메인 개발 (0) | 2024.01.24 |
[스프링 부트와 JPA 활용 1] 4. 상품 도메인 개발 (0) | 2024.01.23 |
[스프링 부트와 JPA 활용1] 2. 도메인 분석 설계 (2) | 2024.01.23 |
[스프링 부트와 JPA 활용 1] 1. 프로젝트 환경 설정 (1) | 2024.01.23 |