Home JPA에서 insertable=false, updatable=false를 하면 반드시 값이 저장되지 않을까?
Post
Cancel

JPA에서 insertable=false, updatable=false를 하면 반드시 값이 저장되지 않을까?

글을 작성하게 된 계기


JPA에서 insertable=false, updatable=false 설정 중, 알게 된 내용을 정리하기 위해 글을 작성하게 되었습니다.





1. 글을 작성하게 된 계기


회사에서 복합 키를 사용해 데이터를 저장할 일이 있었습니다. 이 과정에서 엔티티 간 연관관계를 잘못 맺어 제약 조건에 관한 오류가 발생했는데요, 똑같은 실수를 하지 않기 위해, 혹은 비슷한 문제가 발생했을 경우 다른 팀원들에게 공유할 수 있도록 글을 작성하게 되었습니다. 테이블 구조는 다음과 같으며, 이를 토대로 예제 코드를 살펴보겠습니다.

image





2. 문제 상황


연관 관계는 다음과 같습니다. 직원과 부서, 직원과 담당 업무는 일대 다의 관계를 맺고 있습니다. 여기서 살펴볼 점은 insertable = false, updatable = false 설정인데요, 이 값에 따라 어떻게 결과가 달라지는지 살펴보겠습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Getter
@Entity(name = "employee")
public class Employee {

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

    @Column(name = "name")
    private String name;

    @OneToMany(mappedBy = "employee", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<EmployeeDuty> employeeDuties = new HashSet<>();

    @OneToOne(mappedBy = "employee", cascade = CascadeType.ALL, orphanRemoval = true)
    private Department department;

    ......
    
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Getter
@IdClass(DutyId.class)
@Entity(name = "employee_duty")
public class EmployeeDuty {

    @Id
    @Column(name = "employee_id")
    private Long employeeId;

    @Id
    @Enumerated(EnumType.STRING)
    private DutyCode dutyCode;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "employee_id", referencedColumnName = "id", insertable = false, updatable = false)
    private Employee employee;  // insertable, updatable 설정

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    ......
    
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Getter
@Entity(name = "department")
public class Department {

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

    @Column(name = "name")
    private String name;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "employee_id", referencedColumnName = "id", insertable = false, updatable = false)
    private Employee employee; // insertable, updatable 설정

    ......
    
}





서비스 레이어는 다음과 같습니다. Employee를 저장하며 연관된 엔티티를 저장하는 로직, 이를 조회하는 로직 두 개가 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
@RequiredArgsConstructor
class EmployeeService implements EmployeeSaveUseCase {

    private final EmployeeRepository employeeRepository;

