서버/스프링(Spring)

[Spring] STS4, Oracle, myBatis 연동하기 (2/3)

도트7 2022. 6. 4. 16:01

[바로가기 목록]

1. 프로젝트 생성 및 환경 구축하기

2. 스프링과 mybatis 연동하기 (현재 포스팅)

3. 서버 생성 및 테스트

 

 

 

Spring, myBatis 연동

 앞서 스프링과 오라클이 연동된 것을 확인했다. 이 상태에서 바로 JDBC를 사용할 수도 있지만 JDBC를 보다 편하게 사용할 수 있도록 MyBatis를 연동해보자. 해야 할 작업을 요약하면 다음과 같다.

 

  1. DTO, DAO, Service, ServiceImpl 패키지 생성
  2. MemberDTO.java 작성
  3. MemberDAO.java 작성
  4. MemberService.java 작성
  5. MemberServiceImpl.java 작성
  6. mybatis-config.xml 작성
  7. memberMapper.xml  작성
  8. MemberController.java 작성
  9. root-context.xml에서 mybatis 설정 파일 참조, mapper, DAO Scan 등 지정

 

 

 


 

 

 

 패키지 구성

 연동에 필요한 클래스들을 작성하기에 앞서 패키지를 위와 같이 미리 구성해놓도록 하자.

 

 

 

 DTO class

package com.tistory.ku_develog.dto;

public class MemberDTO {
	private String id, name;
	private int age;
	
	public String getId() {
		return id;
	}

	public String getName() {
		return name;
	}
	
	public int getAge() {
		return age;
	}

	public void setId(String id) {
		this.id = id;
	}
	
	public void setName(String name) {
		this.name = name;
	}
	
	public void setAge(int age) {
		this.age = age;
	}
	
	@Override
	public String toString() {
		return new StringBuilder().append("{id=").append(id).append(", name=")
				.append(name).append(", age=").append(age).append("}").toString();
	}
}

 Member 테이블과 대응될 데이터 클래스를 작성한다. 테이블과 대응되지만 동시에 클라이언트 간의 데이터 전송에도 사용되기 때문에 둘을 따로 구분하지 않고 DTO 이름으로 생성했다. 이 데이터 클래스는 테이블과 매칭될 것이기 때문에 테이블이 가진 모든 속성을 멤버 변수로 가져야 한다.

 

 

 

 DAO Interface

package com.tistory.ku_develog.dao;

import java.util.List;

import com.tistory.ku_develog.dto.MemberDTO;

public interface MemberDAO {
	public List<MemberDTO> getAll();
	public MemberDTO getMember(String id);
	public boolean insertMember(MemberDTO memberDto);
	public boolean deleteMember(String id);
	public MemberDTO updateMember(MemberDTO memeberDto);
}

 DAO 인터페이스에는 DB에 접근할 때 사용할 메서드를 작성한다. 인터페이스지만 이 인터페이스는 구현(implements)되지 않고 뒤에서 생성하는 mapper.xml에서 작성하는 쿼리와 1:1로 매핑된다. 인터페이스의 구성을 보면 테이블의 모든 데이터를 읽어오는 getAll, 특정 id와 일치하는 데이터를 읽어오는 getMember, 그리고 데이터 삽입, 삭제, 수정 등을 수행하는 메서드가 존재한다.

 

 DAO를 작성할 때 주의해야 할 것이 있는데 하나의 DAO Interface는 반드시 하나의 테이블에만 매핑되어야 한다는 것이다. 프로젝트에 다른 테이블을 추가로 사용해야 한다면 그 테이블에 대응하는 DAO를 추가로 생성해야 한다. 데이터베이스에 접근하는 객체라는 의미의 DAO(Data Access Object)가 접미사로 붙지만 마이바티스가 적용된 프로젝트에는 Mapper를 붙이기도 한다.

 

 

 

 Service interface

package com.tistory.ku_develog.service;

import java.util.List;

import com.tistory.ku_develog.dto.MemberDTO;

