Note-Taking / / 2024. 9. 11. 17:23

JPA 연관관계와 지연 로딩 정리

JPA에서 연관관계는 쉽게 말해 하나의 엔티티가 다른 엔티티와 어떻게 연결되어 있는지를 의미해. 객체 지향 프로그래밍에서는 클래스들 간에 서로 참조를 통해 관계를 맺고 데이터를 주고받을 수 있는데, 이런 관계를 데이터베이스에서는 **테이블 간의 외래 키(Foreign Key)**를 통해 표현해.

 

즉, 연관관계란 두 엔티티 간의 관계를 정의하고, 이를 데이터베이스 상에서 테이블 간의 연결로 표현하는 것을 말해. JPA를 사용하면, 이러한 관계를 객체 간의 참조처럼 쉽게 사용할 수 있게 만들어줘.

 

쉽게 정리하면:

 

연관관계 = 엔티티 간의 관계.

JPA에서는 객체 간의 관계(참조)를 데이터베이스의 외래 키로 관리해, 마치 객체 간의 연결을 처리하는 것처럼 데이터베이스 상에서도 쉽게 다룰 수 있어.

 

예를 들어, **회원(Member)**과 **주문(Order)**의 관계에서 회원이 여러 주문을 할 수 있다면, 회원과 주문 간의 1:N 관계가 성립돼. JPA를 사용하면 이러한 관계를 설정하고 관리하는 것이 훨씬 쉬워지는 거야.

 

Tip💡

일반적으로 다대일(N:1) 관계에서 외래 키“다”(N)쪽에 위치해. 이 방식은 여러 개의 엔티티가 하나의 엔티티를 참조하는 구조에서, 각각의 엔티티가 참조하는 하나의 엔티티를 연결하기 위해 외래 키를 가진다는 뜻이야.

 

예시:

 

**회원(Member)**과 주문(Order) 관계에서 N:1 관계를 생각해보면, 여러 개의 주문이 하나의 회원을 참조하는 상황이야.

이 경우 주문(Order) 테이블에 외래 키 member_id가 있어, 각 주문이 어느 회원에게 속해 있는지 알 수 있게 돼.

 

결론:

 

다대일 관계에서 외래 키는 보통 다수(N) 쪽에 위치하며, 이를 통해 하나의(1) 엔티티를 참조할 수 있게 돼.

 

@ManyToOne은 보통 @JoinColumn과 함께 사용돼. @ManyToOne은 여러 엔티티가 하나의 엔티티를 참조하는 다대일(N:1) 관계를 나타내는 어노테이션이야. 그리고 @JoinColumn은 그 관계를 설정할 때, 어느 칼럼이 외래 키로 사용되는지 명시할 때 사용해.

 

역할:

 

@ManyToOne: 다대일 관계를 설정해줌. 예를 들어, 여러 주문이 하나의 회원을 참조하는 구조를 정의할 때 사용돼.

@JoinColumn: 해당 관계에서 외래 키를 어떤 칼럼으로 사용할지를 명시해. 이 어노테이션을 사용하지 않으면 JPA가 자동으로 외래 키 칼럼을 생성하지만, 원하는 이름을 명시적으로 지정할 때 사용해.

 

예시:

@Entity
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;
}

 

여기서 **member_id**는 외래 키로 사용되며, 이를 통해 Order 엔티티가 Member 엔티티와 관계를 맺고 있어.

 

따라서 @ManyToOne@JoinColumn은 함께 자주 사용돼, 외래 키를 명시적으로 지정하는 데 중요한 역할을 해.


LEFT OUTER JOIN 예시 설명

**LEFT OUTER JOIN**은 **왼쪽 테이블의 모든 데이터를 반환**하고, 오른쪽 테이블에서 일치하는 데이터가 있으면 함께 출력해. 일치하지 않는 데이터는 `NULL`로 표시되지. 예시로 **학생(Student)** 테이블과 **수업(Course)** 테이블을 사용해서 설명해볼게.

#### **학생 테이블 (Student)**
student_id | name
-----------|--------
1          | Alice
2          | Bob
3          | Charlie



**학생 테이블**에는 3명의 학생(Alice, Bob, Charlie)이 있어.

#### **수업 테이블 (Course)**
course_id | student_id | course_name
----------|------------|-------------
1         | 1          | Math
2         | 1          | History
3         | 2          | Science


**수업 테이블**에는 각 학생이 수강한 과목이 저장되어 있어. Alice는 Math와 History를 듣고 있고, Bob은 Science를 듣고 있으며, Charlie는 수업을 듣고 있지 않아.


### **LEFT OUTER JOIN 쿼리**

