Overview

compile 'org.springframework.boot:spring-boot-starter-web'
implementation 'org.springframework.boot:spring-boot-starter-web'

testCompile 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.springframework.boot:spring-boot-starter-test'

build.gradle 에 관한 설정을 검색하다보면 의존 라이브러리를 추가할 때 두가지 방법을 자주 봅니다.

compileimplementation 은 무슨 차이가 있을까요?


1. compile 은 상위 모듈까지 가져온다

compileimplementation 보다 더 많은 라이브러리를 빌드합니다.

예를 들어 다음과 같이 의존하는 관계의 프로젝트 세 개가 있다고 가정합니다.

myApp -> mySpring -> myJava

myApp 에서 mySpring 을 의존하고 mySpring 은 myJava 를 의존합니다.

이 때 compile 을 사용해서 mySpring 을 빌드하게 되면 mySpring 이 의존하고 있는 myJava 까지 함께 빌드합니다.

그래서 myApp 에서 myJava 모듈이 제공하는 API 까지 사용할 수 있습니다.

만약 myJava 를 직접적으로 사용할 필요가 없다면 필요하지 않은 API 들이 노출되고 빌드 시간도 오래 걸리기 때문에 비효율적인 행동이 됩니다.

대신 implementation 을 사용해서 빌드하면 mySpring 모듈만 가져오기 때문에 빌드 속도가 빠르고 필요한 API 만 노출해서 사용할 수 있습니다.


2. compile 은 deprecated 되었다

그리고 compile 은 deprecated 되고 api 로 대체되었습니다.

그러니 만약 상위 모듈까지 전부 가져오고 싶을 땐 compile 대신 api 를 사용하면 됩니다.

일반적인 경우에는 implementation 을 사용해서 빌드 속도를 향상시키는 것이 좋습니다.


Conclusion

  • implementation 은 지정한 모듈만 가져오고 compile, api 는 상위 모듈까지 전부 가져옵니다.
  • compile 은 deprecated 되었고 대신 api 를 사용하면 됩니다.
  • 일반적인 상황에서는 빌드 속도가 빠르고 필요한 모듈만 가져오는 implementation 을 사용하면 됩니다.

Reference

1. Overview

Java Enum 에 이어 Kotlin Enum 사용법에 대해서도 알아봅니다.

사용법과 이유에 대해서는 Java 에서 알아보았으니 간단하게 코드만 작성합니다.


2. 기본 사용법

enum class Day {
    MON, TUE, WED, THU, FRI, SAT, SUN
}

3. 필드값 추가

enum class Day(
    val label: String
) {
    MON("Monday"),
    TUE("Tuesday"),
    WED("Wednesday"),
    THU("Thursday"),
    FRI("Friday"),
    SAT("Saturday"),
    SUN("Sunday")
    ;
}

Kotlin 은 원래 ; 을 안쓰지만 Enum 클래스에 필드값을 추가하는 경우 마지막에 꼭 추가해야합니다.


4. 필드값 캐싱

enum class Day(
    val label: String
) {
    MON("Monday"),
    TUE("Tuesday"),
    WED("Wednesday"),
    THU("Thursday"),
    FRI("Friday"),
    SAT("Saturday"),
    SUN("Sunday")
    ;

    companion object {
        private val LABEL_CACHE: Map<String, Day> =
            values().associateBy { it.label }

        fun findByLabel(label: String) = LABEL_CACHE[label]
    }
}

5. 상수별 메소드 구현

enum class Operation {
    PLUS {
        override fun apply(x: Double, y: Double): Double {
            return x + y
        }
    },

    MINUS {
        override fun apply(x: Double, y: Double): Double {
            return x - y
        }
    },

    TIMES {
        override fun apply(x: Double, y: Double): Double {
            return x * y
        }
    },

    DIVIDE {
        override fun apply(x: Double, y: Double): Double {
            return x / y
        }
    };

    abstract fun apply(x: Double, y: Double): Double
}

Reference

1. Overview

