✨KB IT's Your Life✨ 5기 TIL 기자단 활동
KB IT's Your Life 5기에서 학습한 내용을 복습하고자 정리한 글
기간: 8/12 ~ 8/17
학습 내용: Spring & MyBatis Framework

MyBatis Framework

SQL, 저장 프로시저 및 고급 매핑 기능을 지원하는 자바 기반의 persistence framework

XML 파일 및 어노테이션을 사용하여 SQL 쿼리를 작성하고, 자바 객체와 DB 테이블 간의 매핑을 처리

⇒ SQL 쿼리문을 XML 파일에 작성해두면 MyBatis 프레임워크가 자동으로 DB 연결처리 등을 지원하고 자바 객체와 매핑해준다는 얘기!

MyBatis 장단점

장점

  • 직관적인 SQL 사용(어렵지 않음)
  • 유연한 매핑: XML 파일을 사용하여 JAVA 객체와 DB 테이블 간 매핑을 유연하게 관리 가능
  • 동적 SQL문 지원
  • 높은 확장성

단점

  • 복잡성: XML 파일 하나당 테이블 하나로 매핑되는 형태이기 때문에 프로젝트 구조가 커지면 파일 개수 증가하여 복잡
  • SQL 의존성: SQL 쿼리를 직접 작성해야 하므로 DB 변경 시 많은 수정 필요
  • 높은 초기 학습 곡선
    • 사용하기 위해서 XML 파일 어떻게 만드는지, DAO에서 어떻게 끼워서 사용해야하는지, 설정에 따라 마이바티스 어떻게 생성되는지 공부 필요

MyBatis 동작 과정

xml 설정 & DBCP 작업한 걸 config에 주입 → config를 mybatis(SqlSessionTemplate)에 적용 → mybatis가 DAO에 전달하는 흐름

Controller

  • 클라이언트에서 전달한 요청 위임받아서 필요한 메소드 호출

Service

  • 비즈니스 로직에 의한 전처리 & 후처리

DAO

  • DB와 상호작용하는 객체
  • 애플리케이션의 다른 부분이 DB와 직접 상호작용하지 않도록 함
  • MyBatis를 이용해 Mapper.xml 파일에 정의된 SQL 쿼리문 호출
  • SQL 쿼리를 실행하여 DB로부터 데이터를 가져오거나 저장, 수정, 삭제 등의 작업 수행

MyBatis 설정 파일

Mapper.java

  • MyBatis Mapper 인터페이스
  • SQL 쿼리를 JAVA 메서드에 매핑
  • 주로 SQL 쿼리의 id와 동일한 메서드 이름을 사용하여 정의
  • Mapper.xml에서 정의된 SQL 쿼리문을 호출하는 메서드를 선언
  • MyBatis는 해당 인터페이스의 구현체를 생성하여 SQL 실행

SqlSessionFactory

  • config만 담당하는 클래스 = 설정과 관련된것만 처리하는 객체
  • 설정 내용은 mybatis-config.xml에 작성(해당 파일 읽어서 작업)

mybatis-config.xml

  • MyBatis 전역 설정 정의하는 설정 파일
  • 패키지명 단축해서 사용가능하도록 alias 등록
  • MyBatis Mapper 파일 위치, 트랜잭션 관리 설정, 캐시 설정 처리 어떻게 할지 등 DB 전반에 대한 설정 담당
  • MyBatis 초기화 시 로드

DataSource

  • DBCP 초기 설정
  • DB 드라이버 로딩, DB 연결 설정

Mapper.xml

  • MyBatis Mapper XML 설정 파일
  • SQL 쿼리문과 Mapper.java의 메서드 매핑
  • SQL 쿼리는 XML 형식으로 작성
    • <select>, <insert>, <update>, <delete> 태그로 정의
    • 각 태그 내에 SQL문 작성

Mapper.java와 Mapper.xml의 SQL 쿼리 id 동일해야 매핑 가능한 점 주의!

