제네릭이란?

자바 5부터 제네릭(Generic) 타입이 새롭게 추가 되었는데, 제네릭 타입을 이용함으로써 잘못된 타입이 사용 될 수 있는 문제를 컴파일 과정에서 제거할 수 있습니다. 사실 API 문서를 볼때마다 제네릭 표현이 많기 때문에 제네릭을 이해하지 못하면 API 도큐먼트를 정확히 이해할 수 없습니다. 이러한 이유로 제네릭 타입에 대해서 알아보고 간단한 예제코드를 작성하였습니다.

제네릭은 클래스와 인터페이스, 그리고 메소드를 정의할 때 타입(type)파라미터(parameter)로 사용할 수 있도록 합니다. 타입 파라미터는 코드 작성 시 구체적인 타입으로 대체되어 다양한 코드를 생성하도록 해줍니다.

제네릭을 사용하는 이유?

  • 컴파일 시 강한 타입 체크를 할 수 있습니다.
    자바 컴파일러 코드에서 잘못 사용된 타입 때문에 발생하는 문제점을 제거하기 위해 제네릭 코드에 대한 강한 타입 체크를 합니다. 실행 시 타입 에러가 나는 것보다는 컴파일 시에 미리 타입을 강하게 체크해서 에러를 사전에 방지하는 것이 좋습니다.

  • 타입 변환을 제거합니다.
    비제네릭 코드는 불필요한 타입 변환을 하기 때문에 프로그램 성능에 악영향을 미칩니다. 다음 아래 코드를 보면 List에 문자열 요소를 저장했지만, 요소를 찾아올 때는 반드시 String으로 타입 변환을 해야합니다.

List list = new ArrayList();
list.add("hello");
String str = (String) list.get(0);

다음과 같이 제네릭 코드로 수정하면 List에 저장되는 요소를 String 타입으로 국한하기 때문에 요소를 찾아올 때 타입 변환을 할 필요가 없어 프로그램 성능이 향상됩니다.

List<String> list = new ArrayList<String>();
list.add("hello");
String str = list.get(0); // 타입 변환을 하지 않습니다.

제네릭 타입(class, interface)

제네릭 타입은 타입을 파라미터로 가지는 클래스와 인터페이스를 말합니다. 제네릭 타입은 클래스 또는 인터페이스 이름 뒤에 "<>"부호가 붙고, 사이에 타입 파라미터가 위치합니다.

ex)

public class 클래스명<T> {...}
public interface 인터페이스명<T> {...}

타입 파라미터는 변수명과 동일한 규칙에 따라 작성할 수 있지만, 일반적으로 대문자 알파벳 한 글자로 표현합니다. 제네릭 타입을 실제 코드에서 사용하려면 타입 파라미터 구체적인 타입을 지정해야 합니다.

제네릭을 이용한 Box 클래스 예제

public class Box<T>{
    // 클래스 뒤에 <T> 타입 파라미터를 명시했기 때문에 변수의 타입으로 사용 가능합니다.
    private T t; 
    public T get() { return t; }
    public void set(T t){ this.t = t; }
}

이제 구체적인 타입으로 변경하는 코드를 작성하겠습니다.

// 타입 파라미터를 String 타입으로 변경
Box<String> box = new Box<String>();

// 타입 파라미터 T는 String 타입으로 변경되어 Box 클래스의 내부는 다음과 같이 자동으로 재구성 됩니다.
public class Box<String>{
    private String t;
    public String get() { return t; }
    public void set(String t) { this.t = t; }
}

타입 파라미터 T를 Integer 타입으로 변경한다고 하면 마찬가지로 같습니다.

Box<Integer> box = new Box<Integer>();
box.set(6) // 자동 Boxing
int value = box.get(); // 자동 UnBoxing

이와 같이 제네릭은 클래스를 설계할 때 구체적인 타입을 명시하지 않고, 타입 파라미터로 대체했다가 실제 클래스가 사용될 때 구체적인 타입을 지정함으로써 타입 변환을 최소화 시킵니다.

멀티 타입 파라미터(class<K,V,...>, interface<K,V,...>)

제네릭 타입은 두개 이상의 멀티 타입 파라미터를 사용할 수 있습니다. 이 경우 각 타입 파라미터를 콤마로 구분합니다.

ex) 멀티 타입 파라미터를 가진 제네릭 타입을 정의하고 호출하는 예제

public class Product<T, M> {

    private T t;
    private M m;

    public T getT() {
        return t;
    }

    public void setT(T t) {
        this.t = t;
    }

    public M getM() {
        return m;
    }

    public void setM(M m) {
        this.m = m;
    }
}

public class GenericExample {
    public static void main(String[] args) {
        Product<Tv, String> product1 = new Product<>();

        product1.setT(new Tv("삼성전자Tv"));
        product1.setM("디젤");

        Tv tv = product1.getT();
        String name = product1.getM();

        System.out.println(tv.getName() +" " + name);
    }
}

제네릭 메소드(<T,R> R method(T t))

제네릭 메소드는 매개 타입과 리턴 타입으로 파라미터를 갖는 메소드를 말합니다. 제네릭 메소드를 선언하는 방법은 리턴 타입 앞에 <>기호를 추가하고 타입 파라미터를 기술한 다음, 리턴 타입과 매개 타입으로 타입 파라미터를 사용하면 됩니다.

public <타입 파라미터,...> 리턴타입 메소드명(매개변수,...){...}

다음 boxing() 제네릭 메소드 <> 기호 안에 타입 파라미터 T를 기술한 뒤, 매개 변수 타입으로 T를 사용했고, 리턴 타입으로 제네릭 타입 Box를 사용했습니다.

public <T> Box<T> boxing(T t) {...}

제네릭 메소드는 두가지 방식으로 호출할 수 있습니다. 코드에서 타입 파라미터의 구체적인 타입을 명시적으로 지정해도 되고, 컴파일러가 매개값의 타입을 보고 구체적인 타입을 추정하도록 할 수 도 있습니다.

리턴타입 변수 = <구체적인 타입> 메소드명(매개 값);
리턴타입 변수 = 메소드명(매개 값);

ex) 제네릭 메소드 호출 예제

public class Util {

    //제네릭 메소드 선언방법: 리턴 타입 앞에 타입파라미터 기술 후에 리턴 타입과, 매개타입으로 타입 파라미터를 사용하면 됩니다.
    public static <T> Box<T> boxing(T t){
        Box<T> box = new Box<>();
        box.set(t);
        return box;
    }
}

public class BoxingMethodExample{
    public static void main(String[] args){
        // 매개 값의 타입으로 자바 컴파일러에서 타입을 추정합니다.
        Box<Integer> box = Util.boxing(100);
        int initValue = box.get();
        System.out.println(initValue);
    }
}

다음 예제는 Util 클래스에 정적 제네릭 메소드로 compare()를 정의하고 CompareMethodExample 클래스에서 호출했습니다. 타입 파라미터는 K,V로 선언되었는데, 제네릭 타입 Pair가 K와 V를 가지고 있기 때문입니다. compare() 메소드는 두 개의 Pair을 매개값으로 받아 K와 V 값이 동일한지 검사하고 boolean 값을 리턴한다.

public class Util{
    public static <K,V> boolean compare(Pair<K,V> p1, Pair<K,V> p2){

        boolean keyCompare = p1.getKey().equals(p2.getKey());
        boolean valueCompare = p1.getValue().equals(p2.getValue());
        return KeyCompare && valueCompare;
    }
}

public class Pair<K,V> {


    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public void setKey(K key) {
        this.key = key;
    }

    public void setValue(V value) {
        this.value = value;
    }

    public K getKey() {
        return key;
    }

    public V getValue() {
        return value;
    }

}

public class CompareMethodExample{
    public static void main(String[] args){
          Pair<Integer, String> p1 = new Pair<Integer, String>(1, "사과");
          Pair<Integer, String> p2 = new Pair<Integer, String>(1, "사과");

          // 구체적 타입을 명시적으로 지정합니다.
          boolean result1 = Util.<Integer, String>compare(p1,p2);
          if(result1){
               System.out.println("논리적으로 등등한 객체입니다.");
          }else{
              System.out.println("논리적으로 등등하지 않는 객체입니다.");
          }

          Pair<String, String> pair = new Pair<>("user1","홍길동");
          Pair<String, String> pair = new Pair<>("user2","홍길동");
          // 구체적인 타입을 주정합니다.
          boolean result2 = Util.compare(p3,p4);

          f(result2){
               System.out.println("논리적으로 등등한 객체입니다.");
          }else{
              System.out.println("논리적으로 등등하지 않는 객체입니다.");
          }

    }
}

제한된 타입 파라미터(<T extends 최상위타입>)

타입 파라미터에 지정되는 구체적인 타입을 제한할 필요가 종종 있습니다. 예를 들어 숫자를 연산하는 제네릭 메소드는 매개값으로 Number 타입 또는 하위 클래스 타입(Byte, Short, Double, Long, Integer)의 인스턴스만 가져와야 합니다. 이것이 제한된 타입 파라미터가 필요한 이유입니다.

ex)

// 제한된 타입 파라미터 정의
public <T extends 상위타입> 리턴타입 메소드(매개변수,...){...}

타입 파라미터에 지정되는 구체적인 타입은 상위 타입이거나 상위 타입의 하위 또는 구현 클래스만 가능합니다.

주의할점은 메소드의 중괄호 {} 안에서 타입 파라미터 변수로 사용 가능 한 것은 상위 타입의 맴버(필드, 메소드)로 제한됩니다. 하위 타입에만 있는 필드와 메소드는 사용할 수 없습니다. 아래 코드는 숫자 타입만 구체적인 타입으로 갖는 제네릭 메소드 compare() 입니다. 두 개의 숫자 타입을 매개 값으로 받아 차이를 리턴합니다.

    // 제한된 타입 파라미터 정의 구체적인 타입을 제한하기 위해 사용합니다.
    public static <T extends Number> int compare(T t1, T t2){

        double v1 = t1.doubleValue();
        double v2 = t2.doubleValue();

        return Double.compare(v1,v2);
    }

