DTO에 기본 생성자가 필요한 이유
@Getter
@NoArgsConstructor
public class MemberPostDto {
@Email
@NonNull
private String email;
@Pattern(
regexp = "^(?=.*[a-zA-Z])(?=.*\\d)(?=.*[@$!%*?&])[A-Za-z\\d@$!%*?&]{8,15}$",
message = "비밀번호는 알파벳 대소문자, 숫자, 특수문자를 모두 포함해야 합니다."
)
@NonNull
private String password;
@NonNull
private String gender;
@NonNull
private String phoneNumber;
@NonNull
private String address;
@NonNull
private Role role;
기본적으로 스프링은 DTO를 JSON으로 매핑할 때,
Jackson 라이브러리의 ObjectMapper를 사용하여 Json으로 매핑하게 된다.
이때, ObjectMapper는 직렬화(Serialize, JAVA Object -> JSON), 역직렬화(Deserialize, JSON -> JAVA Object)를 수행한다.
따라서 컨트롤러에서 DTO를 @RequestBody를 통해 가져올 때, 바인딩을 ObjectMapper가 수행하는데,
ObjectMapper가 직렬화,역직렬화를 수행하여 매핑 시
DTO의 기본 생성자를 이용하여 생성하게 되므로 이러한 과정에서 Dto의 기본 생성자가 없다면, 매핑 오류가 일어난다.
또한 DTO의 필드들은 ObjectMapper의 Setter 또는 Getter로 가져오지만 reflection 기능을 통해 필드에 값을 주입하기 때문에 Setter또한 열어둘 필요가 없다.
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Entity
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class Member extends Auditable {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long memberId;
@Column(unique = true)
private String email;
private String password;
private String gender;
private String phoneNumber;
private String address;
private Role role;
private String refreshToken;
}
엔티티 국룰 @NoArgsConstructor(access = AccessLevel.PROTECTED) 과연 이걸 왜 써주는걸까
1.일단 JPA의 Entity Class에는 @NoArgsConstructor(기본생성자)가 필요하다.
JPA 공식 문서에서 Entity Class의 요구사항을보면
이렇게 대놓고 가지고 있으라고 한다.
그렇다면 왜 NoArgs를 요구하는 것일까
JPA는 Entity 객체를 인스턴스화 하고 필드에 값을 채워넣기 위해 Reflection을 사용해
런타임 시점에 동적으로 기본생성자를 통해 클래스를 인스턴스화 하여 값을 매핑하기 때문이다.
따라서 엔티티에 기본생성자가 존재하지 않는다면 JPA의 기능을 원할하게 사용하지 못한다는것.
2. access level을 Public 또는 Protected로 제한하는 이유
jpa는 다른 엔티티와 연관관계를 갖는 엔티티를 조회할 떄,
1. 연관된 엔티티 객체를 함께 가져오는 즉시로딩(EAGER) -> 연관된 엔티티 객체를 즉시 조회
/**
* EAGER
**/
@Test
void entityTest() {
Bar bar = new Bar();
em.persist(bar);
Foo foo = new Foo();
foo.setBar(bar);
em.persist(foo);
em.flush();
em.clear();
Foo findFoo = em.createQuery("select f from Foo f where f.id = :fooId", Foo.class)
.setParameter("fooId", foo.getId())
.getSingleResult();
// Output : class com.example.entitytest.test.Bar
System.out.println(findFoo.getBar().getClass());
}
2. 연관된 엔티티 객체를 실제로 조회할 때 가져오는 지연로딩(LAZY) -> 연관된 객체는 Proxy 객체로 존재하는데 이 때, 연관된 Proxy Entity 객체에 직접 접근할 때, 쿼리가 수행되며 실제값을 가져옴
/**
* LAZY
**/
@Test
void entityTest() {
Bar bar = new Bar();
em.persist(bar);
Foo foo = new Foo();
foo.setBar(bar);
em.persist(foo);
em.flush();
em.clear();
Foo findFoo = em.createQuery("select f from Foo f where f.id = :fooId", Foo.class)
.setParameter("fooId", foo.getId())
.getSingleResult();
// Output : class com.example.entitytest.test.Bar$HibernateProxy$olFRLETn
System.out.println(findFoo.getBar().getClass());
}
이 때, Proxy객체는 엔티티 클래스를 상속받아 만들어진 객체이므로 엔티티의 기본 생성자에 대한 접근제어자가 Private이면 엔티티를 상속 받을 수 없어 Proxy 객체를 생성할 수 없다.
즉 JPA가 Entity Class를 상속받는 Proxy 객체를 생성하기 위해 Private이 아닌 Public 혹은 Protected의 접근 제어자를 가져야한다.
3.그래서 access level을 Protected로 제한하는 이유
객체를 생성하고 값을 채워넣는 방식은 크게 3가지이다.
1. 기본생성자를 통해 객체 생성 -> setter를 통해 필드값 주입
2. 매개변수를 가지는 생성자를 통해 객체생성과 동시에 필드값 초기화
3.정적 팩토리 메서드(static factory method) 또는 빌더 패턴을 통해 객체 생성과 동시에 필드값 초기화
여기서 1번의 경우 setter를 열어두고 필드값을 그때마다 주입해주는 방법은 객체 값의 변경 가능성을 열어두는 것으로 어디서 변경 되었는지 추적하기 힘들고, 객체의 일관성 유지에도 좋지 않기 떄문에 지양해야한다.
결국 2,3번의 방법이 권장된다는건데
그렇다면 2,3번의 방법을 사용하게 됨으로써 이 기본생성자는 Entity Class의 프록시 객체를 만들 때 외에는 사용할 일이 없게 된다. 그렇기에 기본 생성자를 Public으로 열어둘 이유가 없고 그에따라 무분별한 객체 생성을 야기할 필요도 없다.
이러한 이유로 Protected로 제한해 접근 범위를 작게 가져가는 방법이 권장되는것이다.
Entity Class에 Lombok @NoArgsConstructor(access=PROTECTED)를 붙이는 이유 — 기록을 통한 복습 (tistory.com)
'공부거리' 카테고리의 다른 글
@Transactional(readOnly = true) (0) | 2024.01.05 |
---|---|
JPA existById 메소드 쿼리에 limit 1이 적용되지 않는 현상 (0) | 2024.01.04 |
JPA - findById() vs existById() (0) | 2024.01.02 |
java.util.Date VS java.time.LocalDate (0) | 2024.01.02 |
Null 체크 시, findById보단 existById 활용해보자. (0) | 2024.01.01 |