Java Enum 1편 : Enum 기본적인 사용에 대해서는 이미 학습했습니다.

이번에는 Enum 에 메소드를 추가하여 원하는 동작을 만들어내는 방법과 그밖의 활용법을 알아봅니다.


2. 메소드 추가 1: Enum 상수 별로 다른 동작이 필요할 때

가장 쉽게 떠올릴 수 있는 방법은 switch 문입니다.

하지만 Enum 클래스에는 상수별 메소드 구현 (Constant-specific Method Implementation) 이라는 좀더 깔끔한 방법이 있습니다.


2.1. Before

public enum Operation {
    PLUS, MINUS, TIMES, DIVIDE;

    // 상수가 뜻하는 연산을 수행한다.
    public double apply(double x, double y) {
        switch (this) {
            case PLUS: return x + y;
            case MINUS: return x - y;
            case TIMES: return x * y;
            case DIVIDE: return x / y;
        }
        throw new AssertionError("알 수 없는 연산: " + this);
    }
}
  • 깔끔해보이지만 뭔가 아쉬움
  • 마지막 AssertionError 는 실제로는 도달하지 기술적으로는 도달 가능하기 때문에 생략 불가능
  • 새로운 상수를 추가하면 case 문도 추가해야함

2.2. After

public enum Operation {
    PLUS   { public double apply(double x, double y) { return x + y; }},
    MINUS  { public double apply(double x, double y) { return x - y; }},
    TIMES  { public double apply(double x, double y) { return x * y; }},
    DIVIDE { public double apply(double x, double y) { return x / y; }};

    public abstract double apply(double x, double y);
}
  • Enum 상수값 별로 다르게 동작하는 코드를 구현
  • apply 라는 추상 메소드를 선언하고 각 상수에서 재정의
  • 이를 상수별 메소드 구현 (constant-specific method implementation) 이라고 함
  • 추상 메소드로 정의되어 있기 때문에 새로운 상수를 추가해도 실수할 가능성이 적음

3. 메소드 추가 2: Enum 상수 일부가 같은 동작을 공유할 때

위에서 본 방법은 Enum 에 있는 각각의 상수가 모두 다른 동작을 할 때 사용했습니다.

만약 일부 상수끼리 같은 동작을 공유해야 할 때는 어떻게 해야 할까요?

일반적으로 생각 가능한 방법은 두가지가 있습니다.

  1. 상수별로 메소드를 구현해서 같은 동작 코드를 중복해서 넣는다.
  2. 별도의 메소드를 하나 만들어서 상수별 메소드에서 호출한다.

위 두가지 방법 모두 중복된 코드를 작성해야 한다는 단점이 있습니다.

다행히 Enum 클래스에서는 이러한 상황에서 전략 열거 타입 (Enum) 이라는 방법이 있습니다.


3.1. Before

public enum Fruit {
    APPLE, ORANGE, BANANA, STRAWBERRY;

    public void printColor() {
        switch (this) {
            case APPLE:
            case STRAWBERRY:
                System.out.println("This is Red");
                break;
            default:
                System.out.println("This is Not Red");
        }
    }
}
  • 과일을 나타내는 Fruit Enum 클래스
  • printColor() 메소드를 호출하면 빨간색 과일들과 나머지 과일들의 출력 결과문이 다름
  • 위의 문제점과 마찬가지로 새로운 빨간색 과일을 추가했을 때 switch 문에도 추가하지 않으면 빨간색 과일인데 "This is Not Red" 가 출력됨

3.2. After

public enum Fruit {
    APPLE(ColorType.RED),
    ORANGE(ColorType.OTHER),
    BANANA(ColorType.OTHER),
    STRAWBERRY(ColorType.RED);

    private final ColorType colorType;

    Fruit(ColorType colorType) {
        this.colorType = colorType;
    }

    public void printColor() {
        colorType.printColor();
    }

    enum ColorType {
        RED {
            void printColor() {
                System.out.println("This is Red");
            }
        },
        OTHER {
            void printColor() {
                System.out.println("This is Not Red");
            }
        };