public interface MemberService {
	public List<MemberDTO> getAll();
	public MemberDTO getMember(String id);
	public boolean insertMember(MemberDTO memberDto);
	public boolean deleteMember(String id);
	public MemberDTO updateMember(MemberDTO memeberDto);
}

 서비스는 비지니스 로직을 처리하기 위한 클래스로서 이 인터페이스는 서비스 클래스가 구현해야 할 메서드를 정의한다. 

 

 혹시 서비스 인터페이스와 DAO 인터페이스가 똑같음을 눈치챘는가? 여담으로 나는 처음 마이바티스가 적용된 예제를 따라해봤을 때 서비스 인터페이스와 DAO 인터페이스의 구조, 선언된 메서드가 완전히 같은 것을 보고 큰 혼란에 빠졌었다. 아예 같은 구조에 이름만 다른데 굳이 두 개의 인터페이스가 존재할 필요가 있는 건가 하는 생각이 머리를 지배했다. 한 두 명이 아닌 많은 블로그에서 비슷한 구조를 띄고 있었기 때문에 분명 의미가 있을거라 생각하고 스프링의 구조, MyBatis의 구조, MVC로 나누어진 구조를 제대로 공부하고 나서 보니 그제서야 왜 두 인터페이스가 같은지 깨달았다.

 

 이유는 프로젝트의 규모가 매우 작기 때문이었다. 현재 이 프로젝트는 예제이기 때문에 Member에 대한 DAO, 서비스, 컨트롤러만 작성되어 있다. DAO는 테이블 하나에서 데이터를 읽어 오는 메서드만을 정의하기 때문에 한정적이지만 서비스는 테이블 하나에 얽매이지 않고 비지니스 로직을 처리하기 위해 지금 예제보다 더 많은 메서드를 정의할 수 있다. 하지만 이 프로젝트는 마이바티스를 어떻게 사용하는지 보여주기 위한 소규모 예제이기 때문에 별도의 메서드를 가지지 않고 둘 다 같은 구조를 띄게 된 것이다.

 

 

 

ServiceImpl class

package com.tistory.ku_develog.service.impl;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.tistory.ku_develog.dao.MemberDAO;
import com.tistory.ku_develog.dto.MemberDTO;
import com.tistory.ku_develog.service.MemberService;

@Service("memberService")
public class MemberServiceImpl implements MemberService {
	@Autowired
	private MemberDAO memberDao;

	@Override
	@Transactional
	public List<MemberDTO> getAll() {
		return memberDao.getAll();
	}

	@Override
	@Transactional
	public MemberDTO getMember(String id) {
		return memberDao.getMember(id);
	}

	@Override
	@Transactional
	public boolean insertMember(MemberDTO memberDto) {
		return memberDao.insertMember(memberDto);
	}

	@Override
	@Transactional
	public boolean deleteMember(String id) {
		return memberDao.deleteMember(id);
	}

	@Override
	@Transactional
	public MemberDTO updateMember(MemberDTO memeberDto) {
		return memberDao.updateMember(memeberDto);
	}
}

 앞서 생성한 Service 인터페이스를 구현하는 ServiceImpl 클래스다. 이 클래스에서는 비지니스 로직을 처리하기 위한 코드를 작성하게 되며 비지니스 로직을 처리하기 위해서 DAO를 사용하여 데이터베이스에 접근, 데이터를 읽어올 수 있다. 서비스에 작성한 메서드들은 컨트롤러에서 호출된다.

 

 사용된 어노테이션

  • Service : 클래스 레벨에서 이 어노테이션을 선언하면 스프링 컨테이너에 bean으로 등록된다. 파라미터로 bean 이름을 별도로 지정할 수 있는데 지정하지 않을 경우 클래스 이름을 카멜 표기법으로 변환하여 사용한다.
  • Autowired : 스프링에서 지원하는 DI 어노테이션으로, 멤버 변수 레벨에서 사용할 수 있다. 선언한 멤버 변수의 클래스 타입과 일치하는 bean이 스프링 컨테이너에 있다면 멤버 변수에 의존성을 주입해주는 역할을 한다. 코드를 보면 MemberDAO 객체에 대해 초기화 작업을 수행하지 않은 채로 객체를 사용하고 있는데 일반적인 자바 코드라면 Null Point Exception을 일으켜야 정상이지만 Autowired를 통해 정상적으로 의존성이 주입되었다면 에러를 일으키지 않는다.
  • Transactional : 이름에서 유추할 수 있듯이 트랜잭션에 관한 어노테이션으로 이 어노테이션이 적용된 메서드는 트랜잭션 처리된다. 앞서 pom.xml에서 트랜잭션에 관한 의존성을 주입(spring-tx)하였고 어노테이션으로 사용하기 위해서 root-context.xml에서 tx:annotation-driven을 사용해 선언적 트랜잭션 제어를 활성화하였다.(어노테이션 기반으로 트랜잭션 설정하는 것을 선언적 트랜잭션 이라고 봐도 된다.)

 

 

 

