목차
역시나 미래의 나를 위해 기록한다.
계층구조 조회가 처음 접하고 익숙해지기 전까지는 좀 어렵다.(물론 지금도 쉬운 건 아니다.)
모든 일이 익숙해지면 아무것도 아닌 게 그전까지는 어려운 법이다.
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와 통신 용도)
네이밍 룰은 서버단이니 카멜케이스로 선언
아래 인텔리제이의 플러그인을 사용하면 좀 수월하다.
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문 돌려서 매핑하는 방법이 있다.
'IT > development' 카테고리의 다른 글
[devTool] postman으로 파일 전송 테스트 (0) | 2023.01.30 |
---|---|
[spring] @RequestBody String type 받기 (0) | 2023.01.24 |
[springBoot/html] table값 변경 후 DB update(feat.contenteditable) (0) | 2022.12.17 |
[티스토리] 코드블럭 클립보드에 복사 추가하기(feat. clipboard.js) (0) | 2022.12.17 |
[mybatis] mybatis multi update(다중 업데이트) (0) | 2022.12.16 |