당니의 개발자 스토리

엔티티 클래스 개발2 본문

스프링/실전! 스프링 부트와 JPA 활용1

엔티티 클래스 개발2

clainy 2024. 4. 19. 21:26

엔티티 클래스 개발2

이어서 이제 Category를 볼게요. 엔티티를 보면,

Item 다대다 관계고, parentchild가 있습니다. 이거는 계층 구조를 이렇게 나타낼 수 있다, 맵핑할 수 있다는 걸 여러분들께 보여드리려고 제가 넣어놨습니다.

이제 가끔 JPA 질문을 올리시면서 뭐 Category 같은 계층 구조는 어떻게 해야 돼요? 물어보시거든요. 그러니까 다른 거를 맵핑하는 거는 해봤는데 자기 자신을 셀프로 맵핑하는 거는 안 해보셨을 것 같아요.

사실 하는 방식은 관계형 DB랑 똑같거든요.

 

자 그리고 db를 보시면,

다대다 맵핑이기 때문에 CATEGORY_ITEM이 나오고 이 구조를 딱 만들 거에요.


자 해보겠습니다.

Category 클래스를 만들어주겠습니다.

여기까지 했습니다. 이젠 익숙하죠.

자 제가 이 Category랑 이 Item List랑 다대다 관계를 만드는 걸 예제 샘플상 보여드리겠습니다.

얘네 둘은 다대다니까 @ManyToMany 관계란 말이에요.

지금 보시면 Category도 List로 Item을 가지고 Item도 List로 Category를 가져요. 양방향 참조죠.

그럼 일단 Item 클래스에다가 Category를 만들어야 되겠네요.

단방향으로 해도 되는데 양방향으로 만들어서 최대한 복잡한 예제를 보여드리겠습니다.

일단 여기까지 해놓으면 되는데,

다대다 관계도 연관관계 주인지정해야하는데, 근데 이건 한번 해보시면 알 거예요.

그 다음에 이제 @JoinTable이 필요합니다. 왜냐면 아까 중간 테이블 보셨죠? CATEGORY_ITEM 이라고 하는 그 중간 테이블을 맵핑을 해줘야 됩니다.

그니까 객체(엔티티)는 다 컬렉션, 컬렉션이 있어서 다대다 관계가 가능한데, 관계형 DB는 컬렉션 관계를 양쪽에 가질 수 있는 게 아니기 때문에 일대다, 다대일로 풀어내는 중간 테이블이 있어야 가능합니다.

그래서 보시면,

CATEGORY_ITEM 이라는 중간 테이블의 CATEGORY_ID CATEGORY 테이블 걸고, ITEM_IDITEM 테이블 걸고 이렇게 되는 아주 정형화된 모습으로 맵핑을 할 수가 있습니다.

이거를 실전, 실무에서 왜 쓰지 말라고 하냐면,

딱 이 그림밖에 안 돼요. 여기서 더 필드를 추가하는 게 불가능하거든요. 원래 실무에서는 지금 이것처럼 막 단순하게 맵핑하고 끝나는 경우가 없잖아요. 중간에 등록한 날짜라도 넣어줘야 되고, 뭐라도 넣어줘야 되는데 이렇게밖에 안되면 못 하는 거죠.

 

자 그래도 뭐 이제 JPA에서 이런게 된다. 정도만 보여드리려고 하는 거구요.

그 다음에 이 중간 테이블 안에 @JoinColumn을 넣어야 됩니다. 이건 아까 보신 그 중간 테이블(category_item)에 있는 category_id예요. 외래키를 맵핑해주는 거죠.

즉, 여기 있는 Foreign Key를 맵핑해준 겁니다.

그 다음에 inverseJoinColumns 라고 해줘야 되는데요. 이거는 이 category_item 테이블에서 Item 쪽으로 들어가는 Foreign Key를 맵핑을 하는 겁니다. 반대편 사이드니까 inverser가 붙는 거예요.

여기까지 해주면 중간 테이블이 완성됩니다.

여기까지 맵핑이 됐는데,

이제 반대편 사이드(Item)에는 @ManyToMany 하고 mappedBy 로 해서, "items" 적어주면 끝나겠죠.

이렇게 하면 됩니다.

 

그 다음에 이제 JPA를 쓰긴 하는데 '아 카테고리 구조 같은거는 어떻게 하지?' 카테고리 구조라는 게 계층 구조로 막 쭉 내려가잖아요. 그리고 이제 위로도 볼 수 있어야 되고, 나의 부모는 누구지? 내 자식은 누구지? 하실 텐데,

