EntityManager가 아직 살아 있어서 그런 거였고, 이는 Spring Boot의 OSIV=true 설정 덕분이라는 걸 알게 되었다.Transaction은
TransactionManager가 관리하는 것이고, EntityManager는 Transaction을 직접 시작하거나 커밋하거나 롤백하지 않는다. 대신EntityManager는 Transaction 관리를 위한 도구로서 사용된다. 그러므로 일반적으로EntityManager는 Transaction 범위 내에서 사용된다.
EntityTransaction tx = entityManager.getTransaction();
try {
tx.begin();
// ... 작업 수행 ...
tx.commit(); // 정상 종료
} catch (Exception e) {
tx.rollback(); // 예외 시 롤백
} finally {
entityManager.close(); // 직접 만든 경우 close 필수
}
EntityManager 과 트랜잭션은 생성 시점과 종료 시점이 원래는 다르다는 것을 명심하고 지나가자. (스프링에서@Transactional 을 붙이면 알아서 트랜잭션과 EntityManager 을 같이 관리해준다)spring.jpa.open-session-in-view=true 값이 기본적으로 활성화된다. 실제로 프로젝트를 실행할 때 콘솔을 보면 아래와 같이WARNING log을 보여준다.2025-04-17T16:32:13.174+09:00 WARN 1332 --- [ main]
JpaBaseConfiguration$JpaWebConfiguration : spring.jpa.open-in-view is enabled by default.
Therefore, database queries may be performed during view rendering.
Explicitly configure spring.jpa.open-in-view to disable this warning.

OSIV=true로 설정되면, 요청이 들어올 때 OpenEntityManagerInViewFilter가 동작한다. 다음은 코드 중 일부이다.try {
EntityManager em = createEntityManager();
EntityManagerHolder emHolder = new EntityManagerHolder(em);
TransactionSynchronizationManager.bindResource(emf, emHolder);
...
}
@Transactional 이 없어도 EntityManager 가 별도로 존재하기 때문에 컨트롤러나 뷰 렌더링 과정에서 Lazy 로딩 등을 수행할 수 있다!entityManager.find(Person.class, id))만 가능하며, 그 외 연산(flush, commit, rollback) 을 진행하면 예외가 발생한다.
JpaRepository를 상속받게 되면 알아서 트랜잭션 적용을 해주기 때문에 상관없지만, Service 단에서는 반드시 @Transactional 어노테이션을 사용해야 EntityManager 를 사용할 수 있다.OSIV=false인 경우, DB 접근 로직이 있는 곳에 반드시 @Transactional을 명시해야 한다. 안하면 LazyInitializationException, TransactionRequiredException 예외가 발생할 것이다.LazyInitializationException 을 해결하기 위해서는 서비스 계층에서 모든 DB 접근(조회/변경)을 끝내고 DTO로 변환해서 컨트롤러로 넘겨야 한다. (컨트롤러에서 지연로딩을 하려고 하면 예외가 발생함)TransactionRequiredException 의 경우 엔티티 쓰기 (persist, remove, merge) 를 해야 되는 상황에서 트랜잭션이 없기 때문에 발생하는 예외이다. 하지만 JpaRepository 를 상속한 Repository를 사용하면 삽입, 수정, 삭제 메서드에 트랜잭션이 걸려있어 예외가 발생하지 않는다. (EntityManager를 직접 사용할 경우 예외가 발생할 것이다)OSIV=false 하면 고생하네. 나는 항상 켜놔야지~SSE (Server Side Event) 란 서버가 클라이언트와 HTTP 연결을 끊지 않고 계속 유지하며 클라이언트가 요청을 하지 않아도 서버에서 정보를 제공해줄 수 있는 기능이다.
OSIV=true 로 EntityManager가 계속 살아있다보니 커넥션 고갈이 발생한 것이다.EntityManager는 내부적으로 데이터베이스 커넥션을 하나 점유하기 때문에, 커넥션 풀 크기에 따라 동시에 생성 가능한 EntityManager의 수가 제한되는 것이다!그래서 OSIV=false로 설정하여 EntityManager의 생명주기를 트랜잭션의 생명주기와 같게 만들어 문제를 해결하였다.
하지만 그 후 지연 로딩 예외가 발생하게 된다.
OSIV=false 로 두게 되면 EntityManager 은 트랜잭션 내에서만 생성된다. 그런데 트랜잭션이 필요한 서비스에서 @Transactional 을 사용하지 않은 것이 문제의 원인이었다. @Override
public List<Long> getMemberIds(String roomId) {
ChatRoom chatRoom = chatRoomRepository.findById(roomId);
List<ChatRoomMember> members = chatRoom.getMembers(); // 지연로딩을 하지 못함!!
return members.stream()
.map((chatRoomMember) -> chatRoomMember.getMember().getId())
.toList();
}
findById() 내에 트랜잭션이 적용되어 있어 정상적으로 찾아올 수 있다. 하지만 chatRoom을 반환되는 순간 더 이상 EntityManager 가 없기 때문에 chatRoom는 detached 상태가 된다. chatRoom.getMembers() 의 경우는 chatRoom이 detached 상태이기에 지연 로딩을 하지 못한다. 그래서 예외가 발생하게 된다.OSIV=false 로 설정했기 때문에, 서비스단에 @Transactional 을 작성해주었다면 즉시 로딩을 하지 않아도 해결할 수 있었던 문제였다.OSIV=false로 해야 한다.OSIV=true로 해도 큰 상관은 없을 것 같다.