엄범

 

Mybatis가 쿼리를 처리하는 방식?


스프링에서는 DB 커넥션을 어떻게 처리하는가?
  • 쿼리 하나 날릴 때 마다 Connection을 맺고 끊는건 비효율적이므로,
  • Connection Pool을 구성하여 Connection을 생성해두고, DB를 사용해야 할 때 Pool에서 커넥션을 빌려 쓰고 반납하게 되어 있다.
  • Spring에서 제공하는 Connection Pool 인터페이스가 바로 `` DataSource``
    • 각 driver vendor들은 자사 규격에 맞게 `` DataSource`` 인터페이스를 구현해서 제공하고 있음!(커넥션 풀 구현체)
      • ojdbc의 `` OracleDataSource``
      • jdbc의 `` ~~DataSource``
    • 각 벤더가 만들어 제공하는게 맞는게, Connection object를 만들어서 가지고 있어야 하는데 이를 만들기 위한 규격을 제공하는게 driver vendor들이 하는 일이고, 그 규격에 맞게 커넥터를 만드는건 자기들이 제일 잘할거니까...
  • 참고하면 도움이 되는 커넥션 풀과 구현체에 대한 설명 : Commons DBCP 이해하기

 

Mybatis에서 xml 기반으로 쿼리 작성 시 어떻게 resolve되는가?
  • `` DataSource``에서 커넥션을 가져와서 쿼리를 수행하거나, prepared statement를 세팅한다거나, 세션을 유지한다거나 하는 동작을 대행해주는 것이 Mybatis
  • Spring-Mybatis에서는 `` SqlSessionFactoryBean``을 사용하는데, 이 Bean에 DataSource, ConfigLocation, MapperLocation 등을 설정하게 된다.
    • mybatis-spring-boot-starter를 사용하면 config.xml설정은 application.yml에서 할 수 있고, mapper xml은 지정 위치에서 자동으로 파싱한다.
    • 그래서 아래와 같은 설정을 생략할 수 있어 여러모로 설정이 간결해짐

```java

factoryBean.setDataSource(dataSource);
factoryBean.setConfigLocation(applicationContext.getResource("classpath:mybatis/config.xml"));
factoryBean.setMapperLocations(applicationContext.getResources("classpath*:mybatis/mappers/**/*.xml"));

```

  • MapperLocation에 지정된 위치에 있는 xml을 파싱하게 되고, 그 안의 mapper namespace를 따라가 그 인터페이스를 확장한 Bean을 생성해준다!

```xml

<mapper namespace="com.example.ex.mapper.TestMapper"> 이면,

interface TestMapper를 확장해서 Bean으로 만들어 준다.

이 때 @Mapper 애너테이션은 그냥 장식임! xml 보고 resolve하는거라서 안붙여도 상관 없다.

```

 