        abstract void printColor();
    }
}
  • Fruit Enum 클래스 내부에 ColorType 이라는 Inner Enum 클래스를 정의
  • printColor() 의 동작을 ColorType 에 위임
  • 새로운 빨간색 과일이 추가되더라도 ColorType 을 지정해야 하므로 실수할 일이 적음

4. 메소드 추가 3: 여러 상수별 동작이 혼합될 때

한 Enum 상수값의 동작에 다른 Enum 상수값이 필요하다면 그냥 switch 문을 쓰는 것이 좋습니다.

public enum Direction {
    NORTH, EAST, SOUTH, WEST;

    public static Direction rotate(Direction dir) {
        switch (dir) {
            case NORTH: return EAST;
            case EAST:  return SOUTH;
            case SOUTH: return WEST;
            case WEST:  return NORTH;
        }
        throw new AssertionError("알 수 없는 방향: " + dir);
    }
}

5. ordinal 메서드 대신 인스턴스 필드를 사용하라

Enum 클래스에는 기본적으로 ordinal 이라는 메소드를 제공합니다.

0 부터 시작되며 특정 상수값의 위치 (Index) 를 리턴해줍니다.

Enum API 문서를 보면 ordinal 에 대해서 이렇게 쓰여 있습니다.

"대부분의 개발자는 이 메소드를 쓸 일이 없다. 이 메소드는 EnumSetEnumMap 같이 열거 타입 기반 범용 자료구조에 쓸 목적으로 설계되었다."

oridnal 을 사용할 때의 단점은 여러 가지 있습니다.

  • 나중에 추가될 Enum 상수값이 꼭 순서대로라는 보장이 없다
  • 중복된 숫자를 가져야 할 때 구분이 불가능하다

그러므로 ordinal 메소드를 사용하지 말고 별도의 인스턴스 필드를 선언해서 사용합시다.


6. ordinal 인덱싱 대신 EnumMap 을 사용하라

Enum 값을 Index 로 사용하고 싶을 때 배열 + ordinal 을 사용하는 것보다 EnumMap 을 사용하는 것이 좋습니다.

EnumMap 도 내부적으로 ordinal 을 사용하기 때문에 성능 상의 차이도 없습니다.

위에서도 한번 언급했었지만 개발자가 직접 ordinal 을 쓸 상황은 없습니다.


7. 비트 필드 대신 EnumSet 을 사용하라

과거에는 여러 값들을 집합으로 사용해야 할 경우 비트로 사용했습니다.

public class Text {
    public static final int STYLE_BOLD          = 1 << 0;   // 1
    public static final int STYLE_ITALIC        = 1 << 1;   // 2
    public static final int STYLE_UNDERLINE     = 1 << 2;   // 4
    public static final int STYLE_STRIKETHROUGH = 1 << 3;   // 8

    // 매개변수 styles 는 0 개 이상의 STYLE_ 상수를 비트별 OR 한 값
    public void applyStyles(int styles) {
        // ...
    }
}

// usage
text.applyStyles(STYLE_BOLD | STYLE_ITALIC);
  • 여러 개의 상수값을 OR 하여 사용하면 집합을 나타낼 수 있음
  • 이렇게 만들어진 집합을 비트 필드 (bit field) 라고 함
  • 비트 필드 값은 해석하기 어려움
  • 최대 몇 비트가 필요한지 API 작성 시 미리 예측하여 적절합 타입 (int, long) 을 선택해야 함

7.1. EnumSet 클래스

public class Text {
    public enum Style { BOLD, ITALIC, UNDERLINE, STRIKETHROUGH }

    public void applyStyles(Set<Style> styles) {
        // ...
    }
}

// usage
text.applyStyles(EnumSet.of(Text.Style.BOLD, Text.Style.ITALIC));
  • java.util 패키지
  • Set 인터페이스를 구현하며, 타입 안전하고, 다른 어떤 Set 구현체와도 함께 사용 가능
  • EnumSet 내부는 비트 벡터로 구현됨