여러분 그냥 잘 생각해 보시면, 이렇게 하면 됩니다. 자 parent, 내 부모가 내 타입이니까 타입을 Category 라고 하고, 내 부모니까 @ManyToOne 이겠죠. 그 다음에 @JoinColumn으로 외래 키를 맵핑해줘야 합니다. 원래 외래 키는 다른 테이블을 참조할 때 쓰는데, parent는 자기 자신을 참조하죠. Order가 Member를 참조할 때, member_id라고 했던 것처럼 parent도 자기 자신을 참조하기 때문에 parent_id라고 지은 겁니다. DB에서는 보통 Join으로 해결하니까 parent_id를 가지고 있으면 되겠죠.

그런 다음에 반대로 내 자식은 어떻게 될까요?

내 자식은 여러 개를 가질 수 있단 말이에요. 타입은 당연히 Category죠. 그리고 @OneToMany가 되겠죠.

자 그리고 mappedBy가 재밌는데요. parent를 넣으면 됩니다. 지금 보면 parent가 연관관계의 주인이 되고, child가 거울이 되는 거죠.

즉, 같은 엔티티(Category)에 대해서 셀프로 양방향 연관관계를 건 거라고 보시면 됩니다.

 

그러니까 딱 이렇게 이해하면 돼요. 다른 엔티티가 아니라, 그냥 이름만 내꺼지 사실 그냥 다른 엔티티를 연관관계 맵핑하는거랑 똑같은 겁니다. parent랑 child라는 테이블이 있는 거라고 이해하시면 편합니다.

 

그래서 정리하자면,

Foreign Key가까운 parent연관관계의 주인이 된 거고, 그 parent와 연관관계를 가진 child연관관계의 거울이 된 겁니다.


자 여기까지 하면 기본적인 맵핑이 다 완성이 되었습니다.

자 그럼 한번 실행을 해볼건데, 저희가 JPA니까 테이블이 생성되는 걸로 보면 되겠죠.

실행을 해봅시다.

우리가 원하는 건 테이블이

딱 이렇게 생성되는 거죠. 이 모양으로 create table이 쫙 나오는지 볼게요.

커넥션 에러가 났네요. DB를 안 켰습니다.

h2를 실행하고 들어가봅시다.

이 상태에서 다시 돌려볼게요.

자 우리가 실행을 하면,

이렇게 쭉쭉쭉 올라오죠. 보기 어려우니까

이걸 눌러서 h2 refesh를 해보시면,

테이블들이 생성되어 있습니다.

이제 저희가 기대했던 대로 테이블들이 다 생성이 되었는지 한번 확인해 보겠습니다.

 

먼저, MEMBER부터 볼게요. MEMBER를 누르고 실행하면,

지금 데이터는 없고, 컬럼들이 다 있는 걸 확인하실 수 있죠.

 

그 다음 ORDERS 볼까요.

역시 그대로 생성이 됐고 나머지는 안 보셔도 될 것 같은데, 일단 대략적으로 봐볼게요.

ORDER_ITEM도 그대로입니다.

DELIVERY도 마찬가지고요.

ITEM은 싱글 테이블 전략으로 DTYPE 포함해서 쭉 만들어졌죠.

그 다음에

CATEGORY랑

CATEGORY_ITEM도 만들어졌죠.

이렇게 해서 저희가 기대한 대로 테이블이 만들어진 거를 확인했습니다.

 

자 그리고 재미있는 게

JPA는 테이블을 만들 때, alter 해가지고 Foreign Key 컬럼을 다 잡아줍니다.

근데 이거는 약간 새는 이야기일 수 있는데 Foreign Key를 꼭 걸어야 돼요, 말아야 돼요? 는 여러분의 시스템마다 다른 것 같아요.

여러분이 실시간 트래픽이 엄청 중요하고 정합성 보다는 서비스가 잘 되는 게, 좀 더 유연한 게 중요하면 사실 Foreign Key를 빼고 인덱스만 잘 잡아 주시면 됩니다. 근데 돈과 관련된, 너무 중요한 거고 데이터가 항상 맞아야 된다라고 하면 Foreign Key를 거는 거에 대해서 진지하게 고민해볼 필요가 있죠.


자 이렇게 해서 테이블 생성된 것까지 확인을 했구요.

지금부터 한번 점검을 해보면서 중요한 내용들을 공유를 해드리겠습니다.

이론적으로 Getter랑 Setter를 엔티티에 다 제공하지 않고 꼭 필요한 별도의 메서드를 제공하는게 가장 이상적이에요. 그런데 실무에서는 조회할 일이 진짜 많거든요. 엔티티에서 데이터들을 조회해서 막 뿌리고 이럴 일이 너무 많기 때문에 기본적으로 Getter는 열어두는게 편해요.

