Spring Boot JPA를 사용하던 중 실제로 어떤 쿼리가 실행되는지 문뜩 궁금해졌습니다. 그래서 MySQL에서 수행되는 실제 쿼리를 살펴봤는데요, 이번 포스팅을 통해 공유해보고자 합니다.
MySql 쿼리 실행 기록
MySql은 쿼리의 실행 기록을 확인할 수 있는 방법이 있습니다. 아래와 같은 쿼리를 실행시키면 general_log 값을 알 수 있습니다. general_log가 OFF일 경우 쿼리 실행 기록이 남지 않습니다. 만약 general_log가 ON이라면 쿼리가 실행될 때마다 general_log_file에 쿼리 실행 이력이 저장됩니다.
그럼 general_log를 ON으로 설정하고 쿼리의 실행 이력이 어떻게 저장되는지 살펴보겠습니다.
※ 주의 ) general_log를 ON 값으로 설정하고 쿼리를 실행시키면 local file system에 쌓이는 로그의 크기가 기하급수적으로 늘어날 수 있습니다. 따라서 general_log를 ON 하고 필요한 작업이 완료되면 OFF 값으로 되돌리는 것이 권고됩니다.
쿼리가 실행될 때 로그에 어떻게 쌓이는지 확인해보기 위해 다음과 같은 명령어로 실시간으로 쌓이는 로그를 확인해보겠습니다.
tail -f /opt/homebrew/var/mysql/AL01983658.log
다음과 같은 쿼리를 실행하면 로그는 다음과 같이 쌓입니다.
쿼리 실행 이력 select * from menu 가 기록되는 것을 확인할 수 있습니다. 추가로 저는 Datagrip이라는 DBMS 툴을 사용 중인데요, Datagrip에서 limit 없이 select문을 수행하면 자동으로 Datagrip이 limit을 추가하는 걸 확인할 수 있습니다. 데이터베이스에서 로그를 확인할 수 있는 방법에 대해 알아봤으니 spring boot JPA에 의해 MySQL에서 실행되는 쿼리가 어떤지 살펴보겠습니다.
Spring Boot JPA는 어떤 쿼리가 실행될까?
사용하는 코드는 다음 링크에서 확인할 수 있습니다.
https://github.com/seonwoo960000/spring-boot-jpa-mysql-log-query-example
Spring Boot JPA를 통해 사용되는 쿼리는 어떻게 실행되는지 살펴보겠습니다. Entity와 관련 Repository는 다음과 같이 설정했습니다.
@Getter
@Builder
@Entity(name = "member")
@AllArgsConstructor
@NoArgsConstructor
public class MemberEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private long pid;
@Column(nullable = false, unique = true, length = 30)
private String username;
@Column(nullable = false, length = 100)
private String name;
}
public interface MemberRepository extends JpaRepository<MemberEntity, Long> {
}
이를 편리하게 접근할 수 있게 Controller도 생성하겠습니다.
@RestController
@RequiredArgsConstructor
@RequestMapping("api")
public class MemberController {
private final MemberRepository memberRepository;
@GetMapping("members")
public List<MemberEntity> findAllMembers() {
return memberRepository.findAll();
}
@GetMapping("members/{id}")
public MemberEntity findById(@PathVariable Long id) {
return memberRepository.findById(id)
.orElse(null);
}
@PostMapping("members")
public MemberEntity save(@RequestParam String username,
@RequestParam String name) {
return memberRepository.save(MemberEntity.builder()
.username(username)
.name(name)
.build());
}
}
그럼 위의 controller endpoint를 호출했을 때 각각 어떤 MySQL 쿼리가 호출되는지 살펴보겠습니다.
findAll()
JPA에서 자동으로 설정해주는 몇 가지 특징을 살펴볼 수 있습니다. 먼저 transaction read only를 실행함으로써 해당 세션의 transaction이 read only임을 알립니다. 그다음 autocommit을 비활성화하고 요청된 쿼리문을 실행 후 직접 commit을 실행시킵니다.
findById(id)
findAll()과 거의 유사하고 id를 통한 검색 조건만 추가됐습니다.
save(MemberEntity)
findAll() 또는 findById()와 다르게 transaction의 read only를 설정하지 않는 것을 볼 수 있습니다.
saveWithDifferentIsolationLevel(name, username)
이번에는 JPA에서 자동으로 생성해주는 메서드가 아닌 제가 직접 쿼리를 생성해서 사용해보겠습니다.
@Transactional(isolation = Isolation.READ_UNCOMMITTED)
@Modifying
@Query(value = "insert into Member (name, username) values (:name, :username)",
nativeQuery = true)
int saveWithDifferentIsolationLevel(@Param("name") String name,
@Param("username") String username);
위 메서드는 isolation_level을 READ_UNCOMMITTED로 설정해서 쿼리를 실행시킵니다. 해당 메서드를 수행하면 MySql은 다음과 같은 로그가 생성됩니다.
MySql은 쿼리 수행 전 해당 세션의 isolation level을 READ UNCOMMITTED로 수정하는 것을 확인할 수 있습니다.
결론
위의 로깅 기능을 활용하면 Spring Boot 뿐만 아니라 다른 프레임워크를 사용해서 데이터베이스에 쿼리를 날릴 때 실제 쿼리가 어떻게 동작하는지 살펴볼 수 있습니다. 하지만 로그의 크기가 커질 수 있음에 주의해서 사용해야 할 것 같습니다.
'Java > Spring Boot' 카테고리의 다른 글
[Spring Boot] Spring Batch Partitioning을 통해 Step을 분리해보자 (0) | 2023.07.08 |
---|---|
[Spring Boot] CRUD Project Template (0) | 2022.11.17 |
[Spring Boot] @Transactional이 COMMIT을 실행하는 코드까지 (0) | 2022.08.06 |
[Spring Boot] refresh context 차근차근 따라가기 (0) | 2022.05.11 |
[Spring Boot] Bean이 살고있는 집 ApplicationContext 기능 살펴보기 (0) | 2022.05.05 |