IDE는 이클립스만 사용해보다가 최근에 Mac OS를 사용하면서 Intellij로 갈아타게 되었습니다. 물론 무엇을 쓰든 상관없었지만, 트렌드에 쓰잘대 없이 민감한 제가… 큰 마음먹고 한번 사용해보았습니다. 아직은 익숙하지가 않지만 간단하게 Intellij에서 스프링 mvc를 Mybatis 모듈과 연동하여 웹 어플리케이션을 구현해보는 시간을 리뷰해 보았습니다.

먼저, 꽤 저렴하지 않는 금전으로 Intellij Ultimate 1년 구독신청 하였기 때문에 해당 IDE로 실행을 하겠습니다.

Intellij를 사용하여 Spring-Mybatis 연동

스크린샷 2019-10-05 오전 12 41 42

미리 만들어놓은 프로젝트의 구조는 다음과 같습니다. 생각보다 복잡해서… 프로젝트 생성과정은 생략하였습니다. 빌드 도구로는 역시… 이클립스에서 사용해왔던 maven을 이용하여 pom.xml 파일에서 의존성 라이브러리들을 관리합니다.

스크린샷 2019-10-05 오전 12 41 51

위의 프로젝트 구조에서 database.properties 파일과 classes, views디렉토리를 제외하고는 Intellij에서 maven과 스프링 mvc로 프로젝트를 생성하여 보여주는 기본구조입니다.

스프링-Mybatis 모듈 연동을 구현하기 전에 간단하게 프로젝트 구조에 대해서 리뷰해보겠습니다.
웹 어플리케이션이 톰캣에서 동작할때 가장 먼저 web.xml파일을 참조하는데 설정내용은 이클립스와 크게 다르지 않습니다.

스프링 설정파일

web.xml 파일

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns="http://xmlns.jcp.org/xml/ns/javaee"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/web-app_4_0.xsd"
         version="4.0">
    <context-param>
        <param-name>contextConfigLocation</param-name>
        <param-value>/WEB-INF/applicationContext.xml</param-value>
    </context-param>
    <listener>
        <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <servlet>
        <servlet-name>dispatcher</servlet-name>
        <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
        <load-on-startup>1</load-on-startup>
    </servlet>
    <servlet-mapping>
        <servlet-name>dispatcher</servlet-name>
        <url-pattern>/</url-pattern>
    </servlet-mapping>
</web-app>

dispatcher-servlet.xml 파일

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:mvc="http://www.springframework.org/schema/mvc"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans.xsd
       http://www.springframework.org/schema/mvc
       http://www.springframework.org/schema/mvc/spring-mvc.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context.xsd">

  <!-- 아래 annotation-driven 태그는 HandlerMapping, HandlerAdapter를 Bean으로 등록하여 요청 url를 controller와 매칭시켜준다.-->
    <mvc:annotation-driven/>

    <mvc:resources mapping="/resources/**" location="/resources/"/>

  <!-- 특정 패키지안의 클래스들을 스캔하고, annotation을 확인 후 bean 인스턴스를 생성한다.-->
    <context:component-scan base-package="com.jun"/>

    <bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/views/"/>
        <property name="suffix" value=".jsp"/>
    </bean>
</beans>

기존의 이클립스와의 차이점은 context파일 명이 servlet-context.xml, root-context.xml 파일이 아니고 dispatcher-servlet.xml, applicationContext.xml 입니다.

