모든 최적화는 ‘기본’에서 시작된다

Dirty Checking, Hybrid 삭제 전략, N+1 문제 해결 등은 모두 JPA의 핵심 원리인 영속성 컨텍스트(Persistence Context)객체-관계 매핑(ORM) 에 대한 깊은 이해를 바탕으로 합니다.

이번 글에서는 doeatfit 프로젝트의 기반이 되는 Base Entity 코드들을 통해 JPA의 기초 원리와 설계 패턴을 정리해 봅니다.

1. 영속성 컨텍스트 (Persistence Context): JPA의 핵심심

Dirty Checking이나 Lazy Loading 같은 기능이 가능한 이유는 바로 영속성 컨텍스트 때문입니다.

💡 개념: 엔티티를 영구 저장하는 환경

영속성 컨텍스트는 애플리케이션과 데이터베이스 사이에서 ‘가상의 데이터베이스’ 역할을 하는 메모리 공간입니다.

⚙️ 핵심 기능 (엔티티 생명주기 관리)

  1. 1차 캐시 (First Level Cache):

    • findById("user1")를 호출하면, DB에 가기 전에 먼저 영속성 컨텍스트(Map<Id, Entity>)를 뒤집니다.

    • 이미 있으면 DB를 거치지 않고 반환합니다. (성능 향상)

  2. 변경 감지 (Dirty Checking):

    • 엔티티 조회 시점의 상태(스냅샷)를 저장해두고, 트랜잭션 커밋 시점에 현재 상태와 비교하여 변경된 부분만 UPDATE 쿼리를 날립니다.
  3. 쓰기 지연 (Transactional Write-Behind):

    • persist()delete()를 해도 즉시 DB에 쿼리를 날리지 않습니다.

    • 트랜잭션이 커밋(commit)되는 순간 모아둔 쿼리를 한 번에 보냅니다. (Batch Processing의 기반)

2. 코드 재사용과 모듈화 전략: 상속 vs 조합

doeatfit 프로젝트에는 모든 테이블에 공통적으로 들어가는 컬럼들이 있습니다. 바로 생성일시(created_at), 수정일시(updated_at), 그리고 UUID입니다. 이를 매번 모든 엔티티에 복사-붙여넣기 하는 것은 비효율적입니다.

JPA는 이를 해결하기 위해 두 가지 강력한 패턴을 제공합니다.

🅰️ @MappedSuperclass (상속 관계 매핑)

부모 클래스를 상속받는 자식 클래스에게 매핑 정보(컬럼)만 제공하고 싶을 때 사용합니다. DB 테이블 상으로는 상속 관계가 아니지만, 객체 입장에서 공통 필드를 묶을 때 유용합니다.

📝 코드 예시: TimeStampEntity.java

@Getter
@MappedSuperclass // "나는 진짜 테이블이 아니야. 자식들에게 컬럼만 물려줄게."
@EntityListeners(AuditingEntityListener.class)
public class TimeStampEntity {
    
    @CreatedDate
    @Column(updatable = false)
    private LocalDateTime createdAt;
 
    @LastModifiedDate
    private LocalDateTime editedAt;
}
  • 특징: UserEntity, NoticeEntity 등은 TimeStampEntityextends 하기만 하면 자동으로 created_at, edited_at 컬럼을 갖게 됩니다.

🅱️ @Embeddable & @Embedded (임베디드 타입)

새로운 값 타입을 직접 정의해서 사용하고 싶을 때 사용합니다. 주로 응집도 높은 데이터를 묶어서 관리할 때 쓰입니다. (예: 주소[시, 구, 동], 좌표[x, y])

프로젝트에서는 UUID 관리를 위해 이 패턴을 사용했습니다.

📝 코드 예시: UuidEntity.java (정의하는 곳)

@Getter
@Embeddable // "나는 다른 엔티티의 '일부분'으로 들어갈 수 있어."
public class UuidEntity {
    @UuidGenerator
    @Column(columnDefinition = "BINARY(16)", unique = true, updatable = false)
    private UUID uuid;
}

📝 코드 예시: UserEntity.java (사용하는 곳)

@Entity
public class UserEntity {
    // ...
    @Embedded // "여기에 UuidEntity의 필드들을 풀어서 넣어줘."
    private UuidEntity uuidEntity;
}
  • 특징:

    • DB 테이블에는 uuid라는 컬럼이 그대로 생성됩니다. (마치 UserEntity에 있는 것처럼)

    • 하지만 객체 관점에서는 user.getUuidEntity().getUuid()처럼 의미 단위로 묶어서 관리할 수 있습니다.

    • 객체 지향적인 설계를 가능하게 하여 코드의 가독성과 재사용성을 높여줍니다.

3. JPA Auditing: 지루한 반복 작업의 자동화

데이터의 생성 시간과 수정 시간은 운영 관점에서 매우 중요합니다. 하지만 모든 INSERT, UPDATE 로직마다 entity.setCreatedAt(now())를 코딩하는 것은 실수하기 딱 좋은 반복 작업입니다.

⚙️ 동작 원리

Spring Data JPA는 이벤트를 감지하여 자동으로 값을 채워주는 Auditing 기능을 제공합니다.

  1. 설정: @EnableJpaAuditing을 설정 클래스에 추가합니다. (JpaAuditingConfiguration.java)

  2. 리스너 등록: 엔티티(혹은 부모 클래스)에 @EntityListeners(AuditingEntityListener.class)를 붙입니다.

  3. 적용:

    • @CreatedDate: 엔티티가 persist 될 때 시간을 자동 저장.

    • @LastModifiedDate: 엔티티가 수정되어 DB에 반영될 때 시간을 자동 갱신.

이 기능을 통해 개발자는 비즈니스 로직에만 집중하고, 데이터 이력 관리는 프레임워크에게 안전하게 위임할 수 있습니다.