Web

3. Spring Boot - MyBatis

kakaroo 2022. 2. 3. 19:32
반응형

article logo

 

MyBatis는?

쿼리 기반 웹 애플리케이션을 개발할 때 가장 많이 사용되는 SQL 매퍼(Mapper) 프레임워크입니다.

객체 지향 언어인 자바의 관계형 데이터베이스 프로그래밍을 좀 더 쉽게 할 수 있게 도와 주는 개발 프레임 워크로서 JDBC를 통해 데이터베이스에 엑세스하는 작업을 캡슐화하고 일반 SQL 쿼리, 저장 프로 시저 및 고급 매핑을 지원하며 모든 JDBC 코드 및 매개 변수의 중복작업을 제거 합니다.

Mybatis에서는 프로그램에 있는 SQL쿼리들을 한 구성파일에 구성하여 프로그램 코드와 SQL을 분리할 수 있는 장점을 가지고 있습니다.

 

  • 마이바티스를 사용하지 않고 직접 JDBC를 이용할 경우 문제점:
    • 개발자가 반복적으로 작성해야 할 코드가 많고, 서비스 로직 코드와 쿼리를 분리하기가 어렵습니다.
    • 또한 커넥션 풀의 설정 등 개발자가 신경 써야 할 부분이 많아 여러 가지 어려움이 있습니다. 
  • 따라서, JDBC를 이용해서 직접 개발하기보다는 마이바티스와 같은 프레임워크를 사용하는 게 일반적입니다.
  • JDBC를 이용하여 프로그래밍을 하는 방식: 
    • 클래스나 JSP와 같은 코드 안에 SQL문을 작성하는 방식
    • 따라서 SQL의 변경 등이 발생할 경우 프로그램을 수정해야 합니다.
      • -> 유연하지 않다, 코드가 복잡하게 섞여 있어서 가독성도 떨어짐
  • 마이바티스에서는 SQL을 XML 파일에 작성하기 때문에, SQL 변환이 자유롭고 가독성도 좋습니다.

 

 

1. Database를 생성 해 봅니다. (MySQL WorkBench)

>create database mybatis_db default character set utf8mb4 default collate utf8mb4_general_ci

>create user 'mybatis'@'localhost' identified by 'mybatis11'

>grant all on mybatis_db.* to 'mybatis'@'localhost'

>flush privileges

//table 생성 ; name에 index를 설정, index를 이용하면 빠르게 조회가 가능하다.

>create table company (id integer auto_increment primary key, name varchar(256), address varchar(256), index (name))

 

 

2. Project 생성

 

 

3. Mapper / Controller 구현

package com.example.testbatis;

public class Company {
	private int id;
	private String name;
	private String address;
    
    /*
    //Spring (XML 방식)
    @Data
    public class CompanyVO {
        // 3개의 프로퍼티를 갖는 클래스 생성
        private int id;
        private String name;
        private String address;
	}*/
}

package com.example.testbatis;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface CompanyMapper {

	@Insert("INSERT INTO company(name, address) VALUES(#{company.name}, #{company.address})")
	@Options(useGeneratedKeys=true, keyProperty="id")	//생성된 키를 id 라는 property에 설정
	int insert(@Param("company") Company company);
	
	@Select("SELECT * FROM company")
	/* 해당 주석 내용을 CompanyMap으로 묶어서 다른 mapping에서 재사용한다.
	 * @Results({
	 * @Result(property="name", column="name"),
	 * @Result(property="address", column="address") })
	 */
	@Results(id="CompanyMap", value={
		@Result(property="name", column="name"),
		@Result(property="address", column="address")
	})
	List<Company> getAll();
	
	@Select("SELECT * FROM company WHERE id=#{id}")
	@ResultMap("CompanyMap")	//중복되는 query를 ResultMap으로 등록해서 사용
	Company getById(@Param("id") int id);
	
	/*
	// Spring (XML)
  	<resultMap type="com.example.testbatis.CompanyVO" id="companyMap">
		<id property="id" column="id" />
		<result property="name" column="name" />
		<result property="address" column="address" />
	</resultMap>

	<select id="getList" resultMap="companyMap" resultType="com.example.testbatis.CompanyVO">
		select * from company 
	</select>
	 */
}

