여기는 @Transactional(readOnly = true) 가 빠져 있네요. 이런 경우 때문에 Query(조회성 메소드 만) / Command(CUD 성 메소드) 서비스를 분리해서 @transactional 을 달아주고는 합니다. 혹시, @Transactional(readOnly=true) 를 왜 사용하는지 아시나요?
@Transactional(readOnly = true)
public List<Accommodation> getAllDeletedAccommodation(Long hostId){
filterManger.enableFilter(FilterConstants.ACCOMMODATION_FILTER
, FilterConstants.FILTER_PARAM
,true);
List<Accommodation> byHostId = accommodationRepository.findByHostId(hostId);
filterManger.disableFilter(FilterConstants.ACCOMMODATION_FILTER);
return byHostId;
}
@Transactional(readOnly = true)를 쓸 때 조회 성능을 더 빠르게 해준다는 말을 듣고서 사용했는데 정확히 언제 사용하면 좋을지 공부했다.

스프링 @Transactional(readOnly=true)에 관한 간단한 고찰

부작용은 없을까?
Is @Transactional(readOnly=true) a silver bullet?
Spring READ 관련 API에서 @Transactional(readOnly = true)는 필수인가?
그렇지는 않은 거 같다.
@Transactional(readOnly = true)를 설정해두면, 그 메서드가 끝날 때까지 db 커넥션을 반납하지 않는다.
직접 테스트를 해봤다.
private final HikariDataSource dataSource;
(중략)
@Transactional(readOnly = true)
public List<Accommodation> getAllAccommodation(){
// filterManger.enableFilter(FilterConstants.ACCOMMODATION_FILTER
// , FilterConstants.FILTER_PARAM
// , false);
List<Accommodation> accommodations = accommodationRepository.findAll();
// filterManger.disableFilter(FilterConstants.ACCOMMODATION_FILTER);
timeSleepAndPrintConnection("트랜잭션 걸음");
return accommodations;
}
public List<Accommodation> getAllAccommodationWithout(){
// filterManger.enableFilter(FilterConstants.ACCOMMODATION_FILTER
// , FilterConstants.FILTER_PARAM
// , false);
List<Accommodation> accommodations = accommodationRepository.findAll();
//
// filterManger.disableFilter(FilterConstants.ACCOMMODATION_FILTER);
timeSleepAndPrintConnection("트랜잭션 안 걸음");
return accommodations;
}
private void timeSleepAndPrintConnection(String methodName) {
try {
HikariDataSource hikariDataSource = dataSource.unwrap(HikariDataSource.class);
if (hikariDataSource != null) {
printConnectionStatus(hikariDataSource, "start " + methodName + "!!");
for (int i = 0; i < 5; i++) {
Thread.sleep(1000);
printConnectionStatus(hikariDataSource, null);
}
printConnectionStatus(hikariDataSource, "end " + methodName + "!!");
} else {
System.out.println("DataSource is not an instance of HikariDataSource");
}
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
} catch (SQLException e) {
System.out.println("Failed to unwrap HikariDataSource: " + e.getMessage());
}
}
private void printConnectionStatus(HikariDataSource hikariDataSource, String message) {
if (message != null) {
System.out.println(message);
}
System.out.println("activeConnections:" + hikariDataSource.getHikariPoolMXBean().getActiveConnections() +
", IdleConnections:" + hikariDataSource.getHikariPoolMXBean().getIdleConnections() +
", TotalConnections:" + hikariDataSource.getHikariPoolMXBean().getTotalConnections());
}

실제로 Transaction(readonly =true)가 걸려 있는 메서드에서는 커넥션이 반납되지 않고 계속 쓰였다.
근데 Transaction(readonly =true)을 안 걸어도 커넥션이 반환이 계속 안됐다.

왜 이런 결과가 나오는지는 모르겠다... open-in-view를 false로 해놓으면 된다는데...
공부한 걸 정리해보면
트랜잭션에서 커넥션이 반환되는 시점은 commit이후다.
@Transactional(readOnly = true) 옵션을 사용하면 해당 조회 메서드가 모두 종료된 이후 DB Connection이 끊어지게 된다는 뜻이고 반대로 해석하면 DB 조회 이후 Connection이 사용되지 않는 상황에서도 자원이 반납되지 않는다는 것이다.
Spring Boot의 open-in-view, 그 위험성에 대하여.
공부하다 이 글도 읽어봤다.

이런 상황이었다고 한다.

원인은 JPA의 open-in-view라고 한다.
open-in-view를 true로 해두면, api 의 요청부터 응답까지 영속성 컨텍스트가 유지된다.
spring boot에서는 open-in-view 속성의 default값이 true이기 때문에, 트랜잭션이 끝나도 DB connection이 반납되지 않는다고 한다.