```sql
SELECT s.student_id, s.name, c.course_name
FROM Student s
LEFT OUTER JOIN Course c
ON s.student_id = c.student_id;
```


이 쿼리는 **Student** 테이블과 **Course** 테이블을 조인해서, **Student 테이블의 모든 행**을 가져오고, **Course 테이블에서 일치하는 데이터가 있으면** 그것을 함께 반환해.

### **LEFT OUTER JOIN 결과 테이블**
student_id | name    | course_name
-----------|---------|-------------
1          | Alice   | Math
1          | Alice   | History
2          | Bob     | Science
3          | Charlie | NULL


- Alice는 두 과목(Math, History)을 듣기 때문에 두 행으로 출력돼.
- Bob은 하나의 과목(Science)을 듣고 있어, 그에 맞는 행이 출력돼.
- Charlie는 수업을 듣고 있지 않지만 **LEFT OUTER JOIN**이기 때문에 학생 정보가 출력되고, 수업 정보는 `NULL`로 표시돼.

이 방식으로 하면 **LEFT OUTER JOIN**의 동작 방식을 쉽게 이해할 수 있을 거야.

 

JPA에서 지연 로딩(LAZY Loading)**을 사용할 때, **프록시 객체(Proxy Object)**를 사용해. 프록시 객체는 실제 객체를 대신하는 가짜 객체라고 할 수 있어. 이 프록시는 실제 데이터베이스 조회를 지연시키고, 필요할 때(실제로 해당 엔티티의 데이터에 접근할 때) 데이터를 불러와.

 

지연 로딩의 동작 방식:

 

1. 지연 로딩 설정: 연관관계에서 fetch = FetchType.LAZY로 설정하면, JPA는 프록시 객체를 만들어 실제 데이터를 나중에 필요할 때 조회해.

2. 프록시 객체의 역할: 처음에 연관된 엔티티 대신 프록시 객체가 생성돼서, 실제 데이터는 조회되지 않아. 프록시 객체는 해당 엔티티의 아이디 정보만 가지고 있고, 다른 정보는 데이터베이스에서 조회하지 않아.

3. 실제 데이터 접근 시 조회: 나중에 엔티티의 필드(예: 연관된 엔티티의 이름)에 접근하려고 하면, 프록시 객체가 실제 데이터베이스를 조회해서 필요한 데이터를 가져와.

 

예시 코드:

@Entity
public class Member {

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "team_id")
    private Team team; // 연관관계 지연 로딩 설정
}

 

이 코드에서 MemberTeam 엔티티와 지연 로딩으로 연관돼 있어.

처음에 Member를 조회할 때, team 필드는 프록시 객체로 대체돼, 실제 데이터는 조회되지 않아.

나중에 team.getName() 같은 메서드를 호출할 때 비로소 데이터베이스에서 실제 데이터를 조회하게 돼.

 

프록시 객체의 특징:

 

프록시 객체는 실제 객체처럼 동작하지만, 실제 데이터베이스에서 값을 가져오기 전까지는 데이터 없이 빈 껍데기만 가지고 있어.

실제 데이터에 접근할 때 데이터베이스에서 값을 조회해.

 

장점:

 

성능 최적화: 필요할 때만 데이터를 조회하므로, 불필요한 데이터 로딩을 줄여 성능을 향상시킬 수 있어.

 

따라서 JPA에서 지연 로딩을 사용하면, 연관된 엔티티를 가짜 프록시 객체로 만들어 실제 데이터 조회를 지연시키는 전략을 사용하게 돼.

 

@ElementCollection은 JPA에서 컬렉션 형태의 값을 관리할 때 사용하는 어노테이션이야. 보통 기본적인 타입(예: String, Integer)이나 값 타입(Embeddable) 컬렉션을 관리할 때 유용해. 이 어노테이션을 사용하면 하나의 엔티티에 여러 개의 값 타입을 저장할 수 있어.

 

주요 특징:

 

1. 값 타입 컬렉션을 관리할 때 사용돼.

2. 값 타입 컬렉션을 별도의 테이블에 저장하는데, 기본적으로 엔티티와 1:N 관계를 형성해.

3. 컬렉션에 저장된 값은 Entity가 아님, 즉, 기본적으로 고유한 식별자(ID)를 가지지 않는 값 타입이야.

 

예시 코드:

@Entity
public class Member {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @ElementCollection
    @CollectionTable(name = "member_address", joinColumns = @JoinColumn(name = "member_id"))
    @Column(name = "address")
    private List<String> addresses = new ArrayList<>();
}

 

설명:

 

@ElementCollectionaddresses라는 List<String> 컬렉션을 관리할 수 있게 해줘.

