이전 포스팅에서 이어집니다.
[Spring Data JPA] 1 : N, N : 1 관계의 양방향 매핑과 단방향 매핑
양방향 매핑과 단방향 매핑 스프링 JPA를 거쳐가며 게시판을 만들어 본 사람이라면 엔티티 연관관계 매핑을 할 때 무조건 만나봤을 그 어노테이션들이다. 특히 글과 댓글과 같은 다대일 관계에
doinitright.tistory.com
[부록] 연관관계 주인이 트랜잭션 권한을 가지는 예시
본 부록은 연관관계로 매핑된 두 엔티티에서 서로의 연관관계를 활용해
본인과 연관된 엔티티의 값을 수정하고 이 결과가 정상적으로 트랜잭션 커밋되는지 실험해본 내용이다.
* 앞서 서술했듯이 영속성 컨텍스트에 의해 관리되는 객체에 변경을 가하는 것은 변경 감지로 인해 종속 엔티티와 주인 엔티티 모두에서 변경이 가능하다.
* 따라서, 영속성 컨텍스트에 의해 관리되지 않는 새 연관 엔티티를 생성하고,
연관 엔티티를 본인의 엔티티 속성으로 집어넣어 해당 내용이 반영되는지 확인해보았다.
문답무용, 코드로 설명하겠다.
우선 초기 데이터 설정이다.
[Company.java]
@Entity
@Table (name = "company")
@NoArgsConstructor
@Setter
public class Company {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long companyId;
// mappedBy : 정의된 엔티티는 종속 엔티티, 정의되지 않은 엔티티는 주인 엔티티
// mappedBy = "company" : 연관관계의 주인 엔티티에서 company 필드에 의해 관리됨을 의미
// 즉, Employee가 가지고 있는 FK가 Company의 PK이므로 company 필드를 매핑시켜줌
@OneToMany(mappedBy = "company")
@Builder.Default
private List<Employee> employees = new ArrayList<>();
}
[Employee.java]
@Entity
@Table (name = "company")
@NoArgsConstructor
@Setter
public class Company {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long companyId;
// mappedBy : 정의된 엔티티는 종속 엔티티, 정의되지 않은 엔티티는 주인 엔티티
// mappedBy = "company" : 연관관계의 주인 엔티티에서 company 필드에 의해 관리됨을 의미
// 즉, Employee가 가지고 있는 FK가 Company의 PK이므로 company 필드를 매핑시켜줌
@OneToMany(mappedBy = "company")
@Builder.Default
private List<Employee> employees = new ArrayList<>();
}
엔티티 클래스는 이전 포스팅과 변동 없이 작성한다.
[DataInitService.java]
@Service
@RequiredArgsConstructor
public class DataInitService {
private final CompanyRepository companyRepository;
private final EmployeeRepository employeeRepository;
@PostConstruct
public void init() {
// 회사 생성
Company company = new Company();
company.setName("ABC Company");
companyRepository.save(company);
// 직원 생성
Employee employee1 = new Employee();
employee1.setName("John Doe");
employee1.setCompany(company);
employeeRepository.save(employee1);
Employee employee2 = new Employee();
employee2.setName("Jane Doe");
employee2.setCompany(company);
employeeRepository.save(employee2);
}
}
각자 한개의 회사 엔티티와 해당 회사에 소속된 두개의 사원 엔티티를 생성해 DB에 커밋한다.
명실상부한 대기업 ABC Company에는 존 도와 제인 도가 소속되어 있다.
[실행 결과 : json, DB]
{
"companyId": 1,
"companyName": "ABC Company",
"employees": [
{
"employeeId": 1,
"employeeName": "John Doe",
"companyId": 1
},
{
"employeeId": 2,
"employeeName": "Jane Doe",
"companyId": 1
}
]
}
엔티티의 구조에는 변동이 없으며, 각자의 엔티티 서비스 로직에 아래와 같은 메소드를 추가했다.
[EmployeeService.java]
@Service
@RequiredArgsConstructor
public class EmployeeService {
private final EmployeeRepository employeeRepository;
private final CompanyRepository companyRepository;
// 조회는 생략 ...
// employee(연관관계의 주인)을 통해 company(연관관계 종속자)의 이름을 수정하는 메소드
public void updateCompanyByEmployee() {
Optional<Employee> opEmployee = employeeRepository.findById(1L);
Employee employee = opEmployee.get();
// 영속성 컨텍스트에 의해 관리되는 상태의 엔티티가 아닌 새로운 엔티티 생성
Company company = new Company();
company.setName("New Company");
companyRepository.save(company);
employee.setCompany(company);
employeeRepository.save(employee);
// employee의 companyId(FK)는 null로 연관관계 매핑 X, 이후 컨트롤러에서 NPE 발생
}
}
그러나 ABC Company의 존속을 위협하는 New Company가 등장한다.
EmployeeService에서는영속성 컨텍스트에 감지되지 않는 이 새로운 New Company를 생성하고,
특정 Employee 의 소속 Company를 이 신규 New Company로 변경해보았다.
[실행 결과 : json, DB]
[
{
"employeeId": 1,
"employeeName": "John Doe",
"companyId": 2
},
{
"employeeId": 2,
"employeeName": "Jane Doe",
"companyId": 1
}
]
이와 같이, mappedBy 로 인해 연관관계에서 주인으로 설정된 Employee에서 Employee 내부 필드의 Company에 대해 수정을 가하는 것은 정상적으로 그 트랜잭션 내용이 반영됨을 확인할 수 있다.
그렇다면 Company에서도 동일한 개념이 적용될지 확인해보자.
[CompanyService.java]
@Service
@RequiredArgsConstructor
public class CompanyService {
private final CompanyRepository companyRepository;
private final EmployeeRepository employeeRepository;
// 조회는 생략 ...
// ABC company 의 맨 첫번째 ID의 사원 이름을 수정하는 메소드
public void updateEmployeeByCompany() {
Optional<Company> opCompany = companyRepository.findById(1L);
Company company = opCompany.get();
// 영속성 컨텍스트에 의해 관리되는 상태의 엔티티가 아닌 새로운 엔티티 생성
Employee employee = new Employee();
employee.setName("Foo");
employeeRepository.save(employee);
company.getEmployees().set(company.getEmployees().size()-1, employee);
companyRepository.save(company);
}
}
본 도메인 로직은 1번 company인 ABC Company의 첫번째 사원의 이름을 수정하는 로직이다.
CompanyService에서도 영속성 컨텍스트에 감지되지 않는 새로운 FOO라는 이름의 Employee를 생성하고,
특정 company를 통해 employee 리스트 속 첫번째 사원의 이름을 FOO로 변경해보았다.
[실행 결과 : json, DB]
종속 엔티티인 Company에서 주인 엔티티인 Employee에 대한 값 변경은 위의 결과와 같이 연관관계가 매핑되지 않는다는 것을 확인할 수 있었다.
결론
- mappedBy 설정으로 인해 다대일 양방향 관계에서 N은 연관관계의 주인이 된다.
- 연관관계의 주인인 N은 연관된 엔티티를 생성, 수정, 삭제 하는 트랜잭션 권한을 가지고 있다.
- 연관관계의 종속자인 1은 트랜잭션 권한은 없고, 조회 권한만 가지고 있다.
- 그러나, 영속성 컨텍스트에 의해 기존에 있는 값을 수정하는 것은 어느 엔티티에서든지 가능하다.
- 따라서, 특정 엔티티의 서비스 로직에서 다른 엔티티를 변경하는 일은 없게끔 명확한 책임 분리가 요구된다.
- 예시처럼 하면 절대 안된다.
- 안전한 구현 방향은 아무래도 양방향 연관관계로 연결된 두 엔티티의 특정 필드가 변경될 시 두 엔티티를 동일하게 변경하는 것이다.
- 구현 방법에는 편의 메소드나,영속성 컨텍스트의 Persist, Merge를 활용하는 방법이 있을 것이다.
# 구현 코드
'DEV > Spring' 카테고리의 다른 글
[트러블슈팅][JPA] OneToOne 양방향 관계 시 의도하지 않은 N+1 문제 발생 원인과 해결 방법 (0) | 2024.02.20 |
---|---|
[트러블슈팅] @ReqestParam, @PathVariable 사용 시 Name for argument of type [”type”] not specified 에러 해결 (0) | 2024.02.19 |
[Spring Data JPA] 1 : N, N : 1 관계의 양방향 매핑과 단방향 매핑 (1) | 2024.01.16 |
[트러블슈팅] @Transactional 미작동 시 해결방법 (4) | 2024.01.04 |
[Spring Boot] CORS 에러 해결, CORS 설정 (0) | 2023.12.30 |