[hibernate] 단방향/양방향 연관성 구현 (OneToMany, ManyToOne)

개요

Object Relational Mapping(ORM)을 사용하기 위해 도메인 주도 설계(Domain Driven Design)를 공부하던 중에 단방향/양방향 연관성에 대해 알게 되었고, 그것을 구현하려면 OneToOne, OneToMany, ManyToOne, ManyToMany를 사용해야 한다.
그러나 막상 모델을 저장하려 했더니 원하는 테이블과 컬럼에 저장하는 것이 쉬운일이 아니였다. 예제를 보면서 확인해보자.

예제

먼저 예제는 부서인 Department 안에 속하는 사람들을 Member 로 표현하겠다.


테이블 구조


 tbl_dept

 dept_no


 tbl_member

 member_no

dept_no 


목표 : 객체 간의 양방향/단방향 레퍼런스를 위 테이블 구조로 저장


예제1 : 양방향 (Department <-> Member)


Department.java

package com.tistory.jekalmin.domain;

import java.util.Collection;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@Entity
@Table(name="tbl_dept")
public class Department {

    @Id
    @GeneratedValue
    private int deptNo;

    @OneToMany(mappedBy="dept")
    private Collection<Member> members;


}

mappedBy 이후에 들어오는 문자열은 Member.java에서 Department를 가리키는 변수명과 일치해야한다.

Member.java

package com.tistory.jekalmin.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name="tbl_member")
public class Member {

    @Id
    @GeneratedValue
    private int memberNo;

    @ManyToOne
    @JoinColumn(name="dept_no")
    private Department dept;

}

JoinColumn은 명시하지 않아도 알아서 ID를 찾지만, 그럴 경우 컬럼 명이 원하는 대로 생성되지 않기 때문에 JoinColumn을 이용해 명시해주었다.



예제2 : 단방향 (Department <- Member)


이번 예제에서는 Member에서만 Department를 참조하는 단방향 예제이다.

Department.java

package com.tistory.jekalmin.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.Table;

@Entity
@Table(name="tbl_dept")
public class Department {

    @Id
    @GeneratedValue
    private int deptNo;

}

Member.java

package com.tistory.jekalmin.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name="tbl_member")
public class Member {

    @Id
    @GeneratedValue
    private int memberNo;

    @ManyToOne
    @JoinColumn(name="dept_no")
    private Department dept;

}

양방향과 다른 점이라면 Department에서는 Member를 더이상 가지고 있지 않다.



예제3 : 단방향 (Department -> Member)


이번에는 Department에서만 Member를 참조하는 단방향 예제이다.

Department.java

package com.tistory.jekalmin.domain;

import java.util.Collection;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.OneToMany;
import javax.persistence.Table;

@Entity
@Table(name="tbl_dept")
public class Department {

    @Id
    @GeneratedValue
    private int deptNo;

    @OneToMany
    @JoinColumn(name="dept_no")
    private Collection<Member> members;

}

Member.java

package com.tistory.jekalmin.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;

@Entity
@Table(name="tbl_member")
public class Member {

    @Id
    @GeneratedValue
    private int memberNo;

}

이전 예제들과 약간 다른 점이라면, 이전에는 JoinColumn을 썼던 이유가 원하는 컬럼명을 명시하기 위해서 였는데, 이번 예제에서는 JoinColumn을 사용하지 않으면 전혀 다른 테이블 구조가 생성이 된다. 그리고 Department.java의 JoinColumn 안에 들어가는 컬럼명은 tbl_member 에서 tbl_dept를 참조할 컬럼명이다.

JoinColumn을 제외하고 실행하면 아래와 같은 테이블이 생성된다.

create table tbl_dept (dept_no integer generated by default as identity (start with 1), primary key (dept_no));

create table tbl_dept_members (tbl_dept_deptNo integer not null, members_memberNo integer not null);

create table tbl_member (member_no integer generated by default as identity (start with 1), primary key (member_no));

tbl_dept_members라는 원하지 않았던 테이블이 생기고, tbl_member는 dept_no를 가지고 있지 않은 구조로 된다.

그래서 JoinColumn을 이전 두 예제에서는 생략해도 컬럼명만 바뀌지만, 이 경우엔 테이블 구조가 예상치 못한 결과가 나왔다.



결론

OneToMany, ManyToOne 어노테이션은 위에 달아만 주면 알아서 구조를 만들어 주기는 하지만, 그 구조가 내가 생각했던 테이블과 컬럼 구조 인지는 잘 확인해 보시길 바란다. 위 예제는 전부 같은 테이블 구조를 생성한다.


