코드를 짜다 고민이 생겼다.
아래 엔티티는 사업체 신청서 라는 엔티티다. 어떤 유저가 사업주 신청을 할 때 필요한 엔티티다.
이때 이 사업주는 A라는 필드에서도, B라는 필드에서도 다양하게 사업체를 폭넓게 영위하고 있다.
(예를 들어, 인테리어 업체라고 하면 아파트 인테리어도 하고, 가게 인테리어도 하면 아파트, 가게를 field로 가지는 것이다)
@Entity
@AllArgsConstructor
@NoArgsConstructor
@Getter
@Builder
public class BusinessApplication extends BaseTimeEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "user_id")
private User user;
@Enumerated(EnumType.STRING)
private Status status;
private String businessNumber;
private String businessName;
private String phoneNumber;
@OneToMany
private List<BusinessFieldMap> fields;
}
그러면 이 사업체 신청이라는 엔티티와 필드 엔티티는 N:M관계를 가지게 된다.
왜냐하면 사업체 신청 엔티티에는 당연히 여러 필드가 들어가니 1:N관계.(BusinessApplication->Field 1:N관계)
필드 엔티티 입장에서도 하나의 필드가 여러개의 사업체 신청에 의해 선책될 수 있다.(Field<- BusinessApplication N:1관계)
그래서 이 다대다 관계를 풀기 위한 연결테이블이 필요하다.
(논리적으로는 외래관계를 맺지만 실질적으로는 외래관계를 맺지않도록 db를 구성하려고 NO-CONSTRAINT 를 걸었다)
@AllArgsConstructor
@NoArgsConstructor
@Data
@Builder
@Entity
public class BusinessFieldMap {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "apply_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
private BusinessApplication businessApplication;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "field_id", foreignKey = @ForeignKey(ConstraintMode.NO_CONSTRAINT))
private Field field;
}
이러한 엔티티 구조에서는 유저가 사업주 신청을 할 때 BusinessApplication 객체를 만드는 것 뿐만 아니라,
BusinessFieldMap 객체도 만들어줘야한다.
그런데 BusinessFieldMap 클래스를 보면 Field객체가 또 필요하다.
그러면 아래 사업주 신청 코드에서 비효율이 생긴다.
//1.비즈니스 멤버 신청
public Long applyBusiness(BusinessApplyReqDto dto){
BusinessApplication application = businessRepository.save(dto.toEntityFromApplyReqDto(user));
for(Long a :dto.getFields()){
Field field = fieldRepository.findById(dto.getFieldId).orElseThrow~~//이 부분!!!
BusinessFieldMap map = BusinessFieldMap.builder().field(field).businessApplication(application).build();
businessFieldMapRepository.save(map);
}
return application.getId();
}
한 유저가 사업주신청을 했는데 아파트,가게 라는 field의 id값을 dto에 넘겨 전달했다면,
백엔드에서는 businessApplication을 db에 인서트하기 위해서는 또 fieldRepository에서 field를 db에서 조회해와야한다.
그렇다면, 유저가 3~4개의 field를 선택했다면 3~4번 조회쿼리가 발생한다.
이때 필요한 것이 JPA에서 프록시 객체다.
프록시 객체란?
JPA에서 실제 엔티티의 ID 값만 가지고 있으며, 실제 엔티티 객체를 대신하는 '대리인' 역할을 하는 객체다.
주로, 지연로딩(Lazy Loading)을 구현하기 위해 사용된다.
JPA에서는 연관 관계를 설정할 때, 실제 객체 대신 프록시 객체를 주입한다. 이 프록시 객체는 실제 엔티티의 ID값만 가지고 있는데
이 시점에서는 db에 쿼리를 발생시키지 않는다,.
그런데 이 프록시 객체의 ID를 제외한 다른 필드(ex.getName() 등등)에 접근하려 할 때 이 프록시 객체가 그제서야 db에 select쿼리를 날려 데이터를 가져와 ID를 제외한 나머지 필드의 데이터를 로딩한다. 이것이 JPA에서 Lazy Loading이 구현되는 방식이다.
이 프록시 객체는 EntityManagere.getReference() 메서드로 수동으로 얻을 수 있다.
위에서 field객체를 주입하기 위해 레포지토리에 id값을 넘겨주어 실제 객체를 찾아오는 것과 달리
Field field = entityManger.getReference(Field.class,fieldId); 이렇게 메서드를 호출시키면 field의 프록시객체가 만들어지고 이를 BusinessFieldMap을 저장할 때 이 프록시객체를 사용하면 따로 field에 대한 select쿼리를 발생시키지 않아도 되는 것이다.
// 1.비즈니스 멤버 신청
public Long applyBusiness(BusinessApplyReqDto dto){
BusinessApplication application = businessRepository.save(dto.toEntityFromApplyReqDto(user));
for(Long a :dto.getFields()){
Field field = entityManager.getReference(Field.class,a);
BusinessFieldMap map = BusinessFieldMap.builder().field(field).businessApplication(application).build();
businessFieldMapRepository.save(map);
}
return application.getId();
}
이때 당연히 클래스 차원에서 EntityManager 의존성을 주입해야한다.
'스프링부트 > JPA' 카테고리의 다른 글
| [Spring] QueryDSL을 통해 DTO로 바로 리턴 받기, 리스트 매핑받기 (4) | 2025.08.31 |
|---|---|
| [Spring] Query DSL 문법 정리 (1) | 2025.08.26 |
| [Spring] MyBatis 사용법 - JPA와 차이점을 중심으로 (4) | 2025.07.30 |
| [Spring] JPA에서 양방향 관계를 지양해야 하는 이유 (5) | 2025.07.12 |
| [Spring] JPA환경에서 검색 및 필터 기능 구현 2 - QueryDSL 방법 이용 (2) | 2025.06.30 |