doubleValue() 메소드는 Number 클래스에 정의되어 있는 메소드로 숫자를 double 타입으로 변환합니다. Double.compare() 메소드는 첫 번째 매개값이 작으면 -1을, 같으면 0을, 크면 1을 리턴합니다.

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

스트림 처리 메소드 1편  (0) 2019.12.04
람다식을 통한 메소드 참조  (0) 2019.12.03
프록시 패턴  (0) 2019.11.20
Object- 1장 객체, 설계  (0) 2019.11.04
Stream(스트림)  (0) 2019.10.05

Git - 실전 가이드

VCS(Version Control System)는 버전관리 시스템으로 파일의 상태를 커밋 단위로 구분하여 버전을 관리해주는 시스템입니다. 백업용뿐만 아니라 협업용으로도 넘사벽으로 많은 개발자분들이 사용하는 협업 관리 툴입니다. 간단하게 Git은 거대한 오픈소스 프로젝트답게 정말 파워풀한 기능들을 제공하지만 Git을 활용하기 위해서는 그만큼 높은 런닝커브를 요구합니다. 간단하게 개인용으로 프로젝트를 만들면서 기존의 소스를 건드리지 않고 내 마음대로 테스트하고 소스가 잘못되는 경우에도 이전의 커밋 상태를 되돌릴 때 Git처럼 유용한 도구는 없을 것입니다. 하지만 너무나 많은 기능이 있기 때문에 자주 사용하는 기능을 위주로 정리해보았습니다.

세가지 상태

Git은 기본적으로 파일을 Commited, Modified, Staged 이렇게 세가지 상태로 관리합니다.

  • commited란 데이터가 로컬 데이터베이스에 안전하게 저장됐다는 것을 의미합니다.
  • Modified는 수정한 파일을 아직 로컬 데이터베이스에 커밋하지 않는 상태입니다.
  • Staged란 현재 수정한 파일을 곧 커밋할 것이라고 표시한 상태를 의미합니다.

워킹 트리, Stagin Area(index), Git 디렉토리

스크린샷 2019-11-29 오후 6 20 40

Git을 설치하고 프로젝트를 추적 및 관리할 때 가장 먼저 사용해야 하는 명령어는 git init입니다. 그러면 .git 디렉토리가 생성되는 것을 확인 할 수가 있는데 이 Git 디렉토리는 Git이 프로젝트의 메타데이터와 객체 데이터베이스를 저장하는 곳을 말합니다. 이 Git 디렉토리가 Git의 핵심입니다.
다른 컴퓨터에 있는 저장소를 clone 할 때 Git 디렉토리가 만들어집니다.

워킹 트리는 프로젝트의 특정 버전을 Checkout 한 것입니다. Git 디렉토리는 지금 작업하는 디스크에 있고 그 디렉토리 안에 압축된 데이터베이스에서 파일을 가져와서 워킹 트리를 만듭니다.

Staging Area는 index라고 불리며 Git 디렉토리에 있습니다. 단순한 파일이고 곧 커밋할 파일에 대한 정보를 지정합니다. Staging Area는 Working Directoy에 있는 파일들 중 수정한 파일이 존재하면 개발자가 커밋에 포함시킬 파일을 선별해서 Staging Area에 올립니다. 그리고 커밋을 수행하면 새로운 커밋 개체가 생성되어 개발자가 올린 파일들에 대해서만 이전 커밋정보를 관리합니다. 따라서 언제든지 원복을 할 수 있는 장점이 있습니다.

Git으로 하는 일은 기본적으로 아래와 같습니다.

  1. 워킹 트리에서 파일을 수정합니다.
  2. Staging Area에 파일을 Stage 해서 커밋할 스냅샷을 만듭니다. 모든 파일을 추가할 수도 있고 선택하여 추가할 수도 있습니다.
  3. Staging Area에 있는 파일들을 커밋해서 Git 디렉토리에 영구적인 스냅샷으로 저장합니다.

Git 디렉토리에 있는 파일들은 Committed 상태입니다. Checkout 하고 나서 수정했지만, 아직 Staging Area에 추가하지 않았으면 Modified 입니다.

참고로 Git은 데이터를 저장하기 전에 항상 체크섬을 구하고, 그 체크섬으로 데이터를 관리합니다. 그래서 체크섬을 이해하는 깃 없이는 어떠한 파일이나 디렉토리도 변경 할 수 없습니다. 기본적으로 Git은 SHA-1 해시를 사용하여 체크섬을 만듭니다. 체크섬은 40자 길이의 16진수 문자열로 구성되어 있고, 파일의 내용이나 디렉토리의 구조를 이용하여 체크섬을 구합니다.

24b9da6552252987aa493b52f8696cd6d3b00373

이제 위에 Git을 이해하기 위한 최소한의 개념과 용어들을 알아봤으니 바로 실제 많이 사용하는 Git 명령어들을 살펴보겠습니다.

Git 실전 명령 가이드

먼저 IDE(인텔리제이, 이클립스 등)로 프로젝트를 만들고 나서 그 프로젝트를 추적하고 관리하기 위해 사용하는 명령어를 소개합니다.

1.현재 프로젝트 경로에 들어가서 git을 사용할 수 있도록 초기화 해주는 명령어입니다.

git init 

2.프로젝트 디렉토리내의 모든 파일(.은 모든파일을 의미)을 Git의 Staging Area에 추가하는 명령어 입니다. 이 말은 커밋할 파일의 목록들을 의미하기도 합니다.

git add .

참고로 .을 아큐먼트로 주게 되면 모든 파일을 Staging Area에 추가하기 때문에 만약 커밋에 포함시키고 싶지 않는 파일이 존재하게 된다면 .gitignore 파일을 만들고 그 안에 무시할 파일 패턴을 적으면 됩니다.

ex) .gitignore 작성 예시

# 확장자가 .a인 파일 무시
*.a

# 윗 라인에서 확장자가 .a인 파일은 무시하게 했지만 lib.a는 무시하지 않음
!lib.a

# 현재 디렉토리에 있는 TODO 파일은 무시하고 subdir/TODO 처럼 하위 디렉토리에 있는 파일은 무시하지 않음
/TODO

# build/ 디렉토리에 있는 모든 파일은 무시
build/

# doc/notes.txt 파일은 무시하고 doc/server/arch.txt 파일은 무시하지 않음
doc/*.txt

# doc 디렉토리 아래의 모든 .pdf 파일을 무시

doc/**/*.pdf

3.커밋과 동시에 -m 옵션으로 커밋 메시지를 작성할 수 있습니다.

git commit -m "initial commit"

참고로 git commit -a -m "initial commit"에서 -a 옵션을 추가하게 된다면 Staging Area에 추가없이 Git이 관리하는 모든 파일을 자동으로 커밋상태로 만듭니다.

4.원격 저장소, 즉 github repository 주소를 origin이라는 이름으로 등록합니다.

git remote add origin repository주소

5.origin이라는 원격 저장소에 master 브랜치에 푸시합니다. 이때 -u옵션을 쓴다면 다음번 부터 git push만 입력해도 origin의 master 브랜치로 푸시가 됩니다.

git push -u origin master

새로운 변경 사항이 있을 때

로컬에서 파일을 수정하고 원격 저장소에 변경 이력을 반영하고 싶을 때 명령어 순서입니다.

git add
git commit -m "commit message"
git push

과정은 위에 내용이랑 비슷하지만 init과 원격 저장소 등록을 할 필요는 없습니다.

branch, merge 사용법

브랜치 생성은 Git에서 제공해주는 최고의 기능이라고 생각합니다. 이것 때문에 오랫동안 VCS로 Git이 개발자들 사이에 각광받는 이유가 아닐까 싶습니다.

브랜치란 독립적으로 어떤 작업을 진행하기 위한 개념입니다. 필요에 의해 만들어지는 각각의 븐래치는 다른 브랜치의 영향을 받지 않기 때문에 여러 작업을 동시에 진행할 수 있습니다.

Branch

스크린샷 2019-11-29 오후 7 16 45

또한 이렇게 만들어진 브랜치는 다른 브랜치와 병합(Merge)함으로써, 작업한 내용을 새로운 하나의 브랜치로 모을 수 있습니다.
master 브랜치는 저장소를 처음 만들면, Git은 바로 master라는 이름의 브랜치를 만들어 둡니다. 이 새로운 저장소에 새로운 파일을 추가 한다거나 추가한 파일의 내용을 변경하여 그 내용을 저장(commit)하는 것은 모두 master라는 이름의 브랜치를 통해 처리할 수 있는 일이 됩니다.
master가 아닌 또 다른 새로운 브랜치를 만들어서 이제부터 이 브랜치를 사용할거야! 라고 선언하지 않는 이상 모든 작업은 master 브랜치에서 이루어 집니다.

이제 브랜치에 대한 개념을 살펴보았으니 위에 명령어와 마찬가지로 브랜치 생성 및 병합에 대한 명령어 순서를 살펴보겠습니다.

1.새로운 브랜치를 생성(기존 브랜치는 master)

git branch 브랜치 이름설정

만약 브랜치 생성 후 Checkout까지 바로 하고 싶으면 아래 처럼 옵션을 주어서 사용할 수 있습니다.

git checkout -b feature-01

2.1번에서 생성한 브랜치로 변경

git checkout 브랜치이름

3.Staging Area에 모든 파일 추가

git add .

4.커밋개체 생성 및 메시지 작성

git commit -m "commit message"

5.origin 저장소의 새로운 브랜치에 푸시

git push origin 브랜치이름

