스크린샷 2019-10-30 오후 11 32 53

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

public class HashMaraton {


    public String solution(String[] participant, String[] completion) {

        Arrays.sort(participant);
        Arrays.sort(completion);


        int i = 0;
        for (i = 0; i < completion.length; i++) {
            if(!participant[i].equals(completion[i])){
               return participant[i];
            }
        }

        return participant[i];
    }


    public static void main(String[] args) {

        HashMaraton maraton = new HashMaraton();

        String[] participant = {"leo", "kiki", "kiki", "eden"};
        String[] completion = {"eden", "kiki"};

        System.out.println(maraton.solution(participant, completion));


    }
}

이 문제는 해시를 이용하여 푸는 문제입니다. 위의 코드는 해시를 사용하지 않고 정렬 후 단순히 participant와 completion 배열의 값을 비교하여
completion에 없는 참가자를 리턴하면 됩니다.

하지만 문제의 의도는 해시로 완주하지 못한 참가자의 이름을 리턴하라고 나왔기 때문에 아래코드와 같이 해시 맵을 이용해서 문제를 풀어보았습니다.

public String solution(String[] participant, String[] completion){        

        String answer = "";

        Map<String, Integer> map = new HashMap<>();

        for (String player : participant){

            if(map.containsKey(player)){
                map.put(player, map.get(player) + 1);
                continue;
            }
            map.put(player, 1);

        }

        for (String player : completion){
            if (map.containsKey(player)){
                map.put(player, map.get(player) - 1);
            }
        }


        Set<String> keySet = map.keySet();

        for (String key : keySet){

            if(map.get(key) != 0){
                answer = key;
            }
        }

        return answer;
 }

참조: 아래 코드는 8버전부터 JAVA에서 제공해주는 HashMap 메소드입니다. 중복체크를 할때 유용하게 사용 할 수 있습니다.
Map에서 원하는 key 값이 존재하지 않아도 기본적으로 value를 가지고 싶을때 사용하는 메소드입니다.

 for (String player : participant) map.put(player, map.getOrDefault(player, 0) + 1);

조인 테이블

데이터베이스 테이블의 연관관계를 설계하는 방법은 크게 2가지 입니다.

  • 조인 컬럼 사용(외래 키)
  • 조인 테이블 사용(테이블 사용)

테이블 간에 관계는 주로 조인 컬럼이라 부르는 외래 키 컬럼을 사용해서 관리합니다.

스크린샷 2019-10-30 오전 1 16 48

조인 컬럼 사용

스크린샷 2019-10-30 오전 1 13 19

조인 컬럼 데이터

위의 그림에서 회원과 사물함이 있고 각각 테이블에 데이터를 등록했다가 회원이 원할 때 사물함을 선택할 수 있다고 가정해보겠습니다. 회원이 사물함을 사용하기 전까지 아직 둘 사이에 관계가 없으므로 MEMBER 테이블의 LOCKER_ID 외래 키에 null을 입력해두어야 합니다. 이렇게 외래 키에 null을 허용하는 관계를 선택적 비식별 관계라고 합니다.

선택적 비식별 관계는 외래 키에 null을 허용하므로 회원과 사물함을 조인할 때 외부 조인을 사용해야 합니다. 실수로 내부 조인을 사용하면 사물함과 관계 없는 회원은 조회되지 않습니다. 그리고 회원과 사물함이 아주 가끔 관계를 맺는다면 외래 키 값 대부분이 null로 지정되는 단점이 있습니다.

조인 테이블 사용

아래 그림은 조인 컬럼을 사용하는 대신에 조인 테이블을 사용해서 연관관계를 관리하고 있습니다.

스크린샷 2019-10-30 오전 1 38 42

조인 테이블 사용

스크린샷 2019-10-30 오전 1 44 41

조인 테이블 데이터

이 방법은 조인 테이블이라는 별도의 테이블을 사용해서 연관관계를 관리합니다.
조인 컬럼을 사용하는 방법은 단순히 외래 키 컬럼만 추가해서 연관관계를 맺지만 조인 테이블을 사용하는 방법은 연관관계를 관리하는 조인 테이블을 추가하고 여기서 두 테이블의 외래 키를 가지고 연관관계를 관리합니다. 따라서 MEMBER, LOCKER 테이블에는 연관관계를 관리하기 위한 외래 키 컬럼이 없습니다.

조인 테이블의 가장 큰 단점은 테이블을 하나 추가해야 한다는 점입니다. 따라서 관리해야 하는 테이블이 늘어나고 회원과 사물함 두 테이블을 조인하려면 MEMBER_LOCKER 테이블까지 추가로 조인해야 합니다. 따라서 기본은 조인 컬럼을 사용하고 필요하다고 판단되면 조인 테이블을 사용하면 됩니다.

일대일 조인 테이블

조인 테이블 일대일에서 조인 테이블을 살펴보겠습니다. 일대일 관계를 만들려면 조인 테이블의 외래 키 컬럼 각각에 총 2개의 유니크 제약조건을 걸어야 합니다.

@Setter
@Getter
@Entity
public class Parent {

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