Mybatis에서 애너테이션 기반으로 쿼리 작성 시 어떻게 resolve 되는가?
  • `` @Mapper`` 애너테이션을 달면 `` @MapperScan``을 통해 스프링 빈으로 만들어준다.
  • 이 때 쿼리 구문을 넘기기 위해서 `` @Select()``와 같은 애너테이션을 사용함
  • 자바에서는 줄바꿈 스트링이 지원되지 않기 때문에 ``java "" + "" +``로 연결해야 해서 아주 지저분하다
  • 그러나 코틀린은 ``kt """`` 문자열이 지원되기 때문에, 애너테이션 방식이 오히려 깔끔한 것 같다.
    • 가독성도 괜찮고 Mapper interface의 메서드와 실제 query가 xml로 분리되어서 따라가기 귀찮은 점과 관리포인트가 늘어난다는 점도 해소되고..

 

Mybatis는 어떻게 DB에서 꺼내온 데이터를 객체에 매핑해주는가?
  • 필드 이름과 컬럼 이름을 기반으로 직접 할당한다.
    • constructor, setter, getter 모두 없는 private 필드여도 이름 매칭으로 잘 할당된다. reflection 기반 할당인 듯.
      • 이름 기반 매칭이므로 필드 순서는 상관 없고, 객체 필드 개수와 DB select 컬럼 개수가 달라도 상관 없다.
    • setter가 있으면 setter를 호출해서 할당해준다.
      • 단, 값이 null인 필드는 애초에 할당 자체를 안하기 때문에 setter가 호출되지 않는다!
        • 그래서 null 필드에 기본값 넣어주는 용도로는 사용 불가
      • 그리고 resultType(resultMap)에 없는 이름의 필드의 setter도 당연히 호출되지 않는다.
        • 그래서 ``java setDomain() {domain = card.getDomain()}`` 이렇게는 사용 불가. setDomain이 호출되지 않으니까.
        • 단, 반대로 이렇게는 가능! 사실 이 방향 초기화가 더 맞다!

```java

public void setCard(card) {

    this.card = card;

    this.domain = card.getDomain();

}

```

 

  • @Builder와 같이 사용할 때 주의해야 함!
    • @Builder만 단독으로 사용 시 이름 기반 매칭을 사용하는게 아니라 순서 기반 매칭을 사용함!!!
      • 객체의 1번째 필드와 DB Select 문의 1번째 컬럼을 매핑하기 때문에 순서가 다르면 에러가 발생
      • 객체의 필드 개수가 DB에서 select하는 컬럼 개수 보다 많으면 IndexOutOfBoundException이 발생함
    • setter 있으면 이거 호출해서 넣어주는건 동일.
    • @Builder를 사용하면서 이름 기반 매칭을 사용하려면?
      • @NoArgsConstructor, @AllArgsConstructor를 추가로 붙여주면 됨! + @Data 까지 4개 붙여주는게 일반적

 

Mybatis 설정 / 시작하기


 

mybatis-spring-boot-starter

 

mybatis get sequence after insert

 

mybatis에서 enum을 query 파라미터로 쓸 때, lang3을 사용하면 예외가 발생한다

```xml

<if test="@org.apache.commons.lang3.StringUtils@isNotEmpty(userCode)">

--- error 발생

Caused by: java.lang.IllegalArgumentException: Unable to convert type com.naver.dbill.common.enums.UserCode of AB to type of java.lang.CharSequence

```

  • 애초에 enum을 StringUtils로 비교한다는 것 자체가 type이 안맞기 때문임.
  • lang3이 아닌 lang.StringUtils를 쓰면 예외 없이 동작은 하는데, 타입 체크를 안하기 때문. lang은 안쓰는 것이 좋다.
  • enum은 ``java userCode != null``로 비교해주면 된다.

 

jdbcType : nullable column에 null이 들어갈 때?

  • MyBatis는 nullable 컬럼의 parameter로 null이 넘어왔을 때, jdbcType이 명시되어 있지 않으면 TypeException을 던진다.
    • setNull로 해당 타입에 맞는 null값(VARCHAR인 경우 "")을 넣어줘야 하는데, 뭘 넣어줄지 모르니까 예외가 발생하는 것
    • The JDBC Type is required by JDBC for all nullable columns, if null is passed as a value. You can investigate this yourself by reading the JavaDocs for the PreparedStatement.setNull() method.
      [www.mybatis.org/mybatis-3/sqlmap-xml.html]  
  • 반대로 null이 아닌 값이 넘어왔을 때는 해당 자바 타입의 setNonNullParameter를 호출하는데 여기서 parameter의 타입 -> DB 타입으로 변환해주어야 한다. 적절하게 변환하지 않으면 "부적합한 열 유형" 예외가 발생함.
  • 모든 nullable column에 jdbcType을 명시해주기 귀찮다면, 다음 방법으로 처리할 수 있음.

 

```java

sqlSessionFactory!!.configuration.jdbcTypeForNull = JdbcType.NULL

// xml base 설정도 가능

```

 

CustomTypeHandler 정의
  • https://mybatis.org/mybatis-3/ko/configuration.html#typeHandlers  
  • 애너테이션 사용 시 주의. 꼼꼼하게 읽어보고 사용해야 함.
  • 마이바티스는 타입핸들러를 선택하기 위해 ``java javaType=[TheJavaType], jdbcType=null``조합을 사용한다.
    • jdbcType이 쿼리 파라미터에 명시되어 있지 않은 경우 javaType만 본다!
  • 즉 기본으로 제공되는 AClassTypeHandler<A>와 내가 직접 정의한 AClassTypeHandler<A>가 모두 존재한다면, 후자를 사용하기 위해서는
    • 쿼리 파라미터에 jdbcType을 명시하거나,
      • => @MappedJdbcTypes에 적은 타입과 동일한 타입. 해당 쿼리 파라미터는 ``java jdbcType=null``이 아니라 `` [TheJdbcType]``으로 넘어오니 내가 정의한 타입핸들러를 타게 된다.
    • 쿼리 파라미터에 typeHandler를 적어 아예 내 핸들러를 타도록 명시하거나,
      • => 이 경우 typeAliases로 Handler가 등록 되어 있어야 prefix없이 쓴다.
    • MappedJdbcTypes 애너테이션을 아예 사용하지 않거나, 
      • ``java javaType=[TheJavaType], jdbcType=[null&AllType]`` 에 대해서 내 핸들러를 탄다
    • 사용하면서 includeNullJdbcType = true로 적어주어야 한다.
      • ``java javaType=[TheJavaType], jdbcType=[null&TheJdbcType]`` 에 대해서 내 핸들러를 탄다
      • 이건 조금 애매한 방법일 것 같은게... db 상에 VARCHAR인 것도 jdbcType 명시 안하면 null로 넘어올거고, DATE인 것도 jdbcType 명시 안하면 null로 넘어오게 될텐데, 이러면 두 타입 모두 includeNullJdbcType = true인 핸들러를 타게 된다.
      • 각각 다른 핸들러를 타기 위해서는 둘 중 하나의 db타입 파라미터에는 jdbcType을 꼭 명시해줘야 되는데...
      • 그럴 바에야 두 타입 모두 jdbcType을 명시해주는게 더 나으니까.