beadss / jpa-study

jpa슽터디입니다
1 stars 2 forks source link

7장 정리 완료(야호!) #18

Open joont92 opened 5 years ago

joont92 commented 5 years ago

상속 관계 매핑(DTYPE)

RDB는 객체지향 언어처럼 상속이라는 개념이 없다.
대신 슈퍼타입 서브타입 관계라는 모델링 기법이 있는데, 이게 상속 개념과 가장 유사하다.
아래는 슈퍼타입 서브타입 관계의 전략들이다.

각각의 테이블로 변환(조인 전략)

엔티티 각각(자식, 부모 전부)을 테이블로 만들고,
자식 테이블이 부모의 기본키를 받아서 기본키 + 외래키로 사용하는 방법이다.

joined tale

가장 객체지향에 가깝게 생기긴 했다.

DTYPE을 지정한 것은 탐색의 범위를 줄위기 위함이다(아마도?)
(부모 테이블 전체를 탐색하는 것은 낭비이기 때문이다)

DTYPE은 데이터베이스 관점에서 무조건적으로 필요한 컬럼이다.
album -> item 으로 접근할때는 상관없지만, item 에서 접근할때는 DTYPE이 없으면 어느 테이블에 조인할 지 전혀 모르기 때문이다.
DTYPE을 통해서 if문을 넣을수도 있고, 뷰모델에서 뿌리는 걸 나눠줄수도 있다.
JPA 표준 명세에서는 이를 지정해주도록 하지만 하이버네이트는 굳이 지정해주지 않아도 잘 동작한다.

/**
 * 엔티티 정의
**/
@Entity
@Inheritance(strategy = InheritanceType.JOINED) // 1
@DiscriminatorColumn(name = "DTYPE") // 2
public abstract class Item{
    @Id
    @GeneratedValue
    private Integer id;

    private String name;

    private int price;
}

@Entity
@DiscriminatorValue("A") // 3
public class Album extends Item{
    private String author;
}

@Entity
@DiscriminatorValue("M") // 3
@PrimaryKeyJoinColumn(nane = "MOVIE_ID")
public class Movie extends Item{
    private String director;

    private String actor;
}

/**
 * 등록, 조회
**/
public void save(){
    Album album = new Album();
    // set...

    em.persist(album);
}

public void select(){
    Album album = em.find(Album.class, 1);
}

사용하는 부분은 별로 다를 것 없다.

  1. 상속 매핑을 사용할 것이고, 조인 전략을 사용할 것이라는 의미이다.
  2. 자식 테이블을 구분할 컬럼이다. 실제 테이블의 컬럼으로 생성된다. 기본값이 DTYPE 이다.
  3. 구분 컬럼에 저장될 값이다.
  4. 기본적으로 자식 테이블은 부모 테이블의 ID 컬럼명을 그대로 사용하는데, 이를 바꿔주고 싶을 때 사용한다.

실행결과

-- 삽입
INSERT INTO ITEM(id, name, price, DTYPE) VALUES(1, '앨범', 10000, 'A');
INSERT INTO ALBUM(id, author) VALUES(1, '소녀시대');

INSERT INTO ITEM(id, name, price, DTYPE) VALUES(1, '인셉션', 10000, 'M');
INSERT INTO MOVIE(id, director, actor) VALUES(1, '크리스토퍼 놀란', '디카프리오');

-- 조회
select
    *
from
    album album0_ 
inner join
    item album0_1_ 
        on album0_.id=album0_1_.id 
where
    album0_.id = 1
    -- and i.DTYPE = 'A'

hibernate에서는 조건절에 따로 DTYPE이 추가되지 않았다

통합 테이블로 변환(단일 테이블 전략)

전략 이름 그대로 하나의 테이블에 다 때려넣는 전략이다.
그리고 구분 컬럼(DTYPE)으로 어떤 자식 데이터가 저장되었는지 구분한다.

single table

조인 전략과 달리 자식 -> 부모 쪽으로 접근할 수 있는 방법이 없으므로 구분 컬럼은 필수로 사용해야한다.

@Entity
@Inheritance(strategy = InheritanceType.SINGLE_TABLE) // 1
@DiscriminatorColumn(name = "DTYPE")
public abstract class Item{
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private int price;
}