Reference

Set

JavaScript 에서 Set 자료 구조는 ES6 에서 추가되었습니다.

Set 은 중복을 허용하지 않고 순서가 없는 리스트입니다.


생성자 Constructor

const a = new Set()
// Set { }

const b = new Set([1, 2, 3])
// Set { 1, 2, 3 }

const c = new Set([1, 1, 1])
// Set { 1 }

add

값을 추가합니다.

값을 추가할 때 Object.is() 메서드를 사용해서 값을 비교합니다.

const a = new Set()
// Set { }

a.add(1)
// Set { 1 }

a.add(2)
// Set { 1, 2 }

a.add(1)
// Set { 1, 2 }

size

크기를 알려줍니다.

size() 가 아니라 size 입니다.

const a = new Set([1, 2, 3])
// Set { 1, 2, 3 }

a.size
// 3

has

값이 이미 있는 지 검사합니다.

const a = new Set([1])
// Set { 1 }

a.has(1)
// true

a.has(2)
// false

delete

값을 지웁니다.

만약 값이 있어서 지우는데 성공하면 true 를 리턴하고 아니면 false 를 리턴합니다.

const a = new Set[1, 2, 3])
// Set { 1, 2, 3 }

a.delete(1)
// true
// Set { 2, 3 }

a.delete(4)
// false
// Set { 2, 3 }

clear

Set 에 있는 모든 값을 지웁니다.

const a = new Set([1, 2, 3])
// Set { 1, 2, 3 }

a.clear()
// Set { }

forEach

forEach 를 사용하여 Set 을 순회할 수 있습니다.

forEach 는 콜백 함수를 파라미터로 받으며 콜백 함수는 세 가지 파라미터를 받습니다.

  1. 키 (index)
  2. 현재 배열 (여기서는 Set)

Set 은 키값이 따로 없기 때문에 1번 2번이 같은 값을 가집니다.

const a = new Set([1, 2, 3, 4, 5])
// Set { 1, 2, 3, 4, 5 }

a.forEach((value) => {
    console.log(value)
})
// 1
// 2
// 3
// 4
// 5

a.forEach((key, value) => {
    console.log(key, value)
})
// 1 1
// 2 2
// 3 3
// 4 4
// 5 5

a.forEach((key, value, currentSet) => {
    console.log(key value, currentSet)
})
// 1 1 Set { 1, 2, 3, 4, 5 }
// 2 2 Set { 1, 2, 3, 4, 5 }
// 3 3 Set { 1, 2, 3, 4, 5 }
// 4 4 Set { 1, 2, 3, 4, 5 }
// 5 5 Set { 1, 2, 3, 4, 5 }

Set ⇒ Array 또는 Array ⇒ Set

전개 연산자 (spread) 를 사용하면 간단하게 Set 을 Array 로, Array 를 Set 으로 변경할 수 있습니다.

const a = new Set([1, 2, 3]) 
// Array => Set { 1, 2, 3}

const b = [...a]
// Set => [1, 2, 3]

문자열 한글 포함 여부 확인

정규식을 사용하면 한글의 포함 여부를 알 수 있습니다.

const koRegex = /[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]/;

koRegex.check("hello world"); // false
koRegex.test("안녕"); // true
koRegex.test("반가워 Hi"); // true
koRegex.test("ㅏㅏㅁㄹㄹㄲ"); // true
  1. IntelliJ IDEA > Preferences > Editor > Inspections 으로 이동
  2. Non-ASCII characters 체크 해제

1. Overview

Java Enum 타입은 일정 개수의 상수 값을 정의하고, 그 외의 값은 허용하지 않습니다.

과거에는 특정 상수값을 사용하기 위해선 모두 상수로 선언해서 사용했습니다.

public static final String MON = "Monday";
public static final String TUE = "Tuesday";
public static final String WED = "Wednesday";

이렇게 사용하면 개발자가 실수하기도 쉽고 한눈에 알아보기도 쉽지 않습니다.

