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

1. Overview

JPA 의 Entity 는 그 자체로 테이블을 나타내기 때문에 어떻게 설계하는지가 중요합니다.

데이터는 테이블에 저장되어 있지만 그걸 사용하는 코드는 객체입니다.

객체와 테이블의 차이점을 알아야 제대로 된 엔티티 설계를 할 수 있습니다.

데이터베이스는 외래키를 이용해서 서로 다른 테이블끼리 상호 작용을 하는데, JPA 에서는 연관 관계라는 걸 이용합니다.

JPA 는 엔티티 사이의 연관 관계를 위해 @OneToOne, @OneToMany, @ManyToOne 라는 어노테이션을 제공합니다.


2. 연관 관계란?

JPA 에서 연관 관계를 매핑할 때 고려해야 할 점은 크게 두 가지가 있습니다.

  • 단뱡향, 양방향
  • 연관 관계의 주인

2.1. 단방향, 양방향

데이터베이스 관점에서는 Join 을 통해 여러 테이블을 한꺼번에 조회 가능하기 때문에 방향이라는 개념이 없습니다.

그러나 엔티티 (객체) 가 다른 연관된 엔티티를 조회하려면 필드값으로 참조해야 합니다.

예를 들어 Member (1) : Car (N) 관계의 테이블이 있다고 가정합니다.

Member 의 자동차 목록을 가져오기 위해선 List<Car> cars 필드값이 필요합니다.

이를 Member -> Car 단방향 참조라고 합니다.

반대로 Car 엔티티만 존재할 때, 이 자동차의 소유자를 알고 싶다면 Member member 필드값이 필요합니다.

이것 역시 Car -> Member 단방향 참조입니다.

이렇게 Member <-> Car 처럼 각 엔티티가 서로를 단방향으로 참조하고 있는 걸 양방향 참조 라고 합니다.

성능상 문제도 없는데 전부 양방향으로 참조 하면 되지 않을까? 하는 생각이 들 수도 있습니다.

하지만 양방향 참조를 한다는건 엔티티의 필드 갯수가 그만큼 늘어난다는 뜻입니다.

특히 사용자를 나타내는 User, Member, Account 등의 엔티티가 모든 엔티티에 대해 참조를 해버리면 필드 갯수가 어마어마하게 많은 복잡한 클래스가 될 겁니다.

그렇기 때문에 엔티티 설계를 할 때 참조가 꼭 필요하지 않은 상황이라면 단방향 참조만 하는 것을 추천합니다.


2.2. 연관 관계의 주인

두 엔티티가 양방향 관계일 때는 연관 관계의 주인를 정해야 합니다.

연관 관계의 주인이란 외래키를 관리하며 외래키 저장, 수정, 삭제의 권한을 갖는 실질적인 엔티티이고, 주인이 아닌 엔티티는 조회만 가능합니다.

주인이 아닌 엔티티에서 mappedBy 속성을 사용해서 주인 엔티티 필드에 붙이면 됩니다.

연관 관계의 주인은 mappedBy 속성을 사용하지 않습니다.

예를 들어 다대일에서 @ManyToOne 을 사용하는 다(N) 쪽은 항상 연관 관계의 주인이기 때문에 mappedBy 옵션을 지원하지 않습니다.

일반적으로 외래키가 있는 곳을 연관 관계의 주인로 많이 설정합니다. (다대일에서 다 쪽)


3. 일대일 (1:1)

요구사항

  • 도메인: 사람(Person), 회사(Compnay), 집(House)
  • 회사는 소유자가 존재한다 (Company -> Person)
  • 집은 주인이 존재한다 (House -> Person)
  • 사람은 집 정보를 갖고 있다 (Person -> House)

우선 사람이라는 공통 Domain Entity 가 존재합니다.

@Entity
public class Person {

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

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

    @OneToOne(mappedBy = "person")
    private House house;
}

3.1. 일대일 (1:1) 단방향

@Entity
public class Company {

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

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

    @OneToOne(cascade = CascadeType.PERSIST)
    @JoinColumn(name = "person_id")
    private Person person;
}
  • Company(1) -> Person(1)

