본문 바로가기

Language/Spring

[SQL Mapper] Mybatis 개념 및 구조

Mybatis
  • SQL Mapper로 SQL 중심의 프로젝트에 적합
    → 객체 중심의 설계라면 JPA(Hibernate)가 더 적합
  • 객체와 SQL 쿼리 사이의 매핑을 처리하는 도구
  • ORM처럼 객체를 자동으로 매핑하지 않고, 개발자가 직접 SQL 쿼리를 작성하고 SQL 결과를 자바 객체와 수동으로 매핑하는 방식을 사용
    → 수동이긴 하지만 객체와 데이터베이스 테이블 간의 매핑을 다룬다는 점에서 마치 ORM 처럼 사용되긴 하지만 실질적으로는 SQL Mapper
  • SQL 쿼리의 세밀한 제어가 필요할 때 유용하고, SQL의 최적화와 복잡한 쿼리 작성에서 강점을 보임

Mybatis 등장 배경
  1. 복잡한 SQL 쿼리 관리의 어려움
    • 기존의 JDBC를 사용했을 때는 개발자가 직접 SQL 쿼리를 작성하고, 쿼리 실행 결과를 Java 객체로 수작업으로 매핑
    • 프로젝트가 커질수록 SQL이 복잡해지고, 이로 인해 코드 유지보수가 어려워짐
  2. SQL과 Java 코드의 강한 결합
    • JDBC를 사용할 때 SQL 쿼리는 Java 코드에 직접 포함되기 때문에 SQL과 비즈니스 로직이 강하게 결합되는 것이 문제
    • 이로 인해 코드 가독성이 낮아지고, 쿼리를 수정하거나 변경하기 어려웠음
  3. ORM 프레임워크에 대한 과잉 추상화 문제
    • MyBatis가 등장하기 전에는 Hibernate와 같은 ORM(Object Relational Mapping) 프레임워크가 주로 사용되었으나, ORM은 SQL을 추상화하여 객체 중심으로 데이터베이스를 조작하도록 설계되었기 때문에 복잡한 SQL을 요구하는 프로젝트에서는 비효율적
    • 특히, 대규모 프로젝트에서는 직접 작성한 SQL이 더 효율적이고 최적화된 경우가 많았음
  4. SQL 중심 개발에 대한 요구
    • 데이터베이스 중심의 애플리케이션에서는 복잡한 쿼리와 데이터 조작이 필수적이기 때문에 SQL을 중심으로 개발하면서도 반복적인 JDBC 작업을 줄이는 방식을 원했음
    • SQL의 자유도를 유지하면서 SQL과 Java 객체를 매핑해주는 프레임워크의 필요성이 대두되었음
MyBatis의 등장을 통한 해결
  • SQL의 유연성 유지 : 개발자가 작성한 SQL을 그대로 사용할 수 있도록 지원
  • 반복 작업 제거 : JDBC의 반복적인 코드를 줄이고, SQL 쿼리 결과를 Java 객체로 자동 매핑
  • SQL 관리 용이 : XML 파일 또는 주석 기반으로 SQL을 별도로 관리할 수 있어 가독성과 유지보수성이 향상
  • 객체-관계 매핑 간소화 : 복잡한 매핑 작업을 간단한 설정으로 처리

Mybatis 동작 과정

  • mybatis-config.xml
    • 클래스 Alias 설정, DB 연결 설정, SQL 경로 설정 등 MyBatis의 기본 설정을 담고 있음
  • SqlSessionFactoryBuilder
    • mybatis-config.xml 파일을 읽어들여 build(InputStream) 메서드를 호출하여 SqlSessionFactory 객체를 생성
  • SqlSessionFactory
    • openSession() 메서드를 통해 데이터베이스와 연결된 SqlSession 객체를 생성
    • Thread-safe하며 애플리케이션 내에서 단 한 번 생성 후 재사용
  • SqlSession
    • mapper.xml에 정의된 SQL을 실행하고, 결과를 반환
    • 제공되는 주요 메서드는 selectOne, selectList, insert, update, delete 등
    • 자동 커밋 여부 설정 가능
    • Thread-safe하지 않으므로 요청마다 새로 생성, 작업 종료 후 반드시 close() 호출
  • mapper.xml
    • SQL 쿼리와 Java 인터페이스를 매핑한 설정 파일
    • namespace를 통해 구분되며 각 메서드는 고유한 ID로 식별

