1. 초기 프로젝트 구성
Lombok 환경 설정
1. Settings → Plugins → Lombok 검색 → 설치되었는지 확인
2. Preferences → Compiler → Annotation Processors의 Enable Annotation Processing 체크
3. Preferences → Build Tools → Gradle -> Build and run using, Run tests using: IntelliJ IDEA로 변경
자바로 바로 실행하게끔 하여 Gradle로 빌드하는 것보다 속도가 더 빠르다.
추가 공부할 라이브러리
1. Hikari Connection Pool (Spring Boot 기본)
2. Hibernate
3. 스프링 ORM
4. 스프링 데이터 JPA
5. 로깅: SLF4J 인터페이스 + LogBack 구현체
View 환경 설정
# Thymeleaf
HTML 마크업을 깨지 않고 구문을 추가해서 빌드한다는 장점이 있다.
H2 Database 설치
① h2database.com에서 파일 다운로드 → /h2/bin의 h2.sh 실행 → 콘솔 브라우저 오픈
② 최소 한번 실행
DB 파일을 생성할 경로를 지정해서 실행하면 파일 모드로 실행
③ 이후의 접속은 네트워크 모드로 실행한다.
2. application.yml
JPA, DB 설정 및 동작 확인
application.properties → application.yml
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/jpashop;
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 # hibernate가 남기는 모든 로그가 debug 로그로 다 확인할 수 있다.
# spring.jpa.hibernate.ddl-auto: create
애플리케이션 실행 시점에 테이블을 drop 하고, 다시 생성한다.
# show.sql
System.out 에 하이버네이트 실행 SQL을 남긴다.
EntityManager를 통한 모든 데이터의 변경은 항상 트랜잭션 안에서 이루어져야 한다.
# format.sql
보여지는 쿼리를 더욱 가독성 있게 정리한다. (굳이 사용 X)
도메인, Repository 생성
# 도메인 생성
package jpabook.jpashop;
import lombok.Getter;
import lombok.Setter;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
@Getter @Setter
public class Member {
@Id @GeneratedValue
private Long id;
private String username;
}
① @Id
해당 column이 테이블의 primary key 역할
② @GeneratedValue
primary key 값을 위한 자동 생성 전략
- GenerationType.Auto: MySQL(IDENTITY), ORACLE(SEQUENCE)
- GenerationType.IDENTITY: 각 엔티티 클래스마다 독립적으로 id가 auto_increment 된다.
- GenerationType.SEQUENCE: 모든 엔티티 클래스의 id가 모두 연결되어 자동으로 auto_increment 된다.
③ @Entity
JPA에서 정의된 필드들을 바탕으로 데이터베이스에 테이블을 만들어준다.
# Repository 생성
package jpabook.jpashop;
import org.springframework.stereotype.Repository;
import javax.persistence.EntityManager;
import javax.persistence.PersistenceContext;
@Repository // 루트 컨테이너에 Bean 객체로 생성되어, 기본 컴포넌트 스캔의 대상이 된다
public class MemberRepository {
// Spring Boot를 사용하므로 스프링 컨테이너 위에서 모든 것이 동작한다.
// Spring Boot가 @PersistenceContext 어노테이션이 있으면 Entity Manager를 주입한다.
@PersistenceContext
private EntityManager em;
// 저장 코드
// 왜 member 객체를 반환하지 않고 id를 반환하게 했을까?
// Command와 Query를 분리하기 위함
public Long save(Member member) {
em.persist(member);
return member.getId();
}
public Member find(Long id) {
return em.find(Member.class, id);
}
}
# @Repository
@Repository 어노테이션은 해당 클래스를 루트 컨테이너에 빈(Bean) 객체로 생성해준다.
↔ @Controller 어노테이션은 핸들러가 스캔할 수 있는 빈(Bean) 객체가 되어 서블릿용 컨테이너에 생성된다.
# 루트 컨테이너 vs 서블릿용 컨테이너
스프링 컨테이너에는 ①루트 컨테이너, ②각 서블릿들이 하나씩 가지고 있는 컨테이너, ③개발자가 직접 만드는 컨테이너 크게 3가지로 나뉜다.
루트 컨테이너는 프로젝트 단위로 딱 하나만 생기는 최상위 부모 컨테이너로, 웹 기술과는 관계 없는 자원에 대한 빈(Bean)을 만들어 관리한다.
서블릿 컨테이너는 구현되어 있는 서블릿 클래스의 규칙에 맞게 서블릿을 관리하며 클라이언트의 요청을 받으면 HttpServletRequest와 HttpServletResponse 객체를 생성하여 post, get 여부에 따라 동적인 페이지를 생성하여 응답한다.
각 서블릿용 컨테이너에서는 해당 서블릿 고유의 설정과 빈(Bean)을 생성해 사용하게 된다. 서블릿용 컨테이너는 루트 컨테이너의 자식이며, 부모 컨테이너로부터 필요한 걸 가져와 사용할 수 있지만 그 반대는 불가하다.
DB 작업을 위해 이 DAO 객체를 불러오려면 new를 이용하여 생성하면 안되고, 컨테이너에서 받아와야 한다. 루트 컨테이너의 객체는 어디서든 공유가 가능하기 때문에, 아래와 같이 자동 의존성 주입(DI) 어노테이션(@Autowired 등)을 이용해 객체를 받아올 수 있다.
다음은 자동 의존성 주입 어노테이션인 @Autowired를 사용하여 Test 코드를 작성한 예시이다.
package jpabook.jpashop;
import org.assertj.core.api.Assertions;
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.annotation.Rollback;
import org.springframework.test.context.junit4.SpringRunner;
import org.springframework.transaction.annotation.Transactional;
@RunWith(SpringRunner.class)
@SpringBootTest
public class MemberRepositoryTest {
@Autowired MemberRepository memberRepository;
@Test
@Transactional // Testcase에 있으면 Test가 끝난 후 DB를 롤백한다
@Rollback(false) // Rollback 하지 않는 방법
public void testMember() throws Exception {
// given
Member member = new Member();
member.setUsername("memberA");
// when
Long saveId = memberRepository.save(member);
Member findMember = memberRepository.find(saveId);
// then
Assertions.assertThat(findMember.getId()).isEqualTo(member.getId());
Assertions.assertThat(findMember.getUsername()).isEqualTo(member.getUsername());
// 이렇게는 비교 가능할까? YES
// 같은 트랜잭션 안에서 저장하고 조회하면 영속성 콘텍스트가 똑같다
// 같은 영속성 콘텍스트 안에서는 ID 값이 같으면 같은 Entity로 식별한다(영속성 콘텍스트에서는 식별자가 같으면 같은 Entity로 인식, 1차 캐시에서 기존에 관리하던 것을 반환)
Assertions.assertThat(findMember).isEqualTo(member);
}
}
# @RunWith(SpringRunner.class)
JUnit에게 Spring과 관련된 것으로 알리는 역할
# @Transactional (추가 공부 필요)
DB의 데이터를 핸들링하다가 예외가 발생된다면, 수정 이전 상태로 되돌리거나 다시 수정 작업을 진행해야 한다. 이처럼 여러 작업을 진행하다가 문제가 생겼을 경우 이전 상태로 롤백하기 위해 사용되는 것이 트랜잭션이다.
트랜잭션은 더 이상 쪼갤 수 없는 최소 작업 단위를 의미한다.
트랜잭션 + Try-Catch 문을 통해 발생할 수 있는 예외(Exception)를 핸들링할 수 있는데, 어쩔 수 없이 비즈니스 로직 코드와 트랜잭션 코드를 분리할 수 없어 복잡하게 얽혀져 있다. 이때 @Transactional 어노테이션을 사용한다면 마치 트랜잭션 코드가 존재하지 않는 것처럼 해당 로직을 클래스 밖으로 빼서 별도의 모듈로 만드는 AOP를 적용할 수 있다.
@Transactional은 그럼 정확히 어떻게 동작할까?
@Transactional가 적용된 메서드가 호출될 경우, PlatformTransactionManager를 사용하여 트랜잭션을 시작하고, 정상 여부에 따라 Commit/Rollback 동작을 수행한다. 트랜잭션 처리를 JDK Dynamic Proxy 객체에게 대신 위임하여 AOP로 동작하게 된다.
3. Test 코드를 위한 application.yml 설정
spring:
datasource:
url: jdbc:h2:tcp://localhost/~/jpashop;
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 # hibernate가 남기는 모든 로그가 debug 로그로 다 확인할 수 있다.
org.hibernate.type: trace
# org.hibernate.type: trace
sql query를 로그로 확인할 수 있다.
# p6spy
P6Spy는 기존 어플리케이션에서 코드를 변경하지 않고도 데이터베이스의 데이터를 자연스럽게 가로채고, 로그도 남기게 해주는 프레임워크.
→ build.gradle에 추가
implementation 'com.github.gavlyukovskiy:p6spy-spring-boot-starter:1.5.6'
→ 추가 설정
# Register P6LogFactory to log JDBC events
decorator.datasource.p6spy.enable-logging=true
# Use com.p6spy.engine.spy.appender.MultiLineFormat instead of com.p6spy.engine.spy.appender.SingleLineFormat
decorator.datasource.p6spy.multiline=true
# Use logging for default listeners [slf4j, sysout, file, custom]
decorator.datasource.p6spy.logging=slf4j
# Log file to use (only with logging=file)
decorator.datasource.p6spy.log-file=spy.log
# Class file to use (only with logging=custom). The class must implement com.p6spy.engine.spy.appender.FormattedLogger
decorator.datasource.p6spy.custom-appender-class=my.custom.LoggerClass
# Custom log format, if specified com.p6spy.engine.spy.appender.CustomLineFormat will be used with this log format
decorator.datasource.p6spy.log-format=
# Use regex pattern to filter log messages. If specified only matched messages will be logged.
decorator.datasource.p6spy.log-filter.pattern=
# Report the effective sql string (with '?' replaced with real values) to tracing systems.
# NOTE this setting does not affect the logging message.
decorator.datasource.p6spy.tracing.include-parameter-values=true
https://github.com/gavlyukovskiy/spring-boot-data-source-decorator
출처
'🌱 Spring > JPA ①' 카테고리의 다른 글
[Spring] 주문 Entity, Repository, Service 개발 (0) | 2023.08.09 |
---|---|
[Spring] 상품 Entity, Repository, Service 개발 (0) | 2023.08.05 |
[Spring] 회원 Service Unit Test 작성하기 (0) | 2023.08.05 |
[Spring] 회원 Service & Repository 개발 (0) | 2023.08.04 |
[Spring] Entity 설계 및 구현 (0) | 2023.08.03 |