[Spring Boot] 섹션6 연관관계

정수현·2025년 4월 20일

캡스톤

목록 보기
7/8

33강 객체지향적 개발

현재 코드

① 대출 기능

Book 책 정보를 가져온다.
UserLoanHistoryRepository 검증한다.
User 사용자 정보를 가져온다.
BookService
UserLoanHistory 에 기록을 만들어 저장한다.

② 반납 기능

User 사용자 정보를 가져온다.
UserLoanHistory 대출 기록을 가져와 반납으로 처리한다.
BookService

N : 1 관계

객체 지향적 개발

  • UserUserLoanHistory 사용자 정보와 대출 기록을 가져와 바로 대출 및 반납을 처리한다.
    BookService

  • 선행 조건 : UserUserLoanHistory 가 서로를 알아야 한다.

@ManyToOne 어노테이션

  • 내가 N이고, 너가 1이다.
  • 예시) 학생 여러명이 교실에 들어갈 수 있다. → 학생 N : 교실 1
@Entity public class UserLoanHistory
{
	@ManyToOne
    private User user;
}

User 객체를 UserLoanHistory 객체의 멤버 필드로 사용할 수 있다.

@OneToMany 어노테이션

  • 1 : N 관계
  • 내가 1이고, 너가 N
@Entity
public class User
{
	@OneToMany
	private List<UserLoanHistory> userLoanHistoryList = new ArrayList<>();
}

UserLoanHistory 객체 타입을 List 로 사용할 수 있다.

③ 연관관계의 주인

연결되어 있는 Table을 보았을 때 누가 관계의 주도권을 갖고 있는가

// user 테이블
CREATE TABLE user(
	user_id BIGINT AUTO_INCREMENT NOT NULL,
    user_name VARCHAR(25) NOT NULL,
    user_age INT NOT NULL,
    PRIMARY KEY(user_id)
);

// user_loan_history 테이블
CREATE TABLE user_loan_history(
	load_id BIGINT AUTO_INCREMENT,
    user_id BIGINT,
    book_name VARCHAR(255),
    is_return TINYINT(1),
    PRIMARY KEY(load_id)
);

user_loan_history 테이블이 주도권을 갖고 있다.

mappedBy 옵션

↪ 연관관계의 주인이 아닌 쪽에 mappedBy 옵션을 달아줘야 한다.
@OneToMany(mappedBy = "user")

@Entity
public class User
{
	@OneToMany(mappedBy = "user")
	private List<UserLoanHistory> userLoanHistoryList = new ArrayList<>();
}

↪ 연관관계의 주인의 값이 설정되어야만 진정한 데이터가 저장된다.



34강 JPA 연관관계

1:1 연관관계

테이블 생성

CREATE TABLE person (
	person_id BIGINT AUTO_INCREMENT PRIMARY KEY,
    person_name VARCHAR(255),
    address_id BIGINT
);

CREATE TABLE address(
	address_id BIGINT AUTO_INCREMENT PRIMARY KEY,
    city VARCHAR(255),
    street VARCHAR(255)
);

person이 주도권을 가지고 있다. person이 연관관계의 주인

엔티티 클래스 구현

Person

@Entity
public class Person
{
	// [멤버 필드]
	@OneToOne
    private Address address;
}

Address

@Entity
public class Address
{
	// [멤버 필드]
    @OneToOne(mappedBy = "address")
    private Person person;
}	

연관관계의 주인 효과

  • 객체가 연결되는 기준이 된다.
  • 연관관계 주인이 가르치는 객체의 Setter 가 호출되어야만
    객체가 데이터베이스에서 두 테이블을 연결시킬 수 있다.
@Transactioinal
public void savePerson()
{
	Person person = personRepository.save(new Person());
    Address address = addressRepository.save(new Address());
    person.setAddress(address); // 정상 반영
   	// address.setPerson(person); -> null
}
  1. 상대 테이블을 참조하고 있으면 연관관계의 주인이다.
  2. 연관관계의 주인이 아니면 mappedBy 옵션을 사용한다.
  3. 연관관계의 주인의 Setter 가 호출되어야만 테이블을 연결할 수 있다.