    @OneToOne
    @JoinTable(name = "PARENT_CHILD",
        joinColumns = @JoinColumn(name = "PARENT_ID"),
        inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
    )
    private Child child;
}

@Getter
@Setter
@Entity
public class Child {

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






@Test
@Transactional
@Rollback(false)
public void 식별자_테스트() throws Exception {

        Parent parent = new Parent();
        parent.setName("임종수");

        Child child = new Child();
        child.setName("임준영");

        parent.setChild(child);
        em.persist(parent);
        em.persist(child);

}

다대일 조인 테이블

@Setter
@Getter
@Entity
public class Parent {

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

    @OneToMany
    private List<Child> child = new ArrayList<Child>();
}


@Getter
@Setter
@Entity
public class Child {

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


    @ManyToOne(optional = false)
    @JoinTable(name = "PARENT_CHILD",
            joinColumns = @JoinColumn(name = "CHILD_ID"),
            inverseJoinColumns = @JoinColumn(name = "PARENT_ID")
    )
    private Parent parent;

}

다대다 조인 테이블

다대다 관계를 만들려면 조인 테이블의 두 컬럼을 합해서 하나의 복합 유니크 제약조건을 걸어야 합니다.

@Setter
@Getter
@Entity
public class Parent {

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

    @ManyToMany
    @JoinTable(name = "PARENT_CHILD",
            joinColumns = @JoinColumn(name = "PARENT_ID"),
            inverseJoinColumns = @JoinColumn(name = "CHILD_ID")
    )
    private List<Child> child = new ArrayList<Child>();
}

@Getter
@Setter
@Entity
public class Child {

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

}

엔티티 하나에 여러 테이블 매핑

잘 사용하지는 않지만 @SecondaryTable을 사용하면 한 엔티티에 여러 테이블을 매핑할 수 있습니다.

스크린샷 2019-10-30 오전 2 39 20

@Entity
@Table(name = "BOARD")
@SecondaryTable(name = "BOARD_DETAIL",
pkJoinColumns = @PrimaryKeyJoinColumn(name = "BOARD_DETAIL_ID"))
public class Board{

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

    private String title;

    @Column(table = "BOARD_DETAIL")
    private String content;

}

위의 코드를 살펴보면 Board 엔티티는 @Table을 사용해서 BOARD 테이블과 매핑했습니다. 그리고 @SecondaryTable을 사용해서 BOARD_DETAIL 테이블을 추가로 매핑했습니다.

@SecondaryTable 속성은 다음과 같습니다.

@SecondaryTable.name: 매핑할 다른 테이블의 이름, 예제에서는 테이블명을 BOARD_DETAIL로 지정했습니다.

@SecondaryTable.pkJoinColumn: 매핑할 다른 테이블의 기본 키 컬럼 속성, 예제에서는 기본 키 컬럼명을 BOARD_DETAIL_ID로 지정했습니다.

@Column(table = "BOARD_DETAIL")
private String content;

content 필드는 @Column(table = "BOARD_DETAIL")을 사용해서 BOARD_DETAIL 테이블의 컬럼에 매핑했습니다. title 필드처럼 테이블을 지정하지 않으면 기본 테이블인 BOARD에 매핑됩니다.

@SecondaryTables({

    @SecondaryTable(name = "BOARD_DETAIL"),
    @SecondaryTable(name = "BOARD_FILE")

})

더 많은 테이블을 매핑하려면 @SecondaryTables를 사용하면 됩니다.
하지만 이 방법은 항상 두 테이블을 조회하므로 최적화하기가 어렵습니다. 반면에 일대일 매핑은 원하는 부분만 조회 할 수 있고 필요하면 둘다 함께 조회됩니다.

참고: 김영한의 JAVA ORM 표준 JPA 프로그래밍

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

지연로딩과 즉시로딩  (0) 2019.11.23
프록시와 연관관계 관리  (0) 2019.11.22
복합 키와 식별관계 매핑  (0) 2019.10.29
고급매핑 - 상속관계 매핑  (0) 2019.10.28
스프링 부트 테스트 : @DataJpaTest  (0) 2019.10.09

문제 설명

비밀지도

네오는 평소 프로도가 비상금을 숨겨놓는 장소를 알려줄 비밀지도를 손에 넣었다. 그런데 이 비밀지도는 숫자로 암호화되어 있어 위치를 확인하기 위해서는 암호를 해독해야 한다. 다행히 지도 암호를 해독할 방법을 적어놓은 메모도 함께 발견했다.
지도는 한 변의 길이가 n인 정사각형 배열 형태로, 각 칸은 공백(" ) 또는벽(#") 두 종류로 이루어져 있다.
전체 지도는 두 장의 지도를 겹쳐서 얻을 수 있다. 각각 지도 1과 지도 2라고 하자. 지도 1 또는 지도 2 중 어느 하나라도 벽인 부분은 전체 지도에서도 벽이다. 지도 1과 지도 2에서 모두 공백인 부분은 전체 지도에서도 공백이다.
지도 1과 지도 2는 각각 정수 배열로 암호화되어 있다.
암호화된 배열은 지도의 각 가로줄에서 벽 부분을 1, 공백 부분을 0으로 부호화했을 때 얻어지는 이진수에 해당하는 값의 배열이다.

secret8

네오가 프로도의 비상금을 손에 넣을 수 있도록, 비밀지도의 암호를 해독하는 작업을 도와줄 프로그램을 작성하라.

입력 형식
입력으로 지도의 한 변 크기 n 과 2개의 정수 배열 arr1, arr2가 들어온다.
1 ≦ n ≦ 16
arr1, arr2는 길이 n인 정수 배열로 주어진다.
정수 배열의 각 원소 x를 이진수로 변환했을 때의 길이는 n 이하이다. 즉, 0 ≦ x ≦ 2n - 1을 만족한다.
출력 형식
원래의 비밀지도를 해독하여 '#', 공백으로 구성된 문자열 배열로 출력하라.

스크린샷 2019-10-29 오후 11 25 54

2018년도 카카오 블라인드 공채 문제 중에서 가장 적중률이 높은 비밀지도 문제에 대해서 리뷰하겠습니다. 사실 그림과 설명만 보면 복잡해보이지만 해당 문제의 의도는 이진연산의 개념을 묻는 문제로 그 중에서 or 연산을 사용할줄 알면 쉽게 풀 수 있는 문제였습니다.

처음에 or연산이라는것을 코드를 작성할때 쓸일이 거의 없어서 가물가물했지만... 찾아보니 값1 | 값2 으로 손 쉽게 이진 or연산이 가능합니다. 하지만 연산만으로는 정답을 맞추기가 힘듭니다. n의 값의 해당하는 자리수를 맞춰줘야기 때문에 고려해야할 부분이 조금 있습니다. 저같은 경우에는 계속 테스트 케이스가 1~2개씩 틀려서 대체 어디서 틀린걸까 많은 고민 끝에 좋은 풀이 방법이 있어서 소개해드릴려고 합니다.

참고로 값1 & 값2는 and 연산을 의미합니다.

public class FindHiddenMap {