@Entity
@DiscriminatorValue("A")
public class Album extends Item{
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item{
}
  1. 단일 테이블 전략을 사용할 것이라는 의미이다.

실행 결과

-- 삽입
INSERT INTO ITEM(id, name, price, artist, DTYPE) VALUES(1, '앨범', 10000, '소녀시대', 'A');

-- 조회
select
    *
from
    item album0_ 
where
    album0_.id=1  
    and album0_.DTYPE='A'

서브타입 테이블로 변환(구현 클래스마다 테이블 전략)

자식 엔티티마다 테이블을 만드는 전략이다.
자식 테이블에 필요한 컬럼이 모두 있다. 구분 컬럼이 필요없다.

concrete table

데이터베이스 설계자와 객체지향 설계자 둘 다 추천하지 않는 방법이다.
조인이나 단일 테이블 전략을 고려하는 것이 좋다.

@Entity
@Inheritance(strategy = InheritanceType.TABLE_PER_CLASS) // 1
public abstract class Item{
    @Id
    @GeneratedValue
    private Long id;

    private String name;

    private int price;
}

@Entity
@DiscriminatorValue("A")
public class Album extends Item{
}
@Entity
@DiscriminatorValue("M")
public class Movie extends Item{
}
  1. 구현 클래스마다 테이블 전략을 사용하겠다는 의미이다. DTYPE이 필요없다.

매핑 정보만 상속(@MappedSupperClass)

부모 클래스는 테이블과 매핑하지 않고 부모 클래스를 상속 받는 자식 클래스에게 매핑 정보만 제공하고 싶을 경우 사용한다.
단순히 매핑 정보만 상속할 목적으로 사용한다.

@MappedSuperClass // 1
public abstract class BaseEntity{
    @Id
    @GeneratedValue
    private Long id;

    @Temporal(TemporalType.TIMESTAMP)
    private Date createdDate;

    @Temporal(TemporalType.TIMESTAMP)
    private Date lastModifiedDate;
}

@Entity
class Member extends BaseEntity{
    // ...
}

@Entity
@AttributeOverride(name = "id", column = @Column(name = "TEAM_ID")) // 2
class Team extends BaseEntity{
    // ...
}

BaseEntity는 테이블과 매핑되지 않고 단순히 자식 엔티티에게 매핑 정보만 제공하는 용도로 사용된다.
(ORM에서 말하는 진정한 상속 매핑은 처음 설명했던 상속 관계 매핑을 말한다)

  1. 매핑 정보만 제공할 클래스라는 의미이다.
  2. 매핑정보를 재정의 하고 싶을 경우 사용한다. 여러개를 지정하고 싶을 경우 @AttributeOverrides를 사용한다.
  3. 위에 명시하진 않았지만 관계를 재정의 하고 싶을 경우 @AssociationOverride를 사용한다.

근데 이것보다 그냥 @Embeddable을 쓰는게 나을 것 같다.
@MappedSuperClass는 추상 클래스만이 가능한데, 다중 상속이 안되는 자바에서 단순히 매핑 정보를 추가 정의하기 위해 상속을 써버리는 것은 좋지 않은 것 같다..
일단 위에는 createdDate, lastModifiedDate로 작성했지만, 이렇게 사용하는게 좋은 예시는 아닌 것 같다.

복합키 매핑

JPA에서 식별자를 둘 이상 사용하려면 별도의 식별자 클래스를 만들어야 한다.
그냥 자바 기본 타입 2개 쓰고 @Id 선언하면 안된다.

JPA에서 별도의 식별자 클래스를 만드는 방법은 2가지가 있다.
두 방식의 장단점이 있으니, 원하는 방식을 선택해서 일관성 있게 하나만 사용하는 것이 좋다.

@IdClass

@IdClass를 이용한 복합키 선언은 아래와 같다.

@Entity
@IdClass(ParendId.class)
public class Parent{
    @Id
    @Column(name = "PARENT_ID1")
    private String id1;

    @Id
    @Column(name = "PARENT_ID2")
    private String id2;
}

@NoArgsConstructor
@AllArgsConstructor
public class ParentId implements Serializable{
    private String id1; // Parent.id1 에 대한 정보 제공
    private String id2; // Parent.id2 에 대한 정보 제공