mybatis-config.xml 생성하기

 mybatis에 대한 설정 파일을 작성하기 위해 "src/main/resources" 폴더에 우클릭 후 위와 같이 "mybatis-config.xml" 파일을 추가한다. 이름을 바꿔도 되지만 이름을 바꿀 경우 root-context.xml에서 이 파일을 환경설정 파일로 지정할 때 이름도 맞춰서 적어줘야 한다.

 

 

 

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>
		<typeAlias alias="memberDto" type="com.tistory.ku_develog.dto.MemberDTO"/>
	</typeAliases> 
</configuration>

 파일을 생성하면 XML 선언부만 존재하는데 바로 아래 줄에 위와 같이 DTD 설정을 추가한다.

 

 루트 앨리먼트인 configuration 태그에서는 mybatis에 관한 설정을 추가할 수 있다. 예제에 사용된 typeAliase는 클래스의 별명을 지정해줄 수 있는데 이 별명은 바로 다음 파트에서 작성할 "memberMapper.xml"에서 유용하게 사용될 것이다. xml은 java와 다르게 다른 클래스를 import 할 수 없기 때문에 앞서 만든 MemberDTO를 xml에서 사용하려면 "com.tistory.ku_develog.dto.MemberDTO"와 같이 풀 패키지명을 적어주어야 한다. 하지만 빈번하게 사용하는 클래스의 경우 코드가 길어짐은 물론 오타가 발생할 가능성도 있다. 하지만 위와 같이 별명을 부여하면 "memberDto"로 간편하게 사용할 수 있다.

 

 

 

 Mapper.xml

 resources 폴더에 우클릭하여 [New → Folder]로 "mapper" 폴더를 추가한다. mapper 폴더에는 DAO와 매핑될 매퍼 파일들을 작성할 것이다. 폴더 하위에 "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">

