문제
@Slf4j
@Service
@RequiredArgsConstructor
public class DataInitService {
private final PasswordEncoder passwordEncoder;
private final JDBCRepository JDBCRepository;
private final MemberRepository memberRepository;
private final RoomRepository roomRepository;
public void initializeData() {
// Rooms 생성
List<Room> rooms = new ArrayList<>();
for (int i = 1; i <= 1; i++) {
Room room = Room.builder()
.id(String.valueOf(i))
.name("room" + i)
.memo("memo" + i)
.build();
rooms.add(room);
}
JDBCRepository.saveAllRooms(rooms);
rooms = roomRepository.findAll(); //엔티티 조회
// Places 생성
List<Place> places = new ArrayList<>();
for (int i = 1; i <= 1; i++) {
Place place = Place.builder()
.siDo("siDo" + i)
.siGunGu("siGunGu" + i)
.roadNameAddress("roadNameAddress" + i)
.addressLatitude(37.0 + i * 0.0001)
.addressLongitude(127.0 + i * 0.0001)
.room(rooms.get(i - 1))
.member(members.get(i - 1))
.googlePlaceId("googlePlaceId" + i)
.build();
places.add(place);
}
// entityManager.clear(); // room들이 변경감지되어 place를 저장하기 전에 clear
JDBCRepository.saveAllPlaces(places);
}
}
위 로직에서 Place를 저장할 때 LazyInitializationException이 발생했다.
분석해보니 Place의 연관관계 편의 메서드에서 아래와 같이 Room의 place들을 조회해오면서 발생하였다.
Room의 places들을 조회하면서 LazyLoading을 하려했으나 @Transactional이 없어 실패했다는 것이다.
해결 과정
1. Room들을 영속성 컨텍스트에서 detach하면 LazyInitializationException이 안발생하지 않을까?
@Slf4j
@Service
@RequiredArgsConstructor
public class DataInitService {
private final PasswordEncoder passwordEncoder;
private final JDBCRepository JDBCRepository;
private final MemberRepository memberRepository;
private final RoomRepository roomRepository;
@PersistenceContext
private final EntityManager entityManager;
@Transactional
public void initializeData() {
// Rooms 생성
List<Room> rooms = new ArrayList<>();
for (int i = 1; i <= 1; i++) {
Room room = Room.builder()
.id(String.valueOf(i))
.name("room" + i)
.memo("memo" + i)
.build();
rooms.add(room);
}
JDBCRepository.saveAllRooms(rooms);
rooms = roomRepository.findAll();
for (Room room : rooms) { // 조회해온 Room들을 detach
emtityManager.detach(room);
}
// Places 생성
List<Place> places = new ArrayList<>();
for (int i = 1; i <= 1; i++) {
Place place = Place.builder()
.siDo("siDo" + i)
.siGunGu("siGunGu" + i)
.roadNameAddress("roadNameAddress" + i)
.addressLatitude(37.0 + i * 0.0001)
.addressLongitude(127.0 + i * 0.0001)
.room(rooms.get(i - 1))
.member(members.get(i - 1))
.googlePlaceId("googlePlaceId" + i)
.build();
places.add(place);
}
JDBCRepository.saveAllPlaces(places);
}
}
내 개인적인 착각으로 LazyLoading은 영속성 컨텍스트에 있을 때만 작동하고, 아니면 동작하지 않는다고 생각했다.
하지만 LazyLoading은 엔티티를 생성할 때 프록시를 사용해 프록시 객체를 필드에 주입해주고,
실제 필드를 조회하게 될 때 프록시 객체의 코드가 실행되면서 조회해오는 방식이었다.
따라서 영속성 컨텍스트와 무관하게 LazyLoading은 항상 동작하게 된다.
LazyLoading과 LazyInitializationException에 대해서는 아래에 상세히 적어뒀습니다.
https://yooooonshine.tistory.com/50
LazyInitializationException과 LazyLoading의 동작원리
@Transactional이 붙지 않은 메서드 내에서 LazyLoading된 필드를 접근 시 하이버네이트가 LazyInitializationException을 발생시킨다.왜냐하면 Lazy Loading이 동작하려면 영속성 컨텍스트가 필요하지만, 이게 닫
yooooonshine.tistory.com
참고 - Bean으로 가져온 EntityManager와 트랜잭션의 EntityManager는 동일하다.
참고로 위 과정에서 Bean으로 가져온 EntityManager와 트랜잭션의 EntityManager는 동일합니다.
이에 대해서는 아래 글을 먼저 읽으시면 이해할 수 있습니다.
https://yooooonshine.tistory.com/57
즉 빈으로 등록된 EntityManager는 실제 EntityManager가 아닌 프록시 객체이며 이 프록시 객체는 TransactionSynchronizationManager를 통해 ThreadLocal에서 EntityManager가 있으면 찾아오며
트랜잭션 또한 TransactionSynchronizationManager를 통해 ThreadLocal에서 EntityManager가 있으면 찾아오기 때문에 동일합니다.
2. @Transactional을 붙여준다.
단순하게 LazyLoading에서 에러가 발생한 이유는 Room들의 연관관계를 가져오려는 데 영속성 컨텍스트가 닫혀있어 발생했기에 그냥 @Transactional을 붙여주면 됐다.
하지만 나는 Room들이 영속성 컨텍스트에서 관리되기를 원치 않았다.
첫 번째로는 데이터가 50만개나 되기에 영속성 컨텍스트에서 관리되면 메모리에 부담이 될 거라 생각했기 때문이었다.
두 번째로는 Room이 영속성 컨텍스트에서 관리되면, 연관관계 편의 메서드로 인해 Room은 Place에 대한 변경감지가 이뤄지는데, Place들을 저장할 때에는 JDBC를 사용해 저장하므로 영속성 컨텍스트에 저장되지 않는다.
이로 인해 트랜잭션이 끝날 때 Place의 변경감지를 처리해주는 과정에서 Place가 중복저장되어버린다.
따라서 다른 방법이 필요했다.
2. 저장할 때 아예 LazyLoading이 발생하지 않도록 하자.
영속성 컨텍스트와 무관하게 LazyLoading이 발생하므로 아예 연관관계를 접근하지 않도록 하면 됐다.
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Place {
@Id
@Column(name = "place_id")
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
...
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "room_id")
private Room room;
...
pubilc Place(String siDo, String siGunGu, String roadNameAddress, Double addressLatitude,
Double addressLongitude, Room room, Member member, String googlePlaceId) {
this.siDo = siDo;
this.siGunGu = siGunGu;
this.roadNameAddress = roadNameAddress;
this.addressLatitude = addressLatitude;
this.addressLongitude = addressLongitude;
addRoom(room);
this.member = member;
this.googlePlaceId = googlePlaceId;
}
private void addRoom(Room room) {
this.room = room;
room.getPlaces().add(this);
}
}
그럼 위에서 생성자에 addRoom(room)을 빼버리면 됐지만,
이러면 모든 서비스 로직을 고쳐야할 게 분명해졌다…
단순 더미 데이터 로직 때문에 서비스 코드를 변경하는 것은 옳지 않다고 생각이 들었다.
그래서 다른 방법을 생각했다.
// Places 생성
List<Place> places = new ArrayList<>();
for (int i = 1; i <= 1; i++) {
Place place = Place.builder()
.siDo("siDo" + i)
.siGunGu("siGunGu" + i)
.roadNameAddress("roadNameAddress" + i)
.addressLatitude(37.0 + i * 0.0001)
.addressLongitude(127.0 + i * 0.0001)
.room(rooms.get(i - 1))
.member(members.get(i - 1))
.googlePlaceId("googlePlaceId" + i)
.build();
places.add(place);
}
JDBCRepository.saveAllPlaces(places);
위에서 JDBCRepository의 saveAllPlaces 메서드는 사실 필드 값들만 필요하지 엔티티를 저장하는 JPA와는 달랐다.
따라서 Place들의 값들을 갖는 PlaceDTO를 넘겨줘도 무관했다.
List<PlaceDummyDto> places = new ArrayList<>();
for (int i = 1; i <= DummyDataConstant.PLACE_COUNT.count; i++) {
PlaceDummyDto place = new PlaceDummyDto(
siDo,
siGunGu,
roadNameAddress,
latitude,
longitude,
String.valueOf(i), // roomId
(long)i, // memberId
googlePlaceId
);
places.add(place);
}
jdbcRepository.saveAllPlaces(places);
따라서 위와 같이 아예 PlaceDTO로 넘겨주는 방식으로 변경했다.
느낀 점
- 추후에 Entity를 개발할 때 연관관계 편의 메서드는 생성자 안에 넣지 말아야겠다. 생성자는 순수 생성자가 되어야 예상치 못한 에러를 줄일 수 있을 것 같다.
'Spring' 카테고리의 다른 글
트랜잭션의 EntityManager 관리방법과 Bean으로 주입받는 EntityManger의 관리방식 (0) | 2025.02.22 |
---|---|
EntityManagerFactory, EntityManager, 영속성 컨텍스트와 영속성 컨텍스트의 특징 (0) | 2025.02.22 |
영속성 컨텍스트의 1차 캐시는 어떻게 저장될까? (0) | 2025.02.22 |
@Transactional 내에서가 아니여도 OSIV가 on이면 조회한 데이터들이 같은 영속성 컨텍스트를 공유하는 이유 (0) | 2025.02.22 |
@Profile과 @ActiveProfile (0) | 2025.02.20 |