SQL Server JDBC Driver 를 사용하면서 배운, 그리고 주의해야 할 특징

안녕하세요. 비즈상품개발팀에서 광고시스템을 개발하고 있는 김승영입니다.

현재 광고시스템의 일부분이 Spring Boot / JPA 기반에 SQL Server JDBC Driver 를 사용해서 운영하고 있는데요. 그 SQL Server JDBC Driver 를 사용하면서 배운, 주의해야 할 1가지 특징을 공유하고자 합니다.

결론 먼저

결론부터 말씀드리면, 이 글의 핵심 내용은 아래와 같습니다.

  • SQL Server JDBC Driver 는 String type 의 pamameter 를 기본적으로 NVARCHAR 로 매핑한다는 사실을 반드시 기억하고 사용하자
  • 그렇기 때문에, String type 의 parameter 를 기본 VARCHAR 로 매핑하고 싶다면 또는 해야한다면, JDBC URL 에 sendStringParametersAsUnicode=false 를 추가하자
  • Hibernate / JPA 기반이라면 NVARCHAR 컬럼을 매핑한 필드에 @Nationalized를 사용하자

문제점

위에서 먼저 언급한 것처럼, SQL Server JDBC Driver 는 String 파라미터를 모두 NVARCHAR 로 매핑합니다. 심지어 PreparedStatement.set 호출시 명시적으로 VARCHAR 로 매핑하라고 지정해도 NVARCHAR 로 매핑합니다.

/*************************************************************** 
  sendStringParametersAsUnicode=true 일 경우
  BookRepository.findBooksByBookType(bookType) 실행시 수행되는 쿼리 
****************************************************************/
(@P0 nvarchar(4000))
select book0_.id as id1_0_, book0_.author as author2_0_, book0_.book_type as book_typ3_0_, book0_.created_at as created_4_0_, book0_.title as title5_0_ 
  from books book0_ 
 where book0_.book_type=@P0

저는 처음에 형변환도 알아서 잘 되고, 조회도 잘 되는데, 뭐가 문제지? 큰 문제인가? 라고 생각했었는데요.

이러한 특징 때문에 VARCHAR 컬럼에 대해 조회 조건을 사용할 경우, 해당 컬럼에 대한 INDEX 가 무시됩니다. 이로 인해 성능 저하가 발생합니다. 규모가 큰 서비스일수록 성능 저하의 영향도 크게 받게 되는 것이죠.

형변환과 조회 모두 잘 동작하는데, 갑자기 INDEX 는 왜 무시되는걸까요?

지나가다 만나면 반가움을 주먹으로 표현하는 ㅋㅋ 다정한 DBA 님의 명쾌한 설명으로 대신 할께요.

MSSQL은 DataType 에 대한 우선순위 가 있습니다. 이 우선순위에 따라 우선순위가 높은 쪽으로 묵시적 형변환이 일어나게 되는데요. 때문에 이 문제가 발생하게 됩니다.

NVARCHAR 가 VARCHAR 보다 우선순위가 높기 때문에 INDEX 가 있는 VARCHAR 컬럼에 NVARCHAR Parameter 가 매핑된 경우, 우선순위에 따라 VARCHAR 컬럼이 모두 NVARCHAR 로 형변환이 일어난 후 조건을 비교하게 됩니다. 이렇기 때문에 INDEX가 무시가 되는 것이구요.

여기서 중요한 것은 VARCHAR 컬럼이 모두 NVARCHAR 로 형변환 이 일어난다는 것입니다.

예를 들어, 위의 예제 코드처럼 Book 이라는 엔티티의 bookType 이라는 필드 (VARCHAR 컬럼) 를 조회조건으로 사용한다고 생각해보겠습니다. where 절을 만들기 위한 string 타입의 파라미터는 SQL Server JDBC Driver 에 의해 NVARCHAR 로 매핑되어 쿼리를 보내게 됩니다. 그럼 VARCHAR 컬럼의 bookType 데이터들과 조회하려는 NVARCHAR 파라미터와 비교를 하는데, 이 때 우선순위에 때라 VARCHAR 컬럼의 모든 bookType 들이 전부 NVARCHAR 로 형변환이 일어난다는 것입니다. 형변환이 일어나기 때문에 원래 설정된 INDEX 도 무시되고, 데이터가 많을수록 그만큼 비용도 많이 들기 때문에 성능도 떨어지는 것입니다.

해결방법

그럼 이 문제를 어떻게 해야할까요?

아래와 같이 JDBC URL 에 sendStringParametersAsUnicode=false 를 추가합니다.

spring:
  datasource:
    type: org.apache.tomcat.jdbc.pool.DataSource
    driverClassName: com.microsoft.sqlserver.jdbc.SQLServerDriver
    url: jdbc:sqlserver://rubyhost:6436;database=SbSvc;sendStringParametersAsUnicode=false

이 설정 덕분에 앞으로 모든 String 파라미터를 모두 VARCHAR 타입으로 매핑하게 됩니다.