package com.example.testbatis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/company")
public class CompanyController {
	@Autowired	//생성자 주입
	private CompanyMapper companyMapper;
	
	@PostMapping("")
	public int post(@RequestBody Company company) {
		return companyMapper.insert(company);
	}

}

 

 

4. Post method via postman

 

 

5. Select * from company

name과 address가 정상적으로 들어가 있지 않고, null data 가 들어가 있습니다.

 

 

Data Class (여기서는 Company 클래스)에 Getter/Setter가 설정이 안 되어서 값이 안 들어갔습니다.

Lombok의 @Data annotation을 사용해서 수정해줍니다.

간혹 위 annotation으로도 Lombok 설정이 안 될 수도 있는데, Lombok 설치까지 해주어야 합니다.

설치 방법은 여기 블로그를 참조하세요.

 

https://congsong.tistory.com/31

 

이클립스(Eclipse)에 롬복(Lombok) 설치하기

롬복은 테이블을 구조화한 도메인 클래스(Entity 또는 DTO, VO)에서 getter/setter 메서드와 toString, equals, hashCode 등의 메서드를 애너테이션으로 사용할 수 있도록 해주는 라이브러리입니다. 이외에도

congsong.tistory.com

 

 

Lombok(롬복)은 Java 라이브러리로 반복되는 getter, setter, toString 등의 메서드 작성 코드를 줄여주는 코드 다이어트 라이브러리입니다. 보통 Model 클래스나 Entity 같은 도메인 클래스 등에는 수많은 멤버변수가 있고 이에 대응되는 getter와 setter 그리고 toString() 메서드 그리고 때에 따라서는 멤버변수에 따른 여러개의 생성자를 만들어주게 되는데, 거의 대부분 이클립스같은 IDE의 힘만으로 생성한다고 하지만 이 역시도 번거로운 작업이 될 수 있습니다. 뿐만 아니라 코드 자체가 반복되는 메서드로 인해 매우 복잡해지게 됩니다.

Lombok은 여러가지 어노테이션을 제공하고 이를 기반으로 코드를 컴파일과정에서 생성해 주는 방식으로 동작하는 라이브러리입니다. 즉 코딩 과정에서는 롬복과 관련된 어노테이션만 보이고 getter와 setter 메서드 등은 보이지 않지만 실제로 컴파일된 결과물(.class)에는 코드가 생성되어 있다는 뜻입니다.

 

 

package com.example.testbatis;

import lombok.Data;

@Data
public class Company {
	private int id;
	private String name;
	private String address;
}

 

 

 


Employee 도 동일하게 구현해 보겠습니다.

 

먼저 Table 을 생성합니다.

company table의 id를 reference할 수 있게 foreign key도 등록합니다.

company table에 없는 company_id(foreign key)를 insert 할 수 없습니다.

 

create table employee (
	id integer auto_increment primary key,
	company_id integer,
	employee_name varchar(128),
	employee_address varchar(256),
	index (employee_name),
	foreign key (company_id) references company(id)
)

 

package com.example.testbatis;

import lombok.Data;

@Data
public class Employee {
	private int id;
	private int company_id;
	private String name;
	private String address;
}

package com.example.testbatis;

import java.util.List;