연관관계 사용 시 주의해야 할 점

@Transactioinal
public void savePerson()
{
	Person person = personRepository.save(new Person());
    Address address = addressRepository.save(new Address());
    person.setAddress(address); // 정상 반영
   	
    System.out.println(address.getPerson()); // null
}
  • 트랜잭션이 끝나지 않았을 때, 한쪽만 연결해두면 반대쪽은 알 수 없다.
    ↪ 해결책 : Setter 한 번에 둘을 같이 이어준다.!

해결 방법

Person

@Entity
public class Person
{    
	// [Setter] 
    public void setAddress(Address address)
    {
        this.address = address;
        this.address.setPerson(this); // -> Setter끼리 연결 
    }
}

Address

@Entity
public class Address
{    
    // [Setter]
    public void setPerson(Person person) 
    {
    	this.person = person;
    }
    
    // [Getter]
    public Person getPerson()
    {
    	return this.person;
    }
}	

PersonService

@Transactoinal void savePerson()
{
	Person person = personRepository.save(new Person());
    Address address = addressRepository.save(new Address());
    person.setAddress(address); // 테이블 간 연결
    address.getPerson(); // 객체끼리 연결 
}

정리

  • N:1 관계 - @ManyToOne@OneToMany
    ↪ 연관관계의 주인인 쪽이 N이다.
  • @ManyToOne을 단방향으로 사용할 수 있다.
    User 클래스에서 private List<UserLoanHistory> userLoanHistoryList를 생략할 수 있다.

N:M 관계 @ManyToMany

  • 구조가 복잡하고, 테이블이 직관적으로 매핑되지 않아 사용하지 않는 것을 추천 ,,
  • 예시) 동아리 : 학생 관계

기타 옵션
JoinColumn 어노테이션
cascade 옵션
orphanRemoval 옵션

@JoinColumn

  • 연관관계의 주인이 활용할 수 있는 어노테이션
  • 필드의 이름이나 null 여부, 유일성 여부, 업데이트 여부 등을 지정한다.
    @Column 어노테이션과 역할은 유사하나, @JoinColumn은 연관관계의 주인에게 활용할 수 있는 어노테이션이다.

cascade 옵션

  • cascade : 폭포처럼 흐르다.
  • 한 객체가 저장되거나 삭제될 때, 그 변경이 폭포처럼 흘러 연결되어 있는 객체도 함께 저장되거나 삭제되는 기능

예시) User가 책 1과 책 2를 빌렸을 때, User 객체를 삭제하면 → User만 삭제되고 UserLoanHistory는 남아있게 된다.

@OneToMany(mappedBy = "user", cacade = CascadeType.ALL)
private List<UserLoanHistory> userLoanHistoryList = new ArrayList<>();

orphanRemoval 옵션

UserLoanHistory에서 User가 빌린 책을 한권만 삭제하고 싶을 때

@Transactional
public void deleteUserHistory() 
{
	User user = userRepository.findByName("정수현")
    	.orElseThrow(IllegalArgumentException::new);
    
    user.removeOneHistory();
}

public void removeOneHistory()
{
	userLoanHistoryList.removeIf(history -> "책1".equals(history.getBookName()));
}

↪ 데이터베이스 상에 아무런 변화 없다.

orphanRemoval 옵션을 통한 해결 방법

@OneToMany(mappedBy = "user", cascade = CascadeType.ALL, orphanRemoval = true)
private List<UserLoanHistory> userLoanHistoryList = new ArrayList<>();
  • 객체 간의 관계가 끊어진 데이터를 자동으로 제거하는 옵션
  • 관계가 끊어진 데이터 = orphan (고아) / 제거 = removal