<mapper namespace="com.tistory.ku_develog.dao.MemberDAO">
	<select id="getAll" resultType="memberDto">
		SELECT * 
		FROM member_t 
		ORDER BY id
	</select>
	
	<select id="getMember" parameterType="String" resultType="memberDto">
		SELECT * FROM member_t
		WHERE id = #{id} 
		ORDER BY id
	</select>
	
	<insert id="insertMember" parameterType="memberDto">
		INSERT INTO member_t
		VALUES(#{id}, #{name}, #{age})
	</insert>
	
	<delete id="deleteMember" parameterType="String">
		DELETE FROM member_t
		WHERE id = #{id}
	</delete>
	
	<update id="updateMember" parameterType="memberDto">
		UPDATE member_t SET
		name = #{name}, age = #{age}
		WHERE id = #{id}
	</update>
</mapper>

 Mapper.xml은 테이블에 사용할 쿼리를 작성하는 곳이다. 루트 앨리먼트인 <mapper>를 보면 namespace 속성에 앞서 작성한 DAO 인터페이스가 지정되어 있는 것을 볼 수 있는데 이는 mapper.xml 파일 하나와 DAO 인터페이스 하나가 1:1로 매핑되어 사용된다는 것을 의미한다.

 

 쿼리를 작성할 때 예제처럼 <select>, <insert>, <update>, <delete> 등을 사용한다. 사용된 태그들의 속성을 자세히 살펴보면 DAO 인터페이스에서 작성했던 메서드와 비슷한 꼴임을 알 수 있는데 id는 메서드의 이름, parameterType은 메서드가 받을 파라미터 타입, resultType은 메서드의 반환형이다. SQL의 결과에 따라 대충 끼워 맞춘 것이 아닌 DAO에 작성했던 메서드 하나하나의 구조와 정확히 매칭되고 있음을 알 수 있다.

 

 id가 DAO에 작성한 메서드의 이름과 1:1로 매칭된다고 했으니 중복이 있어서는 안되며 DAO에는 없는 메서드 이름을 id로 사용하면 안된다. 네임 스페이스에 사용한 패키지 경로와 메서드 이름을 결합하면 "com.tistory.ku_develog.dao.MemberDAO.getMember()"와 같이 되기 때문이다.

 

 메서드의 매핑에 관해 설명할 때 잠깐 나왔지만 resultType 속성은 쿼리 결과를 어떤 타입으로 매핑할 것인가를 결정하고 parameterType은 쿼리에 사용할 파라미터 타입을 결정한다. 여기서 마이바티스의 장점을 볼 수 있는데 기존 JDBC로 데이터를 조회하면 ResultSet 형태로 조회 결과를 받고 레코드를 하나씩 꺼내 결과를 파싱하여 데이터 객체에 세팅하는 과정을 수행해야 한다. 하지만 마이바티스는 모든 과정을 생략하고 파라미터 타입, 리턴 타입만을 지정하면 자동으로 결과를 가공해준다.

 

 resultType의 데이터 타입은 기본 자료형, 커맨드 객체, 컬렉션 등을 지정할 수 있다. 위 예제에서 resultType에 대해 다소 특이한 부분이 있는데 쿼리 결과로 리스트를 반환하는 getAll과 객체 하나를 반환하는 getMember의 반환 타입이 같다는 것이다. getAll은 결과가 없거나 1개 이상의 결과를, getMember는 결과가 없거나 1개의 결과를 반환함에도 불구하고 왜 getAll에는 리스트를 사용하지 않았을까? resultType에 지정하는 타입은 레코드 하나를 저장할 타입을 지정하는 것이다. 마이바티스는 결과가 2개 이상일 경우 자동으로 결과를 리스트화해서 반환해주기 때문에 리스트의 타입만 제대로 설정해주면 된다.

 

 속성 "parameterType"은 쿼리에 필요한 데이터 타입 혹은 객체를 지정한다. 메서드에서 작업을 처리하기 위해서 파라미터를 받는 것과 같다고 보면 되는데 실제로 MemberDAO에서 작성한 메서드의 파라미터 타입과 매핑되고 있음을 볼 수 있다. 파라미터를 SQL 문에서 사용하려면 #{parameter}와 같이 사용할 수 있으며 DAO에서 작성한 파라미의 이름을 사용할 수 있다. 또한 타입이 데이터 클래스인 경우 멤버 변수 이름을 쿼리에 그대로 사용하는 것도 가능하다.

 

 

 

 Controller Class

package com.tistory.ku_develog.controller;

import java.util.List;

import javax.annotation.Resource;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import com.tistory.ku_develog.dto.MemberDTO;
import com.tistory.ku_develog.service.MemberService;

@RestController
public class MemberController { 
	@Resource(name = "memberService")
	private MemberService memberService;
	private static final Logger logger = LoggerFactory.getLogger(MemberController.class);
	
	@RequestMapping(value="/getAll", method = RequestMethod.GET)
	public List<MemberDTO> getAll() {
		logger.info("[Request/getAll]");
		return memberService.getAll();
	}
	
	@RequestMapping(value="/getMember", method = RequestMethod.GET)
	public MemberDTO getMember(@RequestParam(value = "id") String id) {
		logger.info("[Request/getMember] {id=" + id + "}");
		return memberService.getMember(id);
	}
	
	@RequestMapping(value="/insertMember", method=RequestMethod.GET)
	public void addMember(MemberDTO memberDto) {
		logger.info("[Request/insertMember] " + memberDto);
		if(!memberService.insertMember(memberDto))
			logger.error("insert error " + memberDto);
	}
	
	@RequestMapping(value="/deleteMember", method=RequestMethod.GET)
	public void deleteMember(String id) {
		logger.info("[Request/deleteMember] {id=" + id + "}");
		 if(!memberService.deleteMember(id))
			 logger.error("delete error. {id=" + id + "}");
	}
	
	@RequestMapping(value="/updateMember", method=RequestMethod.GET)
	public void updateMember(MemberDTO memberDto) {
		logger.info("[Request/updateMember]");
		if(memberService.updateMember(memberDto) == null)
			logger.error("update error " + memberDto);
	}
}

 컨트롤러는 클라이언트의 요청을 받는 역할을 한다. 클라이언트로부터 요청이 오면 디스패처 서블릿에 의해서 URL에 맞는 메서드가 호출될 것이다. 여기서는 오직 요청의 분리, 적절한 서비스 호출만을 생각하면 되기 때문에 앞서 작성한 서비스를 호출하여 비지니스 로직을 처리하고 결과를 클라이언트로 넘긴다.

 

 사용된 어노테이션

  • RestController : 클래스 레벨에서 사용하는 어노테이션으로 @Controller와 @ResponseBody 어노테이션을 같이 사용한 것과 같다. 이 어노테이션이 사용되면 클래스 내 모든 메서드에 @ResponseBody 어노테이션이 적용된다.
  • ResponseBody : 여기서 사용되지는 않았지만 RestController로 인해 메서드 레벨에 선언되어 있는 것이나 마찬가지이기 때문에 짚고 넘어가자. 이 어노테이션이 적용되면 클라이언트와의 통신에서 XML, JSON 형태로 데이터를 주고 받는 것이 가능해진다.
  • Resource : 자바에서 지원하는 DI 어노테이션으로 bean의 이름을 특정해서 의존성을 주입받을 수 있다. 파라미터로 전달한 "memberService"는 앞서 MemberServiceImpl 클래스에 선언한 @Service 어노테이션으로 지정한 값으로, 컨테이너에서 "memberService"라는 key 값을 가진 bean의 의존성을 주입받겠다는 의미를 가진다. 이는 스프링 컨테이너가 key-value 형태로 bean을 관리하기 때문에 가능한 것이다.
  • RequestMapping : API를 호출할 때 사용되는 URL을 지정하는 역할을 하며 value는 호출할 url, method는 HTTP 통신에서 사용할 메서드 타입을 지정한다.

 

 

 

 mybatis-config.xml 설정 파일 지정하기

<!-- SqlSessionFactory -->
<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
	<property name="dataSource" ref="dataSource" /> 
	<property name="configLocation" value="classpath:/mybatis-config.xml" /> 
	<property name="mapperLocations" value="classpath:/mapper/*.xml" />
</bean>

 "src/main/resources"에 생성한 마이바티스 설정 파일, 매퍼 파일들을 sqlSessionFactory에 세팅한다. root-context.xml로 돌아와서 sqlSessionFactory에 "configLocation", "mapperLocations" 프로퍼티를 추가하고 파일의 경로를 지정한다.

 

 configLocation은 mybatis의 설정 파일을 지정해줄 수 있지만 필수는 아니다. 지정해주지 않을 경우 디폴트 설정을 가진 마이바티스가 빌드된다. mapperLocations는 xml 파일 기반의 매퍼를 탐색한다. 프로퍼티의 값인 "/mapper/*.xml"은 아까 "src/main/resources"에 생성한 mapper 폴더에서 xml 확장자를 가진 모든 파일(* = 와일드 카드)을 매퍼 파일로 지정한다는 의미를 갖는다.

 

 

 

 스프링 컨테이너에 bean 등록하기

 마지막으로 앞서 생성한 controller, service, dao 클래스들을 스프링 컨테이너에 bean으로 등록해야 한다. 클래스 하나하나 bean으로 등록하는 방법도 있지만 Component Scan을 사용하는 것이 일반적이다. 컴포넌트 스캔은 특정 패키지 경로를 지정하여 지정된 패키지를 포함 하위 모든 패키지를 탐색하여 @Component가 적용된 클래스를 bean으로 등록시킨다. 앞서 사용했던 @Controller, @Service도 내부를 들여다 보면 @Component가 선언되어 있기 때문에 bean 등록 대상이 된다.

 

 

servlet-context.xml
root-context.xml

 

 servlet-context.xml에는 컨트롤러와 관련된 패키지를, root-context.xml에는 비지니스 로직과 관련된 패키지를 스캔한다.

 

 servlet-context.xml에는 기본적으로 컴포넌트 스캔 태그가 존재하는데 스캔 경로(base-package)의 기본값으로 프로젝트를 생성할 때 입력한 패키지가 지정된다. 그리고 프로젝트를 생성할 때 입력한 패키지에는 HomeController가 자동 생성되기 때문에 처음 아무것도 설정하지 않아도 프로젝트를 실행하면 HomeController가 bean으로 등록되어 서버를 실행했을 때 "/" 경로로 동작 여부를 테스트할 수 있게 된다.

 

 root-context.xml에서 사용한 태그를 자세히 보면 서비스는 component-scan이지만 dao는 "mybatis-spring:scan"인 것을 볼 수 있다. 이 스캔 방식은 지정한 패키지를 포함한 하위 모든 패키지를 탐색하여 발견되는 모든 인터페이스를 bean으로 등록해버리기 때문에 사용에 주의가 필요하다. 패키지 구분을 하지 않고 클래스를 한꺼번에 모아놨다면 bean으로 생성할 생각이 없었던 인터페이스들도 모두 bean으로 등록된다.

 

 


 

 

 이로서 모든 연동 작업이 끝났다. 다음 포스팅에서는 프로젝트가 제대로 구성되었는지 테스트해 볼 것이다.

 

 

 

[Spring] STS4, Oracle, myBatis 연동하기 (3/3)

[바로가기 목록] 1. 프로젝트 생성 및 환경 구축하기 2. 스프링과 mybatis 연동하기 3. 서버 생성 및 테스트 (현재 포스팅)  앞서 mybatis와 스프링 연동에 필요한 작업을 마쳤습니다. 테스트에 앞서 서

ku-develog.tistory.com