    // equals, hashCode
}

@IdClass가 정보 제공용도(식별자 정보는 여기를 참고해라) 정도로 쓰이고 있다.
@IdClass로 사용된 식별자 클래스는 아래 조건을 만족해야 한다.

실제 사용은 아래와 같다.

// save
public void save(){
    Parent parent = new Parent();
    parent.setId1("id1");
    parent.setId2("id2");

    em.persist(parent);
}

// select
public void select()}{
    ParentId parentId = new ParentId("id1", "id2");
    Parent foundParent = em.find(Parent.class, parentId);
}

저장 코드에 식별자 클래스인 ParentId가 보이지 않는 이유는 em.persist를 호출하면 JPA가 내부에서 Parent.id1, Parent.id2 값을 이용해서 ParentId를 생성하고 영속성 컨텍스트의 키로 사용하기 때문이다.
이 부분만 보면 확실히 RDB에 가까운 방법이긴 하다.

하지만 조회쪽을 보면 또 그렇지도 않음.. 결국 ParentId를 생성해서 조회하고 있다.

@EmbededId

@IdClass보다 좀 더 객체지향적인 방법이다.

@Entity
public class Parent{
    @EmbeddedId
    private ParentId id;
}

@NoArgsConstructor
@AllArgsConstructor
@Embeddable
public class ParentId implements Serializable{
    @Column(name = "PARENT_ID1")
    private String id1;
    @Column(name = "PARENT_ID2")
    private String id2;

    // equals, hashCode
}

(사용하는 쪽에서 @EmbeddedId로 사용하므로 @Id를 사용할 필요없고, 복합키이므로 자동생성을 사용할 수 없다)
@IdClass 처럼 정보 제공 용도로 사용하지 않고 직접 엔티티에서 사용해버렸다.
매핑 정보도 ParentId 클래스에 들어감으로써 키를 명확히 하나의 클래스로 분리한 느낌이 난다.
확실히 좀 더 객체지향적인 방법이다.

@EmbeddedId를 사용한 식별자 클래스는 아래 조건을 만족해야 한다.

실제 사용은 아래와 같다.

// save
public void save(){
    Parent parent = Parent.builder()
        .id(new ParentId("id1", "id2"))
        .build();

    em.persist(parent);
}

// select
public void select()}{
    ParentId parentId = new ParentId("id1", "id2");
    Parent foundParent = em.find(Parent.class, parentId);
}

복합키의 equals, hashCode

위의 복합키 조건을 보면 equals와 hashCode를 필수로 구현해줘야 한다고 하는데,
이는 JPA는 영속성 컨텍스트에 엔티티를 보관할 때 엔티티의 식별자를 키로 사용하고,
식별자를 구분하기 위해 equals와 hashCode를 사용해서 동등성 비교를 하기 때문이다.

이게 단일 식별자일 경우에는 자바의 기본 타입을 사용하므로 별 문제없이 동등성이 보장되지만,
복합 식별자일 경우에는 클래스를 사용하므로 equals와 hashCode를 구현해주지 않으면 동등성을 보장할 수 없다.

ParentId id1 = new ParentId("id1", "id2");
ParentId id2 = new Parentid("id1", "id2");

assertTrue(id1.equas(id2)); // fail

같은 id 값을 가졌지만, 동등하지 않은 것이 된다.
java는 equals, hashCode를 오버라이드 하지 않으면 기본적으로 Object의 것을 사용하기 때문이다.
기본적으로 Object의 equals는 동일성 비교(==)를 하기 때문에 위의 두 키는 동등하지 않은 것이 된다.

JPA는 엔티티의 식별자를 가지고 영속성 컨텍스트를 관리하기 때문에
식별자의 동등성이 지켜지지 않으면 예상과 다른 엔티티가 조회되거나 엔티티를 찾을 수 없는 등 심각한 문제가 발생할 수 있다.
그러므로 equals와 hashCode는 필수로 구현해줘야 한다.

식별/비식별 관계에서 복합키 사용

식별 관계와 비식별 관계

식별관계

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

CREATE TABLE parent(
    parent_id integer,
    PRIMARY KEY(parent_id)
)

CREATE TABLE child(
    parent_id integer,
    child_id integer,
    PRIMARY KEY(parent_id, child_id),
    FOREIGN KEY(parent_id) REFERENCES parent(parent_id)
)