Merge 수행

git checkout master <- master 브랜치로 변경

git merge 브랜치 이름 

위의 과정으로 master 브랜치에 새로운 브랜치를 merge 할수 있습니다.

이제 Merge를 수행했기 때문에 새로운 브랜치가 필요없다면 삭제를 해줘야 합니다.
예를 들어서 feature-01 브랜치를 생성하고 master 브랜치와 Merge 후에 삭제한다고 가정한다면 아래와 같습니다.

git checkout master <- master 브랜치로 이동해서 feature-01 브랜치 삭제

git branch -d feature-01

그러나 작업된 사항이나 commit한 이력이 남아 있는 경우, 해당 command로 branch가 삭제 되지 않는 경우가 있습니다.
이러한 경우에는 강제로 삭제할 수 있습니다.

git branch -D  feature-01 <- -D옵션을 사용함

이 경우, local의 branch는 삭제 되었으나, remote branch는 삭제가 되지 않았습니다. remote branch를 삭제하기 위해서는, 다음과 같은 command를 수행합니다.

git push origin :feature-01

해당 command를 통해서 원격 remote branch를 삭제할 수 있습니다.

Github(https://github.com/) 페이지에서 로그인 후에 새로운 repository 생성 후 로컬에서 작업한 내용을 올리기 위한 명령어 순서입니다.

충돌 시 해결 방법

pull이나 push를 했을 때 원격저장소의 내용과 로컬폴더내의 내용중 같은 라인에 다른 내용이 있다면 이 경우에는 충돌이 발생합니다.
터미널 로그에 충돌이 난 파일이 표시되니 직접 수정 후 다시 pull이나 push를 수행해야 합니다.

가장 좋은 방법은 협업자들끼리 역할을 완전히 분담해 애초에 같은 코드를 건드리지 않는 것이지만, 어쩔 수 없이 그렇게 된다면 항상 작업 전에 pull을 습관화하면 충돌을 최소한으로 줄일 수 있습니다.

풀 리퀘스트 보내기

오픈소스를 만들 때 가장 많이 쓰는 방법입니다.

내가 push한 내용을 repository의 마스터 권한을 가진 사람에게 pull 해달라고 요청합니다. 우선 참여하고 싶은 repository에 들어가 오른쪽 상단의 fork로 나의 repository에 복사합니다.

1.원본 repo의 코드들을 나의 로컬환경에 clone 합니다.

git clone 원본 repositoty 주소

2.포크해온 나의 repo주소를 새로운 이름으로 추가해줍니다.

git remote add 나의 repo 이름 나의 repo 주소

3.현재 등록되어 있는 원격 저장소가 어떤게 있나 확인합니다. 아마 origin이라는 이름의 원본 repo 주소와 새로 설정한 이름의 나의 repo의 주소가 존재할 것입니다.

git remote -v <- 현재 원격 프로젝트 저장소 및 url을 출력해줍니다.

git branch -r <- 원격 저장소 branch 리스트 확인

git branch -a <- 로컬, 원격 저장소의 branch 리스트 확인

4,혹시 모를 코드 변화가 있을 수 있으니 한번 pull해서 확인해줍니다.

git pull origin
  1. 원본 repo에 이슈명이나 키워드로 브랜치를 생성하고 이동합니다.
git checkout -b 브랜치이름(이슈)설정 origin/master

6.모든 작업 후에 6번으로 나의 repo에 푸시합니다.

git push 나의 repo 이름 브랜치 이름

위의 과정 이후에 github에서 fork한 나의 repo에서 방금 푸시한 브랜치명을 선택하면 옆에 pull request 버튼이 생기고, 절차에 따라 누르면 풀리퀘스트가 진행됩니다.

여기서도 충돌이 발생할 수 있으니, 코드를 보며 적절히 수정하면 됩니다.

아래 워너비 스페셜님의 글을 대부분 참조하였습니다.

참조:https://takeuu.tistory.com/103

원격 저장소의 branch 가져오기

현재 원격 저장소에 여러 개의 branch가 있다고 가정합니다. 하지만 원격 저장소의 모든 내용을 pull이나 clone을 받은 후에 git branch로 확인해 보면 원격 저장소의 branch는 받아지지 않고 기존에 있던 master 브랜치 하나만 존재합니다.

먼저 원격 브랜치에 접근하기 위해 git remote를 갱신해줄 필요가 있습니다.

git remote update

위의 상황에 만약 원격 저장소의 feature/test01 branch를 가져오고 싶다면, 아래 명령어를 사용하면 됩니다.

git checkout -t origin/feature/test01

-t옵션과 원격 저장소의 branch 이름을 입력하면 로컬의 동일한 이름의 branch를 생성하면서 해당 branch로 Checkout을 합니다.

만약 branch를 변경하여 가져오고 싶다면 위에서 언급한것 처럼 아래 명령어를 입력하면 됩니다.

git checkout -b 생성할 branch 이름 원격 저장소의 branch 이름

참조 로컬 브랜치로 생성없이 원격 저장소의 브랜치로 바로 Checkout한다면 원격 저장소의 브랜치로 워킹트리로 변경이 됩니다. 하지만 메시지에 detached HEAD 상태이고 소스를 보고 변경도 해볼 수 있지만 이곳에서 변경한 것은 잠시 확인해 보는 용도로 사용될뿐 저장되지 않습니다. 다른 브랜치로 checkout을 하면 사라집니다.

그렇기 때문에 브랜치를 추적하고 싶다면 위의 언급한 명령어인 git checkout -b 생성할브랜치명 원격브랜치명처럼 해줘어야 합니다.
이렇게 하는 이유는 Subversion과는 다르게 git같은 경우는 여러개의 원격저장소를 연결할 수 있고 그중에는 브랜치명이 겹칠 수도 있기 때문으로 보입니다.

Git stash 명령어 사용하기

stash 명령어를 배우기 전에 왜 사용해야 되는지 상황을 설명하겠습니다.
자신이 어떤 작업을 하던 중에 다른 요청이 들어와 하던 작업을 멈추고 잠시 브랜치를 변경해야 할 일이 있다고 합시다. 아직 완료하지 않는 일을 commit하는것은 껄그럽습니다. 이때 stash 기능을 사용하면 됩니다.

스크린샷 2019-11-29 오후 9 11 10

Git stash란?

아직 마무리하지 않는 작업을 스택에 잠시 저장할 수 있도록 하는 명령어 입니다. 이를 통해 아직 완료하지 않는 일을 commit하지 않고 나중에 다시 꺼내와 마무리할 수 있습니다.

  • git stash 명령을 사용하면 워킹 디렉토리에서 수정한 파일들만 저장합니다.

  • stash란 아래에 해당하는 파일들을 보관해두는 장소입니다.

    * Modified이면서 Tracked 상태인 파일

    - Tracked 상태인 파일을 수정한 경우
    - Tracked: 과거에 이미 commit하여 스냅샷에 넣어진 관리 대상 상태의 파일

*Staging Area에 있는 파일(Staged 상태의 파일)

  - git add 명령을 실행한 경우
  - Staged 상태로 만들려면 git add 명령을 실행해야 한다.
  - git add는 파일을 새로 추적할 때도 사용하고 수정한 파일을 Staged 상태로 만들 때도 사용한다.

하던 작업 임시로 저장하기

git stash 명령어를 통해 새로운 stash를 스택에 만들어 하던 작업을 임시로 저장합니다.

  • 예를 들어, 파일 1개를 수정한다면 아직 commit할게 아니기 때문에 stash에 넣습니다.

스크린샷 2019-11-29 오후 9 18 42

위의 명령어 git stash를 실행하면 스택에 새로운 stash가 만들어집니다. 이 과정을 통해서 Working directory는 깨끗해집니다.

stash 목록 확인하기

git stash list 

stash@{0}: WIP on master: 049d078 added the index file

stash 적용하기(했던 작업을 다시 가져오기)

// 가장 최근의 stash를 가져와 적용합니다.
git stash apply 
// stash 이름
git stash apply [stash 이름]

스크린샷 2019-11-29 오후 9 22 11

  • 위의 명령어로는 Staged 상태였던 파일을 자동으로 다시 Staged 상태로 만들어 주지 않는다. –index 옵션을 주어야 Staged 상태까지 복원한다. 이를 통해 원래 작업하던 파일의 상태로 돌아올 수 있다.
git stash apply --index
  • index 옵션 유무의 차이

git stash apply

스크린샷 2019-11-29 오후 9 29 02

git stash apply --index

스크린샷 2019-11-29 오후 9 29 30

수정했던 파일들을 복원할 때 반드시 stash했을 때와 같은 브랜치일 필요는 없습니다. 만약 다른 작업 중이던 브랜치에 이전의 작업들을 추가했을 때 충돌이 있으면 알려줍니다.

stash 제거하기

// 가장 최근의 stash를 제거합니다.
git stash drop

// stash 이름(ex. stash@{2})에 해당하는 stash를 제거한다.
git stash drop [stash 이름]

만약 적용과 동시에 스택에 해당 stash를 제거하고 싶으면 아래와 같은 명령어를 사용합니다.

git stash pop

stash 되돌리기

실수로 잘못 stash 적용한 것을 되돌리고 싶으면 위의 명령어를 이용합니다.

// 가장 최근의 stash를 사용하여 패치를 만들고 그것을 거꾸로 적용한다.
git stash show -p | git apply -R
// stash 이름(ex. stash@{2})에 해당하는 stash를 이용하여 거꾸로 적용한다.
git stash show -p [stash 이름] | git apply -R

TIP alias로 편리하게 사용이 가능합니다.

git config --global alias.stash-unapply '!git stash show -p | git apply -R'
git stash apply
#... work work work
// alias로 등록한 stash 되돌리기 명령어
git stash-unapply

참조:https://gmlwjd9405.github.io/2018/05/18/git-stash.html

'잡다한 것' 카테고리의 다른 글

IntelliJ 자주 사용하는 단축키 정리  (0) 2019.11.24
Dev Festival을 다녀와서...  (0) 2019.11.17
자기소개  (0) 2019.10.03

JPA 사실에 대한 오해

NHN FORWARD 워크숍에 참가하여 신동민 개발자님이 발표한 JPA 사실에 대한 오해에 대해서 듣고 실습을 통해서 의미있는 유익한 시간을 보냈습니다.

JPA에 대해서 흔히 잘못알고 있는 사실 중 하나가 엔티티와의 연관관계는 단방향이면 매핑이 끝나는 것은 맞지만 성능상 일대다 단방향 관계를 가질때 자식엔티티를 영속석 전이를 통해 저장하게 되면 트랜잭션 커밋시점에 플러시가 호출되어 insert 쿼리가 발생 한 후에 자식 엔티티에 대해서 update 문이 자식엔티티 수만큼 수행되기 때문에 성능상의 문제가 발생합니다.

다음은 OrderDetail -> Order 단방향 연관관계를 가질때 발생하는 쿼리를 확인해 보겠습니다.

@ManyToOne 단방향일 경우

// Order(주문정보) 엔티티
@Getter
@Setter
@Entity
@Table(name = "Orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    @Column(name = "order_id")
    private Long orderId;

    @Column(name = "order_dt")
    private LocalDateTime orderDate;
}
// OrderDetails(주문내역) 엔티티
@Getter
@Setter
@Entity
@Table(name = "OrderDetails")
public class OrderDetail {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    @Column(name = "order_detail_id")
    private Long orderDetailId;

    @ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "order_id")
    private Order order;

    private String type;

    private String description;

}