    public static void main(String[] args) {

        int n = 5;
        int[] arr1 = {9, 20, 28, 18, 11};
        int[] arr2 = {30, 1, 21, 17, 28};

        String[] result = new String[n];

        for (int i = 0; i < n; i++) {
            int arr = arr1[i] | arr2[i];
            String resultString = "";
            int target = 1;
            //n 자리수를 맞추기 위해서 포문을 추가
            for (int j = 0; j < n; j++) {
                resultString = ((arr & target) > 0 ? "#" : " ") + resultString;
                target = target << 1;  //좌측 Shift 연산 : target 변수 값 1을 1비트씩 좌측으로 이동시킵니다.

            }

            result[i] = resultString;
            System.out.println(result[i]);
        }
    }
}

위의 코드에서는 Left Shift 연산을 이용하여 n의 값에 해당하는 자리수를 맞추는 방식입니다. 사실 Shift 연산도 많이 쓸일이 없었지만 이번 카카오 문제를 풀면서 공부하게 되었습니다.

위의 코드 target 값을 1로 대입을 하고 n의 값만큼 내부적으로 for문을 수행하여 이진 or 연산의 결과 값과 다시 and 연산을 수행한 후에 target을 1비트씩 Left Shift 연산을 수행하고 있습니다.

int target = 1;

target = target << 1;  // 1비트씩 좌측으로 이동합니다.

현재 target 값이 1이기 때문에 실제로 루프를 돌때마다 아래와 같이 1비트씩 이동하는 것을 알 수 있습니다.

n = 3일때

j = 0일때
0000 0000 0000 0000 0000 0000 0000 0001

j = 1일때
0000 0000 0000 0000 0000 0000 0000 0010

j = 2일때
0000 0000 0000 0000 0000 0000 0000 0100

두번째 방법은 Shift 연산을 사용하지 않아도 쉽게 풀 수 있는 방법입니다.

public class FindHiddenMap {

