👇🏻 이전 글
2024.08.06 - [Spring] - Spring의 IoC(제어의 역전), DI(의존성 주입) 이해하기
ORM과 JPA
ORM : Object-Relational Mapping 객체와 데이터베이스를 매핑해주는 도구
JPA는 자바 ORM 중 대표적인 표준 명세이다.
JPA는 애플리케이션과 JDBC 사이에서 동작되며 DB 연결 과정을 자동으로 처리해주고, 객체를 통해 간접적으로 DB 데이터를 다룰 수 있어 DB 작업을 보다 쉽게 처리 할 수 있다.
Entity
Entity란 JPA에서 관리되는 객체. Entity는 DB의 테이블과 매핑되어 JPA에 의해 관리 됨.
Memo Entity
@Entity // JPA가 관리할 수 있는 Entity 클래스 지정
@Table(name = "memo") // 매핑할 테이블의 이름을 지정
public class Memo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
// nullable: null 허용 여부
// unique: 중복 허용 여부 (false 일때 중복 허용)
@Column(name = "username", nullable = false, unique = true)
private String username;
// length: 컬럼 길이 지정
@Column(name = "contents", nullable = false, length = 500)
private String contents;
}
@Entity : JPA가 관리할 수 있는 Entity 클래스로 지정.
@Table : 매핑할 테이블 지정.
@Id : 테이블의 기본 키 지정. 이 기본 키는 영속성 컨텍스트에서 Entity를 구분하고 관리할 때 사용되는 식별자 역할 수행. ( 기본 키를 넣지 않고 저장하면 오류 발생 )
@Column : 필드와 매핑할 테이블의 컬럼 지정.
@GeneratedValue : 이 옵션을 추가하면 기본 키 생성을 DB에 위임 할 수 있음.
@GeneratedValue(strategy = GenerationType.IDENTITY) : 이 옵션을 추가하면 개발자가 직접 id 값을 넣어주지 않아도 자동으로 순서에 맞게 기본 키가 추가 됨.
create table memo (
id bigint not null auto_increment,
contents varchar(500) not null,
username varchar(255) not null,
primary key (id)
);
위와 같은 코드를 통해 다음과 같은 sql문이 만들어짐을 확인 할 수 있다.
영속성 컨텍스트 Persistence Context
영속성 Persistence = 객체가 생명(객체가 유지되는 시간)이나 공간(객체의 위치)을 자유롭게 유지하고 이동할 수 있는 객체의 성질
영속성 컨텍스트는 Entity 객체를 효율적으로 쉽게 관리하기 위해 만들어진 공간
개발자가 JPA를 사용하여 DB 작업을 처리 할 때 이 과정을 효율적으로 처리하기 위해 JPA는 영속성 컨텍스트에 Entity 객체들을 저장하여 DB와 소통한다.
EntityManager : Entity를 관리하는 관리자. EntityManager는 EntityManagerFactory를 통해 생성하여 사용 할 수 있음.
EntityManagerFactory : 일반적으로 DB 하나에 하나만 생성되어 애플리케이션이 동작하는 동안 사용됨.
JPA의 트랜잭션
트랜잭션 : DB 데이터들의 무결성과 정합성을 유지하기 위한 하나의 논리적 개념. 여러 개의 SQL이 하나의 트랜잭션에 포함 될 수 있음. 이 때, 모든 SQL이 성공적으로 수행이 되면 DB에 영구적으로 변경을 반영하지만 SQL 중 단 하나라도 실패한다면 모든 변경을 되돌림(rollback)
JPA에서도 영속성 컨텍스트로 관리하고 있는 변경이 발생한 객체들의 정보를 쓰기 지연 저장소에 전부 가지고 있다가 마지막에 SQL을 한번에 DB에 요청해 변경을 반영함.
EntityTransaction et = em.getTransaction();
et.begin(); // 트랜잭션을 시작하는 명령어
et.commit(); // 트랜잭션의 작업들을 영구적으로 DB에 반영하는 명령어
et.rollback(); // 오류가 발생했을 때 트랜잭션의 작업을 모두 취소하고 이전 상태로 되돌리는 명령어
영속성 컨텍스트는 내부적으로 캐시 저장소를 가지고 있고 Entity 객체들은 이 저장소에 저장된다. 캐시 저장소는 Map 자료구조 형태로 되어 있다. Key에는 @Id로 매핑한 기본 키를 저장하고, value에는 해당 Entity 객체를 저장한다.
Entity 저장
em.persist(entity);
Entity 조회
em.find( entity.class , Id );
- 캐시 저장소에 조회하는 Id가 존재하지 않는 경우
캐시 저장소에 존재하지 않기 때문에 DB 조회 후 캐시 저장소에 저장
- 캐시 저장소에 조회하는 Id가 존재하는 경우
이미 캐시 저장소에 해당값이 존재하기 때문에 DB 조회하지 않고 캐시 저장소에서 해당값 반환
Entity 삭제
em.remove(entity);
삭제할 Entity를 조회한 후 캐시 저장소에 없다면 DB에서 조회한 후 캐시 저장소에 저장함.
이후 em.remove( ) 를 호출 해 DB에 DELETE SQL이 요청된다.
flush( )
em.flush();
트랜잭션 commit 후 추가적인 동작 > 메소드 em.flush( ) 호출
영속성 컨텍스트의 변경 내용들을 DB에 반영하는 역할 수행. commit으로 트랜잭션이 종료되면 flush로 쓰기 지연 저장소의 SQL들을 DB에 요청해야 함.
변경 감지 Dirty Checking
JPA는 트랜잭션이 commit되고 flush가 호출되면 Entity의 현재 상태와 저장한 최초 상태를 비교. 변경 내용이 있다면 Update SQL을 생성하여 쓰기 지연 저장소에 저장하고 DB에 반영 요청.
> 변경하고자 하는 데이터가 있다면 먼저 데이터 조회 후 해당 Entity 객체의 데이터를 변경하면 자동으로 Update SQL이 생성되어 반영됨. 이러한 과정을 변경 감지 Dirty Checking 이라고 함.
Entity의 상태
- 비영속 Transient : new 연산자를 통해 인스턴스화 된 Entity 객체. 아직 영속성 컨텍스트에 저장되지 않았기 때문에 JPA의 관리를 받지 않음.
- 영속 Managed : 비영속 Entity를 EntityManager를 통해 영속성 컨텍스트에 저장하여 관리되고 있는 상태로 만듬.
- 준영속 Detached : 영속성 컨텍스트에 저장되어 관리되다가 분리된 상태 의미. 영속 상태에서 준영속 상태로 변경하려면 detach( ) 사용. 준영속 상태로 전환되면 캐시 저장소에서 제거되기 때문에 JPA의 관리를 받지 못 함. 준영속 상태에서 다시 영속 상태로 변경하려면 merge( ) 사용.
- 삭제 Removed : 삭제하기 위해 조회해온 영속 상태의 Entity를 파라미터로 전달받아 삭제 상태로 전환 함.
SpringBoot JPA
build.gradle
// JPA 설정
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
application.properties
spring.jpa.hibernate.ddl-auto=update
spring.jpa.properties.hibernate.show_sql=true
spring.jpa.properties.hibernate.format_sql=true
spring.jpa.properties.hibernate.use_sql_comments=true
show_sql, format_sql, user_sql_comments 옵션 : SQL을 보기 좋게 출력
ddl-auto
- create : 기존 테이블 삭제 후 다시 생성
- create-drop : create와 같으나 종료 시점에서 테이블 drop
- update : 변경된 부분만 반영
- validate : Entity와 테이블이 정상 매핑 되었는지만 확인
- none : 아무것도 하지 않음
Memo Entity
@Entity // JPA가 관리할 수 있는 Entity 클래스 지정
@Getter
@Setter
@Table(name = "memo") // 매핑할 테이블의 이름을 지정
@NoArgsConstructor
public class Memo {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "username", nullable = false)
private String username;
@Column(name = "contents", nullable = false, length = 500)
private String contents;
public Memo(MemoRequestDto requestDto) {
this.username = requestDto.getUsername();
this.contents = requestDto.getContents();
}
public void update(MemoRequestDto requestDto) {
this.username = requestDto.getUsername();
this.contents = requestDto.getContents();
}
}
SpringBoot 환경에서는 EntityManagerFactory와 EntityManager를 자동으로 생성해준다.
@PersistenceContext
EntityManager em;
@PersistenceContext 에너테이션을 사용하면 자동으로 생성된 EntityManager를 주입받아 사용할 수 있다.
Spring 프레임워크에서는 DB의 트랜잭션 개념을 애플리케이션에 적용할 수 있도록 트랜잭션 관리자를 제공 한다.
- @Transactional 에너테이션을 클래스나 메소드에 추가하면 쉽게 트랜잭션 개념을 적용할 수 있다.
- 메소드가 호출되면 해당 메소드 내에서 수행되는 모든 DB 연산 내용은 하나의 트랜잭션으로 묶인다.
- 이 때, 해당 매소드가 정상적으로 수행되면 트랜잭션을 커밋하고, 예외가 발생하면 롤백한다.
- 클래스에 선언한 @Transactional은 해당 클래스 내부의 모든 메소드에 트랜잭션 기능을 부여한다.
Spring Data JPA
Spring Data JPA는 JPA를 쉽게 사용할 수 있게 만들어놓은 하나의 모듈. JPA를 추상화시킨 Repository 인터페이스를 제공한다.
MemoRepository.java
// JpaRepository<"@Entity클래스", "@Id의 데이터 타입"> 를 상속받는 interface 선언
public interface MemoRepository extends JpaRepository<Memo, Long> {
}
Spring Data JPA에 의해 자동으로 Bean 등록 됨.
MemoRepository는 DB의 memo 테이블과 연결되어 CRUD 작업을 처리하는 인터페이스가 된다.
MemoService.java
@Service
public class MemoService {
private final MemoRepository memoRepository;
public MemoService(MemoRepository memoRepository) {
this.memoRepository = memoRepository;
}
public MemoResponseDto createMemo(MemoRequestDto requestDto) {
// RequestDto -> Entity
Memo memo = new Memo(requestDto);
// DB 저장
// save 메소드를 사용해 Entity를 영속성 컨텍스트에 저장
Memo saveMemo = memoRepository.save(memo);
// Entity -> ResponseDto
MemoResponseDto memoResponseDto = new MemoResponseDto(memo);
return memoResponseDto;
}
public List<MemoResponseDto> getMemos() {
// findAll 메소드를 사용해 해당 테이블의 전체 데이터를 조회
// 반환 타입이 List<MemoResponseDto> 이기 때문에 .stream().map(MemoResponseDto::new).toList()로 변환
return memoRepository.findAll().stream().map(MemoResponseDto::new).toList();
}
@Transactional
public Long updateMemo(Long id, MemoRequestDto requestDto) {
// 해당 메모가 DB에 존재하는지 확인
Memo memo = findMemo(id);
// 변경 감지를 통해 update 진행
// 변경 감지가 적용되도록 @Transactional 추가
// Memo Entity 클래스의 update 함수를 사용하여 memo 안의 데이터 변경
memo.update(requestDto);
return id;
}
public Long deleteMemo(Long id) {
// 해당 메모가 DB에 존재하는지 확인
Memo memo = findMemo(id);
memoRepository.delete(memo);
return id;
}
private Memo findMemo(Long id) {
// findById 메소드의 반환 타입은 Optional이기에 orElseThrow()로 예외 처리
return memoRepository.findById(id).orElseThrow(()->
new IllegalArgumentException("Memo not found")
);
}
}
update와 delete시 메모 조회하는 부분은 공통으로 사용되는 코드이므로 findMemo 이름의 메소드로 따로 빼서 구현.
JPA Auditing
데이터의 생성시간과 수정시간은 포스팅, 게시글, 댓글 등 다양한 데이터에 매우 자주 활용된다.
Spring Data JPA에서는 시간에 대해서 자동으로 값을 넣어주는 기능인 JPA Auditing을 제공
Timestamped.java
@Getter
@MappedSuperclass
@EntityListeners(AuditingEntityListener.class)
public abstract class Timestamped {
@CreatedDate
@Column(updatable = false)
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime createdAt;
@LastModifiedDate
@Column
@Temporal(TemporalType.TIMESTAMP)
private LocalDateTime modifiedAt;
}
- @MappedSuperclass : JPA Entity 클래스들이 해당 추상 클래스를 상속할 경우 createdAt, modifiedAt 처럼 추상 클래스에 선언한 멤버변수를 컬럼으로 인식할 수 있다.
- @EntityListeners(AuditingEntityListener.class) : 해당 클래스에 Auditing 기능을 포함시킨다.
- @CreatedDate : Entity 객체가 생성되어 저장될 때 시간이 자동으로 저장 된다. 최초 생성 시간이 저장되고 그 이후에는 수정되면 안되기 때문에 updatable = false 옵션 추가.
- @LastModifiedData : 조회한 Entity 객체의 값을 변경할 때 변경된 시간이 자동으로 저장 된다.
- @Temporal : 날짜 타입( java.util.Date, java.util.Calendar )을 매핑할 때 사용한다.
❗️ @SpringBootApplication이 있는 클래스에 @EnableJpaAuditing 추가 : Auditing 기능을 사용하겠다느 정보를 전달하기 위함
MemoResponseDto.java
@Getter
public class MemoResponseDto {
private Long id;
private String username;
private String contents;
private LocalDateTime createdAt;
private LocalDateTime modifiedAt;
public MemoResponseDto(Memo memo) {
this.id = memo.getId();
this.username = memo.getUsername();
this.contents = memo.getContents();
this.createdAt = memo.getCreatedAt();
this.modifiedAt = memo.getModifiedAt();
}
}
Memo Entity에서 Timestamped를 상속받아 사용할 것이기 때문에 Memo extends Timestamped를 해준다.
또한 MemoResponseDto에서 새롭게 저장된 시간 칼럼을 반환해야 하기 때문에 변수 선언도 해줘야 한다.
Query Methods
Spring Data JPA에서는 메소드 이름으로 SQL을 생성할 수 있는 Query Methods 기능을 제공한다.
// 예시
public interface MemoRepository extends JpaRepository<Memo, Long> {
List<Memo> findAllByOrderByModifiedAtDesc();
List<Memo> findAllByUsername(String username);
}
findAllByOrderByModifiedAtDesc( ) : 해당 메소드를 통해 Memo 테이블에서 ModifiedAt(수정시간)을 기준으로 전체 데이터를 내림차순으로 가져오는 SQL을 실행 할 수 있다.
findAllByUsername(String username) : 이 메소드는 파라미터로 넘겨받는 username에 해당하는 데이터 전부를 가져오는 SQL을 실행하므로 파라미터에 해당 값의 데이터 타입과 변수명을 선언해준다.
MemoService.java
public List<MemoResponseDto> getMemos() {
return memoRepository.findAllByOrderByModifiedAtDesc().stream().map(MemoResponseDto::new).toList();
}
위와 같이 사용해 가장 최신 메모가 상단에 올 수 있도록 수정할 수 있다.
TIL 💭
Entity와 트랜잭션에 대해 깊이 있게 배울 수 있어서 좋았다. JPA가 어떤 방식으로 편리하게 DB 관리를 도와주는지 직접 코드로 구현해보니 이해하기 쉬웠다.
'Spring' 카테고리의 다른 글
Spring의 Filter 이해하기 및 구현하기 (0) | 2024.08.16 |
---|---|
Spring에서 JWT 이해하기 + 쿠키와 세션 개념까지 (0) | 2024.08.16 |
Spring의 IoC(제어의 역전), DI(의존성 주입) 이해하기 (0) | 2024.08.06 |
Spring을 3 Layer Architecture로 역할 분리하기 (0) | 2024.08.06 |
Spring과 MySQL로 CRUD 기능이 있는 메모장 만들기 (0) | 2024.08.05 |