@CollectionTable을 통해 **별도의 테이블(member_address)**에 값을 저장하며, 이 테이블은 member_id를 외래 키로 가져.

이렇게 값 타입의 리스트나 셋을 엔티티에 포함시킬 수 있어.

 

활용 예:

 

1. 주소, 전화번호 같은 값 타입의 데이터를 리스트로 관리할 때.

2. 엔티티가 아닌 단순한 값을 여러 개 저장하고 싶을 때(예: 여러 개의 태그나 속성).

 

제약 사항:

 

@ElementCollection은 값 타입에만 적용되기 때문에 엔티티 간의 관계(1:1, 1:N 등)와는 다르게 취급돼. 값 타입은 식별자가 없고, 데이터 변경 시 전체 삭제 후 다시 저장하는 방식으로 처리돼.

 

 **@JoinColumn(name=“TEAM_ID”)**에서 TEAM_ID는 **외래 키(Foreign Key)**로 사용돼. 외래 키는 현재 테이블(예: Member 테이블)에서 다른 테이블(예: Team 테이블)의 기본 키(id)를 참조하는 컬럼이야.

 

설명:

 

@JoinColumn: 두 엔티티 간의 관계를 정의할 때, 어느 컬럼을 외래 키로 사용할지를 지정하는 어노테이션이야. 여기서 **TEAM_ID**가 외래 키로 설정되며, 이 컬럼은 Member 테이블에서 Team 테이블의 기본 키인 id 값을 참조하게 돼.

 

예시로:

@ManyToOne
@JoinColumn(name="TEAM_ID")
private Team team;

 

이 코드는 Member 엔티티가 다대일(Many-to-One) 관계로 Team 엔티티와 연결돼 있음을 나타내.

TEAM_IDMember 테이블의 외래 키로, 이 값은 Team 테이블의 ID를 참조해.

 

따라서, TEAM_ID외래 키로서 Member 테이블과 Team 테이블 간의 관계를 설정하는 중요한 역할을 해.

 

study ahead💡

Cascade는 JPA에서 엔티티 간의 연관관계가 있을 때, 하나의 엔티티 상태 변화(저장, 삭제 등)가 다른 엔티티에도 영향을 미치도록 설정하는 기능을 의미해. 쉽게 말해, 부모 엔티티를 저장하거나 삭제할 때, 자식 엔티티도 함께 처리되도록 하는 설정이야. Cascade는 주로 부모-자식 관계처럼 하나의 엔티티가 다른 엔티티를 관리할 때 사용돼.

 

Cascade의 종류:

 

1. CascadeType.PERSIST: 부모 엔티티가 저장될 때, 연관된 자식 엔티티도 자동으로 저장돼.

2. CascadeType.REMOVE: 부모 엔티티가 삭제될 때, 연관된 자식 엔티티도 자동으로 삭제돼.

3. CascadeType.MERGE: 부모 엔티티가 병합(업데이트)될 때, 연관된 자식 엔티티도 자동으로 병합돼.

4. CascadeType.REFRESH: 부모 엔티티가 새로고침될 때, 연관된 자식 엔티티도 새로고침돼.

5. CascadeType.DETACH: 부모 엔티티가 영속성 컨텍스트에서 분리될 때, 자식 엔티티도 함께 분리돼.

6. CascadeType.ALL: 위의 모든 Cascade 옵션을 한 번에 적용하는 설정.

 

예시 코드:

@Entity
public class Parent {

    @OneToMany(cascade = CascadeType.ALL) // 자식 엔티티에 모든 Cascade를 적용
    private List<Child> children;
}

 

실생활 예시:

 

부모-자식 관계에서 부모 엔티티를 저장할 때 자식 엔티티도 자동으로 저장되어야 하는 경우가 있어. 예를 들어, **팀(Team)**과 팀원(Member) 관계에서, 을 저장할 때 팀원이 함께 저장되도록 하고 싶다면 Cascade 설정을 사용할 수 있어.

 

Cascade의 활용:

 

Cascade는 관계된 여러 엔티티를 한꺼번에 처리할 때 유용해. 데이터 일관성을 유지하는 데 도움을 주며, 각각의 엔티티에 대해 수동으로 처리할 필요 없이 JPA가 자동으로 관리해줘.

 

하지만 Cascade를 사용할 때는 조심해야 해. CascadeType.REMOVE처럼 부모가 삭제될 때 자식도 삭제되는 설정은, 잘못 사용하면 의도치 않은 데이터 손실을 초래할 수 있기 때문이야.

 