    public static void main(String[] args) {

        int n = 5;
        int[] arr1 = {9, 20, 28, 18, 11};
        int[] arr2 = {30, 1, 21, 17, 28};

        String[] result = new String[n];

        for (int i = 0; i < n; i++) {
            result[i] = Integer.toBinaryString(arr1[i] | arr2[i]);
        }

        for (int i = 0; i < n; i++) {
            result[i] = String.format("%" + n + "s", result[i]);
            result[i] = result[i].replaceAll("1", "#");
            result[i] = result[i].replaceAll("0", " ");
        }

        for (String c : result){
            System.out.println(c);
        }
    }
}

위의 두번째 풀이 방식은 제가 푼 풀이는 아니고 프로그래머스에서 다른사람 풀이에서 가장 깔끔하게 짜서 참고하였습니다.

result[i] = Integer.toBinaryString(arr1[i] | arr2[i]);

Integer 클래스의 toBinaryString 메소드는 or 연산의 결과 값을 바이너리 표현 방식으로 바꿔주는 Util 메소드 입니다.

이후 배열 길이만큼 루프를 타면서 String.format을 이용하여 해당 값의 자리수를 n 값만큼 맞춘 후에 replace 값으로 "1" -> "#", "0" -> " "으로 변경하면 됩니다.

'알고리즘' 카테고리의 다른 글

해시- 전화번호 목록  (0) 2019.11.05
해시 - 완주하지 못한 마라톤 선수  (0) 2019.10.31
Hackerrank- EqualizeTheArray  (0) 2019.10.28
완전탐색 - 카펫  (0) 2019.10.25
[카카오 2020 공채] 문자열 압축  (0) 2019.10.09

복합 키과 식별 관계 매핑

복합 키를 매핑하는 방법과 식벽관계, 비식별 관계를 매핑하는 방법을 알아보겠습니다.

식별관계 vs 비식별 관계

데이터베이스 테이블 사이에 관계는 외래 키가 기본 키에 포함되는지 여부에 따라 식별 관계와 비식별 관계로 구분됩니다. 두 관계의 특징을 이해하고 각각을 어떻게 매핑하는지 알아봅시다.

식별관계

식별 관계는 부모 테이블의 기본 키를 내려받아서 자식 테이블의 기본 키 + 외래 키로 사용하는 관계 입니다.

스크린샷 2019-10-28 오후 10 53 41

위의 그림을 보면 PARENT 테이블의 기본 키 PARENT_ID를 받아서 CHILD 테이블의 기본키 (PK) + 외래 키(FK)로 사용합니다.

비식별 관계

비식별 관계는 부모 테이블의 기본 키를 받아서 자식 테이블의 외래 키로만 사용하는 관계입니다.

스크린샷 2019-10-28 오후 10 55 20

PARENT 테이블의 기본 키 PARENT_ID를 받아서 CHILD 테이블의 외래 키(FK)로만 사용한다.
비식별 관계는 외래 키에 NULL을 허용하는지에 따라 필수적 비식별 관게와 선택적 비식별 관계로 나눕니다.

  • 필수적 비식별 관계: 외래키에 NULL을 허용하지 않습니다. 연관관계를 필수적으로 맺어야 합니다.

  • 선택적 식별 관계: 외래키에 NULL을 허용합니다. 연관관계를 맺을지 말지 선택할 수 있습니다.

최근에는 테이블을 설계할 때 식별 관계나 비식별 관계 중 하나를 선택해야 합니다.
최근에는 비식별 관계를 주로 사용하고 꼭 필요한 곳에만 식별 관계를 사용하는 추세입니다.
JPA는 식별 관계와 비식별 관계를 모두 지원합니다.

복합 키: 비식별 관계 매핑

둘 이상의 컬럼으로 구성된 복합 기본 키는 아래 코드처럼 매핑하면 될 것 같지만 막상 해보면 매핑 오류가 발생합니다. JPA에서는 식별자를 둘 이상 사용하려면 별도의 식별자 클래스가 필요하기 때문입니다.

@Entity
public class Hello{

    @Id
    private String id1;
    @Id
    private String id2; // 실행 시점에 매핑 예외발생

}

JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용합니다. 그리고 식별자를 구분하기 위해 equals와 hashCode를 사용해서 동등성 비교를 합니다. 그런데 식별자 필드가 하나일 때는 보통 자바의 기본타입을 사용하므로 문제가 없지만, 식별자 필드가 2개 이상이면 별도의 식별자 클래스를 만들고 그곳에 equals와 hashCode를 구현해야 합니다.

JPA는 복합 키를 지원하기 위해 @IdClass와 @EmbeddedId 2가지 방법을 제공하는데 @IdClass는 관계형 데이터베이스에 가까운 방법이고 @EmbeddedId는 좀 더 객체지향에 가까운 방법입니다. 먼저 @IdClass부터 알아봅시다.

@IdClass

복합 키 테이블은 비식별 관계이고 PARENT는 복합 기본 키를 사용합니다. 참고로 여기서 이야기하는 부모 자식은 객체의 상속과는 무관합니다. 단지 테이블의 키를 내려받는 것을 강조하려고 이름을 이렇게 지었습니다.

스크린샷 2019-10-28 오후 11 12 22

PARENT 테이블을 보면 기본 키를 PARENT_ID1, PARENT_ID2로 묶은 복합 키로 구성했습니다. 따라서 복합 키를 매핑하기 위해서 식별자 클래스를 만들어야 합니다.

@Entity
@IdClass(ParentId.class)
public class Parent{

    @Id
    @Column(name = "PARENT_ID1")
    private String id1;  // ParentId.id1과 연결

    @Id
    @Column(name = "PARENT_ID2")
    private String id2;  // ParentId.id2와 연결

    private String name;

}
public class ParentId implements Serializable{

    private String id1; // Parent.id1 매핑
    private String id2; // Parent.id2 매핑

    public ParentId(){}



    public ParentId(String id1, String id2)}
        this.id1 = id1;
        this.id2 = id2;
    }

    @Override
    public boolean equals(Object obj) { ... }

    @Override
    public int hashCode() {...}

}