AppConfig.java

마이바티스 싱글톤 생성 설정 담당

MyBatis도 싱글톤으로 만들어 DAO에 DI하여 사용
MyBatis는 외부 객체 = 어노테이션을 통해서 싱글톤 생성 불가능한 클래스
→ XML이나 자바 통해서 따로 싱글톤 생성 설정 필요

DBCP, myBatis, myBatis 설정만 담당해서 반복적인 작업 진행하는 팩토리 클래스 3개 존재

import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import javax.sql.DataSource;

@Configuration
//@ComponentScan(basePackages = "com.multi")
public class AppConfig {

    public AppConfig(){
        System.out.println("AppConfig created");
    }

		// DB 드라이버 및 연결 설정 & DBCP 초기 설정
    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/shop2?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=UTF-8&useUnicode=true");
        dataSource.setUsername("root");
        dataSource.setPassword("1234");
        dataSource.setInitialSize(5); // 초기 커넥션 수
        dataSource.setMaxTotal(10); // 최대 커넥션 수
        return dataSource;
    }

		// MyBatis 초기 설정
    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
        return sessionFactory.getObject();
    }

		// MyBatis
    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

DBCP(Data Base Connection Pool)

pool = 대기줄

DB와 연결하는 객체를 요청이 들어올 때마다 생성 및 소멸시키는게 아닌, connection pool이라는 대기줄에 미리 객체 여러 개를 생성해 둔 뒤 요청이 들어오면 미리 만들어져 있는 객체를 할당하고 객체를 반납하도록 하는 식으로 동작

반납된 connection 객체는 다른 요청에서 사용 가능

MyBatis 실습

board도 member 디렉토리와 동일한 구조

DAO, Mapper.java(인터페이스), Mapper.xml → 테이블 당 하나씩 생성

views의 jsp에서 member table과 관련된 요청 들어온다면

MemberController → MemberService → MemberDAO → MemberMapper.java → memberMapper.xml → DB 순서로 호출!

Controller

member/controller/MemberController.java

import com.multi.spring3.member.domain.MemberVO;
import com.multi.spring3.member.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpSession;
import java.util.List;

@Controller
@RequestMapping("/member")
public class MemberController {
    private final MemberService memberService;

    @Autowired
    public MemberController(MemberService memberService) {
        System.out.println("MemberController created");
        this.memberService = memberService;
    }

    @GetMapping
    public String index() {
        return "member/member";
    }

    @PostMapping("/insert")
    public ModelAndView insert(MemberVO memberVO) {
        System.out.println(memberVO);
        String result = memberService.insert(memberVO);
        System.out.println("--------->> " + result);
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("result", result);
        modelAndView.setViewName("member/insert_result");
        return modelAndView;
    }

    @PostMapping("/update")
    public ModelAndView update(MemberVO memberVO) {
        String result = memberService.update(memberVO);
        System.out.println("--------->> " + result);
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("result", result);
        modelAndView.setViewName("member/update_result");
        return modelAndView;
    }

    @PostMapping("/delete")
    public ModelAndView delete(@RequestParam("id") String id) {
        String result = memberService.delete(id);
        System.out.println("--------->> " + result);
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("result", result);
        modelAndView.setViewName("member/delete_result");
        return modelAndView;
    }

    @GetMapping("/one")
    public ModelAndView one(String id) {
        MemberVO memberVO = memberService.one(id);
        System.out.println("--------->> " + memberVO);
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("memberVO", memberVO);
        modelAndView.setViewName("member/one_result");
        return modelAndView;
    }

    @GetMapping("/all")
    public ModelAndView all() {
        List<MemberVO> all = memberService.all();
        System.out.println("--------->> " + all);
        ModelAndView modelAndView = new ModelAndView();
        modelAndView.addObject("all", all);
        modelAndView.setViewName("member/all_result");
        return modelAndView;
    }
}

