객체끼리 협업하게 코딩하자. 좀 더 객체지향적으로!
연관관계?
객체와 객체 사이의 관계를 의미하며, 이를 데이터베이스의 테이블 간 관계와 매핑하는 것
예시) 도서관에서, 사용자(User)와 대출 기록(UserLoanHistory) 객체는 서로 연관 관계에 있기 때문에, 이 두 객체를 Service가 모두 다루게 하지 않고, 서비는 User 객체만 다루고, 이 두 객체끼리 서로 협업하게 하여 User 객체 안에서 UserLoanHistory 객체를 다루게 하는 것이 좀 더 객체지향적인 코드
선행 조건
두 협업하는 객체가 서로를 알아야 한다.
UserLoanHistory 객체 안의 요소로 User를 선언
이때 JPA에게, 이 User라는 요소가 테이블의 어느 필드에 해당하는 지 알려줘야 함.


두 객체에게 서로를 알려주기 위한 코드
뒤에서 추가 설명
연관관계 어노테이션
@ManyToOne
N:1 관계
학생 여러명이 교실에 들어갈 수 있다. 학생 N : 교실 1
이런 N:1 관계를 JPA에서는 @ManyToOne을 통해 매핑해줌
하나의 User가 여러 개의 UserLoanHistory(대출 기록)을 가질 수 있는 것처럼

UserLoanHistory 내의 요소로 User를 선언해준 것. 이제 UserLaonHistory는 User와 연결됨
@OneToMany
1:N 관계
User 입장에서는 UserLoanHistory와의 관계가 1:N 즉, 사용자 한 명은 여러 개의 대출 기록을 가질 수 있다.
User 객체 안에 UserLoanHistory 여러개를 갖는 List를 선언해줌