@IdClass를 사용할 때 식별자 클래스는 다음 조건을 만족해야 합니다.

  • 식별자 클래스의 속성명과 엔티티에서 사용하는 식별자의 속성명이 같아야 합니다.
    실제로 코드를 보면 Parent.id1과 Parent.id2, 그리고 ParentId.id1, ParentId.id2가 같습니다.

  • Serializable 인터페이스를 구현해야 합니다.

  • equals, hashCode를 구현해야 합니다.

  • 기본 생성자가 있어야 합니다.

  • 식별자 클래스는 public이어야 합니다.

실제 어떻게 사용하는지 코드로 살펴보겠습니다. 먼저 복합 키를 사용하는 엔티티를 저장합니다.


    @Test
    @Transactional
    @Rollback(false)
    public void 식별자_테스트() throws Exception {

        //given
        Parent parent = new Parent();
        parent.setId1("myId1");
        parent.setId2("myId2");
        parent.setName("parentName");

        //when
        em.persist(parent);
        ParentId parentId = new ParentId("myId1", "myId2");

        //복합 키로  조회
        Parent findParent = em.find(Parent.class, parentId);
        //then
        System.out.println(findParent.getId1() + " " + findParent.getId2() + " " + findParent.getName());
     }

저장 코드를 보면 식별자 클래스인 ParentId가 보이지 않는데, em.persist()를 호출하면 영속성 컨텍스트에 엔티티를 등록하기 직전에 내부에서 Parent.id1, Parent.id2 값을 사용해서 식별자 클래스인 ParentId를 생성하고 영속성 컨텍스트의 키로 사용합니다.

아래 코드는 자식 클래스를 추가하였습니다.

@Getter
@Setter
@Entity
public class Child {

    @Id
    private String id;

    @ManyToOne
    @JoinColumns({@JoinColumn(name = "PARENT_ID1", referencedColumnName = "PARENT_ID1"),
    @JoinColumn(name = "PARENT_ID2", referencedColumnName = "PARENT_ID2")})
    private Parent parent;

}

부모 테이블의 기본 키 컬럼이 복합 키이므로 자식 테이블의 외래 키도 복합 키입니다. 따라서 외래 키 매핑시 여러 컬럼을 매핑해야 하므로 @JoinColumns 어노테이션을 사용하고 각각의 외래 키 컬럼을 @JoinColumn으로 매핑합니다.

참고로 @JoinColumn의 name 속성과 referencedColumnName 속성의 값이 같으면 referencedColumnName은 생략해도 됩니다.

@EmbeddedId

@IdClass가 데이터베이스에 맞춘 방법이라면 @EnbeddedId는 좀 더 객체지향적인 방법입니다.

@EqualsAndHashCode
@Embeddable
public class ParentId implements Serializable {

    @Column(name = "PARENT_ID1")
    private String id1; // Parent.id1 매핑
    @Column(name = "PARENT_ID2")
    private String id2; // Parent.id2 매핑

    public ParentId(){}


    public ParentId(String id1, String id2){
        this.id1 = id1;
        this.id2 = id2;
    }
}


@Setter
@Getter
@Entity
@IdClass(ParentId.class)
public class Parent {

    @EmbeddedId
    ParentId parentId;

    private String name;

}

@IdClass와는 다르게 EmbeddedId를 적용한 식별자 클래스는 식별자 클래스에 기본키를 직접 매핑합니다.

@EmbeddedId를 적용한 식별자 클래스는 다음 조건을 만족해야 합니다.

  • @Embeddedable 어노테이션을 붙어주어야 합니다.
  • Serializable 인터페이스를 구현해야 합니다.
  • equals, hashCode를 구현해야 합니다.
  • 기본 생성자가 있어야 합니다.
  • 식별자 클래스는 public이어야 합니다.

@EmbeddedId를 사용하는 코드를 보겠습니다.

    @Test
    @Transactional
    @Rollback(false)
    public void 식별자_테스트() throws Exception {

        Parent parent = new Parent();
        ParentId parentId = new ParentId("myId1", "myId2");
        parent.setParentId(parentId);
        parent.setName("parentName");

        em.persist(parent);

        ParentId parentId1 = new ParentId("myId1", "myId2");
        Parent findParent = em.find(Parent.class, parentId1);

        System.out.println(findParent.getName());
     }

위의 코드를 보면 식별자 클래스 parentId를 직접 생성해서 사용합니다.

@IdClass와 @EmbeddedId는 각각 장단점이 있으므로 본인의 취향에 맞는 것을 일관성 있게 사용하면 됩니다.

복합 키에는 @GenerateValue를 사용 할 수 없습니다. 복합 키를 구성하는 여러 컬럼 중 하나에도 사용할 수 없습니다.

김영한의 JAVA ORM 표준 JPA 프로그래밍

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

프록시와 연관관계 관리  (0) 2019.11.22
고급매핑 - 조인테이블  (0) 2019.10.30
고급매핑 - 상속관계 매핑  (0) 2019.10.28
스프링 부트 테스트 : @DataJpaTest  (0) 2019.10.09
JPA 다양한 연관관계 매핑  (0) 2019.10.04