그리고 관련있는 값들끼리 묶으려면 접두어를 사용해서 점점 변수명도 지저분해집니다.

Enum 클래스는 이러한 문제점을 말끔히 해결해주는 굉장히 유용한 클래스입니다.

추가적인 활용법은 Java Enum 2편 : 여러가지 활용법 에서 다루기로 하고 여기서는 기본적인 사용법에 대해서 알아봅니다.


2. 정의

public enum Day {
    MON, TUE, WED, THU, FRI, SAT, SUN
}

위처럼 단순하게 요일을 열거한 Enum 클래스를 만들 수 있습니다.

하지만 각각의 요소들이 특정한 값을 갖게 하고 싶을 수도 있습니다.

예를 들어, 각 요일의 풀네임 (full-name) 이 필요할 때도 있을겁니다.


3. 생성자와 final 필드 추가

public enum Day {
    MON("Monday"),
    TUE("Tuesday"),
    WED("Wednesday"),
    THU("Thursday"),
    FRI("Friday"),
    SAT("Saturday"),
    SUN("Sunday")
    ;

    private final String label;

    Day(String label) {
        this.label = label;
    }

    public String label() {
        return label;
    }
}

Enum 요소에 특정 값을 매핑하고 싶다면 위 코드처럼 필드값을 추가하면 됩니다.

여기서는 label 이라는 String 값을 추가했습니다.

필드값을 추가하면 생성자도 함께 추가해야하는데 Enum 클래스는 생성자가 있다고 하더라도 new 연산으로 생성할 수 없습니다.


System.out.println(Day.MON.name());      // MON
System.out.println(Day.MON.label());     // Monday

이렇게 규칙이 존재하는 특정 요소들을 하나의 Enum 클래스로 묶어두면 가독성도 좋아지고 if 문으로 일일히 검사할 필요도 없어서 편리합니다.

필드값을 추가할 때 이름을 name 으로 정하는건 피하는게 좋습니다.

Enum 클래스 자체에서 name() 이라는 메소드를 제공하기 때문에 헷갈릴 수 있습니다.


4. 필드값으로 Enum 값 찾기

Enum 은 자체적으로 name() 값으로 Enum 값을 찾는 valueOf() 메소드를 제공합니다.

특정 필드값으로 찾는 기능은 제공하지 않기 때문에 직접 만들어야 합니다.


4.1. 직접 Enum values() 순회하며 찾기

public enum Day {
    // ..codes

    public static Day valueOfLabel(String label) {
        return Arrays.stream(values())
                    .filter(value -> value.label.equals(label))
                    .findAny()
                    .orElse(null);
    }
}

다른 필드값으로 찾기 위해선 위 코드와 같이 모든 Enum 값을 순회하면서 일치하는 값이 있는지 찾아야 합니다.


4.1. 캐싱해서 순회 피하기

public enum Day {
    // ..codes

    private static final Map<String, Day> BY_LABEL =
            Stream.of(values()).collect(Collectors.toMap(Day::label, e -> e));

    public static Day valueOfLabel(String label) {
        return BY_LABEL.get(label);
    }
}

HashMap 을 사용해서 값을 미리 캐싱해두면 조회할 때마다 모든 값을 순회할 필요가 없습니다.

처음부터 값을 캐싱해두는게 싫다면 valueOfLabel() 에 처음 접근할 때 Lazy Caching 할 수 있습니다.

다만, 이 때는 동시성 문제 해결을 위해 HashMap 을 동기화 해야 합니다.

그리고 위 코드에서는 valueOfLabel() 메소드를 그냥 리턴했기 때문에 없는 label 값으로 호출하면 null 값이 리턴됩니다.

이런 경우에 사용자에게 nullable 가능성을 알려주기 위해 반환 값을 Optional<Day> 로 넘겨주는 방법도 있습니다.


5. 여러 개의 값 연결하기

public enum Day {
    MON("Monday", 10),
    TUE("Tuesday", 20),
    WED("Wednesday", 30),
    THU("Thursday", 40),
    FRI("Friday", 50),
    SAT("Saturday", 60),
    SUN("Sunday", 70)
    ;