db 커넥션이 반환되지 않았던 건 lazy loading 때문인거 같아서 타입을 EARGER로 바꿨는데도 커넥션이 반환이 안 됐다....
OSIV는 영속성 컨텍스트를 뷰까지 열어두는 기능을 말한다. 영속성 컨텍스트가 유지되면 엔티티도 영속 상태로 유지된다.
! JPA에서는 OEIV(Open EntityManager In View), 하이버네이트에선 OSIV(Open Session In View)라고 한다. 하지만 관례상 둘 다 OSIV로 부른다.

서비스 계층에서 트랜잭션이 끝나면 컨트롤러와 뷰에는 트랜잭션이 유지되지 않는다. 엔티티를 변경하지 않고 단순히 조회만 할 때는 트랜잭션이 없어도 동작하는데, 이것을 트랜잭션 없이 읽기(Nontransactional reads)라 한다. 하여 만약 프록시를 뷰 렌더링하는 과정에 초기화(Lazy loading)가 일어나게 되어도 조회 기능이므로 트랜잭션이 없이 읽기가 가능하다.
만약 트랜잭션 범위 밖인 컨트롤러와 뷰에서 엔티티를 수정하여도 영속성 컨텍스트의 변경 감지에 의한 데이터 수정이 다음 2가지 이유로 동작하지 않는다.

다만, OSIV를 쓸 때는 주의해야할 점이 있다고 한다. OSIV 전략은 트랜잭션과 마찬가지로 최초 데이터베이스 커넥션 시점부터 API응답이 끝날 때까지 영속성 컨텍스트와 데이터 베이스 커넥션을 유지한다.
그런데 이 전략은 너무 오랜시간동안 데이터베이스 커넥션 리소스를 사용하기 때문에, 실시간 트래픽이 중요한 애플리케이션에서는 커넥션이 모자랄 수 있다. 이것은 결국 장애로 이어진다.
그래서 OSIV를 끄면 트랜잭션 종료시 영속성 컨텍스트를 닫고, 데이터베이스 커넥션도 반환한다. 따라서 커넥션 리소스를 낭비하지 않는다.
대신 OSIV를 끄면 모든 지연로딩을 트랜잭션 안에서 처리해야 한다. 따라서 지금까지 작성한 많은 지연 로딩 코드를 트랜잭션 안으로 넣어야 하는 단점이 있다. 그리고 view template에서 지연로딩이 동작하지 않는다.
결론적으로 트랜잭션이 끝나기 전에 지연 로딩을 강제로 호출해 두어야 한다.
커멘드와 쿼리 분리
실무에서 OSIV를 끈 상태로 복잡성을 관리하는 좋은 방법이 있다. 바로 Command와 Query를 분리하는것이다.
보통 비즈니스 로직은 특정 엔티티 몇 개를 등록하거나 수정하는 것이므로 성능이 크게 문제가 되지 않는다. 그런데 복잡한 화면을 출력하기 위한 쿼리는 화면에 맞추어 성능을 최적화 하는 것이 중요하다. 하지만 그 복잡성에 비해 핵심 비즈니스에 큰 영향을 주는 것은 아니다.
그래서 크고 복잡한 애플리케이션을 개발한다면, 이 둘의 관심사를 명확하게 분리하는 선택은 유지보수 관점에서 충분히 의미 있다.
단순하게 설명해서 다음처럼 분리하는 것이다.
OrderService
OrderService: 핵심 비즈니스 로직
OrderQueryService: 화면이나 API에 맞춘 서비스 (주로 읽기 전용 트랜잭션 사용)
@Service
public class OrderQueryService {
@Autowired
private OrderRepository orderRepository;
@Transactional(readOnly = true)
public List<Order> getAllOrdersWithItems() {
return orderRepository.findAllWithItems();
}
}
보통 서비스 계층에서 트랜잭션을 유지한다. 두 서비스 모두 트랜잭션을 유지하면서 지연 로딩을 사용할 수있다.
@Transactional 을 붙이게 되면 해당 클래스내의 메소드에 Transaction.start() 와 commit() 을 각 시작점과 종단에 붙여주게 되는데요. 지금 상황에서는 Transaction.start() 를 하게 되고, 데이터 베이스와는 무관한 socialLoginApiService 를 통해 외부 서버에 통신을 요청하고 있는 상태입니다.
RDB 는 기본적으로 쓰기를 요청할때 lock 을 걸고 사용할 수 있으므로 트랜잭션이 길어지면 길어질수록 데드락, 혹은 장애 등등의 요인이 될수 있습니다. 지금 단계에서 엄청 필요한 고민은 아니지만, 한번 정도 어떤 문제가 생기는지 고민해보시고 공부해보셔도 좋을거 같아요 :).