오랜만에 해커랭크 Easy 문제를 풀어보면서...금방 풀줄 알았지만... 2시간정도 걸렸습니다.
사실 난이도가 Easy임에도 불구하고... 너무 많은 생각을 많이했습니다. 이걸 풀때즘에 저의 문제해결 능력이 너무 나쁘다는걸 깨달았습니다. 저 자신에 대한 프로그래머로써의 자질도 의심하게 되더군요....

사실 이 문제를 해결하기 위해서 어떤 자료구조를 써야할지.. 많은 고민을 했습니다.
제가 생각한 전체적인 흐름은 컬렉션 타입인 List 객체를 사용하여 중복없는 원소 값들을 저장하고 그 저장된 원소값들과 기존의 정수형 타입의 배열과 비교를 통해서 각각의 중복 값의 개수를 저장하는 배열을 얻습니다. 이후 배열 속에서 중복 개수가 가장 많은 배열의 index를 이용하여 처음 주어진 배열에서 최소한의 삭제 횟수를 구하도록 비즈니스 로직을 구현하는 것입니다.

이걸 푼 다음에 다른 분들의 풀이방법을 보고나서... 정말 쉽게 풀수있는 방법이 많았습니다.

스크린샷 2019-10-27 오후 10 01 21

package hackerrank;

import java.util.*;

public class EqualizeTheAraay {

    private static final Scanner sc = new Scanner(System.in);

    public static int equalizeArray(int[] arr) {

        int duplicate = findDuplicate(arr);
        int deleteCount = 0;

        // 중복 개수가 가장 많은 값과 비교하여 일치하지 값을 삭제합니다.
        for (int i = 0; i < arr.length; i++) {
            if (arr[i] != duplicate)
                deleteCount++;
        }
        return deleteCount;
    }

    // 중복 개수가 가장 많은 값을 찾기
    static int findDuplicate(int[] arr) {

        int duplicateValue = 0;
        List<Integer> list = new ArrayList<>();

        //스트림 객체의 필터 메소드를 이용하여 중복 없는 원소들을 가진 정수형 타입의 배열을 리턴합니다.
        int[] noRedundant = Arrays.stream(arr).distinct().toArray();

        int[] count = new int[noRedundant.length];

        for (int i = 0; i < noRedundant.length; i++){
            int redundant = 0;
            for (int j = 0; j < arr.length; j++) {
                if(noRedundant[i] == arr[j]){
                    redundant++;
                }
            }
            count[i] = redundant;
        }

        duplicateValue = nooRedundant[getMax(count)];

        return duplicateValue;
    }


    // 중복 개수가 들어있는 배열에서 중복개수가 가장 많은 원소의 인덱스를 리턴.
    static int getMax(int[] count){

        int max = count[0];
        int index = 0;

        for (int i = 0; i < count.length; i++) {
            if(max <= count[i]){
                max = count[i];
                index = i;
            }
        }
        return index;
    }


    public static void main(String[] args) {

        int n = sc.nextInt();
        sc.skip("(\r\n|[\n\r\u2028\u2029\u0085])?");

        int[] arr = new int[n];

        String[] arrItems = sc.nextLine().split(" ");

        for (int i = 0; i < n; i++) {
            int arrItem = Integer.parseInt(arrItems[i]);
            arr[i] = arrItem;
        }

        int result = equalizeArray(arr);
        System.out.println(result);

        sc.close();
    }
}

같은 원소만 가진 배열

위의 문제를 해석해보면 정수타입의 배열이 주어질때 칼이라는 아이는 이 배열안에 있는 모든 원소들이 같아질때까지 배열의 크기를 줄기를 원한다는 게 이 문제의 핵심입니다.

그렇기 위해서는 배열안의 엘리먼트 중에서 중복 개수가 가장많은 값을 제외하고 나머지 원소들의 삭제 횟수를 구하는게 핵심입니다.
한마디로 중복 개수가 제외한 값을 제외하고 나머지 원소들의 삭제를 몇건이나 했는지 출력하라는 문제입니다.

고급매핑

상속 관계 매핑

  • 상속관계 매핑은 객체의 상속관계를 데이터베이스에 어떻게 매핑하는지 다룹니다.

관계형 데이터베이스에는 객체지향 언어에서 다루는 상속이라는 개념이 없습니다.
대신에 슈퍼타입 서브타입 관계라는 모델링 기법이 객체의 상속 개념과 가장 유사합니다.
ORM에서 이야기하는 상속 관계 매핑은 객체의 상속 구조와 데이터베이스의 슈퍼타입 서브타입 관계를 매핑하는 것 입니다.

객체 상속 모델

스크린샷 2019-10-27 오후 11 10 37

슈퍼타입 서브타입 논리 모델을 실제 물리 모델의 테이블로 구현할 때는 3가지 방법을 선택할 수 있습니다.

  • 각각의 테이블로 변환: 위 그림과 같이 각각을 모두 테이블로 만들고 조회할 때 조인을 사용합니다. JPA에서는 조인 전략이라고 할수 있습니다.

  • 통합 테이블로 변환: 테이블을 하나만 사용해서 통합합니다. JPA에서는 싱글 테이블 전략이라고 합니다.

  • 서브타입 테이블로 변환: 서브 타입마다 하나의 테이블을 만듭니다. JPA에서는 구현 클래스마다 테이블 전략이라 합니다.

