SQL을 직접 쓰고 싶은데, JDBC로 ResultSet을 하나하나 꺼내는 건 너무 번거롭지 않나요?

SQL 작성은 개발자가, 결과 매핑은 프레임워크가 처리해주면 좋겠다는 요구에서 MyBatis가 탄생했습니다. JDBC의 반복 코드를 제거하면서도 SQL 제어권은 그대로 유지하는 것이 핵심입니다.

개념 정의

MyBatis 는 SQL과 자바 객체 사이의 매핑을 자동화하는 SQL Mapper 프레임워크 입니다. 개발자가 SQL을 직접 작성하고, MyBatis가 파라미터 바인딩과 결과 매핑을 처리합니다.

왜 필요한가

  • **JDBC 보일러플레이트 제거 **: Connection 획득, PreparedStatement 생성, ResultSet 순회, 자원 해제를 매 쿼리마다 반복하는 코드가 사라집니다
  • ** 레거시 DB 대응 **: 테이블 설계가 객체지향적이지 않은 기존 시스템에서 JPA의 엔티티 매핑이 오히려 짐이 될 때, SQL을 직접 작성하는 편이 훨씬 빠릅니다
  • **DBA 협업 **: DBA가 튜닝한 SQL을 그대로 XML에 넣을 수 있어서 개발자-DBA 간 커뮤니케이션 비용이 줄어듭니다

JPA와 뭐가 다른가

기준JPA (ORM)MyBatis (SQL Mapper)
SQL 작성프레임워크가 자동 생성개발자가 직접 작성
객체 매핑엔티티 연관관계 자동ResultMap으로 수동
학습 곡선높음 (영속성 컨텍스트, 프록시)낮음 (SQL 알면 바로 시작)
캐싱1차/2차 캐시 내장별도 구현 필요
복잡한 쿼리Querydsl 등 추가 도구 필요네이티브 SQL 자유롭게

핵심 차이는 "누가 SQL을 만드느냐"입니다. JPA는 프레임워크가 SQL을 생성하고, MyBatis는 개발자가 SQL을 작성합니다.

동작 원리 — SqlSessionFactory에서 쿼리 실행까지

MyBatis가 쿼리를 실행하는 전체 흐름을 살펴보겠습니다.

PLAINTEXT
설정 파일(XML/Java Config)


SqlSessionFactoryBuilder → SqlSessionFactory (애플리케이션당 1개)


                          SqlSession (요청당 1개)


                          Mapper 인터페이스 → SQL 실행 → 결과 매핑

이 흐름에서 각 컴포넌트의 역할을 하나씩 살펴보겠습니다.

**1단계 — SqlSessionFactory 생성 **: XML 또는 Java 설정을 읽어서 DataSource, Mapper 위치, 설정값을 가진 팩토리 객체를 만듭니다. 애플리케이션 전체에서 하나만 생성합니다.

**2단계 — SqlSession 획득 **: 각 요청(또는 트랜잭션)마다 SqlSession을 열어서 DB 커넥션을 획득합니다. JDBC의 Connection과 비슷한 역할입니다.

**3단계 — Mapper를 통한 쿼리 실행 **: SqlSession이 Mapper 인터페이스의 프록시 객체를 생성합니다. 메서드를 호출하면 대응하는 SQL이 실행되고, 결과가 자바 객체로 변환됩니다.

첫 SELECT 실행하기

MyBatis 설정 파일은 DataSource와 Mapper 파일 경로를 지정합니다.

XML
<!-- mybatis-config.xml -->
<configuration>
  <environments default="dev">
    <environment id="dev">
      <transactionManager type="JDBC"/>
      <dataSource type="POOLED">
        <property name="driver" value="com.mysql.cj.jdbc.Driver"/>
        <property name="url" value="jdbc:mysql://localhost:3306/mydb"/>
        <property name="username" value="root"/>
        <property name="password" value="password"/>
      </dataSource>
    </environment>
  </environments>
  <mappers>
    <mapper resource="mapper/UserMapper.xml"/>
  </mappers>
</configuration>

위 설정에서 핵심은 <dataSource><mappers>입니다. 커넥션 풀 방식(POOLED)으로 DB에 연결하고, SQL이 정의된 Mapper XML 파일을 등록합니다.

Mapper 인터페이스와 XML을 작성합니다.

JAVA
public interface UserMapper {
    User findById(Long id);
}
XML
<!-- mapper/UserMapper.xml -->
<mapper namespace="com.example.mapper.UserMapper">
  <select id="findById" resultType="com.example.domain.User">
    SELECT id, name, email
    FROM users
    WHERE id = #{id}  <!-- 파라미터 바인딩 -->
  </select>
</mapper>

여기서 #{id}가 PreparedStatement의 ?로 변환됩니다. MyBatis가 파라미터 타입을 자동으로 감지해서 바인딩하기 때문에 SQL Injection 걱정 없이 사용할 수 있습니다.

실행 코드는 다음과 같습니다.

JAVA
// SqlSessionFactory 생성 (애플리케이션 시작 시 1회)
SqlSessionFactory factory = new SqlSessionFactoryBuilder()
    .build(Resources.getResourceAsStream("mybatis-config.xml"));

// SqlSession으로 쿼리 실행
try (SqlSession session = factory.openSession()) {
    UserMapper mapper = session.getMapper(UserMapper.class);
    User user = mapper.findById(1L);  // ← SQL 실행 + 결과 매핑
}

위 코드에서 session.getMapper()가 Mapper 인터페이스의 프록시 객체를 반환합니다. 실제 SQL 실행은 이 프록시가 XML에 정의된 쿼리를 찾아서 수행합니다.

주의할 점

#{} vs ${} 혼동

#{}는 PreparedStatement의 파라미터로 바인딩되어 안전합니다. 반면 ${}는 문자열 그대로 SQL에 삽입됩니다.

XML
<!-- 안전: PreparedStatement 파라미터 바인딩 -->
<select id="findByName" resultType="User">
  SELECT * FROM users WHERE name = #{name}
</select>

<!-- 위험: SQL Injection 가능! -->
<select id="findByColumn" resultType="User">
  SELECT * FROM users ORDER BY ${columnName}
</select>

${}는 테이블명이나 컬럼명처럼 PreparedStatement의 ?로 대체할 수 없는 곳에서만 사용해야 합니다. 사용자 입력값에 ${}를 쓰면 SQL Injection 공격에 노출됩니다.

SqlSession을 닫지 않는 실수

SqlSession은 DB 커넥션을 점유합니다. try-with-resourcesfinally 블록에서 반드시 닫아야 합니다. Spring Boot 환경에서는 mybatis-spring이 자동으로 관리하지만, 순수 MyBatis를 사용할 때 이를 누락하면 커넥션 풀이 고갈되어 애플리케이션이 멈출 수 있습니다.

namespace 불일치

Mapper XML의 namespace가 인터페이스의 풀 패키지 경로와 정확히 일치해야 합니다. 하나라도 다르면 BindingException이 발생합니다. 패키지 리팩토링 후 XML의 namespace를 같이 변경하지 않는 실수가 흔합니다.

정리

항목설명
MyBatis란SQL과 자바 객체 매핑을 자동화하는 SQL Mapper
핵심 차이 (vs JPA)개발자가 SQL을 직접 작성, 프레임워크는 매핑만 담당
SqlSessionFactory애플리케이션당 1개, 설정을 읽어서 생성
SqlSession요청당 1개, DB 커넥션 관리
파라미터 바인딩#{}는 안전, ${}는 SQL Injection 주의
주의 사항SqlSession 반드시 닫기, namespace 일치 확인
댓글 로딩 중...