IT/development

[spring/mybatis] Rest API 계층구조(1:N) 객체 조회(feat .쉬운 예제)

알 수 없는 사용자 2022. 12. 25. 17:12
반응형

목차

    역시나 미래의 나를 위해 기록한다.

    계층구조 조회가 처음 접하고 익숙해지기 전까지는 좀 어렵다.(물론 지금도 쉬운 건 아니다.)

    모든 일이 익숙해지면 아무것도 아닌 게 그전까지는 어려운 법이다.

    Rest API 계층구조 조회 간단 예시다.

    사용자(UserDto) 객체가 도서 객체(BookDto)를 list로 가지고 있는 구조다.

    회원과 도서의 관계를 1:N으로 매핑했고 회원이 부모고 책이 자식이다.

    아래처럼 한명의 회원이 여러개의 책을 대출할 수 있는 조건

    그래서 회원 객체는 도서 객체를 list로 가지고 있어야 한다.

    아래 DB 테이블을 보면 회원번호 1번인 천둥의 신 토르가 열혈강호 1권~5권까지 가지고 있다.

    API니까 당연히 클라이언트에 전달할 객체인 dto와 db 조작시 사용할 객체인 vo를 따로 두었다.


    DB 😄

    회원 테이블

    CREATE TABLE `temp_user` (
      `user_seq` int(20) NOT NULL AUTO_INCREMENT COMMENT '사용자번호',
      `user_name` varchar(30) NOT NULL COMMENT '사용자명',
      `user_email` varchar(30) NOT NULL COMMENT '사용자 이메일',
      `reg_date` timestamp NULL DEFAULT NULL COMMENT '등록일',
      PRIMARY KEY (`user_seq`,`user_email`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci

    테스트 데이터

    INSERT INTO temp_user
    (user_seq, user_name, user_email, reg_date)
    VALUES(1, '토르', 'thor@naver.com', '2022-12-21 22:57:07.000');
    INSERT INTO temp_user
    (user_seq, user_name, user_email, reg_date)
    VALUES(2, '헐크', 'hulk@naver.com', '2022-12-21 22:57:07.000');
    INSERT INTO temp_user
    (user_seq, user_name, user_email, reg_date)
    VALUES(3, '타노스', 'tanos@naver.com', '2022-12-21 22:57:07.000');

    도서 테이블

    CREATE TABLE `temp_book` (
      `book_seq` int(20) NOT NULL AUTO_INCREMENT COMMENT '도서 번호',
      `book_name` varchar(30) NOT NULL COMMENT '도서명',
      `author` varchar(30) NOT NULL COMMENT '저자',
      `user_seq` int(11) NOT NULL COMMENT '사용자번호',
      PRIMARY KEY (`book_seq`,`book_name`)
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci

    테스트데이터

    INSERT INTO temp_book
    (book_seq, book_name, author, user_seq)
    VALUES(25, '열혈강호1권', '양극진', 1);
    INSERT INTO temp_book
    (book_seq, book_name, author, user_seq)
    VALUES(26, '열혈강호2권', '양극진', 1);
    INSERT INTO temp_book
    (book_seq, book_name, author, user_seq)
    VALUES(27, '열혈강호3권', '양극진', 1);
    INSERT INTO temp_book
    (book_seq, book_name, author, user_seq)
    VALUES(28, '열혈강호4권', '양극진', 1);
    INSERT INTO temp_book
    (book_seq, book_name, author, user_seq)
    VALUES(29, '열혈강호5권', '양극진', 1);
    INSERT INTO temp_book
    (book_seq, book_name, author, user_seq)
    VALUES(30, '원피스1권', '일본1', 2);
    INSERT INTO temp_book
    (book_seq, book_name, author, user_seq)
    VALUES(31, '원피스6권', '일본1', 2);
    INSERT INTO temp_book
    (book_seq, book_name, author, user_seq)
    VALUES(32, '원피스7권', '일본1', 2);
    INSERT INTO temp_book
    (book_seq, book_name, author, user_seq)
    VALUES(33, '슬램덩크1권', '일본2', 3);
    INSERT INTO temp_book
    (book_seq, book_name, author, user_seq)
    VALUES(34, '슬램덩크100권', '일본2', 3);

    DB 도서 테이블의 외래키는 테스트니 논리적으로만 설정했다.


    Java단 😘

    Vo(DB와 통신 용도)

    네이밍 룰은 서버단이니 카멜케이스로 선언

    아래 인텔리제이의 플러그인을 사용하면 좀 수월하다.

     

    [IntelliJ] camelCase <-> snake_case 변환(feat. CamelCase plugin)

    목차 camelCase snake_case 😊 개발시에 대체적으로 java에서는 카멜케이스(userName)를 사용하고 DB 필드는 스케이크케이스(user_name)을 사용하는데 수작업으로 하는 것보다 더 괜찮은 플러그인을 발견해

    yaga.tistory.com

    package study.lsyrestapitest1.domain.vo;
    
    import lombok.Getter;
    import lombok.Setter;
    import lombok.ToString;
    
    import java.util.ArrayList;
    import java.util.List;
    
    @Getter @Setter
    @ToString
    // 회원
    public class UserVo {
        private int userSeq;
        private String userName;
        private String userEmail;
        private String regDate;
        private List<BookVo> bookVoList = new ArrayList<>();
    }
    package study.lsyrestapitest1.domain.vo;
    
    import lombok.Getter;
    import lombok.Setter;
    import lombok.ToString;
    
    @Getter @Setter @ToString
    // 도서
    public class BookVo {
        private int bookSeq;
        private String bookName;
        private String author;
    }

    Dto(클라이언트와 통신 용도)

    쉬운 예제를 위해 빌더패턴이 아닌 Getter/Setter를 이용했다.

    네이밍 룰은 클라이언트단이니 스네이크케이스로 선언

    package study.lsyrestapitest1.domain.dto;
    
    import lombok.Getter;
    import lombok.NoArgsConstructor;
    import lombok.Setter;
    import lombok.ToString;
    
    import java.util.ArrayList;
    import java.util.List;
    
    @Getter @Setter
    @ToString
    @NoArgsConstructor
    // 회원
    public class UserDto {
        private int user_seq;
        private String user_name;
        private String user_email;
        private String reg_date;
        private List<BookDto> book_dto_list = new ArrayList<>();
    
        public UserDto(int user_seq, String user_name, String user_email, String reg_date) {
            this.user_seq = user_seq;
            this.user_name = user_name;
            this.user_email = user_email;
            this.reg_date = reg_date;
        }
    }
    package study.lsyrestapitest1.domain.dto;
    
    import lombok.*;
    
    @Getter @Setter
    @ToString
    @NoArgsConstructor
    // 도서
    public class BookDto {
        private int book_seq;
        private String book_name;
        private String author;
    
        public BookDto(int book_seq, String book_name, String author) {
            this.book_seq = book_seq;
            this.book_name = book_name;
            this.author = author;
        }
    }

    mapper xml

    resultMap의 collection을 이용해 계층구조를 매핑했고 resultMap으로 받았다.

    <?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="study.lsyrestapitest1.mapper.UserMapper">
        <resultMap id="userMap" type="study.lsyrestapitest1.domain.vo.UserVo">
            <id column="user_seq" property="userSeq"/>
            <result column="user_name" property="userName"/>
            <result column="user_email" property="userEmail"/>
            <result column="reg_date" property="regDate"/>
            <!-- 책 list -->
            <collection property="bookVoList" javaType="java.util.ArrayList" resultMap="bookMap"/>
        </resultMap>
    
        <!--  책 -->
        <resultMap id="bookMap" type="study.lsyrestapitest1.domain.vo.BookVo">
            <id column="book_seq" property="bookSeq"/>
            <result column="book_name" property="bookName"/>
            <result column="author" property="author"/>
        </resultMap>
    
        <select id="findUserList" resultMap="userMap">
            SELECT  tu.user_seq
                   ,tu.user_name
                   ,tu.user_email
                   ,tu.reg_date
                   ,tb.book_seq
                   ,tb.book_name
                   ,tb.author
            FROM temp_user tu LEFT OUTER JOIN temp_book tb ON tu.user_seq = tb.user_seq
        </select>
    </mapper>

    혹은 아래처럼 collection의 ofType을 대상 클래스로 선언하고 resultMap안에다 넣어도 된다.

    ofType을 선언하지 않을 경우 NPE가 발생한다.

    <?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="study.lsyrestapitest1.mapper.UserMapper">
        <resultMap id="userMap" type="study.lsyrestapitest1.domain.vo.UserVo">
            <id column="user_seq" property="userSeq"/>
            <result column="user_name" property="userName"/>
            <result column="user_email" property="userEmail"/>
            <result column="reg_date" property="regDate"/>
            <!-- 책 list -->                                                                      
            <collection property="bookVoList" ofType="study.lsyrestapitest1.domain.vo.BookVo">
                <result column="book_seq" property="bookSeq"/>
                <result column="book_name" property="bookName"/>
                <result column="author" property="author"/>             
            </collection>
        </resultMap>
    
        <select id="findUserList" resultMap="userMap">
            SELECT  tu.user_seq
                 ,tu.user_name
                 ,tu.user_email
                 ,tu.reg_date
                 ,tb.book_seq
                 ,tb.book_name
                 ,tb.author
            FROM temp_user tu LEFT OUTER JOIN temp_book tb ON tu.user_seq = tb.user_seq
        </select>
    </mapper>

    ※ hasOne(부모 자식의 관계가 1:1)관계는 association 태그를 이용하면 되고 아래처럼 사용하면 된다.

    <association property="BookInfo" javaType="com.test.lsy.vo.bookInfo">
        <result column="book_no" property="BookNo"/>
        <result column="book_nm" property="BookNm"/>
    </association>

    mapper interface

    package study.lsyrestapitest1.mapper;
    
    import org.apache.ibatis.annotations.Mapper;
    import org.springframework.stereotype.Repository;
    import study.lsyrestapitest1.domain.vo.UserVo;
    
    import java.util.List;
    
    @Mapper @Repository
    public interface UserMapper {
    
        List<UserVo> findUserList();
    }

    service

    DB 조회 값을 Vo타입 list에 받아 list를 루프 돌며 새로 만든 dto객체에 세팅을 시킨 후 DtoList 반환

    물론 stream 등을 이용해 더 코드를 간결화 할 수도 있지만 누구나 보기 쉽게 향상된 for문 이용

    아니면 그냥 mapper에서부터 dto로 받으면 서비스 코드도 매우 간결해 진다.(이게 더 나을 듯 하다.)

    package study.lsyrestapitest1.service;
    
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Service;
    import org.springframework.transaction.annotation.Transactional;
    import study.lsyrestapitest1.domain.dto.BookDto;
    import study.lsyrestapitest1.domain.dto.UserDto;
    import study.lsyrestapitest1.domain.vo.BookVo;
    import study.lsyrestapitest1.domain.vo.UserVo;
    import study.lsyrestapitest1.mapper.UserMapper;
    
    import java.util.ArrayList;
    import java.util.List;
    
    @Service
    @Slf4j
    @RequiredArgsConstructor
    @Transactional(readOnly = true)
    public class UserService {
    
        private final UserMapper userMapper;
        
        public List<UserDto> findUserList() {
        	// DB값 조회한 값을 userVoList에 저장
            List<UserVo> userVoList = userMapper.findUserList();
            List<UserDto> userDtoList = new ArrayList<>();
    		// userVoList를 루프 돌며 UserDto객체를 생성 해 Vo의 값을 dto에 세팅
            for (UserVo userVo : userVoList) {
                UserDto userDto = new UserDto(userVo.getUserSeq(), userVo.getUserName(), userVo.getUserEmail(), userVo.getRegDate());
    			// UserVo의 bookVoList값을 bookVoList에 저장 후 루프 돌며 bookDtoList에 저장            
                List<BookVo> bookVoList = userVo.getBookVoList();
                List<BookDto> bookDtoList = new ArrayList<>();
                for (BookVo bookVo : bookVoList) {
                    BookDto bookDto = new BookDto(bookVo.getBookSeq(), bookVo.getBookName(), bookVo.getAuthor());
                    bookDtoList.add(bookDto);
                    // userDto의 book_dto_list에 bookVoList의 값을 세팅
                    userDto.setBook_dto_list(bookDtoList);
                }
                // userDtoList에 userDto 객체 추가
                userDtoList.add(userDto);
            }
            return userDtoList;
        }
    }

    controller

    반환 data를 view 없이 Http Body에 실어 HttpStatus 200코드와 함께 반환한다.

    @RestController는 @Controller와 @ResponseBody가 합쳐진 애노테이션이다.

    package study.lsyrestapitest1.controller.api;
    
    import lombok.RequiredArgsConstructor;
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.http.HttpStatus;
    import org.springframework.http.ResponseEntity;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    import study.lsyrestapitest1.domain.dto.UserDto;
    import study.lsyrestapitest1.domain.vo.UserVo;
    import study.lsyrestapitest1.service.UserService;
    
    import java.util.HashMap;
    import java.util.List;
    import java.util.Map;
    
    @RestController
    @Slf4j
    @RequiredArgsConstructor
    @RequestMapping(value = "/api/v1/users")
    public class UserApiController {
    
        private final UserService userService;
    
        @GetMapping(value = "")
        public ResponseEntity<?> findUserList() {
        
            Map<String, Object> resultMap = new HashMap<>();
            
            try {
                resultMap.put("resultData", userService.findUserList());
                resultMap.put("resultCode", "0000");
                resultMap.put("resultMsg", "정상적으로 처리되었습니다.");
                return new ResponseEntity(resultMap, HttpStatus.OK);
            } catch (Exception e) {
                log.info("exception :: {}", e.getMessage());
                resultMap.put("resultCode", "9999");
                resultMap.put("errorMsg", e.getMessage());
                resultMap.put("resultMsg", "내부 서비스 오류입니다.");
                return new ResponseEntity(resultMap, HttpStatus.BAD_REQUEST);
            }
        }
    }

    테스트

    내가 원한 데이터 구조대로 잘 반환이 된다.

    Map({})안에 list([])안에 uerDto객체({book_dto_list[]})가 담겨 있고 http 상태코드 200이 반환되었다.

    처음에는 객체안에 객체타입 리스트가 있는 구조가 무척 헷갈렸는데 익숙해지니 좀 나아졌다. 

    실무는 더 복잡한 구조가 많기에 이 간단한 구조부터 이해가 되어야 응용이 가능하다.

    프로젝트  생성 후 패키지명만 변경해서 구현하면 결과값이 잘 나올 것이다.

    이렇게 mybatis에서 계층 매핑하는 방법도 있고 쿼리로 데이터 한번에 가져와서 list에 담아 java단에서 for문 돌려서 매핑하는 방법이 있다.

    반응형