간단한 테스트를 위해서 lombok을 이용하여 setter를 넣었지만 실환경에서 영속성을 이용한 수정 과정에서 공유 참조를 하여 실수로 값 변경이 잘못되면 큰 재앙을 초래하기 때문에 setXxx() 메소드를 써야할지 고민해야 된다고 생각합니다.

// 실행 클래스
@Bean
CommandLineRunner onStartUp(OrderService orderService) {
    return args -> {
        orderService.createOrderWithDetails();
    };
}

@Service
public class OrderService {

    private final OrderDetailRepository orderDetailRepository;


    public OrderService(OrderDetailRepository orderDetailRepository) {
        this.orderDetailRepository = orderDetailRepository;
    }


    @Transactional
    public void createOrderWithDetails() {
        Order order = new Order();
        order.setOrderDate(LocalDateTime.now());

        OrderDetail orderDetail1 = new OrderDetail();
        orderDetail1.setOrder(order);
        orderDetail1.setType("type1");
        orderDetail1.setDescription("order1-type1");

        OrderDetail orderDetail2 = new OrderDetail();
        orderDetail2.setOrder(order);
        orderDetail2.setType("type2");
        orderDetail2.setDescription("order1-type2");

        orderDetailRepository.saveAll(Arrays.asList(orderDetail1, orderDetail2));
    }

CommandLineRunner를 스프링 빈으로 등록하여 웹 애플리케이션 실행 시에 Order 엔티티와 OrderDetail 엔티티의 영속성에 저장할때 자식 엔티티인 OrderDetail에 영속성 전이를 설정했기 때문에 자식 엔티티를 영속성 컨텍스트에 저장하면 자동으로 부모 엔티티도 영속화 됩니다. 실제 트랜잭션이 커밋되는 시점에 플러시가 호출되어 insert 쿼리가 부모 1, 자식 2개에 대해서만 데이터베이스에 보내는 것을 알 수가 있습니다.

// 영속성 전이 설정
@ManyToOne(cascade = CascadeType.ALL)
    @JoinColumn(name = "order_id")
    private Order order;

실행결과

스크린샷 2019-11-27 오후 9 15 28

이제 일대다(@OneToMany) 단 방향 연관관계일 때 영속성 컨테스트에 저장 시 발생하는 쿼리 수를 확인 해보겠습니다.

@Getter
@Setter
@Entity
@Table(name = "Orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    @Column(name = "order_id")
    private Long orderId;

    @Column(name = "order_dt")
    private LocalDateTime orderDate;

    @OneToMany(cascade = CascadeType.ALL)
    @JoinColumn(name = "order_id")
    List<OrderDetail> orderDetails = new ArrayList<OrderDetail>();
}


@Getter
@Setter
@Entity
@Table(name = "OrderDetails")
public class OrderDetail {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    @Column(name = "order_detail_id")
    private Long orderDetailId;

    private String type;
    private String description;

}

@Transactional
public void createOrderWithDetails() {
    Order order = new Order();
    order.setOrderDate(LocalDateTime.now());
    orderRepository.save(order);

    OrderDetail orderDetail1 = new OrderDetail();
    orderDetail1.setType("type1");
    orderDetail1.setDescription("order1-type1");

    OrderDetail orderDetail2 = new OrderDetail();
    orderDetail2.setType("type2");
    orderDetail2.setDescription("order1-type2");

    order.getOrderDetails().add(orderDetail1);
    order.getOrderDetails().add(orderDetail2);
}

위의 코드를 보면 일대다 단 방향 연관관계를 가질 경우에는 Order 엔티티에서 연관관계의 주인이지만 실제로 JPA에서 영속성 저장 후 insert 쿼리를 보면 외래키는 다에 해당하는 OrderDetail 테이블에 들어가는 것을 확인할 수 있습니다.
그렇기 때문에 플러시를 호출하면 OrderDetail 엔티티에 대한 UPDATE 쿼리가 발생하기 때문에 성능적으로 좋지 않습니다.

일대다(@OneToMany) 단 방향 연관관계 실행 결과

스크린샷 2019-11-27 오후 9 41 18

실행 결과를 보면 insert 쿼리 3개, UPDATE 쿼리 2개가 발생한 것을 확인 할 수가 있습니다... 만약 자식 엔티티의 수가 더 많았다면 자식 엔티티 수 만큼 쿼리가 데이터베이스에 전송될 것 입니다.

그럼 이제 오해를 풀기 위해서 일대다(@ManyToOne) 양 방향 연관관계로 설절 후에 영속성 컨텍스트에 저장 시 발생하는 쿼리를 확인해 보겠습니다.

@Getter
@Setter
@Entity
@Table(name = "Orders")
public class Order {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    @Column(name = "order_id")
    private Long orderId;

    @Column(name = "order_dt")
    private LocalDateTime orderDate;

    @OneToMany(mappedBy = "order", cascade = CascadeType.ALL)
    List<OrderDetail> orderDetails = new ArrayList<OrderDetail>();
}


@Getter
@Setter
@Entity
@Table(name = "OrderDetails")
public class OrderDetail {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    @Column(name = "order_detail_id")
    private Long orderDetailId;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "order_id")
    private Order order;

    private String type;
    private String description;

}


@Transactional
public void createOrderWithDetails() {
    Order order = new Order();
    order.setOrderDate(LocalDateTime.now());

    OrderDetail orderDetail1 = new OrderDetail();
    orderDetail1.setOrder(order);
    orderDetail1.setType("type1");
    orderDetail1.setDescription("order1-type1");

    OrderDetail orderDetail2 = new OrderDetail();
    orderDetail2.setOrder(order);
    orderDetail2.setType("type2");
    orderDetail2.setDescription("order1-type2");

    order.getOrderDetails().add(orderDetail1);
    order.getOrderDetails().add(orderDetail2);

    orderRepository.save(order);
}

일대다 양방향 연관관계 매핑 시 영속성 컨텍스트에 저장을 하면 아래 실행 결과처럼 다대일 단방향 연관관계처럼 insert 쿼리가 3개만 나가는 것을 확인 할 수 있었습니다.

일대다(@ManyToOne) 양 방향 연관관계 실행 결과

스크린샷 2019-11-27 오후 9 48 35

위의 예제코드를 통해서 일대다 양방향 매핑단방향 매핑 보다는 조금 더 복잡하지만 실환경에서 성능상의 이점을 누릴 수 있다는 사실을 깨닫게 되었습니다.

N+1 문제

그 다음 내용은 JPA를 프로젝트를 하면서 가장 많이 겪게되는 N+1 문제에 대해서 리뷰하겠습니다.
N + 1 문제는 엔티티에 대해 하나의 쿼리로 N개의 레코드를 가져왔을 때, 연관관계 엔티티를 가져오기 위해 쿼리를 N번 추가적으로 수행하는 문제를 말합니다.

대부분 N+1 문제에 대한 오해는 흔히 글로벌 페치 전략인 즉시 로딩(EAGER)에서 발생한다고 많이 알고 있지만 사실 즉시 로딩뿐만 아니라 지연 로딩(LAZY)에서도 N+1은 존재합니다. 지연 로딩은 조회하려는 엔티티를 가지고 올때 연관된 엔티티는 조회하지 않고 실제로 객체 그래프 탐색으로 연관된 엔티티를 사용하는 시점에 프록시를 통해서 조회를 요청하는 페치 전략 입니다.
따라서 결국에는 영속성 컨텍스트에 해당 엔티티가 존재하지 않는다면 데이터베이스를 통해서 쿼리를 발송해야 하기 때문에 N + 1은 피할 수 없는 문제입니다.

다음과 같이 2가지의 대표적인 해결 방법이 있습니다.

  • Fetch Join
  • Entity Graph

