--- tags: github, jpa,embedded --- study2. 값객체와 @Embdedd 를 이용한 city, gu, bunji 리팩토링 === ## 목표 9장 에서 `값객체` 와 `@Embdedd` 에 대한 내용이 나오는데, 간단 하지만 매우 강력하다. 편리한 학습을 위해 먼저 학습 해 보자. #### 사전 준비사항 1. 실습 할 환경 > [study1. 학습용 Spring Boot + JPA 환경 만들기](/BRj-hewDTb-vs1Faqexp6Q) 참고 > 전체 코드와 TEST는 [github](https://github.com/bonjugi/jpa-study-embedded) 에서 볼수 있다. ## city, gu, bunji 문제점 위의 사전 준비사항을 마쳤다면 현재 Member 엔티티는 다음과 같을 것이다. ```java= @Entity public class Member { @Id @GeneratedValue private Long id; private String name; private String city; private String gu; private String bunji; public Member(String name) { this.name = name; } } ``` 생성되는 DDL 은 다음과 같다. ``` create table member ( id bigint not null auto_increment, name varchar(255), city varchar(255), gu varchar(255), bunji varchar(255), primary key (id) ) engine=InnoDB ``` 여기에서 멤버의 주소지가 서울 인지 체크하는 서비스 로직을 추가 한다고 가정 해 보자. 다음과 같은 서비스 레이어가 생길 것 이다. (가독성을 위해 코드들은 약간씩 간소화 하겠다) ```java= public class MemberService { private final MemberRepository repository; public boolean inSeoul(Long id){ Member member = repository.findById(id); return member.inSeoul(); } } ``` 도메인 로직인 `멤버가 서울인지?` 를 inSeoul() 메소드로 체크 하려면 다음과 같을것 이다. ```java= public class Member { ... public boolean inSeoul(){ if (bunji.isEmpty() || city.isEmpty() || gu.zipCode()) { throw new IllegalStateException("주소 정보가 부족합니다."); } return city.equals("Seoul"); } } ``` Member 엔티티가 주소와 관련된 값들을 비교하는 로직에 불편함을 느껴 지는가? > 혹시 위와 같은 로직을 서비스에 만들고자 하진 않았는지? > 만약 그러하다면 여러곳에서 중복코드가 발생하고있진 않은지? 주소 에 대한 역할은 Address 라는 객체 한테 떠넘기면 더욱 객체지향적일것 같다. 예를들면 이렇게 말이다. ```java= public class Member { ... public boolean inSeoul(){ return address.inSeoul(); } } ``` > 물론 현재는 address 라는 멤버변수가 없어 컴파일 에러가 날 것이다. > 아래의 리팩토링을 통해 address 값객체를 만들어 보자. ## 값 객체로 리팩토링 3개의 값 들을 Address 라는 객체 로 표현하면 좋을것 같다. 기존의 city, gu, bunji 를 Address 객체로 바꿔보자. ```java= public class Member { @Id @GeneratedValue private Long id; private String name; private Address address; /* new! */ public Member(String name) { this.name = name; } } ``` ```java= public class Address { private String city; private String gu; private String bunji; public Address(String city, String gu, String bunji) { if (bunji.isEmpty() || city.isEmpty() || gu.isEmpty()){ throw new IllegalStateException("주소 정보가 부족합니다."); } this.city = city; this.gu = gu; this.bunji = bunji; } public boolean inSeoul(){ return city.equals("Seoul"); } } ``` 도대체 어떻게 좋다는 건지 감이 오지 않는가? 객체지향 관점에서 아래와 같은 장점이 생긴다. #### 1. 표현이 좋아진다. 각각으로써는 의미가 없던 city, gu, bunji 가 모여 주소를 표현하게 된다. 세 값들에 연관성을 설명하지 않더라도 각각이 Address 의 구성요소임을 알수 있다. #### 2. 응집력이 높아진다. Address 안에는 이미 주소관련된 필드들을 모두 가지고 있고 무엇이든 연산 할 수 있다. Member 와 Address 가 각각 적절한 책임을 나눠 갖게 되었다. 지번주소나 빌딩번호를 활용한 도메인 로직이 추가되어도 Address로 해결이 가능하다. #### 3. POJO 로써, 재사용 가능하다. Member 엔티티 뿐 아니라, Company 같은 다른 엔티티에서도 재활용이 가능하다. ```java= public class Company { ... private Address address; } ``` Company 도 기존에 만들어진 inSeoul() 메소드를 쓸수있는것은 물론, 추가적인 주소관련 메소드도 추가하여 활용할 수 있다. 값객체의 장점은 여기까지만 설명 하겠다. > 불변객체로 설계할수있어 엔티티와 큰 차이가 있는데 좀 길어질것 같다. 여기서 이런 의문이 들수 있다. Address 객체가 DB 상의 3개의 필드를 대체 한다는것인가? 그렇다면 필드 타입은 무엇인가? 대체당한 3개의 필드들은 사라지는가? 예를들면 DDL 이 이렇게 되진 않을까? ``` create table member ( id bigint not null auto_increment, /* 세개의 필드는 사라지나? city varchar(255), gu varchar(255), bunji varchar(255), */ address ??? (255), /* 새로 생기는 address 필드의 타입은 뭘까? */ name varchar(255), primary key (id) ) engine=InnoDB ``` 이것은 `@Embedded` (or `@Embeddable`) 로 관계매핑을 가능하게 해준다. 자세히 알아보자. ## @Embedded (or @Embeddable) 엔티티에 `값객체`를 매핑하기 위해서는 @Embeddable 어노테이션만 추가 하면 된다. ```java= ... @Embeddable public class Address { private String city; private String gu; private String bunji; ... } ``` Address를 참조하는 모든 엔티티는, Address 의 필드들을 @Column 처럼 사용 할 수 있게 된다. 다시 프로젝트를 런 해서 생성되는 테이블을 확인 해 보자. ``` create table member ( id bigint not null auto_increment, city varchar(255), gu varchar(255), bunji varchar(255), name varchar(255), primary key (id) ) engine=InnoDB ``` > 여전히 city, gu, bunji 필드들이 살아있다. > DB 매핑은 보존하면서 세 필드들을 객체화 시켰다. ### @Embedded 는 아직 설명하지 않았는데? 큰 차이는 없다. 단지 @Embeddable 과 달리, @Embedded 은 엔티티에서 작성 할수 있게 해준다. ```java= public class Member { @Id @GeneratedValue private Long id; private String name; @Embedded private Address address; public Member(String name) { this.name = name; } } ``` @Embeddable 또는 @Embedded 은, 둘중 하나만 쓰고 하나는 생략해도 괜찮다. 어디를 생략할지는 장단점이 있어 팀마다 다르게 운용될수 있다.