위의 전략들을 하나하나씩 살펴보겠습니다.

조인전략은 그림과 같이 엔티티 각각을 모두 테이블로 만들고 자식 테이블이 부모 테이블의 기본 키를 받아서 기본 키 + 외래 키로 사용하는 전략입니다. 따라서 조회할 때 조인을 자주 사용합니다. 이 전략을 사용할 때 주의할 점이 객체는 타입으로 구분할 수 있지만 테이블은 타입 개념이 없습니다. 따라서 타입을 구분하는 컬럼을 추가해야 합니다. 여기서는 DTYPE 컬럼을 구분 컬럼으로 사용했습니다.

조인전략

스크린샷 2019-10-27 오후 11 28 01

조인전략 예제 코드

@Entity
@Inheritance(strategy = InheritanceType.JOINED)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {

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

    private String name;
    private int price;

}

@Entity
@DiscriminatorValue("B")
public class Book extends Item {

    private String author;
    private String isbn;
}

@Entity
@DiscriminatorValue("A")
public class Album extends Item {

    private String artist;

}

@Entity
@DiscriminatorValue("M")
public class Movie extends Item {

    private String director;

    private String actor;
}

@Inheritance(strategy = InheritanceType.JOINED): 상속 매핑은 부모 클래스에 @Inheritance를 사용합니다. 그리고 매핑 전략을 지정해야 하는데 여기서는 조인 전략을 사용하므로 InheritanceType.JOINED를 사용했습니다.

@DiscriminatorColumn(name = "DTYPE"): 부모 클래스에 구분 컬럼을 지정합니다. 이컬럼으로 저장된 자식 테이블을 구분할 수 있습니다. 기본 값이 DTYPE이므로 @DiscriminatorColumn으로 줄여 사용해도 된다.

@DiscriminatorValue("M"): 엔티티를 저장할 때 구분 컬럼에 입력할 값을 지정한다.
만약 영화 엔티티를 지정하면 구분 컬럼인 DTYPE에 값 M이 저장됩니다.

기본값으로 자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용하는데, 만약 자식 테이블의 기본 키 컬럼명을 변경하고 싶으면 아래 코드처럼 사용하시면 됩니다.

@Entity
@DiscriminatorValue("B")
@PrimaryKeyJoinColumn(name = "BOOK_ID") //ID 재정의
public class Book extends Item{

    private String author; //저자
    private String isbn;   // ISBN

}
  • 장점

    - 테이블이 정규화 됩니다.

    - 외래키 참조 무결성 제약조건을 활용할 수 있습니다.

    - 저장공간을 효율적으로 사용합니다.

  • 단점

    - 조회할 때 조인이 많이 사용되므로 성능이 저하될 수 있습니다.

    - 조회쿼리가 복잡합니다.

    - 데이터를 등록할 INSERT SQL을 두 번 실행합니다.

JPA 표준 명세는 구분 컬럼을 사용하도록 하지만 하이버네이트를 포함한 몇몇 구현체는 구분컬럼 없이도 동작합니다.

싱글 테이블 전략

싱글 테이블 전략은 아래 그림과 같이 이름 그대로 테이블 하나만 사용합니다. 그리고 구분 컬럼으로 어떤 자식 데이터가 저장되었는지 구분합니다. 조회할 때 조인을 사용하지 않으므로 일반적으로 가장 빠릅니다.

싱글 테이블 전략

스크린샷 2019-10-27 오후 11 31 27

이 전략을 사용할 때 주의점은 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 한다는 점입니다. 예를 들어 Book 엔티티를 저장하면 ITEM 테이블의 AUTHOR, ISBN 컬럼만 사용하고 다른 엔티티와 매핑된 ARTIST, DIRECTOR, ACTOR 컬럼은 사용하지 않으므로 null이 입력되기 때문입니다.

싱글테이블 예제 코드

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE)
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item {

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

    private String name;
    private int price;

}

@Entity
@DiscriminatorValue("B")
public class Book extends Item {

    private String author;
    private String isbn;
}

@Entity
@DiscriminatorValue("A")
public class Album extends Item {

    private String artist;

}

@Entity
@DiscriminatorValue("M")
public class Movie extends Item {

    private String director;

    private String actor;
}

InheritanceType.SINGLE_TABLE로 지정하면 단일 테이블 전략을 사용합니다.
테이블 하나에 모든 것을 통합하므로 구분 컬럼을 필수로 사용해야 합니다. 단일 테이블 전략의 장단점은 하나의 테이블을 사용하는 특징과 관련 있습니다.

  • 장점

    - 조인이 필요 없으므로 일반적으로 조회 성능이 빠릅니다.

    - 조회 쿼리가 단순합니다.

  • 단점

    - 자식 엔티티가 매핑한 컬럼은 모두 null을 허용해야 합니다.

    - 단일 테이블에 모든 것을 저장하므로 테이블이 커질 수 있습니다. 그러므로 상황에 따라 조회 성능이 오히려 느려질 수 있습니다.

