MyBatis 시작하기 — SQL Mapper의 기본 개념과 설정
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가 쿼리를 실행하는 전체 흐름을 살펴보겠습니다.
설정 파일(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 파일 경로를 지정합니다.
<!-- 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을 작성합니다.
public interface UserMapper {
User findById(Long id);
}
<!-- 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 걱정 없이 사용할 수 있습니다.
실행 코드는 다음과 같습니다.
// 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에 삽입됩니다.
<!-- 안전: 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-resources나 finally 블록에서 반드시 닫아야 합니다. 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 일치 확인 |