MyBatis로 1:n 결과를 갖는 JOIN절 처리하기

MyBatis는 SQL 구문을 손쉽게 실행할 수 있게 해 주는 Java의 가장 대표적 프레임워크 중 하나 입니다. 단일 테이블에서의 실행은 쉽게 처리되지만, 1:n을 갖는 Join의 경우 적용법에 접근하기가 쉽지 않습니다. 하지만 한번 익혀두고 나면, 이후 어렵지 않게 사용 가능합니다. ~~ ^^ 이 예제에서는 다음과 같은 파일들을 요구합니다. MyBatis의 라이브러리와 Log4j 의 설정방법 등은 수업 시간의 예제를 참고하시기 바랍니다.

 

테이블 구조

다음과 같이 1:n의 관계를 갖는 member 테이블과 hobby 테이블이 있다고 가정하겠습니다.

 

각 테이블에 저장되어 있는 데이터는 다음과 같습니다.

hobby 테이블의 데이터
member 테이블의 데이터

즉, 회원 한 명당 여러 개의 취미 데이터를 갖는 형식입니다. 이 두 테이블에 대해 Join을 걸면 다음과 같이 결과가 나오게 됩니다.

위와 동일한 결과를 JSP 웹 페이지에서 표시하도록 해 보겠습니다.    

 

Model 클래스 구성하기

기본적으로 Model 클래스는 테이블의 구조에 대응되게 구성하지만, Join문과 같이 두 개 이상의 테이블이 병합된 경우 SELECT절의 결과에 맞는 별도의 Model 클래스를 갖기도 합니다. 여기에서는 Join절의 결과를 저장하기 위한 MemberHobby라는 클래스를 다음과 같이 준비하였습니다.

     

MyBatis 설정 XML 준비하기

MyBatis 설정을 위한 config.xml에서 다음과 같이 데이터베이스의 접속 정보와 Mapper XML의 경로를 명시합니다. config.xml의 전체 코드는 다음과 같습니다.

Spring의 경우 위와 같은 설정 파일만 있으면 SqlSession 객체가 컨트롤러나 서비스 객체에 자동으로 주입되지만 Model1/2 방식으로 개발하는 경우 다음과 같은 SqlSessionFactory 클래스가 추가적으로 필요합니다.

   

DAO 역할을 할 MyBatis Mapper 정의하기

DAO 패턴과 Service 패턴을 정말 정확하게 적용한다면 다음과 같은 구조가 되는 것이 맞습니다.

Controller(서블릿이나 JSP페이지) –> Service 구현체 –> Dao 구현체 –> Mapper (MyBatis) –> DATABASE

 

하지만 Service와 Dao의 경우 비슷한 코드가 정확한 설계 없이 그저 남들이 다 그렇게 하니까 무의미하게 인터페이스와 구현체 클래스만을 정의해 놓는 경우가 많습니다. 그래서 최근에는 Dao 구현체 클래스를 별도로 두지 않고 MyBatis의 Mapper 자체를 DAO의 역할을 하도록 다음과 같이 구성하는 경우도 점점 늘고 있습니다.

Controller(서블릿이나 JSP페이지) –> Service 구현체 –> Mapper (MyBatis) = DAO –> DATABASE

 

여기서도 MyBatis 자체를 DAO 계층으로 보고 진행합니다. config.xml에 정의한 Mapper XML파일을 다음과 같이 정의합니다. ResultMap으로 MemberHobby 클래스의 멤버변수와 SELECT 절에서 명시하는 컬럼의 이름에 대한 맵핑처리를 정의하고, Join절을 구현한 <select> 태그 기능을 구현하였습니다.

   

Service 계층 정의 및 구현

Service 계층은 컨트롤러로의 호출에 따라서 적합한 DAO를 대신 호출해 주는 중간 단계의 객체 입니다. 이 과정에서 컨트롤러로부터 전달받은 값을 DAO에 전달하기 전에 파라미터를 가공하거나, DAO로부터 리턴받은 값을 컨트롤러에게 리턴하기 전에 값을 가공할 수 있습니다. 이러한 처리를 비지니스 로직이라고 합니다. 비지니스 로직은 컨트롤러가 클라이언트의 요청을 식별하고, 응답을 구성하는 역할에만 집중할 수 있도록 데이터 연동에 필요한 각종 처리를 모두 분리해 낸 형태라고 볼 수 있을 겁니다. Service 계층은 구현하고자 하는 기능의 목록을 Interface 형태로 나열하고, 구현채 클래스를 별도로 두어 구성합니다. 이는 객체간의 의존성을 줄여줄 수 있는 방법이 됩니다. 먼저 다음과 같이 인터페이스를 정의합니다. 여기에서는 데이터 조회 기능만을 구현할 것이므로 SELECT를 위한 기능 하나만 정의 했습니다.

 

이렇게 정의한 인터페이스를 상속받는 MemberHobbyJoinServiceImpl 클래스를 정의합니다. 이 클래스에는 MyBatis의 Mapper에 정의한 list 기능을 호출하고 상황에 따른 예외처리와 리턴을 수행합니다.

   

컨트롤러 구성하기

JSP Model1 방식에서는 컨트롤러의 역할을 JSP 페이지가 담당합니다. 이 때, JSP 페이지에서는 HTML 처리와 컨트롤러로서의 로직 처리를 모두 담당하기 때문에 코드의 복잡도가 증가하게 됩니다. 이러한 부분을 개선하기 위해서 등장한 Model2 방식은 MVC 패턴이라고도 부릅니다. JSP 컨트롤러의 역할을 View와 Controller로 분리하여 JSP는 HTML 처리만 수행하고 서블릿이 Controller의 역할을 하게 됩니다. 코드가 분리되어 복잡도가 감소하고 협업과 유지보수가 더 유리하게 되었지만 두 방식간의 소스코드 차이는 없다고 봐도 무관합니다. 여기에서는 Model1 방식으로 구현하였습니다. 수업을 잘 따라오셨다면 Model2 방식의 구현도 어렵지 않게 할 수 있을 거라 생각합니다.

 

실행결과는 다음과 같습니다.

7 thoughts on “MyBatis로 1:n 결과를 갖는 JOIN절 처리하기

  1. ronica

    페이징 처리는 어떻게가능한가요?
    리스트로보여질때에는 강사/학생 2로우여야하는데..
    쿼리자체는 5개의로우여서..
    limit offset으로 페이징처리시에 문제가생길거같습니다.

  2. JOIN문의 SQL 실행 결과가 5개의 행을 표시하고 있기 때문에, 이 처리는 처음부터 5개의 row를 목록을 구현하는 페이지에 출력한다는 전제를 갖고 시작되는 내용입니다.
    만약 목록 페이지에서 2개의 행을 보여주어야 한다면 JOIN문을 사용하지 않고, 예제에서 제시한 member 테이블에 대해서만 쿼리를 수행해야 합니다.

    꼭 JOIN에 나오는 데이터를 함께 보여줘야 하는 이유가 있다면, JOIN문에 대해서 GROUP BY절을 함께 사용하는 것도 방법이겠습니다.
    MySQL에서는 GROUP BY 절에 포함되지 않은 데이터에 대해서 집계 함수 뿐만 아니라 group_concat() 이라는 함수도 제공합니다.
    GROUP BY절에 포함되지 않은 컬럼들을 콤마(,)로 구분하여 하나의 행으로 묶어주는 기능을 합니다.

    참고하시면 좋은 결과가 있을 것 같습니다.

댓글 남기기