비식별 관계

부모 테이블의 기본키를 내려받아서 자식 테이블의 외래키로만 사용하는 관계이다.
요즘은 비식별 관계를 주로 사용하고, 필요할 때만 식별 관계를 사용하는 추세이다.

CREATE TABLE parent(
    parent_id integer,
    PRIMARY KEY(parent_id)
)

CREATE TABLE child(
    parent_id integer,
    child_id integer,
    PRIMARY KEY(child_id),
    FOREIGN KEY(parent_id) REFERENCES parent(parent_id)
)
  1. 필수적 비식별 관계 : FK NOT NULL(INNER JOIN 사용됨)
  2. 선택적 비식별 관계 : FK NULLALBE(OUTER JOIN 사용됨)

식별 관계 매핑

부모, 자식, 손자까지 계속 기본키를 전달하는 식별관계이다.
식별관계는 부모의 키를 포함해 복합키를 구성해야 하므로 @IdClass나 @EmbeddedId를 사용해야 한다.

CREATE TABLE parent(
    parent_id integer,
    PRIMARY KEY(parent_id)
)

CREATE TABLE child(
    parent_id integer,
    child_id integer,
    PRIMARY KEY(parent_id, child_id),
    FOREIGN KEY(parent_id) REFERENCES parent(parent_id)
)

CREATE TABLE grandchild(
    parent_id integer,
    child_id integer,
    grandchild_id integer,
    PRIMARY KEY(parent_id, child_id, grandchild_id),
    FOREIGN KEY(parent_id) REFERENCES parent(parent_id),
    FOREIGN KEY(child_id) REFERENCES child(child_id)
)

@IdClass

@Entity
public class Parent{
    @Id
    private String parentId;
}

@Entity
@IdClass(ChildId.class)
public class Child{
    // 매핑 정보 나열
    @Id
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;

    @Id
    private String childId;
}

@EqualsAndHashCode
public class ChildId implements Serializable{
    private String parent; // Child.parent 에 대한 정보 제공
    private String childId; // Child.childId 에 대한 정보 제공
}

@Entity
@IdClass(GrandChildId.class)
public class GrandChild {
    @Id
    @ManyToOne
    @JoinColumns({
            @JoinColumn(name = "parent_id"),
            @JoinColumn(name = "child_id")
    })
    private Child child;

    @Id
    private String grandChildId;
}

@EqualsAndHashCode
public class GrandChildId implements Serializable {
    private ChildId child; // GrandChild.child 에 대한 정보 제공
    private String grandChildId; // GrandChild.grandChildId 에 대한 정보 제공
}

@IdClass가 pk에 매핑되는 애들에게 정보를 바로 제공하고 있다.

@EmbeddedId

@Entity
public class Parent {
    @Id
    private String parentId;
}

@Entity
public class Child {
    @EmbeddedId
    private ChildId childId;

    @MapsId("parentId")
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

@EqualsAndHashCode
@Embeddable
public class ChildId implements Serializable {
    private String parentId; // @MapsId("paretnId") 로 매핑
    private String childId;
}

@Entity
public class GrandChild {
    @EmbeddedId
    private GrandChildId grandChildId;

    @MapsId("childId")
    @ManyToOne
    @JoinColumns({
            @JoinColumn(name = "parent_id"),
            @JoinColumn(name = "child_id")
    })
    private Child child;
}

@EqualsAndHashCode
@Embeddable
public class GrandChildId implements Serializable {
    private ChildId childId; // @MapsId("childId") 로 매핑
    private String grandChildId;
}

id들을 따로 묶고 @MapsId를 통해 연관관계와 id를 연결했다.
(@IdClass에서 id들을 class로 모으는 과정이 추가된 형태라고 봐도 될듯하다.)

@mapsid는 @id로 지정한 컬럼에 @OnetoOne이나 @OneToMany 관계를 매핑시키는 역할을 한다.
http://docs.jboss.org/hibernate/jpa/2.2/api/javax/persistence/MapsId.html
매핑의 대상이 되는 속성은 @OnetoOne이나 @OneToMany의 기본키와 타입이 같아야한다.

※ 번외로 아래와 같이 세팅 할수도 있는데, 이는 잘못된 방식이다.

@Entity
public class Parent {
    @Id
    private String parentId;
}

@Entity
public class Child {
    @EmbeddedId
    private ChildId childId;
}

@EqualsAndHashCode
@Embeddable
public class ChildId implements Serializable {
    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;

