본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 12. 21. · 4 Views
데이터 영속성 JPA 완벽 가이드
자바 개발자라면 반드시 알아야 할 JPA의 핵심 개념부터 실무 활용법까지 담았습니다. 엔티티 설계부터 연관관계 매핑까지, 초급 개발자도 쉽게 이해할 수 있도록 친절하게 설명합니다.
목차
1. JPA와 Hibernate 개념
김개발 씨는 오늘도 회사에서 신규 프로젝트 투입을 앞두고 있습니다. 선배가 던진 한마디가 마음에 걸립니다.
"이번 프로젝트는 JPA 기반이니까, JDBC로 SQL 직접 짜던 버릇은 잠시 내려놓으세요."
**JPA(Java Persistence API)**는 자바 객체와 데이터베이스 테이블 사이의 다리 역할을 하는 표준 명세입니다. Hibernate는 이 JPA 표준을 실제로 구현한 가장 유명한 프레임워크입니다.
마치 USB라는 표준이 있고, 삼성이나 샌디스크가 그 표준에 맞는 제품을 만드는 것과 같습니다.
다음 코드를 살펴봅시다.
// JPA 설정 예시 (application.properties)
spring.datasource.url=jdbc:mysql://localhost:3306/mydb
spring.datasource.username=root
spring.datasource.password=1234
// Hibernate가 JPA 구현체로 동작합니다
spring.jpa.hibernate.ddl-auto=update
spring.jpa.show-sql=true
// 엔티티 클래스 - JPA가 관리하는 객체
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
}
김개발 씨는 입사 후 줄곧 JDBC를 사용해 데이터베이스를 다뤄왔습니다. Connection 객체를 얻고, PreparedStatement를 만들고, ResultSet으로 결과를 받아오는 일련의 과정이 이제는 손에 익었습니다.
그런데 이번 프로젝트에서는 JPA를 써야 한다니, 뭔가 새로운 세계가 열리는 느낌입니다. 선배 박시니어 씨가 커피 한 잔을 건네며 말합니다.
"JPA가 뭔지 알아요? 쉽게 말하면, SQL을 직접 안 짜도 되는 마법 같은 거예요." 그렇다면 JPA란 정확히 무엇일까요?
JPA는 Java Persistence API의 약자로, 자바 진영의 ORM(Object-Relational Mapping) 표준 명세입니다. 여기서 ORM이란 객체와 관계형 데이터베이스를 매핑해주는 기술을 말합니다.
쉽게 비유하자면, JPA는 마치 통역사와 같습니다. 자바 객체라는 언어와 데이터베이스 테이블이라는 언어 사이에서 서로의 말을 번역해주는 역할을 합니다.
그런데 JPA는 명세일 뿐, 실제로 동작하는 코드가 아닙니다. 마치 교통법규가 있지만 실제로 운전하는 건 자동차인 것처럼요.
여기서 Hibernate가 등장합니다. Hibernate는 JPA라는 표준을 실제로 구현한 프레임워크입니다.
스프링 부트에서 JPA를 사용하면 기본적으로 Hibernate가 동작합니다. JPA가 없던 시절에는 어땠을까요?
개발자들은 모든 SQL을 직접 작성해야 했습니다. 회원 정보를 저장하려면 INSERT 문을, 조회하려면 SELECT 문을, 수정하려면 UPDATE 문을 일일이 만들어야 했습니다.
테이블 컬럼이 하나 추가되면? 관련된 모든 SQL을 찾아 수정해야 했습니다.
야근의 주범이 바로 여기 있었습니다. 더 큰 문제는 패러다임 불일치였습니다.
자바는 객체지향 언어인데, 데이터베이스는 테이블 중심입니다. 객체의 상속, 연관관계, 데이터 타입 등을 테이블에 맞게 변환하는 작업이 매번 필요했습니다.
이 과정에서 실수도 잦았고, 코드도 복잡해졌습니다. JPA를 사용하면 이런 문제들이 해결됩니다.
개발자는 SQL 대신 자바 객체를 다루면 됩니다. 객체를 저장하고 싶으면 save() 메서드를, 조회하고 싶으면 find() 메서드를 호출하면 됩니다.
JPA가 알아서 적절한 SQL을 생성해서 데이터베이스에 전달합니다. 마치 자동번역기가 영어를 한국어로 바꿔주는 것처럼요.
위의 코드를 살펴보겠습니다. spring.jpa.hibernate.ddl-auto=update 설정은 엔티티 클래스를 기반으로 테이블을 자동으로 생성하거나 수정하라는 의미입니다.
Member 클래스에 @Entity 어노테이션을 붙이면, Hibernate가 이 클래스를 데이터베이스 테이블과 매핑합니다. 실무에서는 어떻게 활용할까요?
예를 들어 쇼핑몰에서 상품 정보를 관리한다고 가정해봅시다. 상품명, 가격, 재고 등의 정보를 담은 Product 클래스를 만들고 @Entity를 붙이면, JPA가 알아서 product 테이블을 관리해줍니다.
SQL을 직접 작성하는 수고로움에서 해방되는 것입니다. 다시 김개발 씨의 이야기로 돌아가 봅시다.
박시니어 씨의 설명을 들은 김개발 씨는 눈이 반짝입니다. "SQL을 안 짜도 된다니, 정말 편하겠네요!" JPA를 제대로 익히면 개발 생산성이 크게 향상됩니다.
실전 팁
💡 - JPA는 표준 명세이고, Hibernate는 구현체라는 점을 기억하세요
- 스프링 부트에서는 spring-boot-starter-data-jpa 의존성만 추가하면 바로 사용할 수 있습니다
2. 엔티티 클래스 설계
김개발 씨가 첫 번째 엔티티를 작성하려고 키보드에 손을 올렸습니다. 그런데 막상 시작하려니 막막합니다.
"어노테이션이 이렇게 많은데, 어떤 걸 어디에 붙여야 하죠?" 옆자리 박시니어 씨가 웃으며 다가옵니다.
**엔티티(Entity)**는 JPA가 관리하는 자바 객체로, 데이터베이스 테이블과 1대1로 매핑됩니다. 마치 엑셀 시트의 한 행이 자바 객체 하나가 되는 것과 같습니다.
@Entity, @Id, @Column 등의 어노테이션을 통해 테이블과의 매핑 정보를 지정합니다.
다음 코드를 살펴봅시다.
@Entity
@Table(name = "members") // 테이블명 지정
public class Member {
@Id // 기본키 지정
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(nullable = false, length = 50) // 컬럼 속성 지정
private String name;
@Column(unique = true)
private String email;
private LocalDateTime createdAt;
// 기본 생성자 필수
protected Member() {}
}
박시니어 씨가 화이트보드에 그림을 그리기 시작합니다. "엔티티를 이해하려면 먼저 매핑이라는 개념을 알아야 해요." 엔티티란 무엇일까요?
쉽게 말해, 데이터베이스 테이블을 자바 클래스로 표현한 것입니다. 마치 설계도와 실제 건물의 관계와 비슷합니다.
엔티티 클래스가 설계도라면, 데이터베이스 테이블은 그 설계도대로 지어진 건물입니다. JPA는 이 설계도를 보고 테이블을 만들고, 데이터를 저장하고 조회합니다.
@Entity 어노테이션은 "이 클래스는 JPA가 관리하는 엔티티입니다"라고 선언하는 것입니다. 이 어노테이션이 없으면 JPA는 해당 클래스를 무시합니다.
반드시 클래스 위에 붙여야 합니다. 모든 엔티티에는 **기본키(Primary Key)**가 필요합니다.
@Id 어노테이션이 바로 기본키를 지정하는 역할을 합니다. 데이터베이스에서 각 행을 구분하는 유일한 값이죠.
회원 테이블이라면 회원 번호, 상품 테이블이라면 상품 코드가 기본키가 됩니다. @GeneratedValue는 기본키 값을 자동으로 생성하라는 의미입니다.
IDENTITY 전략을 사용하면 데이터베이스가 알아서 1, 2, 3... 순서대로 값을 넣어줍니다.
MySQL의 AUTO_INCREMENT와 같은 역할입니다. @Column 어노테이션은 필드와 테이블 컬럼의 매핑 정보를 세부적으로 지정합니다.
nullable=false는 NOT NULL 제약조건을, length=50은 VARCHAR(50)을 의미합니다. 이 어노테이션을 생략하면 필드명이 그대로 컬럼명이 됩니다.
한 가지 중요한 규칙이 있습니다. 엔티티 클래스에는 반드시 기본 생성자가 있어야 합니다.
JPA가 객체를 생성할 때 리플렉션을 사용하기 때문입니다. 접근 제어자는 public 또는 protected여야 합니다.
위의 코드에서 Member 클래스를 살펴보겠습니다. @Table(name = "members")로 테이블명을 명시적으로 지정했습니다.
생략하면 클래스명이 그대로 테이블명이 됩니다. id 필드는 기본키로, name과 email은 일반 컬럼으로 매핑됩니다.
실무에서 흔히 하는 실수 중 하나는 final 클래스로 엔티티를 만드는 것입니다. JPA는 프록시 객체를 생성하기 위해 클래스를 상속해야 하는데, final 클래스는 상속이 불가능합니다.
같은 이유로 final 필드도 사용할 수 없습니다. 김개발 씨가 고개를 끄덕입니다.
"아, 그래서 Lombok의 @Data 대신 @Getter, @Setter를 따로 쓰라고 하셨군요!" 맞습니다. 엔티티 설계에는 나름의 규칙이 있고, 이를 지켜야 JPA가 제대로 동작합니다.
실전 팁
💡 - 엔티티 클래스에는 반드시 기본 생성자가 필요합니다
- @Id와 @GeneratedValue는 거의 항상 함께 사용합니다
3. JpaRepository 인터페이스
엔티티 클래스를 완성한 김개발 씨가 다음 단계로 넘어갑니다. "이제 데이터를 저장하고 조회하는 코드를 짜야 하는데..." 예전 같으면 DAO 클래스를 만들고 EntityManager를 주입받아 복잡한 코드를 작성했을 겁니다.
하지만 스프링 데이터 JPA에는 더 쉬운 방법이 있습니다.
JpaRepository는 스프링 데이터 JPA가 제공하는 인터페이스로, 기본적인 CRUD 메서드를 자동으로 제공합니다. 마치 은행 ATM기처럼, 복잡한 내부 동작은 숨기고 간단한 버튼만 누르면 원하는 작업이 처리됩니다.
인터페이스만 정의하면 스프링이 구현체를 자동으로 생성합니다.
다음 코드를 살펴봅시다.
// Repository 인터페이스 - 구현체는 스프링이 자동 생성
public interface MemberRepository extends JpaRepository<Member, Long> {
// Member: 엔티티 타입
// Long: 기본키(Id) 타입
// 기본 제공 메서드들 (별도 선언 불필요)
// save(), findById(), findAll(), delete(), count() 등
}
// 서비스에서 사용하기
@Service
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
public Member saveMember(Member member) {
return memberRepository.save(member); // INSERT 또는 UPDATE
}
}
박시니어 씨가 묻습니다. "혹시 예전에 DAO 패턴 써본 적 있어요?" 김개발 씨가 고개를 끄덕입니다.
예전 프로젝트에서 MemberDao 클래스를 만들고, 모든 CRUD 메서드를 직접 구현했던 기억이 납니다. save 메서드, findById 메서드, findAll 메서드...
거의 모든 테이블마다 비슷한 코드를 반복해서 작성했습니다. "스프링 데이터 JPA를 쓰면 그런 반복 작업이 사라져요." JpaRepository는 스프링 데이터 JPA의 핵심 인터페이스입니다.
이 인터페이스를 상속받기만 하면, 기본적인 CRUD 기능이 자동으로 제공됩니다. 마법처럼 느껴지지만, 사실 스프링이 런타임에 구현 클래스를 동적으로 생성해주는 것입니다.
인터페이스 선언 방식을 자세히 살펴보겠습니다. **JpaRepository<Member, Long>**에서 첫 번째 타입 파라미터는 엔티티 클래스, 두 번째는 기본키의 타입입니다.
Member 엔티티의 기본키가 Long 타입이므로 Long을 지정했습니다. 이렇게 선언만 하면 어떤 메서드들을 사용할 수 있을까요?
**save()**는 엔티티를 저장합니다. 새로운 엔티티면 INSERT, 이미 존재하는 엔티티면 UPDATE를 수행합니다.
**findById()**는 기본키로 엔티티를 조회합니다. **findAll()**은 모든 엔티티를 리스트로 반환합니다.
**delete()**는 엔티티를 삭제하고, **count()**는 전체 개수를 세어줍니다. 여기서 중요한 점은 이 모든 메서드를 개발자가 직접 구현할 필요가 없다는 것입니다.
인터페이스에 아무 코드도 작성하지 않았는데 모든 기능이 동작합니다. 스프링 컨테이너가 시작될 때 JpaRepository를 상속받은 인터페이스를 찾아 프록시 객체를 만들어 빈으로 등록합니다.
서비스 클래스에서는 이 Repository를 주입받아 사용하면 됩니다. @RequiredArgsConstructor와 final 필드를 조합하면 생성자 주입이 자동으로 이루어집니다.
memberRepository.save(member) 한 줄로 데이터베이스에 회원 정보가 저장됩니다. "정말 코드가 간결해지네요!" 김개발 씨의 눈이 반짝입니다.
예전에는 수십 줄이 필요했던 작업이 이제는 한 줄로 끝납니다. 이것이 바로 스프링 데이터 JPA의 강력함입니다.
실전 팁
💡 - JpaRepository는 CrudRepository와 PagingAndSortingRepository를 상속받아 더 많은 기능을 제공합니다
- 인터페이스 위에 @Repository 어노테이션은 생략해도 됩니다
4. CRUD 메서드 사용
이론은 충분히 배웠으니 이제 실전입니다. 김개발 씨가 회원 관리 기능을 구현하려고 합니다.
"저장, 조회, 수정, 삭제... 각각 어떻게 하면 되나요?" 박시니어 씨가 에디터를 열며 답합니다.
"직접 해보면서 익히는 게 최고예요."
JpaRepository가 제공하는 CRUD 메서드를 사용하면 데이터베이스 조작이 매우 간단해집니다. **save()**로 저장과 수정을, **findById()**로 단건 조회를, **findAll()**로 전체 조회를, **deleteById()**로 삭제를 수행합니다.
각 메서드는 내부적으로 적절한 SQL을 생성하여 실행합니다.
다음 코드를 살펴봅시다.
@Service
@Transactional(readOnly = true)
@RequiredArgsConstructor
public class MemberService {
private final MemberRepository memberRepository;
// 저장 (Create)
@Transactional
public Member createMember(String name, String email) {
Member member = new Member(name, email);
return memberRepository.save(member);
}
// 조회 (Read)
public Member getMember(Long id) {
return memberRepository.findById(id)
.orElseThrow(() -> new IllegalArgumentException("회원 없음"));
}
// 수정 (Update) - 변경 감지 활용
@Transactional
public void updateName(Long id, String newName) {
Member member = getMember(id);
member.setName(newName); // 트랜잭션 종료 시 자동 UPDATE
}
// 삭제 (Delete)
@Transactional
public void deleteMember(Long id) {
memberRepository.deleteById(id);
}
}
박시니어 씨가 말합니다. "CRUD는 모든 애플리케이션의 기본이에요.
이것만 잘 알아도 80%는 해결돼요." 먼저 **Create(생성)**부터 살펴보겠습니다. 새로운 회원을 저장하려면 Member 객체를 생성하고 save() 메서드에 전달하면 됩니다.
save() 메서드는 전달받은 엔티티의 상태를 확인합니다. 기본키 값이 null이면 새로운 엔티티로 판단하여 INSERT 쿼리를 실행합니다.
**Read(조회)**는 두 가지 방식이 있습니다. **findById()**는 기본키로 단건을 조회합니다.
반환 타입이 Optional이라는 점에 주목하세요. 해당 ID의 엔티티가 없을 수도 있기 때문입니다.
orElseThrow()를 사용하면 없을 때 예외를 던질 수 있습니다. **findAll()**은 테이블의 모든 데이터를 List로 반환합니다.
**Update(수정)**가 가장 흥미로운 부분입니다. JPA에는 update() 메서드가 없습니다.
대신 변경 감지(Dirty Checking) 기능을 사용합니다. 트랜잭션 내에서 엔티티를 조회하고, 필드 값을 변경하면, 트랜잭션이 커밋되는 시점에 JPA가 변경된 부분을 감지하여 자동으로 UPDATE 쿼리를 실행합니다.
마치 은행 계좌와 같습니다. 계좌에서 돈을 인출하면 잔고가 자동으로 바뀌는 것처럼, 엔티티의 상태를 변경하면 데이터베이스에도 자동으로 반영됩니다.
이것이 JPA를 사용하면 SQL을 직접 작성하지 않아도 되는 핵심 이유입니다. **Delete(삭제)**는 deleteById() 또는 delete() 메서드를 사용합니다.
deleteById()는 기본키로 직접 삭제하고, delete()는 엔티티 객체를 전달받아 삭제합니다. 여기서 중요한 것이 @Transactional 어노테이션입니다.
데이터를 변경하는 메서드에는 반드시 @Transactional을 붙여야 합니다. 이 어노테이션이 없으면 변경 감지가 동작하지 않습니다.
클래스 레벨에 @Transactional(readOnly = true)를 붙이고, 변경이 필요한 메서드에만 @Transactional을 붙이는 것이 좋은 패턴입니다. 김개발 씨가 코드를 따라 치며 말합니다.
"update 메서드 없이도 수정이 된다니, 신기하네요!" 변경 감지는 JPA의 가장 강력한 기능 중 하나입니다. 이 개념을 잘 이해하면 훨씬 깔끔한 코드를 작성할 수 있습니다.
실전 팁
💡 - 조회만 하는 메서드에는 @Transactional(readOnly = true)를 붙이면 성능이 향상됩니다
- findById()의 반환 타입이 Optional이므로 null 처리를 잊지 마세요
5. 쿼리 메서드 작성
기본 CRUD는 마스터했습니다. 그런데 김개발 씨에게 새로운 요구사항이 들어왔습니다.
"이메일로 회원을 찾아야 하는데, findByEmail 같은 메서드가 없네요?" 박시니어 씨가 미소를 짓습니다. "없으면 만들면 되죠.
아주 쉽게요."
쿼리 메서드는 메서드 이름만으로 쿼리를 자동 생성하는 스프링 데이터 JPA의 강력한 기능입니다. findBy, existsBy, countBy 등의 키워드와 필드명을 조합하면 원하는 쿼리가 만들어집니다.
마치 영어 문장을 조립하듯 메서드 이름을 작성하면 됩니다.
다음 코드를 살펴봅시다.
public interface MemberRepository extends JpaRepository<Member, Long> {
// 이메일로 회원 조회
Optional<Member> findByEmail(String email);
// 이름에 특정 문자열 포함 + 정렬
List<Member> findByNameContainingOrderByCreatedAtDesc(String name);
// 이메일 존재 여부 확인
boolean existsByEmail(String email);
// 특정 나이 이상인 회원 수
long countByAgeGreaterThanEqual(int age);
// 이름으로 삭제
void deleteByName(String name);
// 여러 조건 조합 (AND)
List<Member> findByNameAndEmail(String name, String email);
// 여러 조건 조합 (OR)
List<Member> findByNameOrEmail(String name, String email);
}
박시니어 씨가 설명을 시작합니다. "스프링 데이터 JPA의 꽃이라고 할 수 있어요.
이름 규칙만 지키면 알아서 쿼리가 만들어지거든요." 쿼리 메서드란 메서드 이름을 분석하여 자동으로 쿼리를 생성하는 기능입니다. 마치 음성 인식 비서에게 "이메일이 abc@test.com인 회원 찾아줘"라고 말하면 알아서 검색해주는 것과 같습니다.
메서드 이름이 곧 쿼리가 됩니다. 기본 패턴은 find + By + 필드명입니다.
findByEmail은 "email 필드로 찾아라"라는 의미입니다. findByName은 "name 필드로 찾아라"입니다.
이렇게 간단합니다. 하나의 조건만으로 부족할 때는 And와 Or로 연결합니다.
findByNameAndEmail은 이름과 이메일이 모두 일치하는 경우, findByNameOrEmail은 둘 중 하나만 일치해도 조회합니다. 비교 연산도 가능합니다.
GreaterThan은 초과, GreaterThanEqual은 이상, LessThan은 미만, LessThanEqual은 이하를 의미합니다. countByAgeGreaterThanEqual(20)은 나이가 20 이상인 회원 수를 셉니다.
문자열 검색에는 Containing, StartingWith, EndingWith를 사용합니다. findByNameContaining("김")은 이름에 "김"이 포함된 모든 회원을 찾습니다.
내부적으로 LIKE '%김%' 쿼리가 생성됩니다. 정렬도 메서드 이름에 포함할 수 있습니다.
OrderBy + 필드명 + Asc/Desc를 붙이면 됩니다. findByNameContainingOrderByCreatedAtDesc는 이름 검색 결과를 생성일 기준 내림차순으로 정렬합니다.
existsBy는 해당 조건의 데이터가 존재하는지 boolean으로 반환합니다. 회원가입 시 이메일 중복 체크에 유용합니다.
countBy는 조건에 맞는 데이터 개수를 셉니다. deleteBy는 조건에 맞는 데이터를 삭제합니다.
반환 타입도 다양하게 지정할 수 있습니다. 단건 조회는 엔티티 타입 또는 Optional로, 다건 조회는 List나 Collection으로 받습니다.
결과가 없을 수 있는 단건 조회에는 Optional 사용을 권장합니다. 김개발 씨가 감탄합니다.
"SQL을 한 줄도 안 썼는데 이렇게 다양한 조회가 가능하다니!" 쿼리 메서드만 잘 활용해도 대부분의 조회 요구사항을 해결할 수 있습니다.
실전 팁
💡 - 메서드 이름이 너무 길어지면 @Query 어노테이션으로 직접 JPQL을 작성하는 것도 방법입니다
- IDE의 자동완성 기능을 활용하면 사용 가능한 키워드를 쉽게 확인할 수 있습니다
6. 연관관계 매핑 기초
김개발 씨가 새로운 도전에 직면했습니다. "회원이 여러 개의 주문을 가질 수 있는데, 이걸 어떻게 표현하죠?" 데이터베이스에서는 외래키로 테이블을 연결하지만, 객체 세계에서는 다르게 접근해야 합니다.
박시니어 씨가 화이트보드에 그림을 그리기 시작합니다.
연관관계 매핑은 엔티티 사이의 관계를 설정하는 것입니다. 데이터베이스의 외래키 관계를 객체의 참조로 표현합니다.
@ManyToOne은 다대일, @OneToMany는 일대다 관계를 나타냅니다. 연관관계의 주인은 외래키를 관리하는 쪽이며, 보통 다(N) 쪽이 주인이 됩니다.
다음 코드를 살펴봅시다.
// 주문(다) -> 회원(일) : ManyToOne
@Entity
public class Order {
@Id @GeneratedValue
private Long id;
@ManyToOne(fetch = FetchType.LAZY) // 지연 로딩 권장
@JoinColumn(name = "member_id") // 외래키 컬럼명
private Member member;
private LocalDateTime orderDate;
}
// 회원(일) -> 주문(다) : OneToMany (양방향 설정 시)
@Entity
public class Member {
@Id @GeneratedValue
private Long id;
private String name;
@OneToMany(mappedBy = "member") // 연관관계 주인 지정
private List<Order> orders = new ArrayList<>();
}
박시니어 씨가 묻습니다. "쇼핑몰에서 회원과 주문의 관계는 뭘까요?" 김개발 씨가 대답합니다.
"한 회원이 여러 주문을 할 수 있으니까... 일대다 관계요?" 정답입니다.
회원 입장에서 보면 일대다(One to Many), 주문 입장에서 보면 다대일(Many to One) 관계입니다. 데이터베이스에서는 이런 관계를 외래키로 표현합니다.
orders 테이블에 member_id라는 컬럼을 두고, members 테이블의 id를 참조합니다. 그런데 객체 세계에서는 외래키 대신 참조를 사용합니다.
Order 객체가 Member 객체를 직접 참조하는 것이죠. @ManyToOne은 "나는 여럿 중 하나이고, 상대방은 하나다"라는 의미입니다.
Order 입장에서 보면 여러 주문이 한 회원에게 속합니다. 그래서 Order 클래스에 @ManyToOne을 붙입니다.
@JoinColumn은 외래키 컬럼명을 지정합니다. @JoinColumn(name = "member_id")라고 하면 orders 테이블에 member_id라는 외래키 컬럼이 생성됩니다.
반대로 Member에서 Order를 참조하고 싶다면 어떻게 할까요? @OneToMany를 사용합니다.
"나는 하나이고, 상대방은 여럿이다"라는 의미입니다. 이렇게 양쪽에서 서로를 참조하는 것을 양방향 연관관계라고 합니다.
양방향 관계에서 중요한 개념이 연관관계의 주인입니다. 주인은 외래키를 실제로 관리하는 쪽입니다.
주인이 아닌 쪽은 mappedBy로 주인을 지정합니다. @OneToMany(mappedBy = "member")는 "Order의 member 필드가 주인"이라는 뜻입니다.
왜 주인을 정해야 할까요? 양쪽에서 모두 관계를 수정할 수 있다면 혼란이 생깁니다.
Order.member를 바꿨는데 Member.orders도 같이 바뀌어야 하나요? 누가 진짜 주인인가요?
이런 혼란을 막기 위해 한쪽을 주인으로 정합니다. 주인만이 외래키를 등록, 수정, 삭제할 수 있고, 주인이 아닌 쪽은 읽기만 가능합니다.
fetch = FetchType.LAZY는 매우 중요합니다. 지연 로딩이라고 하는데, 연관된 엔티티를 실제로 사용할 때까지 조회를 미룹니다.
즉시 로딩(EAGER)을 사용하면 불필요한 쿼리가 발생하여 성능 문제가 생길 수 있습니다. 실무에서는 거의 항상 LAZY를 사용합니다.
김개발 씨가 정리합니다. "외래키가 있는 쪽이 주인이고, 항상 LAZY 로딩을 쓰면 되는 거군요!" 연관관계 매핑은 JPA에서 가장 어려운 부분 중 하나입니다.
하지만 이 기본 원칙만 기억하면 복잡한 관계도 설계할 수 있습니다.
실전 팁
💡 - 가능하면 단방향 연관관계로 시작하고, 필요할 때만 양방향으로 확장하세요
- @ManyToOne은 기본이 EAGER이므로 반드시 LAZY로 변경하세요
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
관찰 가능한 마이크로서비스 완벽 가이드
마이크로서비스 환경에서 시스템의 상태를 실시간으로 관찰하고 모니터링하는 방법을 배웁니다. Resilience4j, Zipkin, Prometheus, Grafana, EFK 스택을 활용하여 안정적이고 관찰 가능한 시스템을 구축하는 실전 가이드입니다.
Prometheus 메트릭 수집 완벽 가이드
Spring Boot 애플리케이션의 메트릭을 Prometheus로 수집하고 모니터링하는 방법을 배웁니다. Actuator 설정부터 PromQL 쿼리까지 실무에 필요한 모든 내용을 다룹니다.
스프링 관찰 가능성 완벽 가이드
Spring Boot 3.x의 Observation API를 활용한 애플리케이션 모니터링과 추적 방법을 초급 개발자 눈높이에서 쉽게 설명합니다. 실무에서 바로 적용할 수 있는 메트릭 수집과 분산 추적 기법을 다룹니다.
Zipkin으로 추적 시각화 완벽 가이드
마이크로서비스 환경에서 분산 추적을 시각화하는 Zipkin의 핵심 개념과 활용 방법을 초급자도 쉽게 이해할 수 있도록 실무 스토리로 풀어낸 가이드입니다. Docker 실행부터 UI 분석까지 단계별로 배웁니다.
Micrometer Tracing 완벽 가이드
분산 시스템에서 요청 흐름을 추적하는 Micrometer Tracing의 핵심 개념과 실전 활용법을 초급 개발자도 쉽게 이해할 수 있도록 실무 스토리와 비유로 풀어낸 완벽 가이드입니다.