JPA는 단건 조회(findOne())를 할 경우네는 외래키에 null 허용여부에 따라 선택적 관계 또는 필수적 관계를 가지게 됩니다. 이때 JPA는 선택적 관계이면 null이 존재할 수 있다고 가정하여 left outer join으로 연관된 엔티티를 한번에 가져오게 됩니다. 필수적 관계일 경우에는 null을 허용하지 않기 때문에 최적의 성능으로 inner join을 사용하여 연관관계를 가져옵니다. 하지만 JPQL로 작성한 findAll()과 같은 여러 건의 엔티티를 조회하는 메소드는 실제로 조회를 했을 때 데이터베이스에서 연관관계를 가진 엔티티를 join을 해서 가져오지만 영속상태로 반환하지 않습니다. 그리고 스프링 Data JPA는 다시 글로벌 패치전략을 보고 즉시 로딩일 경우 데이터베이스에 연관관계를 가진 엔티티를 다시 조회하게 됩니다. 그렇기 때문에 N + 1 문제가 발생하는 겁니다.

위의 설명한 이러한 문제를 해결하기 위해서 join fetch를 사용하여 N + 1 문제가 발생이 안하는지 확인하겠습니다.

public interface OrderRepository extends JpaRepository<Order, Long> {

    @Query("select o from Order o" +
    " join o.orderDetails od")
    public List<Order> getAllOrderWithDetails();

}

// 실행결과
@Bean
CommandLineRunner onStartUp(OrderService orderService) {
    return args -> {
        orderService.createOrderWithDetails();
        orderService.getAllOrderWithDetails();
    };
}



Hibernate: select order0_.order_id as order_id1_1_, order0_.order_dt as order_dt2_1_ from orders order0_ inner join order_details orderdetai1_ on order0_.order_id=orderdetai1_.order_id

Hibernate: select orderdetai0_.order_id as order_id4_0_0_, orderdetai0_.order_detail_id as order_de1_0_0_, orderdetai0_.order_detail_id as order_de1_0_1_, orderdetai0_.description as descript2_0_1_, orderdetai0_.order_id as order_id4_0_1_, orderdetai0_.type as type3_0_1_ from order_details orderdetai0_ where orderdetai0_.order_id=?

Hibernate: select orderdetai0_.order_id as order_id4_0_0_, orderdetai0_.order_detail_id as order_de1_0_0_, orderdetai0_.order_detail_id as order_de1_0_1_, orderdetai0_.description as descript2_0_1_, orderdetai0_.order_id as order_id4_0_1_, orderdetai0_.type as type3_0_1_ from order_details orderdetai0_ where orderdetai0_.order_id=?

위의 JpaRepository 인터페이스를 확장한 OrderRepository 클래스를 만들어서 JPQL로 @Query 어노테이션을 적용한 쿼리 메소드를 작성하였습니다. Order 엔티티와 OrderDetail 엔티티를 join을 수행하는 메소드이지만 실제로 실행 결과를 보면 select 쿼리문이 데이터베이스에 3번 전송되는 것이 확인됩니다.

만약 OrderDetail 엔티티들이 같은 Order 엔티티를 참조하게 된다면 추가로 OrderDetail 엔티티는 한번만 조회하겠지만 대부분은 아마 각각 다른 Order 엔티티를 참조하기 때문에 이미 처음에 조회된 Order 엔티티들의 수 만큼 참조하는 OrderDetail 엔티티에 대한 조회가 N + 1만큼 이루어 집니다.
이런한 문제를 join fetch를 사용하면 아래와 같은 실행 결과가 나옵니다.

페치 조인(Fetch Join)

public interface OrderRepository extends JpaRepository<Order, Long> {

    @Query("select o from Order o" +
    " join fetch o.orderDetails od")
    public List<Order> getAllOrderWithDetails();
}

// 실행 결과
Hibernate: select order0_.order_id as order_id1_1_0_, orderdetai1_.order_detail_id as order_de1_0_1_, order0_.order_dt as order_dt2_1_0_, orderdetai1_.description as descript2_0_1_, orderdetai1_.order_id as order_id4_0_1_, orderdetai1_.type as type3_0_1_, orderdetai1_.order_id as order_id4_0_0__, orderdetai1_.order_detail_id as order_de1_0_0__ from orders order0_ inner join order_details orderdetai1_ on order0_.order_id=orderdetai1_.order_id

짠!.... 각각의 Order 엔티티를 OrderDetail 엔티티가 참조하고 있지만 join fetch로 인해서 한번만 쿼리가 발생하는 것을 확인할 수 가 있습니다.

Entity Graph 방법

사실 객체 그래프 방법은 오늘 처음 듣게 되었고 처음 실습시간에 사용해봤습니다. 이 객체 그래프 방식은 실제 엔티와의 연관관계가 복잡해질때 어디까지 연관된 엔티티를 조회할지 개발자가 직접 정의해서 사용할 수 있는 방법입니다. 객체 그래프 방법은 도메인애 @NamedEntityGraphs 어노테이션을 적용하여 repository에서 도메인에 정의된 @NamedEntityGraphs의 name을 이용하여 @Query 메소드와 함께 사용할 수 있습니다.

Member 엔티티와 MemberDetails 엔티티가 일대다(@OneToMany) 양 방향 연관관계를 가지고 있을때 객체 그래프 방법으로 join을 하는 예제코드를 살펴보겠습니다.

@NamedEntityGraph(name = "memberWithDetails", attributeNodes = {
        @NamedAttributeNode("details")
})
@Getter
@Setter
@Entity
@Table(name = "Members")
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    @Column(name = "member_id")
    private Long memberId;

    private String name;

    @Column(name = "create_dt")
    private LocalDateTime createDate;

    @OneToMany(cascade = CascadeType.ALL, mappedBy = "member")
    private List<MemberDetail> details = new ArrayList<>();

}


@Getter
@Setter
@Entity
@Table(name = "MemberDetails")
public class MemberDetail {
    @Id
    @GeneratedValue(strategy = GenerationType.SEQUENCE)
    @Column(name = "member_detail_id")
    private Long memberDetailId;

    @ManyToOne
    @JoinColumn(name = "member_id")
    private Member member;

    private String type;

    private String description;

}

public interface MemberRepository extends JpaRepository<Member, Long> {
    // TODO : @EntityGraph로 설정한 Entity Graph를 이용
    @EntityGraph("memberWithDetails")
    @Query("select m from Member m")
    List<Member> getAllBy();

}

위의 코드를 살펴보면 @NamedAttributeNode("details")는 Member 엔티티가 참조하고 있는 MemberDetail 엔티티의 컬렉션 객체의 참조 변수를 넣었습니다.
즉, 연관관계를 조회할 때 MemberDetail 엔티티까지 조회하겠다는 뜻입니다.

그리고 MemberRepository에 @Query 어노테이션 안에 있는 쿼리는 Member 엔티티만 조회를 하지만 @EntityGraph 어노테이션에서 Member 도메인에 정의 되어있는 memberWithDetails를 명시해주었기 때문에 실제로 getAllBy() 메소드를 호출하게 되면 Member와 MemberDetail 엔티티를 조인한 쿼리문을 데이터베이스에 전송하여 조회하게 됩니다.

실행결과

Hibernate: select member0_.member_id as member_i1_3_0_, details1_.member_detail_id as member_d1_2_1_, member0_.create_dt as create_d2_3_0_, member0_.name as name3_3_0_, details1_.description as descript2_2_1_, details1_.member_id as member_i4_2_1_, details1_.type as type3_2_1_, details1_.member_id as member_i4_2_0__, details1_.member_detail_id as member_d1_2_0__ from members member0_ left outer join member_details details1_ on member0_.member_id=details1_.member_id

여기까지 제가 오늘 NHN 워크숍에서 배운 JPA에 대한 사실과 오해에 대한 발표내용이였습니다. N + 1 문제를 해결하는 방법은 join fetch 방법밖에 몰랐었는데 객체 그래프 방법을 배움으로써 상황에 따라서 객체 그래프 방법을 이용하면 페치 조인 방법처럼 성능을 최적화 할 수 있다고 생각합니다.

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

객체지향 쿼리 소개(Criteria, QueryDSL, Native Query)  (0) 2019.12.19
JPA Auditing 기능이란?  (0) 2019.12.11
값 타입  (0) 2019.11.26
영속성 전이 및 고아객체 제거  (0) 2019.11.23
지연로딩과 즉시로딩  (0) 2019.11.23

값 타입

JPA는 데이터 타입을 가장 크게 분류하면 엔티티 타입과 값 타입으로 나눌 수 있습니다. 엔터티 타입은 @Entity로 정의하는 객체이고, 값 타입은 int, Integer, String 처럼 단순히 값으로 사용하는 자바 기본 타입이나 객체를 말합니다. 엔팉티 타입은 식별자를 통해서 지속해서 추적할 순 있지만, 값 타입은 식별자가 없고 숫자나 문자같은 속성만 있으므로 추적할 수 없습니다.엔티티는 키나 나이와 같은 값을 변경해도 식별자만 유지되면 같은 회원으로 인식할 수 있습니다. 하지만 숫자 값 100을 200으로 변경하면 완전히 다른 값으로 대체 됩니다.
비유하자면 엔티티 타입은 살아있는 생물이고 값 타입은 단순한 수치 정보입니다.

  • 기본값 타입

    - 자바 기본 타입(int, double)

    - 래퍼 클래스(Integer, Double, Long)

    - String

    • 임베디드 타입(복합 값 타입)

    • 컬렉션 값 타입

    기본값 타입은 String, int처럼 자바가 제공하는 기본 데이터 타입이고 임베디드 타입은 JPA에서 사용자가 직접 정의한 값 타입입니다. 마지막으로 컬렉션 값 타입은 하나 이상의 값 타입을 저장 할 때 사용합니다.

가장 단순한 기본값 타입은 아래 코드와 같습니다

@Entity
public class Member{

    @Id @GeneratedValue
    private Long id;

    private String name;

    private int age;

}

위의 코드를 보면 값 타입인 name, age 속성은 식별자 값도 없고 생명주기도 회원 엔티티에 의존합니다. 따라서 회원 엔티티의 인스턴스를 제거하면 name, age 값도 제거 됩니다.