import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Options;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Result;
import org.apache.ibatis.annotations.ResultMap;
import org.apache.ibatis.annotations.Results;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface EmployeeMapper {
	
	//employee(company_id, employee_name, employee_address) 는 DB column 값이다.
	@Insert("INSERT INTO employee(company_id, employee_name, employee_address) VALUES(#{employee.company_id}, #{employee.name}, #{employee.address})")
	@Options(useGeneratedKeys=true, keyProperty="id")	//생성된 키를 id 라는 property에 설정
	int insert(@Param("employee") Employee employee);
	
    //property는 Employee 클래스의 멤버변수이다.
	@Select("SELECT * FROM employee")
	@Results(id="EmployeeMap", value= {
			@Result(property="name", column="employee_name"),
			@Result(property="address", column="employee_address")
	})
	List<Employee> getAll();
	
	@Select("SELECT * FROM employee WHERE id=#{id}")
	@ResultMap("EmployeeMap")
	Employee getById(int id);
}

package com.example.testbatis;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/employee")
public class EmployeeController {
	
	@Autowired
	private EmployeeMapper employeeMapper;
	
	/*	😎 Post(RequestBody) method 의 인자는 Employee 이다.
	 *  Database에 들어갈 JSON 형태의 data값은 DB column 값이 아니라 Employee class의 내부 객체멤버로 할당해야 한다.
	 * 즉, 아래 name이 DB column 이름인 employee_name으로 하면 안 되고, class 멤버 변수인 name으로 지정되어야 한다. 
	 * 헷갈리는 부분이니 유의하자
 	{
	    "company_id": 13,
	    "name": "홍길남",
	    "address": "서울시 강서구 강서3동"
	}
	 */
	@PostMapping("")
	public Employee post(@RequestBody Employee employee) {
		employeeMapper.insert(employee);
		return employee;	
	}
	
	@GetMapping("")
	public List<Employee> getAll() {
		return employeeMapper.getAll();
	}
	
	@GetMapping("/{id}")
	public Employee getById(@PathVariable("id") int id) {
		return employeeMapper.getById(id);
	}
}

 

Company 데이터베이스가 아래와 같이 있다고 가정합니다.

아래 company_id 14를 가지는 company(id)가 위 table에 있기 때문에 아래 값은 insert가 가능합니다.

하지만, ompany_id 17을 가지는 company(id)가 위 table에 없기 때문에 아래 값은 insert가 불가능합니다

error를 리턴한다.

 


Company 를 다니는 Employee 관계로 수정해보겠습니다.

Company class에 Employee 멤버를 추가합니다.

@Data
public class Company {
	private int id;
	private String name;
	private String address;
	private List<Employee> employeeList;
}

Service 레벨의 형태로 구현하기 위해 CompanyService 클래스를 생성합니다.

@Service
public class CompanyService {
	
	@Autowired
	private CompanyMapper companyMapper;
	
	@Autowired
	private EmployeeMapper employeeMapper;
	
	List<Company> getAll() {
		List<Company> companyList = companyMapper.getAll();
		if(companyList != null && companyList.size() > 0) {
			for(Company company : companyList) {
				company.setEmployeeList(employeeMapper.getByCompanyId(company.getId()));
			}
		}		
		return companyList;
	}
}

CompanyMapper와 EmployeeController 에서 getAll 부분을 수정합니다.

//CompanyMapper.java
@Autowired
private CompanyService companyService;	

...

@GetMapping("")
public List<Company> getAll() {
    return companyService.getAll();
}

//EmployeeController.java
@GetMapping("/{id}/employee")
public List<Employee> getByCompanyId(@PathVariable("id") int id) {
    return employeeMapper.getByCompanyId(id);
}

localhost:8080/company 한 결과 Employee 정보가 같이 나타납니다.

 

[
    {
        "id": 14,
        "name": "대우자동차",
        "address": "서울시 강남구 양재4동",
        "employeeList": [
            {
                "id": 7,
                "company_id": 14,
                "name": "홍길동",
                "address": "서울시 강서구 강서1동"
            }
        ]
    },
    {
        "id": 15,
        "name": "현대자동차",
        "address": "서울시 강남구 양재1동",
        "employeeList": [
            {
                "id": 8,
                "company_id": 15,
                "name": "홍길서",
                "address": "서울시 강서구 강서2동"
            }
        ]
    },
    {
        "id": 16,
        "name": "삼성전자",
        "address": "수원시 영통구 매탄3동",
        "employeeList": [
            {
                "id": 9,
                "company_id": 16,
                "name": "홍길남",
                "address": "서울시 강서구 강서3동"
            }
        ]
    }
]

 