두 테이블은 일대일 관계이기 때문에 어느 쪽에서 외래키를 관리할 지만 정하면 됩니다.

어느 쪽에서 참조하냐에 따라 외래키 위치가 정해지기 때문에 설계할 때 신중하게 정하는게 좋습니다.

예를 들어, 사람은 회사를 소유하지 않을 수도 있지만 주인 없는 회사는 일반적으로 없습니다.

객체 지향 관점에서 보면 사람이 회사를 소유하고 있기 때문에 PersonCompany 필드를 갖는게 자연스럽습니다.

하지만 그렇게 되면 person 테이블이 company_id 라는 외래키 컬럼을 갖게 되는데, 회사를 소유하지 않은 사람은 해당 컬럼값이 null 로 세팅됩니다.

null 값을 갖는 건 데이터베이스 관점에서는 바람직하지 않기 때문에 company 테이블에 person_id 컬럼을 만들면 null 데이터는 사라집니다.

이처럼 객체 지향 관점이냐 데이터베이스 관점이냐에 따라 외래키의 위치가 달라집니다.

또한 나중에 회사를 여러 사람이 공동 소유하거나, 한 사람이 소유할 수 있는 회사가 많아지면서 일대일 관계가 다대일 관계로 확장될 수 있습니다.

어느 쪽이 옳다고 정해진 것은 없으므로 여러 가지 상황과 확장성을 고려해서 테이블 및 엔티티를 설계하는게 좋습니다.


3.2. 일대일 (1:1) 양방향

@Entity
public class House {

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

    @Column(name = "address")
    private String address;

    @OneToOne(cascade = CascadeType.PERSIST)
    @JoinColumn(name = "person_id")
    private Person person;
}
  • House(1) <-> Person(1)

양쪽에 @OneToOne 으로 모두 참조값을 넣어주고 mappedBy 로 연관 관계의 주인을 설정해주면 됩니다.

두 엔티티가 서로를 참조할 수 있기 때문에 객체지향 관점에서는 걱정할 것이 없고 데이터베이스 관점으로 정하거나 추후 확장성을 고려해서 연관 관계의 주인을 설정해주는게 좋습니다.

여기서는 Person 객체에서 mappedBy 가 설정되어 있고 House 는 설정하지 않았기 때문에 House 가 연관 관계의 주인이 되어 외래키를 관리합니다.


4. 다대일 (N:1)

요구사항

  • 도메인: 학교(School), 학생(Student), 선생(Teacher)
  • 한 학교에 여러 학생이 다닐 수 있다 (Student -> School)
  • 한 학교에 여러 선생이 근무할 수 있다 (Teacher -> School)
  • 학교는 선생님들의 정보를 갖고 있다 (School -> Teacher)

우선 학교라는 공통 Domain Entity 가 존재합니다.

@Entity
public class School {

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

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

4.1. 다대일 (N:1) 단방향

@Entity
public class Student {

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

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

    @ManyToOne(cascade = CascadeType.PERSIST)
    @JoinColumn(name = "school_id")
    private School school;
}
  • Student(N) -> School(1)

다대일은 다(N) 쪽에 외래키가 있는 게 일반적입니다.

@ManyToOne 을 사용해서 School 필드값을 참조하면 되며, School 엔 별다른 설정을 하지 않아도 됩니다.

코드상으로는 학생은 학교의 정보가 있지만 학교 입장에서는 학생들의 정보를 직접적으로 알 수 없습니다.


4.2. 다대일 (N:1) 양방향

@Entity
public class Teacher {

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

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