참고: 자바에서 int, double 같은 기본 타입은 절대 공유되지 않습니다. call by value라는 것을 알아야 됩니다. 만약 a의 값을 변경했다고 b까지 변경되면 정말 끔찍할 것입니다.

임베디드 타입(보합 값 타입)

새로운 값 타입을 직접 정의해서 사용할 수 있는데 이것을 임베디드 타입이라고 합니다. 중요한 것은 직접 정의한 임베디드 타입도 int, String 처럼 값 타입이라는 것입니다.

@Entity
public class Member{

    @Id @GeneratedValue
    private Long id;

    private String name;

    // 근무 기간
    @Temporal(TemporalType.DATE) java.util.Date. startDate;
    @Temporal(TemporalType.DATE) java.util.Date. endDate;


    // 집 주소 표현
    private String city;
    private String street;
    private String zipcode;

}
  • 회원 엔티티는 이름, 근무 시작일, 근무 종료일, 주소 도시, 주소 번지, 주소 우편 번호를 가집니다.

이런설명은 단순히 정보를 풀어둔 것 뿐이니다. 그리고 근무 시작일과 우편번호는 서로 아무 관련이 없습니다. 이것보단 아래처럼 명확하게 설명하는 것이 좋습니다

  • 회원 엔티티는 이름, 근무 기간, 집 주소를 가집니다.

회원이 상세한 데이터를 그대로 가지고 있는 것은 객체 지향적이지 않으며 응직력만 떨어뜨립니다. 대신에 근무 기간, 주소 같은 타입이 있다면 코드가 더 명확해 질 것입니다. [근무기간, 집 주소] 를 가지도록 임베디드 타입을 사용해 보겠습니다.

@Entity
public class Member{

    @Id @GeneratedValue
    private Long id;

    private String name;

    @Embedded Period workPeriod; // 근무 기간
    @Embedded Address homeAddress; // 집 주소
}
public class Period{

    // 근무 기간
    @Temporal(TemporalType.DATE) java.util.Date. startDate;
    @Temporal(TemporalType.DATE) java.util.Date. endDate;

    public boolean isWork(Date date){
        //.. 값 타입을 위한 메소드를 정의 할 수 있습니다.
    }

}
public class Address{

    // 집 주소 표현
    private String city;
    private String street;
    private String zipcode;
    // ...
}

임베디드 타입 사용 후 엔티티 연관관계

스크린샷 2019-11-25 오후 9 59 13

임베디트 타입을 사용하니 엔티티가 더욱 의미 있고 응집력 있게 변한 것을 알 수 있습니다.

  • startDate, endDate를 합해서 Period 클래스를 만들었습니다.

  • city, street, zipcode를 합해서 Address 클래스를 만들었습니다.

새로 정의한 값 타입들은 재사용할 수 있고 응집도도 아주 높습니다. 또한 Period.isWork() 매소드 처럼 해당 값 타입만 사용하는 의미 있는 메소드도 만들 수 있습니다. 임베디드 타입을 사용하려면 다음 2가지 어노테이션이 필요합니다.

  • @Embeddable: 값 타입을 정의하는 곳에 표시

  • @Embedded: 값 타입을 사용하는 곳에 표시

참고로 위의 2가지 어노테이션 중 하나는 생략해도 됩니다.

그리고 임베디드 타입은 기본 생성자가 필수입니다.
하이버네이트는 임베디드 타입을 컴포넌트(component)라고 합니다.

임베디드 타입은 엔티티의 값일 뿐입니다. 따라서 값이 속한 엔티티의 테이블에 매핑합니다. 임베디드 타입 덕분에 객체와 테이블을 아주 세밀하게 매핑하는 것이 가능합니다. 잘 설계한 ORM 애플리케이션은 매핑한 테이블의 수보다 클래스의 수가 더 많습니다.

임베디드 타입과 연관관계

임베디드 타입은 값 타입을 포함하거나 엔티티를 참조할 수 있습니다. 엔티티는 공유될 수 있으므로 참조한다고 표현하고, 값 타입은 특정 주인에 소속되고 논리적인 개념상 공유되지 않으므로 포함된다고 표현합니다.

스크린샷 2019-11-25 오후 10 25 59

public class Member{

    @Embedded 
    private Address address; // 임베디드 타입 포함

    @Embedded 
    private PhoneNumber phoneNumber; // 임베디드 타입 포함
}

@Embeddable
public class Address{

    private String street;
    private String city;
    private String state;

    @Embedded
    Zipcode zipcode; // 임베디드 타입 포함
}

@Embeddable
public class Zipcode{

    private String zip;
    private String plusFour;
}


@Embeddable
public class phoneNumber{
    private String areaCode;
    private String localNumber;

    @ManyToOne
    PhoneServiceProvider provider; // 엔티티 참조
    ...
}

@Entity
public class PhoneServiceProvider{

    @Id
    private String name;

}

코드를 보면 값 타입인 Address가 값 타입인 Zipcode를 포함하고, 값 타입인 PhoneNumber가 엔티티 타입인 PhoneServiceProvider를 참조하고 있습니다.

@AttributeOverride 속성 재정의

임베디드 타입에 정의한 매핑정보를 재정의하려면 엔티티에 @AttributeOverride를 사용하면 됩니다. 예를 들어 회원에 주소가 하나 더 필요하면 아래 코드와 같이 @AttributeOverrides를 사용해서 매핑정보를 재정의해야 합니다.

@Entity
public class Member{

    @Id @GeneratedValue
    private Long id;
    private String name;

    @Embedded Address homeAddress;


    @Embedded
    @AttributeOverrides({
        @AttributeOverride(name = "city", column=@Column(name = "COMPANY_CITY")),
        @AttributeOverride(name = "street", column=@Column(name = "COMPANY_STREET")),
        @AttributeOverride(name = "zipcode", column=@Column(name = "COMPANY_ZIPCODE"))
    })
    private Address companyAddress;
}

참고 @AttributeOverrides는 엔티티에 설정해야 합니다. 임베디드 타입이 임베디드 타입을 가지고 있어도 엔티티에 설정해야 합니다.

임베디드 타입과 null

임베디드 타입이 null이면 매핑한 컬럼 값은 모두 null 입니다.

member.setAddress(null);
em.persist(member);

회원 테이블의 주소와 관련된 city, street, zipcode 컬럼 값은 모두 null이 됩니다.

값 타입과 불변 객체

값 타입은 복잡한 객체 세상을 조금이라도 단순화하여 만든 개념입니다. 따라서 값 타입은 단순하고 안전하게 다룰 수 있어야 합니다.

값 타입 공유 참조

임베디드 타입 값 타입을 여러 엔티티에서 공유하면 위험합니다. 공유하려면 어떤 문제가 발생하는지 알아봐야 합니다.

스크린샷 2019-11-26 오전 12 32 59

위의 그림을 코드로 나타내면 다음과 같습니다.

member1.setAddress(new Address("OldCity"));
Address address = member1.getAddress();

address.setCity("NewCity");
member2.setHomeAddress(address);

회원2에 새로운 주소를 할당하려고 회원1의 주소를 그대로 참조해서 사용했습니다. 실제로 회원2만 NewCity로 변경되길 기대했지만 회원1의 주소도 NewCity로 변경되어 버립니다. 회원1과 회원2 둘 다 같은 address 인스턴스를 참조하기 때문입니다. 영속성 컨텍스트는 회원1과 회원2 둘 다 city 속성이 변경된 것으로 판단해서 회원1, 회원2 각각 UPDATE SQL을 실행한다.

이러한 공유 참조로 발생하는 버그는 정말 찾아내기 어렵습니다. 이렇듯 뭔가를 수정했는데 전혀 예상치 못한 곳에서 문제가 발생하는 것을 부작용이라 합니다. 이런 부작용을 막으려면 값을 복사해서 사용하면 됩니다.

값 타입의 복사

값 타입의 실제 인스턴스인 값을 공유하는 것은 위험합니다. 대신에 값(인스턴스)를 복사해서 사용해야 합니다.

스크린샷 2019-11-26 오전 12 53 37

member1.setAddress(new Address("OldCity"));
Address address = member1.getAddress();

// 회원1의 address 값을 복사해서 새로운 newAddress 값을 생성
Address newAddress = address.clone();

address.setCity("NewCity");
member2.setHomeAddress(address);

회원2에 새로운 주소를 할당하기 위해 clone() 메소드를 만들었는데, 이 메소드는 자신을 복사해서 반환하도록 구현했습니다. 이 코드를 실행하면 의도한 대로 회원2 주소만 NewCity로 변경됩니다. 그리고 영속성 컨텍스트는 회원2의 주소만 변경된 것으로 판단해서 회원2에 대해서만 UPDATE SQL을 실행합니다.

이처럼 항상 값을 복사해서 사용하면 공유 참조로 인해 발생하는 부작용을 피할 수 있습니다.
문제는 임베디드 타입처럼 직접 정의한 값 타입은 자바의 기본타입이 아니라 객체 타입이라는 것입니다.
자바는 기본 타입에 값을 대입하면 값을 복사해서 전달합니다.
자바는 객체에 값을 대입하면 항상 참조 값을 전달합니다.
Address b=a 에서 a가 참조하는 인스턴스의 참조 값을 b에 넘겨줍니다. 따라서 a와 b는 같은 인스턴스를 공유 참조합니다. 마지막 줄의 b.setCity("New")의 의도는 b.city 값만 변경하려 했지만 공유 참조로 인해 부작용이 발생해서 a.city 값도 변경됩니다.

물론 객체는 대입할 때마다 인스턴스를 복사해서 대입하면 공유 참조를 피할 수 있습니다. 문제는 복사하지 않고 원본의 참조 값을 직접 넘기는 것을 막을 방법이 없다는 것입니다. 자바는 대입하려는 것이 값 타입인지 아닌지는 신경쓰지 않습니다. 단지 자바 기본 타입이면 값을 복사해서 넘기고, 객체면 참조를 넘길 뿐입니다.