CompanyService 에서 getAll을 따로 처리하던 것을 기존 CompanyMapper에서 수정해 보겠습니다.

sub query 부분을 수정해서 기존 CompanyService 부분에서 처리하던 것을 동일하게 수행할 수 있습니다.

@Results(id="CompanyMap", value={
		@Result(property="name", column="name"),
		@Result(property="address", column="address"),
		//여러개의 sub query를 수행, EmployeeMapper.getByCompanyId 의 id라는 칼럼을 파라미터로 사용
		@Result(property="employeeList", column="id", many=@Many(select="com.example.testbatis.EmployeeMapper.getByCompanyId"))
})
List<Company> getAll();

//companyService 대신 companyMapper로 다시 원복
@GetMapping("")
public List<Company> getAll() {
    return companyMapper.getAll();
}

 

DB Query 부분을 디버깅 로그로 보기 위해서는 아래와 같이 추가합니다.

logging.level.com.example.testbatis.CompanyMapper=TRACE
logging.level.com.example.testbatis.EmployeeMapper=TRACE

 


2022-02-04 15:51:59.544 DEBUG 18644 --- [nio-8080-exec-1] c.e.testbatis.CompanyMapper.getAll       : ==>  Preparing: SELECT * FROM company
2022-02-04 15:51:59.563 DEBUG 18644 --- [nio-8080-exec-1] c.e.testbatis.CompanyMapper.getAll       : ==> Parameters: 
2022-02-04 15:51:59.579 TRACE 18644 --- [nio-8080-exec-1] c.e.testbatis.CompanyMapper.getAll       : <==    Columns: id, name, address
2022-02-04 15:51:59.579 TRACE 18644 --- [nio-8080-exec-1] c.e.testbatis.CompanyMapper.getAll       : <==        Row: 14, 대우자동차, 서울시 강남구 양재4동
2022-02-04 15:51:59.580 DEBUG 18644 --- [nio-8080-exec-1] c.e.t.EmployeeMapper.getByCompanyId      : ====>  Preparing: SELECT * FROM employee WHERE company_id=?
2022-02-04 15:51:59.581 DEBUG 18644 --- [nio-8080-exec-1] c.e.t.EmployeeMapper.getByCompanyId      : ====> Parameters: 14(Integer)
2022-02-04 15:51:59.582 TRACE 18644 --- [nio-8080-exec-1] c.e.t.EmployeeMapper.getByCompanyId      : <====    Columns: id, company_id, employee_name, employee_address
2022-02-04 15:51:59.582 TRACE 18644 --- [nio-8080-exec-1] c.e.t.EmployeeMapper.getByCompanyId      : <====        Row: 7, 14, 홍길동, 서울시 강서구 강서1동
2022-02-04 15:51:59.583 DEBUG 18644 --- [nio-8080-exec-1] c.e.t.EmployeeMapper.getByCompanyId      : <====      Total: 1
2022-02-04 15:51:59.585 TRACE 18644 --- [nio-8080-exec-1] c.e.testbatis.CompanyMapper.getAll       : <==        Row: 15, 현대자동차, 서울시 강남구 양재1동
2022-02-04 15:51:59.585 DEBUG 18644 --- [nio-8080-exec-1] c.e.t.EmployeeMapper.getByCompanyId      : ====>  Preparing: SELECT * FROM employee WHERE company_id=?
2022-02-04 15:51:59.585 DEBUG 18644 --- [nio-8080-exec-1] c.e.t.EmployeeMapper.getByCompanyId      : ====> Parameters: 15(Integer)
2022-02-04 15:51:59.586 TRACE 18644 --- [nio-8080-exec-1] c.e.t.EmployeeMapper.getByCompanyId      : <====    Columns: id, company_id, employee_name, employee_address
2022-02-04 15:51:59.586 TRACE 18644 --- [nio-8080-exec-1] c.e.t.EmployeeMapper.getByCompanyId      : <====        Row: 8, 15, 홍길서, 서울시 강서구 강서2동
2022-02-04 15:51:59.586 DEBUG 18644 --- [nio-8080-exec-1] c.e.t.EmployeeMapper.getByCompanyId      : <====      Total: 1
2022-02-04 15:51:59.587 TRACE 18644 --- [nio-8080-exec-1] c.e.testbatis.CompanyMapper.getAll       : <==        Row: 16, 삼성전자, 수원시 영통구 매탄3동
2022-02-04 15:51:59.587 DEBUG 18644 --- [nio-8080-exec-1] c.e.t.EmployeeMapper.getByCompanyId      : ====>  Preparing: SELECT * FROM employee WHERE company_id=?
2022-02-04 15:51:59.587 DEBUG 18644 --- [nio-8080-exec-1] c.e.t.EmployeeMapper.getByCompanyId      : ====> Parameters: 16(Integer)
2022-02-04 15:51:59.588 TRACE 18644 --- [nio-8080-exec-1] c.e.t.EmployeeMapper.getByCompanyId      : <====    Columns: id, company_id, employee_name, employee_address
2022-02-04 15:51:59.588 TRACE 18644 --- [nio-8080-exec-1] c.e.t.EmployeeMapper.getByCompanyId      : <====        Row: 9, 16, 홍길남, 서울시 강서구 강서3동
2022-02-04 15:51:59.588 DEBUG 18644 --- [nio-8080-exec-1] c.e.t.EmployeeMapper.getByCompanyId      : <====      Total: 1
2022-02-04 15:51:59.588 DEBUG 18644 --- [nio-8080-exec-1] c.e.testbatis.CompanyMapper.getAll       : <==      Total: 3

 

 

 