    @ManyToOne(cascade = CascadeType.PERSIST)
    @JoinColumn(name = "school_id")
    private School school;
}
  • Teacher(N) <-> School(1)

단방향과 동일하지만 School 도메인에 @OneToMany 적용이 필요합니다.

다대일이기 때문에 참조하는 필드는 List 로 설정합니다.

학교는 teachers 를 통해 선생들의 정보를 알 수 있습니다.


5. 다대일 양방향 관계 데이터 저장

다대일 양방향 관계에서 데이터를 저장할 때는 Teacher.setSchool() 메소드와 School.getTeachers().add() 메소드를 모두 호출해서 객체끼리 동기화를 해주는 것이 중요합니다.

특히 주의할 점은 연관 관계의 주인이 데이터를 세팅해야 외래키에 값이 제대로 들어간다는 사실입니다.


5.1. 연관 관계의 주인이 아닌 일(1) 쪽에서만 세팅

School school = new School();
Teacher teacher = new Teacher();

school.getTeachers().add(teacher);  // 주인이 아닌 엔티티가 세팅

schoolRepository.save(school)
teacherRepository.save(teacher);
  • 위 코드처럼 List 에만 데이터를 넣고 저장하면 외래키가 세팅되지 않습니다.
  • school, teacher 각각 데이터는 들어가지만 teacher.school_id 값이 null 이 됩니다.

5.2. 연관 관계의 주인인 다(N) 쪽에서만 세팅

School school = new School();
Teacher teacher = new Teacher();

teacher.setSchool(school);  // 주인인 엔티티가 세팅

schoolRepository.save(school)
teacherRepository.save(teacher);
  • 위 코드를 실행하면 외래키까지 데이터가 제대로 저장됩니다.
  • 연관 관계의 주인이 외래키의 저장, 수정, 삭제를 담당하기 때문입니다.
  • 다만, 순수한 객체끼리의 데이터 동기화를 위해 school.getTeachers().add(teacher) 도 호출하는 것이 좋습니다.
    • 만약 List 에 데이터를 넣지 않으면 school.getTeachers() 를 호출해도 리스트가 비어 있습니다.
    • 물론 다른 트랜잭션에서 호출하면 DB 를 조회해서 가져오긴 하지만 같은 트랜잭션 내에서 추가로 작업을 한다면 혼란이 생길 수 있습니다.

Reference

'Framework > Spring' 카테고리의 다른 글

JPA Batch Size  (0) 2021.09.14
JPA Entity 삭제: orphanRemoval vs CascadeType.REMOVE  (0) 2021.08.28
Spring Boot 에서 Redis 사용하기  (4) 2021.08.11
Spring Boot Swagger 3.x 적용  (4) 2021.07.26
JPA 의 getById() vs findById()  (0) 2021.07.24

Overview

Java Coding Best Practices And Standards 에 있는 글을 번역한 글입니다.

여러 개의 항목이 있었는데, 그 중에서 제가 개발하면서 공감되었던 부분들만 간단하게 정리했습니다.

굉장히 기본적인 내용도 있는데 초심을 잊지 말자는 의미에서 기록합니다.