객체의 공유 참조는 피할 수 없습니다. 따라서 근본적인 해결책이 필요한데 가장 단순한 방법은 객체의 값을 수정하지 못하게 막으면 됩니다. 수정자 메소드인 setCity() 같은 메소드를 모두 제거하면 됩니다. 이렇게 하면 공유 참조를 해도 값을 변경하지 못하므로 부작용의 발생을 막을 수가 있습니다.

불변 객체

한번 만들면 절대 변경할 수 없는 객체를 불변 객체라고 합니다. 불변 객체의 값은 조회할 수 있지만 수정할 수는 없습니다. 불변 객체도 결국은 객체입니다. 따라서 인스턴스의 공유 참조를 피할 수 없습니다. 하지만 참조 값을 공유해도 인스턴스의 값을 수정할 수 없으므로 부작용이 발생하지 않습니다.

불변 객체를 구현하는 다양한 방법이 있지만 가장 간단한 방법은 생성자로만 값을 설정하고 수정자를 만들지 않으면 됩니다. Address를 불변 객체로 만들면 아래 코드와 같습니다.

@Embeddable
public class Address {

    private String city;

    protected Address() {} // JPA에서 기본 생성자는 필수입니다.

    public Address(String city){
        this.city = city;
    }

    public String getCity(){
        return city;
    }

    // 수정자(setter)는 만들지 않습니다.

}
불변 객체 사용
Address address = member1.getHomeAddress();
//회원1의 주소값을 조회해서 새로운 주소값을 생성
Address newAddress = new Address(address.getCity());
member2.setHomeAddress(newAddress);

Address는 이제 불변 객체입니다. 값을 수정할 수 없으므로 공유해도 부작요이 발생하지 않습니다. 만약 값을 수정해야 한다면 새로운 객체를 생성해서 사용해야 합니다. 참고로 Integer, String은 자바가 제공하는 대표적인 불변 객체 입니다.

결론은 불변이라는 작은 제약으로 부작용이라는 큰 재앙을 막을 수 있습니다.

참조: ORM 표준 JPA 프로그래밍

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

JPA Auditing 기능이란?  (0) 2019.12.11
JPA에 대한 사실과 오해  (0) 2019.11.28
영속성 전이 및 고아객체 제거  (0) 2019.11.23
지연로딩과 즉시로딩  (0) 2019.11.23
프록시와 연관관계 관리  (0) 2019.11.22

IntellJ 단축키

개발자에게는 프레임워크 기술에 대한 전반적인 지식과 문제해결능력이 생산성에 가장 직결되는 요소라고 생각하지만 IDE를 잘 다루는 능력 또한 중요하다고 생각합니다.
이러한 이유로 Mac OS 환경을 기준으로 jetbrains에서 만든 IntelliJ에서 자주 사용하는 단축키들을 정리해보았습니다.
참고로 맥에서 command(⌘) 키는 cmd, option(⌥) 키는 alt, control(⌃) 키는 각각 다른 키인것을 안다는 가정하에 정리하였습니다.

cmd(⌘) + N : 현재위치를 기준으로 디렉토리, 패키지 및 생성목록을 보여주는 것 이외에도 생성자/ getter/setter도 보여준다.

cmd(⌘) + P : 해당 클래스가 인스턴스를 생성하기 위해 필요한 인자 값을 확인 가능합니다.

alt(⌥) + space : 특정 메소드의 구현부를 보는 단축키 입니다.

alt(⌥) : Focus를 이동할때 단어별로 이동할때는 옵션(⌥) 키를 누릅니다.

alt(⌥) + 방향 키(^) : 해당 포커스의 단어가 선택됩니다. ^키를 한번 더 누르면 선택이 확장 됩니다. 계층구조로 포커스의 범위가 넓어집니다.

F1 : 키를 누르면 해당 메소드나 클래스의 Doc을 보여줍니다.

Fn + 좌우상하 키: 현재 포커스에서 좌우 상하로 쉽게 이동 가능합니다. 라인 첫/끝 이동, page Up / page Down

Fn + Shift(⇧) : 라인 전체를 선택 가능합니다.

cmd(⌘) + [,] : 이전 포커스 이동, 다음 포커스 이동을 하는 단축키입니다. 다른 클래스 파일에 있는 포커스에도 적용이 가능하기 때문에 클래스 단위로 이동이 가능합니다.

cmd(⌘) + F : 현재 파일에서 특정 문자열을 찾는 단축키 입니다.

cmd(⌘) + Shift(⇧) + F : 프로젝트 전체에서 특정 문자열 검색이 가능합니다.

cmd(⌘) + R : 현재 파일에서 해당 문자열을 변경하는 단축키입니다. 주로 클래스 파일에서 전역변수를 바꿀때 유용합니다.

cmd(⌘) + Shift(⇧) + R : 프로젝트 전체에서 특정 문자열 변경이 가능합니다.

control(⌃) + Shift(⇧) + R : 현재 클래스 파일의 실행 결과를 보여줍니다.

control(⌃) + R : 이전에 실행했던 클래스 파일의 실행 결과를 보여줍니다.

cmd(⌘) + Shift(⇧) + O : 파일 검색을 하는 단축키 입니다.

cmd(⌘) + alt(⌥) + O : getter, setter등 원하는 메소드를 검색하여 찾을 수 있는 단축키 입니다.

cmd(⌘) + Shift(⇧) + A : Action(rename, theme: 등)을 검색할 수 있는 단축키로 저는 주로 파일 명을 변경할 때 사용합니다.

cmd(⌘) + E : 최근에 열었던 파일들의 목록을 볼 수 있습니다.

cmd(⌘) + Shift(⇧) + E : 최근에 수정했던 파일 목록들을 보여줍니다.

Ctrl(⌃) + I : 인터페이스에 있는 추상메소드를 즉 오버라이딩 할 목록을 보여줍니다. Copy and Paste 작업을 할 필요가 없어지는 좋은 단축키입니다.