편의 메서드를 통해 양방향 연관관계를 쉽게 관리할 수 있어. 양방향 관계에서는 두 엔티티가 서로를 참조하게 되는데, 이를 설정할 때 편의 메서드를 사용하면 연관된 양쪽 엔티티에 대해 관계가 자동으로 동기화되도록 할 수 있어.

 

양방향 연관관계 문제

 

양방향 관계에서는 한쪽 엔티티에서 다른 엔티티를 참조할 때, **연관관계의 주인(owner)**과 **비주인(non-owner)**이 생겨.

예를 들어, Member 엔티티와 Team 엔티티가 양방향 관계일 때, 연관관계의 주인은 보통 외래 키를 관리하는 쪽(예: Member)이야.

하지만 양쪽 엔티티에서 관계를 설정할 때, 두 엔티티 간의 관계를 일관성 있게 관리하기 위해 양쪽 엔티티의 필드를 모두 수정해야 하는데, 이를 매번 수동으로 관리하기 어려워.

 

편의 메서드의 역할

 

편의 메서드는 양방향 관계에서 양쪽 엔티티에 대한 관계를 동기화할 수 있도록 도와줘. 즉, 하나의 메서드에서 양쪽의 관계를 동시에 설정할 수 있게 도와주는 거야.

 

예시:

 

Team 클래스와 Member 클래스가 양방향 관계일 때:

@Entity
public class Team {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    private String name;

    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    // 편의 메서드 추가
    public void addMember(Member member) {
        members.add(member);
        member.setTeam(this);  // 양방향 관계 설정
    }
}

 

이 편의 메서드를 사용하면, 팀에 멤버를 추가할 때, 양방향 관계가 자동으로 설정돼.

**members.add(member)**는 Team 엔티티에 **Member**를 추가하고, 동시에 **member.setTeam(this)**는 Member 엔티티에 **Team**을 설정해줘.

 

편의 메서드를 통한 양방향 관계 설정 흐름:

 

1. 멤버를 추가할 때 Team.addMember(Member member)를 호출.

2. 이 메서드는 members 리스트멤버를 추가하면서, 동시에 멤버의 팀 필드도 업데이트해.

3. 이렇게 하면 양쪽 모두의 관계가 일관성 있게 유지돼.

 

결론:

 

편의 메서드는 양방향 관계를 더 쉽게 관리할 수 있게 도와줘. 각 엔티티의 관계를 개별적으로 관리하는 대신, 하나의 메서드를 통해 양방향으로 관계가 일관성 있게 동기화되도록 할 수 있어.

 

Tip💡

StackOverflow 에러는 주로 Java에서 재귀 호출이나 무한 재귀 루프 등으로 인해 스택 메모리가 초과될 때 발생하는 에러야. 이 에러는 **JVM(Java Virtual Machine)**이 함수 호출이나 메서드 호출 시 사용하는 스택 메모리가 가득 차서 더 이상 사용할 수 없을 때 발생하지.

 

StackOverflowError 원인:

 

1. 무한 재귀 호출: 재귀 함수에서 종료 조건이 없거나 잘못 설정되면 계속해서 자기 자신을 호출하게 되어 스택이 가득 차버려.

2. 잘못된 메서드 호출 구조: 한 메서드가 다른 메서드를 계속해서 호출하는 구조가 순환 참조로 이어지면 발생할 수 있어.

3. 너무 깊은 재귀: 재귀 호출이 너무 깊으면, 메모리 한도를 넘어설 수 있어.

 

예시:

public class StackOverflowExample {
    public static void recursiveMethod() {
        recursiveMethod(); // 무한 재귀 호출
    }

    public static void main(String[] args) {
        recursiveMethod(); // 실행 시 StackOverflowError 발생
    }
}

 

위 예시에서 **recursiveMethod**는 무한 재귀로 인해 StackOverflowError가 발생해. 이 메서드는 스택 메모리를 다 사용하게 돼.

 

해결 방법:

 

1. 종료 조건 설정: 재귀 함수를 사용할 때는 반드시 종료 조건을 명확히 설정해야 해.

2. 반복문 대체: 깊은 재귀가 필요한 경우, 가능하다면 반복문을 사용해서 재귀 호출을 대체할 수 있어.

3. 메모리 조정: JVM의 스택 메모리 크기를 조정할 수 있지만, 이는 근본적인 해결책은 아니고 재귀 깊이를 제한하는 게 더 나은 방법이야.

 

이 에러는 무한 루프나 무한 재귀 호출이 가장 흔한 원인이라, 코드를 다시 점검해서 호출이 정상적으로 종료되는지 확인하는 것이 중요해.

  • 네이버 블로그 공유
  • 네이버 밴드 공유
  • 페이스북 공유
  • 카카오스토리 공유