Springboot 에서 DATA-JPA(Hibernate) 사용하기[3] - JOIN

샘플

지금부터 설명하는 것들에 대한 샘플은 https://github.com/doohwan-yoo/jpa-sample 에서 확인할 수 있다

테이블 만들기

지난 시간에 얘기했던 것처럼 JPA 를 사용해서 여러 테이블에 관계를 맺도록 해볼려고 한다.

먼저 간단한 구조를 가지는 테이블 두개를 만들어 보자

CREATE TABLE `tbl_user` (
  `user_no` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `user_name` varchar(45) NOT NULL,
  PRIMARY KEY (`user_no`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8;

CREATE TABLE `tbl_user_extra` (
  `user_no` int(10) unsigned NOT NULL,
  `phone_num` varchar(11) NOT NULL,
  KEY `fk_user_extra_01_idx` (`user_no`),
  CONSTRAINT `fk_user_extra_01` FOREIGN KEY (`user_no`) REFERENCES `tbl_user` (`user_no`) ON DELETE NO ACTION ON UPDATE NO ACTION
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

첫번 째 테이블은 사용자 번호(PK)와 사용자 이름을 갖고, 두번 째 테이블은 사용자 번호(FK) 와 전화번호를 갖는 테이블을 구성 했다.

Entity 설정

위에 설정한 테이블과 맵핑이 될 테이블을 만들자

@Entity
@Table(name = "tbl_user")
public class User extends BaseUser{

    @Id
    @GeneratedValue(strategy= GenerationType.IDENTITY)
    @Column(name = "user_no", updatable = false, insertable = false)
    private Integer userNo;

    @Column(name = "user_name")
    private String userName;

    public Integer getUserNo() {
        return userNo;
    }

    public void setUserNo(Integer userNo) {
        this.userNo = userNo;
    }
    public String getUserName() {
        return userName;
    }

    public void setUserName(String userName) {
        this.userName = userName;
    }

    // @OneToOne(fetch = FetchType.LAZY, cascade={CascadeType.ALL})
    @OneToOne(cascade={CascadeType.ALL})
    @JoinColumn(name="user_no", insertable = false)
    private UserExtra userExtra;

    public UserExtra getUserExtra() {
        return userExtra;
    }

    public void setUserExtra(UserExtra userExtra) {
        this.userExtra = userExtra;
    }
}

첫번 째 테이블이다. 사용자 번호 컬럼은 auto_increment 가 동작하도록 GenerateValue 를 설정해 주었다. 여기부터 핵심 인데, 하단에 UserExtra 테이블과 관계를 설정하도록 하였는데 1:1 에 관계를 설정했다. @OneToMany @ManyToOne 과 같이 여러 형태의 Annotation 을 지원한다. cascade 속성은 update 나 delete 시 하위 테이블도 함께 수정 되도록 하는 역할을 한다.

@Entity
@Table(name = "tbl_user_extra")
public class UserExtra {

    @Id
    @Column(name = "user_no")
    private Integer userNo;

    @Column(name = "phone_num")
    private String phoneNum;


    public Integer getUserNo() {
        return userNo;
    }

    public void setUserNo(Integer userNo) {
        this.userNo = userNo;
    }

    public String getPhoneNum() {
        return phoneNum;
    }

    public void setPhoneNum(String phoneNum) {
        this.phoneNum = phoneNum;
    }
}

두번째 테이블이다. 이전 글을 봤다면 잘 이해가 갈 것이다.

Repository 만들고 데이터 입력하기

해당 Entity 를 맵핑하고 CRUD 동작을 위해서 다음의 Repository interface 를 생성했다.

public interface UserRepository extends JpaRepository<User, Integer> {
}

그리고 Repository 를 사용할 Service 를 하나 만들고 저장 및 업데이트 하도록 했다.

@Service
public class DemoService {

    private UserRepository userRepository;

    public DemoService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Transactional
    public User userSaveAndUpdate(User input) {
        return userRepository.save(input);
    }
}

그리고 테스트 할 Controller 도 하나 만들어 주었다.

@RequestMapping(value="/test", method = RequestMethod.POST)
    public @ResponseBody
    User test(@RequestBody User input) {

        return demoService.userSaveAndUpdate(input);
    }

이제 테스트를 위해 Postman 을 이용해 봤다. (유닛 테스트를 해도 무관 하다)

성공적으로 저장이 이뤄졌다. 같은 url로 업데이트도 가능한데 여기서 부터 흥미로운 점이 있다. 아래와 같이 User Entity 의 구조대로 JSON 리퀘스트를 보내면

이렇게!, user table 은 업데이트를 하고, user_extra 테이블은 insert를 하게 된다. 물론 기존에 데이터가 있다면 update 동작을 하게 된다. 얼마나 간편하게 동작을 하는가!! 예전에 처음으로 이러한 동작을 봤을 때 우워~~ 를 연발했었다.

Join 된 테이블 가져오기

이제 넣은 데이터를 가져와 보자. 그렇게 하기 위해서 Service 에서 기본키인 userNo로 데이터를 얻어오는 getUser 함수 하나를 만들었다.

@Service
public class DemoService {

    private UserRepository userRepository;

    public DemoService(UserRepository userRepository) {
        this.userRepository = userRepository;
    }

    @Transactional
    public User userSaveAndUpdate(User input) {
        return userRepository.save(input);
    }

    public User getUser(Integer id) {
        return userRepository.findOne(id);
    }
}

Controller 도 만들어준다. Controller 는 Restful 한 형식으로 만들었다.

경축! 아무것도 안하여 에스천사게임즈가 새로운 모습으로 재오픈 하였습니다.
어린이용이며, 설치가 필요없는 브라우저 게임입니다.
https://s1004games.com

@RequestMapping(value="/user/{id}", method = RequestMethod.GET)
    public @ResponseBody
    User getUser(@PathVariable Integer id) {

        return demoService.getUser(id);
    }

이제 결과를 한번 보자.

User 객체에 대한 요청만 했는데, 그 안에 Join된 UserExtra 도 함께 얻어온다.

여기서 눈여겨 볼 점이 하나 있는데 Hibernate 가 Query를 수행하는 방식이다 쿼리를 어떻게 가져왔는지 보자

select
  user0_.user_no as user_no1_0_0_,
  user0_.user_name as user_nam2_0_0_,
  userextra1_.user_no as user_no1_1_1_,
  userextra1_.phone_num as phone_nu2_1_1_
from
  tbl_user user0_ left outer join
  tbl_user_extra userextra1_
on
  user0_.user_no=userextra1_.user_no
where
  user0_.user_no=?

위에 보는 것과 같이 User 에 대해서 요청을 하자마자 연관된 객체에 대해서 join 동작을 수행해서 가져왔다. 이러한 방식을 hibernate 에서는 (Eager 방식이라고 한다)

그렇다면 다른 방식도 있을까? User Entity 를 다시 한번 보자

    @OneToOne(fetch = FetchType.LAZY, cascade={CascadeType.ALL})
    @JoinColumn(name="user_no", insertable = false)
    private UserExtra userExtra;

관계를 설정 하는 부분에 FetchType 을 Lazy 로 해보자. 그런 다음 다시 요청을 해보자.

결과는?

Hibernate: select user0_.user_no as user_no1_0_0_, user0_.user_name as user_nam2_0_0_ from tbl_user user0_ where user0_.user_no=?

Hibernate: select userextra0_.user_no as user_no1_1_0_, userextra0_.phone_num as phone_nu2_1_0_ from tbl_user_extra userextra0_ where userextra0_.user_no=?

위와 같이 Join 을 수행하는 것이 아니라. 각 table 에 대해서 두번의 질의를 요청한다. 그렇다면 비효율 적인 것이 아니냐? 라는 의문을 가질 수 있는데 실제로 두번째 쿼리가 호출 될 때는 UserExtra 객체에 대해서 직접 데이터를 요청할 때이다. 예를 들어 보겠다

public User getUser(Integer id) {
    User user = userRepository.findOne(id);
    logger.debug("사용자 번호 : " + user.getUserNo());
    return null;
}

위와 같이 User 안에 UserExtra 객체에 대해서 호출이 일어나지 않는다면 두번째 쿼리는 실행디지 않는다.

public User getUser(Integer id) {
    User user = userRepository.findOne(id);
    logger.debug("사용자 번호 : " + user.getUserNo());
    logger.debug("이 순간 쿼리 실행 : " + user.getUserExtra().getPhoneNum());
    return null;
}

Eager 에 경우 불필요한 조인이 매번 일어날 수 있다. Lazy의 경우는 그러한 것을 방지해 준다. Eager에 경우 N+1 쿼리 실행에 문제도 있다. 언관 구조를 잘 보고 Fetch 계획을 세우는 것이 중요하다.

여기까지 data-jpa 기본 연재를 마치겠다. 다음은 Querydsl 에 대해 준비해 보도록 할까 한다.

 

[출처] https://doohwan-yoo.github.io/data-jpa-3/

 

 

 

본 웹사이트는 광고를 포함하고 있습니다.
광고 클릭에서 발생하는 수익금은 모두 웹사이트 서버의 유지 및 관리, 그리고 기술 콘텐츠 향상을 위해 쓰여집니다.
번호 제목 글쓴이 날짜 조회 수
27 [java][maven] jar 파일 의존성 한번에 다운로드 maven 사용 졸리운_곰 2023.08.24 192
26 Prometheus + Grafana로 Java 애플리케이션 모니터링하기 file 졸리운_곰 2020.12.17 279
25 Blockchain Implementation With Java Code file 졸리운_곰 2019.06.16 338
24 Java 코드로 이해하는 블록체인(Blockchain) 졸리운_곰 2019.06.16 377
23 순수 Java Application 코드로 Restful api 호출 졸리운_곰 2018.10.10 415
22 WebDAV 구현을 위한 환경 설정 file 졸리운_곰 2017.09.24 268
21 [Java] Apache Commons HttpClient로 SSL 통신하기 졸리운_곰 2017.03.27 784
20 JSoup를 이용한 HTML 파싱 졸리운_곰 2017.03.04 320
19 jsoup을 활용해서 Java에서 HTML 파싱하는 방법 정리 file 졸리운_곰 2017.03.04 579
18 NSA의 Dataflow 엔진 Apache NiFi 소개와 설치 file 졸리운_곰 2017.01.23 630
17 wordpress-java-integration 자바와 워드프레스 통합 졸리운_곰 2016.12.30 298
16 Create New Posts in Wordpress using Java and XMLRpc 졸리운_곰 2016.11.14 268
15 자바로 POST 방식으로 통신하기, java httppost 클래스를 활용한 예제 졸리운_곰 2016.11.14 647
14 [Java]아파치 HttpClient사용하기 file 졸리운_곰 2016.11.14 301
13 Building a Search Engine With Nutch Solr And Hadoop file 졸리운_곰 2016.04.21 435
12 Nutch and Hadoop Tutorial file 졸리운_곰 2016.04.21 391
11 Latest step by Step Installation guide for dummies: Nutch 0. file 졸리운_곰 2016.04.21 305
10 Nutch 초간단 빌드와 실행 졸리운_곰 2016.04.21 681
9 Nutch로 알아보는 Crawling 구조 - Joinc 졸리운_곰 2016.04.21 536
8 A tiny bittorrent library Java: 자바로 만든 작은 bittorrent 라이브러리 file 졸리운_곰 2016.04.20 418
대표 김성준 주소 : 경기 용인 분당수지 U타워 등록번호 : 142-07-27414
통신판매업 신고 : 제2012-용인수지-0185호 출판업 신고 : 수지구청 제 123호 개인정보보호최고책임자 : 김성준 sjkim70@stechstar.com
대표전화 : 010-4589-2193 [fax] 02-6280-1294 COPYRIGHT(C) stechstar.com ALL RIGHTS RESERVED