``cmd(⌘) + J` : 현재 포커스를 기준으로 나올 수 있는 라이브템플릿 축약어들을 보여줍니다. iter,inn,ifn 등이 존재합니다.

cmd(⌘) + alt(⌥) + L : 자동으로 코드를 정렬 해주기 때문에 코드의 가독성이 향상되는 효과가 있습니다. 정말 자주 사용하는 단축키입니다.

cmd(⌘) + alt(⌥) + O : 불필요한 Import 문을 없애주는 단축키 입니다. 마찬가지로 자주 사용되는 단축키입니다.

참고: Action(cmd(⌘) + Shift(⇧) + A) 입력 값에 optimize imports on the fly를 클릭하고 설정을 해주면 자동으로 불필요한 import 문을 없애주기 때문에 사용하지 않는 import문을 개발자가 신경을 안써도 됩니다.

Shift(⇧) + F6 : 클래스, 변수, 파라미터 이름을 일괄적으로 변경해주는 단축키 입니다.

cmd(⌘) + Shift(⇧) + F6: 타입을 일괄적으로 변경 할 수 있습니다. 리턴타입도 자동으로 변경 됩니다.

F6 : Inner Class를 외부로 추출하거나, 다른 클래스의 내부 클래스로 이동 할 수 있습니다.

F2: 에러가 발생한 곳으로 포커스를 이동하는 단축키 입니다.

스마트 자동 완성

Ctrl(⌃) + Shift(⇧) + Space : 클래스의 인스턴스 생성시 나올 수 있는 클래스만 보여주는 단축키입니다. 구글링하지 않고도 들어갈 수 있는 인자값을 넣을 수 있는 장점이 있습니다.

Shift(⇧) + Space*2 : 스태틱 메소드가 자동 완성되는 단축키 입니다.

디버깅 단축키

control(⌃) + Shift(⇧) + D : 현재 위치의 메소드에서 디버그 모드로 실행되는 단축키 입니다.

cmd(⌘) + alt(⌥) + R : Resume 다음 브레이크 포인트로 이동하는 단축키 입니다.

F8 : Step Over 현재 브레이크에서 다음 한줄로 이동하는 단축키

F7 : Step Into 현재 브레이크의 다음 메소드로 이동하는 단축키

Shift(⇧) + F8 : Step Out 현재 메소드 밖으로 이동하는 단축키

alt(⌥) + F8 : Evaluate Expression 브레이크 된 상태에서 코드 사용하는 단축키 입니다.

Watch : 브레이크 이후의 코드 변경을 확인하는 것으로 단축키는 없고 안경모양으로 되어 있습니다.

리팩토링 단축키

정말 IntelliJ에서 제공해주는 가장 최고의 기능 중 하나라고 생각하는 리펙토링을 자동으로 해주는... 대단히 훌륭한 기능이라고 생각해서 따로 뺐습니다. 동작하는 프로그램보다 유지보수가 쉽고 확장성에 대해서 열려있는 기능을 구현하기 위해서는 다른 개발자들이 내 소스를 얼마나 쉽게 읽고 이해 할 수 있는것도 신경쓰는게 정말 중요합니다. 그렇게 하기 위해서는 리팩토링 연습을 꾸준히 해야하는데 인텔리제이에서 제공해주는 단축키를 쓰면 습관을 들이는데 좋은 밑거름이 되지 않을까 싶습니다. 서론이 너무 길었습니다 ㅎㅎ...

cmd(⌘) + alt(⌥) + M : extractMethod로 복잡한 반복문 같은 경우 리팩토링하여 메소드로 뽑아내는 기능입니다. 정말 꿀 같은 기능입니다.

cmd(⌘) + alt(⌥) + V : 파라미터 변수를 생성하고 출력까지 합니다.

cmd(⌘) + alt(⌥) + P : extractVariable 변수를 만들어서 해당 값을 저장하고 출력까지 한번에 가능한 단축키로 마찬가지로 정말 좋은 단축키입니다.

쿼리를 위한 단축키

Ctrl(⌃) + Shift(⇧) + J : 하단에 있는 문자열을 합치는 단축키 입니다. 쿼리문 같은 문자열을 한줄로 합칠때 사용합니다.

Live Template Customizing 하는 방법

이것도 자주 사용하는 기능들을 축약어로 설정하여 빠르게 템플릿을 커스터마이징 해주는 훌륭한 기능입니다. 저 같은 경우에는 TDD를 실천하기 위해서 given, when, then 방식으로 테스트를 수행하기 위해서 tdd 축약어라는 테스트 코드를 라이브 템플릿으로 만들어 봤습니다.

스크린샷 2019-11-24 오후 5 27 45

'잡다한 것' 카테고리의 다른 글

Git- 실전 가이드  (0) 2019.11.29
Dev Festival을 다녀와서...  (0) 2019.11.17
자기소개  (0) 2019.10.03

영속성 전이: CASCADE

특정 엔티티를 영속 상태로 만들 때 연관된 엔티티도 함께 영속 상태로 만들고 싶으면 영속성 전이 기능을 사용하면 됩니다. JPA는 CASCADE 옵션으로 영속성 전이를 제공합니다. 쉽게 말해서 영속성 전이를 사용하면 부모 엔티티를 저장할 때 자식 엔티티도 함께 저장 할 수 있습니다.

// 부모 엔티티
@Setter
@Getter
@Entity
public class Parent {

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

    private String name;

    @OneToMany(mappedBy = "parent")
    private List<Child> children = new ArrayList<Child>();
}

//자식 엔티티
@Getter
@Setter
@Entity
public class Child {

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

    @ManyToOne
    private Parent parent;

}

위의 코드는 부모 엔티티가 여러 자식 엔티티를 가진다고 가정해보겠습니다.

@Test
@Transactional
public void printUser() throws Exception {
    // 부모 저장
    Parent parent = new Parent();
    parent.setName("임종수");
    entityManager.persist(parent);

    // 1번 자식 저장
    Child child1 = new Child();
    child1.setName("임준영");
    child1.setParent(parent); // 자식 -> 부모 연관관계 설정
    parent.getChildren().add(child1); // 부모 -> 자식
    entityManager.persist(child1);

    // 2번 자식 저장
    Child child2 = new Child();
    child2.setName("임주리");
    child2.setParent(parent); // 자식 -> 부모 연관관계 설정
    parent.getChildren().add(child2); // 부모 -> 자식
    entityManager.persist(child2);       
}

JPA에서 엔티티를 저장할 때 연관된 모든 엔티티는 영속 상태어야 합니다.
따라서 위의 코드를 보면 부모 엔티티를 영속 상태로 만들고 자식 엔티티도 각각 영속 상태로 만듭니다. 이럴 때 영속성 전이를 사용하면 부모 엔티티만 영속 상태로 만들면 연관된 자식까지 한번에 영속 상태로 만들 수 있습니다.

영속성 전이: 저장

영속성 전이를 활성화하는 CASCADE 옵션을 적용해보겠습니다.

@Setter
@Getter
@Entity
public class Parent {

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

    private String name;


    @OneToMany(mappedBy = "parent", cascade = CascadeType.PERSIST)
    private List<Child> children = new ArrayList<Child>();
}

부모를 영속화 할 때 자식들도 함께 영속화하라고 cascade = CascadeType.PERSIST 옵션을 설정했습니다. 이 옵션을 적용하면 아래 코드처럼 간편하게 부모와 자식 엔티티를 한 번에 영속화 할 수 있습니다.

@Test
@Transactional
@Rollback(false)
public void printUser() throws Exception {

    // 1번 자식 저장
    Child child1 = new Child();
    // 2번 자식 저장
    Child child2 = new Child();

    Parent parent = new Parent();
    parent.setName("임종수");
    child1.setName("임준영");
    child2.setName("임주리");
    child1.setParent(parent); // 자식 -> 부모 연관관계 설정
    child2.setParent(parent); // 자식 -> 부모 연관관계 설정
    parent.getChildren().add(child1); // 부모 -> 자식
    parent.getChildren().add(child2); // 부모 -> 자식

    // 부모 저장
    entityManager.persist(parent);
}

CASCADE 실행

스크린샷 2019-11-23 오전 2 52 25

부모만 영속화하면 CascadeType.PERSIST로 설정한 자식 엔티티까지 함께 영속화해서 저장합니다.

이 코드의 쿼리 결과를 보면 데이터가 정상적으로 2건 입력된 것을 확인할 수 가 있습니다.

스크린샷 2019-11-23 오전 2 54 54

영속성 전이는 연관관계를 매핑하는 것과 아무 관련이 없습니다. 단지 엔티티를 영속화 할 때 연관된 엔티티도 같이 영속화하는 편리함을 제공할 뿐입니다.

영속성 전이: 삭제

방금 저장한 부모와 자식 엔티티를 모두 제거하려면 다음 코드와 같이 각각의 엔티티를 하나씩 제거해야 합니다.

Parent findParent = em.find(Parent.class, 1L);
Child findChild1 = em.find(Child.class, 1L);
Child findChild2 = em.find(Child.class, 2L);

em.remove(findChild1);
em.remove(findChild2);
em.remove(findParent);

영속성 전이는 엔티티를 삭제할 때도 사용할 수 있습니다. CascadeType.REMOVE로 설정하고 다음 코드처럼 부모 엔티티만 삭제하면 연관된 자식 엔티티도 함께 삭제 됩니다.

Parent findParent = em.find(Parent.class, 1L);
em.remove(findParent);

코드를 실행하면 DELETE SQL을 3번 실행하고 부모는 물론 연관된 자식도 모두 삭제합니다. 삭제 순서는 외래키 제약조건을 고려해서 자식을 먼저 삭제하고 부모를 삭제합니다.

만약 CascadeType.REMOVE를 설정하지 않고 이 코드를 실행하면 부모 엔티티만 삭제 됩니다. 하지만 데이터베이스의 부모 로우를 삭제하는 순간 자식 테이블에 걸려 있는 외래 키 제약조건으로 인해, 데이터베이스에서 외래 키 무결성 예외가 발생합니다.

CASCADE의 종류

public enum CascadeType{

    ALL, // 모두적용
    PERSIST, // 영속
    MERGE, // 병합
    REMOVE, // 삭제
    REFRESH, // REFRESH
    DETACH // DETACH
}

다음처럼 여러 속성을 같이 사용할 수 있습니다.

cascade = {CascadeType.PERSIST, CascadeType.REMOVE}

참고로 CascadeType.PERSIST, CascadeType.REMOVE는 em.persist(), em.remove()를 실행 할 때 바로 전이가 발생하지 않고 플러시를 호출 할 때 전이가 발생합니다.

고아 객체

JPA는 부모 엔티티와 연관관계가 끊어진 자식 엔티티를 자동으로 삭제하는 기능을 제공하는데 이것을 고아 객체 제거라고 합니다.
이 기능을 사용해서 부모 엔티티의 컬렉션에서 자식 엔티티의 참조만 제거하면 자식 엔티티가 자동으로 삭제하도록 코드를 작성해 보겠습니다.

@Setter
@Getter
@Entity
public class Parent {

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

    private String name;

    @OneToMany(mappedBy = "parent", orphanRemoval = true)
    private List<Child> children = new ArrayList<Child>();
}

고아 객체 제거 기능을 활성화하기 위해 컬렉션에 orphanRemoval = true를 설정합니다. 이제 컬렉션에서 제거한 엔티티는 자동으로 삭제됩니다.

Parent parent1 = em.find(Parent.lcass, id);
parent1.getChildren().remove(0); //자식 엔티티를 컬렉션에서 제거

실행결과

스크린샷 2019-11-23 오후 9 06 09

사용 코드를 보면 컬렉션에서 첫 번째 자식을 제거합니다. 고아 객체 제거 기능은 영속성 컨텍스트를 플러시할 때 적용되므로 플러시 시점에 DELETE SQL이 실행됩니다.

모든 자식 엔티티를 제거하려면 다음 코드처럼 컬렉션을 비우면 됩니다.

parent1.getChildren().clear();

고아 객체를 정리하면 참조가 제거된 엔티티는 다른곳에서 참조하지 않는 고아 객체로 보고 삭제하는 기능입니다. 따라서 이 기능은 참조하는 곳이 하나일 때만 사용해야 합니다. 쉽게 말하자면 특정 엔티티가 개인 소유하는 엔티티에만 이 기능을 적용해야 합니다. 만약 삭제한 엔티티를 다른 곳에서도 참조한다면 문제가 발생할 수 있습니다. 이런 이유로 orphanRemovel은 @OneToOne, @OneToMany에만 사용할 수 있습니다.

영속성 전이 + 고아객체, 생명주기

CascadeType.ALL + orphanRemoval = true를 동시에 사용하면 부모 엔티티를 통해서 자식의 생명주기를 관리 할 수 있습니다.

//자식을 저장하려면 부모에 등록만 하면 됩니다.
Parent parent = em.find(Parent.class , parentId);
parent.addChild(child);

//자식을 삭제하려면 부모에서 제거하면 됩니다.
Parent parent = em.find(Parent.class , parentId);
parent.getChildren().remove(removeObject);
참조: ORM 표준 JPA 프로그래밍

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

JPA에 대한 사실과 오해  (0) 2019.11.28
값 타입  (0) 2019.11.26
지연로딩과 즉시로딩  (0) 2019.11.23
프록시와 연관관계 관리  (0) 2019.11.22
고급매핑 - 조인테이블  (0) 2019.10.30

+ Recent posts