@Transactional annotation

DB insert/update/delete 등의 수행 중 exception으로 인해 runtime error 발생하더라도 이전에 수행된 DB 작업이 그대로 기록되어 있습니다. 이를 방지하기 위해 해당 annotation tag 와 아래의 option을 넣으면 option에 맞는 조건(ex. exception) 발생시 이전 버전으로 roll-back됩니다. 

@Transactional 옵션

 - isolation

 - propagation

 - noRollbackFor

 - rollbackFor

 - timeout

 - readOnly

자세한 내용은 아래 블로그를 참조합니다.

 

https://velog.io/@kdhyo/JavaTransactional-Annotation-%EC%95%8C%EA%B3%A0-%EC%93%B0%EC%9E%90-26her30h

 

[Java]@Transactional Annotation 알고 쓰자

초반 @Transactional 어노테이션에 대해 자세히 알아보지 않고,막연히 롤백때 사용한다고 하여 SQL C,U,D 를 할 때마다 메소드 위에 붙여서 사용하곤 하였다.하지만, 내 코드를 보신 선임께서 단지 @Tran

velog.io

 

 

source : https://github.com/kakarooJ/SpringBoot-MyBatis

 

GitHub - kakarooJ/SpringBoot-MyBatis: SpringBoot-MyBatis Example code

SpringBoot-MyBatis Example code. Contribute to kakarooJ/SpringBoot-MyBatis development by creating an account on GitHub.

github.com

반응형

'Web' 카테고리의 다른 글

Spring Boot - Thymeleaf  (0) 2022.02.05
Spring Boot 기본 (공사중..)  (0) 2022.02.05
2. Spring Boot - MySQL/MyBatis 연동  (0) 2022.02.03
1. Spring Boot - Starter Application by Eclipse(간단한 API 서버)  (2) 2022.02.02
JSP parameter 처리  (0) 2022.02.02