Service

member/service/MemberService.java

import com.multi.spring3.member.dao.MemberDAO;
import com.multi.spring3.member.domain.MemberVO;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class MemberService {

    private final MemberDAO memberDAO;

    @Autowired
    public MemberService(MemberDAO memberDAO) {
        this.memberDAO = memberDAO;
        System.out.println("MemberService created");
    }

    public String insert(MemberVO memberVO) {
        String result = "회원가입 실패";
        if (memberDAO.insert(memberVO) == 1) {
            result = "회원가입 성공";
        }
        ;
        return result;
    }

    public String update(MemberVO memberVO) {
        String result = "회원수정 실패";
        if (memberDAO.update(memberVO) != 0) {
            result = "회원수정 성공";
        }
        ;
        return result;
    }

    public String delete(String id) {
        String result = "회원탈퇴 실패";
        if (memberDAO.delete(id) != 0) {
            result = "회원탈퇴 성공";
        }
        ;
        return result;
    }

    public MemberVO one(String id) {
        return memberDAO.one(id);
    }

    public List<MemberVO> all() {
        return memberDAO.all();
    }
}

DAO

member/dao/MemberDAO.java

import com.multi.spring3.member.domain.MemberVO;
import com.multi.spring3.member.mapper.MemberMapper;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

import java.util.List;

@Repository
public class MemberDAO {

    private final SqlSessionTemplate sqlSessionTemplate;

    @Autowired
    public MemberDAO(SqlSessionTemplate sqlSessionTemplate) {
        this.sqlSessionTemplate = sqlSessionTemplate;
    }

    public int insert(MemberVO memberVO) {
        return sqlSessionTemplate.getMapper(MemberMapper.class).insert(memberVO);
    }

    public int update(MemberVO memberVO) {
        return sqlSessionTemplate.getMapper(MemberMapper.class).update(memberVO);
    }

    public int delete(String id) {
        return sqlSessionTemplate.getMapper(MemberMapper.class).delete(id);
    }
    
    public MemberVO one(String id) {
        return sqlSessionTemplate.getMapper(MemberMapper.class).one(id);
    }

    public List<MemberVO> all() {
        return sqlSessionTemplate.getMapper(MemberMapper.class).all();
    }

    /*
        use shop2;
        CREATE TABLE `member` (
          `id` varchar(45) NOT NULL,
          `pw` varchar(45) NOT NULL,
          `name` varchar(45) DEFAULT NULL,
          `tel` varchar(45) DEFAULT NULL,
          PRIMARY KEY (`id`)
        ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='회원관리용 테이블';

        desc member;
     */

}

Mapper.java(인터페이스)

member/mapper/MemberMapper.java

import com.multi.spring3.member.domain.MemberVO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface MemberMapper {
  //mapper의 id와 일치해야함
  int insert(MemberVO memberVO); //<insert id="insert" ~~>
  int update(MemberVO memberVO);
  int delete(String id);
  MemberVO one(String id); // <select id="one" ~~>
  List<MemberVO> all();
}

resources/mybatis-config.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
    <typeAliases>
        <package name="com.multi.spring3.member.domain"/>
        <package name="com.multi.spring3.board.domain"/>
    </typeAliases>

    <mappers>
        <mapper resource="mapper/memberMapper.xml"/>
        <mapper resource="mapper/boardMapper.xml"/>
    </mappers>
</configuration>

config/AppConfig.java

import org.apache.commons.dbcp2.BasicDataSource;
import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.ClassPathResource;

import javax.sql.DataSource;

@Configuration
//@ComponentScan(basePackages = "com.multi")
public class AppConfig {

    public AppConfig(){
        System.out.println("AppConfig created");
    }