그리고 이제 Getter는 사실 아무리 호출을 한다 하더라도 그것만으로 어떤 일이 발생하지는 않잖아요. 단순하게 데이터가 조회가 되는 거지. 그렇다고 물론 뭐 이상한 걸 조회해서 막 값을 변경하면 안 되고요.

 

그런데 이제 Setter는 문제가 좀 달라요. Setter를 호출하면 데이터가 변해버린단 말이에요. 그래서 Setter를 엄청 많이 열어두게 되면, 가까운 미래에 도대체 이 엔티티가 언제, 어떻게 수정이 되는지 파악이 안 돼요. 여기저기 여러 서비스 메서드에서 Setter를 호출해서 엔티티를 바꿔버리고 있으면 도대체 얘가 어느 타이밍에 언제 바뀌는지 모릅니다.

이게 언제 어려움이 오냐면 나중에 변경을 해야 될 때, Setter를 다 뒤져가면서 이 엔티티가 어디서 바뀐지 다 찾아야 된단 말이에요. 그게 정말 힘들거든요.

그래서 엔티티를 변경할 때 Setter 대신에 변경 지점이 명확하게, 변경할 때는 별도의 변경용 비즈니스 메서드를 제공하는 게 좋습니다. 그래야 유지 보수성이 확 올라가요. Setter를 막 열어두고 아무데서나 set, set 해서 하고 있으면 진짜 애플리케이션이 조금만 복잡해져도 나중에 변경할 때 1시간이면 고칠 거를 막 3, 4시간을 막 뒤지고 있어야 되거든요.

아무튼 그래서 저 같은 경우에는 실용적인 관점에서 Getter는 다 열고, 제가 실무할 때는 이제 Setter는 다 닫아요. 근데 예제를 설명해 드릴 때는 좀 편하게 하기 위해서 제가 Setter를 다 열었습니다. 이거를 강조해서 말씀드립니다.

 

그 다음에 봅시다.

이 부분, db pk 컬럼명을 그냥 id로 해도 돼요.

근데 왜 테이블명_id로 했냐면 여러분 객체(엔티티)는

변수명 : 타입 이렇게 해서, id 라고만 해도 Member 라는 타입이 있기 때문에 어디 소속인지 명확하거든요.

근데 테이블은 단순하게 id라고 해버리면, 테이블은 타입이 없기 때문에 실무 관점에서 찾기가 쉽지가 않아요. 그리고 Join 할 때도 조금 불편해요. Join 할 때 Member 테이블에 있는 id라는 걸 구분하기 위해서는 명확하게 member_id 라고 해서 Foreign Key랑 이름을 맞춰버리는 거죠.

테이블이라는 게 객체(엔티티)는 Member 같은 타입이 명확하게 있는데 테이블이라는 건 딱 타입이 없거든요. 그래서 이런 이유도 있고 해서 dba 분들도 관례상 "테이블명_id" 같은 스타일 많이 사용합니다. 그래서 거기에 이제 맞추는 거죠.

그 다음에

실무에서는 @ManyToMany 사용하지 말라고 말씀드렸습니다. 아 물론 쓸 수도 있긴 한데 내가 간단한 나만의 애플리케이션 만들 때는 쓰시면 되는데 실무에서는 이걸 쓰면,

이 중간 테이블에 값을 더 넣을 수가 없어요.

지금 보시면, 이렇게밖에 못 써요. 원래 실무에서는 이 중간 테이블에 등록일 수정이라도 들어가야 되거든요. 그런거를 다 못 넣고 운영하기도 되게 어렵습니다.

 

이번에는 주소 값 타입을 한번 보충 설명 드리겠습니다.

우선 값 타입은

지금 제가 Getter만 만들었죠. 기본적으로 값이라는 것 자체는 immutable(변경할 수 없는) 하게 설계되어야 되니까 변경이 되면 안돼요.

그래서 값 타입은 좋은 설계가 Getter는 제공하고, Setter는 아예 제공을 안 하게 되면, 생성할 때만 딱 값이 세팅이 되고나면 완전히 변경이 불가능해지는 거죠. 그래서 지금 cmd + N 해서 Constructor를 만들었습니다.

이렇게 개발을 해놓으면, Setter가 없으니까 값을 변경하려면 똑같은 객체를 만들어서 값을 무조건 새로 등록해야 돼요.

그런데 지금 김영한t 화면을 보면, 빨간불이 들어와있죠. 수강자 화면은 정상인데 왜 그럴까요?