연관관계의 주인
연결되어 있는 두 객체에서, Table을 보았을 때 누가 관계의 주도권을 가지고 있는가
다른 상대방을 보고 있는 테이블이 주도권의 주인
user_loan_history의 user_id가 두 테이블을 연결하는 필드임. 이를 가진 user_loan_history가 연관관계의 주인이다.
연관관계의 주인을 표시하기 위해, 연관관계의 주인이 아닌 쪽에 mappedBy 옵션을 달아줘야 함.
@OneToMany(mappedBy = "연관관계의 주인이 가진 필드 이름")
@ManyToOne은 단방향으로만 사용할 수도 있다.
User 객체의 UserLoanHistory에 써줬던 OneToMany(mappedBy = "user"를 지우고 UserLoanHistory에서만 유저를 가리키게 써줄 수도 있다.

@JoinColumn
연관관계의 주인이 활용할 수 있는 어노테이션
연관관계의 주인에게, 연결하는 필드의 이름, null 여부, 유일성 여부, 업데이트 여부 등을 지정해줄 수 있음
이 요소를 해당 테이블의 어떤 Column과 맵핑해줘. 이름을 지정해주지 않아도 기본적으로 '필드명 + _id 형식으로 설정됨
@OneToOne
1:1 관계
한 사람은 한 개의 실거주 주소만을 갖고 있다.
Person 객체도 Address를 요소로 가지고, Address도 Person을 요소로 가짐
이런 경우에, person 테이블이 address 테이블의 id를 가질 수도 있고, address 테이블이 person 테이블의 id를 가질 수도 있음.
둘 중 하나만 한 쪽의 아이디를 가질 것.
예를 들어 person 테이블이 address_id를 요소로 갖도록 테이블을 생성했다면, 여기서는 Person 객체가 연관관계의 주인인 것
이 두 클래스 각각에 1대1 연관관계를 의미하는 @OneToOne 어노테이션을 붙여주고,
연관관계의 주인을 JPA에게 알려줘야 함.
Address 객체에, 요소 Person 위에 @OneToOne(mappedBy = "address")라고 써줘야 함
Person 객체의 요소 address에 의해 매여있다는 뜻
연관관계의 주인 효과
객체가 연결되는 기준이 된다
@Transactional
public void savePerson() {
Person person = personRepsotiroy.save(new Person()); //Person을 저장
Address address = addressRepsotiroy.save(new Address()); //Address를 저장
person.setAddress(address); //person과 address를 이어주는 코드
}
서비스에 이런 함수를 작성했다고 해보자
원래는 연결되어있지 않은 Person과 Address 각각을 생성자를 호출하여 각 객체가 생성되고, table에도 값이 저장됨
이 상황에서 person.setAddress를 호출하여 person에 있는 address를 업데이트 해주면, 두 table을 확인했을 때 이 두 객체가 연결되어 있을거임 (person에게 address_id가 정상적으로 저장되어 있을 것)
연관관계의 주인이 자신의 필드(address)를 수정하는 함수를 호출해본 것
이번엔 연관관계의 주인이 아닌 Address 객체에서 호출한다면?
@Transactional
public void saveAddress() {
Person person = personRepsotiroy.save(new Person()); //Person을 저장
Address address = addressRepsotiroy.save(new Address()); //Address를 저장
address.setPerson(person); //person과 address를 이어주는 코드
}
이 코드를 실행해보면, 각 테이블에는 각각의 person과 address 객체가 생성되었지만 person의 필드 address_id에 아무것도 저장되지 않음을 확인할 수 있음. 연결이 되지 않은 것
연관관계의 주인 효과
객체가 연결되는 기준이 된다
연관관계의 주인이 가리키는 객체가 세팅되어야만 데이터베이스에서 두 테이블이 연결됨
1. 상대 테이블을 참조하고 있으면 연관관계의 주인
2. 연관관계의 주인이 아니면 mappedBy를 사용
3. 연관관계의 주인의 setter가 사용되어야만 테이블이 연결됨
연관관계 사용 시 주의점
@Transactional
public void savePerson() {
Person person = personRepsotiroy.save(new Person()); //Person을 저장
Address address = addressRepsotiroy.save(new Address()); //Address를 저장
person.setAddress(address); //person과 address를 이어주는 코드
System.out.println(address.getPerson());
}
아까 정상적으로 작동했던 이 코드에서 트랜잭션이 끝나기 전에, 연관관계의 주인이 아닌 객체 address에서 address.getPerson을 호출하면 아무것도 출력되지 않음
트랜잭션이 끝나지 않았을 때, 한 쪽만 연결해두면 반대 쪽은 알 수 없다.
트랜잭션이 끝나지 않았으면 객체끼리는 한 쪽만 연결된 상태이고, 따라서 반대편이 알 수는 없음
(person.getAddress를 하면 불러와지긴 하지만, 그건 방금 저장된 객체에서 불러와지는 값이고, 아직 테이블에는 person도, address도 생기지 않았고 둘이 연결된 상태도 아님. 트랜잭션이 성공으로 끝나면 그제야 객체의 변경 사항이 테이블에 반영되고, 성공적으로 끝나지 않으면 객체도, 위 함수에서 진행한 모든 작업들이 객체에도 반영되지 않은 상태로 돌아간다.)
이런 문제를 해결하기 위해 setter을 한 쪽에서 호출할 때 양쪽 모두 이어주도록 함
setter 코드 안에서 양쪽이 저장하게 해주는 것
public void setAddress(String address) {
this.address = address;
this.address.setPerson(this);
}
이렇게 세터 코드 안에서 양 쪽 모두 연결되게 해주면 아까 널로 호출된 address.getPerson이 정상적으로 호출될 것
@ManyToMany
N:M 관계
예를 들면 학생과 동아리 관계. 한 학생은 여러 동아리에 가입할 수 있고, 한 동아리에는 여러 학생이 있다
이 관계를 JPA에서 표현하기 위한 @ManyToMany 어노테이션이 있으나, 구조가 복잡하고 테이블이 직관적으로 맵핑되지 않아 잘 사용하지 않음
Many TO Many 관계를 모두 Many to One으로 풀어헤쳐, N:M 관계를 N:1 관계를 연결시켜둔 것처럼 풀어 코딩하기 추천
연관관계 어노테이션 옵션
Cascade 옵션
직역: 폭포처럼 흐르다
한 객체가 저장되거나 삭제될 때, 그 변경이 폭포처럼 흘러 연결되어 있는 객체도 함께 저장되거나 삭제되는 기능
예를 들어, 유저 테이블에 A 유저가 저장되어 있고, 대출 기록 테이블에 A 유저가 빌린 책 1과 2가 존재할 때, 유저 테이블에서 A 유저를 삭제하면 대출 기록 테이블에 있는 A 유저가 빌린 책의 기록도 삭제되도록 하는 것
하나의 데이터가 사라지거나 생성될 때 연결되어있는 데이터가 사라지거나 생성될 수 있게 하는 옵션
User 객체 안에 선언해준 UserLoanHistory의 리스트에

이렇게 옵션을 주면 됨
이렇게 하면, User를 삭제했을 때 User와 연결된 UserLoanHistory까지 한 번에 삭제된다.
OrphanRemoval 옵션
객체 간의 관계가 끊어진 데이터를 자동으로 제거하는 옵션
우리가 만든 userLoanHistories 리스트에서, 하나의 UserLoanHistory를 삭제했을 때 그 변경 사항이 데이터베이스에도 반영되게 하는 옵션
연관관계 사용의 장점
1. 각자의 역할에 집중하게 됨
계층별로 응집성이 강해진다.
서비스 계층의 역할: 필요한 경우, 다른 도메인들끼리 협업을 할 수 있게 도와줌, 트랜잭션 관리, 외부 의존성 관리
도메인 계층의 역할: 도메인 객체가 필요로 하고 있는 비즈니스에 대해 로직을 처리하는 역할
2. 새로운 개발자가 코드를 읽을 때 이해하기 쉬워짐
계층이 분리되어 있고 도메인 계층이 어떤 일을 하는 지를 파악할 수 있기 때문에 새로운 개발자가 이해하기 쉬워진다.
3. 테스트 코드 작성이 쉬워짐
도메인 계층에 로직이 들어가면, 도메인 계층의 함수를 단위별로 테스트 할 수 있어 테스트코드 작성이 쉬워짐
연관관계를 사용하는 것이 항상 좋을까?
꼭 그렇지는 않다.
지나치게 사용하면 성능상의 문제가 생길 수 있고, 도메인 간의 복잡한 연결로 인해 시스템을 파악하기 어려워질 수도 있다.
연관관계를 너무 많이 사용하면 서로 다른 객체끼리 복잡하게 얽혀 있기 때문에 성능상 문제가 있을 수 있음
또한 이렇게 너무 얽혀있으면, A를 수정했을 때 B C D까지 영향이 가게 될 수 있음.
따라서 비즈니스 요구사항, 기술적인 요구사항, 도메인 아키텍처 등 여러 부분을 고민하여 연관관계 사용을 선택해야 함
'스프링부트' 카테고리의 다른 글
| [SpringBoot] Transaction 트랜잭션 (1) | 2025.03.10 |
|---|---|
| [SpringBoot] Spring Data JPA를 사용한 CRUD (0) | 2025.03.10 |
| [SpringBoot][DB] JPA (이용 배경, 개념, table과 객체 맵핑 실습) (0) | 2025.03.09 |
| [SpringBoot] Spring Container (0) | 2025.03.09 |
| [SpringBoot] Layered Architecture: Controller, Service, Repository (0) | 2025.03.08 |