    @Bean
    public DataSource dataSource() {
        BasicDataSource dataSource = new BasicDataSource();
        dataSource.setDriverClassName("com.mysql.cj.jdbc.Driver");
        dataSource.setUrl("jdbc:mysql://localhost:3306/shop2?useSSL=false&allowPublicKeyRetrieval=true&serverTimezone=UTC&characterEncoding=UTF-8&useUnicode=true");
        dataSource.setUsername("root");
        dataSource.setPassword("1234");
        dataSource.setInitialSize(5); // 초기 커넥션 수
        dataSource.setMaxTotal(10); // 최대 커넥션 수
        return dataSource;
    }

    @Bean
    public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception {
        SqlSessionFactoryBean sessionFactory = new SqlSessionFactoryBean();
        sessionFactory.setDataSource(dataSource);
        sessionFactory.setConfigLocation(new ClassPathResource("mybatis-config.xml"));
        return sessionFactory.getObject();
    }

    @Bean
    public SqlSessionTemplate sqlSessionTemplate(SqlSessionFactory sqlSessionFactory) {
        return new SqlSessionTemplate(sqlSessionFactory);
    }
}

Mapper.xml

resources/mapper/memberMapper.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">

<!-- 인터페이스(MemberMapper.java)를 namespace로 지정해두면 unique하게 구분 가능 -->
<mapper namespace="com.multi.spring3.member.mapper.MemberMapper">
    <insert id="insert"
            parameterType="com.multi.spring3.member.domain.MemberVO">
        insert into member
        values (#{id}, #{pw}, #{name}, #{tel});
    </insert>

    <update id="update"
            parameterType="com.multi.spring3.member.domain.MemberVO">
        update member set tel = #{tel}
        where id = #{id}
    </update>

    <delete id="delete"
            parameterType="String">
        delete from member
        where id = #{id}
    </delete>

    <select id="one"
            parameterType="String"
            resultType="com.multi.spring3.member.domain.MemberVO">
        select * from member
        where id = #{id}
    </select>

    <select id="all"
            resultType="com.multi.spring3.member.domain.MemberVO">
        select * from member
    </select>
</mapper>

mybatis-config.xml에 작성 가능한 설정들

<settings>
		<!-- MyBatis가 로그 기록할 유형 결정 -->
    <setting name="logImpl" value="SLF4J" />
		
		<!-- 자주 반복되는 쿼리 있다면 캐시에 넣어두고 빠르게 가져올 수 있는 설정 가능 -->
		<setting name="cacheEnabled" value="true" />
		
		<!-- lazyloading 사용 여부 설정 -->		
		<setting name="laztLoadingEnabled" value="true" />
		<!-- lazy loading = 램에 로딩되는 시점 최대한 늦춰서 사용 직전에 로딩 -->
		
		<!-- 하나의 쿼리에서 다중 결과 집합 허용 여부 설정 -->		
		<setting name="multipleResultSetEnabled" value="true" />
		
		<!-- insert 한 이후 auto increment 한 값 자동으로 가져오는 설정 -->		
		<setting name="useGeneratedKeys" value="true" />
		
		<!-- 결과값 가지고 오면 (필드명 일치시) set메소드 호출해서 알아서 매핑하는 설정 -->
		<setting name="autoMappingBehavior" value="true" />
</settings>

useGeneratedKeys

INSERT INTO board
VALUES (null, #{title}, ...)
// useGeneratedKeys 설정해두면 null 위치에 Auto Increment 값 넣어준다!

/* 결과
boardVO
100, 'fun', 'good' ...;
*/

이번 주 수업에 대해서

광복절이 있어서 수업 일수가 짧았지만 이전 수업에서 JDBC가 어떤 단계로 DB와 데이터를 주고받는지 이해했고, Spring 동작 과정에 대해서도 이해한 뒤 MyBatis를 배워서 그런지 크게 어려운 점은 없었다!
다만 MyBatis 내부 객체 구조는 조금 어렵다고 느꼈지만 그림으로 그려보니 이해할 수 있었다.

profile
256의 우연

0개의 댓글