EntityManagerFactory는 애플리케이션 전체에서 하나만 생성해서 공유하지만, EntityManager`는 스레드 간 공유하면 안 된다. 사용 후에는 반드시 닫아야 한다.
또한 JPA의 모든 데이터 변경 작업은 트랜잭션 안에서 실행되어야 한다. 이는 데이터 무결성을 보장하기 위한 필수 규칙이다.
JPA를 이용한 기본적인 CRUD 연산은 생각보다 단순하다.
// 저장
Member member = new Member();
member.setId(1L);
member.setName("김개발");
em.persist(member);
// 조회
Member findMember = em.find(Member.class, 1L);
// 수정 - 별도의 update 메소드 없이 객체 값만 변경
findMember.setName("박개발");
// 삭제
em.remove(findMember);
수정 연산이 특히 인상적이었다. 별도의 update() 메소드 호출 없이 객체의 값만 변경하면 JPA가 자동으로 변경을 감지해서 UPDATE SQL을 생성한다.
단순한 조회는 find() 메소드로 충분하지만, 복잡한 조건의 검색에는 JPQL이 필요하다. JPQL의 핵심은 테이블이 아닌 엔티티 객체를 대상으로 쿼리를 작성한다는 점이다.
List<Member> result = em.createQuery(
"SELECT m FROM Member m WHERE m.name LIKE '%김%'",
Member.class
).getResultList();
SQL과 문법이 유사하지만, 테이블명 대신 엔티티 클래스명을, 컬럼명 대신 필드명을 사용한다. 이것이 바로 객체지향 쿼리의 핵심이다.
영속성 컨텍스트는 JPA의 핵심 개념으로, 엔티티를 영구 저장하는 환경을 의미한다. 엔티티 매니저를 통해 엔티티를 저장하거나 조회하면 엔티티 매니저는 영속성 컨텍스트에 엔티티를 보관하고 관리한다.
// 엔티티 매니저 팩토리 생성 (애플리케이션 전체에서 하나)
EntityManagerFactory emf = Persistence.createEntityManagerFactory("jpabook");
// 엔티티 매니저 생성 (요청마다 생성, 스레드 간 공유 금지)
EntityManager em = emf.createEntityManager();
EntityTransaction tx = em.getTransaction();
try {
tx.begin(); // 트랜잭션 시작
// 비즈니스 로직 수행
tx.commit(); // 트랜잭션 커밋
} catch (Exception e) {
tx.rollback(); // 트랜잭션 롤백
} finally {
em.close(); // 엔티티 매니저 종료
}
emf.close(); // 엔티티 매니저 팩토리 종료
엔티티는 4가지 상태를 가진다:
영속성 컨텍스트와 전혀 관계가 없는 새로운 상태
// 객체를 생성한 상태 (비영속)
Member member = new Member();
member.setId(1L);
member.setName("김개발");
영속성 컨텍스트에 관리되는 상태
// 객체를 저장한 상태 (영속)
em.persist(member);
// 또는 조회한 상태도 영속 상태
Member findMember = em.find(Member.class, 1L);
영속성 컨텍스트에 저장되었다가 분리된 상태
// 엔티티를 영속성 컨텍스트에서 분리 (준영속)
em.detach(member);
// 영속성 컨텍스트를 완전히 초기화
em.clear();
// 영속성 컨텍스트를 종료
em.close();
삭제된 상태
// 엔티티를 영속성 컨텍스트와 데이터베이스에서 삭제
em.remove(member);
영속성 컨텍스트는 내부에 1차 캐시를 가지고 있어, 동일한 트랜잭션 내에서 같은 엔티티를 조회할 때 데이터베이스를 거치지 않고 1차 캐시에서 조회한다.
Member member = new Member();
member.setId(1L);
member.setName("김개발");
// 1차 캐시에 저장됨
em.persist(member);
// 1차 캐시에서 조회 (SELECT SQL이 실행되지 않음)
Member findMember1 = em.find(Member.class, 1L);
Member findMember2 = em.find(Member.class, 1L);
영속성 컨텍스트는 성능상 이점과 엔티티의 동일성을 보장한다.
Member a = em.find(Member.class, 1L);
Member b = em.find(Member.class, 1L);
System.out.println(a == b); // 동일성 비교 true
엔티티 매니저는 트랜잭션을 커밋하기 직전까지 데이터베이스에 엔티티를 저장하지 않고 내부 쿼리 저장소에 INSERT SQL을 모아둔다.
transaction.begin(); // 트랜잭션 시작
em.persist(memberA);
em.persist(memberB);
// 여기까지 INSERT SQL을 데이터베이스에 보내지 않는다.
// 커밋하는 순간 데이터베이스에 INSERT SQL을 모아서 보낸다.
transaction.commit(); // 트랜잭션 커밋
JPA는 엔티티의 변경사항을 자동으로 감지하여 UPDATE SQL을 생성한다.
transaction.begin(); // 트랜잭션 시작
// 영속 엔티티 조회
Member memberA = em.find(Member.class, 1L);
// 영속 엔티티 데이터 수정
memberA.setName("박개발");
memberA.setAge(30);
// em.update(member) 이런 코드가 있어야 하지 않을까?
transaction.commit(); // 트랜잭션 커밋 - 자동으로 UPDATE SQL 실행
변경 감지 동작 원리:
1. 트랜잭션을 커밋하면 엔티티 매니저 내부에서 먼저 flush() 호출
2. 엔티티와 스냅샷을 비교
3. 변경된 엔티티를 찾음
4. 수정된 엔티티에 대해 UPDATE SQL 생성
5. UPDATE SQL을 쓰기 지연 SQL 저장소에 보냄
6. 쓰기 지연 저장소의 SQL을 데이터베이스에 전송
7. 데이터베이스 트랜잭션을 커밋
실제 객체 대신 프록시 객체를 로딩해두고, 해당 객체를 실제 사용할 때 로딩한다.
플러시는 영속성 컨텍스트의 변경내용을 데이터베이스에 반영한다. 플러시가 실행되면:
플러시하는 방법:
// 직접 호출
em.flush();
// 트랜잭션 커밋 시 자동 호출
transaction.commit();
// JPQL 쿼리 실행 시 자동 호출
em.createQuery("SELECT m FROM Member m", Member.class).getResultList();
플러시 모드 옵션:
// 커밋이나 쿼리를 실행할 때 플러시 (기본값)
em.setFlushMode(FlushModeType.AUTO);
// 커밋할 때만 플러시
em.setFlushMode(FlushModeType.COMMIT);
중요한 점:
영속 상태의 엔티티가 영속성 컨텍스트에서 분리된 것을 준영속 상태라 한다. 준영속 상태에서는 영속성 컨텍스트가 제공하는 기능을 사용할 수 없다.
준영속 상태로 만드는 방법:
// 특정 엔티티만 준영속 상태로 전환
em.detach(entity);
// 영속성 컨텍스트를 완전히 초기화
em.clear();
// 영속성 컨텍스트를 종료
em.close();
준영속 상태의 특징:
엔티티 매핑은 객체와 관계형 데이터베이스의 테이블을 연결하는 핵심 기술이다. JPA에서 엔티티 매핑은 크게 4가지 영역으로 나뉜다.
@Entity, @Table@Column@Id@ManyToOne, @JoinColumn@Entity 어노테이션은 JPA의 핵심으로, 이 클래스가 데이터베이스 테이블과 매핑될 엔티티임을 선언한다.
@Entity
public class Member {
@Id
private Long id;
private String name;
// 기본 생성자 필수
public Member() {}
// Getter, Setter...
}
@Entity(name = "Member") // 엔티티 이름 지정 (기본값: 클래스명)
public class Member {
// ...
}
@Entity
@Table(name = "MEMBER_TBL",
catalog = "USER_DB",
schema = "USER_SCHEMA",
uniqueConstraints = {
@UniqueConstraint(name = "NAME_AGE_UNIQUE",
columnNames = {"NAME", "AGE"})
})
public class Member {
// ...
}
개발 단계에서 JPA는 엔티티 정보를 바탕으로 데이터베이스 테이블을 자동으로 생성할 수 있다.
# 개발 초기
hibernate.hbm2ddl.auto=create
# 개발 중기
hibernate.hbm2ddl.auto=update
# 운영 환경
hibernate.hbm2ddl.auto=validate
운영 환경에서는 절대 create, create-drop, update 사용 금지!
@Entity
public class Member {
@Id
private Long id;
@Column(name = "member_name",
nullable = false,
length = 50)
private String username;
@Column(precision = 10, scale = 2)
private BigDecimal salary;
@Column(columnDefinition = "varchar(100) default 'EMPTY'")
private String description;
}
public enum RoleType {
USER, ADMIN
}
@Entity
public class Member {
@Enumerated(EnumType.STRING) // 반드시 STRING 사용!
private RoleType roleType;
}
주의: ORDINAL은 절대 사용하지 말 것! Enum 순서가 바뀌면 데이터가 꼬인다.
@Entity
public class Member {
@Temporal(TemporalType.DATE)
private Date createdDate; // 날짜만
@Temporal(TemporalType.TIME)
private Date createdTime; // 시간만
@Temporal(TemporalType.TIMESTAMP)
private Date createdTimestamp; // 날짜와 시간
// Java 8 이후는 어노테이션 생략 가능
private LocalDate localDate;
private LocalDateTime localDateTime;
}
@Entity
public class Member {
@Lob
private String longText; // CLOB
@Lob
private byte[] binaryData; // BLOB
}
@Entity
public class Member {
@Transient
private String temp; // 데이터베이스에 저장되지 않음
}
@Entity
public class Member {
@Id
private Long id; // 개발자가 직접 할당
}
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // MySQL AUTO_INCREMENT
}
특징: persist() 호출 즉시 INSERT SQL 실행
@Entity
@SequenceGenerator(
name = "MEMBER_SEQ_GENERATOR",
sequenceName = "MEMBER_SEQ",
initialValue = 1,
allocationSize = 50) // 성능 최적화
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.SEQUENCE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;
}
@Entity
@TableGenerator(
name = "MEMBER_SEQ_GENERATOR",
table = "MY_SEQUENCES",
pkColumnValue = "MEMBER_SEQ")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.TABLE,
generator = "MEMBER_SEQ_GENERATOR")
private Long id;
}
Long형 + 대체키 + 키 생성 전략 조합 사용을 권장한다.
@Entity
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id; // 권장하는 방식
private String businessKey; // 비즈니스 식별자는 별도 필드로
}
@Entity
@Table(name = "MEMBER")
public class Member {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "name", nullable = false, length = 10)
private String username;
private Integer age;
@Enumerated(EnumType.STRING)
private RoleType roleType;
@Temporal(TemporalType.TIMESTAMP)
private Date createdDate;
@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;
@Lob
private String description;
// 기본 생성자
public Member() {}
// Getter, Setter...
}
enum RoleType {
USER, ADMIN
}
이렇게 엔티티 매핑을 통해 객체지향적인 도메인 모델과 관계형 데이터베이스를 효과적으로 연결할 수 있다.
객체지향 프로그래밍과 관계형 데이터베이스는 연관관계를 맺는 방식이 근본적으로 다르다.
테이블의 연관관계
SELECT *
FROM MEMBER M
JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID
-- 또는 반대로도 가능
SELECT *
FROM TEAM T
JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID
객체의 연관관계
회원과 팀의 관계를 생각해보자. 회원은 하나의 팀에만 소속될 수 있다(다대일 관계).
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID")
private Team team;
// 연관관계 편의 메소드
public void changeTeam(Team team) {
this.team = team;
}
}
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
// Getter, Setter...
}
핵심 어노테이션
@ManyToOne: 다대일 관계 매핑@JoinColumn(name = "TEAM_ID"): 외래키 매핑, 생략 시 '필드명_참조테이블기본키' 형식조회와 저장
// 팀 저장
Team team = new Team();
team.setName("팀A");
em.persist(team);
// 회원 저장
Member member = new Member();
member.setUsername("김개발");
member.changeTeam(team); // 연관관계 설정
em.persist(member);
// 조회 - 객체 그래프 탐색
Member findMember = em.find(Member.class, member.getId());
Team findTeam = findMember.getTeam();
System.out.println("팀 이름: " + findTeam.getName());
양방향 연관관계는 단방향 연관관계가 2개 있는 것이다. 객체의 양방향 관계는 사실 양방향 관계가 아니라 서로 다른 단방향 관계 2개다.
@Entity
public class Member {
@Id
@GeneratedValue
@Column(name = "MEMBER_ID")
private Long id;
@Column(name = "USERNAME")
private String username;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "TEAM_ID") // 연관관계의 주인
private Team team;
public void changeTeam(Team team) {
this.team = team;
}
}
@Entity
public class Team {
@Id
@GeneratedValue
@Column(name = "TEAM_ID")
private Long id;
private String name;
@OneToMany(mappedBy = "team") // mappedBy로 연관관계의 주인이 아님을 표시
private List<Member> members = new ArrayList<>();
// Getter, Setter...
}
양방향 매핑 규칙:
mappedBy 속성 사용 안함mappedBy 속성으로 주인 지정누구를 주인으로?
// 잘못된 예 - 연관관계의 주인이 아닌 곳에만 값 설정
Team team = new Team();
team.setName("팀A");
em.persist(team);
Member member = new Member();
member.setUsername("김개발");
em.persist(member);
team.getMembers().add(member); // 역방향(주인 아님)만 연관관계 설정
// 결과: MEMBER 테이블의 TEAM_ID가 null
// 올바른 예 - 연관관계의 주인에 값 설정
Member member = new Member();
member.setUsername("김개발");
member.changeTeam(team); // 연관관계의 주인에 값 설정
em.persist(member);
@Entity
public class Member {
// ...
public void changeTeam(Team team) {
// 기존 팀과의 관계 제거
if (this.team != null) {
this.team.getMembers().remove(this);
}
this.team = team;
// 새로운 팀에 멤버 추가
if (team != null) {
team.getMembers().add(this);
}
}
}
또는 팀에서도 편의 메소드 제공:
@Entity
public class Team {
// ...
public void addMember(Member member) {
member.changeTeam(this);
this.members.add(member);
}
}
toString(), lombok, JSON 생성 라이브러리에서 무한 루프 조심:
// 위험한 코드
@Entity
public class Member {
// ...
@Override
public String toString() {
return "Member{" +
"id=" + id +
", username='" + username + '\'' +
", team=" + team + // team.toString()도 member를 호출하면 무한루프
'}';
}
}
해결책: toString()에서는 연관관계 필드 제외하거나, 한쪽만 출력
양방향 매핑 정리
JPA는 단순히 SQL을 대체하는 도구가 아니라, 객체지향과 관계형 데이터베이스 사이의 패러다임 불일치를 해결하는 브릿지 역할을 한다는 점이다.
영속성 컨텍스트의 1차 캐시, 변경 감지, 지연 로딩 등의 기능들은 모두 개발자가 객체지향적 사고에 집중할 수 있도록 도와준다. 더 이상 SQL과 객체 사이의 변환 작업에 시간을 쏟지 않고, 비즈니스 로직 구현에 더 많은 에너지를 투자할 수 있게 되었다.