    private final String label;
    private final int number;

    Day(String label, int number) {
        this.label = label;
        this.number = number;
    }

    public String label() {
        return label;
    }

    public int number() {
        return number;
    }

    private static final Map<String, Day> BY_LABEL =
            Stream.of(values()).collect(Collectors.toMap(Day::label, Function.identity()));

    private static final Map<Integer, Day> BY_NUMBER =
            Stream.of(values()).collect(Collectors.toMap(Day::number, Function.identity()));

    public static Day valueOfLabel(String label) {
        return BY_LABEL.get(label);
    }

    public static Day valueOfNumber(int number) {
        return BY_NUMBER.get(number);
    }
}

위 코드처럼 여러 개의 필드값을 세팅할 수 있습니다.

여기서 사용한 예시는 요일이라서 그냥 number 필드를 추가했지만 만약 과일이라면 사과의 이름, 색, 무게, 가격 등등 여러 요소를 한번에 매핑시켜서 정의할 수 있습니다.


Reference

1. Overview

BatchSize 는 JPA 의 성능 개선을 위한 옵션 중 하나입니다.

여러 개의 프록시 객체를 조회할 때 WHERE 절이 같은 여러 개의 SELECT 쿼리들을 하나의 IN 쿼리로 만들어줍니다.

간단한 테스트와 함께 사용법을 알아봅니다.


2. Domain 정의

@Entity
public class Parent {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "parent_id")
    private Long id;

    private String name;

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


@Entity
public class Child {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "child_id")
    private Long id;

    private String name;

    @ManyToOne(fetch = FetchType.LAZY)
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

간단한 Parent(1) <-> Child(N) 관계의 도메인을 작성했습니다.

편의상 Getter/Setter 는 생략합니다.


3. BatchSize 정의

@Target({TYPE, METHOD, FIELD})
@Retention(RUNTIME)
public @interface BatchSize {
    int size();
}

@BatchSize 클래스 파일을 보면 위과 같이 나와있습니다.

Type, Method, Field 에 사용할 수 있으며 size 를 설정해야 합니다. (Method 에 설정하는 건 자주 사용하지 않아서 이 포스트에선 제외합니다)

size 는 간단히 말해서 IN 절에 들어갈 요소의 최대 갯수를 의미합니다.

만약 IN 절에 size 보다 더 많은 요소가 들어가야 한다면 여러 개의 IN 쿼리로 나누어 날립니다.


3.1. Type (Class) 에 정의

@BatchSize(size = 100)
@Entity
public class Parent {
    ...
}

Entity 클래스 위에 붙일 수 있습니다.

만약 다른 엔티티에서 여러 개의 Parent 객체를 프록시로 호출한다면 배치사이즈가 적용되어 IN 쿼리로 조회할 겁니다.


3.2. Field 에 정의