싱글 테이블 전략에서는 구분 컬럼 @DiscriminatorColumn을 꼭 설정해야 합니다.
@DiscriminatorValue를 지정하지 않으면 기본으로 엔티티 이름을 사용홥니다.

구현 클래스마다 테이블 전략

구현 클래스마다 테이블 전략은 아래 그림과 같이 자식 엔티티마다 테이블을 만듭니다.
그리고 자식 테이블에 각각에 필요한 컬럼이 모두 있습니다.

구현 클래스마다 테이블 전략

스크린샷 2019-10-27 오후 11 34 31

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS)
public abstract class Item {

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

    private String name;
    private int price;

}

@Entity
public class Book extends Item {

    private String author;
    private String isbn;
}

@Entity
public class Album extends Item {

    private String artist;

}

@Entity
public class Movie extends Item {

    private String director;

    private String actor;
}

InheritanceType.TABLE_PER_CLASS를 선택하면 구현 클래스마다 테이블 전략을 사용합니다. 이 전략은 자식 엔티티마다 테이블을 만듭니다. 일반적으로 추천하지 않는 전략입니다.

  • 장점

    - 서브 타입을 구분해서 처리할 때 효과적입니다.

    - not null 제약조건을 사용할 수 없습니다.

  • 단점

    - 여러 자식 테이블을 함꼐 조회할 때 성능이 느립니다.

    - 자식 테이블을 통합해서 쿼리하기 어렵습니다.

이 전략은 데이터베이스 설계자와 ORM 전문가 둘 다 추천하지 않는 전략입니다.
조인이나 싱글 테이블 전략을 고려합시다.

@MappedSuperclass

이전 포스팅에서 상속 관계 매핑은 부모 클래스와 자식 클래스를 모두 데이터베이스 테이블과 매핑하였습니다. 부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속 받는 자식 클래스에게 매핑 정보만 제공하고 싶으면 @MappedSuperclass를 사용하면 됩니다.

@MappedSuperclass는 비유를 하자면 추상 클래스와 비슷한데 @Entity는 실제 테이블과 매핑되지만 @MappedSuperclass는 실제 테이블과는 매핑되지 않습니다.
이것은 단순히 매핑 정보를 상속할 목적으로만 사용합니다.

@MappedSuperclass
public abstract class BaseEntity {

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


}

@Entity
@Setter
@Getter
public class Member extends BaseEntity{

    // ID 상속
    // NAME 상속
    private String email;    
    ...
}

public class Seller extends BaseEntity{

    // ID 상속
    // NAME 상속
    private String shopName;
}

BaseEntity는 객체들이 주로 사용하는 공통 매핑 정보를 정의했습니다. 그리고 자식 엔티티들은 상속을 통해 BaseEntity의 매핑 정보를 물려받았습니다. 여기서 BaseEntity는 테이블과 매핑할 필요가 없고 자식 엔티티에게 공통으로 사용되는 매핑 정보만 제공하면 됩니다. 따라서 @MappeedSuperclass를 사용했습니다.

@Entity
@AttributeOverride(name = "id", column = @Column(name = "MEMBER_ID"))
public class Member extends BaseEntity{

}

위의 코드는 부모에게 물려받은 id 속성의 컬럼명을 MEMBER_ID로 재정의 하였습니다.
매핑 정보를 재정의하려면 AttributeOverrides 나 AttributeOverride를 사용하고, 연관관계를 재정의하려면 @AssociationOverrides나 @AssociationOverride를 사용합니다.

@MappedSuperclass의 특징

  • 테이블과 매핑되지 않고 자식 클래스에 엔티티의 매핑 정보를 상속하기 위해 사용합니다.

  • @MappedSuperclass로 지정한 클래스는 엔티티가 아니므로 em.find()나 JPQL에서 사용 할 수 없습니다.

  • 이 클래스를 직접 생성해서 사용할 일은 거의 없으므로 추상 클래스로 만드는 것을 권장합니다.

결론은 @MapperdSuperclass는 테이블과는 관계가 없고 단순히 엔티티가 공통으로 사용하는 매핑 정보를 모아주는 역할을 할 뿐입니다. ORM에서 이야기 하는 진정한 상속 매핑은 이전에 학습한 객체 상속을 데이터베이스 슈퍼타입 서브타입 관계와 매핑하는 것입니다.

@MapperdSuperclass를 사용하면 등록일자, 수정일자, 등록자, 수정자 같은 여러 엔티티에서 공통으로 사용하는 속성을 효과적으로 관리할 수 있습니다.

참조: 김영한님의 JAVA ORM 표준 JPA 프로그래밍

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

고급매핑 - 조인테이블  (0) 2019.10.30
복합 키와 식별관계 매핑  (0) 2019.10.29
스프링 부트 테스트 : @DataJpaTest  (0) 2019.10.09
JPA 다양한 연관관계 매핑  (0) 2019.10.04
JPA 연관관계 매핑기초  (0) 2019.10.04

+ Recent posts