Mapper.xml 예제
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!-- dtd는 스프링부트에서 사용하는 mybatis 버전 입력 -->
<mapper namespace="com.example.mybatistest.mapper.MemberMapper">

    <!-- db column(스네이크케이스)과 vo(카멜케이스)의 필드명이 상이하므로 resultmap에서 매핑해줌 -->
    <resultMap id="memberMap" type="com.example.mybatistest.domain.vo.MemberVO">
        <id column="member_id" property="memberId"/>
        <result column="email" property="email"/>
        <result column="member_name" property="memberName"/>
        <result column="password" property="password"/>
    </resultMap>

    <!-- ※ mapper interface의 method명과 id가 일치해야 함 -->
    <!-- 회원 등록 -->
    <insert id="insertMember" parameterType="com.example.mybatistest.domain.vo.MemberVO" useGeneratedKeys="true" keyProperty="memberId">
        INSERT INTO MEMBER (email, member_name, password)
        VALUES (#{email}, #{memberName}, #{password})
    </insert>

    <!-- 회원 단건 조회 -->
    <select id="findOneMember" parameterType="Long" resultMap="memberMap">
        SELECT * FROM member WHERE member_id = #{memberId}
    </select>

    <!-- 회원 목록 조회 -->
    <select id="findAllMember" resultMap="memberMap">
        SELECT * FROM member
    </select>

    <!-- 회원 수정 -->
    <update id="updateMember" parameterType="com.example.mybatistest.domain.vo.MemberVO">
        UPDATE member set
                          email = #{email},
                          member_name = #{memberName},
                          password = #{password}
        WHERE member_id = #{memberId}
    </update>

    <!-- 회원 삭제 -->
    <delete id="deleteMember" parameterType="Long">
        DELETE FROM member
        WHERE member_id = #{memberId}
    </delete>
</mapper>
MemberVO(Dao)
package com.example.mybatistest.domain.vo;

import com.example.mybatistest.domain.dto.MemberDto;
import lombok.*;

@Getter @Setter
@ToString
// 객체를 외부에서 함부로 생성하지 못하게 제한(생성 메서드를 이용하도록)
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class MemberVO {
    private Long memberId;
    private String email;
    private String memberName;
    private String password;

    /**
     * dto를 vo로 변환
     * @param memberDto
     * @return
     */
    public static MemberVO toVO(MemberDto memberDto) {
        MemberVO memberVO = new MemberVO();
        memberVO.setEmail(memberDto.getEmail());
        memberVO.setMemberName(memberDto.getMemberName());
        memberVO.setPassword(memberDto.getPassword());
        return memberVO;
    }

    /**
     * 객체 생성 메서드
     * @param email
     * @param memberName
     * @param password
     * @return
     */
    public static MemberVO createMember(String email, String memberName, String password) {
        MemberVO memberVO = new MemberVO();
        memberVO.setEmail(email);
        memberVO.setMemberName(memberName);
        memberVO.setPassword(password);
        return memberVO;
    }

    /**
     * 수정 메서드
     * @param email
     * @param memberName
     * @param password
     */
    public void updatemember(String email, String memberName, String password) {
        this.email = email;
        this.memberName = memberName;
        this.password = password;
    }
}
MemberDto
package com.example.mybatistest.domain.dto;

import com.example.mybatistest.domain.vo.MemberVO;
import lombok.Getter;
import lombok.Setter;
import lombok.ToString;

@Getter @Setter
@ToString
public class MemberDto {
    private Long memberId;
    private String email;
    private String memberName;
    private String password;

    /**
     * vo를 dto로 변환
     * @param memberVO
     * @return
     */
    public static MemberDto toDto(MemberVO memberVO) {
        MemberDto memberDto = new MemberDto();
        memberDto.setMemberId(memberVO.getMemberId());
        memberDto.setEmail(memberVO.getEmail());
        memberDto.setMemberName(memberVO.getMemberName());
        memberDto.setPassword(memberVO.getPassword());
        return memberDto;
    }
}
MemberMapper Interface
package com.example.mybatistest.mapper;

import com.example.mybatistest.domain.vo.MemberVO;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper // 제일 중요한 어노테이션(이걸 설정해야 스프링부트에서 이 클래스를 찾아서 빈으로 등록한다.)
public interface MemberMapper {
	// ※ 메서드명과 mapper.xml의 id가 일치해야 한다.
    // 등록
    void insertMember(MemberVO memberVO);
    // 단건 조회
    MemberVO findOneMember(Long memberId);
    // 목록 조회
    List<MemberVO> findAllMember();
    // 수정
    void updateMember(MemberVO memberVO);
    // 삭제
    void deleteMember(Long memberId);

}
MemberService.java
package com.example.mybatistest.service;

import com.example.mybatistest.domain.dto.MemberDto;
import com.example.mybatistest.domain.vo.MemberVO;
import com.example.mybatistest.mapper.MemberMapper;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;

import static com.example.mybatistest.domain.dto.MemberDto.toDto;
import static java.util.stream.Collectors.toList;

@Service
@Slf4j
@RequiredArgsConstructor
public class MemberService {

    private final MemberMapper memberMapper;

    /**
     * 등록
     * @param memberVO
     */
    public void save(MemberVO memberVO) {
        memberMapper.insertMember(memberVO);
    }

    /**
     * 단건 조회
     * @param memberId
     * @return
     */
    public MemberVO findOne(Long memberId) {
        return memberMapper.findOneMember(memberId);
    }

    /**
     * 목록 조회
     * @return
     */
    public List<MemberDto> findAll()     {
        List<MemberVO> members = memberMapper.findAllMember();
//        일반 foreach 사용
//        List<MemberDto> result = new ArrayList<>();
//        for (MemberVO m : members) {
//            MemberDto memberDto = toDto(m);
//            result.add(memberDto);
//        }
        // java 1.8 stream api 사용
        List<MemberDto> result = members.stream()
                .map(m -> toDto(m)).collect(toList());
        return result;
    }

    /**
     * 수정
     */
    public void updateMember(MemberVO memberVO) {
        memberMapper.updateMember(memberVO);
    }


    /**
     * 삭제
     * @param memberId
     */
    public void remove(Long memberId) {
        memberMapper.deleteMember(memberId);
    }
}