/*************************************************************** 
  sendStringParametersAsUnicode=false 일 경우
  BookRepository.findBooksByBookType(bookType) 실행시 수행되는 쿼리 
*****************************************************************/
(@P0 varchar(8000))
select book0_.id as id1_0_, book0_.author as author2_0_, book0_.book_type as book_typ3_0_, book0_.created_at as created_4_0_, book0_.title as title5_0_ 
  from books book0_ 
 where book0_.book_type=@P0

그런데 말입니다.. 또 다시 문제가 발생합니다. NVARCHAR 로 조회해야하는 경우는 어떻게 해야할까요?

하지만 이 경우는 이전의 문제만큼 INDEX 가 무시되거나 성능 저하가 발생하는 등 크게 문제가 되지 않습니다.

이 또한 우선순위와 관련 있는데요. 다시 DBA 님의 설명을 들어볼께요.

반대로 INDEX 가 NVARCHAR 인 컬럼에 VARCHAR 조건이 매핑된 경우라면, 우선순위에 따라 조건으로 들어온 Parameter 가 형변환이 일어나기 때문에 INDEX 에는 영향을 주지 않게 됩니다.

즉, NVARCHAR 의 경우에는 컬럼들이 모두 형변환이 일어나는 것이 아니라 파라미터만 VARCHAR 에서 NVARCHAR 로 형변환이 일어난 후 조건을 비교하기 때문에, INDEX 도 유지되고 성능에도 큰 문제가 되지 않는 것입니다.

보너스

물론 NVARCHAR 로 조회할 경우에 대한 문제의 해결 방법이 없는 것은 아닙니다.

JDBC API 와 Spring 같은 프레임워크에서 NVARCHAR 로 매핑할 수 있는 방법을 제공하고 있는데요.

많이 사용할 것으로 생각되는 들어본 적이 있는 것들 3가지 경우에 대해 말씀드리겠습니다.

  1. Hibernate / JPA / JPQL

    • NVARCHAR 컬럼을 매핑한 필드에 @Nationalized 을 사용하면, 해당 컬럼의 조회 조건만 NVARCHAR 로 쿼리가 만들어지고 나머지 컬럼들은 VARCHAR 로 매핑 됩니다. 저희 팀에서는 이렇게 사용하고 있습니다.

      @Nationalized
      @Column(name = "Contract_Nm", length = 50)
      private String contractName;
      
    • JPQL, Querydsl-jpa 모두 이 규칙을 따릅니다.
    • 단, EntityManager.createNativeQuery 는 이 규칙을 따르지 않습니다. 따라서 NVARCHAR 매핑시 EntityManager.createNativeQuery 나 @Query(native=true) 를 사용할 경우에는 CONVERT(NVARCHAR, ?) 를 사용해서 convert 해줍니다. 또는 JdbcTemplate 을 직접 사용합니다.
  2. jdbcTemplate / JDBC

    • Spring 의 JdbcTemplate과 JDBC 표준 API는 PreparedStatement.setNString(param) 을 사용합니다.

      jdbcTemplate.query("select * from sqlserver_with_java.dbo.books where book_type = ? and title like ?",
              ps -> {
                  ps.setString(1, bookType.name()); // VARCHAR 매핑
                  ps.setNString(2, title); // NVARCHAR 매핑
              },
              rs -> {
                  log.info("id: {} , title: {}, bookType: {}", rs.getLong("id"), rs.getString("title"), rs.getString("book_type"));
              });
      
  3. MyBatis

    • #{param,jdbcType=NVARCHAR} 를 사용합니다.

      <select id="selectBooksByAuthorAndTitleLike" resultType="in.woowahan.bizgoods.sqlserverwithjava.jpa.Book">
          <![CDATA[
            SELECT * FROM books WHERE author = #{author} AND title LIKE #{title,jdbcType=NVARCHAR}
          ]]>
      </select>
      

다시 결론

SQL Server JDBC Driver 에 대해 위와 같은 특징을 모르고 사용 중이신 분들, 또는 모르는 상태에서 사용할 예정이신 분들에게 도움이 되었으면 좋겠습니다.

그럼 다시 결론을 요약하고 마무리 하겠습니다. 감사합니다.

  • SQL Server JDBC Driver 는 String type 의 pamameter 를 기본적으로 NVARCHAR 로 매핑한다는 사실을 반드시 기억하고 사용하자
  • 그렇기 때문에, String type 의 parameter 를 기본 VARCHAR 로 매핑하고 싶다면 또는 해야한다면, JDBC URL 에 sendStringParametersAsUnicode=false 를 추가하자
  • Hibernate / JPA 기반이라면 NVARCHAR 컬럼을 매핑한 필드에 @Nationalized를 사용하자

P.S.

사실 위의 내용은 팀장님이 사내 위키에 정리해주신 내용을 바탕으로 공부해서 풀어쓴 것입니다. 복붙 했..

이 외에도 유용한 꿀팁들, 학습자료들 등 도움이 될만한 내용들이 가득한데요.

구글보다 친절하고 명확한 설명의 풍부한 컨텐츠, 그리고 그것들을 정리한 개발자들이 옆자리에! 음청난 환경인거죠.

관심? 궁금? 그럼 빨리 채용공고 로 가보세요!

Tag

#sqlserver #sqlserverjdbcdriver #java #string #nvarchar #varchar #sendStringParametersAsUnicode #nationalized #recruit