상황
@Getter
@Entity
@AllArgsConstructor
@Table(name = "p_store")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Store extends BaseEntity {
.
.
@Enumerated(EnumType.STRING)
@Column(name = "status", nullable = false)
private StoreStatus status;
.
.
.
@OneToOne(mappedBy = "store", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private ReservationPolicy reservationPolicy;
}
@Getter
@Entity
@AllArgsConstructor
@Table(name = "p_reservation_policy")
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class ReservationPolicy extends BaseEntity {
.
.
.
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "store_id")
private Store store;
}
Store과 ReservationPolicy는 일대일 양방향 관계이다.
Store entity에서는 mappedBy=”store” 라고 정의함으로서 ReservationPolicy entity에서 store 변수에 연관관계를 맺고자 했다.
이렇게 하면 결과:
store 테이블에는 reservationPolicy_id 컬럼이 없고,
reservationPolicy 테이블에는 store_id가 있다.
문제는 여기서부터다.
Store의 status만 변경하는 api을 요청했는데 ReservationPolicy도 함께 조회가 된다.
@PatchMapping("/{storeId}/status")
public ResponseEntity<ApiResponse<Void>> updateStatus(
@PathVariable UUID storeId,
@RequestBody UpdateStatusRequest request
) {
// TODO: 추후 userRole 작업
String userRole = "OWNER";
// 음식점 상태 변경
storeService.updateStatus(userRole, storeId, request.toCommand());
return ResponseEntity.ok(
ApiResponse.success("음식점 상태 수정 성공", HttpStatus.OK)
);
}
@Transactional
public void updateStatus(String role, UUID storeId, UpdateStoreStatusCommand command) {
Store store = findStore(storeId);
StoreStatus newStatus = StoreStatus.valueOf(command.storeStatus());
StoreStatusTransitionStrategy strategy = strategyFactory.getStrategy(role);
strategy.changeStatus(store, newStatus); // store.chageStatus()
}
@Transactional(readOnly = true)
Store findStore(UUID storeId) { // 여기서 store 조회
return storeRepository.findById(storeId)
.orElseThrow(() -> new AppException(StoreErrorCode.STORE_NOT_FOUND));
}

fetch.LAZY 를 해줬는데도, store에 수정이 일어나면 그 하위 자식인 reservationPolicy도 조회된다. (hibernate select)
상태변경 controller, service에서 그 어디도 getReservationPolicy() 하는 함수는 없는데 왜 조회가 되는걸까?
원인? : 양방향 @OneToOne 에서 mappedBy 지정한 객체에서 fetch.LAZY 적용안됨
public class Store extends BaseEntity {
@OneToOne(mappedBy = "store", cascade = CascadeType.ALL, orphanRemoval = true, fetch = FetchType.LAZY)
private ReservationPolicy reservationPolicy;
public class ReservationPolicy extends BaseEntity {
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "store_id")
private Store store;
결론부터 말하면 @OneToOne 양방향 관계에서 mappedBy 사용한 데에서 fetch.LAZY 적용이 안된다. fetch.EAGER로 매번 하위 자식까지 다 조회를한다.
보통 양방향 OneToOne에서 외래키의 주인이 아닌 상대 entity에 mappedBy 설정을 해줘야한다.
외래키 주인이 아닌 Store entity에서 mappedBy = “store” 를 설정해준 상태이다.
그래서 Store가 수정될 때 ReservationPolicy도 조회가 됐다.
왜 안되는가
Hibernate가 지원하지 않음!!!

이 경고문을 좀 더 빨리 봤다면 삽질의 시간을 줄일 수 있지 않았을까. 엉엉.
@OneToOne 연결에서 소유하지 않는 측에 FetchType.LAZY를 지정하면 로드에 영향을 주지 않습니다. 관련 엔티티는 FetchType.EAGER가 정의된 것처럼 로드됩니다.
@OneToOne 에서 연관관계 주인이 아닌 객체에 mappedBy를 한다.
이 때 mappedBy를 한 객체(Store) 테이블에는 연관관계 주인 (ReservationPolicy) 에 대한 id 컬럼이 없다.
db 관점으로 보았을 때 연관관계 주인이 아닌 Store 테이블에서는 reservationPolicy_id 컬럼이 없어서 storeRepository.findById(storeId) 를 했을 때, Store 테이블만 읽어서 연관관계의 엔티티가 존재하는지 안하는지 확인하기 어렵다.
그래서 확인하기 위해 하위 자식도 조회하는게 아닐까 싶다. (내생각..)
정리
- mappedBy의 속성 값은 외래 키의 주인인 상대 Entity의 필드명을 의미한다.
- 양방향 @OneToOne 에서는 mappedBy 속성을 사용한 객체가 조회될 때, 그 객체와 연관된 객체도 함께 조회된다. (fetch.EAGER)
- 즉, 양방향 @OneToOne 에서 mappedBy 속성을 사용한 객체에 fetch.LAZY는 적용되지 않는다.
'Trouble Shooting' 카테고리의 다른 글
| 트랜잭션 커밋 지연 @TransactionalEventListener로 해결 (0) | 2025.11.23 |
|---|---|
| @Autowired 사용 후 "오토와이어링할 수 없습니다." 오류 해결 방법 (0) | 2025.11.23 |
| rebase merge된 develop 브랜치에서 작업중인 내 브랜치로 pull 하는 방법 (1) | 2025.11.10 |