@Entity
public class Parent {

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

@OneToMany 를 사용하는 Collections 에 붙이면 여러 Parent 객체가 getChildren() 호출할 때 하나의 쿼리로 가져옵니다.


3.3. application.yml 에 정의

spring:
    properties:
      hibernate:
        default_batch_fetch_size: 100

application.yml 에 추가하면 프로젝트 전역으로 배치 사이즈를 적용할 수 있습니다.


4. 호출 테스트

List<Parent> parents = parentRepository.findAll();

// 실제로 사용해야 쿼리가 나가기 때문에 size() 까지 호출해줌
parents.get(0).getChildren().size();
parents.get(1).getChildren().size();

Parent, Child 데이터가 이미 존재한다고 가정하고 테스트 코드를 작성했습니다.

parents 에서 for 문으로 간단하게 작성해도 되지만 명시적으로 두번 호출해봅니다.


4.1. Before

SELECT * FROM parent

SELECT * FROM child WHERE child.parent_id = 1
SELECT * FROM child WHERE child.parent_id = 2

배치 사이즈를 적용하지 않으면 child 테이블을 조회하기 위해 두 개의 쿼리가 날아갑니다.

만약 parents 의 갯수가 더 많다면 갯수만큼 쿼리가 날아갈겁니다.


4.2. After

SELECT * FROM parent

SELECT * FROM child WHERE child.parent IN (1, 2)

배치 사이즈를 추가하면 여러 쿼리를 하나의 IN 쿼리로 만들어줍니다.

IN 절에 들어가는 요소의 갯수는 설정 가능합니다.

만약 조건 갯수보다 설정한 배치사이즈 크기가 더 작다면 IN 쿼리가 추가로 날아갑니다.

예를 들어 size 를 100 으로 설정했기 때문에 데이터가 250 개라면 1 ~ 100, 101 ~ 200, 201 ~ 250 이렇게 세 번에 나누어서 IN 쿼리를 날립니다.

(사실 완전히 똑같은 사이즈로 분배해서 날리지는 않고 내부적으로 최적화한 사이즈로 나누어서 날립니다)


5. Conclusion

BatchSize 옵션을 사용하면 비슷한 조회 쿼리 데이터들을 한번에 가져올 수 있어 성능적으로 효과를 볼 수 있습니다.

1. Overview

JPA 에서 연관 관계를 설정할 때 여러가지 옵션을 추가할 수 있습니다.

삭제에 관련된 옵션은 orphanRemovalcascade 가 있는데 둘이 어떤점이 다른지 알아봅니다.

 

2. orphanRemoval

JPA 2.0 부터 지원하며 부모 엔티티와 관계가 끊어진 자식 엔티티를 자동으로 삭제해줍니다.

@OneToMany@OneToOne 에서 지원하는 옵션입니다.

아마 @ManyToOne 는 보통 연관 관계의 주인인 엔티티가 사용해서 저 옵션이 없는 것 같네요.

 

@Entity
public class School {

    @OneToMany(mappedBy = "school", orphanRemoval = true)
    private List<Teacher> teachers = new ArrayList<>();
}

@Entity
public class Teacher {

    @ManyToOne
    @JoinColumn(name = "school_id")
    private School school;
}

위 도메인은 School(1) <-> Teacher(N) 인 다대일 양방향 관계를 나타냅니다.

원래는 연관 관계의 주인만 외래키의 저장, 조회, 삭제 권한을 갖기 때문에 School.teachers Collections 에서 데이터를 지워도 반영되지 않습니다.

그러나 orphanRemoval = true 옵션을 추가해주면 부모 엔티티의 컬렉션에서 자식 엔티티를 삭제할 때 참조가 끊어지면서 DB 에서도 삭제됩니다.

 

3. CascadeType.REMOVE

@Entity
public class School {

    @OneToMany(mappedBy = "school", cascade = CascadeType.REMOVE)
    private List<Teacher> teachers = new ArrayList<>();
}

@Entity
public class Teacher {

    @ManyToOne
    @JoinColumn(name = "school_id")
    private School school;
}

cascade 는 영속성 전이에 관한 옵션입니다.

설정된 엔티티가 저장/수정/삭제 될 때 연관된 엔티티들도 전부 동일한 액션을 해줍니다.

그래서 연관 관계의 주인이 아니더라도 해당 엔티티를 지우면 관련된 모든 엔티티들이 삭제됩니다.

 

4. Conclusion

  • 부모 엔티티가 삭제되면 자식 엔티티도 전부 삭제되는 것은 동일하지만 원인이 다름
    • orphanRemoval = true 옵션은 부모 엔티티가 사라지면서 자식 엔티티와의 참조(연결)가 끊어져서 삭제됨
    • CascadeType.REMOVE 옵션은 원래 엔티티가 삭제될 때 연관된 엔티티를 전부 삭제하는 옵션
  • orphanRemoval 옵션은 Collections 에서 자식 엔티티를 삭제하는 걸로 DB 에서도 삭제 가능하지만 cascade 는 불가능

 

Reference

+ Recent posts