34강 정리

  1. 상대 테이블을 가리키는 테이블이 연관관계의 주인이다.
    연관관계의 주인이 아닌 객체는 mappedBy를 통해 주인에게 매여 있음을 표시해 주어야 한다.

  2. 양쪽 모두 연관관계를 갖고 있을 때는 양쪽 모두 한 번에 맺어주는 게 좋다.
    예시) 한쪽의 Setter를 부를 때 양쪽이 연결되게끔 양쪽 객체 클래스에 모두 Setter를 이어주도록 구현하는게 중요하다.

  3. cascade 옵션을 활용하면 저장이나 삭제를 할 때 연관관계에 놓인 테이블까지 함께 연관관계에 놓인 테이블까지 함께 저장 또는 삭제가 이루어진다.

  4. orphanRemoval 옵션을 활용하면, 연관관계가 끊어진 데이터를 자동으로 제거해준다.



35강 리팩토링과 지연 로딩

User 클래스에서 일반 메서드로 loanBookreturnBook 기능 함수를 구현하였다.

User

@Entity
public class User
{
	// [일반 메서드]
    // 대출 기능
    public void loanBook(String bookName)
    {
        this.userLoanHistoryList.add(new UserLoanHistory(this, bookName, false));
    }

	// 반납 기능
    public void returnBook(String bookName)
    {
        // 1) 대출 기록을 읽어들여 bookName에 일치하는 기록을 찾는다.
        UserLoanHistory targetHistory = this.userLoanHistoryList.stream() // 리스트를 한줄씩 읽어들인다.
                .filter(history -> history.getBookName().equals(bookName)) // 책이름과 일치하는 기록을 가져온다.
                .findFirst() // 첫번째에 해당하는 기록을 반환한다.
                .orElseThrow(IllegalArgumentException::new);

        // 해당 기록을 찾았으면 반납처리한다.
        targetHistory.doReturn();
    }
}

BookService

@Transactional
public void selectReturn(BookReturnRequest request)
{
	// 1) User 정보를 가져온다.
	User user = userRepository.findByUserName(request.getUserName())
		.orElseThrow(IllegalArgumentException::new);

	// 2) UserLoanHistory에서 반납을 처리한다.
	user.returnBook(request.getBookName());
}

⇒ JPA의 연관관계 옵션을 활용해서 최대한 도메인들끼리 직접 협력할 수 있도록 코드를 수정하였다.

영속성 컨텍스트 네번째 기능

지연 로딩

  • 지연 로딩 (Lazy Loading)
    : 서버가 시작하자마자 UserUserLoanHistory 클래스를 호출하는게 아닌,
    처음에 User만 가져왔다가 UserLoanHistory가 필요한 순간에 호출한다.
    연결되어 있는 객체를 꼭 필요한 순간에만 가져온다.
  • @OneToManyfetch 옵션
  • 트랜잭션 환경에서만 가능하다.

연관관계의 장점

  1. 각자의 역할에 집중하게 된다. (= 응집성)
  2. 새로운 개발자가 코드를 읽을 때 이해하기 쉬워진다.
  3. 테스트 코드 작성이 쉬워진다.

연관관계를 사용하는 것이 항상 좋을까?

  • 지나치게 사용하면, 성능상의 문제가 생길 수도 있고,
    도메인간의 복잡한 연결로 인해 시스템을 파악하기 어려워질 수도 있다.
  • 비즈니스 요구사항, 기술적 요구사항, 도메인 아키텍처 등 여러 부분을 고민해서 연관관계 사용을 선택해야 한다.



36강 요약

  1. 책 생성, 대출 반납 API를 온전히 객발하며 지금까지 다루었떤 모든 개념을 실습해본다.
  2. 객체지향적으로 설계하기 위해 연관관계를 이해하고, 연관관계의 다양한 옵션에 대해 이해한다.
  3. JPA에서 연관관계를 매핑하기 위한 방법을 이해하고,
    연관관계를 사용해 개발할 떄와 사용하지 않고 개발할 때의 차이점을 이해한다.

0개의 댓글