  1. NullPointerException 을 고려하자
  2. String 생성할 때 new 키워드를 사용하지 말자
  3. 반복문 내에서 새로운 객체를 생성하지 말자
  4. Collections 을 반복하는 동안 수정하지 말자
  5. Switch-Case 문에서 break 키워드를 뺴먹지 말자
  6. 객체 비교 "==" 와 "eqauls()" 의 차이를 알자
  7. 무작정 StringBuffer 를 사용하지 말자
  8. Java 파일 작성 시 표준

1. NullPointerException 을 고려하자

개발할 때 NullPointerException, 즉 NPE 를 고려하지 않는 경우가 있습니다.

항상 발생가능한 상황을 고려해서 null 체크를 해주어야 합니다.

Java 8 부터는 Optional 을 사용할 수도 있습니다.


가장 흔한 케이스가 Stringequals() 입니다.

// Bad Case : name 으로 null 값이 넘어오면 NPE 발생
public boolean isKim(String name) {
    return name.equals("Kim");
}

// Good Case 1 : name 이 null 값이어도 NPE 발생하지 않음
public boolean isKim(String name) {
    return "Kim".equals(name);
}

// Good Case 2 : org.apache.commons.lang 에서 제공하는 StringUtils 클래스 사용해서 비교
// (내부적으로 null 체크해서 안전함)
public boolean isKim(String name) {
    return StringUtils.equals(name, "Kim");
}

빈 컬렉션에 null 값을 세팅하는 것도 나쁜 습관입니다.

null 값 대신에 Collections.EMPTY_LIST 같은 값을 넣어줍시다.

// Bad Case : null 세팅
List<String> strings = null;

// Good Case : EMPTY_LISTY 세팅
List<String> strings = Collections.EMPTY_LIST

2. String 생성할 때 new 키워드를 사용하지 말자

// Slow
String s = new String("hello");

// Fast
String s = "hello";

new 키워드를 사용하면 항상 새로운 객체를 만들고 힙에 추가합니다.

반면, 사용하지 않으면 String pool 을 먼저 확인하고 없는 경우에만 추가하기 때문에 좀더 효율적입니다.


3. 반복문 내에서 새로운 객체를 생성하지 말자

for (int i = 0; i < 5; i++) {
    Foo f = new Foo();
    f.getMethod();
}

루프 반복횟수 만큼 많은 객체가 생성되기 때문에 지양해야 합니다.


4. Collections 을 반복하는 동안 수정하지 말자

List<String> names = new ArrayList<>();
names.add("Kim");
names.add("Lee");
names.add("Park");

for (String name : names) {
    if ("Kim".equals(name)) {
        names.remove(name);
    }
}

예를 들어 이름 목록에서 Kim 을 삭제하려고 합니다.

위 코드는 names 라는 리스트를 반복하는 동시에 remove() 로 요소를 삭제하고 있습니다.

이러면 ConcurrentModificationException 가 발생할 수 있습니다.


5. Switch-Case 문에서 break 키워드를 빼먹지 말자

int index = 0;

switch (index) {
    case 0:
        System.out.println("Zero");
    case 1:
        System.out.println("One");
        break;
    case 2:
        System.out.println("Two");
    break;
    // ...
    default:
        System.out.println("Default");
}

위 코드를 보면 case 0 에서 break 문을 빼먹었기 때문에 "Zero" 이후에 "One" 까지 출력됩니다.


6. 객체 비교 "==" 와 "eqauls()" 의 차이를 알자

== 연산자는 객체 참조가 같은 지 비교합니다.

equals() 메소드는 객체의 값을 비교합니다.

대부분은 equals() 메소드를 사용해서 비교하지만, 이 둘의 차이점을 잘 모르고 사용하는 경우가 있습니다.


7. 무작정 StringBuffer 를 사용하지 말자

StringBuilderStringBuffer 의 차이를 잘 모르고 무작정 StringBuffer 를 사용하는 경우가 있습니다.

StringBuffer 는 기본적으로 동기화되기 때문에 많은 오버헤드를 생성할 수 있습니다.

따라서 동기화가 우선순위가 아니라면 StringBuilder 를 사용하는 것도 고려해보아야 합니다.


8. Java 파일 작성 시 표준

  • 변수는 public, protected, private 순으로 정의합니다.
  • 생성자는 필드 수가 적은 생성자를 먼저 정의합니다.
  • 메소드는 접근성 (accessibility) 이 아닌 기능별 (functionality) 로 그룹화되어야 합니다. 예를 들어 public 메소드 사이에 private 메소드가 올 수도 있습니다.
  • 코드 설명을 위해 주석을 사용할 수도 있지만, 최대한 주석을 줄이고 가독성 있는 코드를 작성하는 것이 좋습니다.

Reference

해결하고 나니 굉장히 사소한 실수였습니다.

Error creating bean with name 'tokenRedisRepository' defined in TokenRedisRepository defined in @EnableRedisRepositories declared on RedisRepositoriesRegistrar.EnableRedisRepositoriesConfiguration: 
Invocation of init method failed; 
nested exception is org.springframework.data.mapping.MappingException: Entity com.example.login.entity.
Token requires to have an explicit id field. Did you forget to provide one using @Id?
  • RedisRepository 를 적용하는 도중 위와 같은 에러가 발생
  • Redis 에 사용할 객체의 @Id 어노테이션 임포트를 잘못해서 발생 (JPA 에서 사용하는 @Id 를 임포트함)
  • javax.persistence.Id 대신 org.springframework.data.annotation.Id 을 임포트 해서 해결

+ Recent posts