이거에 대한 답변은 위와 같습니다.

일단 왜 빨간줄이 뜨냐하면, JPA 기본 스펙이 값 타입 같은 것들을 JPA가 생성할 때 reflection이나, proxy 같은 기술을 써야 될 때가 되게 많거든요. 그런에 이렇게 기본 생성자가 없으면, 그러한 기술을 못 쓰기 때문에 기본 생성자를 만들어 줘야 됩니다.

'기본 생성자는 원래 자동으로 만들어지지 않나요?' 하실텐데, 이렇게 매개변수가 있는 초기화 생성자를 쓰면, 우리가 기본 생성자를 따로 만들어주지 않는 한 자동으로 기본 생성자를 만들어주지 않습니다.

그래서 생성자에서 Select None을 눌러서,

기본 생성자를 만들어주면 됩니다. 지금 보시면 얘가 public으로 되어있는데, 이러면 사람들이 많이 호출할 수 있잖아요. 그래서 JPA 스펙에서는 protected 까지 허용을 해줍니다.

protected까지 넣는 걸 허용해줘요. Address를 직접 막 상속할 일은 없잖아요. 그러니까 JPA에서 상속을 함부로 할 수 있는게 아니기 때문에, 기본 생성자를 protected로 해놓으면 아무래도 딱 보고 '아 이건 JPA 스펙상 그냥 만든거구나. 손 대지 말자' 할 거고, 아니면 주석 좀 달아 두셔도 되고, 'Address()를 함부로 new로 생성하면 안되겠네' 라는 게 되기 때문에 이렇게 해두시면 됩니다.


접근제어자

protected는 상속 클래스 접근이 가능하다.


상속에서의 생성자 관계

자식 클래스가 객체를 생성할 때 자동으로 부모의 기본 생성자를 호출하므로, protected로 하면 당연히 호출이 되지만, JPA에서는 값 타입을 상속하는 일은 거의 일어나지 않기 때문에 상속 때문에 protected로 선언한 게 아니라, JPA 스펙상 그냥 만든거기 때문에 건들지 말라는 의미로 해석된다는 뜻이다.


정리하면,

값 타입은 변경 불가능하게 설계해야 되고, Setter는 제거하고, 애초에 만들 때, 즉 생성자에서 값을 모두 초기화해서 변경 불가능한 클래스를 만드는 게 좋습니다. 그리고 JPA 스펙상 EntityEmbedded 타입(@Embeddable)은 Java 기본 생성자를 publicprotected로 생성을 해야 돼요. 이건 JPA 스펙에 적혀있는 겁니다. Hibernate를 쓰면 이런 제약을 좀 더 좁힐 수 있는데 어쨌든 스펙을 맞추는게 좋으니까 protected로 하는 게 좋습니다. 그나마 사람들이 막 기본 생성자를 new 할 수 있는건 아니니까.

그러면서 얘는 지금 public으로 열려있으니까 얘를 써야 되겠네. 라고 인지하겠죠.

그래서 JPA가 이렇게 기본 생성자에 제약을 두는 이유는 JPA 구현 라이브러리가 객체를 생성하거나 Reflection이나 proxy 같은 기술을 사용할 수 있도록 허용해야 되기 때문에 스펙에 되어 있는 겁니다.

 

자 이렇게 말씀드렸고 나머지는 그 다음 시간에 한번 보면 되겠네요.

그런데 이런 질문들을 하시는 분들이 있어요.

'여기 생성된 거를 그대로 써도 되나요?' 이거 그대로 쓰시면 안됩니다. 이 DDL(테이블 생성 스크립트) 스크립트를 참고해서 쓰시는 건 괜찮은데 한번 다 보고 검증하시고 다듬으셔야 돼요.

저는 주로 실무에서 사용하는 방식이 스크립트를 가지고 뽑은 다음에 쭉 한번 보고 디테일하게 수정할 것들을 보면 있거든요. 그런 걸 수정하고 정제해서 주로 사용하는 편입니다.

 

그러면 이번 시간은 여기까지 하고요. 다음 시간에는 이렇게 엔티티를 설계할 때 주의점들에 대해서 설명드리고, 그 주의점을 바탕으로 코드들을 변경을 한 다음에 완성하는 시간을 가지겠습니다.

'스프링 > 실전! 스프링 부트와 JPA 활용1' 카테고리의 다른 글

구현 요구사항  (0) 2024.04.24
엔티티 설계시 주의점  (0) 2024.04.24
엔티티 클래스 개발1  (0) 2024.04.17
도메인 모델과 테이블 설계  (0) 2024.04.15
요구사항 분석  (0) 2024.04.13