    @Override
    @Transactional
    public Employee save(
        String employeeName,
        String departmentName
    ) {
        Employee newEmployee = employeeRepository.save(new Employee(employeeName));
        Set<EmployeeDuty> employeeDuties = new HashSet<>();
        employeeDuties.add(new EmployeeDuty(
            newEmployee,
            DutyCode.ANALYSIS_MARKETING,
            LocalDateTime.now()
        ));
        newEmployee.register(employeeDuties, new Department(departmentName, newEmployee));
        return newEmployee;
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
@RequiredArgsConstructor
class EmployeeSearchService implements EmployeeSearchUseCase {

    private final EmployeeRepository employeeRepository;

    @Override
    @Transactional(readOnly = true)
    public Employee findById(Long memberId) {
        return employeeRepository.findByIdAndDuty(memberId)
            .orElseThrow(EmployeeNotFoundException::new);
    }
}





이를 테스트하면 다음과 같은 에러가 나는 것을 볼 수 있습니다. DataIntegrityViolationException 에러로, 실제 회사에서 겪었던 것과 똑같은 에러입니다. 데이터베이스가 MySQL이라 에러 메시지는 다르지만, 발생한 에러는 정확히 재현됐는데, 이는 Department 엔티티(외래키)가 저장되지 않았기 때문입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
@DisplayName("[IntegrationTest] 직원 저장 통합 테스트")
class EmployeeRelationIntegrationTest extends IntegrationTestBase {

    @Autowired
    private EmployeeSaveUseCase employeeSaveUseCase;

    @Autowired
    private EmployeeSearchUseCase employeeSearchUseCase;

    @Test
    @DisplayName("EmployeeDuty를 생성할 때, 연관관계를 함께 저장할 수 있다.")
    void employeeCreateWithRelationTest() {
        Employee employee = employeeSaveUseCase.save("Jun", "Developer");

        Employee findEmployee = employeeSearchUseCase.findById(employee.getId());

        assertAll(
            () -> assertNotNull(findEmployee.getEmployeeDuties()),
            () -> assertNotNull(findEmployee.getDepartment())
        );
    }
}

image





이는 위에서 Department 매핑을 insertable = false, updatable = false로 설정했기 때문인데요, 즉, insert와 update를 모두 허용하지 않기 때문에 데이터가 저장되지 않아 발생한 문제 입니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Getter
@Entity(name = "department")
public class Department {

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

    @Column(name = "name")
    private String name;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "employee_id", referencedColumnName = "id", insertable = false, updatable = false)
    private Employee employee; // insertable, updatable 설정

    ......

}





해당 설정을 제거하고 이를 실행하면 다음과 같이 데이터가 저장되는 것을 볼 수 있습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Getter
@Entity(name = "department")
public class Department {

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

    @Column(name = "name")
    private String name;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "employee_id", referencedColumnName = "id")
    private Employee employee; // insertable, updatable 설정

    ......

}

image





그런데 여기서 한 가지 의문이 들었는데, @ManyToOne 관계를 설정한 employeeDuties는 데이터가 잘 저장되는 점이었습니다. 분명 똑같은 insertable = false, updatable = false 설정했는데도 말이죠.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
@Getter
@Entity(name = "employee")
public class Employee {

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

    @Column(name = "name")
    private String name;

    @OneToMany(mappedBy = "employee", cascade = CascadeType.ALL, orphanRemoval = true)
    private Set<EmployeeDuty> employeeDuties = new HashSet<>();

    @OneToOne(mappedBy = "employee", cascade = CascadeType.ALL, orphanRemoval = true)
    private Department department;

    ......

}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Getter
@IdClass(DutyId.class)
@Entity(name = "employee_duty")
public class EmployeeDuty {

    @Id
    @Column(name = "employee_id")
    private Long employeeId;

    @Id
    @Enumerated(EnumType.STRING)
    private DutyCode dutyCode;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "employee_id", referencedColumnName = "id", insertable = false, updatable = false)
    private Employee employee;  // 같은 설정

    @Column(name = "created_at")
    private LocalDateTime createdAt;

    ......

}





이유는 간단한데, 해당 테이블은 DutyId에서 복합키로 사용할 값을 이미 지정했기 때문입니다.

1
2
3
4
5
6
7
8
9
10
11
12
@Getter
public class DutyId implements Serializable {

    @Column(name = "employee_id")
    private Long employeeId;

    @Enumerated(EnumType.STRING)
    private DutyCode dutyCode;

    ......

}
1
2
3
4
5
6
@Getter
@IdClass(DutyId.class)  // IdClass 매핑
@Entity(name = "employee_duty")
public class EmployeeDuty {
    ......
}





즉, DutiyId에서 employee 테이블의 PK를 복합 키 중 하나로 지정했으며, 생성자의 employee.getId( )에서 이를 할당해 줬기 때문에 Employee가 저장되지 않더라도 아무 문제가 없었던 것이었습니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
@Getter
@IdClass(DutyId.class)
@Entity(name = "employee_duty")
public class EmployeeDuty {