    private String childId;
}

@Entity
public class GrandChild {
    @EmbeddedId
    private GrandChildId grandChildId;
}

@EqualsAndHashCode
@Embeddable
public class GrandChildId implements Serializable {
    @ManyToOne
    @JoinColumns({
            @JoinColumn(name = "parent_id"),
            @JoinColumn(name = "child_id")
    })
    private Child child;

    private String grandChildId;
}

얼핏보면 more 객체지향스럽긴 하지만,
연관관계를 항상 id를 통해 접근하는 이상한 방식이 탄생하게 되고,
@Embeddable 에서 연관관계까지 equals, hashCode의 대상이 되는 이상한 구조가 탄생한다.

일대일 식별 관계(feat.@MapsId)

일대일 식별 관계는 자식 테이블의 기본키 값으로 부모 테이블의 기본키 값을 사용하는 조금 특별한 관계이다.
이 경우 연관관계의 주인이 될 외래키 칼럼이 없으므로 @MapsId를 사용하여 매핑해줘야 한다.

@Entity
public class Board{
    @Id
    private Long boardId;

    private String title;

    @OneToOne(mappedBy = "board")
    private BoardDetail boardDetail;
}

@Entity
public class BoardDetail{
    @Id
    private Long boardId;

    @Lob
    private String content;

    @MapsId("boardId")
    @OneToOne
    @JoinColumn(name = "board_id")
    private Board board;
}

비식별 관계 매핑

비식별 관계는 복합키를 사용하지 않기 때문에 아주 심플하다.

@Entity
public class Parent {
    @Id
    private String parentId;
}

@Entity
public class Child {
    @Id
    private String childId;

    @ManyToOne
    @JoinColumn(name = "parent_id")
    private Parent parent;
}

@Entity
public class GrandChild {
    @Id
    private String grandChildId;

    @ManyToOne
    @JoinColumn(name = "child_id")
    private Child child;
}

그래서 식별이냐 비식별이냐?

데이터베이스 설계관점에서 보면, 아래와 같은 이유로 비식별 관계를 선호한다.

그래서 정리하면!

조인 테이블

데이터베이스의 테이블의 연관관계를 설정하는 방법은 총 2가지이다.

조인 테이블의 경우 테이블을 하나 추가해야 된다는 단점이 있다.(추가 조인 필요)
그러므로 기본적으로 조인 컬럼을 사용하고, 필요할 때만 조인 테이블을 사용하도록 해야한다.

조인 테이블 == 연결 테이블 == 링크 테이블

하나의 테이블이 여러 테이블과 관계를 맺을 수 있는 구조라던가,
원래 관계가 없었는데 관계가 생겼다거나(FK를 일괄 추가하기에는 너무 부담스럽),
관계 변경(update) 때문에 메인 테이블에 락이 걸리는 걸 방지하기 위해(연결 테이블만 컨트롤 하므로써 성능향상) 사용하는 등 여러 상황에서 사용될 수 있을 것이다.

일대일 조인테이블

@Entity
public class A {
    @Id
    private String id;

    @OneToOne(optional = false)
    @JoinTable(name = "a_b",
            joinColumns = @JoinColumn(name = "a_id"),
            inverseJoinColumns = @JoinColumn(name = "b_id"))
    private B b;
}

@Entity
public class B {
    @Id
    private String id;

    @OneToOne(mappedBy = "b") // optional
    private A a;
}

생성되는 DDL은 아래와 같다.

CREATE TABLE A(
    id varchar(255) not null,
    primary key (id)
)

create table B (
    id varchar(255) not null,
    primary key (id)
)

create table a_b (
    b_id varchar(255) not null,
    a_id varchar(255) not null,
    primary key (a_id),
    FOREIGN KEY(a_id) REFERENCES A(a_id),
    FOREIGN KEY(b_id) REFERENCES B(b_id)
)

alter table a_b 
    add constraint UK_pam4mvekk45ceoippm3ffvi2t unique (b_id)

a_id가 primary key, b_id에 unique constraints가 걸리면서 1:1 관계가 형성된다.

다대일 조인테이블

B를 로 한다.

@Entity
public class B {
    @Id
    private String id;