dispatcher-servlet.xml파일에서 view를 검색하는 역할을 하는 ViewResolver를 빈으로 등록하고 js,css같은 정적파일들은 WAS가 처리하지 않고, WEB 서버에게 위임합니다. 스프링 mvc를 사용한다고 하면, 대표적으로 DispatcherServlet를 등록하고, 모든 요청을 받게 설정을 하는데 /resources/** 로 요청이 오면 /resources/로 매핑시켜주어서 바로 처리할 수 있도록 설정하였습니다.

이제 간단하게 요청 url에 특정 id 값을 보내면 Mybatis에서 sqlSession 객체를 이용하여 해당하는 값을 DB 테이블에서 검색하여 view 화면에서 보여주는 예제를 구현해보겠습니다.

이러한 작업을 위해서… 역시 설정들을 간단한?? 설정을 해주면 됩니다.
그전에 pom.xml에서 mybatis, mybatis-spring, mysql-connector-java 의존성 라이브러리들을 pom.xml에 작성하여 해당 dependency들을 받아와야 됩니다.

mybatis는 sql 명령어를 관리해주는 편리한 라이브러리이고, 유지보수성이나 효율성이 기존에 jdbc에서 사용해왔던 방식보다 훨씬 뛰어납니다.

<!-- MyBatis, MyBatis-Spring, Spring-JDBC -->
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis</artifactId>
            <version>3.4.5</version>
        </dependency>

        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>

            <version>1.3.1</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-jdbc</artifactId>
            <version>4.1.4.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>8.0.11</version>
        </dependency>

mybatis와 mybatis를 연동하는 스프링 그리고 스프링과 jdbc를 연결하는 라이브러리입니다.

mybatis는 필연적으로 데이터베이스를 이용하는 sql문을 설정하는 라이브러리이기에 데이터베이스 개발환경에 꼭 필요하고 스프링 jdbc와 그를 연결하는 mybatis-spring 라이브러리가 필수적입니다.

아래의 그림이 설명이 잘돼어있는거 같아서 참고하였습니다.

스크린샷 2019-10-05 오전 12 42 05

이제 applicationContext.xml파일에서 데이터베이스 관련 설정을 해보겠습니다.

<?xml version="1.0" encoding="UTF-8"?>  
<beans xmlns\="[http://www.springframework.org/schema/beans](http://www.springframework.org/schema/beans)"  
xmlns:xsi\="[http://www.w3.org/2001/XMLSchema-instance](http://www.w3.org/2001/XMLSchema-instance)"  
xmlns:context\="[http://www.springframework.org/schema/context](http://www.springframework.org/schema/context)"  
xsi:schemaLocation\="[http://www.springframework.org/schema/beans](http://www.springframework.org/schema/beans)  
[http://www.springframework.org/schema/beans/spring-beans.xsd](http://www.springframework.org/schema/beans/spring-beans.xsd)  
[http://www.springframework.org/schema/context](http://www.springframework.org/schema/context)  
[http://www.springframework.org/schema/context/spring-context.xsd](http://www.springframework.org/schema/context/spring-context.xsd)"\>  

<context:property-placeholder location\="/WEB-INF/database.properties"/>  
<!--<context:component-scan base-package="com.jun"/>-->  

<!-- <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">  
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>  
<property name="url" value="${jdbc.url}"/>  
<property name="username" value="${jdbc.username}"/>  
<property name="password" value="${jdbc.password}"/>  
</bean>-->  

<bean id\="dataSource" class\="org.springframework.jdbc.datasource.DriverManagerDataSource"\>  
<property name\="driverClassName" value\="com.mysql.jdbc.Driver"/>  
<property name\="url" value\="${jdbc.url}"/>  
<property name\="username" value\="${jdbc.username}"/>  
<property name\="password" value\="${jdbc.password}"/>  
</bean\>  
<!-- Mysql <-> Mybatis를 연결해주는 객체 -->  
<bean id\="sqlSessionFactory" class\="org.mybatis.spring.SqlSessionFactoryBean"\>  
<property name\="dataSource" ref\="dataSource"/>  
<property name\="configLocation" value\="classpath:/mybatis-config.xml"/>  
<property name\="mapperLocations" value\="classpath:/mappers/\*.xml"/>  
</bean\>  

<bean id\="sqlSession" class\="org.mybatis.spring.SqlSessionTemplate"\>  
<constructor-arg ref\="sqlSessionFactory"\></constructor-arg\>  
</bean\>  
</beans\>

DB url, username, password 같은 정보는 보안상 따로 파일을 만들어서 변수를 사용하여 입력하는게 좋은 방법입니다.
<context:property-placeholder location="/WEB-INF/database.properties"/> 태그로 database.properties 파일을 해당 xml파일로 import를 하여 dataSource 빈에 property에 변수를 값으로 넣어줍니다.

sqlSessionFactory 빈에는 mybatis 설정파일과 sql문을 관리하는 mapper xml파일의 경로와 데이터베이스 연결정보들을 가지고 있는 dataSource 객체를 property 값으로 가지고 있습니다.

mybatis-config.xml, member-config.xml 파일은 src/main/resources 디렉토리 밑에 생성하여 만든 설정파일로써 해당경로를 클래스패스로 인식하기 때문에 값을 넣어줄때 classpath:를 써줘야 인식합니다. 안그러면 에러가 발생합니다.

sqlSessionFactory는 데이터베이스 연결과 sql문 실행에 대한 모든 것을 가진 가장 중요한 객체입니다.

여기까지 스프링 웹 어플리케이션에서 DB를 연결하기 위한 설정하는 방법입니다.
이제 자바코드를 이용하여 해당 웹 어플리케이션을 구현해보겠습니다.

  1. Controller
package com.jun.controller;

import com.jun.dto.MemberDto;
import com.jun.service.MemberService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class HomeController {


    @Autowired
    private MemberService memberService;

    @RequestMapping("/home")
    public String home(){

        return "homepage";
    }

    @RequestMapping("/index")
    public String index(){

        return "index";
    }

    @RequestMapping("/loginForm")
    public String loginForm(){

        return "loginForm";
    }
    //해당 url/id 값으로 요청이 들어오면 loginResultView 메소드 실행
    @RequestMapping("/loginResultView/{id}")
    public String loginTestResult(@PathVariable String id, Model model){

        MemberDto dto = memberService.selectMember(id);

        model.addAttribute("member", dto);

        return "loginResultView";
    }
}
  1. DTO
package com.jun.dto;

public class MemberDto {

    private String id;
    private String name;


    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}
  1. DAO
package com.jun.dao;

import com.jun.dto.MemberDto;
import com.jun.mapper.MemberMapper;
import org.apache.ibatis.session.SqlSession;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;

@Repository
public class MemberDao {

    @Autowired
    private SqlSession sqlSession;

    public MemberDto selectMember(final String id){

        MemberMapper mapper = sqlSession.getMapper(MemberMapper.class);

        MemberDto memberDto = mapper.selectMember(id);

        return  memberDto;
    }
}
  1. service
package com.jun.service;

import com.jun.dto.MemberDto;

public interface MemberService {
    public MemberDto selectMember(String id);
}
package com.jun.service;

import com.jun.dao.MemberDao;
import com.jun.dto.MemberDto;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class MemberServiceImpl implements MemberService {

    @Autowired
    private MemberDao memberDao;

    public MemberDto selectMember(String id) {

        MemberDto memberDto = memberDao.selectMember(id);

        return memberDto;
    }
}

sql문에서 테이블에서 select로 단일조회하여 저장할 때도 사용하는 용도로 작성한 파일입니다.

  1. mapper
package com.jun.mapper;

import com.jun.dto.MemberDto;

public interface MemberMapper {
    MemberDto selectMember(String id);
}

sqlSession 객체에서 해당 interface를 구현하여 MemberMapper 객체를 이용하여
member-mapper.xml파일에 정의되어 있는 sql파일을 실행하는 책임을 가지고 있습니다.

  1. mapper-mapper.xml
<?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="com.jun.mapper.MemberMapper">
    <select id="selectMember" parameterType="String" resultType="memberDto">
        SELECT name, id from TEST1 WHERE id =#{id}
    </select>
</mapper>

접근하고자 하는 테이블의 sql 명령어들을 정의한 xml 파일입니다.

  1. mapper-config.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE configuration
        PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>
    <typeAliases>
        <typeAlias alias="memberDto" type="com.jun.dto.MemberDto"/>
    </typeAliases>
</configuration>

mybatis-config.xml 파일은 Mybatis에 별도의 설정들을 정의합니다.
member-mapper.xml에서 정의된 sql문의 리턴타입을 dto로 사용할 경우에 태그를 사용하면 풀 패키지명 없이 간단하게 memberDto를 리턴타입으로 사용할 수 있습니다.

mysql을 실행하여 임의로 해당 테이블을 dto(Member)와 같은 타입으로 셍상히였습니다.
간단한 spring-mybatis 예제이기 때문에 따로 password는 암호화하여 넣지는 않았습니다.

스크린샷 2019-10-05 오전 12 42 19

위의 코드들을 작성했으면 이제 database에서 해당 값을 가져오는지 실행결과를 보겠습니다.

스크린샷 2019-10-05 오전 12 51 27

대충 작성하였지만 스프링 부트를 배우기 전에 다시한번 복습한다는 생각으로 mybatis-spring 연동예제를 구현해보았습니다. 처음에는 eclipse와 환경도 다르고 library 가져오는 방법도 약간 달라서 어려움이 있었지만… 역시 코딩은 삽질이 답인거 같네요. 제 부족한 점을 다시한번 짚어볼 수 있는 유익한 시간이였습니다.

클린 코더스 강의 1.OOP

백명석 선생님의 클린코드라는 강의가 나온지 6년이나 되었지만, 이제 막 프로그래밍 세계에 입문한 저한테는 정말 배울게 많은 강의였습니다. 가장 기억에 남는 말씀은 개발자들이 많이하는 실수는 객체지향 프로그래밍을 할때 기능을 중심으로 생각하는게 아니라 해당 클래스가 가지고 있는 데이터 중심으로 사고를 한다는거 였습니다.

에를들어 글이나 기사를 작성하는 서비스에 대한 네이밍을 WriteArticleService가 아니고 ArticleService라고 작명한는 것도 기능이 아니라 데이터 중심으로 생각을 해서 발생하는 문제입니다.

1강의 핵심은 돌아가는 코드도 중요하지만 사람이 읽을 수 있는 코드에 초점을 맞춘 강의였습니다. 리펙토링을 하는 습관을 항상 가져야되고… 아무리 바쁘더라도 리펙토링을 미루면 안됩니다. 개발을 빨리 한다고 해도 코드가 지저분하고 돌아가는데만 신경을 쓴다면 나중에 유지보수하는데 상당한 골치를 겪을 확률이 높습니다.

이번 시간에는 공통된 데이터나 프로세스를 제공하는 객체들을 하나의 타입(인터페이스)로 추상화 하는 코드에 대해서 배웠습니다.

스크린샷 2019-10-05 오전 12 29 16

위의 클래스 다이어그램에서 해당 객체들의 공통점은 바로 로그를 수집하고 iterator() 메소드를 이용해서 수집한 로그를 반복처리하는 역할을 하는 객체입니다.

만약 이 로직을 변경하지 않고 로그 수집 방법을 변경하기 위해서는 인터페이스를 재사용 하는 것입니다. 인터페이스를 재사용하면 Client는 구현 변경에 대해서 영향을 받지 않습니다. 한마디로 추상화를 통해 유연함을 얻기 위한 규칙입니다.

concrete class를 직접 사용하는 경우

public class FlowController{

    private final Parser parser = new Parser();

    public void process(){
        FileLogCollector collector = new FileCollector();

        FileLogSet logset = collector.collect();

        Iterator<String> it = logSet.iterate();

        FileLogWriter writer = new FileLogWriter();

        for(String line = it.next; line != null;){
            String parsedLine = parser.parse(line);
            writer.write(parsedLine);
        }
    }
}

인터페이스 추상화

public class FlowController{

    private final Parser parser = new Parser();

    public void process(){
        LogCollector collector = new FileCollector();

        LogSet logset = collector.collect();

        Iterator<String> it = logSet.iterate();

        LogWriter writer = new FileLogWriter();

        for(String line = it.next; line != null;){
            String parsedLine = parser.parse(line);
            writer.write(parsedLine);
        }
    }
}

위의 소스에서는 LogCollector, LogSet을 interface로 두어서 타입을 추상화 하여 클라이언트에게 구체적인 모습은 드러내지 않고 변경을 유연하다는 걸 보여줍니다.

아래에 백명석 강사님이 설명하신 변경에 유여한 코드를 따라서 만들어본 예제입니다.
mills 단위로 경과시간을 구해주는 ProceuduralStopWatch가 클래스를 설계하였습니다.

// 스탑워치의 역할을 해야하는 클래스
public class ProceduralStopWatch {

    private long startTime;
    private long stopTime;

    public long getElapsedTime(){
        return stopTime - startTime;
    }
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProceduralStopWatchTest {

    private long expectedElapsedTime = 100l;

    @Test
    public void should_return_elapsed_mills() {

        ProceduralStopWatch stopWatch = new ProceduralStopWatch();
        //mills 단위로 인스턴스 변수 초기화
        stopWatch.startTime = System.currentTimeMillis();

        doSomething();

        stopWatch.stopTime = System.currentTimeMillis();

         assertThat(elapsedTime, is(greaterThanOrEqualTo(expectedElapsedTime);

    }
}

위의 코드를 보면 doSomething() 메소드가 얼마나 걸리는지 밀리타임으로 경과된 시간을 구하는 프로그램입니다. 만약 여기서 nano 단위로 경과된 시간을 구하라는 요구사항이 추가 된다면 데이터 구조 변경이 유발됩니다.

public class ProceduralStopWatch {

    private long startTime;
    private long stopTime;
    private long startNanoTime;
    private long stopNanoTime;

    public long getElapsedTime(){
        return stopTime - startTime;
    }

    public long getElapsedNanoTime(){
        return stopNanoTime - startNanoTime;
    }   
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProceduralStopWatchTest {

    private long expectedElapsedTime = 100l;

    @Test
    public void should_return_elapsed_mills() {

        ProceduralStopWatch stopWatch = new ProceduralStopWatch();
        //mills 단위로 인스턴스 변수 초기화
        stopWatch.startTime = System.nanoTime();

        doSomething();

        stopWatch.stopTime = System.nanoTime();

        long elapsedTime = stopWatch.getElapsedNanoTime();
        assertThat(elapsedTime, is(greaterThanOrEqualTo(expectedElapsedTime * (long)pow(10, 6))));
    }
}

nano 단위의 경과 시간을 구하는 기능이 추가되었을 뿐인데 해당 데이터를 사용하는 모든 코드를 수정해야 합니다. 프로젝트 규모가 커질수록 이렇게 수정하는데 많은 시간이 할애됩니다.

객체지향으로 해당 클래스를 설계한다면 다음과 같이 클라이언트에 영향을 안마치는 코드를 짤수가 있습니다.

public class ProceduralStopWatch {

    private long startTime;
    private long stopTime;


    public void start(){
        this.startTime = System.nanoTime();
    }

    public void stop(){
        this.stopTime = System.nanoTime();
    }

    public Time getElapsedTime(){
        return new Time(stopTime - startTime);
    }
}
public class Time(){

    private long nano;

    public Time(long nano){
        this.nano = nano;
    }

    public void getMilliTime(){
        return (long)(nano / pow(10, 6));
    }

    public void geNanoTime(){
        return nano;
    }
}
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProceduralStopWatchTest {

    private long expectedElapsedTime = 100l;

    @Test
    public void should_return_elapsed_mills() {

        ProceduralStopWatch stopWatch = new ProceduralStopWatch();

        // startTime 필드에 값을 할당하지 않고 기능 실행
        stopWatch.start();

        doSomething();

        // stopTime 필드에 값을 할당하지 않고 기능 실행
        stopWatch.stop();

        Time time = stopWatch.getElapsedTime();    

         assertThat(time.getNanoTime, is(greaterThanOrEqualTo(expectedElapsedTime * (long)pow(10, 6))));

    }
}

이렇게 내부적으로 구현 내용을 감추게 되면 클라이언트에 영향을 안 미치게 됩니다.
Tell, Don’t Ask라는 뜻으로 데이터를 요청해서 변경하고 저장하라고 하지말고 무슨 기능을 실행하라는 뜻입니다. 데이터를 잘 알고 있는 객체에게 기능을 수행하라고 요청하면 클라이언트 입장에서는 그 객체가 어떤 방법을 수행해도 원하는 값만 잘 던져주면 되는거죠…

마지막으로 객체는 각각 역할을 가지고 있습니다. 역할이란 객체가 가지는 책임들의 집합을 의미합니다. 항상 객체지향적으로 사고하는 습 관을 기르기 위해서 데이터보다 기능중심으로 먼저 생각해야 된다는걸 숙지해야 합니다.

참조: 백명석의 클린코드

'SpringFramework > JAVA' 카테고리의 다른 글

프록시 패턴  (0) 2019.11.20
Object- 1장 객체, 설계  (0) 2019.11.04
Stream(스트림)  (0) 2019.10.05
@Annotation 이란?  (0) 2019.10.05
빌더 패턴(Builder Pattern)  (0) 2019.10.05

빌더 패턴(Builder Pattern)

빌더 패턴은 객체를 생성할 때 흔하게 사용하는 패턴입니다.
다음과 같은 코드 스타일로 객체를 생성하는 코드가 있다면, 빌더 패턴을 사용했다고 할 수 있습니다.

Member.Builder builder = new Member.Builder("sa1341", "junyoung");
       builder.age(28)
              .hobby("soccer");

        Member member = builder.build();

언제 Builder 패턴을 사용하는지는 Effective Java에는 다음과 같이 설명합니다.

규칙 2. 생성자 인자가 많을 때는 Builder 패턴 적용을 고라하라.

이펙티브 자바에서 말하는 빌더 패턴은 객체 생성을 깔끔하고 유연하게 하기 위한 기법입니다.

객체를 생성하는 방법을 3가지 정도 소개하고 있습니다.

  • 점층적 생성자 패턴(telescoping constructor pattern)
  • 자바빈 패턴
  • 빌더 패턴

점층적 패턴

먼저 점층적 생성자 패턴을 만드는 방법은 다음과 같습니다.

  1. 필수 인자를 받는 필수 생성자를 하나 만듭니다.
  2. 1개의 선택적 인자를 받는 생성자를 추가합니다.
  3. 2개의 선택적 인자를 받는 생성자를 추가합니다.
  4. 반복 수행
  5. 모든 선택적 인자를 다 받는 생성자를 추가합니다.
//점층적 생성자 패턴 코드의 예: 회원가입 관련 코드
public class Member{

    private final String id;
    private final String name;
    private final String hobby;

    //필수 생성자
    public Member(String id){
        this(id, "이름 비공개", "취미 비공개");
    }

    // 1개의 선택적 인자를 받는 생성자
    public Member(String id, String name){
        this(id, name, "취미 비공개");
    }

    //모든 선택적 인자를 다 받는 생성자
    public Member(String id, String name, String hobby){
        this.id = id;
        this.name = name;
        this.hobby = hobby;
    }
}

장점

new Member(“sa1341”, “이름 비공개”, “취미 비공개”) 같은 호출이 빈번하게 일어난다면, new Member(“sa1341”)로 대체할 수 있습니다.

단점

  • 다른 생성자를 호출하는 생성자가 많으므로, 인자가 추가되는 일이 발생하면 코드를 수정하기 어렵습니다. 또한 코드 가독성도 떨어집니 다.

  • 특히 인자수가 많을 때 호출 코드만 봐서는 의미를 알기 어렵습니다.

//호출 코드만으로는 각 인자의 의미를 알기 어렵습니다.
Car car = new Car(1,30.2,5,7,8,6,4);
Car car = new Car(3,35,210,24);
Car car = new Car(230,71);

자바빈 패턴

점층적 패턴의 대안으로 등장한 자바빈 패턴을 소개합니다.
이 패턴은 setter 메서드를 이용해 생성 코드를 읽기 좋게 만드는 것입니다.

Car car = new Car();
car.setName("Hyundai");
car.setNumber(240);

장점

  • 이제 각 인자의 의미를 파악하기 쉬워졌습니다.
  • 복잡하게 여러 개의 생성자를 만들지 않아도 됩니다.

단점

  • 객체의 일관성이 깨집니다.
    1회의 호출로 객체 생성이 끝나지 않습니다.
    즉 한번에 생성하지 않고 생성한 객체에 값을 계속 셋팅해주고 있습니다.

  • setter 메서드가 있으므로 변경 불가능(immutable)클래스를 만들 수가 없습니다.
    스레드 안전성을 확보하려면 점층적 생성자 패턴보다 많은 일을 해야 합니다.

빌더 패턴(Effective Java 스타일)

public class Member {

    private final String id;
    private final String name;
    private final int age;
    private final String hobbyy;

    public static class Builder {

        private final String id;
        private final String name;
        private int age;
        private String hobby;


        public Builder(String id, String name) {
            this.id = id;
            this.name = name;
        }

        public Builder age(int age){
            this.age = age;
            return this;
        }

        public Builder hobby(String hobby){
            this.hobby = hobby;
            return this;
        }

        public Member build() {
            return new Member(this);
        }

    }

    public Member(Builder builder) {
        this.id = builder.id;
        this.name = builder.name;
        this.age = builder.age;
        this.hobbyy = builder.hobby;
    }

    public String getId() {
        return id;
    }

    public String getName() {
        return name;
    }

    public int getAge() {
        return age;
    }

    public String getHobbyy() {
        return hobbyy;
    }
}

위와 같이 하면 다음과 같이 객체를 생성할 수 있습니다.

Member.Builder builder = new Member.Builder("sa1341", "junyoung");
        builder.age(28)
               .hobby("soccer");

         Member member = builder.build();

아래와 같이 사용할 수도 있습니다.

//각 줄마다 builder를 타이핑 하지 않아도 되어 편리합니다.
Member member = new Member.Builder("sa1341","junyoung") // 필수값 입력
    .age(28)
    .hobby("농구")
    .build(); // build() 메소드가 객체를 생성해 돌려줍니다.

장점

  • 각 인자가 어떤 의미인지 알기 쉽습니다.
  • setter 메소드가 없으므로 변경 불가능한 객체를 만들 수 있습니다.
  • 한번에 객체를 생성하므로 객체 일관성이 깨지지 않습니다.
  • build() 함수가 잘못된 값이 입력되었는지 검증하게 할 수도 있습니다.
참조: https://johngrib.github.io/wiki/builder-pattern/ (기계인간 종립)

'SpringFramework > JAVA' 카테고리의 다른 글

프록시 패턴  (0) 2019.11.20
Object- 1장 객체, 설계  (0) 2019.11.04
Stream(스트림)  (0) 2019.10.05
@Annotation 이란?  (0) 2019.10.05
클린코드 OOP의 장점  (0) 2019.10.05

다양한 연관관계 매핑

지난번 포스팅에 이어서 다양한 연관관계 매핑 방법에 대해서 포스팅 하겠습니다.
엔티티의 연관관계를 매핑할 때는 다음 3가지를 고려해야 합니다.

  • 다중성
  • 단방향, 양방향
  • 연관관계의 주인

먼저 연관관게가 있는 두 엔티티가 일대일 관계인지 일대다 관계인지 다중성을 고려해야 합니다. 두 엔티티 중 한쪽만 참조하는 단방향 관계인지 서로 참조하는 양방향 관계인지도 생각을 해봐야 하고, 마지막으로 양방향 관계이면 누가 연관관계의 주인인지도 정해야 합니다. 생각보다 간단해 보이지만… JPA를 사용할때 고려해야 할점이 한두가지가 아닌거 같습니다.

단방향, 양방향

테이블은 객체와는 다르게 외래 키 하나로 조인을 사용해서 양방향으로 쿼리가 가능하므로 사실상 방향이라는 개념이 없습니다. 반면에 객체는 참조용 필드를 가지고 있는 객체만 연관된 객체를 조회할 수 있습니다. 객체 관계에서 한쪽만 참조하는 것을 단방향 관계라고 하고, 양쪽이 서로 참조하는 것을 양방향 관계라고 합니다.

연관관계의 주인

데이터베이스는 외래 키 하나로 두 테이블이 연관관계를 맺습니다. 따라서 테이블의 연관관계를 관리하는 포인트는 외래키 하나입니다. 반면에 엔티티를 양방향으로 매핑하면 A -> B, B -> A 2곳에서 서로를 참조합니다. 따라서 객체의 연관관계를 관리하는 포인트는 2곳 입니다.
JPA에서는 두 객체 연관관계 중 하나를 정해서 데이터베이스 외래 키를 관리하는데 이것을 연관관계의 주인이라고 합니다. 따라서 A -> B 또는 B -> A 둘 중 하나를 정해서 외래 키를 관리해야 합니다. 외래 키를 가진 테이블과 매핑한 엔티티가 외래키를 관리하는게 효율적이므로 보통 이곳을 연관관계의 주인으로 선택합니다. 주인이 아닌 방향은 외래 키를 변경할 수 없고 읽기만 가능합니다.
즉, 객체 그래프 탐색만 가능하다는 거죠. 연관관계의 주인은 mappedBy 속성을 사용하지 않습니다.

다대일

대다일 관계의 반대 방향은 항상 일대다 관계고 일대다 관계의 반대 방향은 항상 다대일 관계입니다. 데이터베이스 테이블의 일(1), 다(N)관계에서 외래 키는 항상 다쪽에 있습니다. 따라서 객체 양방향 관계에서 연관관계의 주인은 항상 다쪽입니다.

다대일 단방향[N:1]

아래 코드를 통해서 회원 엔티티와 팀 엔티티의 다대일 단방향 연관관계를 간단하게 살펴보겠습니다.

@Entity
@NoArgsConstructor
@ToString(exclude = "team")
@Getter @Setter
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private String id;

    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team; //팀의 참조를 보관
}
@Entity
@NoArgsConstructor
@Getter @Setter
public class Team {

    @Id
    @Column(name = "TEAM_ID")
    private String id;

    private String name;
}

회원은 Member.team으로 팀 엔티티를 참조할 수 있지만 반대로 팀에는 회원을 참조하는 필드가 없습니다. 따라서 회원과 팀은 다대일 단방향 연관관계를 가집니다.

@ManyToOne
@JoinColumn(name = "TEAM_ID") // TEAM_ID 외래 키와 매핑
private Team team;

Member.team 필드로 회원 테이블의 TEAM_ID 외래 키를 관리합니다.

다대일 양방향[N:1, 1:N]

이번엔 위의 예제를 다대일 양방향 관계로 살펴보겠습니다.

@Entity
@NoArgsConstructor
@Getter
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private String id;

    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team; 

    public void setTeam(Team team){

        this.team = team;
        //무한루프에 빠지지 않도록 체크합니다.
        if(!team.getMembers().contains(this)){
            team.getMembers().add(this);
        }
    }
}
@Entity
@NoArgsConstructor
@Getter @Setter
public class Team {

    @Id
    @Column(name = "TEAM_ID")
    private String id;

    private String name;

    //컬렉션은 필드에서 바로 초기화 하는것이 안전합니다.
    //nul 문제에서 안전합니다. 하이버네이트는 엔티티를 영속화 할때, 컬렉션을 감싸서 하이버네이트가 제공하는 내장컬렉션으로 변경합니다. 
    @OneToMany(mappedBy = "team")
    private List<Member> members = new ArrayList<>();

    public addMember(Member member){
        this.members.add(member);
        if(member.getTeam() != this){ // 무한루프에 빠지지 않도록 체크함
            member.setTeam(this);
        }
    }
}
  • 양방향은 외래 키가 있는 쪽이 연관관계의 주인입니다.
    일대다와 다대일 연관관계는 항상 다(N)에 외래 키가 있습니다. 여기서는 다쪽인 MEMBER 테이블이 외래 키를 가지고 있으므로 Member.team이 연관관계의 주인입니다. JPA는 외래 키를 관리할 때 연관관계의 주인만 사용합니다. 주인이 아닌 Team.members는 조 회를 위한 JPQL이내 객체 그래프를 탐색할 때 사용합니다.

  • 양뱡향 연관관계는 항상 서로를 참조해야 합니다.
    어느 한 쪽만 참조하면 양방향 연관관계가 성립하지 않습니다. 항상 서로 참조하게 하려면 연관관계 편의 메소드를 작성하는게 좋은데 회 원의 setTeam() , addMember() 메소드가 이런 편의 메소드들입니다. 편의 메소드는 한 곳에만 작성하거나 양쪽 다 작성할 수 있는 데, 양쪽에 다 작성하면 무한루프에 빠지므로 주의해야 합니다. 위의 코드에서는 편의 메소드를 양쪽에다 작성해서 둘 중 하나만 호출하면 됩니다.

일대다

일대다 관계는 다대일의 관계의 반대 방향입니다. 일대다 관계는 엔티티를 하나 이상 참조할 수 있으므로 자바 컬렉션인 Collection, List, Set, Map 중에 하나를 사용해야 합니다.

일대다 단방향 1:N

@Entity
@Getter @Setter
public class Team {

    @Id @GeneratedValue
    @Column(name = "TEAM_ID")
    private String id;

    private String name;

    @OneToMany
    @JoinColumn(name = "TEAM_ID") //MEMBER 테이블의 TEAM_ID (FK)
    private List<Member> members = new ArrayList<>();
}
@Entity
@Setter
@Getter
public class Member {

    @Id @GeneratedValue
    @Column(name = "MEMBER_ID")
    private String id;

    private String username;
}

코드를 보면 일대다 단방향 관계는 약간 특이합니다. 팀 엔티티의 Team.members로 회원 테이블의 TEAM_ID 외래 키를 관리합니다. 보통 자신이 매핑한 테이블의 외래 키를 관리하는데, 이 매핑은 반대쪽 테이블에 있는 외래 키를 관리합니다. 그럴 수밖에 없는 것이 일대다 관계에서 외래 키는 항상 다쪽 테이블에 있습니다. 하지만 다 쪽인 Member 엔티티에는 외래 키를 매핑할 수 있는 참조 필드가 없습니다. 대신에 반대쪽인 Team 엔티티에만 참조 필드인 members가 있습니다. 따라서 반대편 테이블의 외래 키를 관리하는 특이한 모습입니다.

일대다 단방향 관계를 매핑할 때는 @JoinColumn을 명시해야 합니다. 그렇지 않으면 JPA는 연결 테이블을 중간에 두고 연관관계를 관리하는 조인 테이블 전략을 기본으로 사용해서 매핑합니다.

일대다 단방향 매핑의 단점은 매핑한 객체가 관리하는 외래 키가 다른 테이블에 있다는 점입니다. 본인 테이블에 외래 키가 있으면 엔티티 의 저장과 연관관계 처리가를 INSERT SQL 한 번으로 끝낼 수 있지만, 다른 테이블에 외래 키가 있으면 연관관계 처리를 위한 UPDATE SQL을 추가로 실행해야 합니다.

Member 엔티티는 Team 엔티티를 모릅니다. 그리고 연관관계에 대한 정보는 Team 엔티티의 members가 관리합니다. 따라서 Member 엔티티를 저장할 때는 MEMBER 테이블의 TEAM_ID 외래 키에 아무 값도 저장되지 않습니다. 대신 Team 엔티티를 저장할 때 Team.members의 참조 값을 확인해서 회원 테이블에 있는 TEAM_ID 외래 키를 업데이트 합니다.

이것만 보면… 굳이 이렇게 일대다 단방향 매핑보다는 다대일 양방향 매핑을 하는게 훨씬 편하고 간편하니… 다대일 양방향 매핑을 사용합시다.

일대다 양방향 1:N, N:1

일대다 양방향 매핑은 존재하지 않습니다. 대신 다대일 양방향 매핑을 사용해야 합니다. 더 정확히 말하자면 양방향 매핑에서 @OneToMany는 연관관계의 주인이 될 수 없습니다.
관계형 데이터베이스 특성상 일대다. 다대일 관계는 항상 다 쪽에 외래 키가 있습니다. 이런 이유로 @ManyToOne에는 mappedBy 속성이 없습니다.

일대일

일대일 관계는 양쪽이 서로 하나의 관계만 가집니다. 예를 들어 회원은 하나의 사물함만 사용하고 사물함도 하나의 회원에 의해서만 사용됩니다.

일대일 관계의 특징

  • 일대일 관계는 그 반대도 일대일 관계입니다.
  • 테이블 관계에서 일대다, 다대일은 항상 다(N)쪽이 외래 키를 가집니다. 반면에 일대일 관계는 주 테이블이나 대상 테이블 둘 중 어느곳 이나 외래 키를 가질 수 있습니다.
  1. 주테이블의 외래 키
    주 객체가 대상 객체를 참조하는 것처럼 주 테이블에 외래 키를 두고 대상 테이블을 참조합니다. 외래 키를 객체 참조와 비슷하게 사용 할 수 있어서 객체지향 개발자들이 선호합니다. 이 방법의 장점은 주 테이블이 외래 키를 가지고 있으므로 주 테이블만 확인해도 대상 테이블과 연관관계가 있는지 알 수 있습니다.

  2. 대상 테이블에 외래 키
    전통적인 데이터베이스 개발자들은 보통 대상 테이블에서 외래 키를 두는 것을 선호합니다. 이 방법의 장점은 테이블의 관계를 일대일에 서 일대다로 변경할 때 테이블 구조를 그대로 유지할 수 있습니다.

일대일 단방향 1:1

// 일대일 주 테이블에 외래 키, 단방향 매핑 코드
@Entity
@Getter
@Setter
public class User {

    @Id @GeneratedValue
    @Column(name = "user_id")
    private String id;

    private String username;

    @OneToOne
    @JoinColumn(name = "locker_id")
    private Locker locker;   
}
@Entity
@Getter @Setter
public class Locker {

    @Id @GeneratedValue
    @Column(name = "locker_id")
    private Long id;

    private String name;

}

일대일 관계이므로 객체 매핑에 @OneToOne을 사용했습니다. 참고로 이 관계는 다대일 단방향과 거의 비슷합니다.

일대일 양방향 1:1

// 일대일 주 테이블에 외래 키, 양방향 매핑 코드
@Entity
@Getter
@Setter
public class User {

    @Id @GeneratedValue
    @Column(name = "user_id")
    private String id;

    private String username;

    @OneToOne
    @JoinColumn(name = "locker_id")
    private Locker locker;   
}
@@Entity
@Getter @Setter
public class Locker {

    @Id @GeneratedValue
    @Column(name = "locker_id")
    private Long id;

    private String name;

    @OneToOne(mappedBy = "locker") // 연관관계의 주인이 아님을 선언
    private Member member;
}

양방향이므로 연관관계의 주인을 정해야 합니다.

다대다

관계형 데이터베이스는 정규화된 테이블 2개로 다대다 관계를 표현할 수 없습니다. 그래서 보통 다대다 관계를 일대다, 다대일 관계로 풀어내는 연결 테이블을 사용합니다.
예를 들어 회원들은 상품을 주문합니다. 반대로 상품들은 회원들에 의해 주문됩니다. 둘은 다대다 관계입니다.

스크린샷 2019-10-04 오전 2 42 15

필자가 아이패드로(깨알자랑) 다대다 관계를 그려봤습니다… 악필이라… 이해해주세요.

그래서 아래 그림처럼 중간에 연결 테이블을 추가해야 합니다. Member_Product 연결 테이블을 추가했습니다. 이 테이블은 사용해서 다대다 관계를 일대다, 다대일 관계로 풀어 낼 수 있습니다. 이 연결 테이블은 회원이 주문한 상품을 나타냅니다.

스크린샷 2019-10-04 오전 2 42 23

그런데 객체는 테이블과 다르게 객체 2개로 다대다 관계를 만들 수 있습니다. 예를들어 회원 객체는 컬렉션을 사용해서 상품들을 참조하면 되고 반대로 상품들도 컬렉션을 사용해서 회원들을 참조하면 됩니다.
@ManyToMany를 사용하면 다대다 관계를 편리하게 매핑할 수 있습니다.

다대다 단방향 N:N

마찬가지로 예제로 한번 보겠습니다.

@Entity
@Getter
@Setter
public class User {

    @Id @GeneratedValue
    @Column(name = "user_id")
    private String id;

    private String username;

    @ManyToMany
    @JoinTable(name = "MEMBER_PRODUCT", joinColumns = @JoinColumn(name = "user_id"), inverseJoinColumns =     @JoinColumn(name = "product_id"))
    List<Product> products = new ArrayList<Product>();
}
@Entity
@Getter
@Setter
public class Product {

    @Id
    @JoinColumn(name = "product_id")
    private String id;

    private String name;
}

위의 코드에서 회원 엔티티와 상품 엔티티를 @ManyToMany로 매핑했습니다. 여기서 중요한 점은 @ManyToMany와 @JoinTable을 사용해서 연결 테이블로 바로 매핑한 것입니다. 따라서 회원과 상품을 연결하는 회원_상품 엔티티 없이 매핑을 완료할 수 있습니다.

  • @JoinTable.name: 연결 테이블을 지정합니다. 여기서는 MEMBER_PRODUCT 테이블을 선택했습니다.

  • @JoinTable.joinColumns: 현재 방향인 회원과 매핑할 조인 컬럼 정보를 지정합니다. MEMBER_ID로 지정했음

  • @JoinTable.inverseJoinColumns:반대 방향인 상품과 매핑할 조인 컬럼 정보를 지정합니다. PRODUCT_ID로 지정했음

MEMBER_PRODUCT 테이블은 다대다 관계를 일대다, 다대일 관계로 풀어내기 위해 필요한 연결 테이블 뿐입니다. @ManyToMany로 매핑한 덕분에 다대다 관계를 사용할 때는 이 연결 테이블에 신경을 쓰지 않아도 됩니다.

@Test
// 각 메소드 마다 트랜잭션을 걸어준다.
// 시작점에 transaction.begin(), 끝점에 transaction.commit()이 달린다고 생각하자.
@Transactional
@Rollback(false)
public void manyToManyTest() {

    Product product = new Product();
    product.setId("productA");
    product.setName("상품A");
    em.persist(product);

    User user = new User();
    user.setId("member1");
    user.setUsername("회원1");
    user.getProducts().add(product);
    em.persist(user);
}

결과 값:

스크린샷 2019-10-04 오전 2 42 35

// 탐색
    @Test
    @Transactional
    @Rollback(false)
    public void find() {
        User user = em.find(User.class,"member1" );
        List<Product> products = user.getProducts();
        for (Product product : products) {
            System.out.println("Product.name = " +product.getName());
        }
 }

다대다 양방향 N:N

다대다 양방향은 역방향에도 @ManyToMany를 사용합니다. 그리고 양쪽 중 원하는 곳에 mappedBy로 연관관계의 주인을 지정합니다.

@Entity
public class Product{

    @Id
    private String id;

    @ManyToMany(mappedBy = "products") // 역방향 추가
    private List<Member> members;

}

다대다 양방향 연관관계는 다음처럼 설정하면 됩니다.
member.getProducts().add(product);
product.getMembers().add(member);

또한 연관관계 편의 메소드를 추가해서 관리하는 것이 편리합니다.

public void addProduct(Product product){

    products.add(product);
    product.getMembers().add(this);
}
출저: 자바 ORM 표준 JPA 프로그래밍

연관관계 매핑 기초

이번 공부의 목표는 객체의 참조와 테이블의 외래 키를 매핑하는 것이 목표입니다.

단방향 연관관계

연관관계 중에선 다대일(N:1) 단방향 관계를 가장 먼저 이해해야 합니다.
예를 회원과 팀의 관계를 통해서 설명하겠습니다.

  • 회원과 팀이 있습니다.
  • 회원은 하나의 팀에만 소속될 수 있다.
  • 회원과 팀은 다대일 관계다.

스크린샷 2019-10-04 오전 2 08 17

객체 연관관계

  • 회원 객체는 Member.team 필드(맴버변수)로 팀 객체와 연관관계를 맺습니다.
  • 회원 객체와 팀 객체는 단방향 관계입니다. 회원은 Member.team 필드를 통해서 팀을 알수 있지만 반대로 팀은 회원을 알 수 없습니다. 예를 들어 member -> team의 조회는 member.getTeam()으로 가능하지만 반대 방향인 team -> member를 접근하는 필드는 없습니다.

테이블 연관관계

  • 회원 테이블은 TEAM_ID 외래 키로 팀 테이블과 연관관계를 맺습니다.
  • 회원 테이블과 팀 테이블은 양방향 관계입니다. 회원 테이블의 TEAM_ID 외래 키를 통해서 회원과 팀을 조인할 수 있고 반대로 팀과 회원도 조인할 수 있습니다. 예를 들어 MEMBER 테이블의 TEAM_ID 외래 키 하나로 MEMBER JOIN TEAM과 TEAM JOIN MEMBER 둘 다 가능합니다.

ex) 아래와 같이 외래 키 하나로 양방향으로 조인하는지 살펴보겠습니다.
SELECT * FROM MEMBER M JOIN TEAM T ON M.TEAM_ID = T.TEAM_ID

다음은 반대인 팀과 회원을 조인하는 SQL 입니다.
SELECT * FROM TEAM T JOIN MEMBER M ON T.TEAM_ID = M.TEAM_ID

객체 연관관계와 테이블 연관관계의 가장 큰 차이점

참조를 통한 연관관계는 언제나 단방향 입니다. 객체간에 연관관계를 양방향으로 만들고 싶으면 반대쪽에도 필드를 추가해서 참조를 보관해야 합니다. 결국 연관관계를 하나 더 만들어야 합니다. 이렇게 양쪽에서 서로 참조하는 것을 양방향 연관관계라고 합니다.
하지만 정확히 이야기하면 이것은 양방향 관계가 아니고 서로 다른 단방향 관계 2개 입니다. 반면에 테이블은 외래 키 하나로 양방향으로 조인이 가능 합니다.

단방향 연관관계

class A{
    B b;
}
class B{}

양방향 연관관계

class A{
    B b;
}

class B{
    A a;
}

객체 연관관계 vs 테이블 연관관계 정리

  • 객체는 참조(주소)로 연관관계를 맺는다.
  • 테이블은 외래 키로 연관관계를 맺습니다.

객체는 참조를 사용하지만 테이블은 join을 사용합니다.

  • 참조를 사용하는 객체의 연관관계는 단방향입니다.
    A -> B(a.b)

  • 외래키를 사용하는 테이블의 연관관게는 양방향입니다.
    A JOIN B 가 가능하면 반대로 B JOIN A 도 가능합니다.

  • 객체를 양방향으로 참조하려면 단방향 연관관계를 2개 만들어야 합니다.
    A -> B(a.b)
    B -> A(b.a)

순수한 객체 연관관계

아래 코드는 순수하게 객체만 사용한 연관관계를 알기 위해서 작성하였습니다.
JPA를 사용하지 않는 순수한 회원과 팀 클래스 코드입니다.

public class Member {

    private String id;

    private Team team_id; //팀의 참조를 보관

    private String username;

    public String getId() {
        return id;
    }

    public void setId(String id) {
        this.id = id;
    }

    public Team getTeam_id() {
        return team_id;
    }

    public void setTeam_id(Team team_id) {
        this.team_id = team_id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}
public class Team {

    private String id;
    private String name;

    public String getId() {
        return id;
    }
    public void setId(String id) {
        this.id = id;
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }
}

회원1과 회원2를 팀에 소속시키는 코드입니다.

public static void main(String[] args){

    Member member1 = new Member("member1", "회원1");
    Member member2 = new Member("member2", "회원2");
    Team team1 = new Team("team1", "팀1");

    member1.setTeam(team1);
    member2.setTeam(team1);

    Team findTeam = member1.getTeam();
}

위의 코드를 보면 회원1과 회원2는 팀1에 소속 되었습니다. 그리고 다음 코드로 회원1이 속한 팀을 조회할 수 있습니다.

Team findTeam = member1.getTeam();

이처럼 객체는 참조를 사용해서 연관관계를 탐색할 수 있는데 이것을 객체 그래프 탐색이라고 합니다.

객체 관계 매핑

이제 JPA를 사용해서 둘을 매핑해 보겠습니다.

package com.web.community.domain;

import javax.persistence.*;

@Entity
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private String id;

    private String username;

    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team; //팀의 참조를 보관

    //연관관계 설정
    public void setTeam_id(Team team) {
        this.team = team;
    }

    // Getter, Setter ...
}
package com.web.community.domain;

import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.Id;

@Entity
public class Team {

    @Id
    @Column(name = "TEAM_ID")
    private String id;

    private String name;

    // Getter, Setter ...
}

위의 코드는 Member, Team 엔티티를 매핑한 코드입니다.

  • 객체 연관관계: 회원 객체의 Member.team 필드 사용
  • 테이블 연관관계: 회원 테이블의 MEMBER.TEAM_ID 외래키 컬럼을 사용
@ManyToOne
@JoinColumn(name = "TEAM_ID")
private Team team;

회원 엔티티에 있는 연관관계 패밍부분인데 연관관계를 매핑하기 위한 새로운 어노테이션들 있습니다.

@ManyToOne: 이름 그대로 다대일(N:1)관계라는 매핑 정보입니다. 회원과 팀은 다대일 관계입니다. 연관관계를 매핑 할 때 이렇게 다중성을 나타내는 어노테이션을 필수로 사용해야 합니다.

@JoinColumn(name = “TEAM_ID”): 조인 컬럼은 외래 키를 매핑할 때 사용합니다.
name 속성에는 매핑할 외래 키 이름을 지정합니다. 회원과 팀 테이블은 TEAM_ID 외래 키로 연관관계를 맺으므로 이 값을 지정하면 됩니다. 이 어노테이션은 생략할 수 있습니다.

  • @ManyToOne 주요 속성
    스크린샷 2019-10-04 오전 2 08 37

  • targetEntity 속성의 사용 예 입니다.

@OneToMany
private List<Member>members; //제네릭으로 타입 정보를 알 수 있습니다.

@@OneToMany(targetEntity=Member.class)
private List members; // 제네릭이 없으면 타입 정보를 알 수 없습니다.

연관관계 사용

실제로 연관관계를 등록, 수정, 삭제, 조회하는 예쩨를 코드로 알아보겠습니다.

저장

연관관계를 매핑한 엔티티를 아래와 같은 코드로 저장해보았습니다.

@RunWith(SpringRunner.class)
@SpringBootTest
public class CommunityApplicationTests {

    @Autowired
    MemberRepository memberRepository;

    @Autowired
    TeamRepository teamRepository;

    @Test
    public void testSave() {

        Team team1 = new Team("team1", "팀1");
        teamRepository.save(team1);

        Member member1 = new Member("member1", "맴버1");
        member1.setTeam(team1);

        memberRepository.save(member1);
    }
}

가장 핵심은 member1.setTeam(team1); 회원 -> 팀 참조 부분입니다.
memberRepository.save(member1);

회원 엔티티는 팀 엔티티를 참조하고 저장했습니다. JPA는 참조한 팀의 식별자(Team_id)를 외래 키로 사용해서 적절한 등록 쿼리를 생성합니다. 이때 실행한 SQL은 다음과 같습니다.

insert into team (team_id, name) values ('team1', '팀1')
insert into member (team_id, username, member_id) values ('team1', 'member1', '맴버1')

실제로 데이터가 잘 입력되었는지 MySQL 데이터베이스에서 확인해보았습니다.

스크린샷 2019-10-04 오전 2 08 47

조회

연관관계가 있는 엔티티를 조회하는 방법은 크게 2가지 입니다.

  1. 객체 그래프 탐색
  2. 객체지향 쿼리 사용(JPQL)
  • 객체 그래프 탐색
  • 방금 저장한 대로 회원1이 팀1에 소속해 있다고 가정합니다.
    member.getTeam()을 사용해서 member와 연관된 team 엔티티를 조회할 수 있습니다.
Optional<Member> findMember = memberRepository.findById("member1");
findMember.ifPresent(member -> System.out.println(member.getId() +" " + member.getUsername() + " " + member.getTeam().getId()));

이처럼 객체를 통해 여관된 엔티티를 조회하는 것을 객체 그래프 탐색이라고 합니다.

  • 객체지향 쿼리 사용
    객체지향 쿼리인 JPQL에서 연관관계를 어떻게 사용하는지 알아보겠습니다.
    예를들어서 회원을 대상으로 조회하는데 팀1에 소속된 회원만 조회하려면 회원과 연관된 팀 엔티티를 검색 조건으로 사용해야 합니다. SQL은 연관된 테이블을 조인해서 검색조건을 사용하면 됩니다. JPQL도 조인을 지원하지만 문법이 약간 다릅니다.

저같은 경우에는 스프링 부트로 개발을 하고 있기 때문에 책에 내온 내용과 다르게 JPQL을 짜서 조인을 해보았습니다.

//Repository 클래스에서 쿼리메소드를 작성하였습니다.
public interface MemberRepository extends CrudRepository<Member, String> {
    //테이블이 아니라 엔티티를 중심으로 쿼리를 작성하면 JPA가 분석하여 SQL을 만들어서 데이터베이스에 해당쿼리를 전송합니다. 
    @Query("select m from Member m join m.team t where t.name = ?1")
    public List<Member> findByMember(String id);   
}

 @Test
    public void testSave() {

        Team team1 = new Team("team1", "팀1");
        teamRepository.save(team1);

        Member member1 = new Member("member1", "맴버1");
        member1.setTeam(team1);

        memberRepository.save(member1);

        List<Member> resultList = memberRepository.findByMember("팀1");

        for(Member member : resultList){
            System.out.println(member.getUsername());
        }
    }
    // 결과 값: 맴버1

JPQL의 from Member m join m.team t 부분을 보면 회원이 팀과 관계를 가지고 있는 필드(m.team)를 통해서 Member와 Team을 조인했습니다. 그리고 where 절을 보면 조인한 t.name를 검색조건으로 사용해서 팀1에 속한 회원만 검색했습니다.

참고로 ?1은 첫번째로 들어온 파라미터를 바인딩 받는 문법입니다.

이때 실행되는 SQL은 다음과 같습니다.

SELECT m from Member m join Team t on m.TEAM_ID = t.TEAM_ID
WHERE TEAM_NAME = '팀1';

실행된 SQL과 JPQL을 비교하면 JPQL은 객체(엔티티)를 대상으로 하고 SQL보다 간결합니다.

수정

@Test
    public void updateRelation(){

        Team team1 = new Team("team1", "팀1");
        teamRepository.save(team1);

        Team team2 = new Team("team2", "팀2");
        teamRepository.save(team2);

        Member member1 = new Member("member1", "맴버1");
        member1.setTeam(team1);

        memberRepository.save(member1);


        Member findMember = memberRepository.findByMemberId("member1");

        System.out.println(findMember.getUsername());

        findMember.setTeam(team2);
}

실행되는 수정 SQL은 다음과 같습니다.

UPDATE MEMBER
SET TEAM_ID='team2', ...
WHERE ID='member1'

수정은 update() 같은 메소드가 없습니다. 단순히 불러온 엔티티의 값만 변경해두면 커밋할 때 플러시가 일어나면서 변경감지 기능이 작동합니다.
이것은 연관관계를 수정할 때도 같은데, 참조하는 대상만 변경하면 나머지는 JPA가 자동으로 처리합니다.

연관관계 제거

@Test
public void deleteRelation(){
    Member member1 = MemberRepository.findByMemberId("member1");
    member1.setTeam(null);
}

연관된 엔티티 삭제

연관된 엔티티를 삭제하려면 기존에 있던 연관관계를 먼저 제거하고 삭제해야 합니다. 그렇지 않으면 외래 키 제약조건으로 인해, 데이터베이스에서 오류가 발생합니다.
팀1에는 회원1과 회원2가 소속되어 있습니다. 이때 팀1을 삭제하려면 연관관계를 먼저
끊어야 합니다.

member1.setTeam(null);
member2.setTeam(null);
em.remove(team);

다음에는 양방향 연관관계에 대해서 자세히 공부 후 리뷰해보겠습니다.

양방향 연관관계

회원에서 팀으로 접근하는 다대일 단방향 매핑을 공부해보았습니다. 이번에는 반대 방향인 팀에서 회원으로 접근하는 관계를 추가해보겠습니다. 그래서 회원에서 팀으로 접근하고 반대 방향인 팀에서 회원으로 접근 할 수 있도록 양방향 연관관계로 매핑하겠습니다.

스크린샷 2019-10-04 오전 2 09 30

먼저 객체 연관관계를 보면 회원과 팀은 다대일 관계입니다. 반대로 팀에서 회원은 일대다 관게 입니다. 일대다 관계는 여러 건과 여러관계를 맺을 수 있으므로 컬렉션을 사용해야 합니다. Team.members를 List 컬렉션으로 추가했습니다.

  • 회원 -> 팀(member.team)
  • 팀 -> 회원(Team.members)

데이터베이스 테이블에서는 외래 키 하나로 양방향으로 조회 할 수 있습니다.
따라서 처음부터 양뱡향 관게입니다. 그러므로 데이터베이스에 추가 할 내용은 전혀 없습니다…

왜냐하면 처음에 언급한것 처럼 MEMBER JOIN TEAM <-> TEAM JOIN MEMBER도 가능하기 때문이죠

양방향 연관관계 매핑

회원 엔티티

@Entity
@NoArgsConstructor
@ToString(exclude = "team")
public class Member {

    @Id
    @Column(name = "MEMBER_ID")
    private String id;

    private String username;

    //@ManyToOne(fetch = FetchType.LAZY)
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team; //팀의 참조를 보관

    public Member(String id, String username) {
        this.id = id;
        this.username = username;
    }

    //연관관계 설정
    public void setTeam(Team team) {
        this.team = team;
    }
}

팀 엔티티

@Entity
@NoArgsConstructor
public class Team {

    @Id
    @Column(name = "TEAM_ID")
    private String id;

    private String name;

    //추가 
    @OneToMany(mappedBy = "team")
    List<Member> members = new ArrayList<>();

    public Team(String id, String name) {
        this.id = id;
        this.name = name;
    }
}

위의 팀 엔티티 코드에서 팀과 회원은 일대다 관계입니다. 따라서 List members를 추가했습니다. 그리고 일대다 관계를 매핑하기 위해서 @OneToMany 매핑 정보를 사용했습니다. mappedBy 속성은 양방향 매핑일 때 사용하는데
반대쪽 매핑이 Member.team이므로 team을 값으로 주었습니다.

일대다 컬렉션 조회

@Test
public void biDirection(){
List<Member> lists = teamRepository.getMemberList("team1");
    for (Member member : lists) {
        logger.info(member.getId() + " " + member.getUsername());
    }
}
//결과 member1 맴버1
//결과 member2 맴버2

연관관계의 주인

@OneToMany는 직관적으로 이해가 됩니다. 문제는 mappedBy 속성입니다. 단순히 @OneToMany만 있으면 되지 mapperBy는 왜 사용하는지 모르겠습니다. 사실 객체에는 연관관게/라는 것이 없습니다. 서로 다른 단방향 연관관계 2개를 애플리케이션 로직으로 잘 묶어서 양방향인 것처럼 보이게 할 뿐 입니다. 반면에 데이터베이스 테이블은 앞서 말한것 처럼 외래 키 하나로 양쪽이 서로 조인할 수 있습니다. 따라서 테이블은 외래 키 하나만으로 양방향 연관관계를 맺습니다.

객체 연관관계

  • 회원 -> 팀 연관관계 1개(단방향)
  • 팀 -> 회원 연관관계 1개(단방향)

테이블 연관관계

  • 회원 <-> 팀의 연관관계 1개(양방향)

다시 말해서 테이블은 외래 키 하나로 두 테이블의 연관관계를 관리합니다.
엔티티를 단방향으로 매핑하면 참조를 하나만 사용하므로 이 참조로 외래 키를 관리하면 됩니다. 그런데 엔티티를 양방향으로 매핑하면 회원 -> 팀, 팀 -> 회원 두곳에서 서로를 참조합니다. 따라서 객체의 연관관계를 관리하는 포인트는 2곳으로 늘어납니다.

여기서 문제점이 있습니다. 엔티티를 양뱡향으로 설정하면 객체의 참조는 둘인데 외래 키는 하나입니다. 따라서 둘 사이에 차이가 발생합니다.
이런 차이로 인해 JPA에서는 두 객체 연관관계 중 하나를 정해서 테이블의 외래키를 관리해야 하는데 이것을 연관관계의 주인이라 합니다.

양방향 매핑의 규칙: 연관관계의 주인

양방향 연관관계 매핑 시 지켜야할 규칙이 있는데 두 연관관계 중 하나를 연관관계의 주인으로 정해야 합니다. 연관관계의 주인만이 데이터베이스 연관관계와 매핑되고 외래 키를 관리(등록, 수정, 삭제)할 수 있습니다. 반면에 주인이 아닌 쪽은 읽기만 할 수 있습니다.

어떤 연관관계를 주인으로 정할지는 mappedBy 속성을 사용하면 됩니다.

  1. 주인은 mappedBy 속성을 사용하지 않습니다.
  2. 주인이 아니면 mappedBy 속성을 사용해서 속성의 값으로 연관관계의 주인을 지정해야 합니다.
  • 회원 -> 팀(member.team) 방향
public class Member {
    @ManyToOne
    @JoinColumn(name = "TEAM_ID")
    private Team team; //팀의 참조를 보관
}
  • 팀 -> 회원(team.members) 방향
public class Team { 
    @OneToMany(mappedBy = "team")
    List<Member> members = new ArrayList<>();
}

연관관계의 주인을 정한다는 것은 사실 외래 키 관리자를 선택하는 것입니다.

여기서는 회원 테이블에 있는 TEAM_ID 외래 키를 관리할 관리자를 선택해야 합니다.
만약에 회원 엔티티에 있는 Member.team을 주인으로 선택하면 자기 테이블에 있는 외래 키를 관리하면 됩니다. 하지만 팀 엔티티에 있는 Team.members를 주인으로 선택하면 물리적으로 전혀 다른 테이블의 외래 키를 관리해야 합니다. 왜냐하면 이 경우 Team.members가 있는 Team 엔티티는 TEAM 테이블에 매핑되어 있는데 관리해야할 외래 키는 MEMBER 테이블에 있기 때문입니다.

연관관계의 주인은 외래 키가 있는 곳으로 설정하면 됩니다.
주인이 아닌 Team.members에는 mappedBy=“team” 속성을 사용해서 주인이 아님을 설정하면 됩니다. 여기서 mappedBy의 값으로 사 용된 team은 연관관계의 주인인 Member 엔티티의 team 필드를 말합니다.

결론은 연관관계의 주인만 데이터베이스 연관관계와 매핑되고 외래 키를 관리할 수 있습니다. 주인이 아닌 반대편은 읽기만 가능하고 외래키를 변경하지는 못합니다.

양방향 연관관계의 주의점

양방향 연관관계를 설정하고 가장 흔히 하는 실수는 연관관계의 주인에는 값을 입력하지 않고, 주인이 아닌 곳에만 값을 입력하는 경우입니다. 데이터베이스에 외래 키 값이 정상적으로 저장되지 않으면 이것부터 의심해봐야 합니다.

이것도 코드로 예제를 확인해 보겠습니다.

public void testSaveNonOwner(){

        Team team1 = new Team("team1", "팀1");
        teamRepository.save(team1);

        //맴버1 저장
        Member member1 = new Member("member1", "맴버1");
        memberRepository.save(member1);
        //맴버2 저장
        Member member2 = new Member("member2", "맴버2");
        memberRepository.save(member2);

        //주인이 아닌 곳만 연관관계 설정
        team1.getMembers().add(member1);
        team1.getMembers().add(member2);
        teamRepository.save(team1);
}

회원을 조회한 결과 값

스크린샷 2019-10-04 오전 2 09 41

외래 키 TEAM_ID에 team1이 아닌 null 값이 입력되어 있는데, 연관관계의 주인이 아닌 Team.members에만 값을 저장했기 때문입니다. 다시 한 번 강조하지만 연관관계의 주인만이 외래 키의 값을 변경할 수 있습니다.

순수한 객체까지 고려한 양방향 연관관계

객체지향 관점에서 양쪽 방향에 모두 값을 입력해주는것이 가장 안전합니다.
양쪽 방향 모두 값을 입력하지 않으면 JPA를 사용하지 않는 순수한 객체 상태에서 심각한 문제가 발생할 수 있습니다.

예를 들어 JPA를 사용하지 않고 엔티티에 대한 테스트 코드를 작성한다고 가정해봅시다. ORM은 객체와 관계형 데이터베이스 둘 다 중요합니다. 데이터베이스뿐만 아니라 객체도 함께 고려해야 합니다.

public void test순수한객체_양방향(){

    //팀1
    Team team1 = new Team("team1", "팀1");
    //맴버1 저장
    Member member1 = new Member("member1", "맴버1");
    //맴버2 저장
    Member member2 = new Member("member2", "맴버2"); 

    member1.setTeam(team1);
    member2.setTeam(team1);

    List<Member> members = team1.getMembers();
    System.out.println("members.size = " + members.size());
}
//결과: members.size = 0

예제코드는 JPA를 사용하지 않는 순수한 객체입니다. 코드를 보면 Member.team에만 연관관계를 설정하고 반대 방향은 연관관계를 설정하지 않았습니다. 그래서 결국 팀에 소속된 회원이 몇 명인지를 출력해보면 0이 출력됩니다. 이것은 우리가 기대하는 양방향 연관관계가 아닙니다.

양방향은 양쪽 다 관계를 설정해야 합니다.

public void test순수한객체_양방향(){  

    Team team1 = new Team("team1", "팀1");
    Member member1 = new Member("member1", "맴버1");
    Member member2 = new Member("member2", "맴버2"); 

    member1.setTeam(team1);
    team1.getMembers().add(member1);

    member2.setTeam(team1);
    team1.getMembers().add(member2);

    List<Member> members = team1.getMembers();
    System.out.println("members.size = " + members.size());
}
//결과: members.size = 2

위의 코드는 양쪽 모두 관계를 설정했고, 기대했던 2가 출력되었습니다.
이제 JPA를 사용해서 위의 코드를 완성해봅십다.

public void testORM_양방향(){  

    Team team1 = new Team("team1", "팀1");
    teamRepository.save(team1);

    Member member1 = new Member("member1", "맴버1");

    member1.setTeam(team1);
    team.getMembers().add(member1);
    memberRepository.save(member1);

    Member member2 = new Member("member2", "맴버2"); 

    member2.setTeam(team1);
    team.getMembers().add(member2);
    memberRepository.save(member2);
}

위 코드에서 양쪽에 연관관계를 설정했습니다. 순수한 객체 상태에서도 동작하며, 테이블의 외래 키도 정상 입력됩니다. 물론 외래 키 값은 연관관계의 주인인 Member.team 값을 사용합니다.

member.team: 연관관계의 주인, 이 값으로 외래 키를 관리합니다.
Team.getMembers().add(member1); // 주인이 아님. 저장 시 사용되지 않습니다.

결론은 객체의 양방향 연관관계는 객체 테이블 모두 관계를 맺어주여야 합니다.

연관관계 편의 메소드 작성 시 주의사항

사실 setTeam() 메소드에는 버그가 있습니다.

member1.setTeam(teamA); //1
member1.setTeam(teamA); //2
List<Member> memberList = teamA.getMembers(); //여전히 member1이 조회

위의 코드를 보면 먼저 member1.setTeam(teamA)를 호출한 직후 모습입니다.(그림실력이... 악마수준이라 생각하시고 보시기를...)

스크린샷 2019-10-04 오전 2 09 49

다음으로 member.setTeam(teamB)를 호출한 직후 객체 연관관계인 그림을 봅시다.

스크린샷 2019-10-04 오전 2 09 56

문제는 teamB로 변경할 때 teamA -> member1 관계를 제거하지 않는 겁니다…
연관관계를 변경할 때는 기존 팀이 있으면 기존 팀과 회원의 연관관계를 삭제하는 코드를 추가해야 합니다. 아래 코드처럼 수정합니다.

public void setTeam(Team team){

    if(this.team != null){
        this.team.getMembers().remove(this);
    }
    this.team = team;
    team.getMembers().add(this);
}

이 코드는 객체에서 서로 다른 단방향 연관관계 2개를 양뱡향인 것처럼 보이게 하려고 얼마나 많은 고민과 수고가 필요한지를 보여주고 있습니다. 반면에 관계형 데이터베이스는 외래 키 하나로 문제를 단순히 해결합니다.
결국엔 양방향 연관관계를 사용하려면 로직을 견고하게 작성해야 합니다.

양방향의 장점은 반대방향으로 객체 그래프 탐색 기능이 추가된 것뿐입니다.

  • 단방향 매핑만으로 테이블과 객체의 연관관계 매핑은 이미 완료되었습니다.
  • 단방향을 양방향으로 만들면 반대방향으로 객체 그래프 탐색 기능이 추가됩니다.
  • 양방향 연관관계를 매핑하려면 객체에서 양쪽 방향을 모두 관리해야 합니다.
출저: 자바 ORM 표준 JPA 프로그래밍

엔티티 매핑

JPA를 사용하는데 가장 중요한 일은 엔티티와 테이블을 정확히 매핑하는 일입니다.
매핑 어노테이션을 숙지하고 사용해야 하는데 JPA는 다양한 매핑 어노테이션을 지원합니다.
아래와 같이 크게 4가지로 분류할 수 있습니다.

  • 객체와 테이블 매핑: @Entity, @Table
  • 기본 키 매핑: @Id
  • 필드와 컬럼 매핑: @Column
  • 연관관계 매핑: @ManyToOne, @JoinColumn, @OneToMany

@Entity

JPA를 사용해서 테이블과 매핑할 클래스는 @Entity 어노테이션을 필수로 붙여야 합니다.
@Entity가 붙은 클래스는 JPA가 관리하는 것으로, 엔터티라 부릅니다.

스크린샷 2019-10-04 오전 1 38 40

@Entity 적용 시 주의사항

  • 기본 생성자는 필수다(파라미터가 없는 public 또는 protected 생성자).
  • final 클래스, enum, interface, inner 클래스에는 사용 할 수 없다.
  • 저장할 필드에 final을 사용하면 안 된다.

JPA가 엔터티 객체를 생성할 때 기본 생성자를 사용하므로 이 생성자는 반드시 있어야 합니다. 자바는 생성자가 하나도 없으면 다음과 같이 기본 생성자를 자동으로 만듭니다.
ex)

public Member() {} //기본 생성자

만약 생성자를 하나 이상 만들면 자바는 기본 생성자를 자동으로 만들지 않기 때문에 이때는 개발자가 직접 기본 생성자를 만들어야 합니다.

public Member() {} //직접 만든 기본 생성자

//임의의 생성자
public Member(String name){
this.name = name;
}

@Table

@Table은 엔터티와 매핑할 테이블을 지정한다. 생략하면 매핑한 엔터티 이름을 테이블 이름으로 사용합니다.

스크린샷 2019-10-04 오전 1 38 50

다양한 매핑 사용

JPA 시작하기 장에서 개발하던 회원 관리 프로그램에 다음 요구사항이 추가 되었습니다.

  1. 회원은 일반 회원과 관리자로 구분됩니다.
  2. 회원 가입일과 수정일이 있어야 합니다.
  3. 회원을 설명할 수 있는 필드가 있어야 합니다. 이 필드는 길이 제한이 없습니다.
    아래와 같이 위 요구사항을 만족하는 회원 엔터티에 기능을 추가 할 수 있습니다.
@Entity
@Table(name = "MEMBER") // Member 엔티티를 테이블 명 MEMBER로 매핑
public class Member

@Id //인스턴스 변수 id를 DB 테이블 MEMBER 기본키 ID로 매핑
@Column(name = "ID")
private String id;

@Column(name = "NAME")
private String username;

private Integer age;
//자바의  enum을 사용해서 회원의 타입을 구분하였습니다. 일반 회원은 USER, 관리자는
//ADMIN입니다. 자바의 enum을 사용하려면 @Enumerated 어노테이션으로 매핑해야 합니다.
@Enumerated(EnumType.STRING)
private RoleType roleType;

//자바의 날짜 타입은 @Temporal을 사용해서 매핑해야 합니다. 
@Temporal(TemporalType.TIMESTAMP)
private Date createDate;

@Temporal(TemporalType.TIMESTAMP)
private Date lastModifiedDate;

//회원을 설명하는 필드는 길이 제한이 없고, 따라서 데이터베이스 varchar 타입 대신에
//CLOB 타입으로 저장해야 합니다. @Lob를 사용한다면 CLOB, BLOB 타입을 매핑할 수 있습니다.
@Lob
private String description;


public enum RoleType{
    ADMIN, USER
}

데이터베이스 스키마 자동 생성 기능

JPA는 데이터베이스 스키마를 자동으로 생성하는 기능을 지원합니다. 클래스의 매핑정보를 보면 어떤 테이블에 어떤 컬럼을 사용하는지 알 수 있습니다. JPA는 이 매핑정보와 데이터베이스 방언을 사용해서 데이터베이스 스키마를 생성합니다.

저 같은경우에는 스프링 부트에서 생성한 application.yml 파일에 아래와 같이 정의합니다.

spring:
  jpa:
    hibernate:
      ddl-auto: create

이 속성을 추가하면 어플리케이션 실행 시점에 데이터베이스 테이블을 자동으로 생성합니다.

스크린샷 2019-10-04 오전 1 39 03

운영 서버에서 create, create-drop, update처럼 DDL을 수정하는 옵션은 절대 사용하면 안됩니다. 오직 개발 서버나 개발 단계에서만 사용해야 합니다. 이 옵션들은 운영 중인 데이터베이스 컬럼을 삭제할 수 있기 때문입니다.

  • 개발 초기 단계는 create 또는 update
  • 초기화 상태로 자동화된 테스트를 진행하는 개발자 환경과 CI 서버는 create 또는 create-drop
  • 테스트 서버는 update 또는 validate
  • 스테이징과 운영 서버는 validate 또는 none

DDL 생성 기능

회원 이름은 필수로 입력되어야 하고, 10자를 초과하면 안 되는 제약조건이 추가되었습니다.
스키마 자동 생성하기를 통해 만들어지는 DDL에 이 제약조건을 추가해 봅시다.

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

    @Id
    @Column(name = "ID")
    private String id;

    @Column(name = "NAME", nullable = false, length = 10) // 추가
    private String username;
}

위 코드에서 @Column 매핑정보의 nullable 속성 값을 false로 지정하면 자동 생성되는 DDL에 not null 제약조건을 추가할 수 있습니다. 그리고 length 속성 값을 사용하면 자동 생성되는 DDL에 문자의 크기를 지정할 수 도 있습니다.

기본키 매핑

JPA가 제공하는 데이터베이스 기본 키 생성 전략은 다음과 같습니다.

  • 직접할당: 기본 키를 어플리케이션에서 직접 할당합니다.
  • 자동생성: 대리 키 사용 방식

IDENTITY: 기본 키 생성을 데이터베이스에 위임한다.

SEQUENCE: 데이터베이스 시퀀스를 사용해서 기본 키를 할당한다.

TABLE: 키 생성 테이블을 사용한다.

자동 생성 전략이 이렇게 다양한 이유는 데이터베이스 벤더마다 지원하는 방식이 다르기 때문입니다. 오라클은 시퀀스를 제공하지만, MySQL은 시퀀스를 제공하지 않고 대신에 기본 키 값을 자동으로 채워주는 AUTO_INCREMENT 기능을 제공합니다. 따라서 SEQUENCE나 IDENTITY 전략은 사용하는 데이터베이스에 의존합니다.

//IDENTITY 매핑 코드
@Entity
public class Board{

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
}
//IDENTITY 사용 코드
private static void logic(EntityManager em){

    Board board = new Board();
    em.save(board);
    System.out.println("board.id = " + board.getId());
}

위의 코드는 em.save()를 호출해서 엔터티를 저장한 직후에 할당된 식별자 값을 출력하였습니다. 출력된 값 1은 저장 시점에 데이터베이스가 생성한 값을 JPA가 조회한 것입니다.

IDENTITY 전략과 최적화

IDENTITY 전략은 데이터를 데이터베이스에 INSERT한 후에 기본 키 값을 조회할 수 있다.따라서 엔터티에 식별자 값을 할당하려면 JPA는 추가로 데이터베이스를 조회해야 합니다.

주의사항
엔터티가 영속 상태가 되려면 식별자가 반드시 필요합니다. 그런데 IDENTITY 식별자 생성 전략은 엔터티를 데이터베이스에 저장해야 식별자를 구할 수 있으므로 em.save()를 호출하는 즉시 INSERT SQL이 데이터베이스에 전달됩니다. 따라서 이전 략은 트랜잭션을 지원하는 쓰기지연이 동작하지 않습니다.

SEQUENCE 전략

데이터베이스 시퀀스는 유일한 값을 순서대로 생성하는 특별한 데이터베이스 오브젝트 입니다.
SEQUENCE 전략은 이 시퀀스를 사용해서 기본 키를 생성합니다. 이 전략은 시퀀스를 지원하는 오라클, H2, PostgreSQL에서 사용할 수 있습니다.

@Entity
@SequenceGenerator(
    name = "BOARD_SEQ_GENERATOR",
    sequenceName = "BOARD_SEQ", //매핑할 데이터베이스 시퀀스 이름
    initialValue = 1, allocationSize = 1)
public class Board{


    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE,
    generator = "BOARD_SEQ_GENERATOR")
    private Long id;
}

위의 코드에서 BOARD_SEQ_GENERATOR라는 시퀀스 생성기를 등록했습니다. 이 시퀀스 생성기를 실제 데이터베이스 BOARD_SEQ 시퀀스와 매핑합니다.
이제부터 id 식별자 값은 BOARD_SEQ_GENERATOR 시퀀스 생성기가 할당합니다.

//SEQUENCE 사용 코드
private static void logic(EntityManager em){

    Board board = new Board();
    em.save(board);
    System.out.println("board.id = " + board.getId());
}

IDENTITY 전략과 SEQUENCE 전략의 차이점
시퀀스 사용코드는 IDENTITY 전략과 같지만 내부 동작 방식은 다릅니다. SEQUENCE 전략은 em.save()를 호출 할 때 먼저 데이터베이스 시퀀스를 사용해서 식별자를 조회합니다. 그리고 조회한 식별자를 엔터티에 할당한 후에 엔터티를 영속성 컨텍스트에 저장합니다. 이후 트랜잭션을 커밋해서 플러시가 일어나면 데이터베이스에 저장을 합니다. 반대로 이전에 설명했던 IDENTITY 전략은 먼저 엔터티를 데이터 베이스에 저장한 후에 식별자를 조회해서 엔터티의 식별자에 할당합니다.

필드와 컬럼 매핑: 레퍼런스

@Enumerated

자바의 enum 타입을 매핑할 때 사용합니다.

스크린샷 2019-10-04 오전 1 39 21

ex)

enum RoleType{
    ADMIN, USER
}

다음은 enum 이름으로 매핑합니다.
@Enumerated(EnumType.STRING)
private RoleType roleType;

member.setRoleType(RoleType.ADMIN); //-> DB에 문자 ADMIN으로 저장됩니다.

@Enumerated를 사용하면 편리하게 enum 타입을 데이터베이스에 저장할 수 있습니다.

EnumType.ORDINAL은 enum에 정의된 순서대로 ADMIN은 0, USER은 1값이 데이터베이스에 저장됩니다.

  • 장점: 데이터베이스에 저장되는 데이터 크기가 작습니다.

  • 단점: 이미 저장된 enum의 순서를 변경할 수 없습니다.
    EnumType.STRING은 enum 이름 그대로 ADMIN은 ‘ADMIN’, USER는 'USER’라는 문자로 데이터베이스에 저장된다.

  • 장점: 저장된 enum의 순서가 바뀌거나 enum이 추가되어도 안전합니다.

  • 단점: 데이터베이스에 저장되는 데이터 크기가 ORDINAL에 비해서 큽니다.

@Transient

이 필드는 매핑하지 않습니다. 따라서 데이터베이스에 저장하지 않고 조회되지도 않습니다. 객체에 임시로 어떤값을 보관하고 싶을때 사용합니다.

@Transient
private Integer temp;

@Access

JPA가 엔터티 테이블에 접근하는 방식을 지정합니다.
테이블에 매핑할때 필드에 적용된 매핑정보를 읽은 다음에 테이블에 매핑을 하기 때문에 필드 접근방식을 지정하는 방법을 다르게 설정할 수 있습니다.

  • 필드 접근: AccessType.FIELD로 지정합니다. 필드에 직접 접근합니다. 필드 접근 권한이 private이어도 접근할 수 있습니다.

  • 프로퍼티 접근: AccessType.PROPERTY로 지정합니다. 접근자 Getter를 사용합니다.

//필드 접근 코드
@Entity
@Access(AccessType.FIELD)
public class Member{

    @Id
    private String id;

    private String data1;

    private String data2;

    ...
}

@Id가 필드에 있으므로 @Access(AccessType.FIELD)로 설정한 것과 같습니다. 따라서 @Access는 생략해도 됩니다.

//프로퍼티 접근 코드
@Entity
@Access(AccessType.PROPERTY)
public class Member{


    private String id;

    private String data1;

    private String data2;

     @Id
     public String getId(){
         return id;
     }

     @Column
     public String getData1(){
         return data1;
     }

     public String getData2(){
         return data2;
     }
}

@Id가 프로퍼티에 있으므로 @Access(AccessType.PROPERTY)로 설정한 것과 같습니다. @Access는 생략해도 됩니다.

//필드, 프로퍼티 접근 코드 함께 사용
@Entity
public class Member{

    @Id
    private String id;

    @Transient
    private String firstName;

    @Transient
    private String lastName;

     @Access(AccessType.PROPERTY)
     public String getFullName(){
         return firstName + lastName;
     }
}

@Id가 필드에 있으므로 기본은 필드 접근 방식을 사용하고, getFullName()만 프로퍼티 접근 방식을 사용합니다. 따라서 회원 엔터티를 저장하면 회원 테이블의 FULLNAME 컬럼에 firstName + lastName의 결과가 저장됩니다.

출저: 자바 ORM 표준 JPA 프로그래밍

+ Recent posts