    ......

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "employee_id", referencedColumnName = "id", insertable = false, updatable = false)
    private Employee employee;

    ......

    public EmployeeDuty(
        Employee employee,
        DutyCode dutyCode,
        LocalDateTime createdAt
    ) {
        this.employeeId = employee.getId();  // 실제 복합 키로 사용되는 값. Employee 엔티티가 null이어도 괜찮다.
        this.employee = employee;
        this.dutyCode = dutyCode;
        this.createdAt = createdAt;
    }

    ......

}





employee.getId( )를 제거하면 다음과 같이 에러가 발생하는 것을 볼 수 있습니다. 위에서 발생했던 에러와 똑같죠?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
@Getter
@IdClass(DutyId.class)
@Entity(name = "employee_duty")
public class EmployeeDuty {

    ......

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "employee_id", referencedColumnName = "id", insertable = false, updatable = false)
    private Employee employee;

    ......

    protected EmployeeDuty() {
    }

    public EmployeeDuty(
        Employee employee,
        DutyCode dutyCode,
        LocalDateTime createdAt
    ) {
        // this.employeeId = employee.getId();  
        this.employee = employee;
        this.dutyCode = dutyCode;
        this.createdAt = createdAt;
    }

    ......

}

image





정리해 보면 insertable = false, updatable = false을 설정하면 연관관계를 등록해도 엔티티가 저장되지 않습니다. 단, 복합키에 사용될 실제 값을 이미 할당했다면, 해당 엔티티가 null로 들어가더라도 아무 상관이 없습니다. 만약 데이터를 저장해야 한다면 아래와 같이 해당 설정을 제거해 줘야 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
@Getter
@Entity(name = "department")
public class Department {

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

    @Column(name = "name")
    private String name;

    @OneToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "employee_id", referencedColumnName = "id")
    private Employee employee; // insertable, updatable 설정 제거

    ......

}





3. 주의할 점


이때, 한 가지 더 주의할 점이 있는데, 연관 관계를 저장할 때 @Transactional을 붙이지 않으면 에러가 발생합니다. 이는 조금만 생각해 보면 답을 찾을 수 있는데, Jpa가 제공하는 기능들은 기본적으로 트랜잭션의 범위 내에서 영속화가 이루어지기 때문입니다. 즉, 트랜잭션이 없으면 연관관계를 맺어도 이를 영속성 컨텍스트에 저장할 수 없기 때문에 반영이 안 됩니다. 사소하지만 이런 점도 반드시 챙기도록 합니다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
@Service
@RequiredArgsConstructor
class EmployeeService implements EmployeeSaveUseCase {

    private final EmployeeRepository employeeRepository;

    @Override
    // @Transactional 
    public Employee save(
        String employeeName,
        String departmentName
    ) {
        Employee newEmployee = employeeRepository.save(new Employee(employeeName));
        Set<EmployeeDuty> employeeDuties = new HashSet<>();
        employeeDuties.add(new EmployeeDuty(
            newEmployee,
            DutyCode.ANALYSIS_MARKETING,
            LocalDateTime.now()
        ));
        newEmployee.register(employeeDuties, new Department(departmentName, newEmployee));
        return newEmployee;
    }
}

image





4. 정리


복합 키를 처음 사용해 보며 겪은 이슈로, insertable = false, updatable = false 설정하면 엔티티가 할당되더라도 저장되지 않습니다. 제가 실수한 부분을 돌아보기 위해, 비슷한 예외를 겪는 동료가 있다면 시간을 단축할 수 있도록 글을 작성했습니다. 이 글을 읽고 저처럼 많은 시간을 들이지 않고 문제를 잘 해결하셨으면 합니다.

테스트 코드가 있으면 이를 정말 빠르게 해결할 수 있는데요, 배포 전에 어떤 에러가 있는지 미리 확인할 수 있기 때문입니다. 이 부분은 어떻게 해결할 지 조금 더 고민을 해봐야 할 것 같습니다. 😭


This post is licensed under CC BY 4.0 by the author.

JPA 프록시 컬렉션

인프라 아키텍처 개선 후기