1. 회원 리포지토리 개발
@Repository // 컴포넌트 스캔에 의해 자동으로 등록(스프링 빈으로 등록)
@RequiredArgsConstructor
public class MemberRepository {
// @PersistenceContext 사용하면 EntityManager를 영속성 컨텍스트로 등록해 주입받을 수 있음
// @RequiredArgsConstructor + final 조합은
// final이 붙은 필드를 파라미터로 받는 생성자를 자동으로 생성해준다.
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은 엔티티를 대상으로 매핑한다. SQL은 테이블을 기준
.getResultList();
}
public List<Member> findName(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) 주입 (쓸 일 거의 없음)
@PersistenceUnit
private EntityManagerFactory emf;
2. 회원 서비스 개발
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {
// @RequiredArgsConstructor + final 조합으로
// final이 붙은 필드를 파라미터로 받는 생성자를 자동으로 생성
private final MemberRepository memberRepository;
// 회원 가입
@Transactional
public Long join(Member member){
// 중복회원 검증
validateDuplicateMember(member);
memberRepository.save(member);
return member.getId();
}
private void validateDuplicateMember(Member member) {
List<Member> findMembers = memberRepository.findName(member.getName());
// 실무에서는 검증 로직이 있어도 멀티 쓰레드 상황을 고려해서
// 회원 테이블의 회원명 컬럼에 유니크 제약 조건을 추가하는 것이 안전하다.
if(!findMembers.isEmpty()){
throw new IllegalStateException("이미 존재하는 회원");
}
}
// 회원 전체 조회
public List<Member> findAll(){
return memberRepository.findAll();
}
// 회원 개별 조회
public Member findOne(Long memberId){
return memberRepository.findOne(memberId);
}
}
- @Transactional: 트랜잭션, 영속성 컨텍스트
- readOnly=true: 데이터의 변경이 없는 읽기 전용 메서드에 사용, 영속성 컨텍스트를 플러시 하지 않으므로 약간의 성능 향상(읽기 전용에는 다 적용)
- 데이터베이스 드라이버가 지원하면 DB에서 성능 향상
클래스 레벨에 @Transactional(readOnly = true)를 붙여 전체 기본은 읽기 전용으로 하고, 쓰기 작업이 필요한 메서드에는 메서드 단위로 @Transactional을 덧붙여 readOnly를 false로 덮어씌움 Spring은 메서드 레벨의 @Transactional이 클래스보다 우선순위가 높기 때문에 readOnly false로 동작한다.
- @Autowired: 스프링 빈에 등록되어있는 MemberRepository를 injection. 생성자 Injection 많이 사용, 생성자가 하나면 생략 가능
필드주입 vs 생성자 주입vs롬복
public class MemberService {
@Autowired
MemberRepository memberRepository;
...
}
- 스프링이 memberRepository에 값을 자동으로 넣어줌
- 테스트하기 어려움 (객체를 직접 만들기 힘듦)
- final을 못 붙임 → 누가 나중에 값을 바꿀 수도 있음
- 생성 시점에 값이 안 들어있을 수도 있음 → 오류를 컴파일 때 못 잡음
public class MemberService {
private final MemberRepository memberRepository;
public MemberService(MemberRepository memberRepository) {
this.memberRepository = memberRepository;
}
...
}
- 그래서 필드주입보다는 생성자 주입 방식을 권장한다.
- 생성자 주입 방식을 사용하면 변경 불가능한 안전한 객체 생성 가능하다.(생성될 때 딱 한 번 주입되고, 이후엔 바꿀 수 없음)
- 생성자가 하나면, `@Autowired` 를 생략할 수 있다.
- `final` 키워드를 추가하면 컴파일 시점에 `memberRepository` 를 설정하지 않는 오류를 체크할 수 있다.(보통 기본 생성자를 추가할 때 발견 -> 실수 방지 가능)
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
}
- 롬복의 @RequiredArgsConstructor를 사용하면 final이 붙은 필드를 파라미터로 받는 생성자를 자동으로 생성해준다.
3. 회원 테스트
@SpringBootTest
@Transactional
@Rollback(value = false)
class MemberServiceTest {
@Autowired
MemberService memberService;
@Autowired
MemberRepository memberRepository;
@Autowired
EntityManager em;
@Test
@DisplayName("회원가입")
public void join() throws Exception {
// given
Member member = new Member();
member.setName("kim");
// when
Long saveId = memberService.join(member);
// then (가입이 되었는지 확인. "kim"을 넣은 member 객체와 memberRepository에서 saveId로 찾은 객체가 같은지)
em.flush(); // 영속성 컨텍스트에 있는 데이터를 쿼리로 날려서 DB에 저장시킨다
assertEquals(member, memberRepository.findOne(saveId));
}
- @SpringBootTest: 스프링 부트 띄우고 테스트(이게 없으면 @Autowired 다 실패한다.)
- @Transactional: 반복 가능한 테스트 지원, 각각의 테스트를 실행할 때마다 트랜잭션을 시작하고 테스트가 끝나면 트랜잭션을 강제로 롤백시킨다. (이 어노테이션이 테스트 케이스에서 사용될 때만 롤백)
- @Rollback: (vlaue=false)로 지정하면 @Transactional에서 강제로 롤백시키는 롤백을 정지시킨다.

em.flush()는 그 순간까지 영속성 컨텍스트(Persistence Context)에만 존재하던 변경 사항을 DB에 반영해서, insert 쿼리가 로그에 보이도록 하는 역할을 하기 때문에 insert문을 로그에서 볼 수 있다.

즉, @Rollback(false)를 지정하면 트랜잭션이 커밋되어 실제 DB(H2)에 반영된다. 따라서 테스트 이후에도 "kim"이라는 이름의 회원이 DB에 남아 있고, 직접 조회 가능하다. @Transactional이 테스트 클래스나 메서드에 붙어 있으면, 테스트 메서드가 끝난 후 자동으로 트랜잭션을 롤백한다. 그래서 테스트 중에는 INSERT가 일어나더라도, DB에는 남지 않는다.
테스트 케이스를 위한 설정
테스트는 케이스 격리된 환경에서 실행하고, 끝나면 데이터를 초기화하는 것이 좋다. 그런 면에서 메모리 DB를 사용하는 것이 가장 이상적이다. 추가로 테스트 케이스를 위한 스프링 환경과, 일반적으로 애플리케이션을 실행하는 환경은 보통 다르므로 설정 파일을 다르게 사용하면 된다.
test/resources/application.yml에 다음과 같이 간단하게 테스트용 설정 파일을 추가하면 된다.
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.orm.jdbc.bind: trace
테스트를 실행하면, 스프링이 먼저 설정 파일(application.yml)을 찾는다.
- 테스트용 설정 파일이 있다면 (예: test/resources/application.yml) → 이걸 먼저 읽음
- 없으면 src/main/resources/application.yml 기본 설정 파일을 읽는다.
스프링 부트는 datasource 설정이 없으면, 기본적을 메모리 DB를 사용하고, driver-class도 현재 등록된 라이브러를 보고 찾아준다.
추가로 `ddl-auto`도`create-drop`모드로 동작한다. 따라서 데이터소스나, JPA 관련된 별도의 추가 설정을 하지 않아도 된다.
'BE > SpringBoot' 카테고리의 다른 글
| [JPA] 6. 주문 도메인 개발 - 주문, 주문상품 엔티티 개발 (1) | 2025.05.14 |
|---|---|
| [JPA] 5. 상품 도메인 개발 (0) | 2025.05.14 |
| [JPA] 3. 애플리케이션 구현 준비 (0) | 2025.05.12 |
| [JPA] 2. 도메인 분석 설계 - 엔티티 설계시 주의점 (0) | 2025.05.11 |
| [JPA] @Embedded @Embedable 임베디드 타입이란? (0) | 2025.05.07 |