    @ManyToOne
    @JoinTable(name = "a_b",
            joinColumns = @JoinColumn(name = "a_id"),
            inverseJoinColumns = @JoinColumn(name = "b_id"))
    private A a;
}

@Entity
public class A {
    @Id
    private String id;

    @OneToMany(mappedBy = "a") // optional
    private List<B> bList;
}
CREATE TABLE A(
    id varchar(255) not null,
    primary key (id)
)

create table B (
    id varchar(255) not null,
    primary key (id)
)

CREATE TABLE a_b(
    a_id varchar(255) not null,
    b_id varchar(255) not null,
    PRIMARY KEY(b_id),
    FOREIGN KEY(a_id) REFERENCES A(a_id),
    FOREIGN KEY(b_id) REFERENCES B(b_id)
)

다 쪽이 primary key로 생성됨으로써 다대일 관계 형성이 가능하다.

일대다 조인테이블

일대다 조인컬럼처럼 쪽에서 연관관계를 컨트롤 하고 싶을 경우 형성하는 방법이다.

@Entity
public class A {
    @Id
    private String id;

    @OneToMany
    @JoinTable(name = "a_b",
            joinColumns = @JoinColumn(name = "a_id"),
            inverseJoinColumns = @JoinColumn(name = "b_id"))
    private List<B> bList;
}

@Entity
public class B {
    @Id
    private String id;
}

일대다 조인컬럼때와 같이 단방향만을 지원한다.

아래는 생성되는 DDL이다.

CREATE TABLE A(
    id varchar(255) not null,
    primary key (id)
)

create table B (
    id varchar(255) not null,
    primary key (id)
)

CREATE TABLE a_b(
    a_id varchar(255) not null,
    b_id varchar(255) not null,
    FOREIGN KEY(a_id) REFERENCES A(a_id),
    FOREIGN KEY(b_id) REFERENCES B(b_id)
)

alter table a_b 
    add constraint UK_pam4mvekk45ceoippm3ffvi2t unique (b_id)

pk 대신 unique로 생성되는게 조금 다르다.

다대다 조인테이블

앞서 나왔으므로 작성하지 않겠다.
parent_id, child_id 에 각각 FK가 생성되고, PK로 묶이지는 않는다.

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

아까 위의 일대일 식별 관계에서 나왔었던 형태이다.

board와 board_detail을 나눠서 저장하고, 같은 PK를 쓰는 형태

자주 사용하는 형태는 아니지만 가끔 나오기도 한다.

@Entity
@SecondaryTable(name = "board_detail", // 1
    pkJoinColumns = @PrimaryKeyJoinColumn(name = "board_detail_id")) // 2
public class Board{
    @Id
    private Long boardId;

    private String title;

    @Column(table = "board_detail") // 3
    private String content;
}

@SecondaryTable을 사용해 board_detail 테이블을 추가로 매핑했다.

  1. 추가로 매핑할 테이블의 이름이다.
  2. 추가로 매핑된 테이블의 기본키 컬럼명이다.
  3. 추가로 매핑된 테이블에 저장될 속성이다.
CREATE TABLE board (
    board_id BIGINT NOT NULL,
    title VARCHAR(255),
    PRIMARY KEY (board_id)
)

CREATE TABLE board_detail (
    board_detail_id BIGINT NOT NULL,
    content VARCHAR(255),
    PRIMARY KEY (board_detail_id)
)

테이블을 2개로 나누는건 의미가 있어서 2개로 나눈건데, 이렇게 엔티티 하나로 묶으면 항상 같이 조회될것이다.
그럼 무슨 의미가 있는건지..ㅋㅋ 어디서 쓰일지 궁금하다.

beadss commented 5 years ago

세상에 사진을 찍다니.. 난 그 생각을 못해서 구글이미지 복사했네 ㅋㅋㅋㅋㅋ

joont92 commented 5 years ago

난 저걸 못찾아서 사진찍음....

beadss commented 5 years ago

엥?! 아무거나 가져다 썼는데, 똑같은 거였군;;;;