포스트

2021-05-27-TIL

2021-05-27-TIL

쿼리별 메서드 오버로딩? vs 동적 쿼리 생성

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public List<Room> findByFilter(String location) {
        return jdbcTemplate.query("SELECT * FROM room WHERE location = ?", roomRowMapper(), location);
    }

    public List<Room> findByFilter(String location, String checkIn, String checkOut) {
        return jdbcTemplate.query("SELECT * FROM room WHERE location = ? AND id NOT IN (SELECT room_id FROM reserve WHERE check_in < ? AND check_out > ?)", roomRowMapper(), location, checkOut, checkIn);
    }

    public List<Room> findByFilter(String location, String checkIn, String checkOut, Integer min, Integer max) {
        return jdbcTemplate.query("SELECT * FROM room WHERE location = ? AND id NOT IN (SELECT room_id FROM reserve WHERE check_in < ? AND check_out > ?) AND (charge BETWEEN ? AND ?)", roomRowMapper(), location, checkIn, checkOut, min, max);
    }

    public List<Room> findByFilter(String location, String checkIn, String checkOut, Integer min, Integer max, Integer guests) {
        return jdbcTemplate.query("SELECT * FROM room WHERE location = ? AND id NOT IN (SELECT room_id FROM reserve WHERE check_in < ? AND check_out > ?) AND (charge BETWEEN ? AND ?) AND guests >= ?", roomRowMapper(), location, checkIn, checkOut, min, max, guests);
    }

필터 조합마다 findByFilter를 오버로딩하면 조건이 하나 늘 때마다 메서드가 늘어난다. 조건이 n개이고 각각 있을 수도 없을 수도 있다면 조합은 최대 2^n개다. 위 코드는 조건이 앞에서부터 차례로 추가된다고 가정해서 네 개로 끝났지만, 위치 없이 가격만 거는 검색이 생기면 바로 깨진다.

오버로딩 방식에는 실수하기 쉬운 지점도 보인다. SQL의 조건은 모두 check_in < ? AND check_out > ? 순서인데, 두 번째 메서드는 checkOut, checkIn 순서로 값을 넘기고 세 번째와 네 번째 메서드는 checkIn, checkOut 순서로 넘긴다. 날짜 겹침 조건으로 보면 첫 번째 ?에는 요청 체크아웃, 두 번째 ?에는 요청 체크인이 들어가야 하므로 세 번째와 네 번째 메서드의 인자 순서가 뒤바뀌어 있다. 같은 SQL 조각이 여러 메서드에 복사되어 있으면 이런 차이가 눈에 잘 띄지 않는다.

동적 쿼리는 값이 들어온 조건만 SQL에 붙인다. NamedParameterJdbcTemplate과 MapSqlParameterSource를 쓰면 ?의 순서 대신 이름으로 값을 묶을 수 있어서, 위와 같은 순서 실수가 생기지 않는다(Spring 문서).

1
2
3
4
5
6
7
8
9
10
11
12
13
14
StringBuilder sql = new StringBuilder("SELECT * FROM room WHERE 1 = 1");
MapSqlParameterSource params = new MapSqlParameterSource();

if (location != null) {
    sql.append(" AND location = :location");
    params.addValue("location", location);
}
if (checkIn != null && checkOut != null) {
    sql.append(" AND id NOT IN (SELECT room_id FROM reserve WHERE check_in < :checkOut AND check_out > :checkIn)");
    params.addValue("checkIn", checkIn).addValue("checkOut", checkOut);
}
// 가격, 인원 조건도 같은 방식으로 추가

return namedParameterJdbcTemplate.query(sql.toString(), params, roomRowMapper());

값은 끝까지 파라미터로 바인딩하고, SQL 문자열에는 고정된 조각만 붙인다. 사용자 입력을 문자열로 이어 붙이면 SQL 인젝션이 생긴다. 조건이 더 복잡해지면 MyBatis의 동적 SQL이나 Querydsl 같은 도구를 검토할 만하다.


GRASP 패턴

  • https://nesoy.github.io/articles/2019-05/GRASP-Pattern

GRASP(General Responsibility Assignment Software Patterns)는 Craig Larman이 “Applying UML and Patterns”에서 정리한, 객체에 책임을 나누는 기준이다. Information Expert(정보를 가진 객체에 책임을 준다), Creator, Controller, Low Coupling, High Cohesion, Polymorphism, Pure Fabrication, Indirection, Protected Variations의 아홉 가지로 이루어진다. 위 고민에 대입하면, 검색 조건을 조립하는 책임을 리포지토리 메서드 여러 개에 흩어 두기보다 검색 조건 객체 하나가 갖게 하는 쪽이 Information Expert와 High Cohesion에 가깝다.


RID

  • https://blog.sqlauthority.com/2020/01/25/sql-server-heaps-scans-and-rid-lookup/#:~:text=RID%20Lookup%3A%20A%20RID%20Lookup,data%20in%20the%20heap%20table.

SQL Server에서 클러스터드 인덱스가 없는 테이블을 힙(heap)이라고 한다. 힙의 각 행은 파일 번호, 데이터 페이지 번호, 페이지 안의 슬롯 번호로 이루어진 8바이트 행 식별자 RID(Row Identifier)로 찾는다(Microsoft 문서). 비클러스터드 인덱스로 조건에 맞는 행을 찾은 뒤, 인덱스에 없는 컬럼을 읽으려고 RID로 힙의 행을 다시 찾아가는 것이 RID Lookup이다. 실행 계획에서는 항상 Nested Loops와 함께 나타난다(실행 계획 연산자 문서). 찾는 행이 많으면 행마다 랜덤 I/O가 생기므로, 자주 읽는 컬럼을 인덱스에 포함하면 이 단계를 없앨 수 있다.


Non-Repeatable Read vs Phantom Read

  • https://stackoverflow.com/questions/11043712/what-is-the-difference-between-non-repeatable-read-and-phantom-read
  • https://postgresql.kr/blog/pg_phantom_read.html

PostgreSQL 문서의 정의로 두 현상을 구분하면 다음과 같다.

  • Non-Repeatable Read: 이미 읽은 행을 다시 읽었더니, 그 사이 커밋한 다른 트랜잭션 때문에 값이 바뀌어 있다. 같은 행의 내용이 달라지는 문제다.
  • Phantom Read: 같은 조건으로 다시 조회했더니, 그 사이 커밋한 다른 트랜잭션 때문에 조건을 만족하는 행의 집합이 달라져 있다. 행이 새로 생기거나 사라지는 문제다.

SQL 표준은 Repeatable Read 격리 수준에서 Non-Repeatable Read는 막고 Phantom Read는 허용한다. PostgreSQL의 Repeatable Read는 표준보다 강해서 Phantom Read도 생기지 않는다. 표준은 각 수준에서 일어나면 안 되는 현상만 정하므로 더 강한 보장은 허용된다. 위의 예약 검색처럼 “조건에 맞는 행이 없음”을 확인하고 예약을 넣는 로직은 Phantom Read의 영향을 받는 대표적인 경우다.

참고한 자료외부 출처 5

외부 출처

이 글은 저작권자의 CC BY 4.0 라이선스를 따릅니다.

댓글

아직 댓글이 없습니다