spring boot + spring-data-jpa 설정

개요

이전 글인 (http://jekalmin.tistory.com/entry/springdatajpa-%EA%B8%B0%EB%B3%B8-%EC%98%88%EC%A0%9C) 에서 봤듯이 spring-data-jpa 를 설정하려면 xml에 상당히 많은 설정이 필요한 것을 볼 수 있다. 이런 많은 설정들이 spring boot를 사용하면서 기본으로 많이 제공해준다. 예제를 보면서 얼마나 간편해졌는지 확인해보자.

예제

먼저 eclipse를 쓰고 있다면 STS(Spring Tool Suite) 플러그인을 설치하자. 설치하고 나면 New > Project > Spring Starter Project 로 프로젝트를 생성하자.

생성할 때 스타일을 사용할 것인지 선택할 수 있다. 예제에서는 JPA와 Web만 선택하겠다.

먼저 hsqldb 라이브러리를 추가하자.

<dependency>
    <groupId>org.hsqldb</groupId>
    <artifactId>hsqldb</artifactId>
</dependency>

자, 이제 설정 준비가 끝났다. 클래스를 작성해보자.


Member.java

package com.tistory.jekalmin.domain;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;

@Entity
public class Member {

    @Id
    @GeneratedValue
    private int memberSeq;
    private String name;
    private int age;

    /**
     * 다른 생성자를 만들었다면 기본 생성자를 따로 만들어 주는 것을 잊지말자.
     */
    public Member(){}

    public Member(String name, int age) {
        super();
        this.name = name;
        this.age = age;
    }

    public int getMemberSeq() {
        return memberSeq;
    }
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public int getAge() {
        return age;
    }
    public void setAge(int age) {
        this.age = age;
    }

    @Override
    public String toString() {
        return "Member [memberSeq=" + memberSeq + ", name=" + name + ", age="
                + age + "]";
    }

}


MemberRepository.java

package com.tistory.jekalmin.repository;

import org.springframework.data.repository.CrudRepository;

import com.tistory.jekalmin.domain.Member;

public interface MemberRepository extends CrudRepository<Member, Integer>{

}


MemberController.java

package com.tistory.jekalmin.controller;

import javax.annotation.Resource;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

import com.tistory.jekalmin.domain.Member;
import com.tistory.jekalmin.repository.MemberRepository;

@Controller
public class MemberController {

    @Resource
    private MemberRepository repository;

    @RequestMapping(value="/member/join")
    public void join(Member member){
        System.out.println( repository.save(member) );
    }

}


save 하고나서 저장된 객체를 리턴해주기 때문에 바로 print 찍어보았다.

http://localhost:8080/member/join?name=min&age=26 요청을 해본 결과는 다음과 같았다.

Member [memberSeq=1, name=min, age=26]

결론

이전의 어마어마한 세팅들이 다 어디갔나 싶을 정도로 boot에서는 기본 설정이 많이 내장되어 있는 것 같다. boot를 이용해서 만들면 설정은 hsqldb 라이브러리 추가하는 것이 전부였고, entity와 repository를 만들어서 바로 사용하니 된다.

설정을 변경할 필요가 있다면 지원하는 어노테이션을 검색해 봐야 겠지만, 기본 설정하나는 정말 쉬워진 것 같다.

[Java] Http 요청 보내기 (HttpClient)

개요

자바스크립트에서는 태그에 넣으면 host, pathname, protocol, port, search 등을 지원해서 url 관리하기가 편리하다.

하지만 자바에서는 Url을 핸들링하기가 쉽지않다. 그래서 아파치에서 제공하는 유틸을 소개하려 한다.

예제

  • Uri 생성하기

    먼저 http 요청을 하게 해주는 라이브러리를 추가해야 한다.

      <dependency>
          <groupId>org.apache.httpcomponents</groupId>
          <artifactId>httpclient</artifactId>
          <version>4.3.5</version>
      </dependency>
    

    그리고 아래와 같이 코드를 작성해준다.

    HttpTest.java

      package com.tistory.jekalmin;
    
      import java.io.IOException;
      import java.net.URI;
      import java.net.URISyntaxException;
    
      import org.apache.http.client.ClientProtocolException;
      import org.apache.http.client.utils.URIBuilder;
    
      public class HttpTest {
    
          public static void main(String[] args) throws URISyntaxException, ClientProtocolException, IOException {
    
              URI uri = new URI("http://jekalmin.tistory.com");
              uri = new URIBuilder(uri).addParameter("aaa", "bbb").addParameter("ccc", "ddd").build();
              System.out.println(uri);
    
          }
    
      }
    

    URIBuilder 클래스를 사용해서 처음부터 host, protocol, port 까지 정해주며 생성할 수도 있고, 예제와 같이 파라미터만 추가도 가능하다. 결과는 다음과 같다.

      http://jekalmin.tistory.com?aaa=bbb&ccc=ddd
    
  • http 요청하기

    이제 생성한 uri를 가지고 http 요청을 해보자. 아래와 같이 코드를 추가해준다.

    HttpTest.java

      package cpackage com.tistory.jekalmin;
    
      import java.io.IOException;
      import java.net.URI;
      import java.net.URISyntaxException;
    
      import org.apache.http.HttpEntity;
      import org.apache.http.HttpResponse;
      import org.apache.http.client.ClientProtocolException;
      import org.apache.http.client.HttpClient;
      import org.apache.http.client.methods.HttpGet;
      import org.apache.http.client.utils.URIBuilder;
      import org.apache.http.impl.client.HttpClientBuilder;
      import org.apache.http.util.EntityUtils;
    
      public class HttpTest {
    
          public static void main(String[] args) throws URISyntaxException, ClientProtocolException, IOException {
    
              URI uri = new URI("http://jekalmin.tistory.com");
              uri = new URIBuilder(uri).addParameter("aaa", "bbb").addParameter("ccc", "ddd").build();
    
              HttpClient httpClient = HttpClientBuilder.create().build();
              HttpResponse response = httpClient.execute(new HttpGet(uri)); // post 요청은 HttpPost()를 사용하면 된다. 
              HttpEntity entity = response.getEntity();
              String content = EntityUtils.toString(entity);
              System.out.println(content);
    
          }
    
      }
    

    4.0부터는 DefaultHttpClient 클래스를 사용했으나 4.3부터는 deprecated 된 상태이다. 대신 HttpClientBuilder를 사용하여 생성하기를 권장하고 있다.
    http 요청후 내용 받아오는 부분은 원래 entity.getContent() 인데 getContent()는 InputStream을 반환한다. 바로 String으로 받기 위해 EntityUtils를 사용했다.

  • Uri에서 파라미터 파싱하기

    host, port, path 등의 정보는 URI 객체에서 getHost() 등과 같은 메소드로 바로 가져올 수 있다. 하지만 파라미터의 경우는 제공하지 않는다.
    그래서 아파치에서 제공하는 유틸을 사용했다. 아래 예제를 보자.

    HttpTest.java

      package com.tistory.jekalmin;
    
      import java.io.IOException;
      import java.net.URI;
      import java.net.URISyntaxException;
      import java.util.List;
    
      import org.apache.http.NameValuePair;
      import org.apache.http.client.ClientProtocolException;
      import org.apache.http.client.utils.URIBuilder;
      import org.apache.http.client.utils.URLEncodedUtils;
    
      public class HttpTest {
    
          public static void main(String[] args) throws URISyntaxException, ClientProtocolException, IOException {
    
              URI uri = new URI("http://jekalmin.tistory.com");
              uri = new URIBuilder(uri).addParameter("aaa", "bbb").addParameter("ccc", "ddd").build();
              System.out.println(uri);
    
              List<NameValuePair> paramList = URLEncodedUtils.parse(uri, "utf-8");
              for(NameValuePair param : paramList){
                  System.out.println(param.getName() + " = " + param.getValue());
              }
          }
    
      }
    

    결과는 아래와 같다.

      http://jekalmin.tistory.com?aaa=bbb&ccc=ddd
      aaa = bbb
      ccc = ddd
    

    아파치에서는 URLEncodedUtils 라는 클래스를 제공한다. 위의 parse 외에도 거꾸로 변환해주는 format 기능도 있다.
    두 메소드는 오버로딩을 통해 다양한 파라미터 타입을 제공하기도 한다.

결론

예전에는 자바에서 http 요청을 보낼 때, string을 결합하여 uri를 직접 생성하거나 그 작업을 도와주는 유틸을 만들곤 했었는데, 이미 다 제공되고 있었다. EntityUtil과 URLEncodedUtils는 4.0 버전부터 지원한다.
코딩하다가 불편하다고 느낄때 무작정 유틸을 만드는 것은 많은 테스트를 안하면 안전하지 않을 뿐만 아니라 많은 시간이 소요된다. 내가 불편함을 느낀다면, 다른 사람들도 똑같이 느꼈을 것이다. 직접 만드는 것은 최후의 보루로 남겨두자.